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
18 changes: 12 additions & 6 deletions .github/workflows/dotnet-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ jobs:
publish:
needs: build-test
name: Publish (${{ matrix.runtime }})
runs-on: ubuntu-24.04
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
runtime:
- win-x64
- linux-x64
- osx-arm64
include:
- runtime: win-x64
os: ubuntu-24.04
artifact-path: artifacts/publish/framework-dependent/win-x64
- runtime: linux-x64
os: ubuntu-24.04
artifact-path: artifacts/publish/framework-dependent/linux-x64
- runtime: osx-arm64
os: macos-latest
artifact-path: artifacts/publish/framework-dependent/osx-arm64/*.dmg

steps:
- name: Checkout
Expand All @@ -62,5 +68,5 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: ChapterTool-Avalonia-${{ matrix.runtime }}
path: artifacts/publish/framework-dependent/${{ matrix.runtime }}
path: ${{ matrix.artifact-path }}
if-no-files-found: error
29 changes: 24 additions & 5 deletions scripts/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# ./scripts/publish.sh # win-x64, framework-dependent
# ./scripts/publish.sh -Runtime osx-arm64
# ./scripts/publish.sh -Runtime linux-x64 -SelfContained
# ./scripts/publish.sh -Runtime osx-arm64 -NoRestore -PublishSingleFile
# ./scripts/publish.sh -Runtime osx-arm64 -NoRestore -PublishSingleFile # creates .app and .dmg on macOS
set -euo pipefail

# ---- defaults ----
Expand Down Expand Up @@ -127,9 +127,10 @@ case "$Runtime" in
exit 0
fi

app_name="ChapterTool.Avalonia"
app_bundle="$output/$app_name.app"
app_bundle_tmp="$output/.$app_name.app.tmp"
executable_name="ChapterTool.Avalonia"
app_display_name="ChapterTool"
app_bundle="$output/$app_display_name.app"
app_bundle_tmp="$output/.$app_display_name.app.tmp"
contents_dir="$app_bundle_tmp/Contents"
macos_dir="$contents_dir/MacOS"
resources_dir="$contents_dir/Resources"
Expand Down Expand Up @@ -161,7 +162,7 @@ case "$Runtime" in
printf 'APPL????' > "$contents_dir/PkgInfo"

# Mark the executable so the bundle is double-clickable.
exe_path="$macos_dir/$app_name"
exe_path="$macos_dir/$executable_name"
if [[ ! -f "$exe_path" ]]; then
echo "ERROR: expected executable '$exe_path' not found" >&2
exit 1
Expand All @@ -177,7 +178,25 @@ case "$Runtime" in
# Refresh icon cache so Dock picks up the new art immediately during dev.
touch "$app_bundle"

dmg_staging="$output/.dmg-staging"
dmg_output="$output/$app_display_name-$Runtime.dmg"

rm -rf "$dmg_staging" "$dmg_output"
mkdir -p "$dmg_staging"
cp -R "$app_bundle" "$dmg_staging/"
ln -s /Applications "$dmg_staging/Applications"

hdiutil create \
-volname "$app_display_name" \
-srcfolder "$dmg_staging" \
-ov \
-format UDZO \
"$dmg_output" >/dev/null

rm -rf "$dmg_staging"

echo "Created macOS app bundle at $app_bundle"
echo "Created macOS DMG at $dmg_output"
;;
*)
echo "Published ChapterTool Avalonia to $output"
Expand Down
19 changes: 10 additions & 9 deletions src/ChapterTool.Core/Exporting/ChapterExportService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml.Linq;
using ChapterTool.Core.Diagnostics;
using ChapterTool.Core.Models;
using ChapterTool.Core.Transform;

namespace ChapterTool.Core.Exporting;

public sealed class ChapterExportService
public sealed partial class ChapterExportService
{
private readonly IChapterTimeFormatter timeFormatter;
private readonly ILuaExpressionScriptService luaExpressionService;
Expand Down Expand Up @@ -209,7 +209,7 @@ private static ChapterExportResult Json(ChapterInfo info, ChapterExportOptions o
}

var payload = new JsonPayload(info.SourceType == "MPLS" ? $"{info.SourceName}.m2ts" : null, entries);
return Success(JsonSerializer.Serialize(payload, JsonOptions), ".json");
return Success(JsonSerializer.Serialize(payload, ExportJsonSerializerContext.Default.JsonPayload), ".json");
}

private static ChapterExportResult Lines(string extension, IEnumerable<string> lines) =>
Expand All @@ -236,13 +236,14 @@ private static ChapterExportResult Success(string content, string extension) =>
private static ChapterExportResult Failure(string code, string message) =>
new(false, string.Empty, string.Empty, [new ChapterDiagnostic(DiagnosticSeverity.Error, code, message)]);

private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

private sealed record JsonPayload(string? SourceName, IReadOnlyList<JsonChapter> Chapter);

private sealed record JsonChapter(string Name, double Time);

[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(JsonPayload))]
private sealed partial class ExportJsonSerializerContext : JsonSerializerContext
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
using ChapterTool.Infrastructure.Importing.Media;

namespace ChapterTool.Infrastructure.Configuration;

[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
WriteIndented = true)]
[JsonSerializable(typeof(AppSettings))]
[JsonSerializable(typeof(ThemeColorSettings))]
[JsonSerializable(typeof(FfprobeChapterOutput))]
internal sealed partial class AppJsonSerializerContext : JsonSerializerContext
{
}
116 changes: 12 additions & 104 deletions src/ChapterTool.Infrastructure/Configuration/AppSettingsStore.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using ChapterTool.Core.Services;

namespace ChapterTool.Infrastructure.Configuration;

public sealed partial class AppSettingsStore(string settingsDirectory, IReadOnlyList<string>? legacyDirectories = null)
: ISettingsStore<AppSettings>
public sealed partial class AppSettingsStore(string settingsDirectory) : ISettingsStore<AppSettings>
{
private const string CurrentFileName = "appsettings.json";
private const string LegacyFileName = "chaptertool.json";
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { WriteIndented = true };

private readonly IReadOnlyList<string> legacyDirectories = legacyDirectories ?? [settingsDirectory];
private readonly Lock syncRoot = new();
private AppSettings? cachedSettings;
private SettingsFileState cachedFileState;
private FileStamp cachedFileStamp;

public async ValueTask<AppSettings> LoadAsync(CancellationToken cancellationToken)
{
var currentPath = Path.Combine(settingsDirectory, CurrentFileName);
var fileState = GetFileState(currentPath, legacyDirectories);
var stamp = GetFileStamp(currentPath);

lock (syncRoot)
{
if (cachedSettings is not null && cachedFileState == fileState)
if (cachedSettings is not null && cachedFileStamp == stamp)
{
return cachedSettings;
}
Expand All @@ -35,9 +30,9 @@ public async ValueTask<AppSettings> LoadAsync(CancellationToken cancellationToke
try
{
await using var stream = File.OpenRead(currentPath);
settings = await JsonSerializer.DeserializeAsync<AppSettings>(stream, JsonOptions, cancellationToken)
settings = await JsonSerializer.DeserializeAsync(stream, AppJsonSerializerContext.Default.AppSettings, cancellationToken)
?? new AppSettings();
Cache(settings, fileState);
Cache(settings, stamp);
return settings;
}
catch (JsonException exception)
Expand All @@ -46,24 +41,8 @@ public async ValueTask<AppSettings> LoadAsync(CancellationToken cancellationToke
}
}

foreach (var legacyDirectory in legacyDirectories)
{
var legacyPath = Path.Combine(legacyDirectory, LegacyFileName);
if (!File.Exists(legacyPath))
{
continue;
}

var migrated = await TryLoadLegacyAsync(legacyPath, cancellationToken);
if (migrated is not null)
{
Cache(migrated, fileState);
return migrated;
}
}

settings = new AppSettings();
Cache(settings, fileState);
Cache(settings, stamp);
return settings;
}

Expand All @@ -77,80 +56,32 @@ public async ValueTask SaveAsync(AppSettings settings, CancellationToken cancell
{
await using (var stream = File.Create(tempPath))
{
await JsonSerializer.SerializeAsync(stream, settings, JsonOptions, cancellationToken);
await JsonSerializer.SerializeAsync(stream, settings, AppJsonSerializerContext.Default.AppSettings, cancellationToken);
}

File.Move(tempPath, currentPath, overwrite: true);
Cache(settings, GetFileState(currentPath, legacyDirectories));
Cache(settings, GetFileStamp(currentPath));
}
catch
{
if (File.Exists(tempPath))
{
File.Delete(tempPath);
}
throw;
}
}

private static async ValueTask<AppSettings?> TryLoadLegacyAsync(string path, CancellationToken cancellationToken)
{
try
{
await using var stream = File.OpenRead(path);
var values = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, cancellationToken: cancellationToken);
if (values is null)
{
return null;
}

return new AppSettings(
SavingPath: Get(values, @"Software\ChapterTool.SavingPath"),
Language: Get(values, @"Software\ChapterTool.Language") ?? Get(values, "Language") ?? "",
MainWindowLocation: ParseLocation(
Get(values, @"Software\ChapterTool.Location")
?? Get(values, @"Software\ChapterTool.location")),
MkvToolnixPath: Get(values, @"Software\ChapterTool.mkvToolnixPath") ?? Get(values, "mkvToolnixPath"),
Eac3toPath: Get(values, @"Software\ChapterTool.eac3toPath") ?? Get(values, "eac3toPath"));
}
catch (JsonException)
{
return null;
}
catch (IOException)
{
return null;
throw;
}
}

private static string? Get(IReadOnlyDictionary<string, string> values, string key) =>
values.GetValueOrDefault(key);

private void Cache(AppSettings settings, SettingsFileState fileState)
private void Cache(AppSettings settings, FileStamp stamp)
{
lock (syncRoot)
{
cachedSettings = settings;
cachedFileState = fileState;
cachedFileStamp = stamp;
}
}

private static SettingsFileState GetFileState(string currentPath, IReadOnlyList<string> legacyDirectories)
{
var current = GetFileStamp(currentPath);
FileStamp legacy = default;
foreach (var legacyDirectory in legacyDirectories)
{
legacy = GetFileStamp(Path.Combine(legacyDirectory, LegacyFileName));
if (legacy.Exists)
{
break;
}
}

return new SettingsFileState(current, legacy);
}

private static FileStamp GetFileStamp(string path)
{
var file = new FileInfo(path);
Expand All @@ -159,28 +90,5 @@ private static FileStamp GetFileStamp(string path)
: default;
}

private readonly record struct SettingsFileState(FileStamp Current, FileStamp Legacy);

private readonly record struct FileStamp(bool Exists, DateTime LastWriteTimeUtc, long Length);

private static WindowLocation? ParseLocation(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}

var match = LocationRegex().Match(value);
if (!match.Success)
{
return null;
}

return new WindowLocation(
int.Parse(match.Groups["x"].Value),
int.Parse(match.Groups["y"].Value));
}

[GeneratedRegex(@"\{X=(?<x>-?\d+),Y=(?<y>-?\d+)\}", RegexOptions.CultureInvariant)]
private static partial Regex LocationRegex();
}
12 changes: 0 additions & 12 deletions src/ChapterTool.Infrastructure/Configuration/JsonOptions.cs

This file was deleted.

Loading
Loading