// 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.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Platform; 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.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Select.Carousel { public partial class DrawableCarouselBeatmap : DrawableCarouselItem, IHasContextMenu { public const float CAROUSEL_BEATMAP_SPACING = 5; /// /// The height of a carousel beatmap, including vertical spacing. /// public const float HEIGHT = height + CAROUSEL_BEATMAP_SPACING; private const float height = MAX_HEIGHT * 0.6f; private readonly BeatmapInfo beatmapInfo; private Sprite background = null!; private MenuItem[]? mainMenuItems; private Action? selectRequested; private Action? hideRequested; private Action? copyBeatmapSetUrl; private Triangles triangles = null!; private StarCounter starCounter = null!; private DifficultyIcon difficultyIcon = null!; private OsuSpriteText keyCountText = null!; [Resolved] private BeatmapSetOverlay? beatmapOverlay { get; set; } [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } = null!; [Resolved] private ManageCollectionsDialog? manageCollectionsDialog { get; set; } [Resolved] private RealmAccess realm { get; set; } = null!; [Resolved] private IBindable ruleset { get; set; } = null!; [Resolved] private IBindable> mods { get; set; } = null!; private IBindable starDifficultyBindable = null!; private CancellationTokenSource? starDifficultyCancellationSource; public DrawableCarouselBeatmap(CarouselBeatmap panel) { beatmapInfo = panel.BeatmapInfo; Item = panel; } [BackgroundDependencyLoader] private void load(BeatmapManager? manager, SongSelect? songSelect, Clipboard clipboard, IAPIProvider api) { Header.Height = height; if (songSelect != null) { mainMenuItems = songSelect.CreateForwardNavigationMenuItemsForBeatmap(() => beatmapInfo); selectRequested = b => songSelect.FinaliseSelection(b); } if (manager != null) hideRequested = manager.Hide; if (beatmapInfo.BeatmapSet != null) copyBeatmapSetUrl += () => clipboard.SetText($@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}"); Header.Children = new Drawable[] { background = new Box { RelativeSizeAxes = Axes.Both, }, triangles = new Triangles { TriangleScale = 2, RelativeSizeAxes = Axes.Both, ColourLight = Color4Extensions.FromHex(@"3a7285"), ColourDark = Color4Extensions.FromHex(@"123744") }, new FillFlowContainer { Padding = new MarginPadding(5), Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Children = new Drawable[] { difficultyIcon = new DifficultyIcon(beatmapInfo) { TooltipType = DifficultyIconTooltipType.None, Scale = new Vector2(1.8f), }, new FillFlowContainer { Padding = new MarginPadding { Left = 5 }, Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, Children = new Drawable[] { new FillFlowContainer { Direction = FillDirection.Horizontal, Spacing = new Vector2(4, 0), AutoSizeAxes = Axes.Both, Children = new[] { keyCountText = new OsuSpriteText { Font = OsuFont.GetFont(size: 20), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Alpha = 0, }, new OsuSpriteText { Text = beatmapInfo.DifficultyName, Font = OsuFont.GetFont(size: 20), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft }, new OsuSpriteText { Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmapInfo.Metadata.Author.Username), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft }, } }, new FillFlowContainer { Direction = FillDirection.Horizontal, Spacing = new Vector2(4, 0), Scale = new Vector2(0.8f), AutoSizeAxes = Axes.Both, Children = new Drawable[] { new TopLocalRank(beatmapInfo), starCounter = new StarCounter() } } } } } } }; } protected override void LoadComplete() { base.LoadComplete(); ruleset.BindValueChanged(_ => updateKeyCount()); mods.BindValueChanged(_ => updateKeyCount()); } protected override void Selected() { base.Selected(); MovementContainer.MoveToX(-50, 500, Easing.OutExpo); background.Colour = ColourInfo.GradientVertical( new Color4(20, 43, 51, 255), new Color4(40, 86, 102, 255)); triangles.Colour = Color4.White; } protected override void Deselected() { base.Deselected(); MovementContainer.MoveToX(0, 500, Easing.OutExpo); background.Colour = new Color4(20, 43, 51, 255); triangles.Colour = OsuColour.Gray(0.5f); } protected override bool OnClick(ClickEvent e) { if (Item?.State.Value == CarouselItemState.Selected) selectRequested?.Invoke(beatmapInfo); return base.OnClick(e); } protected override void ApplyState() { if (Item?.State.Value != CarouselItemState.Collapsed && Alpha == 0) starCounter.ReplayAnimation(); starDifficultyCancellationSource?.Cancel(); // Only compute difficulty when the item is visible. if (Item?.State.Value != CarouselItemState.Collapsed) { // We've potentially cancelled the computation above so a new bindable is required. starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmapInfo, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); starDifficultyBindable.BindValueChanged(d => { starCounter.Current = (float)(d.NewValue?.Stars ?? 0); if (d.NewValue != null) difficultyIcon.Current.Value = d.NewValue.Value; }, true); updateKeyCount(); } base.ApplyState(); } private void updateKeyCount() { if (Item?.State.Value == CarouselItemState.Collapsed) return; if (ruleset.Value.OnlineID == 3) { // Account for mania differences locally for now. // Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel. ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); keyCountText.Alpha = 1; keyCountText.Text = $"[{legacyRuleset.GetKeyCount(beatmapInfo, mods.Value)}K]"; } else keyCountText.Alpha = 0; } public MenuItem[] ContextMenuItems { get { List items = new List(); if (mainMenuItems != null) items.AddRange(mainMenuItems); if (beatmapInfo.OnlineID > 0 && beatmapOverlay != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineID))); var collectionItems = realm.Realm.All() .OrderBy(c => c.Name) .AsEnumerable() .Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast().ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, () => copyBeatmapSetUrl?.Invoke())); if (hideRequested != null) items.Add(new OsuMenuItem(CommonStrings.ButtonsHide.ToSentence(), MenuItemType.Destructive, () => hideRequested(beatmapInfo))); return items.ToArray(); } } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); starDifficultyCancellationSource?.Cancel(); } } }