diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs index cae7781db7..5effc1f215 100644 --- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -23,6 +23,11 @@ namespace osu.Game.Tests.Visual.Beatmaps { public class TestSceneBeatmapCard : OsuTestScene { + /// + /// All cards on this scene use a common online ID to ensure that map download, preview tracks, etc. can be tested manually with online sources. + /// + private const int online_id = 163112; + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; private APIBeatmapSet[] testCases; @@ -38,7 +43,6 @@ private void load() var normal = CreateAPIBeatmapSet(Ruleset.Value); normal.HasVideo = true; normal.HasStoryboard = true; - normal.OnlineID = 241526; var withStatistics = CreateAPIBeatmapSet(Ruleset.Value); withStatistics.Title = withStatistics.TitleUnicode = "play favourite stats"; @@ -106,6 +110,9 @@ private void load() explicitFeaturedMap, longName }; + + foreach (var testCase in testCases) + testCase.OnlineID = online_id; } private APIBeatmapSet getUndownloadableBeatmapSet() => new APIBeatmapSet @@ -191,9 +198,9 @@ public void SetUpSteps() private void ensureSoleilyRemoved() { AddUntilStep("ensure manager loaded", () => beatmaps != null); - AddStep("remove soleily", () => + AddStep("remove map", () => { - var beatmap = beatmaps.QueryBeatmapSet(b => b.OnlineID == 241526); + var beatmap = beatmaps.QueryBeatmapSet(b => b.OnlineID == online_id); if (beatmap != null) beatmaps.Delete(beatmap); }); diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs new file mode 100644 index 0000000000..1b4542d946 --- /dev/null +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardThumbnail.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Beatmaps.Drawables.Cards.Buttons; +using osu.Game.Overlays; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Beatmaps +{ + public class TestSceneBeatmapCardThumbnail : OsuManualInputManagerTestScene + { + private PlayButton playButton => this.ChildrenOfType().Single(); + + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + [Test] + public void TestThumbnailPreview() + { + BeatmapCardThumbnail thumbnail = null; + + AddStep("create thumbnail", () => + { + var beatmapSet = CreateAPIBeatmapSet(Ruleset.Value); + beatmapSet.OnlineID = 241526; // ID hardcoded to ensure that the preview track exists online. + + Child = thumbnail = new BeatmapCardThumbnail(beatmapSet) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200) + }; + }); + AddStep("enable dim", () => thumbnail.Dimmed.Value = true); + AddUntilStep("button visible", () => playButton.IsPresent); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(playButton); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for start", () => playButton.Playing.Value && playButton.Enabled.Value); + iconIs(FontAwesome.Solid.Stop); + + AddStep("click again", () => + { + InputManager.MoveMouseTo(playButton); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for stop", () => !playButton.Playing.Value && playButton.Enabled.Value); + iconIs(FontAwesome.Solid.Play); + + AddStep("click again", () => + { + InputManager.MoveMouseTo(playButton); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for start", () => playButton.Playing.Value && playButton.Enabled.Value); + iconIs(FontAwesome.Solid.Stop); + + AddStep("disable dim", () => thumbnail.Dimmed.Value = false); + AddWaitStep("wait some", 3); + AddAssert("button still visible", () => playButton.IsPresent); + + AddUntilStep("wait for track to end", () => !playButton.Playing.Value); + AddUntilStep("button hidden", () => !playButton.IsPresent); + } + + private void iconIs(IconUsage usage) => AddUntilStep("icon is correct", () => playButton.ChildrenOfType().Any(icon => icon.Icon.Equals(usage))); + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs index e3af253db9..4ffad0f065 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -23,7 +23,6 @@ using osuTK; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Resources.Localisation.Web; -using osuTK.Graphics; using DownloadButton = osu.Game.Beatmaps.Drawables.Cards.Buttons.DownloadButton; namespace osu.Game.Beatmaps.Drawables.Cards @@ -42,7 +41,7 @@ public class BeatmapCard : OsuClickableContainer private readonly BeatmapDownloadTracker downloadTracker; - private UpdateableOnlineBeatmapSetCover leftCover; + private BeatmapCardThumbnail thumbnail; private FillFlowContainer leftIconArea; private Container rightAreaBackground; @@ -98,24 +97,17 @@ private void load() Colour = Colour4.White }, }, - new Container + thumbnail = new BeatmapCardThumbnail(beatmapSet) { Name = @"Left (icon) area", Size = new Vector2(height), - Children = new Drawable[] + Padding = new MarginPadding { Right = corner_radius }, + Child = leftIconArea = new FillFlowContainer { - leftCover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) - { - RelativeSizeAxes = Axes.Both, - OnlineInfo = beatmapSet - }, - leftIconArea = new FillFlowContainer - { - Margin = new MarginPadding(5), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(1) - } + Margin = new MarginPadding(5), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(1) } }, new Container @@ -319,10 +311,10 @@ private void load() }; if (beatmapSet.HasVideo) - leftIconArea.Add(new IconPill(FontAwesome.Solid.Film)); + leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); if (beatmapSet.HasStoryboard) - leftIconArea.Add(new IconPill(FontAwesome.Solid.Image)); + leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) }); if (beatmapSet.HasExplicitContent) { @@ -395,10 +387,11 @@ private void updateState() if (IsHovered) targetWidth = targetWidth - icon_area_width + corner_radius; + thumbnail.Dimmed.Value = IsHovered; + mainContent.ResizeWidthTo(targetWidth, TRANSITION_DURATION, Easing.OutQuint); mainContentBackground.Dimmed.Value = IsHovered; - leftCover.FadeColour(IsHovered ? OsuColour.Gray(0.2f) : Color4.White, TRANSITION_DURATION, Easing.OutQuint); statisticsContainer.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); rightAreaBackground.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, TRANSITION_DURATION, Easing.OutQuint); diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs new file mode 100644 index 0000000000..f11a5916e1 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardThumbnail.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps.Drawables.Cards.Buttons; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class BeatmapCardThumbnail : Container + { + public BindableBool Dimmed { get; } = new BindableBool(); + + public new MarginPadding Padding + { + get => foreground.Padding; + set => foreground.Padding = value; + } + + private readonly UpdateableOnlineBeatmapSetCover cover; + private readonly Container foreground; + private readonly PlayButton playButton; + private readonly SmoothCircularProgress progress; + private readonly Container content; + + protected override Container Content => content; + + public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo) + { + InternalChildren = new Drawable[] + { + cover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) + { + RelativeSizeAxes = Axes.Both, + OnlineInfo = beatmapSetInfo + }, + foreground = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + playButton = new PlayButton(beatmapSetInfo) + { + RelativeSizeAxes = Axes.Both + }, + progress = new SmoothCircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(50), + InnerRadius = 0.2f + }, + content = new Container + { + RelativeSizeAxes = Axes.Both + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + progress.Colour = colourProvider.Highlight1; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Dimmed.BindValueChanged(_ => updateState()); + + playButton.Playing.BindValueChanged(_ => updateState(), true); + ((IBindable)progress.Current).BindTo(playButton.Progress); + + FinishTransforms(true); + } + + private void updateState() + { + bool shouldDim = Dimmed.Value || playButton.Playing.Value; + + playButton.FadeTo(shouldDim ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + cover.FadeColour(shouldDim ? OsuColour.Gray(0.2f) : Color4.White, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs new file mode 100644 index 0000000000..4574d37da0 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/PlayButton.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Audio; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Beatmaps.Drawables.Cards.Buttons +{ + public class PlayButton : OsuHoverContainer + { + public IBindable Progress => progress; + private readonly BindableDouble progress = new BindableDouble(); + + public BindableBool Playing { get; } = new BindableBool(); + + private readonly IBeatmapSetInfo beatmapSetInfo; + + protected override IEnumerable EffectTargets => icon.Yield(); + + private readonly SpriteIcon icon; + private readonly LoadingSpinner loadingSpinner; + + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } = null!; + + private PreviewTrack? previewTrack; + + public PlayButton(IBeatmapSetInfo beatmapSetInfo) + { + this.beatmapSetInfo = beatmapSetInfo; + + Anchor = Origin = Anchor.Centre; + + Children = new Drawable[] + { + icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.Play, + Size = new Vector2(14) + }, + loadingSpinner = new LoadingSpinner + { + Size = new Vector2(14) + } + }; + + Action = () => Playing.Toggle(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + HoverColour = colours.Yellow; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playing.BindValueChanged(updateState, true); + } + + protected override void Update() + { + base.Update(); + + if (Playing.Value && previewTrack != null && previewTrack.TrackLoaded) + progress.Value = previewTrack.CurrentTime / previewTrack.Length; + else + progress.Value = 0; + } + + private void updateState(ValueChangedEvent playing) + { + icon.Icon = playing.NewValue ? FontAwesome.Solid.Stop : FontAwesome.Solid.Play; + + if (!playing.NewValue) + { + stopPreview(); + return; + } + + if (previewTrack == null) + { + toggleLoading(true); + LoadComponentAsync(previewTrack = previewTrackManager.Get(beatmapSetInfo), onPreviewLoaded); + } + else + tryStartPreview(); + } + + private void stopPreview() + { + toggleLoading(false); + Playing.Value = false; + previewTrack?.Stop(); + } + + private void onPreviewLoaded(PreviewTrack loadedPreview) + { + // another async load might have completed before this one. + // if so, do not make any changes. + if (loadedPreview != previewTrack) + return; + + AddInternal(loadedPreview); + toggleLoading(false); + + loadedPreview.Stopped += () => Schedule(() => Playing.Value = false); + + if (Playing.Value) + tryStartPreview(); + } + + private void tryStartPreview() + { + if (previewTrack?.Start() == false) + Playing.Value = false; + } + + private void toggleLoading(bool loading) + { + Enabled.Value = !loading; + icon.FadeTo(loading ? 0 : 1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + loadingSpinner.State.Value = loading ? Visibility.Visible : Visibility.Hidden; + } + } +}