1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-31 00:02:56 +08:00
osu-lazer/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs

341 lines
12 KiB
C#
Raw Normal View History

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
using System;
using System.Collections.Generic;
using System.Diagnostics;
2018-04-13 17:19:50 +08:00
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
2018-04-13 17:19:50 +08:00
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
2018-04-13 17:19:50 +08:00
using osu.Game.Beatmaps;
2020-09-02 20:08:31 +08:00
using osu.Game.Collections;
2022-07-27 14:59:36 +08:00
using osu.Game.Database;
2018-04-13 17:19:50 +08:00
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Screens.Select.Carousel
{
2022-11-24 13:32:20 +08:00
public partial class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu
2018-04-13 17:19:50 +08:00
{
public const float HEIGHT = MAX_HEIGHT;
2023-01-09 02:02:48 +08:00
private Action<BeatmapSetInfo> restoreHiddenRequested = null!;
private Action<int>? viewDetails;
2018-04-13 17:19:50 +08:00
2023-01-09 02:02:48 +08:00
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
2020-02-14 21:30:27 +08:00
2023-01-09 02:02:48 +08:00
[Resolved]
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
2020-09-05 02:52:07 +08:00
2022-07-27 14:59:36 +08:00
[Resolved]
2023-01-09 02:02:48 +08:00
private RealmAccess realm { get; set; } = null!;
2022-07-27 14:59:36 +08:00
public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty<DrawableCarouselItem>() : beatmapContainer.AliveChildren;
2023-01-09 02:02:48 +08:00
private Container<DrawableCarouselItem>? beatmapContainer;
2023-01-09 02:02:48 +08:00
private BeatmapSetInfo beatmapSet = null!;
2023-01-09 02:02:48 +08:00
private Task? beatmapsLoadTask;
private MenuItem[]? mainMenuItems;
private double timeSinceUnpool;
2020-10-12 14:36:03 +08:00
[Resolved]
2023-01-09 02:02:48 +08:00
private BeatmapManager manager { get; set; } = null!;
2018-04-13 17:19:50 +08:00
protected override void FreeAfterUse()
{
base.FreeAfterUse();
Item = null;
timeSinceUnpool = 0;
ClearTransforms();
}
2023-01-09 02:02:48 +08:00
[BackgroundDependencyLoader]
private void load(BeatmapSetOverlay? beatmapOverlay, SongSelect? songSelect)
2018-04-13 17:19:50 +08:00
{
if (songSelect != null)
mainMenuItems = songSelect.CreateForwardNavigationMenuItemsForBeatmap(() => (((CarouselBeatmapSet)Item!).GetNextToSelect() as CarouselBeatmap)!.BeatmapInfo);
restoreHiddenRequested = s =>
{
foreach (var b in s.Beatmaps)
manager.Restore(b);
};
2018-04-13 17:19:50 +08:00
if (beatmapOverlay != null)
viewDetails = beatmapOverlay.FetchAndShowBeatmapSet;
2020-10-12 14:36:03 +08:00
}
protected override void Update()
{
base.Update();
Debug.Assert(Item != null);
2023-01-10 17:03:17 +08:00
// position updates should not occur if the item is filtered away.
// this avoids panels flying across the screen only to be eventually off-screen or faded out.
if (!Item.Visible) return;
float targetY = Item.CarouselYPosition;
if (Precision.AlmostEquals(targetY, Y))
Y = targetY;
else
// algorithm for this is taken from ScrollContainer.
// while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct.
Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed));
loadContentIfRequired();
}
private CancellationTokenSource? loadCancellation;
2020-10-12 14:36:03 +08:00
protected override void UpdateItem()
{
loadCancellation?.Cancel();
loadCancellation = null;
2020-10-12 14:36:03 +08:00
base.UpdateItem();
Content.Clear();
Header.Clear();
beatmapContainer = null;
beatmapsLoadTask = null;
if (Item == null)
return;
beatmapSet = ((CarouselBeatmapSet)Item).BeatmapSet;
2022-01-30 13:07:29 +08:00
}
protected override void Deselected()
{
base.Deselected();
2020-10-12 17:19:10 +08:00
MovementContainer.MoveToX(0, 500, Easing.OutExpo);
updateBeatmapYPositions();
}
protected override void Selected()
{
base.Selected();
2020-10-12 14:36:03 +08:00
MovementContainer.MoveToX(-100, 500, Easing.OutExpo);
2020-10-12 14:36:03 +08:00
updateBeatmapDifficulties();
}
private void updateBeatmapDifficulties()
{
2023-01-10 03:59:28 +08:00
Debug.Assert(Item != null);
var carouselBeatmapSet = (CarouselBeatmapSet)Item;
var visibleBeatmaps = carouselBeatmapSet.Items.Where(c => c.Visible).ToArray();
// if we are already displaying all the correct beatmaps, only run animation updates.
// note that the displayed beatmaps may change due to the applied filter.
// a future optimisation could add/remove only changed difficulties rather than reinitialise.
if (beatmapContainer != null && visibleBeatmaps.Length == beatmapContainer.Count && visibleBeatmaps.All(b => beatmapContainer.Any(c => c.Item == b)))
{
updateBeatmapYPositions();
}
else
{
// on selection we show our child beatmaps.
// for now this is a simple drawable construction each selection.
// can be improved in the future.
beatmapContainer = new Container<DrawableCarouselItem>
{
X = 100,
RelativeSizeAxes = Axes.Both,
ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()!)
};
2020-10-12 14:36:03 +08:00
beatmapsLoadTask = LoadComponentAsync(beatmapContainer, loaded =>
{
// make sure the pooled target hasn't changed.
if (beatmapContainer != loaded)
return;
Content.Child = loaded;
updateBeatmapYPositions();
});
}
}
[Resolved]
private BeatmapCarousel.CarouselScrollContainer scrollContainer { get; set; } = null!;
private void loadContentIfRequired()
{
Quad containingSsdq = scrollContainer.ScreenSpaceDrawQuad;
// Using DelayedLoadWrappers would only allow us to load content when on screen, but we want to preload while off-screen
// to provide a better user experience.
// This is tracking time that this drawable is updating since the last pool.
// This is intended to provide a debounce so very fast scrolls (from one end to the other of the carousel)
// don't cause huge overheads.
//
// We increase the delay based on distance from centre, so the beatmaps the user is currently looking at load first.
float timeUpdatingBeforeLoad = 50 + Math.Abs(containingSsdq.Centre.Y - ScreenSpaceDrawQuad.Centre.Y) / containingSsdq.Height * 100;
Debug.Assert(Item != null);
// A load is already in progress if the cancellation token is non-null.
if (loadCancellation != null)
return;
timeSinceUnpool += Time.Elapsed;
// We only trigger a load after this set has been in an updating state for a set amount of time.
if (timeSinceUnpool <= timeUpdatingBeforeLoad)
return;
loadCancellation = new CancellationTokenSource();
LoadComponentsAsync(new CompositeDrawable[]
{
// Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set).
new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)))
{
RelativeSizeAxes = Axes.Both,
},
new SetPanelContent((CarouselBeatmapSet)Item)
{
Depth = float.MinValue,
RelativeSizeAxes = Axes.Both,
}
}, drawables =>
{
Header.AddRange(drawables);
drawables.ForEach(d => d.FadeInFromZero(150));
}, loadCancellation.Token);
}
private void updateBeatmapYPositions()
{
if (beatmapContainer == null)
return;
if (beatmapsLoadTask == null || !beatmapsLoadTask.IsCompleted)
return;
float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING;
bool isSelected = Item?.State.Value == CarouselItemState.Selected;
foreach (var panel in beatmapContainer)
{
2023-01-10 03:59:28 +08:00
Debug.Assert(panel.Item != null);
if (isSelected)
{
panel.MoveToY(yPos, 800, Easing.OutQuint);
2023-01-10 03:59:28 +08:00
yPos += panel.Item.TotalHeight;
}
else
panel.MoveToY(0, 800, Easing.OutQuint);
}
2018-04-13 17:19:50 +08:00
}
public MenuItem[] ContextMenuItems
{
get
{
Debug.Assert(beatmapSet != null);
2018-04-13 17:19:50 +08:00
List<MenuItem> items = new List<MenuItem>();
if (Item?.State.Value == CarouselItemState.NotSelected)
2018-04-13 17:19:50 +08:00
items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => Item.State.Value = CarouselItemState.Selected));
if (mainMenuItems != null)
items.AddRange(mainMenuItems);
if (beatmapSet.OnlineID > 0 && viewDetails != null)
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineID)));
2018-04-13 17:19:50 +08:00
var collectionItems = realm.Realm.All<BeatmapCollection>()
.OrderBy(c => c.Name)
.AsEnumerable()
.Select(createCollectionMenuItem)
.ToList();
2022-07-27 14:59:36 +08:00
if (manageCollectionsDialog != null)
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
2020-09-05 02:52:07 +08:00
2022-07-27 14:59:36 +08:00
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
2020-09-02 20:08:31 +08:00
2020-09-08 11:04:35 +08:00
if (beatmapSet.Beatmaps.Any(b => b.Hidden))
items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet)));
if (dialogOverlay != null)
items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet))));
2018-04-13 17:19:50 +08:00
return items.ToArray();
}
}
private MenuItem createCollectionMenuItem(BeatmapCollection collection)
2020-09-02 20:08:31 +08:00
{
Debug.Assert(beatmapSet != null);
2020-09-02 20:08:31 +08:00
TernaryState state;
2022-07-27 14:59:36 +08:00
int countExisting = beatmapSet.Beatmaps.Count(b => collection.BeatmapMD5Hashes.Contains(b.MD5Hash));
2020-09-02 20:08:31 +08:00
if (countExisting == beatmapSet.Beatmaps.Count)
state = TernaryState.True;
else if (countExisting > 0)
state = TernaryState.Indeterminate;
else
state = TernaryState.False;
var liveCollection = collection.ToLive(realm);
2022-07-27 14:59:36 +08:00
return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
2020-09-02 20:08:31 +08:00
{
liveCollection.PerformWrite(c =>
2020-09-02 20:08:31 +08:00
{
foreach (var b in beatmapSet.Beatmaps)
2020-09-02 20:08:31 +08:00
{
switch (s)
{
case TernaryState.True:
if (c.BeatmapMD5Hashes.Contains(b.MD5Hash))
continue;
c.BeatmapMD5Hashes.Add(b.MD5Hash);
break;
case TernaryState.False:
c.BeatmapMD5Hashes.Remove(b.MD5Hash);
break;
}
2020-09-02 20:08:31 +08:00
}
});
2020-09-02 20:08:31 +08:00
})
{
State = { Value = state }
};
}
2018-04-13 17:19:50 +08:00
}
}