diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs new file mode 100644 index 0000000000..d1b21547dc --- /dev/null +++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCard.cs @@ -0,0 +1,181 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Beatmaps +{ + public class TestSceneBeatmapCard : OsuTestScene + { + private APIBeatmapSet[] testCases; + + #region Test case generation + + [BackgroundDependencyLoader] + private void load() + { + var normal = CreateAPIBeatmapSet(Ruleset.Value); + normal.HasVideo = true; + normal.HasStoryboard = true; + + var undownloadable = getUndownloadableBeatmapSet(); + + var someDifficulties = getManyDifficultiesBeatmapSet(11); + someDifficulties.Title = someDifficulties.TitleUnicode = "some difficulties"; + someDifficulties.Status = BeatmapSetOnlineStatus.Qualified; + + var manyDifficulties = getManyDifficultiesBeatmapSet(100); + manyDifficulties.Status = BeatmapSetOnlineStatus.Pending; + + var explicitMap = CreateAPIBeatmapSet(Ruleset.Value); + explicitMap.HasExplicitContent = true; + + var featuredMap = CreateAPIBeatmapSet(Ruleset.Value); + featuredMap.TrackId = 1; + + var explicitFeaturedMap = CreateAPIBeatmapSet(Ruleset.Value); + explicitFeaturedMap.HasExplicitContent = true; + explicitFeaturedMap.TrackId = 2; + + var longName = CreateAPIBeatmapSet(Ruleset.Value); + longName.Title = longName.TitleUnicode = "this track has an incredibly and implausibly long title"; + longName.Artist = longName.ArtistUnicode = "and this artist! who would have thunk it. it's really such a long name."; + longName.HasExplicitContent = true; + longName.TrackId = 444; + + testCases = new[] + { + normal, + undownloadable, + someDifficulties, + manyDifficulties, + explicitMap, + featuredMap, + explicitFeaturedMap, + longName + }; + } + + private APIBeatmapSet getUndownloadableBeatmapSet() => new APIBeatmapSet + { + OnlineID = 123, + Title = "undownloadable beatmap", + Artist = "test", + Source = "more tests", + Author = new User + { + Username = "BanchoBot", + Id = 3, + }, + Availability = new BeatmapSetOnlineAvailability + { + DownloadDisabled = true, + }, + Preview = @"https://b.ppy.sh/preview/12345.mp3", + PlayCount = 123, + FavouriteCount = 456, + BPM = 111, + HasVideo = true, + HasStoryboard = true, + Covers = new BeatmapSetOnlineCovers(), + Beatmaps = new List + { + new APIBeatmap + { + RulesetID = Ruleset.Value.OnlineID, + DifficultyName = "Test", + StarRating = 6.42, + } + } + }; + + private static APIBeatmapSet getManyDifficultiesBeatmapSet(int count) + { + var beatmaps = new List(); + + for (int i = 0; i < count; i++) + { + beatmaps.Add(new APIBeatmap + { + RulesetID = i % 4, + StarRating = 2 + i % 4 * 2, + }); + } + + return new APIBeatmapSet + { + OnlineID = 1, + Title = "many difficulties beatmap", + Artist = "test", + Author = new User + { + Username = "BanchoBot", + Id = 3, + }, + HasVideo = true, + HasStoryboard = true, + Covers = new BeatmapSetOnlineCovers(), + Beatmaps = beatmaps, + }; + } + + #endregion + + private Drawable createContent(OverlayColourScheme colourScheme, Func creationFunc) + { + var colourProvider = new OverlayColourProvider(colourScheme); + + return new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(OverlayColourProvider), colourProvider) + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5 + }, + new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Full, + Padding = new MarginPadding(10), + Spacing = new Vector2(10), + ChildrenEnumerable = testCases.Select(creationFunc) + } + } + } + }; + } + + private void createTestCase(Func creationFunc) + { + foreach (var scheme in Enum.GetValues(typeof(OverlayColourScheme)).Cast()) + AddStep($"set {scheme} scheme", () => Child = createContent(scheme, creationFunc)); + } + + [Test] + public void TestNormal() => createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo)); + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs new file mode 100644 index 0000000000..8136ebbd70 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCard.cs @@ -0,0 +1,280 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapSet; +using osuTK; +using osu.Game.Overlays.BeatmapListing.Panels; +using osu.Game.Resources.Localisation.Web; +using osuTK.Graphics; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class BeatmapCard : OsuClickableContainer + { + public const float TRANSITION_DURATION = 400; + + private const float width = 408; + private const float height = 100; + private const float corner_radius = 10; + + private readonly APIBeatmapSet beatmapSet; + + private UpdateableOnlineBeatmapSetCover leftCover; + private FillFlowContainer iconArea; + + private Container mainContent; + private BeatmapCardContentBackground mainContentBackground; + + private GridContainer titleContainer; + private GridContainer artistContainer; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + public BeatmapCard(APIBeatmapSet beatmapSet) + : base(HoverSampleSet.Submit) + { + this.beatmapSet = beatmapSet; + } + + [BackgroundDependencyLoader] + private void load() + { + Width = width; + Height = height; + CornerRadius = corner_radius; + Masking = true; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3 + }, + new Container + { + Name = @"Left (icon) area", + Size = new Vector2(height), + Children = new Drawable[] + { + leftCover = new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) + { + RelativeSizeAxes = Axes.Both, + OnlineInfo = beatmapSet + }, + iconArea = new FillFlowContainer + { + Margin = new MarginPadding(5), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(1) + } + } + }, + mainContent = new Container + { + Name = @"Main content", + X = height - corner_radius, + Height = height, + CornerRadius = corner_radius, + Masking = true, + Children = new Drawable[] + { + mainContentBackground = new BeatmapCardContentBackground(beatmapSet) + { + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + titleContainer = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] + { + new OsuSpriteText + { + Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title), + Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + Truncate = true + }, + Empty() + } + } + }, + artistContainer = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new[] + { + new OsuSpriteText + { + Text = createArtistText(), + Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + Truncate = true + }, + Empty() + }, + } + }, + new LinkFlowContainer(s => + { + s.Shadow = false; + s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold); + }).With(d => + { + d.AutoSizeAxes = Axes.Both; + d.Margin = new MarginPadding { Top = 2 }; + d.AddText("mapped by ", t => t.Colour = colourProvider.Content2); + d.AddUserLink(beatmapSet.Author); + }), + } + }, + new FillFlowContainer + { + Name = @"Bottom content", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding + { + Horizontal = 10, + Vertical = 4 + }, + Spacing = new Vector2(4, 0), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Children = new Drawable[] + { + new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Status = beatmapSet.Status, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + new DifficultySpectrumDisplay(beatmapSet) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + DotSize = new Vector2(6, 12) + } + } + } + } + } + }; + + if (beatmapSet.HasVideo) + iconArea.Add(new IconPill(FontAwesome.Solid.Film)); + + if (beatmapSet.HasStoryboard) + iconArea.Add(new IconPill(FontAwesome.Solid.Image)); + + if (beatmapSet.HasExplicitContent) + { + titleContainer.Content[0][1] = new ExplicitContentBeatmapPill + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 5 } + }; + } + + if (beatmapSet.TrackId != null) + { + artistContainer.Content[0][1] = new FeaturedArtistBeatmapPill + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Left = 5 } + }; + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateState(); + FinishTransforms(true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private LocalisableString createArtistText() + { + var romanisableArtist = new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist); + return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist); + } + + private void updateState() + { + float targetWidth = width - height; + if (IsHovered) + targetWidth -= 20; + + mainContent.ResizeWidthTo(targetWidth, TRANSITION_DURATION, Easing.OutQuint); + mainContentBackground.Dimmed.Value = IsHovered; + + leftCover.FadeColour(IsHovered ? OsuColour.Gray(0.2f) : Color4.White, TRANSITION_DURATION, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs new file mode 100644 index 0000000000..392f5d1bfa --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardContentBackground.cs @@ -0,0 +1,71 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays; + +namespace osu.Game.Beatmaps.Drawables.Cards +{ + public class BeatmapCardContentBackground : CompositeDrawable + { + public BindableBool Dimmed { get; } = new BindableBool(); + + private readonly Box background; + private readonly DelayedLoadUnloadWrapper cover; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public BeatmapCardContentBackground(IBeatmapSetOnlineInfo onlineInfo) + { + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + }, + cover = new DelayedLoadUnloadWrapper(() => createCover(onlineInfo), 500, 500) + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Transparent + } + }; + } + + private static Drawable createCover(IBeatmapSetOnlineInfo onlineInfo) => new OnlineBeatmapSetCover(onlineInfo) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill + }; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + background.Colour = colourProvider.Background2; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Dimmed.BindValueChanged(_ => updateState(), true); + FinishTransforms(true); + } + + private void updateState() => Schedule(() => + { + background.FadeColour(Dimmed.Value ? colourProvider.Background4 : colourProvider.Background2, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + + var gradient = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0), Colour4.White.Opacity(0.2f)); + cover.FadeColour(gradient, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); + }); + } +}