mirror of
https://github.com/ppy/osu.git
synced 2025-01-26 12:35:34 +08:00
Merge pull request #27722 from peppy/shared-menu-content
Add ability to schedule and rotate main menu content
This commit is contained in:
commit
02b98671b8
@ -13,30 +13,25 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
public partial class TestSceneMainMenu : OsuGameTestScene
|
||||
{
|
||||
private SystemTitle systemTitle => Game.ChildrenOfType<SystemTitle>().Single();
|
||||
private OnlineMenuBanner onlineMenuBanner => Game.ChildrenOfType<OnlineMenuBanner>().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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
240
osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs
Normal file
240
osu.Game.Tests/Visual/Menus/TestSceneOnlineMenuBanner.cs
Normal file
@ -0,0 +1,240 @@
|
||||
// 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 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<OnlineMenuBanner.MenuImage>();
|
||||
|
||||
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<OnlineMenuBanner.MenuImage>();
|
||||
|
||||
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<OnlineMenuBanner.MenuImage>().SingleOrDefault()?.Size, () => Is.EqualTo(Vector2.Zero));
|
||||
|
||||
AddStep("unset system title", () => onlineMenuBanner.Current.Value = new APIMenuContent());
|
||||
|
||||
AddUntilStep("wait for no image shown", () => !onlineMenuBanner.ChildrenOfType<OnlineMenuBanner.MenuImage>().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<OnlineMenuBanner.MenuImage>();
|
||||
|
||||
if (images.Count() != 2)
|
||||
return false;
|
||||
|
||||
return images.First().IsPresent && !images.Last().IsPresent;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for second image shown", () =>
|
||||
{
|
||||
var images = onlineMenuBanner.ChildrenOfType<OnlineMenuBanner.MenuImage>();
|
||||
|
||||
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<OnlineMenuBanner.MenuImage>().Any(i => i.IsPresent));
|
||||
|
||||
AddUntilStep("wait for one image shown", () =>
|
||||
{
|
||||
var images = onlineMenuBanner.ChildrenOfType<OnlineMenuBanner.MenuImage>();
|
||||
|
||||
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<OnlineMenuBanner.MenuImage>().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<OnlineMenuBanner.MenuImage>();
|
||||
|
||||
if (images.Count() != 2)
|
||||
return false;
|
||||
|
||||
return images.First().IsPresent && !images.Last().IsPresent;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for second image shown", () =>
|
||||
{
|
||||
var images = onlineMenuBanner.ChildrenOfType<OnlineMenuBanner.MenuImage>();
|
||||
|
||||
if (images.Count() != 2)
|
||||
return false;
|
||||
|
||||
return !images.First().IsPresent && images.Last().IsPresent;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for expiry", () =>
|
||||
{
|
||||
return onlineMenuBanner
|
||||
.ChildrenOfType<OnlineMenuBanner.MenuImage>()
|
||||
.Any(i => !i.Image.IsCurrent);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for first image shown", () =>
|
||||
{
|
||||
var images = onlineMenuBanner.ChildrenOfType<OnlineMenuBanner.MenuImage>();
|
||||
|
||||
if (images.Count() != 2)
|
||||
return false;
|
||||
|
||||
return images.First().IsPresent && !images.Last().IsPresent;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -5,10 +5,10 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class GetSystemTitleRequest : OsuJsonWebRequest<APISystemTitle>
|
||||
public class GetMenuContentRequest : OsuJsonWebRequest<APIMenuContent>
|
||||
{
|
||||
public GetSystemTitleRequest()
|
||||
: base(@"https://assets.ppy.sh/lazer-status.json")
|
||||
public GetMenuContentRequest()
|
||||
: base(@"https://assets.ppy.sh/menu-content.json")
|
||||
{
|
||||
}
|
||||
}
|
38
osu.Game/Online/API/Requests/Responses/APIMenuContent.cs
Normal file
38
osu.Game/Online/API/Requests/Responses/APIMenuContent.cs
Normal file
@ -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.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
public class APIMenuContent : IEquatable<APIMenuContent>
|
||||
{
|
||||
/// <summary>
|
||||
/// Images which should be displayed in rotation.
|
||||
/// </summary>
|
||||
[JsonProperty(@"images")]
|
||||
public APIMenuImage[] Images { get; init; } = Array.Empty<APIMenuImage>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
54
osu.Game/Online/API/Requests/Responses/APIMenuImage.cs
Normal file
54
osu.Game/Online/API/Requests/Responses/APIMenuImage.cs
Normal file
@ -0,0 +1,54 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
public class APIMenuImage : IEquatable<APIMenuImage>
|
||||
{
|
||||
/// <summary>
|
||||
/// A URL pointing to the image which should be displayed. Generally should be an @2x image filename.
|
||||
/// </summary>
|
||||
[JsonProperty(@"image")]
|
||||
public string Image { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// A URL that should be opened on clicking the image.
|
||||
/// </summary>
|
||||
[JsonProperty(@"url")]
|
||||
public string Url { get; init; } = string.Empty;
|
||||
|
||||
public bool IsCurrent =>
|
||||
(Begins == null || Begins < DateTimeOffset.UtcNow) &&
|
||||
(Expires == null || Expires > DateTimeOffset.UtcNow);
|
||||
|
||||
/// <summary>
|
||||
/// The time at which this item should begin displaying. If <c>null</c>, will display immediately.
|
||||
/// </summary>
|
||||
[JsonProperty(@"begins")]
|
||||
public DateTimeOffset? Begins { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The time at which this item should stop displaying. If <c>null</c>, will display indefinitely.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
public class APISystemTitle : IEquatable<APISystemTitle>
|
||||
{
|
||||
[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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
249
osu.Game/Screens/Menu/OnlineMenuBanner.cs
Normal file
249
osu.Game/Screens/Menu/OnlineMenuBanner.cs
Normal file
@ -0,0 +1,249 @@
|
||||
// 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 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<APIMenuContent> Current { get; } = new Bindable<APIMenuContent>(new APIMenuContent());
|
||||
|
||||
private const float transition_duration = 500;
|
||||
|
||||
private Container<MenuImage> 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<MenuImage>
|
||||
{
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes <see cref="Current"/> and materialises and displays drawables for all valid images to be displayed.
|
||||
/// </summary>
|
||||
/// <param name="images"></param>
|
||||
private void loadNewImages(ValueChangedEvent<APIMenuContent> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,186 +0,0 @@
|
||||
// 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.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<APISystemTitle?> Current { get; } = new Bindable<APISystemTitle?>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user