diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 4306fc1e6a..5f9e06fda5 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -2,8 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; @@ -14,6 +19,9 @@ namespace osu.Game.Tests.Visual.Playlists { public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene { + [Resolved] + private IAPIProvider api { get; set; } = null!; + protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager; [Test] @@ -37,5 +45,29 @@ namespace osu.Game.Tests.Visual.Playlists AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen()); AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf); } + + [Test] + public void TestCloseButtonGoesAwayAfterGracePeriod() + { + Room room = null!; + PlaylistsRoomSubScreen roomScreen = null!; + + AddStep("create room", () => + { + RoomManager.AddRoom(room = new Room + { + Name = @"Test Room", + Host = api.LocalUser.Value, + Category = RoomCategory.Normal, + StartDate = DateTimeOffset.Now.AddMinutes(-5).AddSeconds(3), + EndDate = DateTimeOffset.Now.AddMinutes(30) + }); + }); + + AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room))); + AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen()); + AddAssert("close button present", () => roomScreen.ChildrenOfType().Any()); + AddUntilStep("wait for close button to disappear", () => !roomScreen.ChildrenOfType().Any()); + } } } diff --git a/osu.Game/Online/API/Requests/ClosePlaylistRequest.cs b/osu.Game/Online/API/Requests/ClosePlaylistRequest.cs new file mode 100644 index 0000000000..545266491e --- /dev/null +++ b/osu.Game/Online/API/Requests/ClosePlaylistRequest.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Net.Http; +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class ClosePlaylistRequest : APIRequest + { + private readonly long roomId; + + public ClosePlaylistRequest(long roomId) + { + this.roomId = roomId; + } + + protected override WebRequest CreateWebRequest() + { + var request = base.CreateWebRequest(); + request.Method = HttpMethod.Delete; + return request; + } + + protected override string Target => $@"rooms/{roomId}"; + } +} diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index e1813c7e4e..5a008bac13 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -375,6 +375,7 @@ namespace osu.Game.Online.Rooms Type = other.Type; MaxParticipants = other.MaxParticipants; ParticipantCount = other.ParticipantCount; + StartDate = other.StartDate; EndDate = other.EndDate; UserScore = other.UserScore; QueueMode = other.QueueMode; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index d396d18b4f..7d36cec7ba 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -1,6 +1,7 @@ // 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.ComponentModel; using osu.Framework.Allocation; @@ -22,9 +23,14 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Input.Bindings; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; +using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Playlists; using osuTK; using osuTK.Graphics; using Container = osu.Framework.Graphics.Containers.Container; @@ -48,6 +54,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved(canBeNull: true)] private LoungeSubScreen? lounge { get; set; } + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + private readonly BindableWithCurrent selectedRoom = new BindableWithCurrent(); private Sample? sampleSelect; private Sample? sampleJoin; @@ -144,13 +156,34 @@ namespace osu.Game.Screens.OnlinePlay.Lounge public Popover GetPopover() => new PasswordEntryPopover(Room); - public MenuItem[] ContextMenuItems => new MenuItem[] + public MenuItem[] ContextMenuItems { - new OsuMenuItem("Create copy", MenuItemType.Standard, () => + get { - lounge?.OpenCopy(Room); - }) - }; + var items = new List + { + new OsuMenuItem("Create copy", MenuItemType.Standard, () => + { + lounge?.OpenCopy(Room); + }) + }; + + if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && Room.Status is not RoomStatusEnded) + { + items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () => + { + dialogOverlay?.Push(new ClosePlaylistDialog(Room, () => + { + var request = new ClosePlaylistRequest(Room.RoomID!.Value); + request.Success += () => lounge?.RefreshRooms(); + api.Queue(request); + })); + })); + } + + return items.ToArray(); + } + } public bool OnPressed(KeyBindingPressEvent e) { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index ac8caa6b88..5d0983f09c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -379,6 +379,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge this.Push(CreateRoomSubScreen(room)); } + public void RefreshRooms() => ListingPollingComponent.PollImmediately(); + private void updateLoadingLayer() { if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index ffea3878fa..4ef31c02c3 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay.Match protected RulesetStore Rulesets { get; private set; } = null!; [Resolved] - private IAPIProvider api { get; set; } = null!; + protected IAPIProvider API { get; private set; } = null!; [Resolved(canBeNull: true)] protected OnlinePlayScreen? ParentScreen { get; private set; } @@ -80,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private PreviewTrackManager previewTrackManager { get; set; } = null!; [Resolved(canBeNull: true)] - private IDialogOverlay? dialogOverlay { get; set; } + protected IDialogOverlay? DialogOverlay { get; private set; } [Cached] private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); @@ -282,7 +282,7 @@ namespace osu.Game.Screens.OnlinePlay.Match } } - protected virtual bool IsConnected => api.State.Value == APIState.Online; + protected virtual bool IsConnected => API.State.Value == APIState.Online; public override bool OnBackButton() { @@ -361,17 +361,17 @@ namespace osu.Game.Screens.OnlinePlay.Match bool hasUnsavedChanges = Room.RoomID == null && Room.Playlist.Count > 0; - if (dialogOverlay == null || !hasUnsavedChanges) + if (DialogOverlay == null || !hasUnsavedChanges) return true; // if the dialog is already displayed, block exiting until the user explicitly makes a decision. - if (dialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog) + if (DialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog) { discardChangesDialog.Flash(); return false; } - dialogOverlay.Push(new ConfirmDiscardChangesDialog(() => + DialogOverlay.Push(new ConfirmDiscardChangesDialog(() => { ExitConfirmed = true; settingsOverlay.Hide(); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/ClosePlaylistDialog.cs b/osu.Game/Screens/OnlinePlay/Playlists/ClosePlaylistDialog.cs new file mode 100644 index 0000000000..08fed037d3 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Playlists/ClosePlaylistDialog.cs @@ -0,0 +1,19 @@ +// 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 osu.Game.Online.Rooms; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.OnlinePlay.Playlists +{ + public partial class ClosePlaylistDialog : DeletionDialog + { + public ClosePlaylistDialog(Room room, Action closeAction) + { + HeaderText = "Are you sure you want to close the following playlist:"; + BodyText = room.Name; + DangerousAction = closeAction; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs index 0d837423a6..6089b4734e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomFooter.cs @@ -2,9 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.ComponentModel; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; using osuTK; namespace osu.Game.Screens.OnlinePlay.Playlists @@ -12,22 +17,104 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public partial class PlaylistsRoomFooter : CompositeDrawable { public Action? OnStart; + public Action? OnClose; + + private readonly Room room; + private DangerousRoundedButton closeButton = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; public PlaylistsRoomFooter(Room room) + { + this.room = room; + } + + [BackgroundDependencyLoader] + private void load() { RelativeSizeAxes = Axes.Both; - InternalChildren = new[] + InternalChild = new FillFlowContainer { - new PlaylistsReadyButton(room) + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10), + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Size = new Vector2(600, 1), - Action = () => OnStart?.Invoke() + new PlaylistsReadyButton(room) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(600, 1), + Action = () => OnStart?.Invoke() + }, + closeButton = new DangerousRoundedButton + { + Text = "Close", + Action = () => OnClose?.Invoke(), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(120, 1), + Alpha = 0, + RelativeSizeAxes = Axes.Y, + } } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + room.PropertyChanged += onRoomChanged; + updateState(); + } + + private void hideCloseButton() + { + closeButton.ResizeWidthTo(0, 100, Easing.OutQuint) + .Then().FadeOut().Expire(); + } + + private void onRoomChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(Room.Status): + case nameof(Room.Host): + case nameof(Room.StartDate): + updateState(); + break; + } + } + + private void updateState() + { + TimeSpan? deletionGracePeriodRemaining = room.StartDate?.AddMinutes(5) - DateTimeOffset.Now; + + if (room.Host?.Id == api.LocalUser.Value.Id) + { + if (deletionGracePeriodRemaining > TimeSpan.Zero && room.Status is not RoomStatusEnded) + { + closeButton.FadeIn(); + using (BeginDelayedSequence(deletionGracePeriodRemaining.Value.TotalMilliseconds)) + hideCloseButton(); + } + else if (closeButton.Alpha > 0) + hideCloseButton(); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + room.PropertyChanged -= onRoomChanged; + } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 1aaae60195..9573155f5a 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -1,6 +1,7 @@ // 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.ComponentModel; using System.Diagnostics; using System.Linq; @@ -13,7 +14,9 @@ using osu.Framework.Screens; using osu.Game.Graphics.Cursor; using osu.Game.Input; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -259,7 +262,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override Drawable CreateFooter() => new PlaylistsRoomFooter(Room) { - OnStart = StartPlay + OnStart = StartPlay, + OnClose = closePlaylist, }; protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new PlaylistsRoomSettingsOverlay(room) @@ -277,6 +281,20 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})"); } + private void closePlaylist() + { + DialogOverlay?.Push(new ClosePlaylistDialog(Room, () => + { + var request = new ClosePlaylistRequest(Room.RoomID!.Value); + request.Success += () => + { + Room.Status = new RoomStatusEnded(); + Room.EndDate = DateTimeOffset.UtcNow; + }; + API.Queue(request); + })); + } + protected override Screen CreateGameplayScreen(PlaylistItem selectedItem) { return new PlayerLoader(() => new PlaylistsPlayer(Room, selectedItem)