Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .agent/DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Decisions Log
# Decisions Log

Record meaningful technical decisions here. Use one entry per decision.

Expand All @@ -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.
Expand Down Expand Up @@ -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.

4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 7 additions & 1 deletion OpenUtauMobile/Assets/Lang/Strings.en.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@
<value>Failed to open link</value>
</data>
<data name="ErrorDialog.Title" xml:space="preserve">
<value>An error occurred</value>
<value>OpenUtau Mobile Runtime Error</value>
</data>
<data name="ErrorDialog.UnknownError" xml:space="preserve">
<value>Unknown error</value>
Expand Down Expand Up @@ -1750,4 +1750,10 @@
<data name="PhonemePanel.Reset.ReleaseHint" xml:space="preserve">
<value>Release to reset parameter</value>
</data>
<data name="ErrorDialog.CopySucceeded" xml:space="preserve">
<value>Error information copied</value>
</data>
<data name="ErrorDialog.CopyFailed" xml:space="preserve">
<value>Failed to copy error information</value>
</data>
</root>
8 changes: 7 additions & 1 deletion OpenUtauMobile/Assets/Lang/Strings.ja.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1069,7 +1069,7 @@
<value>リンクを開けませんでした</value>
</data>
<data name="ErrorDialog.Title" xml:space="preserve">
<value>エラーが発生しました</value>
<value>OpenUtau Mobile 実行時エラー</value>
</data>
<data name="ErrorDialog.UnknownError" xml:space="preserve">
<value>不明なエラー</value>
Expand Down Expand Up @@ -1743,4 +1743,10 @@
<data name="PhonemePanel.Reset.ReleaseHint" xml:space="preserve">
<value>指を離すとパラメーターをリセット</value>
</data>
<data name="ErrorDialog.CopySucceeded" xml:space="preserve">
<value>エラー情報をコピーしました</value>
</data>
<data name="ErrorDialog.CopyFailed" xml:space="preserve">
<value>エラー情報をコピーできませんでした</value>
</data>
</root>
8 changes: 7 additions & 1 deletion OpenUtauMobile/Assets/Lang/Strings.ru.resx
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@
<value>Не удалось открыть ссылку</value>
</data>
<data name="ErrorDialog.Title" xml:space="preserve">
<value>Произошла ошибка</value>
<value>Ошибка выполнения OpenUtau Mobile</value>
</data>
<data name="ErrorDialog.UnknownError" xml:space="preserve">
<value>Неизвестная ошибка</value>
Expand Down Expand Up @@ -1705,4 +1705,10 @@
<data name="PhonemePanel.Reset.ReleaseHint" xml:space="preserve">
<value>Отпустите, чтобы сбросить параметр</value>
</data>
<data name="ErrorDialog.CopySucceeded" xml:space="preserve">
<value>Информация об ошибке скопирована</value>
</data>
<data name="ErrorDialog.CopyFailed" xml:space="preserve">
<value>Не удалось скопировать информацию об ошибке</value>
</data>
</root>
8 changes: 7 additions & 1 deletion OpenUtauMobile/Assets/Lang/Strings.uk.resx
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@
<value>Не вдалося відкрити посилання</value>
</data>
<data name="ErrorDialog.Title" xml:space="preserve">
<value>Сталася помилка</value>
<value>Помилка виконання OpenUtau Mobile</value>
</data>
<data name="ErrorDialog.UnknownError" xml:space="preserve">
<value>Невідома помилка</value>
Expand Down Expand Up @@ -1705,4 +1705,10 @@
<data name="PhonemePanel.Reset.ReleaseHint" xml:space="preserve">
<value>Відпустіть, щоб скинути параметр</value>
</data>
<data name="ErrorDialog.CopySucceeded" xml:space="preserve">
<value>Інформацію про помилку скопійовано</value>
</data>
<data name="ErrorDialog.CopyFailed" xml:space="preserve">
<value>Не вдалося скопіювати інформацію про помилку</value>
</data>
</root>
8 changes: 7 additions & 1 deletion OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@
<value>无法打开链接</value>
</data>
<data name="ErrorDialog.Title" xml:space="preserve">
<value>发生错误</value>
<value>OpenUtau Mobile 运行时错误</value>
</data>
<data name="ErrorDialog.UnknownError" xml:space="preserve">
<value>未知错误</value>
Expand Down Expand Up @@ -1750,4 +1750,10 @@
<data name="PhonemePanel.Reset.ReleaseHint" xml:space="preserve">
<value>松开以重置参数</value>
</data>
<data name="ErrorDialog.CopySucceeded" xml:space="preserve">
<value>已复制报错信息</value>
</data>
<data name="ErrorDialog.CopyFailed" xml:space="preserve">
<value>复制报错信息失败</value>
</data>
</root>
127 changes: 58 additions & 69 deletions OpenUtauMobile/Controls/ErrorDialogPopup.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,75 +4,44 @@
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}"
x:DataType="vm:ErrorDialogViewModel"
Classes="PopupDialogRoot">

<UserControl.Resources>
<!-- 错误图标(感叹号圆形) -->
<StreamGeometry x:Key="IconError">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</StreamGeometry>
<!-- 关闭 / X -->
<StreamGeometry x:Key="IconClose">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</StreamGeometry>
<!-- 展开/折叠 Chevron -->
<StreamGeometry x:Key="IconChevronDown">M7.41,8.59 L12,13.17 L16.59,8.59 L18,10 L12,16 L6,10 L7.41,8.59 Z</StreamGeometry>
<StreamGeometry x:Key="IconChevronUp">M7.41,15.41 L12,10.83 L16.59,15.41 L18,14 L12,8 L6,14 L7.41,15.41 Z</StreamGeometry>
</UserControl.Resources>

<Border Classes="PopupDialogSurface">
<DockPanel>

<!-- ── Header ── -->
<Border Classes="PopupTitleBar"
DockPanel.Dock="Top"
Padding="8,8,8,8">
<Grid ColumnDefinitions="Auto,*,Auto">

<!-- 错误图标 -->
<Panel Grid.Column="0"
Margin="8,0,0,0"
VerticalAlignment="Center">
<PathIcon Width="20" Height="20"
Data="{StaticResource IconError}"
Foreground="{DynamicResource Sem.Color.Error}" />
</Panel>

<!-- 标题 -->
<StackPanel Grid.Column="1"
Margin="8,0,4,0"
VerticalAlignment="Center"
Spacing="1">
<TextBlock Text="{Binding Title}"
Classes="PopupTitle" />
</StackPanel>

<!-- 关闭按钮 -->
<Button Grid.Column="2"
<Grid RowDefinitions="Auto,*,Auto">
<Border Grid.Row="0" Classes="PopupTitleBar" Padding="8">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0"
Margin="8,0,4,0"
Text="{Binding Title}"
Classes="PopupTitle" />
<Button Grid.Column="1"
Classes="IconBtn PopupCloseBtn"
Command="{Binding CloseCommand}">
<PathIcon Width="18" Height="18"
Data="{StaticResource IconClose}"
Foreground="{DynamicResource Sem.Color.OnSurface}" />
<iconPacks:PackIconPhosphorIcons Width="18" Height="18" Kind="X" />
</Button>
</Grid>
</Border>

<!-- ── Footer ── -->
<Border DockPanel.Dock="Bottom"
Padding="12,10,12,12">
<Button Classes="PrimaryBtn"
Content="{DynamicResource Common.Confirm}"
Command="{Binding CloseCommand}" />
</Border>

<!-- ── Content ── -->
<StackPanel Orientation="Vertical" Margin="{x:Static theme:ThemeSemLayoutTokens.InsetCardCompact}"
Spacing="8">
<TextBlock Text="{Binding Message}"
TextWrapping="Wrap" />
<Grid Grid.Row="1"
Margin="{x:Static theme:ThemeSemErrorDialogTokens.ContentInset}"
RowDefinitions="Auto,Auto,*">
<ScrollViewer MaxHeight="{x:Static theme:ThemeSemErrorDialogTokens.MessageMaxHeight}"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<TextBlock Text="{Binding Message}" TextWrapping="Wrap" />
</ScrollViewer>

<!-- 详情折叠切换 -->
<Button DockPanel.Dock="Top"
<Button Grid.Row="1"
Classes="DetailToggle"
IsVisible="{Binding HasDetail}"
x:Name="DetailToggleBtn"
Expand All @@ -82,27 +51,47 @@
x:Name="DetailChevron"
Data="{StaticResource IconChevronDown}"
Foreground="{DynamicResource Sem.Color.OnSurface}" />
<TextBlock Text="{DynamicResource Common.Details}" VerticalAlignment="Center" />
<TextBlock Text="{DynamicResource Common.Details}"
VerticalAlignment="Center" />
</StackPanel>
</Button>

<!-- 详情文本(默认折叠) -->
<ScrollViewer DockPanel.Dock="Top"
x:Name="DetailPanel"
IsVisible="False"
MaxHeight="200"
Margin="0,6,0,0"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<TextBlock Text="{Binding Detail}"
FontFamily="Consolas, Courier New, monospace"
FontSize="11"
TextWrapping="Wrap"
Opacity="0.7" />
</ScrollViewer>

</StackPanel>
<TextBox Grid.Row="2"
x:Name="DetailPanel"
Classes="ErrorDetail"
IsVisible="False"
IsReadOnly="True"
AcceptsReturn="True"
Text="{Binding Detail}"
MaxHeight="{x:Static theme:ThemeSemErrorDialogTokens.DetailMaxHeight}"
Margin="0,6,0,0"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
TextWrapping="Wrap" />
</Grid>

</DockPanel>
<Border Grid.Row="2"
Padding="{x:Static theme:ThemeSemErrorDialogTokens.FooterInset}">
<Grid ColumnDefinitions="*,*">
<Button Classes="SecondaryBtn"
MinHeight="{x:Static theme:ThemeSemErrorDialogTokens.ActionMinHeight}"
Command="{Binding CopyCommand}">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="8">
<iconPacks:PackIconPhosphorIcons Kind="Copy" Width="18" Height="18" />
<TextBlock Text="{DynamicResource Common.Copy}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Grid.Column="1"
Classes="PrimaryBtn"
Margin="{x:Static theme:ThemeSemErrorDialogTokens.ActionSpacing}"
MinHeight="{x:Static theme:ThemeSemErrorDialogTokens.ActionMinHeight}"
Content="{DynamicResource Common.Confirm}"
Command="{Binding CloseCommand}" />
</Grid>
</Border>
</Grid>
</Border>
</UserControl>
</UserControl>
47 changes: 45 additions & 2 deletions OpenUtauMobile/Controls/ErrorDialogPopup.axaml.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
Expand All @@ -21,4 +64,4 @@ private void OnDetailToggleClicked(object? sender, RoutedEventArgs e)
? (Avalonia.Media.Geometry?)Resources["IconChevronUp"]
: (Avalonia.Media.Geometry?)Resources["IconChevronDown"];
}
}
}
32 changes: 32 additions & 0 deletions OpenUtauMobile/Services/AvaloniaClipboardService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Input.Platform;

namespace OpenUtauMobile.Services;

/// <summary>
/// 基于 Avalonia 顶层窗口的跨平台剪贴板实现
/// </summary>
public sealed class AvaloniaClipboardService : IClipboardService
{
public async Task<bool> 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;
}
}
}
Loading
Loading