diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs index 7053a9d544..e2a841d79a 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMainMenu.cs @@ -13,30 +13,25 @@ namespace osu.Game.Tests.Visual.Menus { public partial class TestSceneMainMenu : OsuGameTestScene { - private SystemTitle systemTitle => Game.ChildrenOfType().Single(); + private OnlineMenuBanner onlineMenuBanner => Game.ChildrenOfType().Single(); [Test] - public void TestSystemTitle() + public void TestOnlineMenuBanner() { - AddStep("set system title", () => systemTitle.Current.Value = new APISystemTitle + AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent { - Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", - Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + } + } }); - AddAssert("system title not visible", () => systemTitle.State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddAssert("system title not visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Hidden)); AddStep("enter menu", () => InputManager.Key(Key.Enter)); - AddUntilStep("system title visible", () => systemTitle.State.Value, () => Is.EqualTo(Visibility.Visible)); - AddStep("set another title", () => systemTitle.Current.Value = new APISystemTitle - { - Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", - Url = @"https://osu.ppy.sh/community/contests/189", - }); - AddStep("set title with nonexistent image", () => systemTitle.Current.Value = new APISystemTitle - { - Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2 - Url = @"https://osu.ppy.sh/community/contests/189", - }); - AddStep("unset system title", () => systemTitle.Current.Value = null); + AddUntilStep("system title visible", () => onlineMenuBanner.State.Value, () => Is.EqualTo(Visibility.Visible)); } } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs new file mode 100644 index 0000000000..0b90fd13c3 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs @@ -0,0 +1,240 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.Menu; +using osuTK; + +namespace osu.Game.Tests.Visual.Menus +{ + public partial class TestSceneOnlineMenuBanner : OsuTestScene + { + private OnlineMenuBanner onlineMenuBanner = null!; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create banner", () => + { + Child = onlineMenuBanner = new OnlineMenuBanner + { + FetchOnlineContent = false, + DelayBetweenRotation = 500, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { Value = Visibility.Visible } + }; + }); + } + + [Test] + public void TestBasic() + { + AddStep("set online content", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + } + }, + }); + + AddUntilStep("wait for one image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 1) + return false; + + var image = images.Single(); + + return image.IsPresent && image.Image.Url == "https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023"; + }); + + AddStep("set another title", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", + Url = @"https://osu.ppy.sh/community/contests/189", + } + } + }); + + AddUntilStep("wait for new image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 1) + return false; + + var image = images.Single(); + + return image.IsPresent && image.Image.Url == "https://osu.ppy.sh/community/contests/189"; + }); + + AddStep("set title with nonexistent image", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://test.invalid/@2x", // .invalid TLD reserved by https://datatracker.ietf.org/doc/html/rfc2606#section-2 + Url = @"https://osu.ppy.sh/community/contests/189", + } + } + }); + + AddUntilStep("wait for no image shown", () => onlineMenuBanner.ChildrenOfType().SingleOrDefault()?.Size, () => Is.EqualTo(Vector2.Zero)); + + AddStep("unset system title", () => onlineMenuBanner.Current.Value = new APIMenuContent()); + + AddUntilStep("wait for no image shown", () => !onlineMenuBanner.ChildrenOfType().Any()); + } + + [Test] + public void TestMultipleImages() + { + AddStep("set multiple images", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + }, + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", + Url = @"https://osu.ppy.sh/community/contests/189", + } + }, + }); + + AddUntilStep("wait for first image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 2) + return false; + + return images.First().IsPresent && !images.Last().IsPresent; + }); + + AddUntilStep("wait for second image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 2) + return false; + + return !images.First().IsPresent && images.Last().IsPresent; + }); + } + + [Test] + public void TestFutureSingle() + { + AddStep("set image with time constraints", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + Begins = DateTimeOffset.Now.AddSeconds(2), + Expires = DateTimeOffset.Now.AddSeconds(5), + }, + }, + }); + + AddUntilStep("wait for no image shown", () => !onlineMenuBanner.ChildrenOfType().Any(i => i.IsPresent)); + + AddUntilStep("wait for one image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 1) + return false; + + var image = images.Single(); + + return image.IsPresent && image.Image.Url == "https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023"; + }); + + AddUntilStep("wait for no image shown", () => !onlineMenuBanner.ChildrenOfType().Any(i => i.IsPresent)); + } + + [Test] + public void TestExpiryMultiple() + { + AddStep("set multiple images, second expiring soon", () => onlineMenuBanner.Current.Value = new APIMenuContent + { + Images = new[] + { + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/project-loved-2@2x.png", + Url = @"https://osu.ppy.sh/home/news/2023-12-21-project-loved-december-2023", + }, + new APIMenuImage + { + Image = @"https://assets.ppy.sh/main-menu/wf2023-vote@2x.png", + Url = @"https://osu.ppy.sh/community/contests/189", + Expires = DateTimeOffset.Now.AddSeconds(2), + } + }, + }); + + AddUntilStep("wait for first image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 2) + return false; + + return images.First().IsPresent && !images.Last().IsPresent; + }); + + AddUntilStep("wait for second image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 2) + return false; + + return !images.First().IsPresent && images.Last().IsPresent; + }); + + AddUntilStep("wait for expiry", () => + { + return onlineMenuBanner + .ChildrenOfType() + .Any(i => !i.Image.IsCurrent); + }); + + AddUntilStep("wait for first image shown", () => + { + var images = onlineMenuBanner.ChildrenOfType(); + + if (images.Count() != 2) + return false; + + return images.First().IsPresent && !images.Last().IsPresent; + }); + } + } +} diff --git a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs b/osu.Game/Online/API/Requests/GetMenuContentRequest.cs similarity index 60% rename from osu.Game/Online/API/Requests/GetSystemTitleRequest.cs rename to osu.Game/Online/API/Requests/GetMenuContentRequest.cs index 52ca0c11eb..26747489d6 100644 --- a/osu.Game/Online/API/Requests/GetSystemTitleRequest.cs +++ b/osu.Game/Online/API/Requests/GetMenuContentRequest.cs @@ -5,10 +5,10 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetSystemTitleRequest : OsuJsonWebRequest + public class GetMenuContentRequest : OsuJsonWebRequest { - public GetSystemTitleRequest() - : base(@"https://assets.ppy.sh/lazer-status.json") + public GetMenuContentRequest() + : base(@"https://assets.ppy.sh/menu-content.json") { } } diff --git a/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs b/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs new file mode 100644 index 0000000000..6aad0f6c87 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIMenuContent.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIMenuContent : IEquatable + { + /// + /// Images which should be displayed in rotation. + /// + [JsonProperty(@"images")] + public APIMenuImage[] Images { get; init; } = Array.Empty(); + + public bool Equals(APIMenuContent? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Images.SequenceEqual(other.Images); + } + + public override bool Equals(object? other) => other is APIMenuContent content && Equals(content); + + public override int GetHashCode() + { + var hash = new HashCode(); + + foreach (var image in Images) + hash.Add(image.GetHashCode()); + + return hash.ToHashCode(); + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs b/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs new file mode 100644 index 0000000000..8aff08099a --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIMenuImage.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIMenuImage : IEquatable + { + /// + /// A URL pointing to the image which should be displayed. Generally should be an @2x image filename. + /// + [JsonProperty(@"image")] + public string Image { get; init; } = string.Empty; + + /// + /// A URL that should be opened on clicking the image. + /// + [JsonProperty(@"url")] + public string Url { get; init; } = string.Empty; + + public bool IsCurrent => + (Begins == null || Begins < DateTimeOffset.UtcNow) && + (Expires == null || Expires > DateTimeOffset.UtcNow); + + /// + /// The time at which this item should begin displaying. If null, will display immediately. + /// + [JsonProperty(@"begins")] + public DateTimeOffset? Begins { get; init; } + + /// + /// The time at which this item should stop displaying. If null, will display indefinitely. + /// + [JsonProperty(@"expires")] + public DateTimeOffset? Expires { get; init; } + + public bool Equals(APIMenuImage? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Image == other.Image && Url == other.Url && Begins == other.Begins && Expires == other.Expires; + } + + public override bool Equals(object? other) => other is APIMenuImage content && Equals(content); + + public override int GetHashCode() + { + return HashCode.Combine(Image, Url, Begins, Expires); + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs b/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs deleted file mode 100644 index bfa5c1043b..0000000000 --- a/osu.Game/Online/API/Requests/Responses/APISystemTitle.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using Newtonsoft.Json; - -namespace osu.Game.Online.API.Requests.Responses -{ - public class APISystemTitle : IEquatable - { - [JsonProperty(@"image")] - public string Image { get; set; } = string.Empty; - - [JsonProperty(@"url")] - public string Url { get; set; } = string.Empty; - - public bool Equals(APISystemTitle? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - - return Image == other.Image && Url == other.Url; - } - - public override bool Equals(object? obj) => obj is APISystemTitle other && Equals(other); - - // ReSharper disable NonReadonlyMemberInGetHashCode - public override int GetHashCode() => HashCode.Combine(Image, Url); - } -} diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index decb901c32..235c5d5c56 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -98,7 +98,7 @@ namespace osu.Game.Screens.Menu private ParallaxContainer buttonsContainer; private SongTicker songTicker; private Container logoTarget; - private SystemTitle systemTitle; + private OnlineMenuBanner onlineMenuBanner; private MenuTip menuTip; private FillFlowContainer bottomElementsFlow; private SupporterDisplay supporterDisplay; @@ -178,7 +178,7 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, - systemTitle = new SystemTitle + onlineMenuBanner = new OnlineMenuBanner { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -201,12 +201,12 @@ namespace osu.Game.Screens.Menu case ButtonSystemState.Initial: case ButtonSystemState.Exit: ApplyToBackground(b => b.FadeColour(Color4.White, 500, Easing.OutSine)); - systemTitle.State.Value = Visibility.Hidden; + onlineMenuBanner.State.Value = Visibility.Hidden; break; default: ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.8f), 500, Easing.OutSine)); - systemTitle.State.Value = Visibility.Visible; + onlineMenuBanner.State.Value = Visibility.Visible; break; } }; diff --git a/osu.Game/Screens/Menu/OnlineMenuBanner.cs b/osu.Game/Screens/Menu/OnlineMenuBanner.cs new file mode 100644 index 0000000000..6f98b73939 --- /dev/null +++ b/osu.Game/Screens/Menu/OnlineMenuBanner.cs @@ -0,0 +1,249 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Events; +using osu.Framework.Threading; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Screens.Menu +{ + public partial class OnlineMenuBanner : VisibilityContainer + { + public double DelayBetweenRotation { get; set; } = 7500; + + public bool FetchOnlineContent { get; set; } = true; + + internal Bindable Current { get; } = new Bindable(new APIMenuContent()); + + private const float transition_duration = 500; + + private Container content = null!; + private CancellationTokenSource? cancellationTokenSource; + + private int displayIndex = -1; + + private ScheduledDelegate? nextDisplay; + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + AutoSizeDuration = transition_duration; + AutoSizeEasing = Easing.OutQuint; + + InternalChild = content = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + }; + } + + protected override void PopIn() => content.FadeInFromZero(transition_duration, Easing.OutQuint); + + protected override void PopOut() => content.FadeOut(transition_duration, Easing.OutQuint); + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(loadNewImages, true); + checkForUpdates(); + } + + private void checkForUpdates() + { + if (!FetchOnlineContent) + return; + + var request = new GetMenuContentRequest(); + Task.Run(() => request.Perform()) + .ContinueWith(r => + { + if (r.IsCompletedSuccessfully) + Schedule(() => Current.Value = request.ResponseObject); + + // if the request failed, "observe" the exception. + // it isn't very important why this failed, as it's only for display. + // the inner error will be logged by framework mechanisms anyway. + if (r.IsFaulted) + _ = r.Exception; + + Scheduler.AddDelayed(checkForUpdates, TimeSpan.FromMinutes(5).TotalMilliseconds); + }); + } + + /// + /// Takes and materialises and displays drawables for all valid images to be displayed. + /// + /// + private void loadNewImages(ValueChangedEvent images) + { + nextDisplay?.Cancel(); + + cancellationTokenSource?.Cancel(); + cancellationTokenSource = null; + + // A better fade out would be nice, but the menu content changes *very* rarely + // so let's keep things simple for now. + content.Clear(true); + + LoadComponentsAsync(images.NewValue.Images.Select(i => new MenuImage(i)), loaded => + { + if (!images.NewValue.Equals(Current.Value)) + return; + + // start hidden + foreach (var image in loaded) + image.Hide(); + + content.AddRange(loaded); + + displayIndex = -1; + showNext(); + }, (cancellationTokenSource ??= new CancellationTokenSource()).Token); + } + + private void showNext() + { + nextDisplay?.Cancel(); + + // If the user is interacting with a banner, don't rotate yet. + bool anyHovered = content.Any(i => i.IsHovered || i.IsDragged); + + if (!anyHovered) + { + int previousIndex = displayIndex; + + // To handle expiration simply, arrange all images in best-next order. + // Fade in the first valid one, then handle fading out the last if required. + var currentRotation = content + .Skip(previousIndex + 1) + .Concat(content.Take(previousIndex + 1)); + + // After the loop, displayIndex will be the new valid index or -1 if + // none valid. + displayIndex = -1; + + foreach (var image in currentRotation) + { + if (!image.Image.IsCurrent) continue; + + using (BeginDelayedSequence(previousIndex >= 0 ? 300 : 0)) + { + displayIndex = content.IndexOf(image); + + if (displayIndex != previousIndex) + image.Show(); + + break; + } + } + + if (previousIndex >= 0 && previousIndex != displayIndex) + content[previousIndex].FadeOut(400, Easing.OutQuint); + } + + // Re-scheduling this method will both handle rotation and re-checking for expiration dates. + nextDisplay = Scheduler.AddDelayed(showNext, DelayBetweenRotation); + } + + [LongRunningLoad] + public partial class MenuImage : OsuClickableContainer + { + public readonly APIMenuImage Image; + + private Sprite flash = null!; + + private ScheduledDelegate? openUrlAction; + + public MenuImage(APIMenuImage image) + { + AutoSizeAxes = Axes.Both; + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + + Image = image; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textureStore, OsuGame? game) + { + Texture? texture = textureStore.Get(Image.Image); + if (texture != null && Image.Image.Contains(@"@2x")) + texture.ScaleAdjust *= 2; + + Children = new Drawable[] + { + new Sprite { Texture = texture }, + flash = new Sprite + { + Texture = texture, + Blending = BlendingParameters.Additive, + }, + }; + + Action = () => + { + flash.FadeInFromZero(50) + .Then() + .FadeOut(500, Easing.OutQuint); + + // Delay slightly to allow animation to play out. + openUrlAction?.Cancel(); + openUrlAction = Scheduler.AddDelayed(() => + { + if (!string.IsNullOrEmpty(Image.Url)) + game?.HandleLink(Image.Url); + }, 250); + }; + } + + public override void Show() + { + this.FadeInFromZero(500, Easing.OutQuint); + flash.FadeOutFromOne(4000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + this.ScaleTo(1.05f, 2000, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + this.ScaleTo(1f, 500, Easing.OutQuint); + base.OnHoverLost(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + this.ScaleTo(0.95f, 500, Easing.OutQuint); + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + this + .ScaleTo(0.95f) + .ScaleTo(1, 500, Easing.OutElastic); + base.OnMouseUp(e); + } + + protected override bool OnDragStart(DragStartEvent e) => true; + } + } +} diff --git a/osu.Game/Screens/Menu/SystemTitle.cs b/osu.Game/Screens/Menu/SystemTitle.cs deleted file mode 100644 index 813a470ed6..0000000000 --- a/osu.Game/Screens/Menu/SystemTitle.cs +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Threading; -using System.Threading.Tasks; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Input.Events; -using osu.Framework.Threading; -using osu.Game.Graphics.Containers; -using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; - -namespace osu.Game.Screens.Menu -{ - public partial class SystemTitle : VisibilityContainer - { - internal Bindable Current { get; } = new Bindable(); - - private const float transition_duration = 500; - - private Container content = null!; - private CancellationTokenSource? cancellationTokenSource; - private SystemTitleImage? currentImage; - - private ScheduledDelegate? openUrlAction; - - [BackgroundDependencyLoader] - private void load(OsuGame? game) - { - AutoSizeAxes = Axes.Both; - AutoSizeDuration = transition_duration; - AutoSizeEasing = Easing.OutQuint; - - InternalChild = content = new OsuClickableContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Action = () => - { - currentImage?.Flash(); - - // Delay slightly to allow animation to play out. - openUrlAction?.Cancel(); - openUrlAction = Scheduler.AddDelayed(() => - { - if (!string.IsNullOrEmpty(Current.Value?.Url)) - game?.HandleLink(Current.Value.Url); - }, 250); - } - }; - } - - protected override void PopIn() => content.FadeInFromZero(transition_duration, Easing.OutQuint); - - protected override void PopOut() => content.FadeOut(transition_duration, Easing.OutQuint); - - protected override bool OnHover(HoverEvent e) - { - content.ScaleTo(1.05f, 2000, Easing.OutQuint); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - content.ScaleTo(1f, 500, Easing.OutQuint); - base.OnHoverLost(e); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - content.ScaleTo(0.95f, 500, Easing.OutQuint); - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - content - .ScaleTo(0.95f) - .ScaleTo(1, 500, Easing.OutElastic); - base.OnMouseUp(e); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Current.BindValueChanged(_ => loadNewImage(), true); - - checkForUpdates(); - } - - private void checkForUpdates() - { - var request = new GetSystemTitleRequest(); - Task.Run(() => request.Perform()) - .ContinueWith(r => - { - if (r.IsCompletedSuccessfully) - Schedule(() => Current.Value = request.ResponseObject); - - // if the request failed, "observe" the exception. - // it isn't very important why this failed, as it's only for display. - // the inner error will be logged by framework mechanisms anyway. - if (r.IsFaulted) - _ = r.Exception; - - Scheduler.AddDelayed(checkForUpdates, TimeSpan.FromMinutes(5).TotalMilliseconds); - }); - } - - private void loadNewImage() - { - cancellationTokenSource?.Cancel(); - cancellationTokenSource = null; - currentImage?.FadeOut(500, Easing.OutQuint).Expire(); - - if (string.IsNullOrEmpty(Current.Value?.Image)) - return; - - LoadComponentAsync(new SystemTitleImage(Current.Value), loaded => - { - if (!loaded.SystemTitle.Equals(Current.Value)) - loaded.Dispose(); - - content.Add(currentImage = loaded); - }, (cancellationTokenSource ??= new CancellationTokenSource()).Token); - } - - [LongRunningLoad] - private partial class SystemTitleImage : CompositeDrawable - { - public readonly APISystemTitle SystemTitle; - - private Sprite flash = null!; - - public SystemTitleImage(APISystemTitle systemTitle) - { - SystemTitle = systemTitle; - } - - [BackgroundDependencyLoader] - private void load(LargeTextureStore textureStore) - { - Texture? texture = textureStore.Get(SystemTitle.Image); - if (texture != null && SystemTitle.Image.Contains(@"@2x")) - texture.ScaleAdjust *= 2; - - AutoSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - new Sprite { Texture = texture }, - flash = new Sprite - { - Texture = texture, - Blending = BlendingParameters.Additive, - }, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - this.FadeInFromZero(500, Easing.OutQuint); - flash.FadeOutFromOne(4000, Easing.OutQuint); - } - - public Drawable Flash() - { - flash.FadeInFromZero(50) - .Then() - .FadeOut(500, Easing.OutQuint); - - return this; - } - } - } -}