diff --git a/fluXis/Graphics/Containers/Markdown/FluXisMarkdownCodeBlock.cs b/fluXis/Graphics/Containers/Markdown/FluXisMarkdownCodeBlock.cs index 19fb19893..baccb6469 100644 --- a/fluXis/Graphics/Containers/Markdown/FluXisMarkdownCodeBlock.cs +++ b/fluXis/Graphics/Containers/Markdown/FluXisMarkdownCodeBlock.cs @@ -12,14 +12,39 @@ namespace fluXis.Graphics.Containers.Markdown; public partial class FluXisMarkdownCodeBlock : MarkdownCodeBlock { private FluXisMarkdown markdown { get; } + private CodeBlock codeBlock; + private FluXisMarkdownCopyButton copyButton; public FluXisMarkdownCodeBlock(FluXisMarkdown markdown, [NotNull] CodeBlock codeBlock) : base(codeBlock) { this.markdown = markdown; + this.codeBlock = codeBlock; Margin = new MarginPadding { Bottom = 16 }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + AddInternal + ( + copyButton = new FluXisMarkdownCopyButton(codeBlock) + { + Alpha = 0, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + } + ); + + Scheduler.AddDelayed(() => + { + copyButton.Anchor = Anchor.TopRight; + copyButton.Origin = Anchor.TopRight; + copyButton.FadeTo(1f, 300, Easing.In); + }, 100); + } + protected override Drawable CreateBackground() { var container = new Container diff --git a/fluXis/Graphics/Containers/Markdown/FluXisMarkdownCopyButton.cs b/fluXis/Graphics/Containers/Markdown/FluXisMarkdownCopyButton.cs new file mode 100644 index 000000000..68efc86b6 --- /dev/null +++ b/fluXis/Graphics/Containers/Markdown/FluXisMarkdownCopyButton.cs @@ -0,0 +1,98 @@ +using osu.Framework.Platform; +using Markdig.Syntax; +using fluXis.Localization; +using fluXis.Graphics.Sprites.Icons; +using osu.Framework.Input.Events; +using osu.Framework.Allocation; +using fluXis.Overlay.Notifications; +using fluXis.Graphics.UserInterface.Buttons; +using osu.Framework.Graphics.Cursor; +using fluXis.Graphics.Sprites.Text; +using fluXis.Graphics.UserInterface.Text; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osuTK; +using fluXis.Overlay.Mouse; + +namespace fluXis.Graphics.Containers.Markdown; + +public partial class FluXisMarkdownCopyButton : IconButton, IHasCustomTooltip +{ + [Resolved] + private Clipboard clipboard { get; set; } + + [Resolved] + private NotificationManager notifications { get; set; } + + private readonly CodeBlock codeBlock; + private readonly CopyButtonTooltip tooltip; + + public FluXisMarkdownCopyButton TooltipContent => this; + + public FluXisMarkdownCopyButton(CodeBlock codeBlock) + { + this.codeBlock = codeBlock; + tooltip = new CopyButtonTooltip(); + + Icon = FontAwesome6.Solid.Clipboard; + IconSize = 24; + ButtonSize = 46; + } + + protected override bool OnClick(ClickEvent e) + { + clipboard.SetText(string.Join("\n", codeBlock.Lines)); + notifications.SendSmallText("Copied text to Clipboard", FontAwesome6.Solid.Clipboard); + return base.OnClick(e); + } + + public ITooltip GetCustomTooltip() => tooltip; + + private partial class CopyButtonTooltip : CustomTooltipContainer + { + public CopyButtonTooltip() + { + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Margin = new MarginPadding { Horizontal = 10, Vertical = 6 }, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new FluXisSpriteIcon + { + Size = new Vector2(16), + Margin = new MarginPadding(4), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome6.Solid.Clipboard + }, + new FluXisSpriteText + { + FontSize = 24, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = LocalizationStrings.General.CopyToClipboard + } + } + }, + new FluXisTextFlow + { + AutoSizeAxes = Axes.Both, + FontSize = 18, + Text = LocalizationStrings.General.CopyToClipboardDescription + } + } + }; + } + + public override void SetContent(FluXisMarkdownCopyButton content) {} // Already set in constuctor so, this is pretty much implemented for the sake of it. + } +} \ No newline at end of file diff --git a/fluXis/Graphics/Sprites/Icons/FontAwesome6.cs b/fluXis/Graphics/Sprites/Icons/FontAwesome6.cs index fa3dddf65..77c3da56d 100644 --- a/fluXis/Graphics/Sprites/Icons/FontAwesome6.cs +++ b/fluXis/Graphics/Sprites/Icons/FontAwesome6.cs @@ -78,6 +78,7 @@ public static class Solid public static IconUsage Images => GetSolid(0xf302); public static IconUsage Info => GetSolid(0xf129); public static IconUsage Keyboard => GetSolid(0xf11c); + public static IconUsage Clipboard => GetSolid(0xf328); public static IconUsage LayerGroup => GetSolid(0xf5fd); public static IconUsage LeftRight => GetSolid(0xf337); public static IconUsage Link => GetSolid(0xf0c1); diff --git a/fluXis/Localization/Categories/GeneralStrings.cs b/fluXis/Localization/Categories/GeneralStrings.cs index 9cd01c043..efbc40752 100644 --- a/fluXis/Localization/Categories/GeneralStrings.cs +++ b/fluXis/Localization/Categories/GeneralStrings.cs @@ -22,4 +22,7 @@ public class GeneralStrings : LocalizationCategory public TranslatableString CanNotBeUndone => Get("can-not-be-undone", "This action cannot be undone."); public TranslatableString LoginToUse => Get("login-to-use", "Log in to use this feature."); + + public TranslatableString CopyToClipboard => Get("copy-clipboard", "Copy"); + public TranslatableString CopyToClipboardDescription => Get("copy-clipboard-despcription", "Copy this to Clipboard."); } diff --git a/fluXis/Overlay/Wiki/WikiOverlay.cs b/fluXis/Overlay/Wiki/WikiOverlay.cs index cc1a4c5c3..55dd67c11 100644 --- a/fluXis/Overlay/Wiki/WikiOverlay.cs +++ b/fluXis/Overlay/Wiki/WikiOverlay.cs @@ -1,22 +1,30 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using Humanizer; +using fluXis.Audio; using fluXis.Graphics; using fluXis.Graphics.Containers; using fluXis.Graphics.Containers.Markdown; using fluXis.Graphics.Sprites; +using fluXis.Graphics.Sprites.Icons; using fluXis.Graphics.Sprites.Text; using fluXis.Graphics.UserInterface.Color; +using fluXis.Graphics.UserInterface.Interaction; using fluXis.Input; using fluXis.Online.API.Requests.Wiki; using fluXis.Online.Drawables; using fluXis.Online.Fluxel; using Markdig.Syntax.Inlines; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osuTK; +using osu.Framework.Logging; namespace fluXis.Overlay.Wiki; @@ -27,7 +35,8 @@ public partial class WikiOverlay : OverlayContainer, IKeyBindingHandler currentPath = new(string.Empty); + private Bindable currentHeading = new(string.Empty); private Stack history = new(); private Container content = null!; @@ -80,6 +89,7 @@ private void load() }, scroll = new FluXisScrollContainer { + Margin = new MarginPadding {Top = 80}, RelativeSizeAxes = Axes.Both, ScrollbarVisible = false }, @@ -94,6 +104,98 @@ private void load() Origin = Anchor.Centre, Size = new Vector2(50), Alpha = 0 + }, + new Container + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Margin = new MarginPadding { Top = 60 }, + Height = 75, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Theme.Background2 + }, + new Container + { + Padding = new MarginPadding(10), + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(GridSizeMode.Absolute, 15), + new Dimension(GridSizeMode.Absolute, 1536 - 50 - 15) + }, + Content = new[] + { + new Drawable[] + { + new Container + { + Masking = true, + CornerRadius = 8, + RelativeSizeAxes = Axes.Both, + Margin = new MarginPadding {Left = 5}, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + RelativeSizeAxes = Axes.Both, + Colour = Theme.Background3 + }, + new BackButton(this) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }, + Empty(), + new Container + { + Masking = true, + CornerRadius = 8, + RelativeSizeAxes = Axes.Both, + // Padding = new MarginPadding { Right = 20 }, + Width = 0.987f, // for some reason padding screws up the corners + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + RelativeSizeAxes = Axes.Both, + Colour = Theme.Background3 + }, + new WikiNav(currentPath, currentHeading, this) + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + } + } + } + } + } + } + } } } } @@ -105,13 +207,13 @@ public void NavigateTo(string path, bool keepHistory = false) { Show(); - if (path == currentPath) + if (path == currentPath.Value) return; if (!keepHistory) history.Clear(); - currentPath = path; + currentPath.Value = path; loading.FadeIn(200); var req = new WikiPageRequest(path); @@ -151,6 +253,10 @@ public void NavigateTo(string path, bool keepHistory = false) switch (h.Level) { + case 1: + currentHeading.Value = text?.ToString() ?? string.Empty; + break; + case 2: contents.Add(new ForcedHeightText(true) { @@ -183,17 +289,17 @@ public void NavigateTo(string path, bool keepHistory = false) if (l.StartsWith("/wiki")) l = l[5..]; - history.Push(currentPath); + history.Push(currentPath.Value); NavigateTo(l, true); - } + }, + Text = req.ResponseString }; - md.Text = req.ResponseString; scroll.Add(new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(12) { Top = 50 + 12 }, + Padding = new MarginPadding(12) { Top = 50 + 12, Bottom = 100 }, Child = new GridContainer { RelativeSizeAxes = Axes.X, @@ -264,7 +370,7 @@ public bool NavigateBack() protected override void PopIn() { - if (currentPath == string.Empty) + if (string.IsNullOrEmpty(currentPath.Value)) NavigateTo("/home"); content.ResizeHeightTo(0).MoveToY(1) @@ -296,4 +402,252 @@ public bool OnPressed(KeyBindingPressEvent e) } public void OnReleased(KeyBindingReleaseEvent e) { } + + private partial class WikiNav : FillFlowContainer + { + private Bindable currentPath { get; init; } + private Bindable currentHeading { get; init; } + private readonly WikiOverlay overlay; + private static readonly char[] separator_char = new[] { '/' }; + + public WikiNav(Bindable currentPathBindable, Bindable currentHeadingBindable, WikiOverlay overlay) + { + currentPath = currentPathBindable; + currentHeading = currentHeadingBindable; + this.overlay = overlay; + + Direction = FillDirection.Horizontal; + } + + [BackgroundDependencyLoader] + private void load() + { + currentHeading.BindValueChanged((heading) => + { + if (heading.NewValue == heading.OldValue) return; + + buildNav(currentPath.Value); + }, true); + } + + private void buildNav(string newPath) + { + ScheduleAfterChildren(() => + { + Alpha = 0; + Clear(); + var pathButtons = createPathButtons(newPath); + AddRange(pathButtons); + this.FadeIn(100); + }); + } + + private List createPathButtons(string newPath) + { + var pathNames = newPath.Split(separator_char, StringSplitOptions.RemoveEmptyEntries).ToList(); + var paths = getPaths(newPath); + + if (pathNames.FirstOrDefault() != "home") + { + pathNames.Insert(0, "home"); + paths.Insert(0, "home"); + } + + var pathButtons = new List(); + + foreach ((string name, string path) in pathNames.Zip(paths)) + { + if ("/" + path == newPath || path == newPath) + addPathButton(path, overlay.currentHeading.Value, pathButtons, false); + else + addPathButton(path, name.Humanize(LetterCasing.Title), pathButtons); + } + + return pathButtons; + } + + private void addPathButton(string path, string name, List list, bool addSeparator = true) + { + list.Add(new PathButton(path, name, overlay.NavigateTo)); + + if (addSeparator) + { + list.Add(new Separator()); + } + } + + private List getPaths(string path) + { + var pathNames = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + var paths = new List(); + + if (pathNames.Length == 0) + return paths; + + string currentPath = pathNames[0]; + paths.Add(currentPath); + + for (int i = 1; i < pathNames.Length; i++) + { + currentPath += "/" + pathNames[i]; + paths.Add(currentPath); + } + + return paths; + } + + private partial class Separator : FluXisSpriteIcon + { + public Separator() + { + Icon = FontAwesome6.Solid.AngleRight; + Size = new Vector2(15); + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + } + } + } + + private partial class BackButton : ClickableContainer + { + [Resolved] + private UISamples? samples { get; set; } + + private readonly WikiOverlay overlay; + + protected HoverLayer Hover = null!; + protected FlashLayer Flash = null!; + + public BackButton(WikiOverlay overlay) + { + this.overlay = overlay; + } + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(46); + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + CornerRadius = 10; + Masking = true; + + InternalChildren = new Drawable[] + { + Hover = new HoverLayer(), + Flash = new FlashLayer(), + new FluXisSpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome6.Solid.AngleLeft, + Size = new Vector2(18) + } + }; + } + + protected override bool OnHover(HoverEvent e) + { + samples?.Hover(); + Hover.Show(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + Hover.Hide(); + base.OnHoverLost(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + this.ScaleTo(.9f, 1000, Easing.OutQuint); + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + this.ScaleTo(1, 1000, Easing.OutElastic); + } + + protected override bool OnClick(ClickEvent e) + { + overlay.NavigateBack(); + Flash.Show(); + return base.OnClick(e); + } + } + + private partial class PathButton : ClickableContainer + { + [Resolved] + private UISamples? samples { get; set; } + + public string Path { get; private set; } + public new string Name { get; private set; } + + private readonly Action navigateAction; + + private FluXisSpriteText text = null!; + + public PathButton(string path, string name, Action navigateAction) + { + Path = path; + Name = name; + this.navigateAction = navigateAction; + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + CornerRadius = 5, + Masking = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding(10), + Children = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Child = text = new FluXisSpriteText() + { + Text = Name, + FontSize = 30, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } + } + } + }; + } + + protected override bool OnHover(HoverEvent e) + { + samples?.Hover(); + text.FadeColour(Theme.Highlight, 200, Easing.Out); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + text.FadeColour(Theme.Text, 200, Easing.Out); + base.OnHoverLost(e); + } + + protected override bool OnClick(ClickEvent e) + { + navigateAction?.Invoke(Path, true); + return base.OnClick(e); + } + } }