diff --git a/OpenUtau.Core/Util/Preferences.cs b/OpenUtau.Core/Util/Preferences.cs index 704f53c..201dd6a 100644 --- a/OpenUtau.Core/Util/Preferences.cs +++ b/OpenUtau.Core/Util/Preferences.cs @@ -307,6 +307,11 @@ public class SerializablePreferences { /// 自动保存间隔,单位秒(仅在启用时生效)。 /// public int AutoSaveInterval = 120; + /// + /// Stop button behavior: 1 = StartTick -> SelectedPart -> 0 (default), + /// 2 = StartTick -> 0, 3 = Always 0. + /// + public int StopButtonBehavior = 1; #endregion } } diff --git a/OpenUtauMobile/Assets/Lang/Strings.en.resx b/OpenUtauMobile/Assets/Lang/Strings.en.resx index dcf5941..ccb6ee2 100644 --- a/OpenUtauMobile/Assets/Lang/Strings.en.resx +++ b/OpenUtauMobile/Assets/Lang/Strings.en.resx @@ -276,6 +276,21 @@ Smooth Scrolling + + Stop Button Behavior + + + Configure what happens when you press the stop button during and after playback. + + + Start Tick → Selected Part → 0 (Default) + + + Start Tick → 0 + + + Always Go to 0 + Render & Performance diff --git a/OpenUtauMobile/Assets/Lang/Strings.ja.resx b/OpenUtauMobile/Assets/Lang/Strings.ja.resx index 73abca1..eb06525 100644 --- a/OpenUtauMobile/Assets/Lang/Strings.ja.resx +++ b/OpenUtauMobile/Assets/Lang/Strings.ja.resx @@ -276,6 +276,21 @@ スムーズスクロール有効 + + 停止ボタンの動作 + + + 再生中および再生後に停止ボタンを押したときの動作を設定します。 + + + 開始位置 → 選択部分 → 0(デフォルト) + + + 開始位置 → 0 + + + 常に 0 に移動 + レンダーとパフォーマンス diff --git a/OpenUtauMobile/Assets/Lang/Strings.ru.resx b/OpenUtauMobile/Assets/Lang/Strings.ru.resx index 5395d71..a19e3eb 100644 --- a/OpenUtauMobile/Assets/Lang/Strings.ru.resx +++ b/OpenUtauMobile/Assets/Lang/Strings.ru.resx @@ -276,6 +276,21 @@ Плавная прокрутка + + Поведение кнопки Стоп + + + Настройка поведения кнопки остановки во время и после воспроизведения. + + + Начало → Выделенная партия → 0 (По умолчанию) + + + Начало → 0 + + + Всегда к 0 + Рендеринг и производительность diff --git a/OpenUtauMobile/Assets/Lang/Strings.uk.resx b/OpenUtauMobile/Assets/Lang/Strings.uk.resx index e36f194..493d062 100644 --- a/OpenUtauMobile/Assets/Lang/Strings.uk.resx +++ b/OpenUtauMobile/Assets/Lang/Strings.uk.resx @@ -276,6 +276,21 @@ Плавне прокручування + + Поведінка кнопки Стоп + + + Налаштування поведінки кнопки зупинки під час та після відтворення. + + + Початок → Виділена партія → 0 (За замовчуванням) + + + Початок → 0 + + + Завжди до 0 + Рендеринг і продуктивність diff --git a/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx b/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx index 813ab8c..0d1c69f 100644 --- a/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx +++ b/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx @@ -276,6 +276,21 @@ 启用平滑自动翻页 + + 停止按钮行为 + + + 设置在播放期间和播放后按下停止按钮时的行为。 + + + 起始位置 → 选中部分 → 0(默认) + + + 起始位置 → 0 + + + 始终跳转到 0 + 渲染与性能 diff --git a/OpenUtauMobile/ViewModels/EditorViewModel.cs b/OpenUtauMobile/ViewModels/EditorViewModel.cs index 499756a..9597253 100644 --- a/OpenUtauMobile/ViewModels/EditorViewModel.cs +++ b/OpenUtauMobile/ViewModels/EditorViewModel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -226,6 +226,10 @@ public class EditorViewModel : NavigateViewModelBase, ICmdSubscriber, IDisposabl // 最近一次来自回放流的等待标记(SetPlayPosTickNotification.waitingRendering) private bool _streamWaitingRender; + private int _stopSeekState; + private bool _isSeekingInternally = false; + private bool _playheadMovedSinceStart = false; + // 随机数生成器 private Random Randomer { get; } = new(); @@ -254,13 +258,100 @@ public EditorViewModel(MainViewModel navigator, string path = "") : base(navigat { DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(PlaybackManager.Inst.StartTick, true)); } + + if (PlaybackManager.Inst.PlayingMaster || PlaybackManager.Inst.StartingToPlay) + { + _stopSeekState = 0; + } }).DisposeWith(_disposables); // 停止命令 StopCommand = ReactiveCommand.Create(() => { + bool wasPlaying = PlaybackManager.Inst.OutputActive || PlaybackManager.Inst.PlayingMaster; PlaybackManager.Inst.StopPlayback(); SyncPlaybackStateFromAuthority(); - DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0)); + + int mode = Preferences.Default.StopButtonBehavior; + if (mode <= 0 || mode > 3) + { + mode = 1; + } + + if (mode == 3) + { + _isSeekingInternally = true; + DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0)); + _stopSeekState = 3; + _playheadMovedSinceStart = false; + return; + } + + if (wasPlaying) + { + _isSeekingInternally = true; + DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(PlaybackManager.Inst.StartTick)); + _stopSeekState = 1; + _playheadMovedSinceStart = false; + } + else + { + if (_stopSeekState == 0 && !_playheadMovedSinceStart) + { + _isSeekingInternally = true; + DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(PlaybackManager.Inst.StartTick)); + _stopSeekState = 1; + } + else + { + switch (mode) + { + case 2: + _isSeekingInternally = true; + DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0)); + _stopSeekState = 3; + break; + case 3: + _isSeekingInternally = true; + DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0)); + break; + default: + if (_stopSeekState <= 1) + { + int? selectedTick = null; + if (SelectedParts is { Count: > 0 } parts) + { + selectedTick = parts.Min(p => p.position); + } + + if (selectedTick.HasValue && selectedTick.Value != PlayPosTick) + { + _isSeekingInternally = true; + DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(selectedTick.Value)); + _stopSeekState = 2; + } + else + { + _isSeekingInternally = true; + DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0)); + _stopSeekState = 3; + } + } + else if (_stopSeekState == 2) + { + _isSeekingInternally = true; + DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0)); + _stopSeekState = 3; + } + else + { + _isSeekingInternally = true; + DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0)); + _stopSeekState = 3; + } + break; + } + } + } }).DisposeWith(_disposables); // 工程信息编辑命令 EditBpmCommand = ReactiveCommand.CreateFromTask(EditBpmAsync); @@ -392,6 +483,17 @@ public void OnNext(UCommand cmd, bool isUndo) // TODO: 一定要发到UI线程吗?:不需要!docman已经做了 switch (cmd) { + case SeekPlayPosTickNotification _: + if (_isSeekingInternally) + { + _isSeekingInternally = false; + } + else + { + _playheadMovedSinceStart = true; + _stopSeekState = 0; + } + break; case SetPlayPosTickNotification setPlayPos: // 等待渲染期间冻结回放标记,避免位置在等待点附近抖动漂移。 if (!setPlayPos.waitingRendering) diff --git a/OpenUtauMobile/ViewModels/SettingsViewModel.cs b/OpenUtauMobile/ViewModels/SettingsViewModel.cs index 91d1935..9869ebd 100644 --- a/OpenUtauMobile/ViewModels/SettingsViewModel.cs +++ b/OpenUtauMobile/ViewModels/SettingsViewModel.cs @@ -81,6 +81,25 @@ public PianoKeyBehaviorOption(PianoKeyBehavior value, string displayName) } } +public enum StopButtonBehavior +{ + StartTickSelectedPartZero = 1, + StartTickZero = 2, + AlwaysZero = 3 +} + +public class StopButtonBehaviorOption +{ + public StopButtonBehavior Value { get; } + public string DisplayName { get; } + + public StopButtonBehaviorOption(StopButtonBehavior value, string displayName) + { + Value = value; + DisplayName = displayName; + } +} + /// 走带自动翻页行为选项(用于绑定到选择器)。 public class AutoScrollBehaviorOption { @@ -328,6 +347,20 @@ public class SettingsViewModel : NavigateViewModelBase, IDisposable [Reactive] public AutoScrollBehaviorOption? SelectedAutoScrollBehavior { get; set; } + // ── Edit & Behaviour: Stop Button ────────────────────────────── + /// List of available stop button actions. + public IReadOnlyList AvailableStopButtonBehaviors { get; } = + new List + { + new(StopButtonBehavior.StartTickSelectedPartZero, L.S("Settings.StopButton.StartTickSelectedPartZero")), + new(StopButtonBehavior.StartTickZero, L.S("Settings.StopButton.StartTickZero")), + new(StopButtonBehavior.AlwaysZero, L.S("Settings.StopButton.AlwaysZero")) + }; + + /// The currently selected behavior option for the Stop button. + [Reactive] + public StopButtonBehaviorOption? SelectedStopButtonBehavior { get; set; } + /// 回放刷新率(1-60)。 [Reactive] public int PlaybackRefreshRate { get; set; } @@ -551,6 +584,25 @@ await Task.Run(() => }) .DisposeWith(_disposables); + // Initializing Stop Button Behavior + int stopBehaviorVal = Preferences.Default.StopButtonBehavior; + StopButtonBehavior stopBehavior = stopBehaviorVal is >= 1 and <= 3 + ? (StopButtonBehavior)stopBehaviorVal + : StopButtonBehavior.StartTickSelectedPartZero; + SelectedStopButtonBehavior = AvailableStopButtonBehaviors.FirstOrDefault(b => b.Value == stopBehavior) + ?? AvailableStopButtonBehaviors[0]; + + // Monitor changes in the behavior of the Stop button and persist them to Preferences + this.WhenAnyValue(x => x.SelectedStopButtonBehavior) + .Skip(1) + .WhereNotNull() + .Subscribe(opt => + { + Preferences.Default.StopButtonBehavior = (int)opt.Value; + Preferences.Save(); + }) + .DisposeWith(_disposables); + // 编辑与行为:回放刷新率 / 立绘开关 / 撤销上限 PlaybackRefreshRate = Math.Clamp((int)Math.Round(Preferences.Default.PlaybackRefreshRate), 1, 60); ShowPortraitEnabled = Preferences.Default.ShowPortrait; diff --git a/OpenUtauMobile/Views/SettingsView.axaml b/OpenUtauMobile/Views/SettingsView.axaml index 85f8470..a5c2003 100644 --- a/OpenUtauMobile/Views/SettingsView.axaml +++ b/OpenUtauMobile/Views/SettingsView.axaml @@ -15,20 +15,13 @@ - - - - - - - -