Enhance playback settings management in PLib video library manager. Introduce PlaybackOptions for volume and mute settings, integrating them into AppSettings and IAppSettingsStore. Update VideoPlayerViewModel to persist playback state and adjust UI bindings in VideoPlayerView for volume control. Revise README.md to document new playback settings functionality.

This commit is contained in:
Leonid Pershin
2026-08-09 06:46:33 +03:00
parent b13d0148df
commit a938a48de9
14 changed files with 399 additions and 251 deletions
+19 -12
View File
@@ -1,12 +1,19 @@
using PLib.Desktop.Settings;
namespace PLib.Desktop.Services;
/// <summary>
/// Persists the settings the user can change at runtime. The file it writes is also a
/// configuration source, so <c>IOptionsMonitor</c> picks changes up without a restart.
/// </summary>
public interface IAppSettingsStore
{
Task SaveAsync(AppSettings settings, CancellationToken cancellationToken = default);
}
using PLib.Desktop.Settings;
namespace PLib.Desktop.Services;
/// <summary>
/// Persists the settings the user can change at runtime. The file it writes is also a
/// configuration source, so <c>IOptionsMonitor</c> picks changes up without a restart.
/// </summary>
public interface IAppSettingsStore
{
/// <summary>
/// Everything as it stands right now, read back through configuration. Callers are meant
/// to save <c>Current with { ... }</c>: the file is written whole, so building a snapshot
/// by hand is how a section nobody was thinking about gets wiped.
/// </summary>
AppSettings Current { get; }
Task SaveAsync(AppSettings settings, CancellationToken cancellationToken = default);
}
@@ -1,78 +1,90 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using PLib.Application.Library;
using PLib.Desktop.Settings;
using PLib.Infrastructure.Storage;
namespace PLib.Desktop.Services;
/// <inheritdoc cref="IAppSettingsStore"/>
public sealed class JsonAppSettingsStore(IAppPaths paths) : IAppSettingsStore
{
private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true };
private readonly SemaphoreSlim _writeLock = new(1, 1);
private string SettingsFile => Path.Combine(paths.DataDirectory, "settings.json");
public async Task SaveAsync(AppSettings settings, CancellationToken cancellationToken = default)
{
await _writeLock.WaitAsync(cancellationToken);
try
{
// Merge into whatever is already there: the file is hand-editable and may hold
// keys this version of the settings screen knows nothing about.
var root = await ReadRootAsync(cancellationToken);
var library = Section(root, LibraryOptions.SectionName);
library["Folders"] = new JsonArray([.. settings.Folders.Select(folder => (JsonNode)JsonValue.Create(folder))]);
library["ThumbnailWidth"] = settings.ThumbnailWidth;
library["ThumbnailPositionRatio"] = settings.ThumbnailPositionRatio;
library["MaxIndexingConcurrency"] = settings.MaxIndexingConcurrency;
library["MinimumFileSizeInBytes"] = settings.MinimumFileSizeInBytes;
Section(root, AppearanceOptions.SectionName)["Theme"] = settings.Theme.ToString();
// Write through a temp file so an interrupted save cannot corrupt the settings.
var staging = SettingsFile + ".tmp";
await File.WriteAllTextAsync(staging, root.ToJsonString(WriteOptions), cancellationToken);
File.Move(staging, SettingsFile, overwrite: true);
}
finally
{
_writeLock.Release();
}
}
private static JsonObject Section(JsonObject root, string name)
{
if (root[name] is JsonObject existing)
{
return existing;
}
var created = new JsonObject();
root[name] = created;
return created;
}
private async Task<JsonObject> ReadRootAsync(CancellationToken cancellationToken)
{
if (!File.Exists(SettingsFile))
{
return [];
}
try
{
var json = await File.ReadAllTextAsync(SettingsFile, cancellationToken);
return JsonNode.Parse(json) as JsonObject ?? [];
}
catch (JsonException)
{
// A hand-edited, broken settings file should not stop the app from saving.
return [];
}
}
}
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Options;
using PLib.Application.Library;
using PLib.Desktop.Settings;
using PLib.Infrastructure.Storage;
namespace PLib.Desktop.Services;
/// <inheritdoc cref="IAppSettingsStore"/>
public sealed class JsonAppSettingsStore(
IAppPaths paths,
IOptionsMonitor<LibraryOptions> library,
IOptionsMonitor<AppearanceOptions> appearance,
IOptionsMonitor<PlaybackOptions> playback) : IAppSettingsStore
{
private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true };
private readonly SemaphoreSlim _writeLock = new(1, 1);
private string SettingsFile => Path.Combine(paths.DataDirectory, "settings.json");
public AppSettings Current =>
AppSettings.From(library.CurrentValue, appearance.CurrentValue, playback.CurrentValue);
public async Task SaveAsync(AppSettings settings, CancellationToken cancellationToken = default)
{
await _writeLock.WaitAsync(cancellationToken);
try
{
// Merge into whatever is already there: the file is hand-editable and may hold
// keys this version of the settings screen knows nothing about.
var root = await ReadRootAsync(cancellationToken);
var library = Section(root, LibraryOptions.SectionName);
library["Folders"] = new JsonArray([.. settings.Folders.Select(folder => (JsonNode)JsonValue.Create(folder))]);
library["ThumbnailWidth"] = settings.ThumbnailWidth;
library["ThumbnailPositionRatio"] = settings.ThumbnailPositionRatio;
library["MaxIndexingConcurrency"] = settings.MaxIndexingConcurrency;
library["MinimumFileSizeInBytes"] = settings.MinimumFileSizeInBytes;
Section(root, AppearanceOptions.SectionName)["Theme"] = settings.Theme.ToString();
var playbackSection = Section(root, PlaybackOptions.SectionName);
playbackSection["Volume"] = Math.Round(settings.Volume, 3);
playbackSection["IsMuted"] = settings.IsMuted;
// Write through a temp file so an interrupted save cannot corrupt the settings.
var staging = SettingsFile + ".tmp";
await File.WriteAllTextAsync(staging, root.ToJsonString(WriteOptions), cancellationToken);
File.Move(staging, SettingsFile, overwrite: true);
}
finally
{
_writeLock.Release();
}
}
private static JsonObject Section(JsonObject root, string name)
{
if (root[name] is JsonObject existing)
{
return existing;
}
var created = new JsonObject();
root[name] = created;
return created;
}
private async Task<JsonObject> ReadRootAsync(CancellationToken cancellationToken)
{
if (!File.Exists(SettingsFile))
{
return [];
}
try
{
var json = await File.ReadAllTextAsync(SettingsFile, cancellationToken);
return JsonNode.Parse(json) as JsonObject ?? [];
}
catch (JsonException)
{
// A hand-edited, broken settings file should not stop the app from saving.
return [];
}
}
}