diff --git a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs new file mode 100644 index 0000000000..f810361610 --- /dev/null +++ b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs @@ -0,0 +1,26 @@ +// 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.Game.Collections; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.Collections +{ + public class TestSceneCollectionDialog : OsuTestScene + { + [Cached] + private DialogOverlay dialogOverlay; + + public TestSceneCollectionDialog() + { + Children = new Drawable[] + { + new CollectionDialog { State = { Value = Visibility.Visible } }, + dialogOverlay = new DialogOverlay() + }; + } + } +} diff --git a/osu.Game/Collections/CollectionDialog.cs b/osu.Game/Collections/CollectionDialog.cs new file mode 100644 index 0000000000..e911b507e5 --- /dev/null +++ b/osu.Game/Collections/CollectionDialog.cs @@ -0,0 +1,125 @@ +// 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.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Collections +{ + public class CollectionDialog : OsuFocusedOverlayContainer + { + private const double enter_duration = 500; + private const double exit_duration = 200; + + [Resolved] + private CollectionManager collectionManager { get; set; } + + public CollectionDialog() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(0.5f, 0.8f); + + Masking = true; + CornerRadius = 10; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Children = new Drawable[] + { + new Box + { + Colour = colours.GreySeafoamDark, + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 50), + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Manage collections", + Font = OsuFont.GetFont(size: 30), + } + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeafoamDarker + }, + new CollectionList + { + RelativeSizeAxes = Axes.Both, + Items = { BindTarget = collectionManager.Collections } + } + } + } + }, + new Drawable[] + { + new OsuButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + Padding = new MarginPadding(10), + Text = "Create new collection", + Action = () => collectionManager.Collections.Add(new BeatmapCollection { Name = "My new collection" }) + }, + }, + } + } + } + }; + } + + protected override void PopIn() + { + base.PopIn(); + + this.FadeIn(enter_duration, Easing.OutQuint); + this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutElastic); + } + + protected override void PopOut() + { + base.PopOut(); + + this.FadeOut(exit_duration, Easing.OutQuint); + this.ScaleTo(0.9f, exit_duration); + } + } +} diff --git a/osu.Game/Collections/CollectionList.cs b/osu.Game/Collections/CollectionList.cs new file mode 100644 index 0000000000..990f82e702 --- /dev/null +++ b/osu.Game/Collections/CollectionList.cs @@ -0,0 +1,27 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Collections +{ + public class CollectionList : OsuRearrangeableListContainer + { + protected override ScrollContainer CreateScrollContainer() => base.CreateScrollContainer().With(d => + { + d.ScrollbarVisible = false; + }); + + protected override FillFlowContainer> CreateListFillFlowContainer() => new FillFlowContainer> + { + LayoutDuration = 200, + LayoutEasing = Easing.OutQuint, + Spacing = new Vector2(0, 2) + }; + + protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapCollection item) => new CollectionListItem(item); + } +} diff --git a/osu.Game/Collections/CollectionListItem.cs b/osu.Game/Collections/CollectionListItem.cs new file mode 100644 index 0000000000..527ed57cd0 --- /dev/null +++ b/osu.Game/Collections/CollectionListItem.cs @@ -0,0 +1,152 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +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.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Collections +{ + public class CollectionListItem : OsuRearrangeableListItem + { + private const float item_height = 35; + + public CollectionListItem(BeatmapCollection item) + : base(item) + { + Padding = new MarginPadding { Right = 20 }; + } + + protected override Drawable CreateContent() => new ItemContent(Model); + + private class ItemContent : CircularContainer + { + private readonly BeatmapCollection collection; + + private ItemTextBox textBox; + + public ItemContent(BeatmapCollection collection) + { + this.collection = collection; + + RelativeSizeAxes = Axes.X; + Height = item_height; + Masking = true; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Children = new Drawable[] + { + new DeleteButton(collection) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v) + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = item_height / 2 }, + Children = new Drawable[] + { + textBox = new ItemTextBox + { + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + CornerRadius = item_height / 2, + Text = collection.Name + }, + } + }, + }; + } + } + + private class ItemTextBox : OsuTextBox + { + protected override float LeftRightPadding => item_height / 2; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundUnfocused = colours.GreySeafoamDarker.Darken(0.5f); + BackgroundFocused = colours.GreySeafoam; + } + } + + private class DeleteButton : CompositeDrawable + { + public Func IsTextBoxHovered; + + [Resolved(CanBeNull = true)] + private DialogOverlay dialogOverlay { get; set; } + + private readonly BeatmapCollection collection; + + private Drawable background; + + public DeleteButton(BeatmapCollection collection) + { + this.collection = collection; + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + + Alpha = 0.1f; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + InternalChildren = new[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Red + }, + new SpriteIcon + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + X = -6, + Size = new Vector2(10), + Icon = FontAwesome.Solid.Trash + } + }; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos); + + protected override bool OnHover(HoverEvent e) + { + this.FadeTo(1f, 100, Easing.Out); + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + this.FadeTo(0.1f, 100); + } + + protected override bool OnClick(ClickEvent e) + { + background.FlashColour(Color4.White, 150); + dialogOverlay?.Push(new DeleteCollectionDialog(collection)); + return true; + } + } + } +} diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs new file mode 100644 index 0000000000..a7677e9de2 --- /dev/null +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -0,0 +1,36 @@ +// 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.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Collections +{ + public class DeleteCollectionDialog : PopupDialog + { + [Resolved] + private CollectionManager collectionManager { get; set; } + + public DeleteCollectionDialog(BeatmapCollection collection) + { + HeaderText = "Confirm deletion of"; + BodyText = collection.Name; + + Icon = FontAwesome.Regular.TrashAlt; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"Yes. Go for it.", + Action = () => collectionManager.Collections.Remove(collection) + }, + new PopupDialogCancelButton + { + Text = @"No! Abort mission!", + }, + }; + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5008a3cf3b..1f69888d2e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -31,6 +31,7 @@ using osu.Framework.Input.Events; using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -632,6 +633,7 @@ namespace osu.Game loadComponentSingleFile(CreateUpdateManager(), Add, true); // overlay elements + loadComponentSingleFile(new CollectionDialog(), overlayContent.Add, true); loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 1bd4447248..fe4c95f4e3 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -51,6 +51,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private CollectionManager collectionManager { get; set; } + [Resolved(CanBeNull = true)] + private CollectionDialog collectionDialog { get; set; } + private IBindable starDifficultyBindable; private CancellationTokenSource starDifficultyCancellationSource; @@ -224,12 +227,11 @@ namespace osu.Game.Screens.Select.Carousel if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null) items.Add(new OsuMenuItem("Details", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); - items.Add(new OsuMenuItem("Add to...") - { - Items = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem) - .Append(new OsuMenuItem("More...", MenuItemType.Standard, () => { })) - .ToArray() - }); + var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem).ToList(); + if (collectionDialog != null) + collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, collectionDialog.Show)); + + items.Add(new OsuMenuItem("Add to...") { Items = collectionItems }); return items.ToArray(); } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index e05b5ee951..8a9b0dc5b3 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -38,6 +38,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private CollectionManager collectionManager { get; set; } + [Resolved(CanBeNull = true)] + private CollectionDialog collectionDialog { get; set; } + private readonly BeatmapSetInfo beatmapSet; public DrawableCarouselBeatmapSet(CarouselBeatmapSet set) @@ -145,12 +148,11 @@ namespace osu.Game.Screens.Select.Carousel if (dialogOverlay != null) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); - items.Add(new OsuMenuItem("Add all to...") - { - Items = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem) - .Append(new OsuMenuItem("More...", MenuItemType.Standard, () => { })) - .ToArray() - }); + var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem).ToList(); + if (collectionDialog != null) + collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, collectionDialog.Show)); + + items.Add(new OsuMenuItem("Add all to...") { Items = collectionItems }); return items.ToArray(); }