1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-11 05:17:25 +08:00

Implement beatmap set header design

This commit is contained in:
Salman Alshamrani 2025-02-05 07:15:53 -05:00
parent a5fa04e4d6
commit 206b5c93c0
7 changed files with 860 additions and 72 deletions

View File

@ -0,0 +1,90 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.UserInterface;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneBeatmapCarouselSetPanel : ThemeComparisonTestScene
{
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private BeatmapSetInfo beatmapSet = null!;
public TestSceneBeatmapCarouselSetPanel()
: base(false)
{
}
[Test]
public void TestDisplay()
{
AddStep("set beatmap", () =>
{
beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526)
?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected)
?? TestResources.CreateTestBeatmapSetInfo();
CreateThemedContent(OverlayColourScheme.Aquamarine);
});
}
[Test]
public void TestRandomBeatmap()
{
AddStep("random beatmap", () =>
{
beatmapSet = beatmaps.GetAllUsableBeatmapSets().OrderBy(_ => RNG.Next()).First();
CreateThemedContent(OverlayColourScheme.Aquamarine);
});
}
protected override Drawable CreateContent()
{
return new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 5f),
Children = new Drawable[]
{
new BeatmapSetPanel
{
Item = new CarouselItem(beatmapSet)
},
new BeatmapSetPanel
{
Item = new CarouselItem(beatmapSet),
KeyboardSelected = { Value = true }
},
new BeatmapSetPanel
{
Item = new CarouselItem(beatmapSet),
Expanded = { Value = true }
},
new BeatmapSetPanel
{
Item = new CarouselItem(beatmapSet),
KeyboardSelected = { Value = true },
Expanded = { Value = true }
},
}
};
}
}
}

View File

@ -0,0 +1,62 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene
{
private UpdateBeatmapSetButtonV2 button = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = button = new UpdateBeatmapSetButtonV2
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
});
[Test]
public void TestNullBeatmap()
{
AddStep("null beatmap", () => button.BeatmapSet = null);
AddAssert("button invisible", () => button.Alpha == 0f);
}
[Test]
public void TestUpdatedBeatmap()
{
AddStep("updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo
{
Beatmaps = { new BeatmapInfo() }
});
AddAssert("button invisible", () => button.Alpha == 0f);
}
[Test]
public void TestNonUpdatedBeatmap()
{
AddStep("non-updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo
{
Beatmaps =
{
new BeatmapInfo
{
MD5Hash = "test",
OnlineMD5Hash = "online",
LastOnlineUpdate = DateTimeOffset.Now,
}
}
});
AddAssert("button visible", () => button.Alpha == 1f);
}
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps.Drawables
dotSize = value;
if (IsLoaded)
updateDotDimensions();
updateDisplay();
}
}
@ -42,13 +42,27 @@ namespace osu.Game.Beatmaps.Drawables
dotSpacing = value;
if (IsLoaded)
updateDotDimensions();
updateDisplay();
}
}
private IBeatmapSetInfo? beatmapSet;
public IBeatmapSetInfo? BeatmapSet
{
get => beatmapSet;
set
{
beatmapSet = value;
if (IsLoaded)
updateDisplay();
}
}
private readonly FillFlowContainer<RulesetDifficultyGroup> flow;
public DifficultySpectrumDisplay(IBeatmapSetInfo beatmapSet)
public DifficultySpectrumDisplay(IBeatmapSetInfo? beatmapSet = null)
{
AutoSizeAxes = Axes.Both;
@ -59,25 +73,31 @@ namespace osu.Game.Beatmaps.Drawables
Direction = FillDirection.Horizontal,
};
// matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127
bool collapsed = beatmapSet.Beatmaps.Count() > 12;
foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key))
flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed));
BeatmapSet = beatmapSet;
}
protected override void LoadComplete()
{
base.LoadComplete();
updateDotDimensions();
updateDisplay();
}
private void updateDotDimensions()
private void updateDisplay()
{
foreach (var group in flow)
flow.Clear();
if (beatmapSet == null)
return;
// matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127
bool collapsed = beatmapSet.Beatmaps.Count() > 12;
foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key))
{
group.DotSize = DotSize;
group.DotSpacing = DotSpacing;
flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed, dotSize)
{
Spacing = new Vector2(DotSpacing, 0f),
});
}
}
@ -86,26 +106,14 @@ namespace osu.Game.Beatmaps.Drawables
private readonly int rulesetId;
private readonly IEnumerable<IBeatmapInfo> beatmapInfos;
private readonly bool collapsed;
private readonly Vector2 dotSize;
public RulesetDifficultyGroup(int rulesetId, IEnumerable<IBeatmapInfo> beatmapInfos, bool collapsed)
public RulesetDifficultyGroup(int rulesetId, IEnumerable<IBeatmapInfo> beatmapInfos, bool collapsed, Vector2 dotSize)
{
this.rulesetId = rulesetId;
this.beatmapInfos = beatmapInfos;
this.collapsed = collapsed;
}
public Vector2 DotSize
{
set
{
foreach (var dot in Children.OfType<DifficultyDot>())
dot.Size = value;
}
}
public float DotSpacing
{
set => Spacing = new Vector2(value, 0);
this.dotSize = dotSize;
}
[BackgroundDependencyLoader]
@ -125,7 +133,7 @@ namespace osu.Game.Beatmaps.Drawables
if (!collapsed)
{
foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating))
Add(new DifficultyDot(beatmapInfo.StarRating));
Add(new DifficultyDot(beatmapInfo.StarRating, dotSize));
}
else
{
@ -145,9 +153,10 @@ namespace osu.Game.Beatmaps.Drawables
{
private readonly double starDifficulty;
public DifficultyDot(double starDifficulty)
public DifficultyDot(double starDifficulty, Vector2 dotSize)
{
this.starDifficulty = starDifficulty;
Size = dotSize;
}
[BackgroundDependencyLoader]

View File

@ -3,15 +3,24 @@
using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@ -19,63 +28,182 @@ namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapSetPanel : PoolableDrawable, ICarouselPanel
{
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2;
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f;
private const float arrow_container_width = 20;
private const float corner_radius = 10;
private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel.
private const float set_x_offset = 20f; // constant X offset for beatmap set panels specifically.
private const float preselected_x_offset = 25f;
private const float expanded_x_offset = 50f;
private const float duration = 500;
[Resolved]
private BeatmapCarousel carousel { get; set; } = null!;
private BeatmapCarousel? carousel { get; set; }
private OsuSpriteText text = null!;
private Box box = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
var inputRectangle = DrawRectangle;
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
// Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either below/above it.
inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f });
[Resolved]
private OsuColour colours { get; set; } = null!;
return inputRectangle.Contains(ToLocalSpace(screenSpacePos));
}
private Container panel = null!;
private Box backgroundBorder = null!;
private BeatmapSetPanelBackground background = null!;
private Container backgroundContainer = null!;
private FillFlowContainer mainFlowContainer = null!;
private SpriteIcon chevronIcon = null!;
private Box hoverLayer = null!;
private OsuSpriteText titleText = null!;
private OsuSpriteText artistText = null!;
private UpdateBeatmapSetButtonV2 updateButton = null!;
private BeatmapSetOnlineStatusPill statusPill = null!;
private DifficultySpectrumDisplay difficultiesDisplay = null!;
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(500, HEIGHT);
Masking = true;
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
InternalChildren = new Drawable[]
InternalChild = panel = new Container
{
box = new Box
{
Colour = Color4.Yellow.Darken(5),
Alpha = 0.8f,
Masking = true,
CornerRadius = corner_radius,
RelativeSizeAxes = Axes.Both,
},
text = new OsuSpriteText
EdgeEffect = new EdgeEffectParameters
{
Padding = new MarginPadding(5),
Type = EdgeEffectType.Shadow,
Radius = 10,
},
Children = new Drawable[]
{
new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
backgroundBorder = new Box
{
RelativeSizeAxes = Axes.Y,
Alpha = 0,
EdgeSmoothness = new Vector2(2, 0),
},
backgroundContainer = new Container
{
Masking = true,
CornerRadius = corner_radius,
RelativeSizeAxes = Axes.X,
MaskingSmoothness = 2,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Children = new Drawable[]
{
background = new BeatmapSetPanelBackground
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
},
},
},
}
},
chevronIcon = new SpriteIcon
{
X = arrow_container_width / 2,
Origin = Anchor.Centre,
Anchor = Anchor.CentreLeft,
Icon = FontAwesome.Solid.ChevronRight,
Size = new Vector2(12),
Colour = colourProvider.Background5,
},
mainFlowContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 },
Children = new Drawable[]
{
titleText = new OsuSpriteText
{
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true),
Shadow = true,
},
artistText = new OsuSpriteText
{
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true),
Shadow = true,
},
new FillFlowContainer
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 5f },
Children = new Drawable[]
{
updateButton = new UpdateBeatmapSetButtonV2
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Right = 5f, Top = -2f },
},
statusPill = new BeatmapSetOnlineStatusPill
{
AutoSizeAxes = Axes.Both,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
TextSize = 11,
TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 },
Margin = new MarginPadding { Right = 5f },
},
difficultiesDisplay = new DifficultySpectrumDisplay
{
DotSize = new Vector2(5, 10),
DotSpacing = 2,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
},
}
}
},
hoverLayer = new Box
{
Colour = colours.Blue.Opacity(0.1f),
Alpha = 0,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
},
new HoverSounds(),
}
};
Expanded.BindValueChanged(value =>
{
box.FadeColour(value.NewValue ? Color4.Yellow.Darken(2) : Color4.Yellow.Darken(5), 500, Easing.OutQuint);
});
KeyboardSelected.BindValueChanged(value =>
{
if (value.NewValue)
{
BorderThickness = 5;
BorderColour = Color4.Pink;
}
else
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
BorderThickness = 0;
var inputRectangle = panel.DrawRectangle;
// Cover a gap introduced by the spacing between a BeatmapSetPanel and a BeatmapPanel either above it or below it.
inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f });
return inputRectangle.Contains(panel.ToLocalSpace(screenSpacePos));
}
});
protected override void LoadComplete()
{
base.LoadComplete();
Expanded.BindValueChanged(_ => updateExpandedDisplay(), true);
KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true);
}
protected override void PrepareForUse()
@ -84,16 +212,101 @@ namespace osu.Game.Screens.SelectV2
Debug.Assert(Item != null);
var beatmapSetInfo = (BeatmapSetInfo)Item.Model;
var beatmapSet = (BeatmapSetInfo)Item.Model;
text.Text = $"{beatmapSetInfo.Metadata}";
// Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set).
background.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID));
this.FadeInFromZero(500, Easing.OutQuint);
titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title);
artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist);
updateButton.BeatmapSet = beatmapSet;
statusPill.Status = beatmapSet.Status;
difficultiesDisplay.BeatmapSet = beatmapSet;
updateExpandedDisplay();
FinishTransforms(true);
this.FadeInFromZero(duration, Easing.OutQuint);
}
protected override void FreeAfterUse()
{
base.FreeAfterUse();
background.Beatmap = null;
updateButton.BeatmapSet = null;
difficultiesDisplay.BeatmapSet = null;
}
private void updateExpandedDisplay()
{
if (Item == null)
return;
updatePanelPosition();
backgroundBorder.RelativeSizeAxes = Expanded.Value ? Axes.Both : Axes.Y;
backgroundBorder.Width = Expanded.Value ? 1 : arrow_container_width + corner_radius;
backgroundBorder.FadeTo(Expanded.Value ? 1 : 0, duration, Easing.OutQuint);
chevronIcon.FadeTo(Expanded.Value ? 1 : 0, duration, Easing.OutQuint);
backgroundContainer.ResizeHeightTo(Expanded.Value ? HEIGHT - 4 : HEIGHT, duration, Easing.OutQuint);
backgroundContainer.MoveToX(Expanded.Value ? arrow_container_width : 0, duration, Easing.OutQuint);
mainFlowContainer.MoveToX(Expanded.Value ? arrow_container_width : 0, duration, Easing.OutQuint);
panel.EdgeEffect = panel.EdgeEffect with { Radius = Expanded.Value ? 15 : 10 };
panel.FadeEdgeEffectTo(Expanded.Value
? Color4Extensions.FromHex(@"4EBFFF").Opacity(0.5f)
: Color4.Black.Opacity(0.4f), duration, Easing.OutQuint);
}
private void updateKeyboardSelectedDisplay()
{
updatePanelPosition();
updateHover();
}
private void updatePanelPosition()
{
float x = glow_offset + set_x_offset + expanded_x_offset + preselected_x_offset;
if (Expanded.Value)
x -= expanded_x_offset;
if (KeyboardSelected.Value)
x -= preselected_x_offset;
this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint);
}
private void updateHover()
{
bool hovered = IsHovered || KeyboardSelected.Value;
if (hovered)
hoverLayer.FadeIn(100, Easing.OutQuint);
else
hoverLayer.FadeOut(1000, Easing.OutQuint);
}
protected override bool OnHover(HoverEvent e)
{
updateHover();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateHover();
base.OnHoverLost(e);
}
protected override bool OnClick(ClickEvent e)
{
if (carousel != null)
carousel.CurrentSelection = Item!.Model;
return true;
}

View File

@ -0,0 +1,108 @@
// 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 osu.Framework.Allocation;
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.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class BeatmapSetPanelBackground : ModelBackedDrawable<WorkingBeatmap>
{
protected override bool TransformImmediately => true;
public WorkingBeatmap? Beatmap
{
get => Model;
set => Model = value;
}
protected override Drawable CreateDrawable(WorkingBeatmap? model) => new BackgroundSprite(model);
private partial class BackgroundSprite : CompositeDrawable
{
private readonly WorkingBeatmap? working;
public BackgroundSprite(WorkingBeatmap? working)
{
this.working = working;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
var texture = working?.GetPanelBackground();
if (texture != null)
{
InternalChildren = new Drawable[]
{
new Sprite
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
Texture = texture,
},
new FillFlowContainer
{
Depth = -1,
RelativeSizeAxes = Axes.Both,
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,
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)),
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,
},
}
},
};
}
else
{
InternalChild = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
};
}
}
}
}
}

View File

@ -0,0 +1,108 @@
// 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 System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osuTK;
using Realms;
namespace osu.Game.Screens.SelectV2
{
public partial class TopLocalRankV2 : CompositeDrawable
{
private BeatmapInfo? beatmap;
public BeatmapInfo? Beatmap
{
get => beatmap;
set
{
beatmap = value;
if (IsLoaded)
updateSubscription();
}
}
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private IDisposable? scoreSubscription;
private readonly UpdateableRank updateable;
public ScoreRank? DisplayedRank => updateable.Rank;
public TopLocalRankV2(BeatmapInfo? beatmap = null)
{
AutoSizeAxes = Axes.Both;
InternalChild = updateable = new UpdateableRank
{
Size = new Vector2(40, 20),
Alpha = 0,
};
Beatmap = beatmap;
}
protected override void LoadComplete()
{
base.LoadComplete();
ruleset.BindValueChanged(_ => updateSubscription(), true);
}
private void updateSubscription()
{
scoreSubscription?.Dispose();
if (beatmap == null)
return;
scoreSubscription = realm.RegisterForNotifications(r =>
r.All<ScoreInfo>()
.Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0"
+ $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1"
+ $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}"
+ $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2"
+ $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmap.ID, ruleset.Value.ShortName),
localScoresChanged);
}
private void localScoresChanged(IRealmCollection<ScoreInfo> sender, ChangeSet? changes)
{
// This subscription may fire from changes to linked beatmaps, which we don't care about.
// It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications.
if (changes?.HasCollectionChanges() == false)
return;
ScoreInfo? topScore = sender.MaxBy(info => (info.TotalScore, -info.Date.UtcDateTime.Ticks));
updateable.Rank = topScore?.Rank;
updateable.Alpha = topScore != null ? 1 : 0;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
scoreSubscription?.Dispose();
}
}
}

View File

@ -0,0 +1,198 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Screens.Select.Carousel;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class UpdateBeatmapSetButtonV2 : OsuAnimatedButton
{
private BeatmapSetInfo? beatmapSet;
public BeatmapSetInfo? BeatmapSet
{
get => beatmapSet;
set
{
beatmapSet = value;
if (IsLoaded)
beatmapChanged();
}
}
private SpriteIcon icon = null!;
private Box progressFill = null!;
[Resolved]
private BeatmapModelDownloader beatmapDownloader { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private LoginOverlay? loginOverlay { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
public UpdateBeatmapSetButtonV2()
{
Size = new Vector2(75f, 22f);
}
private Bindable<bool> preferNoVideo = null!;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
const float icon_size = 14;
preferNoVideo = config.GetBindable<bool>(OsuSetting.PreferNoVideo);
Content.Anchor = Anchor.Centre;
Content.Origin = Anchor.Centre;
Content.Shear = new Vector2(OsuGame.SHEAR, 0);
Content.AddRange(new Drawable[]
{
progressFill = new Box
{
Colour = Color4.White,
Alpha = 0.2f,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Width = 0,
},
new FillFlowContainer
{
Padding = new MarginPadding { Horizontal = 5, Vertical = 3 },
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4),
Shear = new Vector2(-OsuGame.SHEAR, 0),
Children = new Drawable[]
{
new Container
{
Size = new Vector2(icon_size),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Children = new Drawable[]
{
icon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.SyncAlt,
Size = new Vector2(icon_size),
},
}
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.Default.With(weight: FontWeight.Bold),
Text = "Update",
}
}
},
});
Action = performUpdate;
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmapChanged();
}
private void beatmapChanged()
{
Alpha = beatmapSet?.AllBeatmapsUpToDate == false ? 1 : 0;
icon.Spin(4000, RotationDirection.Clockwise);
}
protected override bool OnHover(HoverEvent e)
{
icon.Spin(400, RotationDirection.Clockwise);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
icon.Spin(4000, RotationDirection.Clockwise);
base.OnHoverLost(e);
}
private bool updateConfirmed;
private void performUpdate()
{
Debug.Assert(beatmapSet != null);
if (!api.IsLoggedIn)
{
loginOverlay?.Show();
return;
}
if (dialogOverlay != null && beatmapSet.Status == BeatmapOnlineStatus.LocallyModified && !updateConfirmed)
{
dialogOverlay.Push(new UpdateLocalConfirmationDialog(() =>
{
updateConfirmed = true;
performUpdate();
}));
return;
}
updateConfirmed = false;
beatmapDownloader.DownloadAsUpdate(beatmapSet, preferNoVideo.Value);
attachExistingDownload();
}
private void attachExistingDownload()
{
Debug.Assert(beatmapSet != null);
var download = beatmapDownloader.GetExistingDownload(beatmapSet);
if (download != null)
{
Enabled.Value = false;
TooltipText = string.Empty;
download.DownloadProgressed += progress => progressFill.ResizeWidthTo(progress, 100, Easing.OutQuint);
download.Failure += _ => attachExistingDownload();
}
else
{
Enabled.Value = true;
TooltipText = "Update beatmap with online changes";
progressFill.ResizeWidthTo(0, 100, Easing.OutQuint);
}
}
}
}