timeline works💪

This commit is contained in:
sim1222 2022-12-14 02:42:41 +09:00
parent 387f434257
commit dd81505982
No known key found for this signature in database
GPG Key ID: 83C11C5D51F42825
48 changed files with 1985 additions and 514 deletions

View File

@ -11,6 +11,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Responses.Types;
namespace osu.Game.Misskey.Users.Drawables
{
@ -35,7 +36,7 @@ namespace osu.Game.Misskey.Users.Drawables
set => clickableArea.TooltipText = value ? (user?.Username ?? string.Empty) : default_tooltip_text;
}
private readonly I user;
private readonly User user;
[Resolved(CanBeNull = true)]
private OsuGame game { get; set; }
@ -47,7 +48,7 @@ namespace osu.Game.Misskey.Users.Drawables
/// If <see cref="OpenOnClick"/> is <c>true</c>, clicking will open the user's profile.
/// </summary>
/// <param name="user">The user. A null value will get a placeholder avatar.</param>
public ClickableAvatar(I user = null)
public ClickableAvatar(User user = null)
{
this.user = user;
@ -66,6 +67,11 @@ namespace osu.Game.Misskey.Users.Drawables
LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add);
}
protected override void LoadComplete()
{
CornerRadius = DrawHeight / 2;
}
private void openProfile()
{
// if (!string.IsNullOrEmpty(user?.Username))

View File

@ -9,19 +9,20 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Responses.Types;
namespace osu.Game.Misskey.Users.Drawables
{
[LongRunningLoad]
public partial class DrawableAvatar : Sprite
{
private readonly I user;
private readonly User user;
/// <summary>
/// A simple, non-interactable avatar sprite for the specified user.
/// </summary>
/// <param name="user">The user. A null value will get a placeholder avatar.</param>
public DrawableAvatar(I user = null)
public DrawableAvatar(User user = null)
{
this.user = user;

View File

@ -8,15 +8,16 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Responses.Types;
namespace osu.Game.Misskey.Users.Drawables
{
/// <summary>
/// An avatar which can update to a new user when needed.
/// </summary>
public partial class UpdateableAvatar : ModelBackedDrawable<I>
public partial class UpdateableAvatar : ModelBackedDrawable<User>
{
public I User
public User User
{
get => Model;
set => Model = value;
@ -59,7 +60,7 @@ namespace osu.Game.Misskey.Users.Drawables
/// <param name="isInteractive">If set to true, hover/click sounds will play and clicking the avatar will open the user's profile.</param>
/// <param name="showUsernameTooltip">Whether to show the username rather than "view profile" on the tooltip. (note: this only applies if <paramref name="isInteractive"/> is also true)</param>
/// <param name="showGuestOnNull">Whether to show a default guest representation on null user (as opposed to nothing).</param>
public UpdateableAvatar(I user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
public UpdateableAvatar(User user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
{
this.isInteractive = isInteractive;
this.showUsernameTooltip = showUsernameTooltip;
@ -68,7 +69,7 @@ namespace osu.Game.Misskey.Users.Drawables
User = user;
}
protected override Drawable CreateDrawable(I user)
protected override Drawable CreateDrawable(User user)
{
if (user == null && !showGuestOnNull)
return null;

View File

@ -15,6 +15,7 @@ using osu.Game.Misskey.Users.Drawables;
using osu.Framework.Input.Events;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Responses.Types;
namespace osu.Game.Misskey.Users
{
@ -29,7 +30,7 @@ namespace osu.Game.Misskey.Users
private SpriteIcon statusIcon;
private OsuSpriteText statusMessage;
protected ExtendedUserPanel(I user)
protected ExtendedUserPanel(User user)
: base(user)
{
}

View File

@ -14,19 +14,20 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Responses.Types;
using osuTK.Graphics;
namespace osu.Game.Misskey.Users
{
public partial class UserCoverBackground : ModelBackedDrawable<I>
public partial class UserCoverBackground : ModelBackedDrawable<User>
{
public I User
public User User
{
get => Model;
set => Model = value;
}
protected override Drawable CreateDrawable(I user) => new Cover(user);
protected override Drawable CreateDrawable(User user) => new Cover(user);
protected override double LoadDelay => 300;
@ -41,9 +42,9 @@ namespace osu.Game.Misskey.Users
[LongRunningLoad]
private partial class Cover : CompositeDrawable
{
private readonly I user;
private readonly User user;
public Cover(I user)
public Cover(User user)
{
this.user = user;

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Responses.Types;
using osu.Game.Overlays.Profile.Header.Components;
using osuTK;
@ -17,7 +18,7 @@ namespace osu.Game.Misskey.Users
{
private const int margin = 10;
public UserGridPanel(I user)
public UserGridPanel(User user)
: base(user)
{
Height = 120;

View File

@ -11,6 +11,7 @@ using osuTK.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Responses.Types;
using osuTK;
using osu.Game.Overlays.Profile.Header.Components;
@ -18,7 +19,7 @@ namespace osu.Game.Misskey.Users
{
public partial class UserListPanel : ExtendedUserPanel
{
public UserListPanel(I user)
public UserListPanel(User user)
: base(user)
{
RelativeSizeAxes = Axes.X;

View File

@ -17,12 +17,13 @@ using osu.Game.Graphics.Containers;
using JetBrains.Annotations;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Responses.Types;
namespace osu.Game.Misskey.Users
{
public abstract partial class UserPanel : OsuClickableContainer, IHasContextMenu
{
public readonly I User;
public readonly User User;
/// <summary>
/// Perform an action in addition to showing the user's profile.
@ -34,7 +35,7 @@ namespace osu.Game.Misskey.Users
protected Drawable Background { get; private set; }
protected UserPanel(I user)
protected UserPanel(User user)
: base(HoverSampleSet.Button)
{
if (user == null)

View File

@ -18,9 +18,12 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Misskey.Users;
using osu.Game.Online.MisskeyAPI.Requests;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Users;
using osu.Game.Online.MisskeyAPI.Responses.Types;
using User = osu.Game.Online.MisskeyAPI.Responses.Types.User;
using UserActivity = osu.Game.Users.UserActivity;
namespace osu.Game.Online.MisskeyAPI
{
@ -44,10 +47,10 @@ namespace osu.Game.Online.MisskeyAPI
private string password;
public IBindable<Requests.Responses.I> LocalUser => localUser;
public IBindable<User> LocalUser => localUser;
public IBindable<UserActivity> Activity => activity;
private Bindable<Requests.Responses.I> localUser { get; } = new Bindable<Requests.Responses.I>(createGuestUser());
private Bindable<User> localUser { get; } = new Bindable<User>(createGuestUser());
private Bindable<UserActivity> activity { get; } = new Bindable<UserActivity>();
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
@ -108,7 +111,7 @@ namespace osu.Game.Online.MisskeyAPI
if (queue.Count == 0)
{
log.Add(@"Queueing a ping request");
Queue(new Requests.I(AccessToken));
Queue(new Requests.User(AccessToken));
}
break;
@ -149,7 +152,7 @@ namespace osu.Game.Online.MisskeyAPI
}
}
var userReq = new Requests.I(AccessToken);
var userReq = new Requests.User(AccessToken);
userReq.Failure += ex =>
{
@ -428,7 +431,7 @@ namespace osu.Game.Online.MisskeyAPI
flushQueue();
}
private static Requests.Responses.I createGuestUser() => new GuestUser();
private static User createGuestUser() => new GuestUser();
protected override void Dispose(bool isDisposing)
{
@ -439,7 +442,7 @@ namespace osu.Game.Online.MisskeyAPI
}
}
internal class GuestUser : Requests.Responses.I
internal class GuestUser : User
{
public GuestUser()
{

View File

@ -12,6 +12,7 @@ using Newtonsoft.Json;
using osu.Framework.IO.Network;
using osu.Framework.Logging;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Responses.Types;
namespace osu.Game.Online.MisskeyAPI
{
@ -85,7 +86,7 @@ namespace osu.Game.Online.MisskeyAPI
//// <summary>
//// The currently logged in user. Note that this will only be populated during <see cref="Perform"/>.
//// </summary>
protected I User { get; private set; }
protected User User { get; private set; }
/// <summary>
/// Invoked on successful completion of an API request.

View File

@ -7,6 +7,7 @@ using System;
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Responses.Types;
using osu.Game.Users;
namespace osu.Game.Online.MisskeyAPI
@ -17,7 +18,7 @@ namespace osu.Game.Online.MisskeyAPI
/// The local user.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary>
IBindable<Requests.Responses.I> LocalUser { get; }
IBindable<User> LocalUser { get; }
/// <summary>
/// The current user's activity.

View File

@ -8,12 +8,12 @@ using osu.Framework.IO.Network;
namespace osu.Game.Online.MisskeyAPI.Requests
{
public class I : APIRequest<MisskeyAPI.Requests.Responses.I>
public class User : APIRequest<MisskeyAPI.Responses.Types.User>
{
private string i;
public I(string i)
public User(string i)
{
this.i = i;
}

View File

@ -13,17 +13,25 @@ namespace osu.Game.Online.MisskeyAPI.Requests.Notes
{
private string text;
private string i;
private string? cw;
public Create(string i, string Text)
{
this.text = Text;
this.i = i;
this.text = Text;
}
public Create(string i, string Text, string cw)
{
this.i = i;
this.text = Text;
this.cw = cw;
}
private class ReqBody
{
public string? text;
public string? i;
public string? cw;
};
protected override WebRequest CreateWebRequest()
{
@ -34,6 +42,10 @@ namespace osu.Game.Online.MisskeyAPI.Requests.Notes
text = text,
i = i
};
if (cw != null)
{
body.cw = cw;
}
var json = JsonConvert.SerializeObject(body);
Logger.Log(json, LoggingTarget.Network, LogLevel.Debug);
req.AddRaw(json);

View File

@ -6,41 +6,46 @@ using System.Net.Http;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
using osu.Game.Online.MisskeyAPI.Responses.Types;
using Realms;
namespace osu.Game.Online.MisskeyAPI.Requests.Notes
{
public class HybridTimeline : APIRequest<MisskeyAPI.Requests.Responses.Notes.HybridTimeline>
public class HybridTimeline : APIRequest<Note[]>
{
private readonly string i;
private readonly string sinceId;
private readonly string untilId;
// private readonly string? sinceId;
private readonly string? untilId;
public HybridTimeline(
string i,
string sinceId = "",
string untilId = ""
)
{
this.i = i;
this.sinceId = sinceId;
this.untilId = untilId;
}
public HybridTimeline(string i)
{
this.i = i;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
var body = new Dictionary<string, string>
var body = new Dictionary<string, object>
{
{ "i", i },
{ "limit", 30 },
};
if (sinceId != "")
body.Add("sinceId", sinceId);
if (untilId != "")
// if (sinceId != null)
// body.Add("sinceId", sinceId);
if (untilId != null)
body.Add("untilId", untilId);
req.AddRaw(JsonConvert.SerializeObject(body));

View File

@ -1,267 +0,0 @@
// Copyright (c) sim1222 <kokt@sim1222.com>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace osu.Game.Online.MisskeyAPI.Requests.Responses.Notes
{
public class Channel
{
}
public class Emoji
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
}
public class File
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("createdAt")]
public DateTime? CreatedAt { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("md5")]
public string Md5 { get; set; }
[JsonProperty("size")]
public int? Size { get; set; }
[JsonProperty("isSensitive")]
public bool? IsSensitive { get; set; }
[JsonProperty("blurhash")]
public string Blurhash { get; set; }
[JsonProperty("properties")]
public Properties Properties { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("thumbnailUrl")]
public string ThumbnailUrl { get; set; }
[JsonProperty("comment")]
public string Comment { get; set; }
[JsonProperty("folderId")]
public string FolderId { get; set; }
[JsonProperty("folder")]
public Folder Folder { get; set; }
[JsonProperty("userId")]
public string UserId { get; set; }
[JsonProperty("user")]
public User User { get; set; }
}
public class Folder
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("createdAt")]
public DateTime? CreatedAt { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("foldersCount")]
public int? FoldersCount { get; set; }
[JsonProperty("filesCount")]
public int? FilesCount { get; set; }
[JsonProperty("parentId")]
public string ParentId { get; set; }
[JsonProperty("parent")]
public Parent Parent { get; set; }
}
public class MyReaction
{
}
public class Parent
{
}
public class Poll
{
}
public class Properties
{
[JsonProperty("width")]
public int? Width { get; set; }
[JsonProperty("height")]
public int? Height { get; set; }
[JsonProperty("orientation")]
public int? Orientation { get; set; }
[JsonProperty("avgColor")]
public string AvgColor { get; set; }
}
public class Reactions
{
}
public class Renote
{
}
public class Reply
{
}
public class HybridTimeline
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("createdAt")]
public DateTime? CreatedAt { get; set; }
[JsonProperty("text")]
public string Text { get; set; }
[JsonProperty("cw")]
public string Cw { get; set; }
[JsonProperty("userId")]
public string UserId { get; set; }
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("replyId")]
public string ReplyId { get; set; }
[JsonProperty("renoteId")]
public string RenoteId { get; set; }
[JsonProperty("reply")]
public Reply Reply { get; set; }
[JsonProperty("renote")]
public Renote Renote { get; set; }
[JsonProperty("isHidden")]
public bool? IsHidden { get; set; }
[JsonProperty("visibility")]
public string Visibility { get; set; }
[JsonProperty("mentions")]
public List<string> Mentions { get; set; }
[JsonProperty("visibleUserIds")]
public List<string> VisibleUserIds { get; set; }
[JsonProperty("fileIds")]
public List<string> FileIds { get; set; }
[JsonProperty("files")]
public List<File> Files { get; set; }
[JsonProperty("tags")]
public List<string> Tags { get; set; }
[JsonProperty("poll")]
public Poll Poll { get; set; }
[JsonProperty("channelId")]
public string ChannelId { get; set; }
[JsonProperty("channel")]
public Channel Channel { get; set; }
[JsonProperty("localOnly")]
public bool? LocalOnly { get; set; }
[JsonProperty("emojis")]
public List<Emoji> Emojis { get; set; }
[JsonProperty("reactions")]
public Reactions Reactions { get; set; }
[JsonProperty("renoteCount")]
public int? RenoteCount { get; set; }
[JsonProperty("repliesCount")]
public int? RepliesCount { get; set; }
[JsonProperty("uri")]
public string Uri { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("myReaction")]
public MyReaction MyReaction { get; set; }
}
public class User
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("host")]
public string Host { get; set; }
[JsonProperty("avatarUrl")]
public string AvatarUrl { get; set; }
[JsonProperty("avatarBlurhash")]
public object AvatarBlurhash { get; set; }
[JsonProperty("avatarColor")]
public object AvatarColor { get; set; }
[JsonProperty("isAdmin")]
public bool? IsAdmin { get; set; }
[JsonProperty("isModerator")]
public bool? IsModerator { get; set; }
[JsonProperty("isBot")]
public bool? IsBot { get; set; }
[JsonProperty("isCat")]
public bool? IsCat { get; set; }
[JsonProperty("emojis")]
public List<Emoji> Emojis { get; set; }
[JsonProperty("onlineStatus")]
public string OnlineStatus { get; set; }
}
}

View File

@ -6,6 +6,9 @@
using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Misskey.Users;
using UserStatusOnline = osu.Game.Users.UserStatusOnline;
namespace osu.Game.Online.MisskeyAPI.Responses.Types
{
@ -79,7 +82,7 @@ namespace osu.Game.Online.MisskeyAPI.Responses.Types
public class Choice
{
[JsonProperty("isVoted")]
public bool Id { get; set; }
public bool IsVoted { get; set; }
[JsonProperty("text")]
public string Text { get; set; }
@ -96,7 +99,7 @@ namespace osu.Game.Online.MisskeyAPI.Responses.Types
public bool Multiple { get; set; }
[JsonProperty("choices")]
public Choice Choices { get; set; }
public Choice[] Choices { get; set; }
}
public class Note // https://github.com/misskey-dev/misskey.js/blob/c89374c321aeb1cca2582922d4a9a9be059c691e/src/entities.ts#L128
@ -715,6 +718,13 @@ namespace osu.Game.Online.MisskeyAPI.Responses.Types
public class UserLite // https://github.com/misskey-dev/misskey.js/blob/c89374c321aeb1cca2582922d4a9a9be059c691e/src/entities.ts#L9
{
/// <summary>
/// A user ID which can be used to represent any system user which is not attached to a user profile.
/// </summary>
public const string SYSTEM_USER_ID = "system";
public readonly Bindable<UserStatus> Status = new Bindable<UserStatus>();
[JsonProperty("id")]
public string Id { get; set; }
@ -725,6 +735,9 @@ namespace osu.Game.Online.MisskeyAPI.Responses.Types
[CanBeNull]
public string Host { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("onlineStatus")] // 'online' | 'active' | 'offline' | 'unknown'
public string OnlineStatus { get; set; }

View File

@ -28,6 +28,7 @@ using osu.Game.Overlays.BeatmapListing;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
using ExpandedContentScrollContainer = osu.Game.Screens.Misskey.Components.Note.Cards.ExpandedContentScrollContainer;
namespace osu.Game.Overlays
{

View File

@ -18,6 +18,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Rankings.Tables;
using osu.Game.Rulesets;
using osuTK;
using ExpandedContentScrollContainer = osu.Game.Screens.Misskey.Components.Note.Cards.ExpandedContentScrollContainer;
namespace osu.Game.Overlays.Rankings
{

View File

@ -13,26 +13,30 @@ using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osuTK;
namespace osu.Game.Screens.Misskey.Components
{
[LongRunningLoad]
public partial class Avatar : Sprite
{
// private readonly APIUser user;
private Online.MisskeyAPI.Responses.Types.Note note;
/// <summary>
/// A simple, non-interactable avatar sprite for the specified user.
/// </summary>
///// <param name="user">The user. A null value will get a placeholder avatar.</param>
public Avatar()
public Avatar(Online.MisskeyAPI.Responses.Types.Note note)
{
// this.user = user;
RelativeSizeAxes = Axes.Both;
// RelativeSizeAxes = Axes.Both;
Size = new Vector2(10f);
FillMode = FillMode.Fit;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
this.note = note;
}
[BackgroundDependencyLoader]
@ -43,7 +47,7 @@ namespace osu.Game.Screens.Misskey.Components
// // in remaining cases where this is required (chat tabs, local leaderboard), at which point this should be removed.
// Texture = textures.Get(user.AvatarUrl ?? $@"https://a.ppy.sh/{user.Id}");
Texture ??= textures.Get(@"https://simkey.net/files/thumbnail-328eb27f-f06f-4454-ad52-a79d5f780a6b");
Texture ??= textures.Get(note.User.AvatarUrl);
}
protected override void LoadComplete()

View File

@ -0,0 +1,98 @@
// 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.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics;
using osu.Game.Online;
using osu.Game.Overlays;
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
public partial class BeatmapCardDownloadProgressBar : CompositeDrawable
{
public IBindable<DownloadState> State => state;
private readonly Bindable<DownloadState> state = new Bindable<DownloadState>();
public IBindable<double> Progress => progress;
private readonly BindableDouble progress = new BindableDouble();
public override bool IsPresent => true;
private readonly CircularContainer foreground;
private readonly Box backgroundFill;
private readonly Box foregroundFill;
[Resolved]
private OsuColour colours { get; set; }
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
public BeatmapCardDownloadProgressBar()
{
InternalChildren = new Drawable[]
{
new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Child = backgroundFill = new Box
{
RelativeSizeAxes = Axes.Both,
}
},
foreground = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Child = foregroundFill = new Box
{
RelativeSizeAxes = Axes.Both,
}
}
};
}
[BackgroundDependencyLoader]
private void load()
{
backgroundFill.Colour = colourProvider.Background6;
}
protected override void LoadComplete()
{
base.LoadComplete();
state.BindValueChanged(_ => stateChanged(), true);
progress.BindValueChanged(_ => progressChanged(), true);
}
private void stateChanged()
{
switch (state.Value)
{
case DownloadState.Downloading:
FinishTransforms(true);
foregroundFill.Colour = colourProvider.Highlight1;
break;
case DownloadState.Importing:
foregroundFill.FadeColour(colours.Yellow, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
break;
}
}
private void progressChanged()
{
foreground.ResizeWidthTo((float)progress.Value, progress.Value > 0 ? BeatmapCard.TRANSITION_DURATION : 0, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,33 @@
// 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.
#nullable disable
using osu.Game.Screens.Misskey.Components.Note.Cards.Buttons;
using osu.Game.Screens.Misskey.Components.Note.Cards.Statistics;
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
/// <summary>
/// Stores the current favourite state of a beatmap set.
/// Used to coordinate between <see cref="FavouriteButton"/> and <see cref="FavouritesStatistic"/>.
/// </summary>
public readonly struct BeatmapSetFavouriteState
{
/// <summary>
/// Whether the currently logged-in user has favourited this beatmap.
/// </summary>
public bool Favourited { get; }
/// <summary>
/// The number of favourites that the beatmap set has received, including the currently logged-in user.
/// </summary>
public int FavouriteCount { get; }
public BeatmapSetFavouriteState(bool favourited, int favouriteCount)
{
Favourited = favourited;
FavouriteCount = favouriteCount;
}
}
}

View File

@ -0,0 +1,137 @@
// 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.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Misskey.Components.Note.Cards.Buttons
{
public abstract partial class BeatmapCardIconButton : OsuClickableContainer
{
private Colour4 idleColour;
public Colour4 IdleColour
{
get => idleColour;
set
{
idleColour = value;
if (IsLoaded)
updateState();
}
}
private Colour4 hoverColour;
public Colour4 HoverColour
{
get => hoverColour;
set
{
hoverColour = value;
if (IsLoaded)
updateState();
}
}
private float iconSize;
public float IconSize
{
get => iconSize;
set
{
iconSize = value;
Icon.Size = new Vector2(iconSize);
}
}
protected readonly SpriteIcon Icon;
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly Box hover;
protected BeatmapCardIconButton()
{
Origin = Anchor.Centre;
Anchor = Anchor.Centre;
base.Content.Add(content = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Scale = new Vector2(0.8f),
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new Drawable[]
{
hover = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White.Opacity(0.1f),
Blending = BlendingParameters.Additive,
},
Icon = new SpriteIcon
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Scale = new Vector2(1.2f),
},
}
});
IconSize = 12;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
IdleColour = colourProvider.Light1;
HoverColour = colourProvider.Content1;
}
protected override void LoadComplete()
{
base.LoadComplete();
Enabled.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
}
private void updateState()
{
bool isHovered = IsHovered && Enabled.Value;
hover.FadeTo(isHovered ? 1f : 0f, 500, Easing.OutQuint);
content.ScaleTo(isHovered ? 1 : 0.8f, 500, Easing.OutQuint);
Icon.FadeColour(isHovered ? HoverColour : IdleColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,103 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Screens.Misskey.Components.Note.Cards.Buttons
{
public partial class DownloadButton : BeatmapCardIconButton
{
public Bindable<DownloadState> State { get; } = new Bindable<DownloadState>();
private readonly APIBeatmapSet beatmapSet;
private Bindable<bool> preferNoVideo = null!;
private readonly LoadingSpinner spinner;
[Resolved]
private BeatmapModelDownloader beatmaps { get; set; } = null!;
public DownloadButton(APIBeatmapSet beatmapSet)
{
Icon.Icon = FontAwesome.Solid.Download;
Content.Add(spinner = new LoadingSpinner { Size = new Vector2(IconSize) });
this.beatmapSet = beatmapSet;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
preferNoVideo = config.GetBindable<bool>(OsuSetting.PreferNoVideo);
}
protected override void LoadComplete()
{
base.LoadComplete();
preferNoVideo.BindValueChanged(_ => updateState());
State.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
private void updateState()
{
switch (State.Value)
{
case DownloadState.Unknown:
Action = null;
TooltipText = string.Empty;
break;
case DownloadState.Downloading:
case DownloadState.Importing:
Action = null;
TooltipText = string.Empty;
spinner.Show();
Icon.Hide();
break;
case DownloadState.LocallyAvailable:
Action = null;
TooltipText = string.Empty;
this.FadeOut(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
break;
case DownloadState.NotDownloaded:
if (beatmapSet.Availability.DownloadDisabled)
{
Enabled.Value = false;
TooltipText = BeatmapsetsStrings.AvailabilityDisabled;
return;
}
Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value);
this.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
spinner.Hide();
Icon.Show();
if (!beatmapSet.HasVideo)
TooltipText = BeatmapsetsStrings.PanelDownloadAll;
else
TooltipText = preferNoVideo.Value ? BeatmapsetsStrings.PanelDownloadNoVideo : BeatmapsetsStrings.PanelDownloadVideo;
break;
default:
throw new InvalidOperationException($"Unknown {nameof(DownloadState)} specified.");
}
}
}
}

View File

@ -0,0 +1,88 @@
// 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.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Misskey.Components.Note.Cards.Buttons
{
public partial class FavouriteButton : BeatmapCardIconButton, IHasCurrentValue<BeatmapSetFavouriteState>
{
private readonly BindableWithCurrent<BeatmapSetFavouriteState> current;
public Bindable<BeatmapSetFavouriteState> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly APIBeatmapSet beatmapSet;
private PostBeatmapFavouriteRequest favouriteRequest;
[Resolved]
private IAPIProvider api { get; set; }
public FavouriteButton(APIBeatmapSet beatmapSet)
{
current = new BindableWithCurrent<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
this.beatmapSet = beatmapSet;
}
protected override void LoadComplete()
{
base.LoadComplete();
Action = toggleFavouriteStatus;
current.BindValueChanged(_ => updateState(), true);
}
private void toggleFavouriteStatus()
{
var actionType = current.Value.Favourited ? BeatmapFavouriteAction.UnFavourite : BeatmapFavouriteAction.Favourite;
favouriteRequest?.Cancel();
favouriteRequest = new PostBeatmapFavouriteRequest(beatmapSet.OnlineID, actionType);
Enabled.Value = false;
favouriteRequest.Success += () =>
{
bool favourited = actionType == BeatmapFavouriteAction.Favourite;
current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1));
Enabled.Value = true;
};
favouriteRequest.Failure += e =>
{
Logger.Error(e, $"Failed to {actionType.ToString().ToLowerInvariant()} beatmap: {e.Message}");
Enabled.Value = true;
};
api.Queue(favouriteRequest);
}
private void updateState()
{
if (current.Value.Favourited)
{
Icon.Icon = FontAwesome.Solid.Heart;
TooltipText = BeatmapsetsStrings.ShowDetailsUnfavourite;
}
else
{
Icon.Icon = FontAwesome.Regular.Heart;
TooltipText = BeatmapsetsStrings.ShowDetailsFavourite;
}
}
}
}

View File

@ -0,0 +1,48 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Screens.Misskey.Components.Note.Cards.Buttons
{
public partial class GoToBeatmapButton : BeatmapCardIconButton
{
public IBindable<DownloadState> State => state;
private readonly Bindable<DownloadState> state = new Bindable<DownloadState>();
private readonly APIBeatmapSet beatmapSet;
public GoToBeatmapButton(APIBeatmapSet beatmapSet)
{
this.beatmapSet = beatmapSet;
Icon.Icon = FontAwesome.Solid.AngleDoubleRight;
TooltipText = "Go to beatmap";
}
[BackgroundDependencyLoader(true)]
private void load(OsuGame? game)
{
Action = () => game?.PresentBeatmap(beatmapSet);
}
protected override void LoadComplete()
{
base.LoadComplete();
state.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
private void updateState()
{
this.FadeTo(state.Value == DownloadState.LocallyAvailable ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,154 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Screens.Misskey.Components.Note.Cards.Buttons
{
public partial class PlayButton : OsuHoverContainer
{
public IBindable<double> Progress => progress;
private readonly BindableDouble progress = new BindableDouble();
public BindableBool Playing { get; } = new BindableBool();
private readonly IBeatmapSetInfo beatmapSetInfo;
protected override IEnumerable<Drawable> EffectTargets => icon.Yield();
private readonly SpriteIcon icon;
private readonly LoadingSpinner loadingSpinner;
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; } = null!;
private PreviewTrack? previewTrack;
public PlayButton(IBeatmapSetInfo beatmapSetInfo)
{
this.beatmapSetInfo = beatmapSetInfo;
Anchor = Origin = Anchor.Centre;
// needed for touch input to work when card is not hovered/expanded
AlwaysPresent = true;
Children = new Drawable[]
{
icon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Play,
Size = new Vector2(14)
},
loadingSpinner = new LoadingSpinner
{
Size = new Vector2(14)
}
};
Action = () => Playing.Toggle();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
HoverColour = colours.Yellow;
}
protected override void LoadComplete()
{
base.LoadComplete();
Playing.BindValueChanged(updateState, true);
}
protected override void Update()
{
base.Update();
if (Playing.Value && previewTrack != null && previewTrack.TrackLoaded)
progress.Value = previewTrack.CurrentTime / previewTrack.Length;
else
progress.Value = 0;
}
private void updateState(ValueChangedEvent<bool> playing)
{
icon.Icon = playing.NewValue ? FontAwesome.Solid.Stop : FontAwesome.Solid.Play;
if (!playing.NewValue)
{
stopPreview();
return;
}
if (previewTrack == null)
{
toggleLoading(true);
LoadComponentAsync(previewTrack = previewTrackManager.Get(beatmapSetInfo), onPreviewLoaded);
}
else
tryStartPreview();
}
private void stopPreview()
{
toggleLoading(false);
Playing.Value = false;
previewTrack?.Stop();
}
private void onPreviewLoaded(PreviewTrack loadedPreview)
{
// Make sure that we schedule to after the next audio frame to fix crashes in single-threaded execution.
// See: https://github.com/ppy/osu-framework/issues/4692
Schedule(() =>
{
// another async load might have completed before this one.
// if so, do not make any changes.
if (loadedPreview != previewTrack)
{
loadedPreview.Dispose();
return;
}
AddInternal(loadedPreview);
toggleLoading(false);
loadedPreview.Stopped += () => Schedule(() => Playing.Value = false);
if (Playing.Value)
tryStartPreview();
});
}
private void tryStartPreview()
{
if (previewTrack?.Start() == false)
Playing.Value = false;
}
private void toggleLoading(bool loading)
{
Enabled.Value = !loading;
icon.FadeTo(loading ? 0 : 1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
loadingSpinner.State.Value = loading ? Visibility.Visible : Visibility.Hidden;
}
}
}

View File

@ -10,10 +10,9 @@ using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
namespace osu.Game.Screens.Misskey.Components
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
public abstract partial class DrawableNoteCard : OsuClickableContainer
{
@ -24,38 +23,38 @@ namespace osu.Game.Screens.Misskey.Components
public IBindable<bool> Expanded { get; }
public readonly APIBeatmapSet BeatmapSet;
public readonly Online.MisskeyAPI.Responses.Types.Note Note;
protected readonly Bindable<BeatmapSetFavouriteState> FavouriteState;
// protected readonly Bindable<BeatmapSetFavouriteState> FavouriteState;
protected abstract Drawable IdleContent { get; }
protected abstract Drawable DownloadInProgressContent { get; }
// protected abstract Drawable IdleContent { get; }
// protected abstract Drawable DownloadInProgressContent { get; }
protected readonly BeatmapDownloadTracker DownloadTracker;
// protected readonly BeatmapDownloadTracker DownloadTracker;
protected DrawableNoteCard(APIBeatmapSet beatmapSet, bool allowExpansion = true)
protected DrawableNoteCard(Online.MisskeyAPI.Responses.Types.Note note, bool allowExpansion = true)
: base(HoverSampleSet.Button)
{
Expanded = new BindableBool { Disabled = !allowExpansion };
BeatmapSet = beatmapSet;
FavouriteState = new Bindable<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
DownloadTracker = new BeatmapDownloadTracker(beatmapSet);
Note = note;
// FavouriteState = new Bindable<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(note.HasFavourited, note.FavouriteCount));
// DownloadTracker = new BeatmapDownloadTracker(note);
}
[BackgroundDependencyLoader(true)]
private void load(BeatmapSetOverlay? beatmapSetOverlay)
{
Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID);
// Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(Note.OnlineID);
AddInternal(DownloadTracker);
// AddInternal(DownloadTracker);
}
protected override void LoadComplete()
{
base.LoadComplete();
DownloadTracker.State.BindValueChanged(_ => UpdateState());
// DownloadTracker.State.BindValueChanged(_ => UpdateState());
Expanded.BindValueChanged(_ => UpdateState(), true);
FinishTransforms(true);
}
@ -74,24 +73,24 @@ namespace osu.Game.Screens.Misskey.Components
protected virtual void UpdateState()
{
bool showProgress = DownloadTracker.State.Value == DownloadState.Downloading || DownloadTracker.State.Value == DownloadState.Importing;
// bool showProgress = DownloadTracker.State.Value == DownloadState.Downloading || DownloadTracker.State.Value == DownloadState.Importing;
IdleContent.FadeTo(showProgress ? 0 : 1, TRANSITION_DURATION, Easing.OutQuint);
DownloadInProgressContent.FadeTo(showProgress ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
// IdleContent.FadeTo(showProgress ? 0 : 1, TRANSITION_DURATION, Easing.OutQuint);
// DownloadInProgressContent.FadeTo(showProgress ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
}
/// <summary>
/// Creates a beatmap card of the given <paramref name="size"/> for the supplied <paramref name="beatmapSet"/>.
/// Creates a beatmap card of the given <paramref name="size"/> for the supplied <paramref name="note"/>.
/// </summary>
public static BeatmapCard Create(APIBeatmapSet beatmapSet, BeatmapCardSize size, bool allowExpansion = true)
public static NoteCard Create(Online.MisskeyAPI.Responses.Types.Note note, BeatmapCardSize size, bool allowExpansion = true)
{
switch (size)
{
case BeatmapCardSize.Normal:
return new BeatmapCardNormal(beatmapSet, allowExpansion);
return new NoteCardNormal(note, allowExpansion);
case BeatmapCardSize.Extra:
return new BeatmapCardExtra(beatmapSet, allowExpansion);
return new NoteCardNormal(note, allowExpansion); //todo: extra
default:
throw new ArgumentOutOfRangeException(nameof(size), size, @"Unsupported card size");

View File

@ -0,0 +1,81 @@
// 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.
#nullable disable
using System;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
public partial class ExpandedContentScrollContainer : OsuScrollContainer
{
public const float HEIGHT = 200;
protected override ScrollbarContainer CreateScrollbar(Direction direction) => new ExpandedContentScrollbar(direction);
protected override void Update()
{
base.Update();
Height = Math.Min(Content.DrawHeight, HEIGHT);
ScrollbarVisible = allowScroll;
}
private bool allowScroll => !Precision.AlmostEquals(DrawSize, Content.DrawSize);
protected override bool OnDragStart(DragStartEvent e)
{
if (!allowScroll)
return false;
return base.OnDragStart(e);
}
protected override void OnDrag(DragEvent e)
{
if (!allowScroll)
return;
base.OnDrag(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
if (!allowScroll)
return;
base.OnDragEnd(e);
}
protected override bool OnScroll(ScrollEvent e)
{
if (!allowScroll)
return false;
return base.OnScroll(e);
}
protected override bool OnClick(ClickEvent e) => true;
private partial class ExpandedContentScrollbar : OsuScrollbar
{
public ExpandedContentScrollbar(Direction scrollDir)
: base(scrollDir)
{
}
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
// do not handle hover, as handling hover would make the beatmap card's expanded content not-hovered
// and therefore cause it to hide when trying to drag the scroll bar.
// see: `BeatmapCardContent.dropdownContent` and its `Unhovered` handler.
return false;
}
}
}
}

View File

@ -0,0 +1,27 @@
// 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 osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
public partial class HoverHandlingContainer : Container
{
public Func<HoverEvent, bool>? Hovered { get; set; }
public Action<HoverLostEvent>? Unhovered { get; set; }
protected override bool OnHover(HoverEvent e)
{
bool handledByBase = base.OnHover(e);
return Hovered?.Invoke(e) ?? handledByBase;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
Unhovered?.Invoke(e);
}
}
}

View File

@ -0,0 +1,57 @@
// 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.
#nullable disable
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
public abstract partial class IconPill : CircularContainer, IHasTooltip
{
public Vector2 IconSize
{
get => iconContainer.Size;
set => iconContainer.Size = value;
}
private readonly Container iconContainer;
protected IconPill(IconUsage icon)
{
AutoSizeAxes = Axes.Both;
Masking = true;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
},
iconContainer = new Container
{
Size = new Vector2(22),
Padding = new MarginPadding(5),
Child = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Icon = icon,
},
},
};
}
public abstract LocalisableString TooltipText { get; }
}
}

View File

@ -13,7 +13,7 @@ using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
namespace osu.Game.Screens.Misskey.Components
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
public abstract partial class NoteCard : OsuClickableContainer
{
@ -24,38 +24,38 @@ namespace osu.Game.Screens.Misskey.Components
public IBindable<bool> Expanded { get; }
public readonly APIBeatmapSet BeatmapSet;
public readonly Online.MisskeyAPI.Responses.Types.Note Note;
protected readonly Bindable<BeatmapSetFavouriteState> FavouriteState;
// protected readonly Bindable<BeatmapSetFavouriteState> FavouriteState;
protected abstract Drawable IdleContent { get; }
protected abstract Drawable DownloadInProgressContent { get; }
protected readonly BeatmapDownloadTracker DownloadTracker;
// protected readonly BeatmapDownloadTracker DownloadTracker;
protected NoteCard(APIBeatmapSet beatmapSet, bool allowExpansion = true)
protected NoteCard(Online.MisskeyAPI.Responses.Types.Note note, bool allowExpansion = true)
: base(HoverSampleSet.Button)
{
Expanded = new BindableBool { Disabled = !allowExpansion };
BeatmapSet = beatmapSet;
FavouriteState = new Bindable<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
DownloadTracker = new BeatmapDownloadTracker(beatmapSet);
Note = note;
// FavouriteState = new Bindable<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(note.HasFavourited, note.FavouriteCount));
// DownloadTracker = new BeatmapDownloadTracker(note);
}
[BackgroundDependencyLoader(true)]
private void load(BeatmapSetOverlay? beatmapSetOverlay)
{
Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID);
// Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(Note.OnlineID);
AddInternal(DownloadTracker);
// AddInternal(DownloadTracker);
}
protected override void LoadComplete()
{
base.LoadComplete();
DownloadTracker.State.BindValueChanged(_ => UpdateState());
// DownloadTracker.State.BindValueChanged(_ => UpdateState());
Expanded.BindValueChanged(_ => UpdateState(), true);
FinishTransforms(true);
}
@ -74,24 +74,24 @@ namespace osu.Game.Screens.Misskey.Components
protected virtual void UpdateState()
{
bool showProgress = DownloadTracker.State.Value == DownloadState.Downloading || DownloadTracker.State.Value == DownloadState.Importing;
IdleContent.FadeTo(showProgress ? 0 : 1, TRANSITION_DURATION, Easing.OutQuint);
DownloadInProgressContent.FadeTo(showProgress ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
// bool showProgress = DownloadTracker.State.Value == DownloadState.Downloading || DownloadTracker.State.Value == DownloadState.Importing;
//
// IdleContent.FadeTo(showProgress ? 0 : 1, TRANSITION_DURATION, Easing.OutQuint);
// DownloadInProgressContent.FadeTo(showProgress ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
}
/// <summary>
/// Creates a beatmap card of the given <paramref name="size"/> for the supplied <paramref name="beatmapSet"/>.
/// Creates a beatmap card of the given <paramref name="size"/> for the supplied <paramref name="note"/>.
/// </summary>
public static BeatmapCard Create(APIBeatmapSet beatmapSet, BeatmapCardSize size, bool allowExpansion = true)
public static NoteCard Create(Online.MisskeyAPI.Responses.Types.Note note, BeatmapCardSize size, bool allowExpansion = true)
{
switch (size)
{
case BeatmapCardSize.Normal:
return new BeatmapCardNormal(beatmapSet, allowExpansion);
return new NoteCardNormal(note, allowExpansion);
case BeatmapCardSize.Extra:
return new BeatmapCardExtra(beatmapSet, allowExpansion);
// case BeatmapCardSize.Extra:
// return new BeatmapCardExtra(note, allowExpansion);
default:
throw new ArgumentOutOfRangeException(nameof(size), size, @"Unsupported card size");

View File

@ -0,0 +1,75 @@
// 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.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Graphics;
using osu.Game.Misskey.Users.Drawables;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Responses.Types;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
public partial class NoteCardAvatar : Container
{
public BindableBool Dimmed { get; } = new BindableBool();
private Drawable avatar;
private readonly User user;
public NoteCardAvatar(Online.MisskeyAPI.Responses.Types.Note note)
{
this.user = note.User;
}
[BackgroundDependencyLoader]
private void load()
{
ClickableAvatar internalAvatar;
Children = new[]
{
avatar = new DelayedLoadWrapper(
internalAvatar = new ClickableAvatar(user)
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 50,
// EdgeEffect = new EdgeEffectParameters
// {
// Type = EdgeEffectType.Shadow,
// Radius = 1,
// Colour = Color4.Black.Opacity(0.2f),
// },
})
{
RelativeSizeAxes = Axes.Both,
// Size = new Vector2(HEIGHT - edge_margin * 2, HEIGHT - edge_margin * 2),
}
};
// internalAvatar.OnLoadComplete += d => d.FadeInFromZero(200);
}
protected override void LoadComplete()
{
base.LoadComplete();
}
private void updateState()
{
}
}
}

View File

@ -0,0 +1,156 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
public partial class NoteCardContent : CompositeDrawable
{
public Drawable MainContent
{
set => bodyContent.Child = value;
}
public Drawable ExpandedContent
{
set => dropdownScroll.Child = value;
}
public IBindable<bool> Expanded => expanded;
private readonly BindableBool expanded = new BindableBool();
private readonly Box background;
private readonly Container content;
private readonly Container bodyContent;
private readonly Container dropdownContent;
private readonly OsuScrollContainer dropdownScroll;
private readonly Container borderContainer;
public NoteCardContent(float height)
{
RelativeSizeAxes = Axes.X;
Height = height;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChild = content = new HoverHandlingContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
CornerRadius = NoteCard.CORNER_RADIUS,
Masking = true,
Unhovered = _ => updateFromHoverChange(),
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
bodyContent = new Container
{
RelativeSizeAxes = Axes.X,
Height = height,
CornerRadius = NoteCard.CORNER_RADIUS,
Masking = true,
},
dropdownContent = new HoverHandlingContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = height },
Alpha = 0,
Hovered = _ =>
{
updateFromHoverChange();
return true;
},
Unhovered = _ => updateFromHoverChange(),
Child = dropdownScroll = new ExpandedContentScrollContainer
{
RelativeSizeAxes = Axes.X,
ScrollbarVisible = false
}
},
borderContainer = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = NoteCard.CORNER_RADIUS,
Masking = true,
BorderThickness = 3,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
};
}
[BackgroundDependencyLoader]
private void load()
{
background.Colour = Colour4.Gray;
borderContainer.BorderColour = Colour4.White;
}
protected override void LoadComplete()
{
base.LoadComplete();
Expanded.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
private ScheduledDelegate? scheduledExpandedChange;
public void ExpandAfterDelay() => queueExpandedStateChange(true, 100);
public void CancelExpand() => scheduledExpandedChange?.Cancel();
private void updateFromHoverChange() =>
queueExpandedStateChange(content.IsHovered || dropdownContent.IsHovered, 100);
private void queueExpandedStateChange(bool newState, int delay = 0)
{
if (Expanded.Disabled)
return;
scheduledExpandedChange?.Cancel();
scheduledExpandedChange = Scheduler.AddDelayed(() => expanded.Value = newState, delay);
}
private void updateState()
{
// Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards.
// This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left.
this.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint);
background.FadeTo(Expanded.Value ? 1 : 0, NoteCard.TRANSITION_DURATION, Easing.OutQuint);
dropdownContent.FadeTo(Expanded.Value ? 1 : 0, NoteCard.TRANSITION_DURATION, Easing.OutQuint);
borderContainer.FadeTo(Expanded.Value ? 1 : 0, NoteCard.TRANSITION_DURATION, Easing.OutQuint);
content.TweenEdgeEffectTo(new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Offset = new Vector2(0, 2),
Radius = 10,
Colour = Colour4.Black.Opacity(Expanded.Value ? 0.3f : 0f),
Hollow = true,
}, NoteCard.TRANSITION_DURATION, Easing.OutQuint);
}
}
}

View File

@ -7,19 +7,20 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.MisskeyAPI;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
using osuTK;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Misskey.Components.Note.Cards.Statistics;
using osu.Game.Skinning.Components;
using osuTK;
namespace osu.Game.Screens.Misskey.Components
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
public partial class NoteCardNormal : DrawableNoteCard
public partial class NoteCardNormal : NoteCard
{
protected override Drawable IdleContent => idleBottomContent;
protected override Drawable DownloadInProgressContent => downloadProgressBar;
@ -27,23 +28,27 @@ namespace osu.Game.Screens.Misskey.Components
private const float height = 100;
[Cached]
private readonly BeatmapCardContent content;
private readonly NoteCardContent content;
private BeatmapCardThumbnail thumbnail = null!;
private CollapsibleButtonContainer buttonContainer = null!;
private NoteCardAvatar thumbnail = null!;
// private CollapsibleButtonContainer buttonContainer = null!;
private FillFlowContainer<BeatmapCardStatistic> statisticsContainer = null!;
// private FillFlowContainer<BeatmapCardStatistic> statisticsContainer = null!;
private TextFlowContainer noteText = null!;
private FillFlowContainer idleBottomContent = null!;
private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private IAPIProvider api { get; set; } = null!;
public NoteCardNormal(APIBeatmapSet beatmapSet, bool allowExpansion = true)
: base(beatmapSet, allowExpansion)
// [Resolved]
// private OverlayColourProvider colourProvider { get; set; } = null!;
public NoteCardNormal(Online.MisskeyAPI.Responses.Types.Note note, bool allowExpansion = true)
: base(note, allowExpansion)
{
content = new BeatmapCardContent(height);
content = new NoteCardContent(height);
}
[BackgroundDependencyLoader]
@ -56,6 +61,9 @@ namespace osu.Game.Screens.Misskey.Components
FillFlowContainer titleBadgeArea = null!;
GridContainer artistContainer = null!;
// LinkFlowContainer titleText = null!;
LinkFlowContainer artistText = null!;
Child = content.With(c =>
{
c.MainContent = new Container
@ -63,11 +71,11 @@ namespace osu.Game.Screens.Misskey.Components
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
thumbnail = new BeatmapCardThumbnail(BeatmapSet)
thumbnail = new NoteCardAvatar(Note)
{
Name = @"Left (icon) area",
Size = new Vector2(height),
Padding = new MarginPadding { Right = CORNER_RADIUS },
Size = new Vector2(height - 10),
// Padding = new MarginPadding { Right = CORNER_RADIUS },
Child = leftIconArea = new FillFlowContainer
{
Margin = new MarginPadding(5),
@ -76,17 +84,19 @@ namespace osu.Game.Screens.Misskey.Components
Spacing = new Vector2(1)
}
},
buttonContainer = new CollapsibleButtonContainer(BeatmapSet)
{
X = height - CORNER_RADIUS,
Width = WIDTH - height + CORNER_RADIUS,
FavouriteState = { BindTarget = FavouriteState },
ButtonsCollapsedWidth = CORNER_RADIUS,
ButtonsExpandedWidth = 30,
Children = new Drawable[]
{
// buttonContainer = new CollapsibleButtonContainer(Note)
// {
// X = height - CORNER_RADIUS,
// Width = WIDTH - height + CORNER_RADIUS,
// // FavouriteState = { BindTarget = FavouriteState },
// ButtonsCollapsedWidth = CORNER_RADIUS,
// ButtonsExpandedWidth = 30,
// Children = new Drawable[]
// {
new FillFlowContainer
{
// X = height - CORNER_RADIUS,
X = height,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
@ -108,12 +118,14 @@ namespace osu.Game.Screens.Misskey.Components
{
new Drawable[]
{
new OsuSpriteText
new OsuSpriteText()
{
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Text = new RomanisableString(Note.User.Name, Note.User.Username),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true
// Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
// AutoSizeAxes = Axes.Y,
},
titleBadgeArea = new FillFlowContainer
{
@ -142,28 +154,28 @@ namespace osu.Game.Screens.Misskey.Components
{
new[]
{
new OsuSpriteText
artistText = new LinkFlowContainer()
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
// Text = createArtistText(),
// Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true
// Truncate = true
AutoSizeAxes = Axes.Y,
},
Empty()
},
}
},
new LinkFlowContainer(s =>
noteText = new TextFlowContainer
{
s.Shadow = false;
s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold);
}).With(d =>
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 1,
AlwaysPresent = true,
}.With(flow =>
{
d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 2 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(BeatmapSet.Author);
}),
flow.AddText(Note.Text, t => t.Font = OsuFont.Default.With(size: 15));
})
}
},
new Container
@ -184,104 +196,97 @@ namespace osu.Game.Screens.Misskey.Components
AlwaysPresent = true,
Children = new Drawable[]
{
statisticsContainer = new FillFlowContainer<BeatmapCardStatistic>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Alpha = 0,
AlwaysPresent = true,
ChildrenEnumerable = createStatistics()
},
new BeatmapCardExtraInfoRow(BeatmapSet)
// statisticsContainer = new FillFlowContainer<BeatmapCardStatistic>
// {
// RelativeSizeAxes = Axes.X,
// AutoSizeAxes = Axes.Y,
// Direction = FillDirection.Horizontal,
// Spacing = new Vector2(10, 0),
// Alpha = 0,
// AlwaysPresent = true,
// ChildrenEnumerable = createStatistics()
// },
}
},
downloadProgressBar = new BeatmapCardDownloadProgressBar
{
RelativeSizeAxes = Axes.X,
Height = 6,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { BindTarget = DownloadTracker.State },
Progress = { BindTarget = DownloadTracker.Progress }
}
}
}
}
}
// }
// }
}
};
c.ExpandedContent = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10, Vertical = 13 },
Child = new BeatmapCardDifficultyList(BeatmapSet)
};
// c.ExpandedContent = new Container
// {
// RelativeSizeAxes = Axes.X,
// AutoSizeAxes = Axes.Y,
// Padding = new MarginPadding { Horizontal = 10, Vertical = 13 },
// Child = new BeatmapCardDifficultyList(BeatmapSet)
// };
c.Expanded.BindTarget = Expanded;
// titleText.AddLink(Note.User.Name, $"{api.APIEndpointUrl}@{Note.User.Username}@{Note.User.Host}");
artistText.AddLink($"@{Note.User.Username}@{Note.User.Host}", $"{api.APIEndpointUrl}/@{Note.User.Username}@{Note.User.Host}");
});
if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) });
// if (BeatmapSet.HasVideo)
// leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) });
//
// if (BeatmapSet.HasStoryboard)
// leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) });
//
// if (BeatmapSet.FeaturedInSpotlight)
// {
// titleBadgeArea.Add(new SpotlightBeatmapBadge
// {
// Anchor = Anchor.BottomRight,
// Origin = Anchor.BottomRight,
// Margin = new MarginPadding { Left = 5 }
// });
// }
if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) });
// if (BeatmapSet.HasExplicitContent)
// {
// titleBadgeArea.Add(new ExplicitContentBeatmapBadge
// {
// Anchor = Anchor.BottomRight,
// Origin = Anchor.BottomRight,
// Margin = new MarginPadding { Left = 5 }
// });
// }
if (BeatmapSet.FeaturedInSpotlight)
{
titleBadgeArea.Add(new SpotlightBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 }
});
}
if (BeatmapSet.HasExplicitContent)
{
titleBadgeArea.Add(new ExplicitContentBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 }
});
}
if (BeatmapSet.TrackId != null)
{
artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Left = 5 }
};
}
// if (BeatmapSet.TrackId != null)
// {
// artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge
// {
// Anchor = Anchor.BottomRight,
// Origin = Anchor.BottomRight,
// Margin = new MarginPadding { Left = 5 }
// };
// }
}
private LocalisableString createArtistText()
{
var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist);
return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
var romanisableArtist = new RomanisableString(Note.User.Username + "@" + Note.User.Host, Note.User.Username + "@" + Note.User.Host);
return romanisableArtist;
}
private IEnumerable<BeatmapCardStatistic> createStatistics()
{
var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet);
if (hypesStatistic != null)
yield return hypesStatistic;
var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet);
if (nominationsStatistic != null)
yield return nominationsStatistic;
yield return new FavouritesStatistic(BeatmapSet) { Current = FavouriteState };
yield return new PlayCountStatistic(BeatmapSet);
var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet);
if (dateStatistic != null)
yield return dateStatistic;
}
// private IEnumerable<BeatmapCardStatistic> createStatistics()
// {
// var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet);
// if (hypesStatistic != null)
// yield return hypesStatistic;
//
// var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet);
// if (nominationsStatistic != null)
// yield return nominationsStatistic;
//
// yield return new FavouritesStatistic(BeatmapSet) { Current = FavouriteState };
// yield return new PlayCountStatistic(BeatmapSet);
//
// var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet);
// if (dateStatistic != null)
// yield return dateStatistic;
// }
protected override void UpdateState()
{
@ -289,10 +294,10 @@ namespace osu.Game.Screens.Misskey.Components
bool showDetails = IsHovered || Expanded.Value;
buttonContainer.ShowDetails.Value = showDetails;
// buttonContainer.ShowDetails.Value = showDetails;
thumbnail.Dimmed.Value = showDetails;
statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
// statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
}
}
}

View 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 osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
namespace osu.Game.Screens.Misskey.Components.Note.Cards.Statistics
{
public partial class BeatmapCardDateStatistic : BeatmapCardStatistic
{
private readonly DateTimeOffset dateTime;
private BeatmapCardDateStatistic(DateTimeOffset dateTime)
{
this.dateTime = dateTime;
Icon = FontAwesome.Regular.CheckCircle;
Text = dateTime.ToLocalisableString(@"d MMM yyyy");
}
public override object TooltipContent => dateTime;
public override ITooltip GetCustomTooltip() => new DateTooltip();
public static BeatmapCardDateStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetInfo)
{
var displayDate = displayDateFor(beatmapSetInfo);
if (displayDate == null)
return null;
return new BeatmapCardDateStatistic(displayDate.Value);
}
private static DateTimeOffset? displayDateFor(IBeatmapSetOnlineInfo beatmapSetInfo)
{
// reference: https://github.com/ppy/osu-web/blob/ef432c11719fd1207bec5f9194b04f0033bdf02c/resources/assets/lib/beatmapset-panel.tsx#L36-L44
switch (beatmapSetInfo.Status)
{
case BeatmapOnlineStatus.Ranked:
case BeatmapOnlineStatus.Approved:
case BeatmapOnlineStatus.Loved:
case BeatmapOnlineStatus.Qualified:
return beatmapSetInfo.Ranked;
default:
return beatmapSetInfo.LastUpdated;
}
}
}
}

View File

@ -0,0 +1,82 @@
// 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.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Misskey.Components.Note.Cards.Statistics
{
/// <summary>
/// A single statistic shown on a beatmap card.
/// </summary>
public abstract partial class BeatmapCardStatistic : CompositeDrawable, IHasTooltip, IHasCustomTooltip
{
protected IconUsage Icon
{
get => spriteIcon.Icon;
set => spriteIcon.Icon = value;
}
protected LocalisableString Text
{
get => spriteText.Text;
set => spriteText.Text = value;
}
public LocalisableString TooltipText { get; protected set; }
private readonly SpriteIcon spriteIcon;
private readonly OsuSpriteText spriteText;
protected BeatmapCardStatistic()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
spriteIcon = new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(10),
Margin = new MarginPadding { Top = 1 }
},
spriteText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.Default.With(size: 14)
}
}
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
spriteIcon.Colour = colourProvider.Content2;
}
#region Tooltip implementation
public virtual ITooltip GetCustomTooltip() => null;
public virtual object TooltipContent => null;
#endregion
}
}

View File

@ -0,0 +1,47 @@
// 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.
#nullable disable
using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Misskey.Components.Note.Cards.Statistics
{
/// <summary>
/// Shows the number of favourites that a beatmap set has received.
/// </summary>
public partial class FavouritesStatistic : BeatmapCardStatistic, IHasCurrentValue<BeatmapSetFavouriteState>
{
private readonly BindableWithCurrent<BeatmapSetFavouriteState> current;
public Bindable<BeatmapSetFavouriteState> Current
{
get => current.Current;
set => current.Current = value;
}
public FavouritesStatistic(IBeatmapSetOnlineInfo onlineInfo)
{
current = new BindableWithCurrent<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(onlineInfo.HasFavourited, onlineInfo.FavouriteCount));
}
protected override void LoadComplete()
{
base.LoadComplete();
current.BindValueChanged(_ => updateState(), true);
}
private void updateState()
{
Icon = current.Value.Favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart;
Text = current.Value.FavouriteCount.ToMetric(decimals: 1);
TooltipText = BeatmapsStrings.PanelFavourites(current.Value.FavouriteCount.ToLocalisableString(@"N0"));
}
}
}

View File

@ -0,0 +1,26 @@
// 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 osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Misskey.Components.Note.Cards.Statistics
{
/// <summary>
/// Shows the number of current hypes that a map has received, as well as the number of hypes required for nomination.
/// </summary>
public partial class HypesStatistic : BeatmapCardStatistic
{
private HypesStatistic(BeatmapSetHypeStatus hypeStatus)
{
Icon = FontAwesome.Solid.Bullhorn;
Text = hypeStatus.Current.ToLocalisableString();
TooltipText = BeatmapsStrings.HypeRequiredText(hypeStatus.Current.ToLocalisableString(), hypeStatus.Required.ToLocalisableString());
}
public static HypesStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetOnlineInfo)
=> beatmapSetOnlineInfo.HypeStatus == null ? null : new HypesStatistic(beatmapSetOnlineInfo.HypeStatus);
}
}

View File

@ -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 osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Misskey.Components.Note.Cards.Statistics
{
/// <summary>
/// Shows the number of current nominations that a map has received, as well as the number of nominations required for qualification.
/// </summary>
public partial class NominationsStatistic : BeatmapCardStatistic
{
private NominationsStatistic(BeatmapSetNominationStatus nominationStatus)
{
Icon = FontAwesome.Solid.ThumbsUp;
Text = nominationStatus.Current.ToLocalisableString();
TooltipText = BeatmapsStrings.NominationsRequiredText(nominationStatus.Current.ToLocalisableString(), nominationStatus.Required.ToLocalisableString());
}
public static NominationsStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetOnlineInfo)
// web does not show nominations unless hypes are also present.
// see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443
=> beatmapSetOnlineInfo.HypeStatus == null || beatmapSetOnlineInfo.NominationStatus == null ? null : new NominationsStatistic(beatmapSetOnlineInfo.NominationStatus);
}
}

View File

@ -0,0 +1,26 @@
// 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.
#nullable disable
using Humanizer;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Misskey.Components.Note.Cards.Statistics
{
/// <summary>
/// Shows the number of times the given beatmap set has been played.
/// </summary>
public partial class PlayCountStatistic : BeatmapCardStatistic
{
public PlayCountStatistic(IBeatmapSetOnlineInfo onlineInfo)
{
Icon = FontAwesome.Regular.PlayCircle;
Text = onlineInfo.PlayCount.ToMetric(decimals: 1);
TooltipText = BeatmapsStrings.PanelPlaycount(onlineInfo.PlayCount.ToLocalisableString(@"N0"));
}
}
}

View File

@ -0,0 +1,21 @@
// 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.
#nullable disable
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
public partial class StoryboardIconPill : IconPill
{
public StoryboardIconPill()
: base(FontAwesome.Solid.Image)
{
}
public override LocalisableString TooltipText => BeatmapsetsStrings.ShowInfoStoryboard;
}
}

View File

@ -0,0 +1,21 @@
// 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.
#nullable disable
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Misskey.Components.Note.Cards
{
public partial class VideoIconPill : IconPill
{
public VideoIconPill()
: base(FontAwesome.Solid.Film)
{
}
public override LocalisableString TooltipText => BeatmapsetsStrings.ShowInfoVideo;
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
@ -16,6 +17,7 @@ using osu.Game.Online.MisskeyAPI;
using osu.Game.Misskey.Overlays.Settings;
using osu.Game.Online.MisskeyAPI.Requests.Notes;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.OSD;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Misskey;
@ -26,6 +28,8 @@ namespace osu.Game.Screens.Misskey.Components
public partial class PostForm : FillFlowContainer
{
private OnScreenDisplay? onScreenDisplay { get; set; }
[Resolved(CanBeNull = true)]
private INotificationOverlay? notifications { get; set; }
private partial class ResToast : Toast
{
public ResToast(string value, string desc)
@ -57,11 +61,20 @@ namespace osu.Game.Screens.Misskey.Components
{
textBox.Text = string.Empty;
cwBox.Text = string.Empty;
onScreenDisplay?.Display(new ResToast("投稿しました", createReq.CompletionState.ToString()));
notifications?.Post(new SimpleNotification
{
Text = "投稿しました",
Icon = FontAwesome.Solid.Check,
});
};
api.Queue(createReq);
onScreenDisplay?.Display(new ResToast("送信しています", ""));
notifications?.Post(new SimpleNotification
{
Text = "送信しています",
Icon = FontAwesome.Solid.PaperPlane,
});
}
[BackgroundDependencyLoader(permitNulls: true)]
@ -128,15 +141,14 @@ namespace osu.Game.Screens.Misskey.Components
}
}
},
// new SettingsButton
// {
// Text = "Register",
// Action = () =>
// {
// RequestHide?.Invoke();
// accountCreation.Show();
// }
// }
new SettingsButton
{
Text = "test",
Action = () =>
{
onScreenDisplay?.Display(new ResToast("送信しています", ""));
}
}
};
// forgottenPaswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"https://simkey.net/about");

View File

@ -20,7 +20,7 @@ namespace osu.Game.Screens.Misskey
public partial class MisskeyComponents : OsuScreen
{
private Container contentContainer;
private Drawable avatar;
// private Drawable avatar;
[Resolved]
private OsuColour colours { get; set; }
@ -44,23 +44,23 @@ namespace osu.Game.Screens.Misskey
Colour = Color4.Black,
Alpha = 0.6f,
},
new Container
{
// RelativeSizeAxes = Axes.Both,
// AutoSizeAxes = Axes.Y,
Size = new Vector2(200),
Masking = true,
CornerRadius = 100,
AutoSizeEasing = Easing.OutQuint,
Children = new Drawable[]
{
avatar = new DelayedLoadWrapper(
new Avatar()
{
RelativeSizeAxes = Axes.Both,
})
},
}
// new Container
// {
// // RelativeSizeAxes = Axes.Both,
// // AutoSizeAxes = Axes.Y,
// Size = new Vector2(200),
// Masking = true,
// CornerRadius = 100,
// AutoSizeEasing = Easing.OutQuint,
// Children = new Drawable[]
// {
// avatar = new DelayedLoadWrapper(
// new Avatar()
// {
// RelativeSizeAxes = Axes.Both,
// })
// },
// }
}
};
}

View File

@ -98,14 +98,14 @@ namespace osu.Game.Screens.Misskey
Text = "MisskeyLogin",
Action = () => this.Push(new MisskeyLogin())
},
// new OsuButton()
// {
// Anchor = Anchor.Centre,
// Origin = Anchor.Centre,
// Size = new Vector2(500f, 50f),
// Text = "Timeline",
// Action = () => this.Push(new Timeline())
// },
new OsuButton()
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500f, 50f),
Text = "Timeline",
Action = () => this.Push(new Timeline())
},
new OsuButton()
{
Anchor = Anchor.Centre,

View File

@ -0,0 +1,303 @@
// 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.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Audio;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.MisskeyAPI.Requests.Notes;
using osu.Game.Online.MisskeyAPI.Responses.Types;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Misskey.Components;
using osuTK;
using osuTK.Graphics;
using ExpandedContentScrollContainer = osu.Game.Screens.Misskey.Components.Note.Cards.ExpandedContentScrollContainer;
using IAPIProvider = osu.Game.Online.MisskeyAPI.IAPIProvider;
using NoteCard = osu.Game.Screens.Misskey.Components.Note.Cards.NoteCard;
namespace osu.Game.Screens.Misskey
{
public partial class Timeline : OsuScreen
{
// [Cached]
// protected readonly OsuScrollContainer ScrollFlow;
protected readonly LoadingLayer Loading;
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
private OsuScrollContainer panelTarget;
private FillFlowContainer<NoteCard> foundContent;
private int page = 0;
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new FillFlowContainer
{
Margin = new MarginPadding(10f),
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.1f)
},
panelTarget = new OsuScrollContainer()
{
Height = 750f,
Width = 600f,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
// RelativeSizeAxes = Axes.X,
Masking = true,
Padding = new MarginPadding { Horizontal = 20 },
}
},
},
}
};
}
private string lastNoteID;
protected override void LoadComplete()
{
base.LoadComplete();
var req = new HybridTimeline(api.AccessToken);
req.Success += res =>
{
onSearchFinished(res);
lastNoteID = res.Last().Id;
};
onSearchStarted();
api.Queue(req);
}
private CancellationTokenSource cancellationToken;
private Task panelLoadTask;
private void onSearchStarted()
{
cancellationToken?.Cancel();
// if (panelTarget.Any())
// Loading.Show();
}
private void onSearchFinished(Note[] searchResult)
{
cancellationToken?.Cancel();
var newCards = createCardsFor(searchResult);
if (page == 0)
{
//No matches case
if (!newCards.Any())
{
replaceResultsAreaContent(new NotFoundDrawable());
return;
}
var content = createCardContainerFor(newCards);
panelLoadTask = LoadComponentAsync(foundContent = content, replaceResultsAreaContent, (cancellationToken = new CancellationTokenSource()).Token);
}
else
{
// new results may contain beatmaps from a previous page,
// this is dodgy but matches web behaviour for now.
// see: https://github.com/ppy/osu-web/issues/9270
// todo: replace custom equality compraer with ExceptBy in net6.0
// newCards = newCards.ExceptBy(foundContent.Select(c => c.BeatmapSet.OnlineID), c => c.BeatmapSet.OnlineID);
// newCards = newCards.Except(foundContent, BeatmapCardEqualityComparer.Default);
panelLoadTask = LoadComponentsAsync(newCards, loaded =>
{
lastFetchDisplayedTime = Time.Current;
foundContent.AddRange(loaded);
loaded.ForEach(p => p.FadeIn(200, Easing.OutQuint));
}, (cancellationToken = new CancellationTokenSource()).Token);
}
}
private IEnumerable<NoteCard> createCardsFor(Note[] notes) =>
notes.Select(set =>
NoteCard.Create(set, BeatmapCardSize.Normal).With(c =>
{
c.Anchor = Anchor.TopCentre;
c.Origin = Anchor.TopCentre;
})).ToArray();
private static ReverseChildIDFillFlowContainer<NoteCard> createCardContainerFor(IEnumerable<NoteCard> newCards)
{
// spawn new children with the contained so we only clear old content at the last moment.
// reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most).
var content = new ReverseChildIDFillFlowContainer<NoteCard>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
Alpha = 0,
Margin = new MarginPadding
{
Top = 15,
// the + 20 adjustment is roughly eyeballed in order to fit all of the expanded content height after it's scaled
// as well as provide visual balance to the top margin.
Bottom = ExpandedContentScrollContainer.HEIGHT + 20
},
ChildrenEnumerable = newCards
};
return content;
}
private void replaceResultsAreaContent(Drawable content)
{
// Loading.Hide();
lastFetchDisplayedTime = Time.Current;
panelTarget.Child = content;
content.FadeInFromZero();
}
protected override void Dispose(bool isDisposing)
{
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
public partial class NotFoundDrawable : CompositeDrawable
{
public NotFoundDrawable()
{
RelativeSizeAxes = Axes.X;
Height = 250;
Alpha = 0;
Margin = new MarginPadding { Top = 15 };
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
AddInternal(new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Texture = textures.Get(@"Online/not-found")
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = BeatmapsStrings.ListingSearchNotFoundQuote,
}
}
});
}
}
// TODO: localisation requires Text/LinkFlowContainer support for localising strings with links inside
// (https://github.com/ppy/osu-framework/issues/4530)
private const double time_between_fetches = 500;
private double lastFetchDisplayedTime;
protected override void Update()
{
base.Update();
const int pagination_scroll_distance = 200;
bool shouldShowMore = panelLoadTask?.IsCompleted != false
&& Time.Current - lastFetchDisplayedTime > time_between_fetches
&& (panelTarget.ScrollableExtent > 0 && panelTarget.IsScrolledToEnd(pagination_scroll_distance));
if (shouldShowMore)
FetchNextPage();
}
private bool locked;
private void FetchNextPage()
{
if (locked)
return;
var req = new HybridTimeline(api.AccessToken, lastNoteID);
req.Success += res =>
{
lastNoteID = res.Last().Id;
page++;
onSearchFinished(res);
locked = false;
};
api.Queue(req);
locked = true;
}
private class BeatmapCardEqualityComparer : IEqualityComparer<BeatmapCard>
{
public static BeatmapCardEqualityComparer Default { get; } = new BeatmapCardEqualityComparer();
public bool Equals(BeatmapCard x, BeatmapCard y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
return x.BeatmapSet.Equals(y.BeatmapSet);
}
public int GetHashCode(BeatmapCard obj) => obj.BeatmapSet.GetHashCode();
}
}
}

View File

@ -147,7 +147,7 @@ namespace osu.Game.Screens.Misskey
Origin = Anchor.TopCentre,
Margin = new MarginPadding(100),
Width = 400f,
Text = "Misskey.io は、地球で生まれた分散マイクロブログSNSです。Fediverse様々なSNSで構成される宇宙の中に存在するため、他のSNSと相互に繋がっています。\n暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。",
Text = "Misskey.。Fediverse様々なSNSで構成される宇宙の中に存在するため、他のSNSと相互に繋がっています。\n暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。",
Font = new FontUsage(size: 20),
Colour = Colour4.White,
},

View File

@ -45,4 +45,7 @@
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Online\MisskeyAPI\Responses\Notes" />
</ItemGroup>
</Project>