1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-19 10:12:53 +08:00

Merge branch 'url-parsing-support' of https://github.com/freezylemon/osu into url-parsing-support

This commit is contained in:
FreezyLemon 2017-12-02 17:15:14 +01:00
commit 37490c65cc
12 changed files with 484 additions and 61 deletions

View File

@ -2,7 +2,9 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual
@ -10,12 +12,26 @@ namespace osu.Game.Tests.Visual
[Description("Testing chat api and overlay")]
internal class TestCaseChatDisplay : OsuTestCase
{
private BeatmapSetOverlay beatmapSetOverlay;
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent);
public TestCaseChatDisplay()
{
Add(new ChatOverlay
{
State = Visibility.Visible
});
Add(beatmapSetOverlay = new BeatmapSetOverlay());
}
[BackgroundDependencyLoader]
private void load()
{
dependencies.Cache(beatmapSetOverlay);
}
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Game.Graphics.Sprites;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace osu.Game.Graphics.Containers
{
public class OsuLinkTextFlowContainer : OsuLinkTextFlowContainer<OsuLinkSpriteText>
{
public OsuLinkTextFlowContainer(Action<SpriteText> defaultCreationParameters = null)
: base(defaultCreationParameters)
{
}
}
public class OsuLinkTextFlowContainer<T> : OsuTextFlowContainer
where T : OsuLinkSpriteText, new()
{
public override bool HandleInput => true;
public OsuLinkTextFlowContainer(Action<SpriteText> defaultCreationParameters = null) : base(defaultCreationParameters)
{
}
protected override SpriteText CreateSpriteText() => new T();
public void AddLink(string text, string url, Action<SpriteText> creationParameters = null)
{
AddText(text, link =>
{
((T)link).Url = url;
creationParameters?.Invoke(link);
});
}
}
}

View File

@ -0,0 +1,98 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace osu.Game.Graphics.Sprites
{
public class OsuLinkSpriteText : OsuSpriteText
{
private readonly OsuHoverContainer content;
private BeatmapSetOverlay beatmapSetOverlay;
public override bool HandleInput => content.Action != null;
protected override Container<Drawable> Content => content ?? (Container<Drawable>)this;
protected override IEnumerable<Drawable> FlowingChildren => Children;
private string url;
public string Url
{
get
{
return url;
}
set
{
if (value != null)
{
url = value;
loadAction();
}
}
}
public OsuLinkSpriteText()
{
AddInternal(content = new OsuHoverContainer
{
AutoSizeAxes = Axes.Both,
});
}
[BackgroundDependencyLoader]
private void load(BeatmapSetOverlay beatmapSetOverlay)
{
this.beatmapSetOverlay = beatmapSetOverlay;
}
private void loadAction()
{
if (Url == null || String.IsNullOrEmpty(Url))
return;
var url = Url;
if (url.StartsWith("https://")) url = url.Substring(8);
else if (url.StartsWith("http://")) url = url.Substring(7);
else content.Action = () => Process.Start(Url);
if (url.StartsWith("osu.ppy.sh/"))
{
url = url.Substring(11);
if (url.StartsWith("s") || url.StartsWith("beatmapsets"))
content.Action = () => beatmapSetOverlay.ShowBeatmapSet(getIdFromUrl(url));
else if (url.StartsWith("b") || url.StartsWith("beatmaps"))
content.Action = () => beatmapSetOverlay.ShowBeatmap(getIdFromUrl(url));
// else if (url.StartsWith("d")) Maybe later
}
}
private int getIdFromUrl(string url)
{
var lastSlashIndex = url.LastIndexOf('/');
if (lastSlashIndex == url.Length)
{
url = url.Substring(url.Length - 1);
lastSlashIndex = url.LastIndexOf('/');
}
return int.Parse(url.Substring(lastSlashIndex + 1));
}
}
}

View File

@ -0,0 +1,21 @@
using osu.Game.Beatmaps;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace osu.Game.Online.API.Requests
{
public class GetBeatmapRequest : APIRequest<BeatmapInfo>
{
private readonly int beatmapId;
public GetBeatmapRequest(int beatmapId)
{
this.beatmapId = beatmapId;
}
protected override string Target => $@"beatmaps/{beatmapId}";
}
}

View File

@ -0,0 +1,66 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace osu.Game.Online.Chat
{
public class ChatLinkSpriteText : OsuLinkSpriteText
{
public int LinkId;
private Color4 hoverColour;
private Color4 urlColour;
protected override bool OnHover(InputState state)
{
// Every word is one sprite in chat (for word wrap) so we need to find all other sprites that display the same link
var otherSpritesWithSameLink = ((Container<Drawable>)Parent).Children.Where(child => (child as ChatLinkSpriteText)?.LinkId == LinkId && !Equals(child));
var hoverResult = base.OnHover(state);
if (!otherSpritesWithSameLink.Any(sprite => sprite.IsHovered))
foreach (ChatLinkSpriteText sprite in otherSpritesWithSameLink)
sprite.TriggerOnHover(state);
Content.FadeColour(hoverColour, 500, Easing.OutQuint);
return hoverResult;
}
protected override void OnHoverLost(InputState state)
{
var spritesWithSameLink = ((Container<Drawable>)Parent).Children.Where(child => (child as ChatLinkSpriteText)?.LinkId == LinkId);
if (spritesWithSameLink.Any(sprite => sprite.IsHovered))
{
// We have to do this so this sprite does not fade its colour back
Content.FadeColour(hoverColour, 500, Easing.OutQuint);
return;
}
foreach (ChatLinkSpriteText sprite in spritesWithSameLink)
sprite.Content.FadeColour(urlColour, 500, Easing.OutQuint);
base.OnHoverLost(state);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
hoverColour = colours.Yellow;
urlColour = colours.Blue;
}
}
}

View File

@ -2,6 +2,8 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using Newtonsoft.Json;
using osu.Game.Users;
@ -40,6 +42,8 @@ namespace osu.Game.Online.Chat
{
}
public List<MessageFormatter.Link> Links;
public Message(long? id)
{
Id = id;

View File

@ -0,0 +1,170 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace osu.Game.Online.Chat
{
public static class MessageFormatter
{
// [[Performance Points]] -> wiki:Performance Points (https://osu.ppy.sh/wiki/Performance_Points)
private static Regex wikiRegex = new Regex(@"\[\[([^\]]+)\]\]");
// (test)[https://osu.ppy.sh/b/1234] -> test (https://osu.ppy.sh/b/1234)
private static Regex oldLinkRegex = new Regex(@"\(([^\)]*)\)\[([a-z]+://[^ ]+)\]");
// [https://osu.ppy.sh/b/1234 Beatmap [Hard] (poop)] -> Beatmap [hard] (poop) (https://osu.ppy.sh/b/1234)
private static Regex newLinkRegex = new Regex(@"\[([a-z]+://[^ ]+) ([^\[\]]*(((?<open>\[)[^\[\]]*)+((?<close-open>\])[^\[\]]*)+)*(?(open)(?!)))\]");
// advanced, RFC-compatible regular expression that matches any possible URL, *but* allows certain invalid characters that are widely used
// This is in the format (<required>, [optional]):
// http[s]://<domain>.<tld>[:port][/path][?query][#fragment]
private static Regex advancedLinkRegex = new Regex(@"(?<paren>\([^)]*)?" +
@"(?<link>https?:\/\/" +
@"(?<domain>(?:[a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*[a-z][a-z0-9-]*[a-z0-9]" + // domain, TLD
@"(?::\d+)?)" + // port
@"(?<path>(?:(?:\/+(?:[a-z0-9$_\.\+!\*\',;:\(\)@&~=-]|%[0-9a-f]{2})*)*" + // path
@"(?:\?(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?" + // query
@"(?:#(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?)", // fragment
RegexOptions.IgnoreCase);
// 00:00:000 (1,2,3) - test
private static Regex timeRegex = new Regex(@"\d\d:\d\d:\d\d\d? [^-]*");
// #osu
private static Regex channelRegex = new Regex(@"#[a-zA-Z]+[a-zA-Z0-9]+");
// Unicode emojis
private static Regex emojiRegex = new Regex(@"(\uD83D[\uDC00-\uDE4F])");
private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0)
{
int captureOffset = 0;
foreach (Match m in regex.Matches(result.Text, startIndex))
{
var index = m.Index - captureOffset;
var displayText = string.Format(display,
m.Groups[0],
m.Groups.Count > 1 ? m.Groups[1].Value : "",
m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim();
var linkText = string.Format(link,
m.Groups[0],
m.Groups.Count > 1 ? m.Groups[1].Value : "",
m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim();
if (displayText.Length == 0 || linkText.Length == 0) continue;
// Check for encapsulated links
if (result.Links.Find(l => (l.Index <= index && l.Index + l.Length >= index + m.Length) || index <= l.Index && index + m.Length >= l.Index + l.Length) == null)
{
result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText);
//since we just changed the line display text, offset any already processed links.
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
result.Links.Add(new Link(linkText, index, displayText.Length));
//adjust the offset for processing the current matches group.
captureOffset += (m.Length - displayText.Length);
}
}
}
private static void handleAdvanced(Regex regex, MessageFormatterResult result, int startIndex = 0)
{
foreach (Match m in regex.Matches(result.Text, startIndex))
{
var index = m.Index;
var prefix = m.Groups["paren"].Value;
var link = m.Groups["link"].Value;
var indexLength = link.Length;
if (!String.IsNullOrEmpty(prefix))
{
index += prefix.Length;
if (link.EndsWith(")"))
{
indexLength = indexLength - 1;
link = link.Remove(link.Length - 1);
}
}
result.Links.Add(new Link(link, index, indexLength));
}
}
private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3)
{
var result = new MessageFormatterResult(toFormat);
// handle the [link display] format
handleMatches(newLinkRegex, "{2}", "{1}", result, startIndex);
// handle the ()[] link format
handleMatches(oldLinkRegex, "{1}", "{2}", result, startIndex);
// handle wiki links
handleMatches(wikiRegex, "wiki:{1}", "https://osu.ppy.sh/wiki/{1}", result, startIndex);
// handle bare links
handleAdvanced(advancedLinkRegex, result, startIndex);
// handle editor times
handleMatches(timeRegex, "{0}", "osu://edit/{0}", result, startIndex);
// handle channels
handleMatches(channelRegex, "{0}", "osu://chan/{0}", result, startIndex);
var empty = "";
while (space-- > 0)
empty += "\0";
handleMatches(emojiRegex, empty, "{0}", result, startIndex);
return result;
}
public static Message FormatMessage(Message inputMessage)
{
var result = format(inputMessage.Content);
var formatted = inputMessage;
formatted.Content = result.Text;
formatted.Links = result.Links;
return formatted;
}
public class MessageFormatterResult
{
public List<Link> Links = new List<Link>();
public string Text;
public string OriginalText;
public MessageFormatterResult(string text)
{
OriginalText = Text = text;
}
}
public class Link
{
public string Url;
public int Index;
public int Length;
public Link(string url, int startIndex, int length)
{
Url = url;
Index = startIndex;
Length = length;
}
}
}
}

View File

@ -80,7 +80,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
base.Update();
if (Playing.Value && preview != null)
if (Playing.Value && preview != null && preview.Length > 0)
{
progress.Width = (float)(preview.CurrentTime / preview.Length);
}

View File

@ -139,6 +139,20 @@ namespace osu.Game.Overlays
return true;
}
public void ShowBeatmap(int beatmapId)
{
var req = new GetBeatmapRequest(beatmapId);
req.Success += res =>
{
if (!res.OnlineBeatmapSetID.HasValue)
return;
ShowBeatmapSet(res.OnlineBeatmapSetID.Value);
};
api.Queue(req);
}
public void ShowBeatmapSet(int beatmapSetId)
{
// todo: display the overlay while we are loading here. we need to support setting BeatmapSet to null for this to work.

View File

@ -67,12 +67,13 @@ namespace osu.Game.Overlays.Chat
private const float text_size = 20;
private Color4 customUsernameColour;
private Color4 urlColour;
private OsuSpriteText timestamp;
public ChatLine(Message message)
{
Message = message;
Message = MessageFormatter.FormatMessage(message);
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@ -82,7 +83,7 @@ namespace osu.Game.Overlays.Chat
private Message message;
private OsuSpriteText username;
private OsuTextFlowContainer contentFlow;
private OsuLinkTextFlowContainer<ChatLinkSpriteText> contentFlow;
public Message Message
{
@ -104,6 +105,7 @@ namespace osu.Game.Overlays.Chat
private void load(OsuColour colours)
{
customUsernameColour = colours.ChatBlue;
urlColour = colours.Blue;
}
private bool senderHasBackground => !string.IsNullOrEmpty(message.Sender.Colour);
@ -187,7 +189,7 @@ namespace osu.Game.Overlays.Chat
Padding = new MarginPadding { Left = message_padding + padding },
Children = new Drawable[]
{
contentFlow = new OsuTextFlowContainer(t => { t.TextSize = text_size; })
contentFlow = new OsuLinkTextFlowContainer<ChatLinkSpriteText>(t => { t.TextSize = text_size; })
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
@ -210,16 +212,40 @@ namespace osu.Game.Overlays.Chat
timestamp.Text = $@"{message.Timestamp.LocalDateTime:HH:mm:ss}";
username.Text = $@"{message.Sender.Username}" + (senderHasBackground ? "" : ":");
if (message.IsAction)
contentFlow.Clear();
if (message.Links == null || message.Links.Count == 0)
{
contentFlow.Clear();
contentFlow.AddText("[", sprite => sprite.Padding = new MarginPadding { Right = action_padding });
contentFlow.AddText(message.Content, sprite => sprite.Font = @"Exo2.0-MediumItalic");
contentFlow.AddText("]", sprite => sprite.Padding = new MarginPadding { Left = action_padding });
contentFlow.AddText(message.Content, sprite =>
{
if (message.IsAction)
sprite.Font = @"Exo2.0-MediumItalic";
});
return;
}
else
contentFlow.Text = message.Content;
{
int prevIndex = 0;
foreach (var link in message.Links)
{
contentFlow.AddText(message.Content.Substring(prevIndex, link.Index - prevIndex));
prevIndex = link.Index + link.Length;
contentFlow.AddLink(message.Content.Substring(link.Index, link.Length), link.Url, sprite =>
{
if (message.IsAction)
sprite.Font = @"Exo2.0-MediumItalic";
sprite.Colour = urlColour;
// We want to use something that is unique to every formatted link, so I just use the position of the link
((ChatLinkSpriteText)sprite).LinkId = link.Index;
});
}
// Add text after the last link
var lastLink = message.Links[message.Links.Count - 1];
contentFlow.AddText(message.Content.Substring(lastLink.Index + lastLink.Length));
}
}
private class MessageSender : OsuClickableContainer, IHasContextMenu

View File

@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Profile
public class ProfileHeader : Container
{
private readonly OsuTextFlowContainer infoTextLeft;
private readonly LinkFlowContainer infoTextRight;
private readonly OsuLinkTextFlowContainer infoTextRight;
private readonly FillFlowContainer<SpriteText> scoreText, scoreNumberText;
private readonly Container coverContainer, chartContainer, supporterTag;
@ -119,7 +119,7 @@ namespace osu.Game.Overlays.Profile
}
}
},
new LinkFlowContainer.ProfileLink(user)
new ProfileLink(user)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
@ -160,7 +160,7 @@ namespace osu.Game.Overlays.Profile
ParagraphSpacing = 0.8f,
LineSpacing = 0.2f
},
infoTextRight = new LinkFlowContainer(t =>
infoTextRight = new OsuLinkTextFlowContainer(t =>
{
t.TextSize = 14;
t.Font = @"Exo2.0-RegularItalic";
@ -488,57 +488,16 @@ namespace osu.Game.Overlays.Profile
}
}
private class LinkFlowContainer : OsuTextFlowContainer
private class ProfileLink : OsuLinkSpriteText, IHasTooltip
{
public override bool HandleInput => true;
public string TooltipText => "View Profile in Browser";
public LinkFlowContainer(Action<SpriteText> defaultCreationParameters = null) : base(defaultCreationParameters)
public ProfileLink(User user)
{
}
protected override SpriteText CreateSpriteText() => new LinkText();
public void AddLink(string text, string url) => AddText(text, link => ((LinkText)link).Url = url);
public class LinkText : OsuSpriteText
{
private readonly OsuHoverContainer content;
public override bool HandleInput => content.Action != null;
protected override Container<Drawable> Content => content ?? (Container<Drawable>)this;
protected override IEnumerable<Drawable> FlowingChildren => Children;
public string Url
{
set
{
if(value != null)
content.Action = () => Process.Start(value);
}
}
public LinkText()
{
AddInternal(content = new OsuHoverContainer
{
AutoSizeAxes = Axes.Both,
});
}
}
public class ProfileLink : LinkText, IHasTooltip
{
public string TooltipText => "View Profile in Browser";
public ProfileLink(User user)
{
Text = user.Username;
Url = $@"https://osu.ppy.sh/users/{user.Id}";
Font = @"Exo2.0-RegularItalic";
TextSize = 30;
}
Text = user.Username;
Url = $@"https://osu.ppy.sh/users/{user.Id}";
Font = @"Exo2.0-RegularItalic";
TextSize = 30;
}
}
}

View File

@ -268,6 +268,8 @@
<Compile Include="Beatmaps\Drawables\BeatmapSetHeader.cs" />
<Compile Include="Database\DatabaseContextFactory.cs" />
<Compile Include="Database\IHasPrimaryKey.cs" />
<Compile Include="Graphics\Containers\OsuLinkTextFlowContainer.cs" />
<Compile Include="Graphics\Sprites\OsuLinkSpriteText.cs" />
<Compile Include="Graphics\UserInterface\HoverClickSounds.cs" />
<Compile Include="Graphics\UserInterface\HoverSounds.cs" />
<Compile Include="Graphics\UserInterface\OsuButton.cs" />
@ -284,8 +286,11 @@
<DependentUpon>20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs</DependentUpon>
</Compile>
<Compile Include="Migrations\OsuDbContextModelSnapshot.cs" />
<Compile Include="Online\API\Requests\GetBeatmapRequest.cs" />
<Compile Include="Online\API\Requests\GetBeatmapSetRequest.cs" />
<Compile Include="Online\API\Requests\GetBeatmapSetsResponse.cs" />
<Compile Include="Online\Chat\ChatLinkSpriteText.cs" />
<Compile Include="Online\Chat\MessageFormatter.cs" />
<Compile Include="Overlays\BeatmapSet\Scores\ClickableUsername.cs" />
<Compile Include="Overlays\BeatmapSet\Scores\DrawableScore.cs" />
<Compile Include="Overlays\BeatmapSet\Scores\DrawableTopScore.cs" />