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
12 changes: 12 additions & 0 deletions .agent/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ Record meaningful technical decisions here. Use one entry per decision.

## Entries

- Date: 2026-09-02
- Decision: Add a reusable mobile-first option confirmation popup that accepts a title, content, and typed display/value options, lays actions out as full-width touch targets, and returns the selected stable string value or null when dismissed. Require this confirmation before singer uninstall.
- Rationale: The existing exit-editor confirmation is fixed to editor-specific labels and a byte register. A generic string-valued popup supports localized labels without coupling callers to displayed text and keeps destructive operations explicit on narrow screens.
- Alternatives considered: Reuse the fixed exit-editor popup; add a singer-only confirmation popup; compare localized option labels as results.
- Impacted areas: Mobile popup controls/services/theme tokens, singer uninstall interaction, and localization.

- Date: 2026-09-02
- Decision: Centralize installed-singer removal in `SingerManager.UninstallSinger`. Remove directory-based voicebanks recursively and single-file Vogen packages directly, release singer memory first, and always rescan installed singers in `finally`. Run the operation from the singer detail page inside the shared loading popup.
- Rationale: Core owns singer location semantics and the installed-singer index, while Mobile owns progress and error presentation. Keeping the rescan in Core guarantees that success and failure paths expose the current filesystem state.
- Alternatives considered: Delete `USinger.Location` directly in the Mobile ViewModel; refresh only after successful deletion; route removal through a platform storage service.
- Impacted areas: Core singer lifecycle, Mobile singer detail interaction, and singer-detail localization.

- Date: 2026-09-01
- Decision: Add a dedicated batch-lyric popup to the Hand, Note, and MultiSelect piano-roll context menus. Initialize it from the earliest selected note through the end of the active voice part, and apply one undoable multi-note lyric command either sequentially or to the selected-note snapshot.
- Rationale: A focused text editor supports fast paste-and-replace workflows while preserving the current note selection and Core's existing undo/validation behavior. Keeping parsing and scope selection in the Mobile ViewModel avoids changes to upstream-derived Core.
Expand Down
35 changes: 35 additions & 0 deletions OpenUtau.Core/SingerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,41 @@ public USinger GetSinger(string name) {
return null;
}

/// <summary>
/// 卸载歌手,并在操作结束后重新扫描已安装歌手。
/// </summary>
/// <param name="singer">要卸载的已安装歌手。</param>
public void UninstallSinger(USinger singer)
{
try
{
ArgumentNullException.ThrowIfNull(singer);
string location = singer.Location;
if (string.IsNullOrWhiteSpace(location))
{
throw new InvalidOperationException("Singer location is empty.");
}

singer.FreeMemory();
if (Directory.Exists(location))
{
Directory.Delete(location, true);
}
else if (File.Exists(location))
{
File.Delete(location);
}
else
{
throw new DirectoryNotFoundException($"Singer location does not exist: {location}");
}
}
finally
{
SearchAllSingers();
}
}

public void ScheduleReload(USinger singer) {
reloadQueue.Enqueue(singer);
ScheduleReload();
Expand Down
15 changes: 15 additions & 0 deletions OpenUtauMobile/Assets/Lang/Strings.en.resx
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,21 @@
<data name="SingerDetail.Delete" xml:space="preserve">
<value>Delete Singer</value>
</data>
<data name="SingerDetail.Uninstalling" xml:space="preserve">
<value>Uninstalling singer…</value>
</data>
<data name="SingerDetail.UninstallSucceeded" xml:space="preserve">
<value>Uninstalled {0}</value>
</data>
<data name="SingerDetail.UninstallFailed" xml:space="preserve">
<value>Failed to uninstall {0}.</value>
</data>
<data name="SingerDetail.UninstallConfirmTitle" xml:space="preserve">
<value>Uninstall singer?</value>
</data>
<data name="SingerDetail.UninstallConfirmMessage" xml:space="preserve">
<value>Uninstall {0} and permanently remove all of its files? This action cannot be undone.</value>
</data>
<data name="Editor.Undo" xml:space="preserve">
<value>Undo</value>
</data>
Expand Down
15 changes: 15 additions & 0 deletions OpenUtauMobile/Assets/Lang/Strings.ja.resx
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,21 @@
<data name="SingerDetail.Delete" xml:space="preserve">
<value>シンガーを削除</value>
</data>
<data name="SingerDetail.Uninstalling" xml:space="preserve">
<value>シンガーをアンインストールしています…</value>
</data>
<data name="SingerDetail.UninstallSucceeded" xml:space="preserve">
<value>{0} をアンインストールしました</value>
</data>
<data name="SingerDetail.UninstallFailed" xml:space="preserve">
<value>{0} のアンインストールに失敗しました。</value>
</data>
<data name="SingerDetail.UninstallConfirmTitle" xml:space="preserve">
<value>シンガーをアンインストールしますか?</value>
</data>
<data name="SingerDetail.UninstallConfirmMessage" xml:space="preserve">
<value>{0} をアンインストールし、すべてのファイルを完全に削除しますか?この操作は元に戻せません。</value>
</data>
<data name="Editor.Undo" xml:space="preserve">
<value>元に戻す</value>
</data>
Expand Down
15 changes: 15 additions & 0 deletions OpenUtauMobile/Assets/Lang/Strings.ru.resx
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,21 @@
<data name="SingerDetail.Delete" xml:space="preserve">
<value>Удалить банк</value>
</data>
<data name="SingerDetail.Uninstalling" xml:space="preserve">
<value>Удаление банка…</value>
</data>
<data name="SingerDetail.UninstallSucceeded" xml:space="preserve">
<value>Банк {0} удалён</value>
</data>
<data name="SingerDetail.UninstallFailed" xml:space="preserve">
<value>Не удалось удалить банк {0}.</value>
</data>
<data name="SingerDetail.UninstallConfirmTitle" xml:space="preserve">
<value>Удалить банк?</value>
</data>
<data name="SingerDetail.UninstallConfirmMessage" xml:space="preserve">
<value>Удалить банк {0} и навсегда стереть все его файлы? Это действие нельзя отменить.</value>
</data>
<data name="Editor.Undo" xml:space="preserve">
<value>Отменить</value>
</data>
Expand Down
15 changes: 15 additions & 0 deletions OpenUtauMobile/Assets/Lang/Strings.uk.resx
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,21 @@
<data name="SingerDetail.Delete" xml:space="preserve">
<value>Видалити банк</value>
</data>
<data name="SingerDetail.Uninstalling" xml:space="preserve">
<value>Видалення банку…</value>
</data>
<data name="SingerDetail.UninstallSucceeded" xml:space="preserve">
<value>Банк {0} видалено</value>
</data>
<data name="SingerDetail.UninstallFailed" xml:space="preserve">
<value>Не вдалося видалити банк {0}.</value>
</data>
<data name="SingerDetail.UninstallConfirmTitle" xml:space="preserve">
<value>Видалити банк?</value>
</data>
<data name="SingerDetail.UninstallConfirmMessage" xml:space="preserve">
<value>Видалити банк {0} і назавжди стерти всі його файли? Цю дію неможливо скасувати.</value>
</data>
<data name="Editor.Undo" xml:space="preserve">
<value>Скасувати</value>
</data>
Expand Down
15 changes: 15 additions & 0 deletions OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,21 @@
<data name="SingerDetail.Delete" xml:space="preserve">
<value>删除歌手</value>
</data>
<data name="SingerDetail.Uninstalling" xml:space="preserve">
<value>正在卸载歌手…</value>
</data>
<data name="SingerDetail.UninstallSucceeded" xml:space="preserve">
<value>已卸载 {0}</value>
</data>
<data name="SingerDetail.UninstallFailed" xml:space="preserve">
<value>卸载 {0} 失败。</value>
</data>
<data name="SingerDetail.UninstallConfirmTitle" xml:space="preserve">
<value>确认卸载歌手?</value>
</data>
<data name="SingerDetail.UninstallConfirmMessage" xml:space="preserve">
<value>确定卸载 {0} 并永久移除其全部文件吗?此操作不可撤销。</value>
</data>
<data name="Editor.Undo" xml:space="preserve">
<value>撤销</value>
</data>
Expand Down
2 changes: 0 additions & 2 deletions OpenUtauMobile/Controls/ErrorDialogPopup.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
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>
<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">
Expand Down
2 changes: 1 addition & 1 deletion OpenUtauMobile/Controls/NotesCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ private static void DrawNoteResizeHandle(DrawingContext context, Rect noteRect)
private void RenderFinalPitch(int leftTick, int rightTick, DrawingContext context)
{
if (ViewModel == null || Part == null) return;
IPen pen = ThemeResources.GetPen("Sem.Color.Outline", 2);
IPen pen = ViewModel.EditMode == PianoRollEditMode.PitchPen ? ThemeResources.GetPen("Sem.Color.Primary", 2) : ThemeResources.GetPen("Sem.Color.Outline", 2);
StreamGeometry geometry = new();
bool hasVisibleSegment = false;
lock (Part)
Expand Down
74 changes: 74 additions & 0 deletions OpenUtauMobile/Controls/OptionConfirmPopup.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
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.OptionConfirmPopup"
d:DataContext="{d:DesignInstance Type=vm:OptionConfirmPopupViewModel, IsDesignTimeCreatable=False}"
x:DataType="vm:OptionConfirmPopupViewModel"
Classes="PopupDialogRoot">

<Border Classes="PopupDialogSurface">
<DockPanel>
<!-- 标题栏 -->
<Border Classes="PopupTitleBar" DockPanel.Dock="Top">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0"
Text="{Binding Title}"
TextWrapping="Wrap"
Classes="PopupTitle" />
<Button Grid.Column="1"
Classes="PopupCloseBtn"
Command="{Binding CancelCommand}"
ToolTip.Tip="{DynamicResource Common.Close}">
<iconPacks:PackIconPhosphorIcons
Width="{x:Static theme:ThemeSemOptionConfirmTokens.CloseIconSize}"
Height="{x:Static theme:ThemeSemOptionConfirmTokens.CloseIconSize}"
Kind="X" />
</Button>
</Grid>
</Border>

<StackPanel Margin="{x:Static theme:ThemeSemOptionConfirmTokens.ContentInset}"
Spacing="{x:Static theme:ThemeSemOptionConfirmTokens.SectionSpacing}">
<!-- 内容 -->
<ScrollViewer MaxHeight="{x:Static theme:ThemeSemOptionConfirmTokens.MessageMaxHeight}"
VerticalScrollBarVisibility="Auto">
<TextBlock Text="{Binding Content}"
TextWrapping="Wrap"
FontSize="{x:Static theme:ThemeSemTypographyTokens.BodyMSize}" />
</ScrollViewer>

<!-- 选项 -->
<ScrollViewer Height="{x:Static theme:ThemeSemOptionConfirmTokens.OptionsHeight}"
VerticalScrollBarVisibility="Disabled"
HorizontalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Options}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="{x:Static theme:ThemeSemOptionConfirmTokens.OptionSpacing}"
VerticalAlignment="Center"
Orientation="Horizontal"
HorizontalAlignment="Right"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:OptionConfirmOption">
<Button Classes="OptionAction"
Classes.Primary="{Binding IsPrimary}"
Classes.Destructive="{Binding IsDestructive}"
Height="{x:Static theme:ThemeSemOptionConfirmTokens.OptionHeight}"
Content="{Binding Label}"
Command="{Binding $parent[UserControl].((vm:OptionConfirmPopupViewModel)DataContext).SelectOptionCommand}"
CommandParameter="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</DockPanel>
</Border>
</UserControl>
11 changes: 11 additions & 0 deletions OpenUtauMobile/Controls/OptionConfirmPopup.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace OpenUtauMobile.Controls;

public partial class OptionConfirmPopup : PopupDialogControl
{
protected override PopupDialogWidthPreset WidthPreset => PopupDialogWidthPreset.Regular;

public OptionConfirmPopup()
{
InitializeComponent();
}
}
39 changes: 39 additions & 0 deletions OpenUtauMobile/Services/OptionConfirmPopupService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using OpenUtauMobile.Controls;
using OpenUtauMobile.ViewModels;

namespace OpenUtauMobile.Services;

/// <summary>
/// 显示通用选项确认弹窗。
/// </summary>
public static class OptionConfirmPopupService
{
/// <summary>
/// 使用相同的显示文本和返回值展示字符串选项。
/// </summary>
public static Task<string?> ShowAsync(
string title,
string content,
IReadOnlyList<string> options)
{
ArgumentNullException.ThrowIfNull(options);
List<OptionConfirmOption> mappedOptions = options
.Select(option => new OptionConfirmOption(option, option))
.ToList();
return ShowAsync(title, content, mappedOptions);
}

/// <returns>用户所选项的字符串值;关闭弹窗时返回 null。</returns>
public static Task<string?> ShowAsync(
string title,
string content,
IReadOnlyList<OptionConfirmOption> options)
{
OptionConfirmPopupViewModel viewModel = new(title, content, options);
return PopupService.Show<string>(new OptionConfirmPopup(), viewModel);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<StyleInclude Source="/Themes/OpenUtauMobile/Styles/Controls/ErrorDialogPopup.axaml" />
<StyleInclude Source="/Themes/OpenUtauMobile/Styles/Controls/FilePickerPopup.axaml" />
<StyleInclude Source="/Themes/OpenUtauMobile/Styles/Controls/LyricEditPopup.axaml" />
<StyleInclude Source="/Themes/OpenUtauMobile/Styles/Controls/OptionConfirmPopup.axaml" />
<StyleInclude Source="/Themes/OpenUtauMobile/Styles/Controls/PianoRollEditModeSwitcher.axaml" />
<StyleInclude Source="/Themes/OpenUtauMobile/Styles/Controls/PopupCommon.axaml" />
<StyleInclude Source="/Themes/OpenUtauMobile/Styles/Controls/ProjectInfoEditPopup.axaml" />
Expand Down
12 changes: 12 additions & 0 deletions OpenUtauMobile/Themes/OpenUtauMobile/Runtime/ThemeStaticTokens.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,18 @@ public static class ThemeSemErrorDialogTokens
public static Thickness ActionSpacing => new(8, 0, 0, 0);
}

public static class ThemeSemOptionConfirmTokens
{
public static double CloseIconSize => ThemeBaseTypographyTokens.SizeL;
public static double MessageMaxHeight => 160d;
public static double OptionsHeight => ThemeBaseLayoutTokens.SpaceXXL;
public static double OptionHeight => ThemeBaseLayoutTokens.SpaceXL;
public static double SectionSpacing => ThemeSemLayoutTokens.SpaceMD;
public static double OptionSpacing => ThemeSemLayoutTokens.SpaceSM;

public static Thickness ContentInset => ThemeSemLayoutTokens.InsetL;
}

public static class ThemeSemOpacityTokens
{
// ── Semantic Opacity Levels ──
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:theme="clr-namespace:OpenUtauMobile.Themes.OpenUtauMobile.Runtime">
<Style Selector="Button.OptionAction">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="CornerRadius" Value="{x:Static theme:ThemeBaseShapeTokens.CornerM}" />
<Setter Property="Background" Value="{DynamicResource Sem.Color.SecondaryContainer}" />
<Setter Property="Foreground" Value="{DynamicResource Sem.Color.OnSecondaryContainer}" />
<Setter Property="BorderThickness" Value="{x:Static theme:ThemeSemLayoutTokens.BorderNone}" />
<Setter Property="Cursor" Value="Hand" />
</Style>

<Style Selector="Button.OptionAction.Primary">
<Setter Property="Background" Value="{DynamicResource Sem.Color.Primary}" />
<Setter Property="Foreground" Value="{DynamicResource Sem.Color.OnPrimary}" />
</Style>

<Style Selector="Button.OptionAction.Destructive">
<Setter Property="Background" Value="{DynamicResource Sem.Color.Error}" />
<Setter Property="Foreground" Value="{DynamicResource Sem.Color.OnError}" />
</Style>
</Styles>
Loading
Loading