mirror of
https://github.com/ppy/osu.git
synced 2024-12-05 09:42:54 +08:00
Merge branch 'master' into taiko-random-mod
This commit is contained in:
commit
2dbcf9e346
@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
|
||||
.FadeTo(tracking_alpha, 250, Easing.OutQuint);
|
||||
}
|
||||
|
||||
this.RotateTo(currentRotation / 2, validAndTracking ? 500 : 1500, Easing.OutExpo);
|
||||
Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -278,6 +278,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
|
||||
private void setupUserSettings()
|
||||
{
|
||||
AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen());
|
||||
AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmap != null);
|
||||
AddStep("Set default user settings", () =>
|
||||
{
|
||||
|
87
osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
Normal file
87
osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
Normal file
@ -0,0 +1,87 @@
|
||||
// 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.Collections.Generic;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Overlays.Dashboard.Friends;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Framework.Allocation;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
public class TestSceneFriendDisplay : OsuTestScene
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(FriendDisplay),
|
||||
typeof(FriendOnlineStreamControl),
|
||||
typeof(UserListToolbar)
|
||||
};
|
||||
|
||||
protected override bool UseOnlineAPI => true;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
private FriendDisplay display;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Child = new BasicScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = display = new FriendDisplay()
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestOffline()
|
||||
{
|
||||
AddStep("Populate", () => display.Users = getUsers());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnline()
|
||||
{
|
||||
AddStep("Fetch online", () => display?.Fetch());
|
||||
}
|
||||
|
||||
private List<User> getUsers() => new List<User>
|
||||
{
|
||||
new User
|
||||
{
|
||||
Username = "flyte",
|
||||
Id = 3103765,
|
||||
IsOnline = true,
|
||||
CurrentModeRank = 1111,
|
||||
Country = new Country { FlagName = "JP" },
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
|
||||
},
|
||||
new User
|
||||
{
|
||||
Username = "peppy",
|
||||
Id = 2,
|
||||
IsOnline = false,
|
||||
CurrentModeRank = 2222,
|
||||
Country = new Country { FlagName = "AU" },
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
IsSupporter = true,
|
||||
SupportLevel = 3,
|
||||
},
|
||||
new User
|
||||
{
|
||||
Username = "Evast",
|
||||
Id = 8195163,
|
||||
Country = new Country { FlagName = "BY" },
|
||||
CoverUrl = "https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
IsOnline = false,
|
||||
LastVisit = DateTimeOffset.Now
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -227,6 +227,34 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
waitForSelection(set_count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectionEnteringFromEmptyRuleset()
|
||||
{
|
||||
var sets = new List<BeatmapSetInfo>();
|
||||
|
||||
AddStep("Create beatmaps for taiko only", () =>
|
||||
{
|
||||
sets.Clear();
|
||||
|
||||
var rulesetBeatmapSet = createTestBeatmapSet(1);
|
||||
var taikoRuleset = rulesets.AvailableRulesets.ElementAt(1);
|
||||
rulesetBeatmapSet.Beatmaps.ForEach(b =>
|
||||
{
|
||||
b.Ruleset = taikoRuleset;
|
||||
b.RulesetID = 1;
|
||||
});
|
||||
|
||||
sets.Add(rulesetBeatmapSet);
|
||||
});
|
||||
|
||||
loadBeatmaps(sets, () => new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) });
|
||||
|
||||
AddStep("Set non-empty mode filter", () =>
|
||||
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }, false));
|
||||
|
||||
AddAssert("Something is selected", () => carousel.SelectedBeatmap != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test sorting
|
||||
/// </summary>
|
||||
@ -399,27 +427,32 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("filter to ruleset 0", () =>
|
||||
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false));
|
||||
AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false));
|
||||
AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap == null);
|
||||
AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap.RulesetID == 0);
|
||||
|
||||
AddStep("remove mixed set", () =>
|
||||
{
|
||||
carousel.RemoveBeatmapSet(testMixed);
|
||||
testMixed = null;
|
||||
});
|
||||
var testSingle = createTestBeatmapSet(set_count + 2);
|
||||
testSingle.Beatmaps.ForEach(b =>
|
||||
BeatmapSetInfo testSingle = null;
|
||||
AddStep("add single ruleset beatmapset", () =>
|
||||
{
|
||||
b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
|
||||
b.RulesetID = b.Ruleset.ID ?? 1;
|
||||
testSingle = createTestBeatmapSet(set_count + 2);
|
||||
testSingle.Beatmaps.ForEach(b =>
|
||||
{
|
||||
b.Ruleset = rulesets.AvailableRulesets.ElementAt(1);
|
||||
b.RulesetID = b.Ruleset.ID ?? 1;
|
||||
});
|
||||
|
||||
carousel.UpdateBeatmapSet(testSingle);
|
||||
});
|
||||
AddStep("add single ruleset beatmapset", () => carousel.UpdateBeatmapSet(testSingle));
|
||||
AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testSingle.Beatmaps[0], false));
|
||||
checkNoSelection();
|
||||
AddStep("remove single ruleset set", () => carousel.RemoveBeatmapSet(testSingle));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRootIsRandom()
|
||||
public void TestCarouselRemembersSelection()
|
||||
{
|
||||
List<BeatmapSetInfo> manySets = new List<BeatmapSetInfo>();
|
||||
|
||||
@ -429,12 +462,74 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
loadBeatmaps(manySets);
|
||||
|
||||
advanceSelection(direction: 1, diff: false);
|
||||
checkNonmatchingFilter();
|
||||
checkNonmatchingFilter();
|
||||
checkNonmatchingFilter();
|
||||
checkNonmatchingFilter();
|
||||
checkNonmatchingFilter();
|
||||
AddAssert("Selection was random", () => eagerSelectedIDs.Count > 1);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
AddStep("Toggle non-matching filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
|
||||
});
|
||||
|
||||
AddStep("Restore no filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria(), false);
|
||||
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID);
|
||||
});
|
||||
}
|
||||
|
||||
// always returns to same selection as long as it's available.
|
||||
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomFallbackOnNonMatchingPrevious()
|
||||
{
|
||||
List<BeatmapSetInfo> manySets = new List<BeatmapSetInfo>();
|
||||
|
||||
AddStep("populate maps", () =>
|
||||
{
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var set = createTestBeatmapSet(i);
|
||||
|
||||
foreach (var b in set.Beatmaps)
|
||||
{
|
||||
// all taiko except for first
|
||||
int ruleset = i > 0 ? 1 : 0;
|
||||
|
||||
b.Ruleset = rulesets.GetRuleset(ruleset);
|
||||
b.RulesetID = ruleset;
|
||||
}
|
||||
|
||||
manySets.Add(set);
|
||||
}
|
||||
});
|
||||
|
||||
loadBeatmaps(manySets);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
AddStep("Reset filter", () => carousel.Filter(new FilterCriteria(), false));
|
||||
|
||||
AddStep("select first beatmap", () => carousel.SelectBeatmap(manySets.First().Beatmaps.First()));
|
||||
|
||||
AddStep("Toggle non-matching filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
|
||||
});
|
||||
|
||||
AddAssert("selection lost", () => carousel.SelectedBeatmap == null);
|
||||
|
||||
AddStep("Restore different ruleset filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { Ruleset = rulesets.GetRuleset(1) }, false);
|
||||
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID);
|
||||
});
|
||||
|
||||
AddAssert("selection changed", () => carousel.SelectedBeatmap != manySets.First().Beatmaps.First());
|
||||
}
|
||||
|
||||
AddAssert("Selection was random", () => eagerSelectedIDs.Count > 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -484,7 +579,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
checkVisibleItemCount(true, 15);
|
||||
}
|
||||
|
||||
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null)
|
||||
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null)
|
||||
{
|
||||
createCarousel();
|
||||
|
||||
@ -499,7 +594,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
bool changed = false;
|
||||
AddStep($"Load {(beatmapSets.Count > 0 ? beatmapSets.Count.ToString() : "some")} beatmaps", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria());
|
||||
carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria());
|
||||
carousel.BeatmapSetsChanged = () => changed = true;
|
||||
carousel.BeatmapSets = beatmapSets;
|
||||
});
|
||||
@ -593,16 +688,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddAssert("Selection is visible", selectedBeatmapVisible);
|
||||
}
|
||||
|
||||
private void checkNonmatchingFilter()
|
||||
{
|
||||
AddStep("Toggle non-matching filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { SearchText = "Dingo" }, false);
|
||||
carousel.Filter(new FilterCriteria(), false);
|
||||
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID);
|
||||
});
|
||||
}
|
||||
|
||||
private BeatmapSetInfo createTestBeatmapSet(int id)
|
||||
{
|
||||
return new BeatmapSetInfo
|
||||
|
@ -8,7 +8,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Home.Friends;
|
||||
using osu.Game.Overlays.Dashboard.Friends;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
@ -17,20 +17,20 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(FriendsOnlineStatusControl),
|
||||
typeof(FriendOnlineStreamControl),
|
||||
typeof(FriendsOnlineStatusItem),
|
||||
typeof(OverlayStreamControl<>),
|
||||
typeof(OverlayStreamItem<>),
|
||||
typeof(FriendsBundle)
|
||||
typeof(FriendStream)
|
||||
};
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
private FriendsOnlineStatusControl control;
|
||||
private FriendOnlineStreamControl control;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() => Child = control = new FriendsOnlineStatusControl
|
||||
public void SetUp() => Schedule(() => Child = control = new FriendOnlineStreamControl
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -55,9 +55,9 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
}
|
||||
}));
|
||||
|
||||
AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.All)?.Count == 3);
|
||||
AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Online)?.Count == 1);
|
||||
AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == FriendsOnlineStatus.Offline)?.Count == 2);
|
||||
AddAssert("3 users", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.All)?.Count == 3);
|
||||
AddAssert("1 online user", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.Online)?.Count == 1);
|
||||
AddAssert("2 offline users", () => control.Items.FirstOrDefault(item => item.Status == OnlineStatus.Offline)?.Count == 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Home.Friends;
|
||||
using osu.Game.Overlays.Dashboard.Friends;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
@ -70,6 +70,17 @@ namespace osu.Game.Tournament.Components
|
||||
|
||||
protected override ChatLine CreateMessage(Message message) => new MatchMessage(message);
|
||||
|
||||
protected override StandAloneDrawableChannel CreateDrawableChannel(Channel channel) => new MatchChannel(channel);
|
||||
|
||||
public class MatchChannel : StandAloneDrawableChannel
|
||||
{
|
||||
public MatchChannel(Channel channel)
|
||||
: base(channel)
|
||||
{
|
||||
ScrollbarVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected class MatchMessage : StandAloneMessage
|
||||
{
|
||||
public MatchMessage(Message message)
|
||||
|
@ -24,7 +24,13 @@ namespace osu.Game.Tournament.Models
|
||||
// only used for serialisation
|
||||
public List<TournamentProgression> Progressions = new List<TournamentProgression>();
|
||||
|
||||
[JsonIgnore]
|
||||
[JsonIgnore] // updated manually in TournamentGameBase
|
||||
public Bindable<TournamentMatch> CurrentMatch = new Bindable<TournamentMatch>();
|
||||
|
||||
public Bindable<int> ChromaKeyWidth = new BindableInt(1024)
|
||||
{
|
||||
MinValue = 640,
|
||||
MaxValue = 1366,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Tournament.Components;
|
||||
using osu.Game.Tournament.IPC;
|
||||
using osu.Game.Tournament.Models;
|
||||
@ -35,6 +36,8 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
[Resolved]
|
||||
private TournamentMatchChatDisplay chat { get; set; }
|
||||
|
||||
private Box chroma;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(LadderInfo ladder, MatchIPCInfo ipc, Storage storage)
|
||||
{
|
||||
@ -60,11 +63,10 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
Origin = Anchor.TopCentre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
chroma = new Box
|
||||
{
|
||||
// chroma key area for stable gameplay
|
||||
Name = "chroma",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Height = 512,
|
||||
@ -93,6 +95,12 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Toggle chat",
|
||||
Action = () => { State.Value = State.Value == TourneyState.Idle ? TourneyState.Playing : TourneyState.Idle; }
|
||||
},
|
||||
new SettingsSlider<int>
|
||||
{
|
||||
LabelText = "Chroma Width",
|
||||
Bindable = LadderInfo.ChromaKeyWidth,
|
||||
KeyboardStep = 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -101,6 +109,8 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
State.BindTo(ipc.State);
|
||||
State.BindValueChanged(stateChanged, true);
|
||||
|
||||
ladder.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true);
|
||||
|
||||
currentMatch.BindValueChanged(m =>
|
||||
{
|
||||
warmup.Value = m.NewValue.Team1Score.Value + m.NewValue.Team2Score.Value == 0;
|
||||
|
@ -116,7 +116,7 @@ namespace osu.Game.Tournament.Screens
|
||||
{
|
||||
windowSize.Value = new Size((int)(1920 / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), 1080);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
protected ChannelManager ChannelManager;
|
||||
|
||||
private DrawableChannel drawableChannel;
|
||||
private StandAloneDrawableChannel drawableChannel;
|
||||
|
||||
private readonly bool postingTextbox;
|
||||
|
||||
@ -77,6 +77,9 @@ namespace osu.Game.Online.Chat
|
||||
ChannelManager = manager;
|
||||
}
|
||||
|
||||
protected virtual StandAloneDrawableChannel CreateDrawableChannel(Channel channel) =>
|
||||
new StandAloneDrawableChannel(channel);
|
||||
|
||||
private void postMessage(TextBox sender, bool newtext)
|
||||
{
|
||||
var text = textbox.Text.Trim();
|
||||
@ -100,14 +103,14 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
if (e.NewValue == null) return;
|
||||
|
||||
AddInternal(drawableChannel = new StandAloneDrawableChannel(e.NewValue)
|
||||
{
|
||||
CreateChatLineAction = CreateMessage,
|
||||
Padding = new MarginPadding { Bottom = postingTextbox ? textbox_height : 0 }
|
||||
});
|
||||
drawableChannel = CreateDrawableChannel(e.NewValue);
|
||||
drawableChannel.CreateChatLineAction = CreateMessage;
|
||||
drawableChannel.Padding = new MarginPadding { Bottom = postingTextbox ? textbox_height : 0 };
|
||||
|
||||
AddInternal(drawableChannel);
|
||||
}
|
||||
|
||||
protected class StandAloneDrawableChannel : DrawableChannel
|
||||
public class StandAloneDrawableChannel : DrawableChannel
|
||||
{
|
||||
public Func<Message, ChatLine> CreateChatLineAction;
|
||||
|
||||
|
@ -47,6 +47,8 @@ namespace osu.Game
|
||||
{
|
||||
public const string CLIENT_STREAM_NAME = "lazer";
|
||||
|
||||
public const int SAMPLE_CONCURRENCY = 6;
|
||||
|
||||
protected OsuConfigManager LocalConfig;
|
||||
|
||||
protected BeatmapManager BeatmapManager;
|
||||
@ -153,6 +155,8 @@ namespace osu.Game
|
||||
AddFont(Resources, @"Fonts/Venera-Bold");
|
||||
AddFont(Resources, @"Fonts/Venera-Black");
|
||||
|
||||
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
|
||||
|
||||
runMigrations();
|
||||
|
||||
dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore<byte[]>(Resources, "Skins/Legacy")));
|
||||
|
@ -26,6 +26,20 @@ namespace osu.Game.Overlays.Chat
|
||||
protected FillFlowContainer ChatLineFlow;
|
||||
private OsuScrollContainer scroll;
|
||||
|
||||
private bool scrollbarVisible = true;
|
||||
|
||||
public bool ScrollbarVisible
|
||||
{
|
||||
set
|
||||
{
|
||||
if (scrollbarVisible == value) return;
|
||||
|
||||
scrollbarVisible = value;
|
||||
if (scroll != null)
|
||||
scroll.ScrollbarVisible = value;
|
||||
}
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
@ -44,6 +58,7 @@ namespace osu.Game.Overlays.Chat
|
||||
Masking = true,
|
||||
Child = scroll = new OsuScrollContainer
|
||||
{
|
||||
ScrollbarVisible = scrollbarVisible,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
// Some chat lines have effects that slightly protrude to the bottom,
|
||||
// which we do not want to mask away, hence the padding.
|
||||
|
267
osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
Normal file
267
osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
Normal file
@ -0,0 +1,267 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
public class FriendDisplay : CompositeDrawable
|
||||
{
|
||||
private List<User> users = new List<User>();
|
||||
|
||||
public List<User> Users
|
||||
{
|
||||
get => users;
|
||||
set
|
||||
{
|
||||
users = value;
|
||||
|
||||
onlineStreamControl.Populate(value);
|
||||
}
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
private GetFriendsRequest request;
|
||||
private CancellationTokenSource cancellationToken;
|
||||
|
||||
private Drawable currentContent;
|
||||
|
||||
private readonly FriendOnlineStreamControl onlineStreamControl;
|
||||
private readonly Box background;
|
||||
private readonly Box controlBackground;
|
||||
private readonly UserListToolbar userListToolbar;
|
||||
private readonly Container itemsPlaceholder;
|
||||
private readonly LoadingLayer loading;
|
||||
|
||||
public FriendDisplay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
controlBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = 20,
|
||||
Horizontal = 45
|
||||
},
|
||||
Child = onlineStreamControl = new FriendOnlineStreamControl(),
|
||||
}
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = "User List",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Margin = new MarginPadding { Bottom = 20 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Horizontal = 40,
|
||||
Vertical = 20
|
||||
},
|
||||
Child = userListToolbar = new UserListToolbar
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
itemsPlaceholder = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = 50 }
|
||||
},
|
||||
loading = new LoadingLayer(itemsPlaceholder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
background.Colour = colourProvider.Background4;
|
||||
controlBackground.Colour = colourProvider.Background5;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
onlineStreamControl.Current.BindValueChanged(_ => recreatePanels());
|
||||
userListToolbar.DisplayStyle.BindValueChanged(_ => recreatePanels());
|
||||
userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels());
|
||||
}
|
||||
|
||||
public void Fetch()
|
||||
{
|
||||
if (!api.IsLoggedIn)
|
||||
return;
|
||||
|
||||
request = new GetFriendsRequest();
|
||||
request.Success += response => Schedule(() => Users = response);
|
||||
api.Queue(request);
|
||||
}
|
||||
|
||||
private void recreatePanels()
|
||||
{
|
||||
if (!users.Any())
|
||||
return;
|
||||
|
||||
cancellationToken?.Cancel();
|
||||
|
||||
if (itemsPlaceholder.Any())
|
||||
loading.Show();
|
||||
|
||||
var sortedUsers = sortUsers(getUsersInCurrentGroup());
|
||||
|
||||
LoadComponentAsync(createTable(sortedUsers), addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
|
||||
}
|
||||
|
||||
private List<User> getUsersInCurrentGroup()
|
||||
{
|
||||
switch (onlineStreamControl.Current.Value?.Status)
|
||||
{
|
||||
default:
|
||||
case OnlineStatus.All:
|
||||
return users;
|
||||
|
||||
case OnlineStatus.Offline:
|
||||
return users.Where(u => !u.IsOnline).ToList();
|
||||
|
||||
case OnlineStatus.Online:
|
||||
return users.Where(u => u.IsOnline).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private void addContentToPlaceholder(Drawable content)
|
||||
{
|
||||
loading.Hide();
|
||||
|
||||
var lastContent = currentContent;
|
||||
|
||||
if (lastContent != null)
|
||||
{
|
||||
lastContent.FadeOut(100, Easing.OutQuint).Expire();
|
||||
lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y);
|
||||
}
|
||||
|
||||
itemsPlaceholder.Add(currentContent = content);
|
||||
currentContent.FadeIn(200, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private FillFlowContainer createTable(List<User> users)
|
||||
{
|
||||
var style = userListToolbar.DisplayStyle.Value;
|
||||
|
||||
return new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 2),
|
||||
Children = users.Select(u => createUserPanel(u, style)).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private UserPanel createUserPanel(User user, OverlayPanelDisplayStyle style)
|
||||
{
|
||||
switch (style)
|
||||
{
|
||||
default:
|
||||
case OverlayPanelDisplayStyle.Card:
|
||||
return new UserGridPanel(user).With(panel =>
|
||||
{
|
||||
panel.Anchor = Anchor.TopCentre;
|
||||
panel.Origin = Anchor.TopCentre;
|
||||
panel.Width = 290;
|
||||
});
|
||||
|
||||
case OverlayPanelDisplayStyle.List:
|
||||
return new UserListPanel(user);
|
||||
}
|
||||
}
|
||||
|
||||
private List<User> sortUsers(List<User> unsorted)
|
||||
{
|
||||
switch (userListToolbar.SortCriteria.Value)
|
||||
{
|
||||
default:
|
||||
case UserSortCriteria.LastVisit:
|
||||
return unsorted.OrderByDescending(u => u.LastVisit).ToList();
|
||||
|
||||
case UserSortCriteria.Rank:
|
||||
return unsorted.OrderByDescending(u => u.CurrentModeRank.HasValue).ThenBy(u => u.CurrentModeRank ?? 0).ToList();
|
||||
|
||||
case UserSortCriteria.Username:
|
||||
return unsorted.OrderBy(u => u.Username).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
request?.Cancel();
|
||||
cancellationToken?.Cancel();
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
public class FriendOnlineStreamControl : OverlayStreamControl<FriendStream>
|
||||
{
|
||||
protected override OverlayStreamItem<FriendStream> CreateStreamItem(FriendStream value) => new FriendsOnlineStatusItem(value);
|
||||
|
||||
public void Populate(List<User> users)
|
||||
{
|
||||
Clear();
|
||||
|
||||
var userCount = users.Count;
|
||||
var onlineUsersCount = users.Count(user => user.IsOnline);
|
||||
|
||||
AddItem(new FriendStream(OnlineStatus.All, userCount));
|
||||
AddItem(new FriendStream(OnlineStatus.Online, onlineUsersCount));
|
||||
AddItem(new FriendStream(OnlineStatus.Offline, userCount - onlineUsersCount));
|
||||
|
||||
Current.Value = Items.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
18
osu.Game/Overlays/Dashboard/Friends/FriendStream.cs
Normal file
18
osu.Game/Overlays/Dashboard/Friends/FriendStream.cs
Normal file
@ -0,0 +1,18 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
public class FriendStream
|
||||
{
|
||||
public OnlineStatus Status { get; }
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
public FriendStream(OnlineStatus status, int count)
|
||||
{
|
||||
Status = status;
|
||||
Count = count;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,11 +5,11 @@ using System;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Home.Friends
|
||||
namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
public class FriendsOnlineStatusItem : OverlayStreamItem<FriendsBundle>
|
||||
public class FriendsOnlineStatusItem : OverlayStreamItem<FriendStream>
|
||||
{
|
||||
public FriendsOnlineStatusItem(FriendsBundle value)
|
||||
public FriendsOnlineStatusItem(FriendStream value)
|
||||
: base(value)
|
||||
{
|
||||
}
|
||||
@ -22,13 +22,13 @@ namespace osu.Game.Overlays.Home.Friends
|
||||
{
|
||||
switch (Value.Status)
|
||||
{
|
||||
case FriendsOnlineStatus.All:
|
||||
case OnlineStatus.All:
|
||||
return Color4.White;
|
||||
|
||||
case FriendsOnlineStatus.Online:
|
||||
case OnlineStatus.Online:
|
||||
return colours.GreenLight;
|
||||
|
||||
case FriendsOnlineStatus.Offline:
|
||||
case OnlineStatus.Offline:
|
||||
return Color4.Black;
|
||||
|
||||
default:
|
12
osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs
Normal file
12
osu.Game/Overlays/Dashboard/Friends/OnlineStatus.cs
Normal file
@ -0,0 +1,12 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
public enum OnlineStatus
|
||||
{
|
||||
All,
|
||||
Online,
|
||||
Offline
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osuTK;
|
||||
using osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Overlays.Home.Friends
|
||||
namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
public class UserListToolbar : CompositeDrawable
|
||||
{
|
@ -3,7 +3,7 @@
|
||||
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace osu.Game.Overlays.Home.Friends
|
||||
namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
public class UserSortTabControl : OverlaySortTabControl<UserSortCriteria>
|
||||
{
|
@ -1,25 +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.
|
||||
|
||||
namespace osu.Game.Overlays.Home.Friends
|
||||
{
|
||||
public class FriendsBundle
|
||||
{
|
||||
public FriendsOnlineStatus Status { get; }
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
public FriendsBundle(FriendsOnlineStatus status, int count)
|
||||
{
|
||||
Status = status;
|
||||
Count = count;
|
||||
}
|
||||
}
|
||||
|
||||
public enum FriendsOnlineStatus
|
||||
{
|
||||
All,
|
||||
Online,
|
||||
Offline
|
||||
}
|
||||
}
|
@ -1,26 +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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Overlays.Home.Friends
|
||||
{
|
||||
public class FriendsOnlineStatusControl : OverlayStreamControl<FriendsBundle>
|
||||
{
|
||||
protected override OverlayStreamItem<FriendsBundle> CreateStreamItem(FriendsBundle value) => new FriendsOnlineStatusItem(value);
|
||||
|
||||
public void Populate(List<User> users)
|
||||
{
|
||||
var userCount = users.Count;
|
||||
var onlineUsersCount = users.Count(user => user.IsOnline);
|
||||
|
||||
AddItem(new FriendsBundle(FriendsOnlineStatus.All, userCount));
|
||||
AddItem(new FriendsBundle(FriendsOnlineStatus.Online, onlineUsersCount));
|
||||
AddItem(new FriendsBundle(FriendsOnlineStatus.Offline, userCount - onlineUsersCount));
|
||||
|
||||
Current.Value = Items.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
@ -8,16 +8,14 @@ namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
public class SettingsCheckbox : SettingsItem<bool>
|
||||
{
|
||||
private OsuCheckbox checkbox;
|
||||
|
||||
private string labelText;
|
||||
|
||||
protected override Drawable CreateControl() => checkbox = new OsuCheckbox();
|
||||
protected override Drawable CreateControl() => new OsuCheckbox();
|
||||
|
||||
public override string LabelText
|
||||
{
|
||||
get => labelText;
|
||||
set => checkbox.LabelText = labelText = value;
|
||||
set => ((OsuCheckbox)Control).LabelText = labelText = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,22 +33,24 @@ namespace osu.Game.Overlays.Settings
|
||||
|
||||
protected readonly FillFlowContainer FlowContent;
|
||||
|
||||
private SpriteText text;
|
||||
private SpriteText labelText;
|
||||
|
||||
public bool ShowsDefaultIndicator = true;
|
||||
|
||||
public virtual string LabelText
|
||||
{
|
||||
get => text?.Text ?? string.Empty;
|
||||
get => labelText?.Text ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (text == null)
|
||||
if (labelText == null)
|
||||
{
|
||||
// construct lazily for cases where the label is not needed (may be provided by the Control).
|
||||
FlowContent.Insert(-1, text = new OsuSpriteText());
|
||||
FlowContent.Insert(-1, labelText = new OsuSpriteText());
|
||||
|
||||
updateDisabled();
|
||||
}
|
||||
|
||||
text.Text = value;
|
||||
labelText.Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,13 +98,19 @@ namespace osu.Game.Overlays.Settings
|
||||
if (controlWithCurrent != null)
|
||||
{
|
||||
controlWithCurrent.Current.ValueChanged += _ => SettingChanged?.Invoke();
|
||||
controlWithCurrent.Current.DisabledChanged += disabled => { Colour = disabled ? Color4.Gray : Color4.White; };
|
||||
controlWithCurrent.Current.DisabledChanged += _ => updateDisabled();
|
||||
|
||||
if (ShowsDefaultIndicator)
|
||||
restoreDefaultButton.Bindable = controlWithCurrent.Current;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisabled()
|
||||
{
|
||||
if (labelText != null)
|
||||
labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1;
|
||||
}
|
||||
|
||||
private class RestoreDefaultValueButton : Container, IHasTooltip
|
||||
{
|
||||
private Bindable<T> bindable;
|
||||
|
@ -158,6 +158,7 @@ namespace osu.Game.Rulesets.UI
|
||||
dependencies.Cache(textureStore);
|
||||
|
||||
localSampleStore = dependencies.Get<AudioManager>().GetSampleStore(new NamespacedResourceStore<byte[]>(resources, "Samples"));
|
||||
localSampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
|
||||
dependencies.CacheAs<ISampleStore>(new FallbackSampleStore(localSampleStore, dependencies.Get<ISampleStore>()));
|
||||
}
|
||||
|
||||
|
@ -333,8 +333,7 @@ namespace osu.Game.Screens.Select
|
||||
else
|
||||
set = visibleSets.ElementAt(RNG.Next(visibleSets.Count));
|
||||
|
||||
var visibleBeatmaps = set.Beatmaps.Where(s => !s.Filtered.Value).ToList();
|
||||
select(visibleBeatmaps[RNG.Next(visibleBeatmaps.Count)]);
|
||||
select(set);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -751,13 +750,17 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
public CarouselRoot(BeatmapCarousel carousel)
|
||||
{
|
||||
// root should always remain selected. if not, PerformSelection will not be called.
|
||||
State.Value = CarouselItemState.Selected;
|
||||
State.ValueChanged += state => State.Value = CarouselItemState.Selected;
|
||||
|
||||
this.carousel = carousel;
|
||||
}
|
||||
|
||||
protected override void PerformSelection()
|
||||
{
|
||||
if (LastSelected == null)
|
||||
carousel.SelectNextRandom();
|
||||
if (LastSelected == null || LastSelected.Filtered.Value)
|
||||
carousel?.SelectNextRandom();
|
||||
else
|
||||
base.PerformSelection();
|
||||
}
|
||||
|
@ -104,7 +104,8 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
|
||||
private void updateSelected(CarouselItem newSelection)
|
||||
{
|
||||
LastSelected = newSelection;
|
||||
if (newSelection != null)
|
||||
LastSelected = newSelection;
|
||||
updateSelectedIndex();
|
||||
}
|
||||
|
||||
|
@ -150,6 +150,7 @@ namespace osu.Game.Screens.Select
|
||||
},
|
||||
Child = Carousel = new BeatmapCarousel
|
||||
{
|
||||
AllowSelection = false, // delay any selection until our bindables are ready to make a good choice.
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -655,6 +656,8 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
bindBindables();
|
||||
|
||||
Carousel.AllowSelection = true;
|
||||
|
||||
// If a selection was already obtained, do not attempt to update the selected beatmap.
|
||||
if (Carousel.SelectedBeatmapSet != null)
|
||||
return;
|
||||
|
@ -52,7 +52,11 @@ namespace osu.Game.Skinning
|
||||
|
||||
if (storage != null)
|
||||
{
|
||||
Samples = audioManager?.GetSampleStore(storage);
|
||||
var samples = audioManager?.GetSampleStore(storage);
|
||||
if (samples != null)
|
||||
samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
|
||||
|
||||
Samples = samples;
|
||||
Textures = new TextureStore(new TextureLoaderStore(storage));
|
||||
|
||||
(storage as ResourceStore<byte[]>)?.AddExtension("ogg");
|
||||
|
@ -69,6 +69,9 @@ namespace osu.Game.Users
|
||||
[JsonProperty(@"support_level")]
|
||||
public int SupportLevel;
|
||||
|
||||
[JsonProperty(@"current_mode_rank")]
|
||||
public int? CurrentModeRank;
|
||||
|
||||
[JsonProperty(@"is_gmt")]
|
||||
public bool IsGMT;
|
||||
|
||||
|
@ -99,6 +99,9 @@ namespace osu.Game.Users
|
||||
{
|
||||
base.LoadComplete();
|
||||
Status.TriggerChange();
|
||||
|
||||
// Colour should be applied immediately on first load.
|
||||
statusIcon.FinishTransforms();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
|
Loading…
Reference in New Issue
Block a user