mirror of
https://github.com/ppy/osu.git
synced 2026-05-17 22:33:47 +08:00
Merge branch 'master' into song-select-v2-scroll-to-selected
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
<PropertyGroup Label="C#">
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Stabilises hot reload, see: https://platform.uno/docs/articles/studio/Hot%20Reload/hot-reload-overview.html?tabs=vswin%2Cwindows%2Cskia-desktop%2Ccommon-issues -->
|
||||
<GenerateAssemblyInfo Condition="'$(Configuration)'=='Debug'">false</GenerateAssemblyInfo>
|
||||
<!-- Required due to the above -->
|
||||
<NoWarn Condition="'$(Configuration)'=='Debug'">$(NoWarn);CA1416</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>$(MSBuildThisFileDirectory)app.manifest</ApplicationManifest>
|
||||
|
||||
@@ -312,8 +312,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
AddStep("set game volume to max", () => Game.Dependencies.Get<FrameworkConfigManager>().SetValue(FrameworkSetting.VolumeUniversal, 1d));
|
||||
|
||||
AddStep("move to metadata wedge", () => InputManager.MoveMouseTo(
|
||||
songSelect.ChildrenOfType<BeatmapMetadataWedge>().Single()));
|
||||
AddStep("move to details area", () => InputManager.MoveMouseTo(
|
||||
songSelect.ChildrenOfType<BeatmapDetailsArea>().Single()));
|
||||
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
|
||||
AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition));
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
..beatmap4.Beatmaps
|
||||
];
|
||||
|
||||
var results = await runGrouping(GroupMode.NoGrouping, beatmapSets);
|
||||
var results = await runGrouping(GroupMode.None, beatmapSets);
|
||||
Assert.That(results.Select(r => r.Model).OfType<BeatmapSetInfo>(), Is.EquivalentTo(beatmapSets));
|
||||
Assert.That(results.Select(r => r.Model).OfType<BeatmapInfo>(), Is.EquivalentTo(allBeatmaps));
|
||||
assertTotal(results, beatmapSets.Count + allBeatmaps.Length);
|
||||
|
||||
@@ -149,6 +149,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
TextAnchor = Anchor.CentreLeft,
|
||||
},
|
||||
};
|
||||
|
||||
Carousel.Filter(new FilterCriteria());
|
||||
});
|
||||
|
||||
// Prefer title sorting so that order of carousel panels match order of BeatmapSets bindable.
|
||||
@@ -171,7 +173,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
AddStep(description, () =>
|
||||
{
|
||||
var criteria = Carousel.Criteria;
|
||||
var criteria = Carousel.Criteria ?? new FilterCriteria();
|
||||
apply?.Invoke(criteria);
|
||||
Carousel.Filter(criteria);
|
||||
});
|
||||
@@ -191,6 +193,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
|
||||
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
|
||||
|
||||
protected void CheckRequestPresentCount(int expected) =>
|
||||
AddAssert($"check present count is {expected}", () => Carousel.RequestPresentBeatmapCount, () => Is.EqualTo(expected));
|
||||
|
||||
protected void CheckActivationCount(int expected) =>
|
||||
AddAssert($"check activation count is {expected}", () => Carousel.ActivationCount, () => Is.EqualTo(expected));
|
||||
|
||||
protected void CheckDisplayedBeatmapsCount(int expected)
|
||||
{
|
||||
AddAssert($"{expected} diffs displayed", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
|
||||
@@ -356,7 +364,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
""");
|
||||
createHeader("carousel");
|
||||
stats.AddParagraph($"""
|
||||
sorting: {Carousel.IsFiltering}
|
||||
filtering: {Carousel.IsFiltering} (total {Carousel.FilterCount} times)
|
||||
tracked: {Carousel.ItemsTracked}
|
||||
displayable: {Carousel.DisplayableItems}
|
||||
displayed: {Carousel.VisibleItems}
|
||||
@@ -375,6 +383,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
public partial class TestBeatmapCarousel : BeatmapCarousel
|
||||
{
|
||||
public int ActivationCount { get; private set; }
|
||||
public int RequestPresentBeatmapCount { get; private set; }
|
||||
|
||||
public int FilterDelay = 0;
|
||||
|
||||
public IEnumerable<BeatmapInfo> PostFilterBeatmaps = null!;
|
||||
@@ -385,12 +396,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
public new BeatmapSetInfo? ExpandedBeatmapSet => base.ExpandedBeatmapSet;
|
||||
public new GroupDefinition? ExpandedGroup => base.ExpandedGroup;
|
||||
|
||||
public TestBeatmapCarousel()
|
||||
{
|
||||
RequestPresentBeatmap = _ => RequestPresentBeatmapCount++;
|
||||
}
|
||||
|
||||
protected override void HandleItemActivated(CarouselItem item)
|
||||
{
|
||||
ActivationCount++;
|
||||
base.HandleItemActivated(item);
|
||||
}
|
||||
|
||||
protected override async Task<IEnumerable<CarouselItem>> FilterAsync(bool clearExistingPanels = false)
|
||||
{
|
||||
var items = await base.FilterAsync(clearExistingPanels);
|
||||
var items = await base.FilterAsync(clearExistingPanels).ConfigureAwait(true);
|
||||
|
||||
if (FilterDelay != 0)
|
||||
await Task.Delay(FilterDelay);
|
||||
await Task.Delay(FilterDelay).ConfigureAwait(true);
|
||||
|
||||
PostFilterBeatmaps = items.Select(i => i.Model).OfType<BeatmapInfo>();
|
||||
return items;
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
SelectedMods.SetDefault();
|
||||
|
||||
Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title);
|
||||
Config.SetValue(OsuSetting.SongSelectGroupMode, GroupMode.NoGrouping);
|
||||
Config.SetValue(OsuSetting.SongSelectGroupMode, GroupMode.None);
|
||||
|
||||
SongSelect = null!;
|
||||
});
|
||||
|
||||
@@ -6,9 +6,12 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
@@ -35,7 +38,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
[Explicit]
|
||||
public void TestSorting()
|
||||
{
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.NoGrouping);
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.None);
|
||||
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
|
||||
}
|
||||
@@ -53,7 +56,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
public void TestLoadingDisplay()
|
||||
{
|
||||
AddStep("induce slow filtering", () => Carousel.FilterDelay = 2000);
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.NoGrouping);
|
||||
SortAndGroupBy(SortMode.Artist, GroupMode.None);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -92,6 +95,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHighChurnUpdatesStillShowsPanels()
|
||||
{
|
||||
ScheduledDelegate updateTask = null!;
|
||||
|
||||
AddBeatmaps(1, 1);
|
||||
|
||||
AddStep("start constantly updating beatmap in background", () =>
|
||||
{
|
||||
updateTask = Scheduler.AddDelayed(() => { BeatmapSets.ReplaceRange(0, 1, [BeatmapSets.First()]); }, 1, true);
|
||||
});
|
||||
|
||||
CreateCarousel();
|
||||
|
||||
AddUntilStep("panels loaded", () => Carousel.ChildrenOfType<Panel>(), () => Is.Not.Empty);
|
||||
|
||||
AddStep("end task", () => updateTask.Cancel());
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Explicit]
|
||||
public void TestPerformanceWithManyBeatmaps()
|
||||
@@ -116,5 +138,16 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
|
||||
AddStep("add all beatmaps", () => BeatmapSets.AddRange(generated));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleItemDisplayed()
|
||||
{
|
||||
CreateCarousel();
|
||||
RemoveAllBeatmaps();
|
||||
|
||||
SortAndGroupBy(SortMode.Difficulty, GroupMode.None);
|
||||
AddBeatmaps(1, fixedDifficultiesPerSet: 1);
|
||||
AddUntilStep("single item is shown", () => this.ChildrenOfType<PanelBeatmapStandalone>().Count(), () => Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +190,58 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
WaitForSelection(4, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleItemTraversal()
|
||||
{
|
||||
CheckNoSelection();
|
||||
AddBeatmaps(1, 3);
|
||||
|
||||
WaitForSelection(0, 0);
|
||||
CheckActivationCount(0);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForSelection(0, 0);
|
||||
|
||||
// In the case of a grouped beatmap set, the header gets activated and re-selects the recommended difficulty.
|
||||
// This is probably fine.
|
||||
CheckActivationCount(1);
|
||||
// We don't want it to request present though, which would start gameplay.
|
||||
CheckRequestPresentCount(0);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForSelection(0, 0);
|
||||
|
||||
CheckActivationCount(1);
|
||||
CheckRequestPresentCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleItemTraversal_DifficultySplit()
|
||||
{
|
||||
SortBy(SortMode.Difficulty);
|
||||
|
||||
CheckNoSelection();
|
||||
AddBeatmaps(1, 1);
|
||||
|
||||
WaitForSelection(0, 0);
|
||||
CheckActivationCount(0);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForSelection(0, 0);
|
||||
|
||||
// In the case of a grouped beatmap set, the header gets activated and re-selects the recommended difficulty.
|
||||
// This is probably fine.
|
||||
CheckActivationCount(0);
|
||||
// We don't want it to request present though, which would start gameplay.
|
||||
CheckRequestPresentCount(0);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForSelection(0, 0);
|
||||
|
||||
CheckActivationCount(0);
|
||||
CheckRequestPresentCount(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyTraversal()
|
||||
{
|
||||
@@ -243,7 +295,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddBeatmaps(2, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SortAndGroupBy(SortMode.Difficulty, GroupMode.NoGrouping);
|
||||
SortAndGroupBy(SortMode.Difficulty, GroupMode.None);
|
||||
WaitForFiltering();
|
||||
|
||||
AddUntilStep("standalone panels displayed", () => GetVisiblePanels<PanelBeatmapStandalone>().Any());
|
||||
|
||||
@@ -78,6 +78,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
AddUntilStep("wait for results screen", () => Stack.CurrentScreen is ResultsScreen);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleFilterWhenEntering()
|
||||
{
|
||||
ImportBeatmapForRuleset(0);
|
||||
LoadSongSelect();
|
||||
|
||||
AddAssert("single filter", () => Carousel.FilterCount, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCookieDoesNothingIfNothingSelected()
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Position = new Vector2(275, 5)
|
||||
});
|
||||
|
||||
filter.PinItem(GroupMode.NoGrouping);
|
||||
filter.PinItem(GroupMode.None);
|
||||
filter.PinItem(GroupMode.LastPlayed);
|
||||
|
||||
filter.Current.ValueChanged += grouping =>
|
||||
|
||||
@@ -40,14 +40,14 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.Ruleset, string.Empty);
|
||||
SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString());
|
||||
|
||||
SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Local);
|
||||
SetDefault(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Local);
|
||||
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
|
||||
|
||||
SetDefault(OsuSetting.ShowConvertedBeatmaps, true);
|
||||
SetDefault(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1);
|
||||
SetDefault(OsuSetting.DisplayStarsMaximum, 10.1, 0, 10.1, 0.1);
|
||||
|
||||
SetDefault(OsuSetting.SongSelectGroupMode, GroupMode.NoGrouping);
|
||||
SetDefault(OsuSetting.SongSelectGroupMode, GroupMode.None);
|
||||
SetDefault(OsuSetting.SongSelectSortingMode, SortMode.Title);
|
||||
|
||||
SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation);
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -71,6 +72,11 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// </summary>
|
||||
public bool IsFiltering => !filterTask.IsCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// The number of times filter operations have been triggered.
|
||||
/// </summary>
|
||||
internal int FilterCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of displayable items currently being tracked (before filtering).
|
||||
/// </summary>
|
||||
@@ -181,9 +187,13 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// <param name="clearExistingPanels">Whether all existing drawable panels should be reset post filter.</param>
|
||||
protected virtual Task<IEnumerable<CarouselItem>> FilterAsync(bool clearExistingPanels = false)
|
||||
{
|
||||
FilterCount++;
|
||||
|
||||
if (clearExistingPanels)
|
||||
filterReusesPanels.Invalidate();
|
||||
|
||||
filterAfterItemsChanged.Validate();
|
||||
|
||||
filterTask = performFilter();
|
||||
filterTask.FireAndForget();
|
||||
return filterTask;
|
||||
@@ -267,7 +277,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
Items.BindCollectionChanged((_, _) => FilterAsync());
|
||||
Items.BindCollectionChanged((_, _) => filterAfterItemsChanged.Invalidate());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -290,22 +300,29 @@ namespace osu.Game.Graphics.Carousel
|
||||
private Task<IEnumerable<CarouselItem>> filterTask = Task.FromResult(Enumerable.Empty<CarouselItem>());
|
||||
private CancellationTokenSource cancellationSource = new CancellationTokenSource();
|
||||
|
||||
/// <summary>
|
||||
/// For background re-filters, ensure we wait for the previous filter operation to complete before starting another.
|
||||
/// This avoids the carousel never updating its display in high churn scenarios.
|
||||
/// </summary>
|
||||
private readonly Cached filterAfterItemsChanged = new Cached();
|
||||
|
||||
private async Task<IEnumerable<CarouselItem>> performFilter()
|
||||
{
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
var previousCancellationSource = Interlocked.Exchange(ref cancellationSource, cts);
|
||||
await previousCancellationSource.CancelAsync().ConfigureAwait(false);
|
||||
await previousCancellationSource.CancelAsync().ConfigureAwait(true);
|
||||
|
||||
if (DebounceDelay > 0)
|
||||
{
|
||||
log($"Filter operation queued, waiting for {DebounceDelay} ms debounce");
|
||||
await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(false);
|
||||
await Task.Delay(DebounceDelay, cts.Token).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
// Copy must be performed on update thread for now (see ConfigureAwait above).
|
||||
// Could potentially be optimised in the future if it becomes an issue.
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
List<CarouselItem> items = new List<CarouselItem>(Items.Select(m => new CarouselItem(m)));
|
||||
|
||||
await Task.Run(async () =>
|
||||
@@ -524,6 +541,10 @@ namespace osu.Game.Graphics.Carousel
|
||||
do
|
||||
{
|
||||
newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count;
|
||||
|
||||
if (newIndex == originalIndex)
|
||||
break;
|
||||
|
||||
var newItem = carouselItems[newIndex];
|
||||
|
||||
if (CheckValidForGroupSelection(newItem))
|
||||
@@ -531,7 +552,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
HandleItemActivated(newItem);
|
||||
return;
|
||||
}
|
||||
} while (newIndex != originalIndex);
|
||||
} while (true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -611,13 +632,13 @@ namespace osu.Game.Graphics.Carousel
|
||||
{
|
||||
var item = carouselItems[i];
|
||||
|
||||
updateItemYPosition(item, ref lastVisible, ref yPos);
|
||||
|
||||
if (CheckModelEquality(item.Model, currentKeyboardSelection.Model!))
|
||||
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, item.CarouselYPosition, i);
|
||||
|
||||
if (CheckModelEquality(item.Model, currentSelection.Model!))
|
||||
currentSelection = new Selection(currentSelection.Model, item, item.CarouselYPosition, i);
|
||||
|
||||
updateItemYPosition(item, ref lastVisible, ref yPos);
|
||||
}
|
||||
|
||||
// If a keyboard selection is currently made, we want to keep the view stable around the selection.
|
||||
@@ -721,6 +742,9 @@ namespace osu.Game.Graphics.Carousel
|
||||
c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem;
|
||||
c.Expanded.Value = c.Item.IsExpanded;
|
||||
}
|
||||
|
||||
if (!filterAfterItemsChanged.IsValid && !IsFiltering)
|
||||
FilterAsync();
|
||||
}
|
||||
|
||||
protected virtual float GetPanelXOffset(Drawable panel)
|
||||
@@ -751,6 +775,9 @@ namespace osu.Game.Graphics.Carousel
|
||||
{
|
||||
Debug.Assert(carouselItems != null);
|
||||
|
||||
if (carouselItems.Count == 0)
|
||||
return DisplayRange.EMPTY;
|
||||
|
||||
// Find index range of all items that should be on-screen
|
||||
carouselBoundsItem.CarouselYPosition = visibleUpperBound - DistanceOffscreenToPreload;
|
||||
int firstIndex = carouselItems.BinarySearch(carouselBoundsItem);
|
||||
@@ -770,7 +797,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
{
|
||||
Debug.Assert(carouselItems != null);
|
||||
|
||||
List<CarouselItem> toDisplay = range.Last - range.First == 0
|
||||
List<CarouselItem> toDisplay = range == DisplayRange.EMPTY
|
||||
? new List<CarouselItem>()
|
||||
: carouselItems.GetRange(range.First, range.Last - range.First + 1);
|
||||
|
||||
@@ -889,7 +916,10 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// <param name="Index">The index of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
|
||||
private record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null);
|
||||
|
||||
private record DisplayRange(int First, int Last);
|
||||
private record DisplayRange(int First, int Last)
|
||||
{
|
||||
public static readonly DisplayRange EMPTY = new DisplayRange(-1, -1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of scroll container which handles very large vertical lists by internally using <c>double</c> precision
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@@ -49,8 +50,13 @@ namespace osu.Game.Screens.Footer
|
||||
private Container<ScreenFooterButton> hiddenButtonsContainer = null!;
|
||||
private LogoTrackingContainer logoTrackingContainer = null!;
|
||||
|
||||
// TODO: This has some weird update logic local in this class, but it only works for overlay containers.
|
||||
// This is not what we want. The footer is to be displayed on *screens* with different colour schemes.
|
||||
// It needs to update on screen switch.
|
||||
//
|
||||
// For now it's locked to Blue to match song select (the most prominent usage).
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
public ScreenFooter(BackReceptor? receptor = null)
|
||||
{
|
||||
@@ -167,6 +173,7 @@ namespace osu.Game.Screens.Footer
|
||||
temporarilyHiddenButtons.Clear();
|
||||
overlays.Clear();
|
||||
|
||||
this.HidePopover();
|
||||
clearActiveOverlayContainer();
|
||||
|
||||
var oldButtons = buttonsFlow.ToArray();
|
||||
@@ -312,6 +319,8 @@ namespace osu.Game.Screens.Footer
|
||||
|
||||
private void showOverlay(OverlayContainer overlay)
|
||||
{
|
||||
this.HidePopover();
|
||||
|
||||
foreach (var o in overlays.Where(o => o != overlay))
|
||||
o.Hide();
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public enum BeatmapDetailTab
|
||||
{
|
||||
/// <summary>
|
||||
/// Beatmap details.
|
||||
/// </summary>
|
||||
Details,
|
||||
|
||||
/// <summary>
|
||||
/// Local leaderboards.
|
||||
/// </summary>
|
||||
Local,
|
||||
|
||||
/// <summary>
|
||||
/// Country leaderboards.
|
||||
/// </summary>
|
||||
Country,
|
||||
|
||||
/// <summary>
|
||||
/// Global leaderboards.
|
||||
/// </summary>
|
||||
Global,
|
||||
|
||||
/// <summary>
|
||||
/// Friend leaderboards.
|
||||
/// </summary>
|
||||
Friends,
|
||||
|
||||
/// <summary>
|
||||
/// Team leaderboards.
|
||||
/// </summary>
|
||||
Team
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ namespace osu.Game.Screens.Select.Filter
|
||||
{
|
||||
public enum GroupMode
|
||||
{
|
||||
[Description("No Grouping")]
|
||||
NoGrouping,
|
||||
[Description("None")]
|
||||
None,
|
||||
|
||||
[Description("Artist")]
|
||||
Artist,
|
||||
@@ -19,8 +19,8 @@ namespace osu.Game.Screens.Select.Filter
|
||||
[Description("BPM")]
|
||||
BPM,
|
||||
|
||||
[Description("Collections")]
|
||||
Collections,
|
||||
// [Description("Collections")]
|
||||
// Collections,
|
||||
|
||||
[Description("Date Added")]
|
||||
DateAdded,
|
||||
@@ -31,17 +31,17 @@ namespace osu.Game.Screens.Select.Filter
|
||||
[Description("Difficulty")]
|
||||
Difficulty,
|
||||
|
||||
[Description("Favourites")]
|
||||
Favourites,
|
||||
// [Description("Favourites")]
|
||||
// Favourites,
|
||||
|
||||
[Description("Length")]
|
||||
Length,
|
||||
|
||||
[Description("My Maps")]
|
||||
MyMaps,
|
||||
// [Description("My Maps")]
|
||||
// MyMaps,
|
||||
|
||||
[Description("Rank Achieved")]
|
||||
RankAchieved,
|
||||
// [Description("Rank Achieved")]
|
||||
// RankAchieved,
|
||||
|
||||
[Description("Ranked Status")]
|
||||
RankedStatus,
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
|
||||
private Bindable<TabType> selectedTab;
|
||||
private Bindable<BeatmapDetailTab> selectedTab;
|
||||
|
||||
private Bindable<bool> selectedModsFilter;
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.Select
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
selectedTab = config.GetBindable<TabType>(OsuSetting.BeatmapDetailTab);
|
||||
selectedTab = config.GetBindable<BeatmapDetailTab>(OsuSetting.BeatmapDetailTab);
|
||||
selectedModsFilter = config.GetBindable<bool>(OsuSetting.BeatmapDetailModsFilter);
|
||||
|
||||
selectedTab.BindValueChanged(tab => CurrentTab.Value = getTabItemFromTabType(tab.NewValue), true);
|
||||
@@ -86,26 +86,26 @@ namespace osu.Game.Screens.Select
|
||||
new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Team),
|
||||
}).ToArray();
|
||||
|
||||
private BeatmapDetailAreaTabItem getTabItemFromTabType(TabType type)
|
||||
private BeatmapDetailAreaTabItem getTabItemFromTabType(BeatmapDetailTab type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TabType.Details:
|
||||
case BeatmapDetailTab.Details:
|
||||
return new BeatmapDetailAreaDetailTabItem();
|
||||
|
||||
case TabType.Local:
|
||||
case BeatmapDetailTab.Local:
|
||||
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local);
|
||||
|
||||
case TabType.Global:
|
||||
case BeatmapDetailTab.Global:
|
||||
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Global);
|
||||
|
||||
case TabType.Country:
|
||||
case BeatmapDetailTab.Country:
|
||||
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Country);
|
||||
|
||||
case TabType.Friends:
|
||||
case BeatmapDetailTab.Friends:
|
||||
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Friend);
|
||||
|
||||
case TabType.Team:
|
||||
case BeatmapDetailTab.Team:
|
||||
return new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Team);
|
||||
|
||||
default:
|
||||
@@ -113,30 +113,30 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
|
||||
private TabType getTabTypeFromTabItem(BeatmapDetailAreaTabItem item)
|
||||
private BeatmapDetailTab getTabTypeFromTabItem(BeatmapDetailAreaTabItem item)
|
||||
{
|
||||
switch (item)
|
||||
{
|
||||
case BeatmapDetailAreaDetailTabItem:
|
||||
return TabType.Details;
|
||||
return BeatmapDetailTab.Details;
|
||||
|
||||
case BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope> leaderboardTab:
|
||||
switch (leaderboardTab.Scope)
|
||||
{
|
||||
case BeatmapLeaderboardScope.Local:
|
||||
return TabType.Local;
|
||||
return BeatmapDetailTab.Local;
|
||||
|
||||
case BeatmapLeaderboardScope.Country:
|
||||
return TabType.Country;
|
||||
return BeatmapDetailTab.Country;
|
||||
|
||||
case BeatmapLeaderboardScope.Global:
|
||||
return TabType.Global;
|
||||
return BeatmapDetailTab.Global;
|
||||
|
||||
case BeatmapLeaderboardScope.Friend:
|
||||
return TabType.Friends;
|
||||
return BeatmapDetailTab.Friends;
|
||||
|
||||
case BeatmapLeaderboardScope.Team:
|
||||
return TabType.Team;
|
||||
return BeatmapDetailTab.Team;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(item));
|
||||
@@ -146,15 +146,5 @@ namespace osu.Game.Screens.Select
|
||||
throw new ArgumentOutOfRangeException(nameof(item));
|
||||
}
|
||||
}
|
||||
|
||||
public enum TabType
|
||||
{
|
||||
Details,
|
||||
Local,
|
||||
Country,
|
||||
Global,
|
||||
Friends,
|
||||
Team
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@@ -60,9 +61,25 @@ namespace osu.Game.Screens.SelectV2
|
||||
if ((top.Model is GroupDefinition) ^ (bottom.Model is GroupDefinition))
|
||||
return SPACING * 2;
|
||||
|
||||
// Beatmap difficulty panels do not overlap with themselves or any other panel.
|
||||
if (grouping.BeatmapSetsGroupedTogether && (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo))
|
||||
return SPACING;
|
||||
if (grouping.BeatmapSetsGroupedTogether)
|
||||
{
|
||||
// Give some space around the expanded beatmap set, at the top..
|
||||
if (bottom.Model is BeatmapSetInfo && bottom.IsExpanded)
|
||||
return SPACING * 2;
|
||||
|
||||
// ..and the bottom.
|
||||
if (top.Model is BeatmapInfo && bottom.Model is BeatmapSetInfo)
|
||||
return SPACING * 2;
|
||||
|
||||
// Beatmap difficulty panels do not overlap with themselves or any other panel.
|
||||
if (top.Model is BeatmapInfo || bottom.Model is BeatmapInfo)
|
||||
return SPACING;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (top == CurrentSelectionItem || bottom == CurrentSelectionItem)
|
||||
return SPACING * 2;
|
||||
}
|
||||
|
||||
return -SPACING;
|
||||
}
|
||||
@@ -74,9 +91,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
Filters = new ICarouselFilter[]
|
||||
{
|
||||
matching = new BeatmapCarouselFilterMatching(() => Criteria),
|
||||
new BeatmapCarouselFilterSorting(() => Criteria),
|
||||
grouping = new BeatmapCarouselFilterGrouping(() => Criteria),
|
||||
matching = new BeatmapCarouselFilterMatching(() => Criteria!),
|
||||
new BeatmapCarouselFilterSorting(() => Criteria!),
|
||||
grouping = new BeatmapCarouselFilterGrouping(() => Criteria!),
|
||||
};
|
||||
|
||||
AddInternal(loading = new LoadingLayer());
|
||||
@@ -86,20 +103,20 @@ namespace osu.Game.Screens.SelectV2
|
||||
private void load(BeatmapStore beatmapStore, AudioManager audio, OsuConfigManager config, CancellationToken? cancellationToken)
|
||||
{
|
||||
setupPools();
|
||||
setupBeatmaps(beatmapStore, cancellationToken);
|
||||
detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken);
|
||||
loadSamples(audio);
|
||||
|
||||
config.BindWith(OsuSetting.RandomSelectAlgorithm, randomAlgorithm);
|
||||
}
|
||||
|
||||
#region Beatmap source hookup
|
||||
|
||||
private void setupBeatmaps(BeatmapStore beatmapStore, CancellationToken? cancellationToken)
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
detachedBeatmaps = beatmapStore.GetBeatmapSets(cancellationToken);
|
||||
base.LoadComplete();
|
||||
detachedBeatmaps.BindCollectionChanged(beatmapSetsChanged, true);
|
||||
}
|
||||
|
||||
#region Beatmap source hookup
|
||||
|
||||
private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed)
|
||||
{
|
||||
// TODO: moving management of BeatmapInfo tracking to BeatmapStore might be something we want to consider.
|
||||
@@ -467,7 +484,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
#region Filtering
|
||||
|
||||
public FilterCriteria Criteria { get; private set; } = new FilterCriteria();
|
||||
public FilterCriteria? Criteria { get; private set; }
|
||||
|
||||
private ScheduledDelegate? loadingDebounce;
|
||||
|
||||
@@ -493,6 +510,14 @@ namespace osu.Game.Screens.SelectV2
|
||||
}));
|
||||
}
|
||||
|
||||
protected override Task<IEnumerable<CarouselItem>> FilterAsync(bool clearExistingPanels = false)
|
||||
{
|
||||
if (Criteria == null)
|
||||
return Task.FromResult(Enumerable.Empty<CarouselItem>());
|
||||
|
||||
return base.FilterAsync(clearExistingPanels);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Drawable pooling
|
||||
|
||||
@@ -124,14 +124,22 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public static bool ShouldGroupBeatmapsTogether(FilterCriteria criteria)
|
||||
{
|
||||
return criteria.Sort != SortMode.Difficulty && criteria.Group != GroupMode.Difficulty;
|
||||
// In certain cases, we intentionally split out difficulties
|
||||
// where it's more relevant or convenient to view them as individual items.
|
||||
if (criteria.Sort == SortMode.Difficulty || criteria.Group == GroupMode.Difficulty)
|
||||
return false;
|
||||
if (criteria.Sort == SortMode.LastPlayed && criteria.Group == GroupMode.LastPlayed)
|
||||
return false;
|
||||
|
||||
// In the majority case we group sets together for display.
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<GroupMapping> getGroups(List<CarouselItem> items, FilterCriteria criteria)
|
||||
{
|
||||
switch (criteria.Group)
|
||||
{
|
||||
case GroupMode.NoGrouping:
|
||||
case GroupMode.None:
|
||||
return new List<GroupMapping> { new GroupMapping(null, items) };
|
||||
|
||||
case GroupMode.Artist:
|
||||
@@ -152,12 +160,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
case GroupMode.LastPlayed:
|
||||
return getGroupsBy(b =>
|
||||
{
|
||||
DateTimeOffset? maxLastPlayed = aggregateMax(b, items, bb => bb.LastPlayed);
|
||||
var date = b.LastPlayed;
|
||||
|
||||
if (maxLastPlayed == null)
|
||||
if (BeatmapSetsGroupedTogether)
|
||||
date = aggregateMax(b, static b => (b.LastPlayed ?? DateTimeOffset.MinValue));
|
||||
|
||||
if (date == null || date == DateTimeOffset.MinValue)
|
||||
return new GroupDefinition(int.MaxValue, "Never");
|
||||
|
||||
return defineGroupByDate(maxLastPlayed.Value);
|
||||
return defineGroupByDate(date.Value);
|
||||
}, items);
|
||||
|
||||
case GroupMode.RankedStatus:
|
||||
@@ -166,8 +177,12 @@ namespace osu.Game.Screens.SelectV2
|
||||
case GroupMode.BPM:
|
||||
return getGroupsBy(b =>
|
||||
{
|
||||
double maxBPM = aggregateMax(b, items, bb => bb.BPM);
|
||||
return defineGroupByBPM(maxBPM);
|
||||
double bpm = b.BPM;
|
||||
|
||||
if (BeatmapSetsGroupedTogether)
|
||||
bpm = aggregateMax(b, bb => bb.BPM);
|
||||
|
||||
return defineGroupByBPM(bpm);
|
||||
}, items);
|
||||
|
||||
case GroupMode.Difficulty:
|
||||
@@ -176,25 +191,27 @@ namespace osu.Game.Screens.SelectV2
|
||||
case GroupMode.Length:
|
||||
return getGroupsBy(b =>
|
||||
{
|
||||
double maxLength = aggregateMax(b, items, bb => bb.Length);
|
||||
return defineGroupByLength(maxLength);
|
||||
double length = b.Length;
|
||||
|
||||
if (BeatmapSetsGroupedTogether)
|
||||
length = aggregateMax(b, bb => bb.Length);
|
||||
|
||||
return defineGroupByLength(length);
|
||||
}, items);
|
||||
|
||||
case GroupMode.Collections:
|
||||
// TODO: needs implementation
|
||||
goto case GroupMode.NoGrouping;
|
||||
|
||||
case GroupMode.Favourites:
|
||||
// TODO: needs implementation
|
||||
goto case GroupMode.NoGrouping;
|
||||
|
||||
case GroupMode.MyMaps:
|
||||
// TODO: needs implementation
|
||||
goto case GroupMode.NoGrouping;
|
||||
|
||||
case GroupMode.RankAchieved:
|
||||
// TODO: needs implementation
|
||||
goto case GroupMode.NoGrouping;
|
||||
// TODO: need implementation
|
||||
//
|
||||
// case GroupMode.Collections:
|
||||
// goto case GroupMode.None;
|
||||
//
|
||||
// case GroupMode.Favourites:
|
||||
// goto case GroupMode.None;
|
||||
//
|
||||
// case GroupMode.MyMaps:
|
||||
// goto case GroupMode.None;
|
||||
//
|
||||
// case GroupMode.RankAchieved:
|
||||
// goto case GroupMode.None;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
@@ -334,10 +351,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
return new GroupDefinition(11, "Over 10 minutes");
|
||||
}
|
||||
|
||||
private static T? aggregateMax<T>(BeatmapInfo b, IEnumerable<CarouselItem> items, Func<BeatmapInfo, T> func)
|
||||
private static T? aggregateMax<T>(BeatmapInfo b, Func<BeatmapInfo, T> func)
|
||||
{
|
||||
var matchedBeatmaps = items.Select(i => i.Model).Cast<BeatmapInfo>().Where(beatmap => beatmap.BeatmapSet!.Equals(b.BeatmapSet));
|
||||
return matchedBeatmaps.Max(func);
|
||||
var beatmaps = b.BeatmapSet!.Beatmaps.Where(bb => !bb.Hidden);
|
||||
return beatmaps.Max(func);
|
||||
}
|
||||
|
||||
private record GroupMapping(GroupDefinition? Group, List<CarouselItem> ItemsInGroup);
|
||||
|
||||
@@ -27,19 +27,27 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
var criteria = getCriteria();
|
||||
|
||||
bool groupedSets = BeatmapCarouselFilterGrouping.ShouldGroupBeatmapsTogether(criteria);
|
||||
|
||||
return items.Order(Comparer<CarouselItem>.Create((a, b) =>
|
||||
{
|
||||
var ab = (BeatmapInfo)a.Model;
|
||||
var bb = (BeatmapInfo)b.Model;
|
||||
|
||||
if (ab.BeatmapSet!.Equals(bb.BeatmapSet))
|
||||
return compareDifficulty(ab, bb);
|
||||
if (groupedSets)
|
||||
{
|
||||
if (ab.BeatmapSet!.Equals(bb.BeatmapSet))
|
||||
return compareDifficulty(ab, bb, criteria.Sort);
|
||||
|
||||
return compare(ab, bb, criteria.Sort);
|
||||
// If we're grouping by sets, all fallback sorts need to be aggregates for the set.
|
||||
return compare(ab, bb, criteria.Sort, aggregate: true);
|
||||
}
|
||||
|
||||
return compare(ab, bb, criteria.Sort, aggregate: false);
|
||||
})).ToList();
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
private static int compare(BeatmapInfo a, BeatmapInfo b, SortMode sort)
|
||||
private static int compare(BeatmapInfo a, BeatmapInfo b, SortMode sort, bool aggregate)
|
||||
{
|
||||
int comparison;
|
||||
|
||||
@@ -80,15 +88,24 @@ namespace osu.Game.Screens.SelectV2
|
||||
break;
|
||||
|
||||
case SortMode.LastPlayed:
|
||||
comparison = -compareUsingAggregateMax(a, b, static b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
|
||||
if (aggregate)
|
||||
comparison = compareUsingAggregateMax(b, a, static b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
|
||||
else
|
||||
comparison = Nullable.Compare(b.LastPlayed, a.LastPlayed);
|
||||
break;
|
||||
|
||||
case SortMode.BPM:
|
||||
comparison = compareUsingAggregateMax(a, b, static b => b.BPM);
|
||||
if (aggregate)
|
||||
comparison = compareUsingAggregateMax(a, b, static b => b.BPM);
|
||||
else
|
||||
comparison = a.BPM.CompareTo(b.BPM);
|
||||
break;
|
||||
|
||||
case SortMode.Length:
|
||||
comparison = compareUsingAggregateMax(a, b, static b => b.Length);
|
||||
if (aggregate)
|
||||
comparison = compareUsingAggregateMax(a, b, static b => b.Length);
|
||||
else
|
||||
comparison = a.Length.CompareTo(b.Length);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -108,7 +125,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
return comparison;
|
||||
}
|
||||
|
||||
private static int compareDifficulty(BeatmapInfo a, BeatmapInfo b)
|
||||
private static int compareDifficulty(BeatmapInfo a, BeatmapInfo b, SortMode sort)
|
||||
{
|
||||
int comparison = a.Ruleset.CompareTo(b.Ruleset);
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osuTK;
|
||||
|
||||
@@ -28,10 +30,12 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public IBindable<BeatmapLeaderboardScope> Scope => scopeDropdown.Current;
|
||||
|
||||
private readonly Bindable<BeatmapDetailTab> configDetailTab = new Bindable<BeatmapDetailTab>();
|
||||
|
||||
public IBindable<bool> FilterBySelectedMods => selectedModsToggle.Active;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@@ -98,18 +102,95 @@ namespace osu.Game.Screens.SelectV2
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
config.BindWith(OsuSetting.BeatmapDetailTab, configDetailTab);
|
||||
config.BindWith(OsuSetting.BeatmapDetailModsFilter, selectedModsToggle.Active);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
scopeDropdown.Current.Value = tryMapDetailTabToLeaderboardScope(configDetailTab.Value) ?? scopeDropdown.Current.Value;
|
||||
scopeDropdown.Current.BindValueChanged(_ => updateConfigDetailTab());
|
||||
|
||||
tabControl.Current.Value = configDetailTab.Value == BeatmapDetailTab.Details ? Selection.Details : Selection.Ranking;
|
||||
tabControl.Current.BindValueChanged(v =>
|
||||
{
|
||||
leaderboardControls.FadeTo(v.NewValue == Selection.Ranking ? 1 : 0, 300, Easing.OutQuint);
|
||||
updateConfigDetailTab();
|
||||
}, true);
|
||||
}
|
||||
|
||||
#region Reading / writing state from / to configuration
|
||||
|
||||
private void updateConfigDetailTab()
|
||||
{
|
||||
switch (tabControl.Current.Value)
|
||||
{
|
||||
case Selection.Details:
|
||||
configDetailTab.Value = BeatmapDetailTab.Details;
|
||||
return;
|
||||
|
||||
case Selection.Ranking:
|
||||
configDetailTab.Value = mapLeaderboardScopeToDetailTab(scopeDropdown.Current.Value);
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(tabControl.Current.Value), tabControl.Current.Value, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static BeatmapLeaderboardScope? tryMapDetailTabToLeaderboardScope(BeatmapDetailTab tab)
|
||||
{
|
||||
switch (tab)
|
||||
{
|
||||
case BeatmapDetailTab.Local:
|
||||
return BeatmapLeaderboardScope.Local;
|
||||
|
||||
case BeatmapDetailTab.Country:
|
||||
return BeatmapLeaderboardScope.Country;
|
||||
|
||||
case BeatmapDetailTab.Global:
|
||||
return BeatmapLeaderboardScope.Global;
|
||||
|
||||
case BeatmapDetailTab.Friends:
|
||||
return BeatmapLeaderboardScope.Friend;
|
||||
|
||||
case BeatmapDetailTab.Team:
|
||||
return BeatmapLeaderboardScope.Team;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static BeatmapDetailTab mapLeaderboardScopeToDetailTab(BeatmapLeaderboardScope scope)
|
||||
{
|
||||
switch (scope)
|
||||
{
|
||||
case BeatmapLeaderboardScope.Local:
|
||||
return BeatmapDetailTab.Local;
|
||||
|
||||
case BeatmapLeaderboardScope.Country:
|
||||
return BeatmapDetailTab.Country;
|
||||
|
||||
case BeatmapLeaderboardScope.Global:
|
||||
return BeatmapDetailTab.Global;
|
||||
|
||||
case BeatmapLeaderboardScope.Friend:
|
||||
return BeatmapDetailTab.Friends;
|
||||
|
||||
case BeatmapLeaderboardScope.Team:
|
||||
return BeatmapDetailTab.Team;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(scope), scope, null);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public enum Selection
|
||||
{
|
||||
Details,
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
private const float username_min_width = 120;
|
||||
private const float statistics_regular_min_width = 165;
|
||||
private const float statistics_compact_min_width = 90;
|
||||
private const float rank_label_width = 60;
|
||||
private const float rank_label_width = 40;
|
||||
|
||||
private const int corner_radius = 10;
|
||||
private const int transition_duration = 200;
|
||||
@@ -117,6 +117,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private readonly bool sheared;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
var inputRectangle = DrawRectangle;
|
||||
|
||||
inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapLeaderboardWedge.SPACING_BETWEEN_SCORES / 2 });
|
||||
|
||||
return inputRectangle.Contains(ToLocalSpace(screenSpacePos));
|
||||
}
|
||||
|
||||
public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true)
|
||||
{
|
||||
this.score = score;
|
||||
@@ -144,6 +153,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
Alpha = 0.4f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = backgroundColour
|
||||
},
|
||||
@@ -190,6 +200,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
foreground = new Box
|
||||
{
|
||||
Alpha = 0.4f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = foregroundColour
|
||||
},
|
||||
@@ -312,8 +323,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
Child = statisticsContainer = new FillFlowContainer
|
||||
{
|
||||
Name = @"Statistics container",
|
||||
Padding = new MarginPadding { Right = 40 },
|
||||
Spacing = new Vector2(25, 0),
|
||||
Padding = new MarginPadding { Right = 10 },
|
||||
Spacing = new Vector2(20, 0),
|
||||
Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
@@ -567,13 +578,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private DisplayMode getCurrentDisplayMode()
|
||||
{
|
||||
if (DrawWidth >= HEIGHT + username_min_width + statistics_regular_min_width + expanded_right_content_width + rank_label_width)
|
||||
if (DrawWidth >= username_min_width + statistics_regular_min_width + expanded_right_content_width + rank_label_width)
|
||||
return DisplayMode.Full;
|
||||
|
||||
if (DrawWidth >= HEIGHT + username_min_width + statistics_regular_min_width + expanded_right_content_width)
|
||||
if (DrawWidth >= username_min_width + statistics_regular_min_width + expanded_right_content_width)
|
||||
return DisplayMode.Regular;
|
||||
|
||||
if (DrawWidth >= HEIGHT + username_min_width + statistics_compact_min_width + expanded_right_content_width)
|
||||
if (DrawWidth >= username_min_width + statistics_compact_min_width + expanded_right_content_width)
|
||||
return DisplayMode.Compact;
|
||||
|
||||
return DisplayMode.Minimal;
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.PolygonExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
@@ -27,11 +29,14 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapLeaderboardWedge : VisibilityContainer
|
||||
{
|
||||
public const float SPACING_BETWEEN_SCORES = 4;
|
||||
|
||||
public IBindable<BeatmapLeaderboardScope> Scope { get; } = new Bindable<BeatmapLeaderboardScope>();
|
||||
|
||||
public IBindable<bool> FilterBySelectedMods { get; } = new BindableBool();
|
||||
@@ -70,7 +75,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private readonly IBindable<LeaderboardScores?> fetchedScores = new Bindable<LeaderboardScores?>();
|
||||
|
||||
private const float personal_best_height = 80;
|
||||
private const float personal_best_height = 100;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@@ -109,7 +114,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = personal_best_height,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Margin = new MarginPadding { Left = -40f },
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Left = -40f,
|
||||
},
|
||||
CornerRadius = 10f,
|
||||
Masking = true,
|
||||
// push the personal best 1px down to hide masking issues
|
||||
@@ -118,11 +126,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
Alpha = 0f,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
new WedgeBackground(),
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
@@ -256,10 +260,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
foreach (var d in loadedScores)
|
||||
{
|
||||
d.Y = (BeatmapLeaderboardScore.HEIGHT + 4f) * i;
|
||||
d.Y = (BeatmapLeaderboardScore.HEIGHT + SPACING_BETWEEN_SCORES) * i;
|
||||
|
||||
// This is a bit of a weird one. We're already in a sheared state and don't want top-level
|
||||
// shear applied, but still need the `BeatmapLeadeboardScore` to be in "sheared" mode (see ctor).
|
||||
// shear applied, but still need the `BeatmapLeaderboardScore` to be in "sheared" mode (see ctor).
|
||||
d.Shear = Vector2.Zero;
|
||||
|
||||
scoresContainer.Add(d);
|
||||
@@ -352,6 +356,59 @@ namespace osu.Game.Screens.SelectV2
|
||||
placeholder.FadeInFromZero(300, Easing.OutQuint);
|
||||
}
|
||||
|
||||
#region Fade handling
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
const int height = BeatmapLeaderboardScore.HEIGHT;
|
||||
|
||||
float fadeBottom = (float)(scoresScroll.Current + scoresScroll.DrawHeight);
|
||||
float fadeTop = (float)(scoresScroll.Current);
|
||||
|
||||
if (!scoresScroll.IsScrolledToStart())
|
||||
fadeTop += height;
|
||||
|
||||
foreach (var c in scoresContainer)
|
||||
{
|
||||
float topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scoresContainer).Y;
|
||||
float bottomY = topY + height;
|
||||
|
||||
bool requireBottomFade = bottomY >= fadeBottom;
|
||||
bool requireTopFade = topY < fadeTop;
|
||||
|
||||
if (!requireBottomFade && !requireTopFade)
|
||||
{
|
||||
c.Colour = Color4.White;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topY > fadeBottom + height || bottomY < fadeTop - height)
|
||||
{
|
||||
c.Colour = Color4.Transparent;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (requireBottomFade)
|
||||
{
|
||||
c.Colour = ColourInfo.GradientVertical(
|
||||
Color4.White.Opacity(Math.Min(1 - (topY - fadeBottom) / height, 1)),
|
||||
Color4.White.Opacity(Math.Min(1 - (bottomY - fadeBottom) / height, 1)));
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert(requireTopFade);
|
||||
|
||||
c.Colour = ColourInfo.GradientVertical(
|
||||
Color4.White.Opacity(Math.Min(1 - (fadeTop - topY) / height, 1)),
|
||||
Color4.White.Opacity(Math.Min(1 - (fadeTop - bottomY) / height, 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Placeholder? getPlaceholderFor(LeaderboardState state)
|
||||
{
|
||||
switch (state)
|
||||
|
||||
@@ -253,7 +253,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
mapperText.Text = beatmap.Value.Metadata.Author.Username;
|
||||
}
|
||||
|
||||
starRatingDisplay.Current = (Bindable<StarDifficulty>)difficultyCache.GetBindableDifficulty(beatmap.Value.BeatmapInfo, cancellationSource.Token, 200);
|
||||
starRatingDisplay.Current = (Bindable<StarDifficulty>)difficultyCache.GetBindableDifficulty(beatmap.Value.BeatmapInfo, cancellationSource.Token, SongSelect.SELECTION_DEBOUNCE);
|
||||
|
||||
updateCountStatistics(cancellationSource.Token);
|
||||
updateDifficultyStatistics();
|
||||
|
||||
@@ -17,7 +17,6 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
@@ -142,9 +141,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(maxSize: 210),
|
||||
new Dimension(maxSize: 180),
|
||||
new Dimension(GridSizeMode.Absolute, 5),
|
||||
new Dimension(maxSize: 230),
|
||||
new Dimension(maxSize: 180),
|
||||
new Dimension(GridSizeMode.Absolute, 5),
|
||||
new Dimension(),
|
||||
},
|
||||
@@ -152,14 +151,14 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
new[]
|
||||
{
|
||||
sortDropdown = new ShearedDropdown<SortMode>(SortStrings.Default)
|
||||
sortDropdown = new ShearedDropdown<SortMode>("Sort")
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = Enum.GetValues<SortMode>(),
|
||||
},
|
||||
Empty(),
|
||||
// todo: pending localisation
|
||||
groupDropdown = new ShearedDropdown<GroupMode>("Group by")
|
||||
groupDropdown = new ShearedDropdown<GroupMode>("Group")
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = Enum.GetValues<GroupMode>(),
|
||||
|
||||
@@ -68,6 +68,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
foreach (OsuMenuItem item in SongSelect.GetForwardActions(beatmap.BeatmapInfo))
|
||||
{
|
||||
// We can't display menus with child items here, so just ignore them.
|
||||
if (item.Items.Any())
|
||||
continue;
|
||||
|
||||
if (item is OsuMenuItemSpacer)
|
||||
{
|
||||
buttonFlow.Add(new Container
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -20,10 +21,14 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class NoResultsPlaceholder : VisibilityContainer
|
||||
{
|
||||
public Action? RequestClearFilterText { get; init; }
|
||||
|
||||
private FilterCriteria? filter;
|
||||
|
||||
private LinkFlowContainer textFlow = null!;
|
||||
|
||||
private SpriteIcon icon = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
@@ -50,8 +55,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Width = 400;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
@@ -61,11 +65,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 300,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
icon = new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.Solid.Ghost,
|
||||
Anchor = Anchor.TopCentre,
|
||||
@@ -78,7 +84,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Font = OsuFont.Style.Title,
|
||||
Text = "No beatmaps found"
|
||||
Text = "No matching beatmaps"
|
||||
},
|
||||
textFlow = new LinkFlowContainer
|
||||
{
|
||||
@@ -115,6 +121,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
this.ScaleTo(0.9f)
|
||||
.ScaleTo(1f, 1000, Easing.OutQuint);
|
||||
|
||||
icon.ScaleTo(new Vector2(-1, 1))
|
||||
.ScaleTo(new Vector2(1, 1), 500, Easing.InOutSine);
|
||||
|
||||
textFlow.FadeInFromZero(800, Easing.OutQuint);
|
||||
|
||||
textFlow.Clear();
|
||||
@@ -131,6 +140,18 @@ namespace osu.Game.Screens.SelectV2
|
||||
textFlow.AddParagraph("No beatmaps match your filter criteria!");
|
||||
textFlow.AddParagraph(string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(filter?.SearchText))
|
||||
{
|
||||
addBulletPoint();
|
||||
textFlow.AddText("Try ");
|
||||
textFlow.AddLink("clearing", () =>
|
||||
{
|
||||
RequestClearFilterText?.Invoke();
|
||||
});
|
||||
|
||||
textFlow.AddText(" your current search criteria.");
|
||||
}
|
||||
|
||||
if (filter?.UserStarDifficulty.HasFilter == true)
|
||||
{
|
||||
addBulletPoint();
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
private const float corner_radius = 10;
|
||||
|
||||
private const float active_x_offset = 50f;
|
||||
private const float active_x_offset = 25f;
|
||||
|
||||
protected const float DURATION = 400;
|
||||
|
||||
@@ -34,7 +34,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private Box backgroundBorder = null!;
|
||||
private Box backgroundGradient = null!;
|
||||
private Box backgroundAccentGradient = null!;
|
||||
private Container backgroundLayerHorizontalPadding = null!;
|
||||
private Container backgroundContainer = null!;
|
||||
private Container iconContainer = null!;
|
||||
@@ -65,7 +64,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
set
|
||||
{
|
||||
accentColour = value;
|
||||
updateDisplay();
|
||||
updateAccentColour();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,8 +94,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Offset = new Vector2(1f),
|
||||
Radius = 10,
|
||||
Hollow = true,
|
||||
Radius = 2,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@@ -108,7 +107,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
backgroundBorder = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White,
|
||||
Colour = Color4.Black,
|
||||
},
|
||||
backgroundLayerHorizontalPadding = new Container
|
||||
{
|
||||
@@ -124,10 +123,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
backgroundAccentGradient = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
backgroundContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@@ -158,10 +153,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
selectionLayer = new Box
|
||||
{
|
||||
Alpha = 0,
|
||||
Colour = ColourInfo.GradientHorizontal(colours.BlueDark.Opacity(0), colours.BlueDark.Opacity(0.6f)),
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.3f,
|
||||
Width = 0.6f,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
},
|
||||
@@ -190,15 +184,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Expanded.BindValueChanged(_ => updateDisplay(), true);
|
||||
|
||||
Selected.BindValueChanged(selected =>
|
||||
Expanded.BindValueChanged(_ =>
|
||||
{
|
||||
if (selected.NewValue)
|
||||
selectionLayer.FadeIn(100, Easing.OutQuint);
|
||||
else
|
||||
selectionLayer.FadeOut(200, Easing.OutQuint);
|
||||
updateSelectedState();
|
||||
updateXOffset();
|
||||
});
|
||||
|
||||
Selected.BindValueChanged(_ =>
|
||||
{
|
||||
updateSelectedState();
|
||||
updateXOffset();
|
||||
}, true);
|
||||
|
||||
@@ -217,6 +211,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
updateAccentColour();
|
||||
updateXOffset();
|
||||
|
||||
this.FadeIn(DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
@@ -236,18 +233,28 @@ namespace osu.Game.Screens.SelectV2
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
private void updateAccentColour()
|
||||
{
|
||||
var backgroundColour = accentColour ?? Color4.White;
|
||||
|
||||
backgroundBorder.Colour = backgroundColour;
|
||||
|
||||
selectionLayer.Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour.Opacity(0.5f));
|
||||
|
||||
updateSelectedState(animated: false);
|
||||
}
|
||||
|
||||
private void updateSelectedState(bool animated = true)
|
||||
{
|
||||
bool selectedOrExpanded = Expanded.Value || Selected.Value;
|
||||
|
||||
var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF");
|
||||
TopLevelContent.FadeEdgeEffectTo(selectedOrExpanded ? edgeEffectColour.Opacity(0.8f) : Color4.Black.Opacity(0.4f), animated ? DURATION : 0, Easing.OutQuint);
|
||||
|
||||
backgroundAccentGradient.FadeColour(ColourInfo.GradientHorizontal(backgroundColour.Opacity(0.25f), backgroundColour.Opacity(0f)), DURATION, Easing.OutQuint);
|
||||
backgroundBorder.FadeColour(backgroundColour, DURATION, Easing.OutQuint);
|
||||
|
||||
TopLevelContent.FadeEdgeEffectTo(Expanded.Value ? edgeEffectColour.Opacity(0.5f) : Color4.Black.Opacity(0.4f), DURATION, Easing.OutQuint);
|
||||
|
||||
updateXOffset();
|
||||
updateHover();
|
||||
if (selectedOrExpanded)
|
||||
selectionLayer.FadeIn(100, Easing.OutQuint);
|
||||
else
|
||||
selectionLayer.FadeOut(200, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateXOffset()
|
||||
@@ -255,31 +262,28 @@ namespace osu.Game.Screens.SelectV2
|
||||
float x = PanelXOffset + corner_radius;
|
||||
|
||||
if (!Expanded.Value && !Selected.Value)
|
||||
x += active_x_offset;
|
||||
{
|
||||
if (this is PanelBeatmap)
|
||||
x += active_x_offset * 2;
|
||||
else
|
||||
x += active_x_offset * 4;
|
||||
}
|
||||
|
||||
if (!KeyboardSelected.Value)
|
||||
x += active_x_offset * 0.5f;
|
||||
x += active_x_offset;
|
||||
|
||||
TopLevelContent.MoveToX(x, DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void updateHover()
|
||||
{
|
||||
if (IsHovered)
|
||||
hoverLayer.FadeIn(100, Easing.OutQuint);
|
||||
else
|
||||
hoverLayer.FadeOut(1000, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateHover();
|
||||
hoverLayer.FadeIn(100, Easing.OutQuint);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateHover();
|
||||
hoverLayer.FadeOut(1000, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,16 @@ using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@@ -36,10 +40,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
private PanelLocalRankDisplay localRank = null!;
|
||||
private OsuSpriteText difficultyText = null!;
|
||||
private OsuSpriteText authorText = null!;
|
||||
private FillFlowContainer mainFill = null!;
|
||||
|
||||
private IBindable<StarDifficulty>? starDifficultyBindable;
|
||||
private CancellationTokenSource? starDifficultyCancellationSource;
|
||||
|
||||
private Box backgroundAccentGradient = null!;
|
||||
|
||||
private TrianglesV2 triangles = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
@@ -58,6 +67,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
[Resolved]
|
||||
private ISongSelect? songSelect { get; set; }
|
||||
|
||||
public PanelBeatmap()
|
||||
{
|
||||
PanelXOffset = 60;
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
var inputRectangle = TopLevelContent.DrawRectangle;
|
||||
@@ -78,80 +92,108 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
Icon = difficultyIcon = new ConstrainedIconContainer
|
||||
{
|
||||
Size = new Vector2(16f),
|
||||
Margin = new MarginPadding { Horizontal = 5f },
|
||||
Size = new Vector2(9f),
|
||||
Margin = new MarginPadding { Left = 2.5f, Right = 1.5f },
|
||||
Colour = colourProvider.Background5,
|
||||
};
|
||||
|
||||
Content.Children = new[]
|
||||
Background = new Container
|
||||
{
|
||||
new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Padding = new MarginPadding { Left = 10f },
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
backgroundAccentGradient = new Box
|
||||
{
|
||||
new FillFlowContainer
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
triangles = new TrianglesV2
|
||||
{
|
||||
ScaleAdjust = 1.2f,
|
||||
Thickness = 0.01f,
|
||||
Velocity = 0.3f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
Content.Child = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Spacing = new Vector2(3),
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
localRank = new PanelLocalRankDisplay
|
||||
{
|
||||
Scale = new Vector2(0.8f),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
},
|
||||
mainFill = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(3, 0),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
new FillFlowContainer
|
||||
{
|
||||
localRank = new PanelLocalRankDisplay
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Bottom = 4 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.65f)
|
||||
},
|
||||
starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small, animated: true)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.875f),
|
||||
},
|
||||
starCounter = new StarCounter
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.4f)
|
||||
keyCountText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
},
|
||||
difficultyText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 3f },
|
||||
},
|
||||
authorText = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
keyCountText = new OsuSpriteText
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(3),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small, animated: true)
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.875f),
|
||||
},
|
||||
starCounter = new StarCounter
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.4f)
|
||||
}
|
||||
},
|
||||
difficultyText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 3f },
|
||||
},
|
||||
authorText = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -210,7 +252,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE);
|
||||
starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true);
|
||||
starDifficultyBindable.BindValueChanged(starDifficulty =>
|
||||
{
|
||||
starRatingDisplay.Current.Value = starDifficulty.NewValue;
|
||||
starCounter.Current = (float)starDifficulty.NewValue.Stars;
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@@ -225,7 +271,21 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
// Dirty hack to make sure we don't take up spacing in parent fill flow when not displaying a rank.
|
||||
// I can't find a better way to do this.
|
||||
starRatingDisplay.Margin = new MarginPadding { Left = 1 / starRatingDisplay.Scale.X * (localRank.HasRank ? 0 : -3) };
|
||||
mainFill.Margin = new MarginPadding { Left = 1 / starRatingDisplay.Scale.X * (localRank.HasRank ? 0 : -3) };
|
||||
|
||||
var diffColour = starRatingDisplay.DisplayedDifficultyColour;
|
||||
|
||||
if (AccentColour != diffColour)
|
||||
{
|
||||
AccentColour = diffColour;
|
||||
starCounter.Colour = diffColour;
|
||||
|
||||
backgroundAccentGradient.Colour = ColourInfo.GradientHorizontal(diffColour.Opacity(0.25f), diffColour.Opacity(0f));
|
||||
|
||||
difficultyIcon.Colour = starRatingDisplay.DisplayedStars.Value > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5;
|
||||
|
||||
triangles.Colour = ColourInfo.GradientVertical(diffColour.Opacity(0.25f), diffColour.Opacity(0f));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateKeyCount()
|
||||
@@ -249,22 +309,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
keyCountText.Alpha = 0;
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
const float duration = 500;
|
||||
|
||||
var starDifficulty = starDifficultyBindable?.Value ?? default;
|
||||
|
||||
starRatingDisplay.Current.Value = starDifficulty;
|
||||
starCounter.Current = (float)starDifficulty.Stars;
|
||||
|
||||
difficultyIcon.FadeColour(starDifficulty.Stars > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
|
||||
|
||||
var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars);
|
||||
starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint);
|
||||
AccentColour = starRatingColour;
|
||||
}
|
||||
|
||||
public override MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
|
||||
@@ -14,6 +14,8 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@@ -188,6 +190,12 @@ namespace osu.Game.Screens.SelectV2
|
||||
difficultiesDisplay.BeatmapSet = null;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
|
||||
|
||||
public override MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
@@ -215,6 +223,17 @@ namespace osu.Game.Screens.SelectV2
|
||||
items.Add(new OsuMenuItemSpacer());
|
||||
}
|
||||
|
||||
var collectionItems = realm.Realm.All<BeatmapCollection>()
|
||||
.OrderBy(c => c.Name)
|
||||
.AsEnumerable()
|
||||
.Select(createCollectionMenuItem)
|
||||
.ToList();
|
||||
|
||||
if (manageCollectionsDialog != null)
|
||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||
|
||||
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
|
||||
|
||||
if (beatmapSet.Beatmaps.Any(b => b.Hidden))
|
||||
items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => songSelect?.RestoreAllHidden(beatmapSet)));
|
||||
|
||||
@@ -222,5 +241,51 @@ namespace osu.Game.Screens.SelectV2
|
||||
return items.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private MenuItem createCollectionMenuItem(BeatmapCollection collection)
|
||||
{
|
||||
var beatmapSet = (BeatmapSetInfo)Item!.Model;
|
||||
|
||||
Debug.Assert(beatmapSet != null);
|
||||
|
||||
TernaryState state;
|
||||
|
||||
int countExisting = beatmapSet.Beatmaps.Count(b => collection.BeatmapMD5Hashes.Contains(b.MD5Hash));
|
||||
|
||||
if (countExisting == beatmapSet.Beatmaps.Count)
|
||||
state = TernaryState.True;
|
||||
else if (countExisting > 0)
|
||||
state = TernaryState.Indeterminate;
|
||||
else
|
||||
state = TernaryState.False;
|
||||
|
||||
var liveCollection = collection.ToLive(realm);
|
||||
|
||||
return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
|
||||
{
|
||||
liveCollection.PerformWrite(c =>
|
||||
{
|
||||
foreach (var b in beatmapSet.Beatmaps)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
{
|
||||
State = { Value = state }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,13 +63,29 @@ namespace osu.Game.Screens.SelectV2
|
||||
private BeatmapSetOnlineStatusPill statusPill = null!;
|
||||
|
||||
private ConstrainedIconContainer difficultyIcon = null!;
|
||||
private FillFlowContainer difficultyLine = null!;
|
||||
private StarRatingDisplay starRatingDisplay = null!;
|
||||
private StarCounter starCounter = null!;
|
||||
private PanelLocalRankDisplay localRank = null!;
|
||||
private OsuSpriteText keyCountText = null!;
|
||||
private OsuSpriteText difficultyText = null!;
|
||||
private OsuSpriteText authorText = null!;
|
||||
private FillFlowContainer mainFill = null!;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
var inputRectangle = TopLevelContent.DrawRectangle;
|
||||
|
||||
if (Selected.Value)
|
||||
{
|
||||
// Cover the gaps introduced by the spacing between BeatmapPanels so that clicks will not fall through the carousel.
|
||||
//
|
||||
// Caveat is that for simplicity, we are covering the full spacing, so panels with frontmost depth will have a slightly
|
||||
// larger hit target.
|
||||
inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING * 2 });
|
||||
}
|
||||
|
||||
return inputRectangle.Contains(TopLevelContent.ToLocalSpace(screenSpacePos));
|
||||
}
|
||||
|
||||
public PanelBeatmapStandalone()
|
||||
{
|
||||
@@ -83,8 +99,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
Icon = difficultyIcon = new ConstrainedIconContainer
|
||||
{
|
||||
Size = new Vector2(16),
|
||||
Margin = new MarginPadding { Horizontal = 5f },
|
||||
Size = new Vector2(12),
|
||||
Margin = new MarginPadding { Left = 4f, Right = 3f },
|
||||
Colour = colourProvider.Background5,
|
||||
};
|
||||
|
||||
@@ -95,93 +111,105 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
Content.Child = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Padding = new MarginPadding { Left = 10f },
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(3),
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
titleText = new OsuSpriteText
|
||||
localRank = new PanelLocalRankDisplay
|
||||
{
|
||||
Font = OsuFont.Style.Heading2.With(typeface: Typeface.TorusAlternate, weight: FontWeight.Bold),
|
||||
Scale = new Vector2(0.8f),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
},
|
||||
artistText = new OsuSpriteText
|
||||
mainFill = new FillFlowContainer
|
||||
{
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Padding = new MarginPadding { Top = -2 },
|
||||
},
|
||||
difficultyLine = new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Direction = FillDirection.Vertical,
|
||||
Padding = new MarginPadding { Bottom = 2 },
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Top = 4 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
statusPill = new BeatmapSetOnlineStatusPill
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Animated = false,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
TextSize = OsuFont.Style.Caption2.Size,
|
||||
Margin = new MarginPadding { Right = 5f },
|
||||
Font = OsuFont.Style.Heading2.With(typeface: Typeface.TorusAlternate, weight: FontWeight.Bold),
|
||||
},
|
||||
updateButton = new PanelUpdateBeatmapButton
|
||||
artistText = new OsuSpriteText
|
||||
{
|
||||
Scale = new Vector2(0.7f),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Right = 5f, Top = -2f },
|
||||
},
|
||||
keyCountText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
},
|
||||
difficultyText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 3f },
|
||||
},
|
||||
authorText = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft
|
||||
Padding = new MarginPadding { Top = -2 },
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Top = 2, Bottom = 2 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
statusPill = new BeatmapSetOnlineStatusPill
|
||||
{
|
||||
Animated = false,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
TextSize = OsuFont.Style.Caption2.Size,
|
||||
Margin = new MarginPadding { Right = 4f },
|
||||
},
|
||||
updateButton = new PanelUpdateBeatmapButton
|
||||
{
|
||||
Scale = new Vector2(0.8f),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 4f, Bottom = -1f },
|
||||
},
|
||||
keyCountText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Alpha = 0,
|
||||
},
|
||||
difficultyText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 3f },
|
||||
},
|
||||
authorText = new OsuSpriteText
|
||||
{
|
||||
Colour = colourProvider.Content2,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft
|
||||
}
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(3),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small, animated: true)
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.875f),
|
||||
},
|
||||
starCounter = new StarCounter
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.4f)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(3),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
localRank = new PanelLocalRankDisplay
|
||||
{
|
||||
Scale = new Vector2(0.65f),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
},
|
||||
starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small, animated: true)
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.875f),
|
||||
},
|
||||
starCounter = new StarCounter
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.4f)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -229,9 +257,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
localRank.Beatmap = beatmap;
|
||||
difficultyText.Text = beatmap.DifficultyName;
|
||||
authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username);
|
||||
difficultyLine.Show();
|
||||
|
||||
computeStarRating();
|
||||
updateKeyCount();
|
||||
}
|
||||
|
||||
protected override void FreeAfterUse()
|
||||
@@ -257,7 +285,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
var beatmap = (BeatmapInfo)Item.Model;
|
||||
|
||||
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE);
|
||||
starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true);
|
||||
starDifficultyBindable.BindValueChanged(starDifficulty =>
|
||||
{
|
||||
starRatingDisplay.Current.Value = starDifficulty.NewValue;
|
||||
starCounter.Current = (float)starDifficulty.NewValue.Stars;
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@@ -272,7 +304,14 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
// Dirty hack to make sure we don't take up spacing in parent fill flow when not displaying a rank.
|
||||
// I can't find a better way to do this.
|
||||
starRatingDisplay.Margin = new MarginPadding { Left = 1 / starRatingDisplay.Scale.X * (localRank.HasRank ? 0 : -3) };
|
||||
mainFill.Margin = new MarginPadding { Left = 1 / starRatingDisplay.Scale.X * (localRank.HasRank ? 0 : -3) };
|
||||
|
||||
var diffColour = starRatingDisplay.DisplayedDifficultyColour;
|
||||
|
||||
AccentColour = diffColour;
|
||||
starCounter.Colour = diffColour;
|
||||
|
||||
difficultyIcon.Colour = starRatingDisplay.DisplayedStars.Value > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5;
|
||||
}
|
||||
|
||||
private void updateKeyCount()
|
||||
@@ -296,22 +335,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
keyCountText.Alpha = 0;
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
const float duration = 500;
|
||||
|
||||
var starDifficulty = starDifficultyBindable?.Value ?? default;
|
||||
|
||||
starRatingDisplay.Current.Value = starDifficulty;
|
||||
starCounter.Current = (float)starDifficulty.Stars;
|
||||
|
||||
difficultyIcon.FadeColour(starDifficulty.Stars > OsuColour.STAR_DIFFICULTY_DEFINED_COLOUR_CUTOFF ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint);
|
||||
|
||||
var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars);
|
||||
starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint);
|
||||
AccentColour = colours.ForStarDifficulty(starDifficulty.Stars);
|
||||
}
|
||||
|
||||
public override MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -61,34 +62,27 @@ namespace osu.Game.Screens.SelectV2
|
||||
Direction = FillDirection.Horizontal,
|
||||
// This makes the gradient not be perfectly horizontal, but diagonal at a ~40° angle
|
||||
Shear = new Vector2(0.8f, 0),
|
||||
Alpha = 0.5f,
|
||||
Children = new[]
|
||||
{
|
||||
// The left half with no gradient applied
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
Colour = Color4.Black.Opacity(0.5f),
|
||||
Width = 0.4f,
|
||||
},
|
||||
// Piecewise-linear gradient with 3 segments to make it appear smoother
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)),
|
||||
Width = 0.05f,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)),
|
||||
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.5f), Color4.Black.Opacity(0.3f)),
|
||||
Width = 0.2f,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)),
|
||||
Width = 0.05f,
|
||||
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.3f), Color4.Black.Opacity(0.2f)),
|
||||
// Slightly more than 1.0 in total to account for shear.
|
||||
Width = 0.45f,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -73,6 +73,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
yield return new OsuMenuItemSpacer();
|
||||
}
|
||||
|
||||
foreach (var i in CreateCollectionMenuActions(beatmap))
|
||||
yield return i;
|
||||
|
||||
// TODO: replace with "remove from played" button when beatmap is already played.
|
||||
yield return new OsuMenuItem(SongSelectStrings.MarkAsPlayed, MenuItemType.Standard, () => beatmaps.MarkPlayed(beatmap)) { Icon = FontAwesome.Solid.TimesCircle };
|
||||
yield return new OsuMenuItem(SongSelectStrings.ClearAllLocalScores, MenuItemType.Standard, () => dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmap)))
|
||||
|
||||
@@ -21,6 +21,7 @@ using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Carousel;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
@@ -70,15 +71,20 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// </summary>
|
||||
protected bool ControlGlobalMusic { get; init; } = true;
|
||||
|
||||
private readonly ModSelectOverlay modSelectOverlay = new UserModSelectOverlay(OverlayColourScheme.Aquamarine)
|
||||
// Colour scheme for mod overlay is left as default (green) to match mods button.
|
||||
// Not sure about this, but we'll iterate based on feedback.
|
||||
private readonly ModSelectOverlay modSelectOverlay = new UserModSelectOverlay
|
||||
{
|
||||
ShowPresets = true,
|
||||
};
|
||||
|
||||
private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!;
|
||||
|
||||
// Blue is the most neutral choice, so I'm using that for now.
|
||||
// Purple makes the most sense to match the "gameplay" flow, but it's a bit too strong for the current design.
|
||||
// TODO: Colour scheme choice should probably be customisable by the user.
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
private BeatmapCarousel carousel = null!;
|
||||
|
||||
@@ -146,9 +152,9 @@ namespace osu.Game.Screens.SelectV2
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 850),
|
||||
new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 660),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 750),
|
||||
new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 620),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
@@ -162,6 +168,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
// screen-wide scroll handling.
|
||||
Depth = float.MinValue,
|
||||
Shear = OsuGame.SHEAR,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = -CORNER_RADIUS_HIDE_OFFSET,
|
||||
Left = -CORNER_RADIUS_HIDE_OFFSET,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
@@ -177,11 +188,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
wedgesContainer = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Top = -CORNER_RADIUS_HIDE_OFFSET,
|
||||
Left = -CORNER_RADIUS_HIDE_OFFSET
|
||||
},
|
||||
Spacing = new Vector2(0f, 4f),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
@@ -196,8 +202,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new CompositeDrawable[]
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.0f), Color4.Black.Opacity(0.5f)),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@@ -218,7 +229,10 @@ namespace osu.Game.Screens.SelectV2
|
||||
RequestRecommendedSelection = selectRecommendedBeatmap,
|
||||
NewItemsPresented = newItemsPresented,
|
||||
},
|
||||
noResultsPlaceholder = new NoResultsPlaceholder(),
|
||||
noResultsPlaceholder = new NoResultsPlaceholder
|
||||
{
|
||||
RequestClearFilterText = () => filterControl.Search(string.Empty)
|
||||
}
|
||||
}
|
||||
},
|
||||
filterControl = new FilterControl
|
||||
@@ -401,6 +415,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
backgroundModeBeatmap.BlurAmount.Value = 0;
|
||||
backgroundModeBeatmap.Beatmap = beatmap;
|
||||
backgroundModeBeatmap.IgnoreUserSettings.Value = true;
|
||||
backgroundModeBeatmap.DimWhenUserSettingsIgnored.Value = 0.1f;
|
||||
backgroundModeBeatmap.FadeColour(Color4.White, 250);
|
||||
});
|
||||
}
|
||||
@@ -547,12 +562,18 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private void criteriaChanged(FilterCriteria criteria)
|
||||
{
|
||||
// The first filter needs to be applied immediately as this triggers the initial carousel load.
|
||||
double filterDelay = filterDebounce == null ? 0 : filter_delay;
|
||||
|
||||
filterDebounce?.Cancel();
|
||||
filterDebounce = Scheduler.AddDelayed(() => { carousel.Filter(criteria); }, filter_delay);
|
||||
filterDebounce = Scheduler.AddDelayed(() => { carousel.Filter(criteria); }, filterDelay);
|
||||
}
|
||||
|
||||
private void newItemsPresented(IEnumerable<CarouselItem> carouselItems)
|
||||
{
|
||||
if (carousel.Criteria == null)
|
||||
return;
|
||||
|
||||
int count = carousel.MatchedBeatmapsCount;
|
||||
|
||||
if (count == 0)
|
||||
@@ -658,6 +679,12 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
#region Beatmap management
|
||||
|
||||
[Resolved]
|
||||
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
public virtual IEnumerable<OsuMenuItem> GetForwardActions(BeatmapInfo beatmap)
|
||||
{
|
||||
yield return new OsuMenuItem("Select", MenuItemType.Highlighted, () => SelectAndStart(beatmap))
|
||||
@@ -674,6 +701,23 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (beatmap.GetOnlineURL(api, Ruleset.Value) is string url)
|
||||
yield return new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => (game as OsuGame)?.CopyToClipboard(url));
|
||||
}
|
||||
|
||||
yield return new OsuMenuItemSpacer();
|
||||
|
||||
foreach (var i in CreateCollectionMenuActions(beatmap))
|
||||
yield return i;
|
||||
}
|
||||
|
||||
protected IEnumerable<OsuMenuItem> CreateCollectionMenuActions(BeatmapInfo beatmap)
|
||||
{
|
||||
var collectionItems = realm.Realm.All<BeatmapCollection>()
|
||||
.OrderBy(c => c.Name)
|
||||
.AsEnumerable()
|
||||
.Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast<OsuMenuItem>().ToList();
|
||||
|
||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, () => manageCollectionsDialog?.Show()));
|
||||
|
||||
yield return new OsuMenuItem("Collections") { Items = collectionItems };
|
||||
}
|
||||
|
||||
public void ManageCollections() => collectionsDialog?.Show();
|
||||
|
||||
Reference in New Issue
Block a user