feat: restore legacy modal and drag behavior

This commit is contained in:
2026-07-16 01:33:48 +09:00
parent 83398044c6
commit d7ec571343
30 changed files with 3597 additions and 321 deletions

View File

@@ -3374,18 +3374,19 @@ public sealed class LegacyOperatorController
Touch();
return CreateSnapshot();
}
var selectedRevision = selected.NamedPlaylist?.Revision ?? -1;
var saved = await SaveCurrentNamedPlaylistAsync(cancellationToken)
.ConfigureAwait(false);
var succeeded =
(saved.NamedPlaylist?.Revision ?? -1) > selectedRevision &&
string.Equals(
saved.NamedPlaylist?.SelectedDefinitionId,
definitionId,
StringComparison.Ordinal) &&
saved.NamedPlaylist?.LastMutationOutcome is
LegacyNamedPlaylistMutationOutcome.CommittedFresh or
LegacyNamedPlaylistMutationOutcome.CommittedOptimistic &&
saved.StatusKind == LegacyOperatorStatusKind.Information;
LegacyNamedPlaylistMutationOutcome.CommittedOptimistic;
CompleteCommand(command, definitionId, succeeded);
Touch();
return CreateSnapshot();
@@ -3441,6 +3442,88 @@ public sealed class LegacyOperatorController
return CreateSnapshot();
}
/// <summary>
/// Completes a correlated named-playlist command when the native dispatch
/// boundary rejects the request or encounters an unexpected exception.
/// The terminal receipt prevents the WebView modal from waiting indefinitely;
/// the named-playlist workflow remains the authority for any write quarantine
/// that was already latched before the exception escaped.
/// </summary>
public LegacyOperatorSnapshot ReportNamedPlaylistCommandFailure(
string command,
string targetId,
string message)
{
ArgumentException.ThrowIfNullOrWhiteSpace(command);
ArgumentException.ThrowIfNullOrWhiteSpace(targetId);
if (command is not "refresh-named-playlists" and
not "select-named-playlist" and
not "create-named-playlist" and
not "delete-selected-named-playlist" and
not "load-named-playlist-by-id" and
not "save-current-named-playlist-to")
{
throw new ArgumentOutOfRangeException(
nameof(command),
command,
"Only correlated named-playlist commands can be completed here.");
}
CompleteCommand(command, targetId, succeeded: false);
return ReportError(message);
}
public LegacyOperatorSnapshot CompleteNamedPlaylistDispatch(
string command,
string requestId,
bool succeeded)
{
ArgumentException.ThrowIfNullOrWhiteSpace(command);
ArgumentException.ThrowIfNullOrWhiteSpace(requestId);
if (command is not "refresh-named-playlists" and
not "select-named-playlist" and
not "create-named-playlist" and
not "delete-selected-named-playlist")
{
throw new ArgumentOutOfRangeException(
nameof(command),
command,
"The command does not use a named-playlist dispatch id.");
}
CompleteCommand(command, requestId, succeeded);
Touch();
return CreateSnapshot();
}
/// <summary>
/// Terminates an accepted save command without making its database outcome
/// retryable. This is intentionally separate from a known pre-dispatch
/// rejection: an escaped post-dispatch exception permanently quarantines every
/// later named-playlist write in this process.
/// </summary>
public LegacyOperatorSnapshot ReportNamedPlaylistWriteOutcomeUnknown(
string command,
string targetId,
string message)
{
ArgumentException.ThrowIfNullOrWhiteSpace(command);
ArgumentException.ThrowIfNullOrWhiteSpace(targetId);
if (command is not "create-named-playlist" and
not "delete-selected-named-playlist" and
not "save-current-named-playlist-to")
{
throw new ArgumentOutOfRangeException(
nameof(command),
command,
"The command is not a named-playlist write.");
}
_namedPlaylistWorkflow?.QuarantineAmbiguousWriteOutcome();
CompleteCommand(command, targetId, succeeded: false);
return ReportError(message);
}
public LegacyOperatorSnapshot ReportInformation(string message)
{
_statusMessage = message ?? string.Empty;

View File

@@ -297,6 +297,28 @@ public sealed class LegacyNamedPlaylistWorkflowController
}
}
/// <summary>
/// Conservatively latches the no-retry barrier when an exception escapes the
/// native dispatch boundary after a named-playlist write was accepted. The
/// caller cannot prove whether the database observed the mutation, so every
/// later write in this workflow instance must remain blocked until restart.
/// </summary>
public LegacyNamedPlaylistWorkflowSnapshot QuarantineAmbiguousWriteOutcome()
{
lock (_gate)
{
_isWriteQuarantined = true;
_lastMutationOutcome = LegacyNamedPlaylistMutationOutcome.OutcomeUnknown;
_statusMessage = QuarantineMessage;
_statusKind = LegacyOperatorStatusKind.Error;
++_mutationEpoch;
++_listGeneration;
++_loadGeneration;
Touch();
return CreateSnapshot();
}
}
public async Task<LegacyNamedPlaylistWorkflowSnapshot> RefreshAsync(
int maximumResults = DefaultMaximumDefinitions,
CancellationToken cancellationToken = default)
@@ -854,9 +876,7 @@ public sealed class LegacyNamedPlaylistWorkflowController
definitions,
list.IsTruncated,
suggested,
LegacyNamedPlaylistFreshness.Fresh,
programCode);
ApplyDocument(document, LegacyNamedPlaylistFreshness.Fresh);
LegacyNamedPlaylistFreshness.Fresh);
Touch();
}
@@ -968,10 +988,9 @@ public sealed class LegacyNamedPlaylistWorkflowController
IReadOnlyList<NamedPlaylistSummary> summaries,
bool isTruncated,
string suggestedProgramCode,
LegacyNamedPlaylistFreshness freshness,
string? preferredSelectionCode = null)
LegacyNamedPlaylistFreshness freshness)
{
var selectedCode = preferredSelectionCode ?? _selectedProgramCode;
var previousSelectedCode = _selectedProgramCode;
_definitions = summaries.Select(summary => new DefinitionState(
NextOpaqueId("named-definition"),
summary)).ToList();
@@ -979,12 +998,11 @@ public sealed class LegacyNamedPlaylistWorkflowController
_suggestedProgramCode = suggestedProgramCode;
_listFreshness = freshness;
var selected = selectedCode is null
? null
: _definitions.FirstOrDefault(definition => string.Equals(
definition.Summary.ProgramCode,
selectedCode,
StringComparison.Ordinal));
// WinForms assigns a new DataSource on every PList refresh. A non-empty
// ListBox therefore selects index zero, including after add and delete.
// Keep that operator-visible reset instead of preserving a Web selection
// or selecting the newly created definition.
var selected = _definitions.FirstOrDefault();
if (selected is null)
{
_selectedProgramCode = null;
@@ -999,6 +1017,15 @@ public sealed class LegacyNamedPlaylistWorkflowController
_selectedProgramCode = selected.Summary.ProgramCode;
_selectedDefinitionId = selected.DefinitionId;
_selectedTitle = selected.Summary.Title;
if (!string.Equals(
previousSelectedCode,
selected.Summary.ProgramCode,
StringComparison.Ordinal))
{
_rows = [];
_documentFreshness = LegacyNamedPlaylistFreshness.None;
++_loadGeneration;
}
}
private void ApplyDocument(