diff --git a/.agent/DECISIONS.md b/.agent/DECISIONS.md
index 7a72c5d6..4967b98b 100644
--- a/.agent/DECISIONS.md
+++ b/.agent/DECISIONS.md
@@ -1,4 +1,4 @@
-# Decisions Log
+# Decisions Log
Record meaningful technical decisions here. Use one entry per decision.
@@ -12,6 +12,12 @@ Record meaningful technical decisions here. Use one entry per decision.
## Entries
+- Date: 2026-08-31
+- Decision: Expose clipboard text writes through an injectable `IClipboardService` in `ServiceHub`, backed by Avalonia's current `TopLevel` clipboard. Keep error-detail selection and copy orchestration in the Mobile UI/ViewModel layer.
+- Rationale: The service follows the existing cross-platform capability pattern, keeps ViewModels independent of window/platform APIs, and uses Avalonia's native clipboard bridge across desktop, mobile, and browser hosts without duplicating platform code.
+- Alternatives considered: Access `TopLevel.Clipboard` directly from the popup code-behind; add one native clipboard implementation to every platform host; place clipboard behavior in Core.
+- Impacted areas: Shared Mobile services, error-dialog ViewModel/layout/styles, localization, and responsive dialog sizing. Core and platform hosts are unchanged.
+
- Date: 2026-08-31
- Decision: Render track parts with a 4 px rounded container and use `Sem.Color.OnSurfaceVariant` for note and waveform previews. Cache the resolved waveform RGBA value and redraw cached waveforms when the active theme changes it. Keep the part label inside the visible viewport and reserve space for selected-part resize handles.
- Rationale: Note and waveform previews are supporting graphics rather than primary text, so MD3's on-surface-variant role gives them appropriate visual hierarchy while adapting to light, dark, and dynamic color schemes. Rounded clipping keeps cached bitmap content inside the same silhouette as the part background. A viewport-clamped label remains readable while a long part is scrolled, and handle clearance prevents selected-state controls from covering its leading characters.
@@ -280,4 +286,3 @@ Record meaningful technical decisions here. Use one entry per decision.
- Rationale: Platform-generated zero-distance or jitter `PointerMoved` events previously invalidated every two-finger tap, undo could run while another pointer remained captured, coordinate/timestamp snapshot matching was not a stable touch identity, and partial capture-loss recovery could retain another pointer as a false pinch participant.
- Alternatives considered: Add logging without changing recognition; rely on tighter platform event filtering; continue reinitializing a reduced gesture after capture loss.
- Impacted areas: Shared Windows/Android pointer gesture recognition for tap, undo/redo, pinch, cancellation, and post-cancellation single-finger input.
-
diff --git a/AGENTS.md b/AGENTS.md
index af163136..f53fdee0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,3 +6,7 @@ Read [`.agent/README.md`](.agent/README.md) for a detailed explanation of the ag
- Use Git directly for diffs, restoration, and rollback. Do not create or maintain manual backup copies, rollback scripts, rollback directories, or other rollback artifacts.
- At the start of every task, run `git status --short`. If the working tree contains any pending uncommitted changes, stop before making changes and notify the user. Do not alter, stash, discard, or clean those pending changes unless the user explicitly instructs you to do so.
+
+## Avalonia build command
+
+- Remember to execute `$env:AVALONIA_TELEMETRY_OPTOUT='1';` before running `dotnet build` to disable Avalonia telemetry.
\ No newline at end of file
diff --git a/OpenUtauMobile/Assets/Lang/Strings.en.resx b/OpenUtauMobile/Assets/Lang/Strings.en.resx
index 5adec2ec..2ece0fa2 100644
--- a/OpenUtauMobile/Assets/Lang/Strings.en.resx
+++ b/OpenUtauMobile/Assets/Lang/Strings.en.resx
@@ -1072,7 +1072,7 @@
Failed to open link
- An error occurred
+ OpenUtau Mobile Runtime Error
Unknown error
@@ -1750,4 +1750,10 @@
Release to reset parameter
+
+ Error information copied
+
+
+ Failed to copy error information
+
diff --git a/OpenUtauMobile/Assets/Lang/Strings.ja.resx b/OpenUtauMobile/Assets/Lang/Strings.ja.resx
index df765ece..bda2c415 100644
--- a/OpenUtauMobile/Assets/Lang/Strings.ja.resx
+++ b/OpenUtauMobile/Assets/Lang/Strings.ja.resx
@@ -1069,7 +1069,7 @@
リンクを開けませんでした
- エラーが発生しました
+ OpenUtau Mobile 実行時エラー
不明なエラー
@@ -1743,4 +1743,10 @@
指を離すとパラメーターをリセット
+
+ エラー情報をコピーしました
+
+
+ エラー情報をコピーできませんでした
+
diff --git a/OpenUtauMobile/Assets/Lang/Strings.ru.resx b/OpenUtauMobile/Assets/Lang/Strings.ru.resx
index 7dc5a539..b0210414 100644
--- a/OpenUtauMobile/Assets/Lang/Strings.ru.resx
+++ b/OpenUtauMobile/Assets/Lang/Strings.ru.resx
@@ -995,7 +995,7 @@
Не удалось открыть ссылку
- Произошла ошибка
+ Ошибка выполнения OpenUtau Mobile
Неизвестная ошибка
@@ -1705,4 +1705,10 @@
Отпустите, чтобы сбросить параметр
+
+ Информация об ошибке скопирована
+
+
+ Не удалось скопировать информацию об ошибке
+
diff --git a/OpenUtauMobile/Assets/Lang/Strings.uk.resx b/OpenUtauMobile/Assets/Lang/Strings.uk.resx
index bcf5a4ff..ceacd2de 100644
--- a/OpenUtauMobile/Assets/Lang/Strings.uk.resx
+++ b/OpenUtauMobile/Assets/Lang/Strings.uk.resx
@@ -995,7 +995,7 @@
Не вдалося відкрити посилання
- Сталася помилка
+ Помилка виконання OpenUtau Mobile
Невідома помилка
@@ -1705,4 +1705,10 @@
Відпустіть, щоб скинути параметр
+
+ Інформацію про помилку скопійовано
+
+
+ Не вдалося скопіювати інформацію про помилку
+
diff --git a/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx b/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx
index 756c7ea6..5f0987b4 100644
--- a/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx
+++ b/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx
@@ -1072,7 +1072,7 @@
无法打开链接
- 发生错误
+ OpenUtau Mobile 运行时错误
未知错误
@@ -1750,4 +1750,10 @@
松开以重置参数
+
+ 已复制报错信息
+
+
+ 复制报错信息失败
+
diff --git a/OpenUtauMobile/Controls/ErrorDialogPopup.axaml b/OpenUtauMobile/Controls/ErrorDialogPopup.axaml
index becc0365..7da6d96c 100644
--- a/OpenUtauMobile/Controls/ErrorDialogPopup.axaml
+++ b/OpenUtauMobile/Controls/ErrorDialogPopup.axaml
@@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:OpenUtauMobile.ViewModels"
xmlns:theme="clr-namespace:OpenUtauMobile.Themes.OpenUtauMobile.Runtime"
+ xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
mc:Ignorable="d"
x:Class="OpenUtauMobile.Controls.ErrorDialogPopup"
d:DataContext="{d:DesignInstance Type=vm:ErrorDialogViewModel, IsDesignTimeCreatable=False}"
@@ -11,68 +12,36 @@
Classes="PopupDialogRoot">
-
M12,2 C6.48,2 2,6.48 2,12 C2,17.52 6.48,22 12,22 C17.52,22 22,17.52 22,12 C22,6.48 17.52,2 12,2 Z M13,17 L11,17 L11,15 L13,15 L13,17 Z M13,13 L11,13 L11,7 L13,7 L13,13 Z
-
- M19,6.41 L17.59,5 L12,10.59 L6.41,5 L5,6.41 L10.59,12 L5,17.59 L6.41,19 L12,13.41 L17.59,19 L19,17.59 L13.41,12 Z
-
M7.41,8.59 L12,13.17 L16.59,8.59 L18,10 L12,16 L6,10 L7.41,8.59 Z
+ M7.41,15.41 L12,10.83 L16.59,15.41 L18,14 L12,8 L6,14 L7.41,15.41 Z
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/OpenUtauMobile/Controls/ErrorDialogPopup.axaml.cs b/OpenUtauMobile/Controls/ErrorDialogPopup.axaml.cs
index 4a935ac6..a5365319 100644
--- a/OpenUtauMobile/Controls/ErrorDialogPopup.axaml.cs
+++ b/OpenUtauMobile/Controls/ErrorDialogPopup.axaml.cs
@@ -1,10 +1,15 @@
-using Avalonia.Interactivity;
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using OpenUtauMobile.Themes.OpenUtauMobile.Runtime;
namespace OpenUtauMobile.Controls;
public partial class ErrorDialogPopup : PopupDialogControl
{
private bool _detailExpanded;
+ private TopLevel? _topLevel;
protected override PopupDialogWidthPreset WidthPreset => PopupDialogWidthPreset.Regular;
@@ -13,6 +18,44 @@ public ErrorDialogPopup()
InitializeComponent();
}
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+
+ _topLevel = TopLevel.GetTopLevel(this);
+ if (_topLevel != null)
+ {
+ _topLevel.SizeChanged += OnTopLevelSizeChanged;
+ UpdateResponsiveHeight(_topLevel);
+ }
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ if (_topLevel != null)
+ {
+ _topLevel.SizeChanged -= OnTopLevelSizeChanged;
+ _topLevel = null;
+ }
+
+ base.OnDetachedFromVisualTree(e);
+ }
+
+ private void OnTopLevelSizeChanged(object? sender, SizeChangedEventArgs e)
+ {
+ if (sender is TopLevel topLevel)
+ {
+ UpdateResponsiveHeight(topLevel);
+ }
+ }
+
+ private void UpdateResponsiveHeight(TopLevel topLevel)
+ {
+ MaxHeight = Math.Max(
+ topLevel.ClientSize.Height - ThemeSemErrorDialogTokens.ViewportVerticalInset,
+ ThemeSemErrorDialogTokens.ActionMinHeight);
+ }
+
private void OnDetailToggleClicked(object? sender, RoutedEventArgs e)
{
_detailExpanded = !_detailExpanded;
@@ -21,4 +64,4 @@ private void OnDetailToggleClicked(object? sender, RoutedEventArgs e)
? (Avalonia.Media.Geometry?)Resources["IconChevronUp"]
: (Avalonia.Media.Geometry?)Resources["IconChevronDown"];
}
-}
\ No newline at end of file
+}
diff --git a/OpenUtauMobile/Services/AvaloniaClipboardService.cs b/OpenUtauMobile/Services/AvaloniaClipboardService.cs
new file mode 100644
index 00000000..aeb44b6c
--- /dev/null
+++ b/OpenUtauMobile/Services/AvaloniaClipboardService.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Input.Platform;
+
+namespace OpenUtauMobile.Services;
+
+///
+/// 基于 Avalonia 顶层窗口的跨平台剪贴板实现
+///
+public sealed class AvaloniaClipboardService : IClipboardService
+{
+ public async Task SetTextAsync(string text)
+ {
+ try
+ {
+ TopLevel? topLevel = AppService.GetTopLevel();
+ IClipboard? clipboard = topLevel?.Clipboard;
+ if (clipboard == null)
+ {
+ return false;
+ }
+
+ await clipboard.SetTextAsync(text);
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+}
diff --git a/OpenUtauMobile/Services/IClipboardService.cs b/OpenUtauMobile/Services/IClipboardService.cs
new file mode 100644
index 00000000..61c31ff2
--- /dev/null
+++ b/OpenUtauMobile/Services/IClipboardService.cs
@@ -0,0 +1,16 @@
+using System.Threading.Tasks;
+
+namespace OpenUtauMobile.Services;
+
+///
+/// 跨平台剪贴板写入服务
+///
+public interface IClipboardService
+{
+ ///
+ /// 将纯文本写入系统剪贴板
+ ///
+ /// 待写入的文本
+ /// 写入是否成功
+ Task SetTextAsync(string text);
+}
diff --git a/OpenUtauMobile/Services/ServiceHub.cs b/OpenUtauMobile/Services/ServiceHub.cs
index efc7c5fa..04e899e8 100644
--- a/OpenUtauMobile/Services/ServiceHub.cs
+++ b/OpenUtauMobile/Services/ServiceHub.cs
@@ -12,6 +12,7 @@ namespace OpenUtauMobile.Services;
public static class ServiceHub
{
public static Action? InitAudioOutput { get; set; }
+ public static IClipboardService ClipboardService { get; set; } = new AvaloniaClipboardService();
public static IExternalUrlLauncher? ExternalUrlLauncher { get; set; }
public static IExternalStorageService? ExternalStorageService { get; set; }
public static ISystemAccentColorProvider? SystemAccentColorProvider { get; set; }
diff --git a/OpenUtauMobile/Themes/OpenUtauMobile/Runtime/ThemeStaticTokens.cs b/OpenUtauMobile/Themes/OpenUtauMobile/Runtime/ThemeStaticTokens.cs
index eb4a4023..ce154f82 100644
--- a/OpenUtauMobile/Themes/OpenUtauMobile/Runtime/ThemeStaticTokens.cs
+++ b/OpenUtauMobile/Themes/OpenUtauMobile/Runtime/ThemeStaticTokens.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Avalonia;
using Avalonia.Media;
@@ -284,6 +284,18 @@ public static class ThemeSemDependencyManagerTokens
public static CornerRadius BadgeCorner => ThemeBaseShapeTokens.CornerM;
}
+public static class ThemeSemErrorDialogTokens
+{
+ public static double ViewportVerticalInset => ThemeBaseLayoutTokens.SpaceXXL;
+ public static double MessageMaxHeight => 120d;
+ public static double DetailMaxHeight => 280d;
+ public static double ActionMinHeight => ThemeSemLayoutTokens.SizeNavToggleMin;
+
+ public static Thickness ContentInset => new(16, 12);
+ public static Thickness FooterInset => new(12);
+ public static Thickness ActionSpacing => new(8, 0, 0, 0);
+}
+
public static class ThemeSemOpacityTokens
{
// ── Semantic Opacity Levels ──
diff --git a/OpenUtauMobile/Themes/OpenUtauMobile/Styles/Controls/ErrorDialogPopup.axaml b/OpenUtauMobile/Themes/OpenUtauMobile/Styles/Controls/ErrorDialogPopup.axaml
index 62e0b1c7..7eaf1ffc 100644
--- a/OpenUtauMobile/Themes/OpenUtauMobile/Styles/Controls/ErrorDialogPopup.axaml
+++ b/OpenUtauMobile/Themes/OpenUtauMobile/Styles/Controls/ErrorDialogPopup.axaml
@@ -1,6 +1,6 @@
-
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:theme="clr-namespace:OpenUtauMobile.Themes.OpenUtauMobile.Runtime">
-
-
+
+
-
\ No newline at end of file
+
+
+
diff --git a/OpenUtauMobile/ViewModels/ErrorDialogViewModel.cs b/OpenUtauMobile/ViewModels/ErrorDialogViewModel.cs
index fefe16fd..1444589e 100644
--- a/OpenUtauMobile/ViewModels/ErrorDialogViewModel.cs
+++ b/OpenUtauMobile/ViewModels/ErrorDialogViewModel.cs
@@ -1,6 +1,9 @@
-using System.Reactive;
+using System.Reactive;
+using System.Text;
+using System.Threading.Tasks;
using OpenUtau.Core;
using OpenUtauMobile.Helpers;
+using OpenUtauMobile.Services;
using ReactiveUI;
namespace OpenUtauMobile.ViewModels;
@@ -11,19 +14,19 @@ namespace OpenUtauMobile.ViewModels;
///
public class ErrorDialogViewModel : PopupViewModelBase
{
- public string Title { get; }
+ public string Title { get; } = L.S("ErrorDialog.Title");
public string Message { get; }
public string Detail { get; }
public bool HasDetail => !string.IsNullOrWhiteSpace(Detail);
public ReactiveCommand CloseCommand { get; }
+ public ReactiveCommand CopyCommand { get; }
public ErrorDialogViewModel(ErrorMessageNotification notification)
{
// 提取友好摘要
if (notification.e is MessageCustomizableException mce)
{
- Title = L.S("ErrorDialog.Title");
Message = string.IsNullOrWhiteSpace(mce.Message)
? mce.SubstanceException.Message
: mce.Message;
@@ -31,7 +34,6 @@ public ErrorDialogViewModel(ErrorMessageNotification notification)
}
else if (notification.e != null)
{
- Title = L.S("ErrorDialog.Title");
Message = string.IsNullOrWhiteSpace(notification.message)
? notification.e.Message
: notification.message;
@@ -39,7 +41,6 @@ public ErrorDialogViewModel(ErrorMessageNotification notification)
}
else
{
- Title = L.S("ErrorDialog.Title");
Message = string.IsNullOrWhiteSpace(notification.message)
? L.S("ErrorDialog.UnknownError")
: notification.message;
@@ -47,10 +48,28 @@ public ErrorDialogViewModel(ErrorMessageNotification notification)
}
CloseCommand = ReactiveCommand.Create(RequestBack);
+ CopyCommand = ReactiveCommand.CreateFromTask(CopyErrorAsync);
+ }
+
+ private async Task CopyErrorAsync()
+ {
+ StringBuilder text = new();
+ text.AppendLine(Title);
+ text.AppendLine(Message);
+ if (HasDetail)
+ {
+ text.AppendLine();
+ text.Append(Detail);
+ }
+
+ bool copied = await ServiceHub.ClipboardService.SetTextAsync(text.ToString());
+ ToastService.Enqueue(L.S(copied
+ ? "ErrorDialog.CopySucceeded"
+ : "ErrorDialog.CopyFailed"));
}
public override void RequestBack()
{
RaiseClose(null);
}
-}
\ No newline at end of file
+}
diff --git a/OpenUtauMobile/Views/MainView.axaml b/OpenUtauMobile/Views/MainView.axaml
index 43a4d1d8..dee9c81d 100644
--- a/OpenUtauMobile/Views/MainView.axaml
+++ b/OpenUtauMobile/Views/MainView.axaml
@@ -30,10 +30,10 @@
-
+