From a12a0f5366069492dd8280972e2b1ea6e8019424 Mon Sep 17 00:00:00 2001
From: SeoJCalive <180237232+SeoJCalive@users.noreply.github.com>
Date: Tue, 3 Nov 2026 03:01:06 +0900
Subject: [PATCH 1/4] feat(settings): remember which format fields are turned
off
AppSettings gets TurnedOffFields, the ids of the fields from
docs/FORMAT-RULES.md rule 2 that the user turned off. An empty list or a
missing property means every field is on, so settings from before it
need no schema step and the schema stays at 58.
UserDataStore.Load tells a new install by settings.json not existing,
and only then turns off every field that can be turned off except
Developer. A settings file that can't be read belongs to an existing
user and keeps every field on; a null list reads as empty. A Filee that
doesn't know the property drops it when it saves its settings, and every
field comes back on; slots are stored separately and stay as they are.
The CLI reads the same settings and still converts to every format.
Part of #806
---
src/Filee.Core/Settings/AppSettings.cs | 11 ++
src/Filee.Core/Settings/UserDataStore.cs | 7 ++
tests/Filee.Cli.Tests/CliTests.cs | 16 +++
tests/Filee.Core.Tests/UserDataStoreTests.cs | 123 +++++++++++++++++++
4 files changed, 157 insertions(+)
diff --git a/src/Filee.Core/Settings/AppSettings.cs b/src/Filee.Core/Settings/AppSettings.cs
index ec949b58..b872f031 100644
--- a/src/Filee.Core/Settings/AppSettings.cs
+++ b/src/Filee.Core/Settings/AppSettings.cs
@@ -276,6 +276,17 @@ public sealed class AppSettings
///
public WindowBounds? MainWindow { get; set; }
+ ///
+ /// The fields turned off on the Fields page (ids of ): a format that belongs only to
+ /// them is left out of the donut and the preset lists. An empty list and a missing value both mean every field is on, so
+ /// settings from before it read as every field on and need no schema step. A new install gets its defaults from
+ /// .
+ ///
+ public List TurnedOffFields { get; set; } = [];
+
+ /// as a set that ignores case, for .
+ public IReadOnlySet TurnedOffFieldSet() => TurnedOffFields.ToHashSet(StringComparer.OrdinalIgnoreCase);
+
///
/// Returns itself, or a copy with the app-wide output settings applied: it saves to
/// when the preset's location is , and keeps the
diff --git a/src/Filee.Core/Settings/UserDataStore.cs b/src/Filee.Core/Settings/UserDataStore.cs
index 2d7d0fc4..8e496c8a 100644
--- a/src/Filee.Core/Settings/UserDataStore.cs
+++ b/src/Filee.Core/Settings/UserDataStore.cs
@@ -3,6 +3,7 @@
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
+using Filee.Core.Formats;
using Filee.Core.History;
using Filee.Core.Presets;
using Filee.Core.Profiles;
@@ -70,7 +71,13 @@ public UserDataStore(string directory, ILogger? log = null)
public void Load()
{
System.IO.Directory.CreateDirectory(Directory);
+ // A new install is told by the file missing, not by Read returning null: a settings file that can't be read is an
+ // existing user's, who keeps every field on. Only a new install gets the fields that are off by default.
+ var fresh = !File.Exists(SettingsPath);
Settings = Read(SettingsPath, FileeJsonContext.Default.AppSettings) ?? new AppSettings();
+ Settings.TurnedOffFields ??= [];
+ if (fresh)
+ Settings.TurnedOffFields = FormatFields.All.Where(f => f.CanTurnOff && !f.OnByDefault).Select(f => f.Id).ToList();
Presets = Read(PresetsPath, FileeJsonContext.Default.ListPreset) ?? BuiltInData.CreatePresets();
Profiles = Read(ProfilesPath, FileeJsonContext.Default.ListToolbarProfile) ?? BuiltInData.CreateProfiles();
History = Read(HistoryPath, FileeJsonContext.Default.ListHistoryEntry) ?? [];
diff --git a/tests/Filee.Cli.Tests/CliTests.cs b/tests/Filee.Cli.Tests/CliTests.cs
index 733228bf..4afe40ac 100644
--- a/tests/Filee.Cli.Tests/CliTests.cs
+++ b/tests/Filee.Cli.Tests/CliTests.cs
@@ -112,6 +112,22 @@ public void Sort_refuses_an_order_it_does_not_know(string option, string? value,
public void Sort_is_refused_by_watch_rather_than_ignored() =>
Assert.Contains("--sort", CliOptions.Parse(["watch", "inbox", "--to", "pdf", "--sort", "name"]).Error);
+ [Fact]
+ public async Task Converts_to_a_format_of_a_turned_off_field()
+ {
+ File.WriteAllText(Path.Combine(_settings, "settings.json"), """{"schemaVersion":58,"turnedOffFields":["video-pro"]}""");
+ var srt = Path.Combine(_folder, "a.srt");
+ File.WriteAllText(srt, "1\n00:00:01,000 --> 00:00:02,000\nHello\n");
+ var (code, output, errors) = await Run("convert", srt, "--to", "ass");
+ Assert.True(code == Cli.Success, output + errors);
+ var result = Path.Combine(_folder, "a.ass");
+ Assert.True(File.Exists(result));
+ Assert.True(new FileInfo(result).Length > 0);
+ (code, output, errors) = await Run("formats", "srt");
+ Assert.True(code == Cli.Success, errors);
+ Assert.Contains("ass", output);
+ }
+
private static readonly ushort[] FitCrcTable =
[0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400];
diff --git a/tests/Filee.Core.Tests/UserDataStoreTests.cs b/tests/Filee.Core.Tests/UserDataStoreTests.cs
index b8210eef..a48b68e1 100644
--- a/tests/Filee.Core.Tests/UserDataStoreTests.cs
+++ b/tests/Filee.Core.Tests/UserDataStoreTests.cs
@@ -1,4 +1,6 @@
using System.Text.Json;
+using System.Text.Json.Nodes;
+using Filee.Core.Formats;
using Filee.Core.Presets;
using Filee.Core.Profiles;
using Filee.Core.Settings;
@@ -7,6 +9,127 @@ namespace Filee.Core.Tests;
public partial class UserDataStoreTests
{
+ [Fact]
+ public void A_new_install_turns_off_every_field_but_developer()
+ {
+ using var dir = new TempDir();
+ var store = new UserDataStore(dir.Path);
+ store.Load();
+ string[] expected = ["cad", "font", "model3d", "geo", "game", "design", "science", "writing", "video-pro", "audio-pro", "legacy"];
+ Assert.Equal(expected, store.Settings.TurnedOffFields);
+ Assert.Equal(FormatFields.All.Where(f => f.CanTurnOff && !f.OnByDefault).Select(f => f.Id), store.Settings.TurnedOffFields);
+ Assert.DoesNotContain("developer", store.Settings.TurnedOffFields);
+ var path = Path.Combine(dir.Path, "settings.json");
+ Assert.False(File.Exists(path));
+ store.SaveSettings();
+ Assert.Equal(expected, JsonNode.Parse(File.ReadAllText(path))!["turnedOffFields"]!.AsArray().Select(x => x!.GetValue()));
+ var again = new UserDataStore(dir.Path);
+ again.Load();
+ Assert.Equal(expected, again.Settings.TurnedOffFields);
+ }
+
+ [Fact]
+ public void Schema_58_settings_start_with_every_field_on_and_keep_the_library()
+ {
+ using var dir = new TempDir();
+ var store = new UserDataStore(dir.Path);
+ store.Load();
+ string[] slots = ["to-jpg", "to-woff2"];
+ store.Profiles.Single(p => p.Id == "images").PresetIds = [.. slots];
+ store.Presets.Add(new Preset { Id = "test-field-custom", TargetFormat = "png" });
+ store.SaveLibrary();
+ Assert.Equal(slots, store.Profiles.Single(p => p.Id == "images").PresetIds);
+ Assert.Equal(slots, StoredImageSlots(dir.Path));
+ File.WriteAllText(Path.Combine(dir.Path, "settings.json"), """{"schemaVersion":58,"language":"ko"}""");
+ var paths = new[] { "settings.json", "presets.json", "profiles.json" }.Select(f => Path.Combine(dir.Path, f)).ToArray();
+ var before = paths.Select(File.ReadAllBytes).ToArray();
+ var again = new UserDataStore(dir.Path);
+ again.Load();
+ Assert.Empty(again.Settings.TurnedOffFields);
+ Assert.Equal("ko", again.Settings.Language);
+ for (var i = 0; i < paths.Length; i++) Assert.Equal(before[i], File.ReadAllBytes(paths[i]));
+ Assert.Equal(slots, again.Profiles.Single(p => p.Id == "images").PresetIds);
+ }
+
+ [Theory]
+ [InlineData("{not json", true)]
+ [InlineData("null", false)]
+ public void A_settings_file_that_cannot_be_read_keeps_every_field_on(string json, bool broken)
+ {
+ using var dir = new TempDir();
+ var path = Path.Combine(dir.Path, "settings.json");
+ File.WriteAllText(path, json);
+ var store = new UserDataStore(dir.Path);
+ store.Load();
+ Assert.Empty(store.Settings.TurnedOffFields);
+ if (broken) Assert.True(File.Exists(path + ".broken"));
+ }
+
+ [Fact]
+ public void Turned_off_fields_written_as_null_read_as_every_field_on()
+ {
+ using var dir = new TempDir();
+ var path = Path.Combine(dir.Path, "settings.json");
+ File.WriteAllText(path, """{"schemaVersion":58,"turnedOffFields":null}""");
+ var store = new UserDataStore(dir.Path);
+ store.Load();
+ Assert.NotNull(store.Settings.TurnedOffFields);
+ Assert.Empty(store.Settings.TurnedOffFields);
+ Assert.False(File.Exists(path + ".broken"));
+ }
+
+ [Fact]
+ public void An_older_Filee_saving_the_settings_brings_every_field_back_on()
+ {
+ using var dir = new TempDir();
+ var store = new UserDataStore(dir.Path);
+ store.Load();
+ Assert.Contains("font", store.Settings.TurnedOffFields);
+ string[] slots = ["to-png", "to-woff2"];
+ store.Profiles.Single(p => p.Id == "images").PresetIds = [.. slots];
+ store.SaveSettings();
+ store.SaveLibrary();
+ Assert.Equal(slots, store.Profiles.Single(p => p.Id == "images").PresetIds);
+ Assert.Equal(slots, StoredImageSlots(dir.Path));
+ var profilesPath = Path.Combine(dir.Path, "profiles.json");
+ var before = File.ReadAllBytes(profilesPath);
+ var path = Path.Combine(dir.Path, "settings.json");
+ var json = JsonNode.Parse(File.ReadAllText(path))!.AsObject();
+ Assert.True(json.Remove("turnedOffFields"));
+ File.WriteAllText(path, json.ToJsonString());
+ var again = new UserDataStore(dir.Path);
+ again.Load();
+ Assert.Empty(again.Settings.TurnedOffFields);
+ Assert.False(File.Exists(path + ".broken"));
+ Assert.Equal(before, File.ReadAllBytes(profilesPath));
+ Assert.Equal(slots, again.Profiles.Single(p => p.Id == "images").PresetIds);
+ }
+
+ [Fact]
+ public void Unknown_field_ids_are_ignored_and_kept()
+ {
+ using var dir = new TempDir();
+ var path = Path.Combine(dir.Path, "settings.json");
+ File.WriteAllText(path, """{"schemaVersion":58,"turnedOffFields":["cad","no-such-field","IMAGE"]}""");
+ var store = new UserDataStore(dir.Path);
+ store.Load();
+ string[] expected = ["cad", "no-such-field", "IMAGE"];
+ Assert.Equal(expected, store.Settings.TurnedOffFields);
+ var off = store.Settings.TurnedOffFieldSet();
+ Assert.False(FormatFields.IsShown("dwg", off));
+ Assert.True(FormatFields.IsShown("png", off));
+ Assert.True(FormatFields.IsShown("glb", off));
+ store.SaveSettings();
+ var again = new UserDataStore(dir.Path);
+ again.Load();
+ Assert.Equal(expected, again.Settings.TurnedOffFields);
+ }
+
+ private static string[] StoredImageSlots(string directory) =>
+ JsonNode.Parse(File.ReadAllText(Path.Combine(directory, "profiles.json")))!.AsArray()
+ .Single(p => p!["id"]!.GetValue() == "images")!["presetIds"]!.AsArray()
+ .Select(p => p!.GetValue()).ToArray();
+
[Theory]
[InlineData(30)]
[InlineData(31)]
From 51024f1e29a730c482f6a3e3a700680b064f720a Mon Sep 17 00:00:00 2001
From: SeoJCalive <180237232+SeoJCalive@users.noreply.github.com>
Date: Tue, 3 Nov 2026 03:01:07 +0900
Subject: [PATCH 2/4] feat(ui): Fields page, and hide the formats of turned-off
fields
A new Settings > Fields page, after Presets, has one switch per field
that can be turned off, each with example formats the switch can hide.
A switch saves at once and leaves field ids this version doesn't know in
the list.
A slot whose target belongs only to turned-off fields is left out of
both faces of the donut, a group's children included. A group left with
no children isn't drawn, and the other slots close up. The fields of
every dropped file stay shown (rule 4), on the mixed donut too. Saved
profiles and presets aren't changed (rule 7).
Available presets on the Donut toolbar page, the preset list with its
ready-made presets, and the preset editor's target list follow the
switches. A preset whose target is already hidden keeps it: the editor
shows it as a dimmed placeholder with a note and saves it unless another
target is picked, instead of silently falling back to the second entry.
The 18 new strings are in all 19 languages. English and Korean are
written by hand; the others are machine translated with the words each
language's UI already uses for the donut, its slices and Available
presets.
Part of #806
---
src/Filee.App/Assets/i18n/ar.json | 19 +
src/Filee.App/Assets/i18n/de.json | 19 +
src/Filee.App/Assets/i18n/en.json | 19 +
src/Filee.App/Assets/i18n/es.json | 19 +
src/Filee.App/Assets/i18n/fr.json | 19 +
src/Filee.App/Assets/i18n/hi.json | 19 +
src/Filee.App/Assets/i18n/id.json | 19 +
src/Filee.App/Assets/i18n/it.json | 18 +
src/Filee.App/Assets/i18n/ja.json | 19 +
src/Filee.App/Assets/i18n/ko.json | 19 +
src/Filee.App/Assets/i18n/ne.json | 19 +
src/Filee.App/Assets/i18n/pl.json | 19 +
src/Filee.App/Assets/i18n/pt-BR.json | 19 +
src/Filee.App/Assets/i18n/pt-PT.json | 19 +
src/Filee.App/Assets/i18n/ru.json | 19 +
src/Filee.App/Assets/i18n/th.json | 19 +
src/Filee.App/Assets/i18n/tr.json | 19 +
src/Filee.App/Assets/i18n/zh-CN.json | 19 +
src/Filee.App/Assets/i18n/zh-TW.json | 19 +
src/Filee.App/Services/WindowService.cs | 2 +-
.../ViewModels/MainWindowViewModel.cs | 1 +
.../ViewModels/Pages/FieldsPageViewModel.cs | 61 +++
.../ViewModels/Pages/PresetsPageViewModel.cs | 9 +-
.../ViewModels/Pages/ToolbarPageViewModel.cs | 14 +-
.../ViewModels/PresetEditorViewModel.cs | 43 +-
src/Filee.App/ViewModels/RadialViewModel.cs | 33 +-
src/Filee.App/Views/Pages/FieldsPage.axaml | 29 ++
src/Filee.App/Views/Pages/FieldsPage.axaml.cs | 8 +
src/Filee.App/Views/PresetEditorView.axaml | 4 +-
tests/Filee.App.Tests/AutomationNameTests.cs | 1 +
tests/Filee.App.Tests/FieldSwitchTests.cs | 416 ++++++++++++++++++
tests/Filee.App.Tests/FieldsPageTests.cs | 213 +++++++++
tests/Filee.App.Tests/LocalizationTests.cs | 25 ++
tests/Filee.App.Tests/TestApp.cs | 3 +
tests/Filee.App.Tests/ToolbarEditorTests.cs | 58 +++
35 files changed, 1263 insertions(+), 17 deletions(-)
create mode 100644 src/Filee.App/ViewModels/Pages/FieldsPageViewModel.cs
create mode 100644 src/Filee.App/Views/Pages/FieldsPage.axaml
create mode 100644 src/Filee.App/Views/Pages/FieldsPage.axaml.cs
create mode 100644 tests/Filee.App.Tests/FieldSwitchTests.cs
create mode 100644 tests/Filee.App.Tests/FieldsPageTests.cs
diff --git a/src/Filee.App/Assets/i18n/ar.json b/src/Filee.App/Assets/i18n/ar.json
index 405cde66..d6aedd85 100644
--- a/src/Filee.App/Assets/i18n/ar.json
+++ b/src/Filee.App/Assets/i18n/ar.json
@@ -1,11 +1,29 @@
{
"app.name": "Filee",
"app.tagline": "اسحب، أفلت، وحوّل.",
+ "fields.title": "المجالات",
+ "fields.description": "أوقف المجالات التي لا تعمل فيها. لن تظهر الصيغ التي تنتمي فقط إلى المجالات المعطلة في قطاعات شريط الأدوات الدائري، ولا في «الإعدادات المسبقة المتاحة» في الإعدادات ← شريط الأدوات الدائري، ولا في القائمة في الإعدادات ← الإعدادات المسبقة، ولا في قائمة «التحويل إلى» في الإعداد المسبق. يظل Filee يتعرف على ملفاتها ويحول إليها من سطر الأوامر، ويظل الملف الذي تسقطه يعرض صيغ مجالاته.",
+ "fields.formats": "الصيغ: {0}",
+ "fields.always_shown": "تظهر دائمًا: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "نماذج ثلاثية الأبعاد",
+ "fields.name.geo": "الخرائط",
+ "fields.name.font": "الخطوط",
+ "fields.name.game": "الألعاب",
+ "fields.name.design": "التصميم والطباعة",
+ "fields.name.science": "العلوم والطب",
+ "fields.name.developer": "التطوير",
+ "fields.name.writing": "الكتابة التقنية والنشر",
+ "fields.name.video-pro": "إنتاج الفيديو والبث",
+ "fields.name.audio-pro": "الصوت الاحترافي",
+ "fields.name.legacy": "الصيغ القديمة",
+
"nav.home": "الرئيسية",
"nav.general": "عام",
"nav.triggers": "الاختصارات",
"nav.toolbar": "شريط الأدوات الدائري",
"nav.presets": "الإعدادات المسبقة",
+ "nav.fields": "المجالات",
"nav.watch": "مراقبة المجلدات",
"nav.theme": "المظهر",
"nav.engines": "المحركات",
@@ -695,6 +713,7 @@
"presets.revert": "تراجع عن التغييرات",
"presets.name": "الاسم",
"presets.target": "التحويل إلى",
+ "presets.target_hidden": "الهدف الحالي ({0}) ينتمي فقط إلى مجالات معطلة، لذا لا يظهر في القائمة. سيبقى الهدف ما لم تختر هدفًا آخر.",
"presets.same_as_source": "صيغة الملف الأصلي نفسها",
"presets.cad_version": "إصدار AutoCAD",
"presets.cad_version.same": "مثل المصدر",
diff --git a/src/Filee.App/Assets/i18n/de.json b/src/Filee.App/Assets/i18n/de.json
index 785c03dd..91d54a7e 100644
--- a/src/Filee.App/Assets/i18n/de.json
+++ b/src/Filee.App/Assets/i18n/de.json
@@ -1,11 +1,29 @@
{
"app.name": "Filee",
"app.tagline": "Ziehen, ablegen, fertig.",
+ "fields.title": "Bereiche",
+ "fields.description": "Schalte Bereiche aus, in denen du nicht arbeitest. Formate, die nur zu ausgeschalteten Bereichen gehören, erscheinen nicht in den Segmenten des Ringmenüs, unter „Verfügbare Vorlagen“ in Einstellungen → Ringmenü, in der Liste unter Einstellungen → Vorlagen und in der Liste „Konvertieren in“ einer Vorlage. Filee erkennt ihre Dateien weiterhin, die Befehlszeile kann weiterhin dorthin konvertieren und abgelegte Dateien bieten weiterhin die Formate ihrer eigenen Bereiche an.",
+ "fields.formats": "Formate: {0}",
+ "fields.always_shown": "Immer angezeigt: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "3D-Modelle",
+ "fields.name.geo": "Karten",
+ "fields.name.font": "Schriftarten",
+ "fields.name.game": "Spiele",
+ "fields.name.design": "Design und Druck",
+ "fields.name.science": "Wissenschaft und Medizin",
+ "fields.name.developer": "Entwicklung",
+ "fields.name.writing": "Technische Dokumentation und Publikation",
+ "fields.name.video-pro": "Videoproduktion und Rundfunk",
+ "fields.name.audio-pro": "Professionelles Audio",
+ "fields.name.legacy": "Alte Formate",
+
"nav.home": "Start",
"nav.general": "Allgemein",
"nav.triggers": "Tastenkürzel",
"nav.toolbar": "Ringmenü",
"nav.presets": "Vorlagen",
+ "nav.fields": "Bereiche",
"nav.watch": "Überwachte Ordner",
"nav.theme": "Design",
"nav.engines": "Module",
@@ -695,6 +713,7 @@
"presets.revert": "Zurücksetzen",
"presets.name": "Name",
"presets.target": "Konvertieren in",
+ "presets.target_hidden": "Das aktuelle Zielformat ({0}) gehört nur zu deaktivierten Bereichen und steht daher nicht in der Liste. Es bleibt ausgewählt, solange du kein anderes wählst.",
"presets.same_as_source": "Gleiches Format wie die Quelldatei",
"presets.cad_version": "AutoCAD-Version",
"presets.cad_version.same": "Wie die Quelldatei",
diff --git a/src/Filee.App/Assets/i18n/en.json b/src/Filee.App/Assets/i18n/en.json
index bf35bdd9..c8d69828 100644
--- a/src/Filee.App/Assets/i18n/en.json
+++ b/src/Filee.App/Assets/i18n/en.json
@@ -2,11 +2,29 @@
"app.name": "Filee",
"app.tagline": "Drag, drop, converted.",
+ "fields.title": "Fields",
+ "fields.description": "Turn off the fields you don't work in. A format that belongs only to fields that are off is left out of the donut's slices, “Available presets” in Settings → Donut toolbar, the list in Settings → Presets and a preset's “Convert to” list. Filee still recognizes its files and the command line still converts to it, and a file you drop still offers the formats of its own fields.",
+ "fields.formats": "Formats: {0}",
+ "fields.always_shown": "Always shown: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "3D models",
+ "fields.name.geo": "Maps",
+ "fields.name.font": "Fonts",
+ "fields.name.game": "Games",
+ "fields.name.design": "Design & print",
+ "fields.name.science": "Science & medical",
+ "fields.name.developer": "Developer",
+ "fields.name.writing": "Technical writing & publishing",
+ "fields.name.video-pro": "Video production & broadcast",
+ "fields.name.audio-pro": "Pro audio",
+ "fields.name.legacy": "Old formats",
+
"nav.home": "Home",
"nav.general": "General",
"nav.triggers": "Shortcuts",
"nav.toolbar": "Donut toolbar",
"nav.presets": "Presets",
+ "nav.fields": "Fields",
"nav.watch": "Watch folders",
"nav.theme": "Theme",
"nav.engines": "Engines",
@@ -710,6 +728,7 @@
"presets.revert": "Revert",
"presets.name": "Name",
"presets.target": "Convert to",
+ "presets.target_hidden": "The current target ({0}) belongs only to fields that are turned off, so it isn't in the list. It stays the target unless you choose another.",
"presets.same_as_source": "Same format as the source",
"presets.cad_version": "AutoCAD version",
"presets.cad_version.same": "Same as the source",
diff --git a/src/Filee.App/Assets/i18n/es.json b/src/Filee.App/Assets/i18n/es.json
index 8c5a6b7d..173045b9 100644
--- a/src/Filee.App/Assets/i18n/es.json
+++ b/src/Filee.App/Assets/i18n/es.json
@@ -1,11 +1,29 @@
{
"app.name": "Filee",
"app.tagline": "Arrastra, suelta y convierte.",
+ "fields.title": "Áreas",
+ "fields.description": "Desactiva las áreas en las que no trabajas. Los formatos que solo pertenecen a áreas desactivadas no aparecen en las porciones del menú circular, en «Ajustes preestablecidos disponibles» de Ajustes → Menú circular, en la lista de Ajustes → Ajustes preestablecidos ni en la lista «Convertir a» de un ajuste preestablecido. Filee sigue reconociendo sus archivos y la línea de comandos sigue convirtiendo a esos formatos; además, los archivos que sueltes seguirán ofreciendo los formatos de sus propias áreas.",
+ "fields.formats": "Formatos: {0}",
+ "fields.always_shown": "Siempre visibles: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "Modelos 3D",
+ "fields.name.geo": "Mapas",
+ "fields.name.font": "Fuentes",
+ "fields.name.game": "Juegos",
+ "fields.name.design": "Diseño e impresión",
+ "fields.name.science": "Ciencia y medicina",
+ "fields.name.developer": "Desarrollo",
+ "fields.name.writing": "Redacción técnica y publicación",
+ "fields.name.video-pro": "Producción de vídeo y radiodifusión",
+ "fields.name.audio-pro": "Audio profesional",
+ "fields.name.legacy": "Formatos antiguos",
+
"nav.home": "Inicio",
"nav.general": "General",
"nav.triggers": "Atajos",
"nav.toolbar": "Menú circular",
"nav.presets": "Ajustes preestablecidos",
+ "nav.fields": "Áreas",
"nav.watch": "Carpetas supervisadas",
"nav.theme": "Tema",
"nav.engines": "Motores",
@@ -695,6 +713,7 @@
"presets.revert": "Revertir",
"presets.name": "Nombre",
"presets.target": "Convertir a",
+ "presets.target_hidden": "El destino actual ({0}) solo pertenece a áreas desactivadas, por lo que no aparece en la lista. Seguirá siendo el destino salvo que elijas otro.",
"presets.same_as_source": "El mismo formato que el original",
"presets.cad_version": "Versión de AutoCAD",
"presets.cad_version.same": "Igual que el original",
diff --git a/src/Filee.App/Assets/i18n/fr.json b/src/Filee.App/Assets/i18n/fr.json
index a1e32fff..71bc0246 100644
--- a/src/Filee.App/Assets/i18n/fr.json
+++ b/src/Filee.App/Assets/i18n/fr.json
@@ -2,11 +2,29 @@
"app.name": "Filee",
"app.tagline": "Glissez, déposez, c'est converti.",
+ "fields.title": "Domaines",
+ "fields.description": "Désactivez les domaines dans lesquels vous ne travaillez pas. Un format appartenant uniquement à des domaines désactivés n'apparaît pas dans les tranches du menu circulaire, dans « Préréglages disponibles » de Paramètres → Menu circulaire, dans la liste de Paramètres → Préréglages ni dans la liste « Convertir en » d'un préréglage. Filee reconnaît toujours ses fichiers et la ligne de commande peut toujours convertir vers ce format ; un fichier déposé propose toujours les formats de ses propres domaines.",
+ "fields.formats": "Formats : {0}",
+ "fields.always_shown": "Toujours affichés : {0}",
+ "fields.name.cad": "CAO",
+ "fields.name.model3d": "Modèles 3D",
+ "fields.name.geo": "Cartes",
+ "fields.name.font": "Polices",
+ "fields.name.game": "Jeux",
+ "fields.name.design": "Design et impression",
+ "fields.name.science": "Sciences et médecine",
+ "fields.name.developer": "Développement",
+ "fields.name.writing": "Rédaction technique et publication",
+ "fields.name.video-pro": "Production vidéo et diffusion",
+ "fields.name.audio-pro": "Audio professionnel",
+ "fields.name.legacy": "Anciens formats",
+
"nav.home": "Accueil",
"nav.general": "Général",
"nav.triggers": "Raccourcis",
"nav.toolbar": "Menu circulaire",
"nav.presets": "Préréglages",
+ "nav.fields": "Domaines",
"nav.watch": "Dossiers surveillés",
"nav.theme": "Thème",
"nav.engines": "Moteurs",
@@ -710,6 +728,7 @@
"presets.revert": "Rétablir",
"presets.name": "Nom",
"presets.target": "Convertir en",
+ "presets.target_hidden": "La cible actuelle ({0}) appartient uniquement à des domaines désactivés et ne figure donc pas dans la liste. Elle reste la cible tant que vous n'en choisissez pas une autre.",
"presets.same_as_source": "Même format que l'original",
"presets.cad_version": "Version d'AutoCAD",
"presets.cad_version.same": "Comme l'original",
diff --git a/src/Filee.App/Assets/i18n/hi.json b/src/Filee.App/Assets/i18n/hi.json
index 72684bad..0fc173b8 100644
--- a/src/Filee.App/Assets/i18n/hi.json
+++ b/src/Filee.App/Assets/i18n/hi.json
@@ -1,11 +1,29 @@
{
"app.name": "Filee",
"app.tagline": "खींचें, छोड़ें, कन्वर्ट हो गया।",
+ "fields.title": "क्षेत्र",
+ "fields.description": "जिन क्षेत्रों में आप काम नहीं करते उन्हें बंद करें। केवल बंद क्षेत्रों से जुड़े फ़ॉर्मैट डोनट के स्लाइस, सेटिंग्स → डोनट टूलबार के “उपलब्ध प्रीसेट”, सेटिंग्स → प्रीसेट की सूची और प्रीसेट की “इसमें कन्वर्ट करें” सूची में नहीं दिखेंगे। Filee उनकी फ़ाइलें पहचानता रहेगा और कमांड लाइन से उन फ़ॉर्मैट में कन्वर्ट कर सकेंगे। ड्रॉप की गई फ़ाइल अपने क्षेत्रों के फ़ॉर्मैट भी दिखाती रहेगी।",
+ "fields.formats": "फ़ॉर्मैट: {0}",
+ "fields.always_shown": "हमेशा दिखने वाले: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "3D मॉडल",
+ "fields.name.geo": "मानचित्र",
+ "fields.name.font": "फ़ॉन्ट",
+ "fields.name.game": "गेम",
+ "fields.name.design": "डिज़ाइन और प्रिंट",
+ "fields.name.science": "विज्ञान और चिकित्सा",
+ "fields.name.developer": "डेवलपर",
+ "fields.name.writing": "तकनीकी लेखन और प्रकाशन",
+ "fields.name.video-pro": "वीडियो निर्माण और प्रसारण",
+ "fields.name.audio-pro": "पेशेवर ऑडियो",
+ "fields.name.legacy": "पुराने फ़ॉर्मैट",
+
"nav.home": "होम",
"nav.general": "सामान्य",
"nav.triggers": "शॉर्टकट",
"nav.toolbar": "डोनट टूलबार",
"nav.presets": "प्रीसेट",
+ "nav.fields": "क्षेत्र",
"nav.watch": "वॉच फ़ोल्डर",
"nav.theme": "थीम",
"nav.engines": "इंजन",
@@ -695,6 +713,7 @@
"presets.revert": "वापस लौटाएँ",
"presets.name": "नाम",
"presets.target": "इसमें कन्वर्ट करें",
+ "presets.target_hidden": "मौजूदा लक्ष्य ({0}) केवल बंद क्षेत्रों से जुड़ा है, इसलिए सूची में नहीं है। दूसरा लक्ष्य चुनने तक यही लक्ष्य रहेगा।",
"presets.same_as_source": "स्रोत जैसा ही फ़ॉर्मैट",
"presets.cad_version": "AutoCAD संस्करण",
"presets.cad_version.same": "स्रोत जैसा",
diff --git a/src/Filee.App/Assets/i18n/id.json b/src/Filee.App/Assets/i18n/id.json
index 08060560..a734e55f 100644
--- a/src/Filee.App/Assets/i18n/id.json
+++ b/src/Filee.App/Assets/i18n/id.json
@@ -1,11 +1,29 @@
{
"app.name": "Filee",
"app.tagline": "Seret, lepas, terkonversi.",
+ "fields.title": "Bidang",
+ "fields.description": "Nonaktifkan bidang yang tidak Anda gunakan. Format yang hanya termasuk dalam bidang nonaktif tidak ditampilkan di irisan donut, “Preset yang tersedia” di Pengaturan → Toolbar donut, daftar di Pengaturan → Preset, dan daftar “Konversi ke” pada preset. Filee tetap mengenali filenya dan baris perintah tetap dapat mengonversi ke format tersebut; file yang Anda jatuhkan juga tetap menawarkan format dari bidangnya sendiri.",
+ "fields.formats": "Format: {0}",
+ "fields.always_shown": "Selalu ditampilkan: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "Model 3D",
+ "fields.name.geo": "Peta",
+ "fields.name.font": "Font",
+ "fields.name.game": "Game",
+ "fields.name.design": "Desain dan cetak",
+ "fields.name.science": "Sains dan medis",
+ "fields.name.developer": "Pengembangan",
+ "fields.name.writing": "Penulisan teknis dan penerbitan",
+ "fields.name.video-pro": "Produksi video dan penyiaran",
+ "fields.name.audio-pro": "Audio profesional",
+ "fields.name.legacy": "Format lama",
+
"nav.home": "Beranda",
"nav.general": "Umum",
"nav.triggers": "Pintasan",
"nav.toolbar": "Toolbar donut",
"nav.presets": "Preset",
+ "nav.fields": "Bidang",
"nav.watch": "Folder pantau",
"nav.theme": "Tema",
"nav.engines": "Engine",
@@ -695,6 +713,7 @@
"presets.revert": "Kembalikan",
"presets.name": "Nama",
"presets.target": "Konversi ke",
+ "presets.target_hidden": "Target saat ini ({0}) hanya termasuk dalam bidang yang dinonaktifkan, sehingga tidak ada dalam daftar. Target ini tetap dipilih kecuali Anda memilih yang lain.",
"presets.same_as_source": "Format sama seperti sumber",
"presets.cad_version": "Versi AutoCAD",
"presets.cad_version.same": "Sama seperti sumber",
diff --git a/src/Filee.App/Assets/i18n/it.json b/src/Filee.App/Assets/i18n/it.json
index b14f3f9c..c0a2d243 100644
--- a/src/Filee.App/Assets/i18n/it.json
+++ b/src/Filee.App/Assets/i18n/it.json
@@ -1,11 +1,28 @@
{
"app.name": "Filee",
"app.tagline": "Trascina, rilascia, converti.",
+"fields.title": "Settori",
+"fields.description": "Disattiva i settori in cui non lavori. I formati che appartengono solo a settori disattivati non compaiono nelle fette del donut, in “Preset disponibili” di Impostazioni → Barra donut, nell'elenco di Impostazioni → Preset né nell'elenco “Converti in” di un preset. Filee continua a riconoscerne i file e la riga di comando può ancora convertire in quei formati; i file trascinati continuano a offrire i formati dei propri settori.",
+"fields.formats": "Formati: {0}",
+"fields.always_shown": "Sempre visibili: {0}",
+"fields.name.cad": "CAD",
+"fields.name.model3d": "Modelli 3D",
+"fields.name.geo": "Mappe",
+"fields.name.font": "Caratteri",
+"fields.name.game": "Giochi",
+"fields.name.design": "Design e stampa",
+"fields.name.science": "Scienza e medicina",
+"fields.name.developer": "Sviluppo",
+"fields.name.writing": "Scrittura tecnica ed editoria",
+"fields.name.video-pro": "Produzione video e trasmissione",
+"fields.name.audio-pro": "Audio professionale",
+"fields.name.legacy": "Vecchi formati",
"nav.home": "Home",
"nav.general": "Generali",
"nav.triggers": "Scorciatoie",
"nav.toolbar": "Barra donut",
"nav.presets": "Preset",
+"nav.fields": "Settori",
"nav.watch": "Cartelle monitorate",
"nav.theme": "Tema",
"nav.engines": "Motori",
@@ -695,6 +712,7 @@
"presets.revert": "Annulla modifiche",
"presets.name": "Nome",
"presets.target": "Converti in",
+"presets.target_hidden": "Il formato di destinazione attuale ({0}) appartiene solo a settori disattivati, quindi non è nell'elenco. Rimane selezionato finché non ne scegli un altro.",
"presets.same_as_source": "Stesso formato dell'origine",
"presets.cad_version": "Versione AutoCAD",
"presets.cad_version.same": "Come l'origine",
diff --git a/src/Filee.App/Assets/i18n/ja.json b/src/Filee.App/Assets/i18n/ja.json
index 4697eea3..f6d185e5 100644
--- a/src/Filee.App/Assets/i18n/ja.json
+++ b/src/Filee.App/Assets/i18n/ja.json
@@ -1,11 +1,29 @@
{
"app.name": "Filee",
"app.tagline": "ドラッグ。ドロップ。\n変換完了。",
+ "fields.title": "分野",
+ "fields.description": "使わない分野をオフにします。オフの分野にのみ属する形式は、ドーナツメニューの項目、「設定」→「ドーナツメニュー」の「利用可能なプリセット」、「設定」→「プリセット」の一覧、プリセットの「変換先」一覧に表示されません。Fileeは引き続きその形式のファイルを認識し、コマンドラインからその形式に変換できます。ドロップしたファイルには、そのファイルの分野の形式が引き続き表示されます。",
+ "fields.formats": "形式:{0}",
+ "fields.always_shown": "常に表示:{0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "3Dモデル",
+ "fields.name.geo": "地図",
+ "fields.name.font": "フォント",
+ "fields.name.game": "ゲーム",
+ "fields.name.design": "デザイン・印刷",
+ "fields.name.science": "科学・医療",
+ "fields.name.developer": "開発",
+ "fields.name.writing": "技術文書・出版",
+ "fields.name.video-pro": "映像制作・放送",
+ "fields.name.audio-pro": "プロオーディオ",
+ "fields.name.legacy": "古い形式",
+
"nav.home": "ホーム",
"nav.general": "一般",
"nav.triggers": "ショートカット",
"nav.toolbar": "ドーナツメニュー",
"nav.presets": "プリセット",
+ "nav.fields": "分野",
"nav.watch": "監視フォルダー",
"nav.theme": "テーマ",
"nav.engines": "変換エンジン",
@@ -695,6 +713,7 @@
"presets.revert": "元に戻す",
"presets.name": "名前",
"presets.target": "変換先",
+ "presets.target_hidden": "現在の変換先({0})はオフの分野にのみ属しているため、一覧に表示されません。別の変換先を選ばない限り、そのまま保持されます。",
"presets.same_as_source": "元ファイルと同じ形式",
"presets.cad_version": "AutoCAD のバージョン",
"presets.cad_version.same": "元のファイルと同じ",
diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json
index 3517a071..cf31ad58 100644
--- a/src/Filee.App/Assets/i18n/ko.json
+++ b/src/Filee.App/Assets/i18n/ko.json
@@ -2,11 +2,29 @@
"app.name": "Filee",
"app.tagline": "끌어서 놓으면, 변환 끝.",
+ "fields.title": "분야",
+ "fields.description": "하지 않는 분야를 끄세요. 꺼진 분야에만 속한 형식은 도넛의 세그먼트, 설정 → 원형 툴바의 “사용 가능한 프리셋”, 설정 → 프리셋의 목록, 프리셋의 “변환 형식” 목록에서 빠집니다. 그 형식의 파일은 그대로 알아보고 명령줄은 그 형식으로도 바꾸며, 놓은 파일은 자기 분야의 형식을 그대로 보여 줍니다.",
+ "fields.formats": "형식: {0}",
+ "fields.always_shown": "항상 보이는 분야: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "3D 모델",
+ "fields.name.geo": "지도",
+ "fields.name.font": "글꼴",
+ "fields.name.game": "게임",
+ "fields.name.design": "디자인·인쇄",
+ "fields.name.science": "과학·의료",
+ "fields.name.developer": "개발",
+ "fields.name.writing": "기술 문서·출판",
+ "fields.name.video-pro": "영상 제작·방송",
+ "fields.name.audio-pro": "전문 오디오",
+ "fields.name.legacy": "옛 형식",
+
"nav.home": "홈",
"nav.general": "일반",
"nav.triggers": "단축키",
"nav.toolbar": "원형 툴바",
"nav.presets": "프리셋",
+ "nav.fields": "분야",
"nav.watch": "자동 변환 폴더",
"nav.theme": "테마",
"nav.engines": "변환 엔진",
@@ -710,6 +728,7 @@
"presets.revert": "되돌리기",
"presets.name": "이름",
"presets.target": "변환 형식",
+ "presets.target_hidden": "현재 대상({0})은 꺼진 분야에만 속해 목록에 없습니다. 다른 대상을 고르지 않으면 그대로 저장됩니다.",
"presets.same_as_source": "원본과 같은 형식",
"presets.cad_version": "AutoCAD 버전",
"presets.cad_version.same": "원본과 같게",
diff --git a/src/Filee.App/Assets/i18n/ne.json b/src/Filee.App/Assets/i18n/ne.json
index 6670e9f2..7cbdc7b4 100644
--- a/src/Filee.App/Assets/i18n/ne.json
+++ b/src/Filee.App/Assets/i18n/ne.json
@@ -1,11 +1,29 @@
{
"app.name": "Filee",
"app.tagline": "तान्नुहोस्, छाड्नुहोस्, रूपान्तरण भयो।",
+ "fields.title": "क्षेत्रहरू",
+ "fields.description": "तपाईंले काम नगर्ने क्षेत्रहरू बन्द गर्नुहोस्। बन्द गरिएका क्षेत्रहरूमा मात्र पर्ने ढाँचाहरू डोनटका स्लाइस, सेटिङहरू → डोनट टुलबारको “उपलब्ध प्रिसेटहरू”, सेटिङहरू → प्रिसेटहरूको सूची र प्रिसेटको “यसमा रूपान्तरण गर्नुहोस्” सूचीमा देखिँदैनन्। Filee ले ती फाइलहरू चिनिरहन्छ र कमान्ड लाइनले ती ढाँचामा रूपान्तरण गरिरहन्छ। छोडिएको फाइलले आफ्नै क्षेत्रका ढाँचाहरू पनि देखाइरहन्छ।",
+ "fields.formats": "ढाँचाहरू: {0}",
+ "fields.always_shown": "सधैं देखिने: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "3D मोडेलहरू",
+ "fields.name.geo": "नक्साहरू",
+ "fields.name.font": "फन्टहरू",
+ "fields.name.game": "खेलहरू",
+ "fields.name.design": "डिजाइन र मुद्रण",
+ "fields.name.science": "विज्ञान र चिकित्सा",
+ "fields.name.developer": "विकास",
+ "fields.name.writing": "प्राविधिक लेखन र प्रकाशन",
+ "fields.name.video-pro": "भिडियो उत्पादन र प्रसारण",
+ "fields.name.audio-pro": "व्यावसायिक अडियो",
+ "fields.name.legacy": "पुराना ढाँचाहरू",
+
"nav.home": "गृहपृष्ठ",
"nav.general": "सामान्य",
"nav.triggers": "सर्टकटहरू",
"nav.toolbar": "डोनट टुलबार",
"nav.presets": "प्रिसेटहरू",
+ "nav.fields": "क्षेत्रहरू",
"nav.watch": "वाच फोल्डरहरू",
"nav.theme": "थिम",
"nav.engines": "इन्जिनहरू",
@@ -695,6 +713,7 @@
"presets.revert": "पहिलेको जस्तै बनाउनुहोस्",
"presets.name": "नाम",
"presets.target": "यसमा रूपान्तरण गर्नुहोस्",
+ "presets.target_hidden": "हालको लक्ष्य ({0}) बन्द गरिएका क्षेत्रहरूमा मात्र पर्छ, त्यसैले सूचीमा छैन। अर्को लक्ष्य नछान्दासम्म यही लक्ष्य रहन्छ।",
"presets.same_as_source": "स्रोत जस्तै उही ढाँचा",
"presets.cad_version": "AutoCAD संस्करण",
"presets.cad_version.same": "स्रोत जस्तै",
diff --git a/src/Filee.App/Assets/i18n/pl.json b/src/Filee.App/Assets/i18n/pl.json
index 080ed62f..a4095104 100644
--- a/src/Filee.App/Assets/i18n/pl.json
+++ b/src/Filee.App/Assets/i18n/pl.json
@@ -2,11 +2,29 @@
"app.name": "Filee",
"app.tagline": "Przeciągnij, upuść, gotowe.",
+ "fields.title": "Dziedziny",
+ "fields.description": "Wyłącz dziedziny, w których nie pracujesz. Formaty należące wyłącznie do wyłączonych dziedzin nie pojawią się w wycinkach pierścienia, w sekcji „Dostępne ustawienia” w Ustawienia → Pasek-pierścień, na liście w Ustawienia → Ustawienia wstępne ani na liście „Konwertuj do” podczas edycji ustawienia. Filee nadal rozpoznaje pliki w tych formatach, a wiersz poleceń nadal może na nie konwertować. Upuszczony plik nadal oferuje formaty swoich dziedzin.",
+ "fields.formats": "Formaty: {0}",
+ "fields.always_shown": "Zawsze widoczne: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "Modele 3D",
+ "fields.name.geo": "Mapy",
+ "fields.name.font": "Czcionki",
+ "fields.name.game": "Gry",
+ "fields.name.design": "Projektowanie i druk",
+ "fields.name.science": "Nauka i medycyna",
+ "fields.name.developer": "Programowanie",
+ "fields.name.writing": "Dokumentacja techniczna i publikacje",
+ "fields.name.video-pro": "Produkcja wideo i nadawanie",
+ "fields.name.audio-pro": "Profesjonalne audio",
+ "fields.name.legacy": "Stare formaty",
+
"nav.home": "Start",
"nav.general": "Ogólne",
"nav.triggers": "Skróty",
"nav.toolbar": "Pasek-pierścień",
"nav.presets": "Ustawienia wstępne",
+ "nav.fields": "Dziedziny",
"nav.watch": "Obserwowane foldery",
"nav.theme": "Motyw",
"nav.engines": "Silniki",
@@ -710,6 +728,7 @@
"presets.revert": "Cofnij",
"presets.name": "Nazwa",
"presets.target": "Konwertuj do",
+ "presets.target_hidden": "Bieżący format docelowy ({0}) należy tylko do wyłączonych dziedzin, dlatego nie ma go na liście. Pozostanie wybrany, dopóki nie wybierzesz innego.",
"presets.same_as_source": "Ten sam format co źródło",
"presets.cad_version": "Wersja AutoCAD",
"presets.cad_version.same": "Jak w oryginale",
diff --git a/src/Filee.App/Assets/i18n/pt-BR.json b/src/Filee.App/Assets/i18n/pt-BR.json
index f16ca02e..83677778 100644
--- a/src/Filee.App/Assets/i18n/pt-BR.json
+++ b/src/Filee.App/Assets/i18n/pt-BR.json
@@ -2,11 +2,29 @@
"app.name": "Filee",
"app.tagline": "Arraste, solte, convertido.",
+ "fields.title": "Áreas",
+ "fields.description": "Desative as áreas com as quais você não trabalha. Formatos que pertencem apenas a áreas desativadas não aparecem nas fatias do menu circular, em “Predefinições disponíveis” de Configurações → Menu circular, na lista de Configurações → Predefinições nem na lista “Converter para” de uma predefinição. O Filee continua reconhecendo esses arquivos e a linha de comando ainda converte para esses formatos. Um arquivo solto continua oferecendo os formatos de suas próprias áreas.",
+ "fields.formats": "Formatos: {0}",
+ "fields.always_shown": "Sempre exibidos: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "Modelos 3D",
+ "fields.name.geo": "Mapas",
+ "fields.name.font": "Fontes",
+ "fields.name.game": "Jogos",
+ "fields.name.design": "Design e impressão",
+ "fields.name.science": "Ciência e medicina",
+ "fields.name.developer": "Desenvolvimento",
+ "fields.name.writing": "Redação técnica e publicação",
+ "fields.name.video-pro": "Produção de vídeo e transmissão",
+ "fields.name.audio-pro": "Áudio profissional",
+ "fields.name.legacy": "Formatos antigos",
+
"nav.home": "Início",
"nav.general": "Geral",
"nav.triggers": "Atalhos",
"nav.toolbar": "Menu circular",
"nav.presets": "Predefinições",
+ "nav.fields": "Áreas",
"nav.watch": "Pastas monitoradas",
"nav.theme": "Tema",
"nav.engines": "Motores",
@@ -710,6 +728,7 @@
"presets.revert": "Desfazer",
"presets.name": "Nome",
"presets.target": "Converter para",
+ "presets.target_hidden": "O destino atual ({0}) pertence apenas a áreas desativadas e, por isso, não aparece na lista. Ele continuará selecionado até você escolher outro.",
"presets.same_as_source": "O mesmo formato da origem",
"presets.cad_version": "Versão do AutoCAD",
"presets.cad_version.same": "Igual ao original",
diff --git a/src/Filee.App/Assets/i18n/pt-PT.json b/src/Filee.App/Assets/i18n/pt-PT.json
index 347589b0..4a3b1b7d 100644
--- a/src/Filee.App/Assets/i18n/pt-PT.json
+++ b/src/Filee.App/Assets/i18n/pt-PT.json
@@ -2,11 +2,29 @@
"app.name": "Filee",
"app.tagline": "Arrasta, larga, convertido.",
+ "fields.title": "Áreas",
+ "fields.description": "Desativa as áreas em que não trabalhas. Os formatos que pertencem apenas a áreas desativadas não aparecem nos segmentos da barra circular, em «Predefinições disponíveis» de Definições → Barra circular, na lista de Definições → Predefinições nem na lista «Converter para» de uma predefinição. O Filee continua a reconhecer os respetivos ficheiros e a linha de comandos continua a converter para esses formatos. Um ficheiro largado continua a apresentar os formatos das suas próprias áreas.",
+ "fields.formats": "Formatos: {0}",
+ "fields.always_shown": "Sempre apresentados: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "Modelos 3D",
+ "fields.name.geo": "Mapas",
+ "fields.name.font": "Tipos de letra",
+ "fields.name.game": "Jogos",
+ "fields.name.design": "Design e impressão",
+ "fields.name.science": "Ciência e medicina",
+ "fields.name.developer": "Desenvolvimento",
+ "fields.name.writing": "Redação técnica e publicação",
+ "fields.name.video-pro": "Produção de vídeo e emissão",
+ "fields.name.audio-pro": "Áudio profissional",
+ "fields.name.legacy": "Formatos antigos",
+
"nav.home": "Página inicial",
"nav.general": "Geral",
"nav.triggers": "Atalhos",
"nav.toolbar": "Barra circular",
"nav.presets": "Predefinições",
+ "nav.fields": "Áreas",
"nav.watch": "Pastas monitorizadas",
"nav.theme": "Tema",
"nav.engines": "Motores",
@@ -710,6 +728,7 @@
"presets.revert": "Reverter",
"presets.name": "Nome",
"presets.target": "Converter para",
+ "presets.target_hidden": "O destino atual ({0}) pertence apenas a áreas desativadas e, por isso, não aparece na lista. Continua selecionado até escolheres outro.",
"presets.same_as_source": "Mesmo formato da origem",
"presets.cad_version": "Versão do AutoCAD",
"presets.cad_version.same": "Igual à origem",
diff --git a/src/Filee.App/Assets/i18n/ru.json b/src/Filee.App/Assets/i18n/ru.json
index f95f60ac..314fa646 100644
--- a/src/Filee.App/Assets/i18n/ru.json
+++ b/src/Filee.App/Assets/i18n/ru.json
@@ -2,11 +2,29 @@
"app.name": "Filee",
"app.tagline": "Перетащил — и готово.",
+ "fields.title": "Области",
+ "fields.description": "Отключите области, с которыми вы не работаете. Форматы, относящиеся только к отключённым областям, не отображаются в секторах кругового меню, в разделе «Доступные пресеты» в «Настройки → Круговое меню», в списке в «Настройки → Пресеты» и в списке «Конвертировать в» у пресета. Filee по-прежнему распознаёт их файлы, а командная строка может конвертировать в эти форматы. Для перетащенного файла по-прежнему предлагаются форматы его собственных областей.",
+ "fields.formats": "Форматы: {0}",
+ "fields.always_shown": "Всегда отображаются: {0}",
+ "fields.name.cad": "САПР",
+ "fields.name.model3d": "3D-модели",
+ "fields.name.geo": "Карты",
+ "fields.name.font": "Шрифты",
+ "fields.name.game": "Игры",
+ "fields.name.design": "Дизайн и печать",
+ "fields.name.science": "Наука и медицина",
+ "fields.name.developer": "Разработка",
+ "fields.name.writing": "Техническая документация и публикация",
+ "fields.name.video-pro": "Видеопроизводство и вещание",
+ "fields.name.audio-pro": "Профессиональное аудио",
+ "fields.name.legacy": "Старые форматы",
+
"nav.home": "Главная",
"nav.general": "Общие",
"nav.triggers": "Горячие клавиши",
"nav.toolbar": "Круговое меню",
"nav.presets": "Пресеты",
+ "nav.fields": "Области",
"nav.watch": "Папки наблюдения",
"nav.theme": "Тема",
"nav.engines": "Движки",
@@ -710,6 +728,7 @@
"presets.revert": "Отменить изменения",
"presets.name": "Название",
"presets.target": "Конвертировать в",
+ "presets.target_hidden": "Текущий формат назначения ({0}) относится только к отключённым областям, поэтому его нет в списке. Он останется выбранным, пока вы не выберете другой.",
"presets.same_as_source": "Тот же формат, что у исходника",
"presets.cad_version": "Версия AutoCAD",
"presets.cad_version.same": "Как в исходном",
diff --git a/src/Filee.App/Assets/i18n/th.json b/src/Filee.App/Assets/i18n/th.json
index 5858e042..de2fe959 100644
--- a/src/Filee.App/Assets/i18n/th.json
+++ b/src/Filee.App/Assets/i18n/th.json
@@ -1,11 +1,29 @@
{
"app.name": "Filee",
"app.tagline": "ลาก วาง แปลงไฟล์ทันที",
+ "fields.title": "สาขา",
+ "fields.description": "ปิดสาขาที่คุณไม่ได้ใช้งาน รูปแบบที่อยู่ในสาขาที่ปิดไว้เท่านั้นจะไม่แสดงในชิ้นส่วนของโดนัท “พรีเซ็ตที่ใช้งานได้” ในการตั้งค่า → แถบโดนัท รายการในการตั้งค่า → พรีเซ็ต และรายการ “แปลงเป็น” ของพรีเซ็ต Filee ยังคงรู้จักไฟล์เหล่านั้นและบรรทัดคำสั่งยังแปลงเป็นรูปแบบดังกล่าวได้ ส่วนไฟล์ที่คุณวางก็ยังแสดงรูปแบบของสาขาที่ไฟล์นั้นอยู่",
+ "fields.formats": "รูปแบบ: {0}",
+ "fields.always_shown": "แสดงเสมอ: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "โมเดล 3D",
+ "fields.name.geo": "แผนที่",
+ "fields.name.font": "แบบอักษร",
+ "fields.name.game": "เกม",
+ "fields.name.design": "ออกแบบและสิ่งพิมพ์",
+ "fields.name.science": "วิทยาศาสตร์และการแพทย์",
+ "fields.name.developer": "การพัฒนา",
+ "fields.name.writing": "เอกสารทางเทคนิคและการเผยแพร่",
+ "fields.name.video-pro": "ผลิตวิดีโอและออกอากาศ",
+ "fields.name.audio-pro": "เสียงระดับมืออาชีพ",
+ "fields.name.legacy": "รูปแบบเก่า",
+
"nav.home": "หน้าแรก",
"nav.general": "ทั่วไป",
"nav.triggers": "ปุ่มลัด",
"nav.toolbar": "แถบโดนัท",
"nav.presets": "พรีเซ็ต",
+ "nav.fields": "สาขา",
"nav.watch": "โฟลเดอร์เฝ้าติดตาม",
"nav.theme": "ธีม",
"nav.engines": "เครื่องมือแปลงไฟล์",
@@ -695,6 +713,7 @@
"presets.revert": "คืนค่าเดิม",
"presets.name": "ชื่อ",
"presets.target": "แปลงเป็น",
+ "presets.target_hidden": "เป้าหมายปัจจุบัน ({0}) อยู่ในสาขาที่ปิดไว้เท่านั้น จึงไม่อยู่ในรายการ เป้าหมายนี้จะยังคงอยู่จนกว่าคุณจะเลือกเป้าหมายอื่น",
"presets.same_as_source": "ฟอร์แมตเดียวกับต้นฉบับ",
"presets.cad_version": "เวอร์ชัน AutoCAD",
"presets.cad_version.same": "เหมือนต้นฉบับ",
diff --git a/src/Filee.App/Assets/i18n/tr.json b/src/Filee.App/Assets/i18n/tr.json
index 00a08c22..c2e95f07 100644
--- a/src/Filee.App/Assets/i18n/tr.json
+++ b/src/Filee.App/Assets/i18n/tr.json
@@ -1,11 +1,29 @@
{
"app.name": "Filee",
"app.tagline": "Sürükle, bırak, dönüştür.",
+ "fields.title": "Alanlar",
+ "fields.description": "Çalışmadığınız alanları kapatın. Yalnızca kapalı alanlara ait biçimler halkanın dilimlerinde, Ayarlar → Halka araç çubuğu bölümündeki “Kullanılabilir ön ayarlar” listesinde, Ayarlar → Ön ayarlar listesinde ve bir ön ayarın “Hedef biçim” listesinde gösterilmez. Filee bu biçimlerdeki dosyaları tanımaya, komut satırı da bu biçimlere dönüştürmeye devam eder. Bıraktığınız dosya kendi alanlarının biçimlerini sunmaya devam eder.",
+ "fields.formats": "Biçimler: {0}",
+ "fields.always_shown": "Her zaman gösterilenler: {0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "3B modeller",
+ "fields.name.geo": "Haritalar",
+ "fields.name.font": "Yazı tipleri",
+ "fields.name.game": "Oyunlar",
+ "fields.name.design": "Tasarım ve baskı",
+ "fields.name.science": "Bilim ve tıp",
+ "fields.name.developer": "Geliştirme",
+ "fields.name.writing": "Teknik yazım ve yayıncılık",
+ "fields.name.video-pro": "Video prodüksiyonu ve yayıncılık",
+ "fields.name.audio-pro": "Profesyonel ses",
+ "fields.name.legacy": "Eski biçimler",
+
"nav.home": "Ana sayfa",
"nav.general": "Genel",
"nav.triggers": "Kısayollar",
"nav.toolbar": "Halka araç çubuğu",
"nav.presets": "Ön ayarlar",
+ "nav.fields": "Alanlar",
"nav.watch": "İzlenen klasörler",
"nav.theme": "Tema",
"nav.engines": "Dönüştürme motorları",
@@ -695,6 +713,7 @@
"presets.revert": "Geri al",
"presets.name": "Ad",
"presets.target": "Hedef biçim",
+ "presets.target_hidden": "Geçerli hedef ({0}) yalnızca kapalı alanlara ait olduğundan listede yer almıyor. Başka bir hedef seçmediğiniz sürece geçerli hedef korunur.",
"presets.same_as_source": "Kaynakla aynı biçim",
"presets.cad_version": "AutoCAD sürümü",
"presets.cad_version.same": "Kaynakla aynı",
diff --git a/src/Filee.App/Assets/i18n/zh-CN.json b/src/Filee.App/Assets/i18n/zh-CN.json
index 917002da..182d478c 100644
--- a/src/Filee.App/Assets/i18n/zh-CN.json
+++ b/src/Filee.App/Assets/i18n/zh-CN.json
@@ -2,11 +2,29 @@
"app.name": "Filee",
"app.tagline": "拖一拖,放一放,转换完成。",
+ "fields.title": "领域",
+ "fields.description": "关闭您不使用的领域。仅属于已关闭领域的格式不会出现在圆环的扇区、“设置 → 环形工具栏”中的“可用预设”、“设置 → 预设”中的列表以及预设的“转换为”列表中。Filee 仍会识别这些格式的文件,命令行仍可转换为这些格式;拖入的文件也仍会提供其所属领域的格式。",
+ "fields.formats": "格式:{0}",
+ "fields.always_shown": "始终显示:{0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "3D 模型",
+ "fields.name.geo": "地图",
+ "fields.name.font": "字体",
+ "fields.name.game": "游戏",
+ "fields.name.design": "设计与印刷",
+ "fields.name.science": "科学与医疗",
+ "fields.name.developer": "开发",
+ "fields.name.writing": "技术写作与出版",
+ "fields.name.video-pro": "视频制作与广播",
+ "fields.name.audio-pro": "专业音频",
+ "fields.name.legacy": "旧格式",
+
"nav.home": "首页",
"nav.general": "常规",
"nav.triggers": "快捷操作",
"nav.toolbar": "环形工具栏",
"nav.presets": "预设",
+ "nav.fields": "领域",
"nav.watch": "自动转换文件夹",
"nav.theme": "主题",
"nav.engines": "转换引擎",
@@ -710,6 +728,7 @@
"presets.revert": "还原",
"presets.name": "名称",
"presets.target": "转换为",
+ "presets.target_hidden": "当前目标({0})仅属于已关闭的领域,因此不在列表中。除非选择其他目标,否则将保留当前目标。",
"presets.same_as_source": "与源文件相同的格式",
"presets.cad_version": "AutoCAD 版本",
"presets.cad_version.same": "与源文件相同",
diff --git a/src/Filee.App/Assets/i18n/zh-TW.json b/src/Filee.App/Assets/i18n/zh-TW.json
index d3e20007..255894b1 100644
--- a/src/Filee.App/Assets/i18n/zh-TW.json
+++ b/src/Filee.App/Assets/i18n/zh-TW.json
@@ -2,11 +2,29 @@
"app.name": "Filee",
"app.tagline": "拖一拖,放一放,轉換完成。",
+ "fields.title": "領域",
+ "fields.description": "關閉您不使用的領域。僅屬於已關閉領域的格式不會出現在圓環的扇形、「設定 → 環形工具列」中的「可用預設」、「設定 → 預設」中的清單以及預設的「轉換為」清單中。Filee 仍會辨識這些格式的檔案,命令列仍可轉換為這些格式;拖入的檔案也仍會提供其所屬領域的格式。",
+ "fields.formats": "格式:{0}",
+ "fields.always_shown": "一律顯示:{0}",
+ "fields.name.cad": "CAD",
+ "fields.name.model3d": "3D 模型",
+ "fields.name.geo": "地圖",
+ "fields.name.font": "字型",
+ "fields.name.game": "遊戲",
+ "fields.name.design": "設計與印刷",
+ "fields.name.science": "科學與醫療",
+ "fields.name.developer": "開發",
+ "fields.name.writing": "技術寫作與出版",
+ "fields.name.video-pro": "影片製作與廣播",
+ "fields.name.audio-pro": "專業音訊",
+ "fields.name.legacy": "舊格式",
+
"nav.home": "首頁",
"nav.general": "一般",
"nav.triggers": "觸發方式",
"nav.toolbar": "環形工具列",
"nav.presets": "預設",
+ "nav.fields": "領域",
"nav.watch": "自動轉換資料夾",
"nav.theme": "主題",
"nav.engines": "轉換引擎",
@@ -710,6 +728,7 @@
"presets.revert": "還原",
"presets.name": "名稱",
"presets.target": "轉換為",
+ "presets.target_hidden": "目前的目標({0})僅屬於已關閉的領域,因此不在清單中。除非選擇其他目標,否則會保留目前的目標。",
"presets.same_as_source": "與來源檔案相同的格式",
"presets.cad_version": "AutoCAD 版本",
"presets.cad_version.same": "與來源檔案相同",
diff --git a/src/Filee.App/Services/WindowService.cs b/src/Filee.App/Services/WindowService.cs
index b530afce..7dcfe4f7 100644
--- a/src/Filee.App/Services/WindowService.cs
+++ b/src/Filee.App/Services/WindowService.cs
@@ -653,7 +653,7 @@ public async Task EditPresetAsync(Preset preset, Window? owner)
return await EditGroupAsync(preset, owner);
var dialog = new PresetEditorWindow
{
- DataContext = new PresetEditorViewModel(preset, loc),
+ DataContext = new PresetEditorViewModel(preset, loc, services.GetRequiredService().Settings.TurnedOffFieldSet()),
Topmost = owner?.Topmost ?? false,
};
PrepareWindow(dialog);
diff --git a/src/Filee.App/ViewModels/MainWindowViewModel.cs b/src/Filee.App/ViewModels/MainWindowViewModel.cs
index 9529b97e..7c740723 100644
--- a/src/Filee.App/ViewModels/MainWindowViewModel.cs
+++ b/src/Filee.App/ViewModels/MainWindowViewModel.cs
@@ -73,6 +73,7 @@ public MainWindowViewModel(IServiceProvider services, ILocalizer loc, UpdateServ
new("triggers", "nav.triggers", "Icon.Keyboard", typeof(TriggersPageViewModel)),
new("toolbar", "nav.toolbar", "Icon.Donut", typeof(ToolbarPageViewModel)),
new("presets", "nav.presets", "Icon.Tune", typeof(PresetsPageViewModel)),
+ new("fields", "nav.fields", "Icon.Shapes", typeof(FieldsPageViewModel)),
new("watch", "nav.watch", "Icon.Folder", typeof(WatchFoldersPageViewModel)),
new("theme", "nav.theme", "Icon.Palette", typeof(ThemePageViewModel)),
new("engines", "nav.engines", "Icon.Engine", typeof(EnginesPageViewModel)),
diff --git a/src/Filee.App/ViewModels/Pages/FieldsPageViewModel.cs b/src/Filee.App/ViewModels/Pages/FieldsPageViewModel.cs
new file mode 100644
index 00000000..f7b24417
--- /dev/null
+++ b/src/Filee.App/ViewModels/Pages/FieldsPageViewModel.cs
@@ -0,0 +1,61 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Filee.Core.Formats;
+using Filee.Core.Localization;
+using Filee.Core.Settings;
+
+namespace Filee.App.ViewModels.Pages;
+
+/// Settings → Fields: one switch per field that can be turned off (docs/FORMAT-RULES.md, #806).
+public sealed class FieldsPageViewModel : ObservableObject
+{
+ public FieldsPageViewModel(UserDataStore store, ILocalizer loc)
+ {
+ Fields = FormatFields.All.Where(f => f.CanTurnOff)
+ .Select(f => new FieldSwitchViewModel(f, store, loc)).ToList();
+ // The fields that can't be turned off are the categories, so their names are the category names.
+ AlwaysShown = loc.Format("fields.always_shown", string.Join(", ", FormatFields.All.Where(f => !f.CanTurnOff)
+ .Select(f => loc[$"format.category.{Enum.Parse(f.Id, true)}"])));
+ }
+
+ public IReadOnlyList Fields { get; }
+ public string AlwaysShown { get; }
+}
+
+/// One field's switch: on removes its id from , off adds it, and either saves.
+public sealed partial class FieldSwitchViewModel : ObservableObject
+{
+ private readonly UserDataStore _store;
+
+ public FieldSwitchViewModel(FormatField field, UserDataStore store, ILocalizer loc)
+ {
+ _store = store;
+ Id = field.Id;
+ Name = loc[$"fields.name.{Id}"];
+ // Only formats this switch can hide: one that is also in a field that can't be turned off (TGA in Image, DXF in
+ // Vector) stays whatever the switches say.
+ var formats = FormatRegistry.Known
+ .Where(f => f.Writable && FormatFields.Of(f).Contains(field) && FormatFields.Of(f).All(x => x.CanTurnOff))
+ .Select(f => f.DisplayName).ToList();
+ Formats = formats.Count == 0 ? "" : loc.Format("fields.formats",
+ string.Join(", ", formats.Take(6)) + (formats.Count > 6 ? "\u2026" : ""));
+ // Set the field, not the property: reading the saved list must not save it.
+ _isOn = !store.Settings.TurnedOffFields.Contains(Id, StringComparer.OrdinalIgnoreCase);
+ }
+
+ public string Id { get; }
+ public string Name { get; }
+ public string Formats { get; }
+
+ [ObservableProperty] private bool _isOn;
+
+ // Only this field's id is added or removed: ids this version doesn't know (a newer Filee's fields) stay in the list.
+ partial void OnIsOnChanged(bool value)
+ {
+ var off = _store.Settings.TurnedOffFields;
+ if (value)
+ off.RemoveAll(id => string.Equals(id, Id, StringComparison.OrdinalIgnoreCase));
+ else if (!off.Contains(Id, StringComparer.OrdinalIgnoreCase))
+ off.Add(Id);
+ _store.SaveSettings();
+ }
+}
diff --git a/src/Filee.App/ViewModels/Pages/PresetsPageViewModel.cs b/src/Filee.App/ViewModels/Pages/PresetsPageViewModel.cs
index 9b3c4b44..e8e92df8 100644
--- a/src/Filee.App/ViewModels/Pages/PresetsPageViewModel.cs
+++ b/src/Filee.App/ViewModels/Pages/PresetsPageViewModel.cs
@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Filee.App.Services;
+using Filee.Core.Formats;
using Filee.Core.Localization;
using Filee.Core.Presets;
using Filee.Core.Settings;
@@ -43,11 +44,13 @@ public PresetsPageViewModel(UserDataStore store, ILocalizer loc, WindowService w
_loc = loc;
_windows = windows;
VisiblePresets = new FilteredList(Presets, p => [p.Name, p.Target, p.Preset.TargetFormat]);
+ var off = store.Settings.TurnedOffFieldSet();
// Groups and tools convert nothing: they live on the donut only (Settings → Toolbar).
- foreach (var preset in store.Presets.Where(p => !p.IsGroup && !p.IsTool))
+ foreach (var preset in store.Presets.Where(p => !p.IsGroup && !p.IsTool && FormatFields.IsShown(p.TargetFormat, off)))
Presets.Add(new PresetListItem(preset, loc));
SelectedPreset = Presets.FirstOrDefault();
- PlacePresets = [.. BuiltInData.PlacePresets().Select(p => new PresetListItem(p, loc))];
+ PlacePresets = [.. BuiltInData.PlacePresets().Where(p => FormatFields.IsShown(p.TargetFormat, off))
+ .Select(p => new PresetListItem(p, loc))];
}
public ObservableCollection Presets { get; } = [];
@@ -65,7 +68,7 @@ public PresetsPageViewModel(UserDataStore store, ILocalizer loc, WindowService w
partial void OnSelectedPresetChanged(PresetListItem? value)
{
- Editor = value is null ? null : new PresetEditorViewModel(value.Preset, _loc);
+ Editor = value is null ? null : new PresetEditorViewModel(value.Preset, _loc, _store.Settings.TurnedOffFieldSet());
SavedMessage = null;
}
diff --git a/src/Filee.App/ViewModels/Pages/ToolbarPageViewModel.cs b/src/Filee.App/ViewModels/Pages/ToolbarPageViewModel.cs
index 0ae6e98e..63258378 100644
--- a/src/Filee.App/ViewModels/Pages/ToolbarPageViewModel.cs
+++ b/src/Filee.App/ViewModels/Pages/ToolbarPageViewModel.cs
@@ -726,7 +726,7 @@ private void Rebuild()
}
var candidates = new List<(PaletteChip Chip, PaletteTab Tab, bool Group)>();
// Groups first: few, and otherwise at the very end, below every conversion.
- foreach (var preset in _store.Presets.Where(p => !placed.Contains(p.Id)).OrderBy(p => !p.IsGroup))
+ foreach (var preset in ShownPresets().Where(p => !placed.Contains(p.Id)).OrderBy(p => !p.IsGroup))
{
string? reason = null;
var use = PresetUse.Usable;
@@ -816,13 +816,19 @@ private void Rebuild()
OnPropertyChanged(nameof(BackCount));
}
+ private IEnumerable ShownPresets()
+ {
+ var off = _store.Settings.TurnedOffFieldSet();
+ return _store.Presets.Where(p => FormatFields.IsShown(p.TargetFormat, off));
+ }
+
private void RefreshVisiblePalette()
{
var query = PaletteSearch.Trim();
var placed = Profile is null ? [] : Profile.PresetIds.Concat(Profile.BackPresetIds ?? []).ToHashSet();
_judgingByProfile.TryGetValue(Profile?.Id ?? "", out var judging);
var candidates = new List<(PaletteChip Chip, PaletteTab Tab, bool Group)>();
- foreach (var preset in _store.Presets.Where(p => !placed.Contains(p.Id)).OrderBy(p => !p.IsGroup))
+ foreach (var preset in ShownPresets().Where(p => !placed.Contains(p.Id)).OrderBy(p => !p.IsGroup))
{
var judgement = judging?.Judged.GetValueOrDefault(preset.Id) ?? new(PresetUse.Usable, null);
if (Profile is { Extensions.Count: > 0 } && !preset.IsGroup && !HasJudgement(Profile.Id, preset.Id))
@@ -860,7 +866,7 @@ private void JudgeSome(object pass, string profileId)
if (!_judgingByProfile.TryGetValue(profileId, out var judging))
_judgingByProfile[profileId] = judging = new PaletteJudging();
var placed = profile.PresetIds.Concat(profile.BackPresetIds ?? []).ToHashSet();
- var pending = _store.Presets.Where(p => !p.IsGroup && !placed.Contains(p.Id) && !judging.Judged.ContainsKey(p.Id)).ToList();
+ var pending = ShownPresets().Where(p => !p.IsGroup && !placed.Contains(p.Id) && !judging.Judged.ContainsKey(p.Id)).ToList();
if (pending.Count > 0)
{
judging.Session ??= _availability.ForPalette(profile.Extensions.Select(FormatRegistry.FindByExtension)
@@ -874,7 +880,7 @@ private void JudgeSome(object pass, string profileId)
break;
}
}
- if (_store.Presets.Any(p => !p.IsGroup && !placed.Contains(p.Id) && !judging.Judged.ContainsKey(p.Id)))
+ if (ShownPresets().Any(p => !p.IsGroup && !placed.Contains(p.Id) && !judging.Judged.ContainsKey(p.Id)))
Dispatcher.UIThread.Post(() => JudgeSome(pass, profileId), DispatcherPriority.Background);
else
{
diff --git a/src/Filee.App/ViewModels/PresetEditorViewModel.cs b/src/Filee.App/ViewModels/PresetEditorViewModel.cs
index 7f4aae8b..32eb041a 100644
--- a/src/Filee.App/ViewModels/PresetEditorViewModel.cs
+++ b/src/Filee.App/ViewModels/PresetEditorViewModel.cs
@@ -79,7 +79,7 @@ public sealed partial class PresetEditorViewModel : ObservableObject
private readonly Preset _edit;
private readonly ILocalizer _loc;
- public PresetEditorViewModel(Preset preset, ILocalizer loc)
+ public PresetEditorViewModel(Preset preset, ILocalizer loc, IReadOnlySet? turnedOff = null)
{
_original = preset;
_edit = preset.Clone();
@@ -91,7 +91,7 @@ public PresetEditorViewModel(Preset preset, ILocalizer loc)
[
new Choice(BuiltInData.SameAsSource, loc["presets.same_as_source"]),
.. FormatRegistry.Known
- .Where(f => f.Writable)
+ .Where(f => f.Writable && (turnedOff is null || FormatFields.IsShown(f.Id, turnedOff)))
.Select(f => new Choice(
f.Id,
f.Id == FormatRegistry.Folder ? loc["presets.target_folder"] : f.DisplayName,
@@ -151,7 +151,15 @@ .. WhisperLanguages.All
];
_name = string.IsNullOrWhiteSpace(_edit.Name) ? loc.DisplayName(_edit) : _edit.Name;
- _target = Targets.FirstOrDefault(t => t.Value == _edit.TargetFormat) ?? Targets[1];
+ if (FormatRegistry.FindById(_edit.TargetFormat) is { Writable: true } hidden
+ && turnedOff is not null && !FormatFields.IsShown(hidden.Id, turnedOff))
+ {
+ _target = new Choice(hidden.Id, hidden.DisplayName, FormatLabels.Category(loc, hidden.Category));
+ _hiddenTarget = hidden.DisplayName;
+ _hiddenTargetNote = loc.Format("presets.target_hidden", hidden.DisplayName);
+ }
+ else
+ _target = Targets.FirstOrDefault(t => t.Value == _edit.TargetFormat) ?? Targets[1];
_resize = ResizeModes.First(r => r.Value == (_edit.Image.CustomSize is not null ? ResizeMode.Custom
: _edit.Image.ClampedMinShortEdge is not null ? ResizeMode.MinShortEdge
: _edit.Image.Resize));
@@ -383,6 +391,29 @@ public async Task TestTranslationAsync()
nameof(ShowImageSize), nameof(SizeRangeError), nameof(IsValid))]
private Choice _target;
+ // A target whose fields are all turned off isn't added to Targets (the list follows the switches), but it stays the
+ // target: the box shows its name as a placeholder with a note, and Apply saves it unless another one is picked.
+ [ObservableProperty] private string? _hiddenTarget;
+ [ObservableProperty] private string? _hiddenTargetNote;
+
+ /// The target box has no selection while the current target is hidden by its field switch.
+ public Choice? TargetSelection
+ {
+ get => HiddenTarget is null ? Target : null;
+ set
+ {
+ if (value is null)
+ {
+ if (HiddenTarget is null)
+ OnPropertyChanged(nameof(TargetSelection));
+ return;
+ }
+ HiddenTarget = null;
+ HiddenTargetNote = null;
+ Target = value;
+ }
+ }
+
private LivePhotoKind _livePhotoValue;
/// What a video becomes for a JPG or HEIC target: one still frame, or a Live / Motion Photo it can be written as.
@@ -734,7 +765,11 @@ public Choice CopyAction
[ObservableProperty] private Choice _animationSpeed;
// A combo box that loses its selection sets null: every choice keeps its value (see Choices.Restore).
- partial void OnTargetChanged(Choice? oldValue, Choice newValue) => Choices.Restore(oldValue, newValue, v => Target = v);
+ partial void OnTargetChanged(Choice? oldValue, Choice newValue)
+ {
+ Choices.Restore(oldValue, newValue, v => Target = v);
+ OnPropertyChanged(nameof(TargetSelection));
+ }
partial void OnVideoSizeUnitChoiceChanged(Choice? oldValue, Choice newValue)
{
diff --git a/src/Filee.App/ViewModels/RadialViewModel.cs b/src/Filee.App/ViewModels/RadialViewModel.cs
index 85df52da..2968d3f0 100644
--- a/src/Filee.App/ViewModels/RadialViewModel.cs
+++ b/src/Filee.App/ViewModels/RadialViewModel.cs
@@ -91,6 +91,9 @@ private sealed record Face(IReadOnlyList Presets, IReadOnlyList _backIds = [];
private IReadOnlyList _formats = [];
+ // The turned-off fields the last Load hid, already without the dropped files' own fields, so the back face is
+ // filtered with the same set as the front.
+ private IReadOnlySet _off = new HashSet(StringComparer.OrdinalIgnoreCase);
private Face? _back;
private int _loads;
@@ -132,6 +135,7 @@ public void ResetForDrag()
ReserveSubRing = store.Profiles.Any(profile => HasGroups(profile.PresetIds.Concat(profile.BackPresetIds ?? [])));
_loads++;
(_front, _back, _backIds, _formats) = (Face.Empty, Face.Empty, [], []);
+ _off = new HashSet(StringComparer.OrdinalIgnoreCase);
ShowFace(back: false);
CenterTitle = loc["donut.center_drag"];
CenterSubtitle = null;
@@ -156,9 +160,15 @@ public void Load(IReadOnlyList files, bool clickMode, bool backFace = fa
var counted = CompanionFiles.WithoutCompanions(files);
var profile = ProfileSelector.Select(store.Profiles, counted);
var formats = ProfileSelector.DetectFormats(counted);
+ // A turned-off field still shows the formats of the dropped files' own fields (FORMAT-RULES rule 4): a font file
+ // offers font conversions with Fonts off. The front takes this local, not _off, which holds the last load's set
+ // until the line after it.
+ var off = store.Settings.TurnedOffFields
+ .Except(formats.SelectMany(FormatFields.Of).Select(field => field.Id), StringComparer.OrdinalIgnoreCase)
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
// Both faces follow the same rule: a slice that can't convert these files is greyed out and says why.
- _front = BuildFace(profile?.PresetIds ?? [], formats);
- (_back, _backIds, _formats) = (null, [.. profile?.BackPresetIds ?? []], formats);
+ _front = BuildFace(profile?.PresetIds ?? [], formats, off);
+ (_back, _backIds, _formats, _off) = (null, [.. profile?.BackPresetIds ?? []], formats, off);
var load = ++_loads;
if (backFace)
{
@@ -218,15 +228,16 @@ private void ShowFace(bool back, bool checkBack = true)
}
/// The back face's slices for the files, checked now if they weren't yet.
- private Face BackFace() => _back ??= BuildFace(_backIds, _formats);
+ private Face BackFace() => _back ??= BuildFace(_backIds, _formats, _off);
private bool HasGroups(IEnumerable presetIds) => presetIds.Any(id => store.FindPreset(id)?.IsGroup == true);
- private Face BuildFace(IEnumerable presetIds, IReadOnlyList formats)
+ private Face BuildFace(IEnumerable presetIds, IReadOnlyList formats, IReadOnlySet off)
{
var presets = presetIds
.Select(store.FindPreset)
.OfType()
+ .Where(p => FormatFields.IsShown(p.TargetFormat, off))
.ToList();
var subPresets = presets
@@ -234,6 +245,20 @@ private Face BuildFace(IEnumerable presetIds, IReadOnlyList
.Select(store.FindPreset).OfType().Where(c => !c.IsGroup).Take(Preset.MaxSubPresets).ToList())
.ToList();
+ // Children are filtered after Take, so a hidden child doesn't let in one the group editor doesn't show. A group
+ // whose children were all hidden leaves the donut like a hidden slice; one that was empty already stays, greyed
+ // out as before. Both lists lose the same index: Items reads a group's children by its position.
+ for (var i = presets.Count - 1; i >= 0; i--)
+ {
+ var children = subPresets[i];
+ subPresets[i] = children.Where(p => FormatFields.IsShown(p.TargetFormat, off)).ToList();
+ if (presets[i].IsGroup && children.Count > 0 && subPresets[i].Count == 0)
+ {
+ presets.RemoveAt(i);
+ subPresets.RemoveAt(i);
+ }
+ }
+
DonutItem ItemOf(Preset p, bool caption = true)
{
var reason = availability.Check(p, formats);
diff --git a/src/Filee.App/Views/Pages/FieldsPage.axaml b/src/Filee.App/Views/Pages/FieldsPage.axaml
new file mode 100644
index 00000000..bf197f9f
--- /dev/null
+++ b/src/Filee.App/Views/Pages/FieldsPage.axaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Filee.App/Views/Pages/FieldsPage.axaml.cs b/src/Filee.App/Views/Pages/FieldsPage.axaml.cs
new file mode 100644
index 00000000..5e7ee226
--- /dev/null
+++ b/src/Filee.App/Views/Pages/FieldsPage.axaml.cs
@@ -0,0 +1,8 @@
+using Avalonia.Controls;
+
+namespace Filee.App.Views.Pages;
+
+public partial class FieldsPage : UserControl
+{
+ public FieldsPage() => InitializeComponent();
+}
diff --git a/src/Filee.App/Views/PresetEditorView.axaml b/src/Filee.App/Views/PresetEditorView.axaml
index 4cccec7b..18e823e6 100644
--- a/src/Filee.App/Views/PresetEditorView.axaml
+++ b/src/Filee.App/Views/PresetEditorView.axaml
@@ -38,7 +38,8 @@
-
@@ -50,6 +51,7 @@
+
diff --git a/tests/Filee.App.Tests/AutomationNameTests.cs b/tests/Filee.App.Tests/AutomationNameTests.cs
index 894af70b..9eadd6c3 100644
--- a/tests/Filee.App.Tests/AutomationNameTests.cs
+++ b/tests/Filee.App.Tests/AutomationNameTests.cs
@@ -24,6 +24,7 @@ public class AutomationNameTests
[InlineData("triggers")]
[InlineData("toolbar")]
[InlineData("presets")]
+ [InlineData("fields")]
[InlineData("theme")]
[InlineData("engines")]
[InlineData("about")]
diff --git a/tests/Filee.App.Tests/FieldSwitchTests.cs b/tests/Filee.App.Tests/FieldSwitchTests.cs
new file mode 100644
index 00000000..a5afeac0
--- /dev/null
+++ b/tests/Filee.App.Tests/FieldSwitchTests.cs
@@ -0,0 +1,416 @@
+using Avalonia.Controls;
+using Avalonia.Headless.XUnit;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+using Filee.App.Controls;
+using Filee.App.Services;
+using Filee.App.ViewModels;
+using Filee.App.ViewModels.Pages;
+using Filee.App.Views;
+using Filee.Core.Formats;
+using Filee.Core.Presets;
+using Filee.Core.Profiles;
+using Filee.Core.Settings;
+
+namespace Filee.App.Tests;
+
+[Collection("Motion")]
+public class FieldSwitchTests
+{
+ private sealed class Fixture : IDisposable
+ {
+ public UserDataStore Store { get; }
+ public LocalizationService Loc { get; }
+ public RadialViewModel Vm { get; }
+ public RadialWindow Window { get; }
+ private readonly List _off;
+ private readonly List _presets;
+ private readonly List<(ToolbarProfile Profile, List Front, List? Back)> _faces;
+
+ public Fixture()
+ {
+ TestServices.EnsureInitialized("en");
+ Store = AppHost.Get();
+ Loc = AppHost.Get();
+ _off = [.. Store.Settings.TurnedOffFields];
+ _presets = [.. Store.Presets];
+ _faces = Store.Profiles.Select(p => (p, p.PresetIds.ToList(), p.BackPresetIds?.ToList())).ToList();
+ Store.Settings.TurnedOffFields = [];
+ Vm = new RadialViewModel(Store, Loc, AppHost.Get());
+ Window = new RadialWindow { DataContext = Vm };
+ Window.Show();
+ }
+
+ public ToolbarProfile Profile(params string[] files) => Assert.IsType(ProfileSelector.Select(Store.Profiles, files));
+
+ public Preset Add(string format)
+ {
+ var preset = new Preset { Id = $"test-field-{format}", Name = format, TargetFormat = format };
+ Store.Presets.Add(preset);
+ return preset;
+ }
+
+ public Preset Group(string name, params string[] children)
+ {
+ var preset = new Preset { Id = $"test-field-{name}", Name = name, Kind = PresetKind.Group, SubPresetIds = [.. children] };
+ Store.Presets.Add(preset);
+ return preset;
+ }
+
+ public void Load(params string[] files)
+ {
+ Vm.Load(files, clickMode: true);
+ Dispatcher.UIThread.RunJobs();
+ Assert.Same(Vm.Items, Window.DonutControl.Items);
+ }
+
+ public void Face(IEnumerable ids)
+ {
+ Dispatcher.UIThread.RunJobs();
+ var expected = ids.ToArray();
+ Assert.Equal(expected, Vm.Presets.Select(p => p.Id));
+ Assert.Equal(expected, Vm.Items.Select(p => p.Id));
+ Assert.Equal(expected, Window.DonutControl.Items!.Select(p => p.Id));
+ }
+
+ public void Dispose()
+ {
+ Vm.ResetForDrag();
+ Dispatcher.UIThread.RunJobs();
+ Window.Close();
+ Store.Settings.TurnedOffFields = _off;
+ Store.Presets.Clear();
+ Store.Presets.AddRange(_presets);
+ foreach (var (profile, front, back) in _faces)
+ (profile.PresetIds, profile.BackPresetIds) = (front, back);
+ }
+ }
+
+ [AvaloniaFact]
+ public void A_default_slot_of_a_turned_off_field_leaves_the_donut_and_comes_back()
+ {
+ using var f = new Fixture();
+ var candidates =
+ from profile in f.Store.Profiles
+ where !profile.IsFallback && profile.Extensions.Count > 0
+ let file = $"/d/a.{profile.Extensions[0]}"
+ where ReferenceEquals(profile, ProfileSelector.Select(f.Store.Profiles, [file]))
+ let formats = ProfileSelector.DetectFormats([file])
+ from field in FormatFields.All
+ where field.CanTurnOff && !formats.SelectMany(FormatFields.Of).Any(x => x.Id == field.Id)
+ let off = new HashSet([field.Id], StringComparer.OrdinalIgnoreCase)
+ let presets = profile.PresetIds.Select(f.Store.FindPreset).OfType().ToList()
+ where presets.Any(p => !p.IsGroup && !FormatFields.IsShown(p.TargetFormat, off))
+ where !presets.Where(p => p.IsGroup).SelectMany(p => p.SubPresetIds)
+ .Select(f.Store.FindPreset).OfType().Any(p => !FormatFields.IsShown(p.TargetFormat, off))
+ select (Profile: profile, File: file, Off: off);
+ var chosen = candidates.FirstOrDefault();
+ Assert.NotNull(chosen.Profile);
+ var saved = chosen.Profile.PresetIds.ToArray();
+ f.Load(chosen.File);
+ var baseline = f.Vm.Presets.ToArray();
+ f.Store.Settings.TurnedOffFields = [.. chosen.Off];
+ f.Load(chosen.File);
+ f.Face(baseline.Where(p => FormatFields.IsShown(p.TargetFormat, chosen.Off)).Select(p => p.Id));
+ Assert.Equal(saved, chosen.Profile.PresetIds);
+ f.Store.Settings.TurnedOffFields.Clear();
+ f.Load(chosen.File);
+ f.Face(baseline.Select(p => p.Id));
+ }
+
+ [AvaloniaFact]
+ public void Every_slot_of_a_turned_off_field_leaves_and_the_rest_keep_their_order()
+ {
+ using var f = new Fixture();
+ string[] order = ["png", "yaml", "jpg", "toml", "webp"];
+ var ids = order.Select(format => f.Add(format).Id).ToArray();
+ var profile = f.Profile("/p/a.png");
+ profile.PresetIds = [.. ids];
+ f.Store.Settings.TurnedOffFields = ["developer"];
+ f.Load("/p/a.png");
+ f.Face([ids[0], ids[2], ids[4]]);
+ RenderTests.Save(f.Window, "donut-fields-developer-off.png");
+ Assert.Equal(ids, profile.PresetIds);
+ f.Store.Settings.TurnedOffFields.Clear();
+ f.Load("/p/a.png");
+ f.Face(ids);
+ RenderTests.Save(f.Window, "donut-fields-developer-on.png");
+ }
+
+ [AvaloniaFact]
+ public void User_added_slots_follow_the_switch_on_both_faces()
+ {
+ using var f = new Fixture();
+ var custom = f.Add("woff2");
+ var profile = f.Profile("/p/a.jpg");
+ profile.PresetIds = ["to-png", "to-woff2", custom.Id];
+ // PNG keeps the back usable even when font outputs are hidden or their engine is missing.
+ profile.BackPresetIds = ["to-png", "to-ttf"];
+ f.Store.Settings.TurnedOffFields = ["font"];
+ f.Load("/p/a.jpg");
+ f.Face(["to-png"]);
+ Assert.True(f.Vm.ToggleFace());
+ f.Face(["to-png"]);
+ f.Store.Settings.TurnedOffFields.Clear();
+ f.Load("/p/a.jpg");
+ f.Face(["to-png", "to-woff2", custom.Id]);
+ Assert.True(f.Vm.ToggleFace());
+ f.Face(["to-png", "to-ttf"]);
+ }
+
+ [AvaloniaFact]
+ public void A_format_that_also_belongs_to_a_field_that_is_on_stays()
+ {
+ using var f = new Fixture();
+ f.Profile("/p/a.png").PresetIds = ["to-dxf", "to-dwg"];
+ f.Store.Settings.TurnedOffFields = ["cad"];
+ f.Load("/p/a.png");
+ f.Face(["to-dxf"]);
+ }
+
+ [AvaloniaFact]
+ public void Group_children_of_a_turned_off_field_leave_the_ring()
+ {
+ using var f = new Fixture();
+ var png = f.Add("png");
+ var jpg = f.Add("jpg");
+ var webp = f.Add("webp");
+ var woff2 = f.Add("woff2");
+ var ttf = f.Add("ttf");
+ var b = f.Group("B", woff2.Id, ttf.Id);
+ var a = f.Group("A", png.Id, woff2.Id);
+ var c = f.Group("C", "test-missing");
+ var d = f.Group("D", jpg.Id, webp.Id);
+ Preset[] groups = [b, a, c, d];
+ var saved = groups.Select(p => p.SubPresetIds.ToArray()).ToArray();
+ f.Profile("/p/a.jpg").PresetIds = groups.Select(p => p.Id).ToList();
+ f.Store.Settings.TurnedOffFields = ["font"];
+ f.Load("/p/a.jpg");
+ f.Face([a.Id, c.Id, d.Id]);
+ Assert.Single(f.Window.DonutControl.Items![0].Children!);
+ Assert.Same(png, f.Vm.PresetShownAt(0, 0));
+ Assert.Null(f.Vm.PresetShownAt(0, 1));
+ Assert.Empty(f.Window.DonutControl.Items[1].Children!);
+ Assert.Equal(f.Loc["donut.group_empty"], f.Window.DonutControl.Items[1].DisabledReason);
+ Assert.Equal(2, f.Window.DonutControl.Items[2].Children!.Count);
+ Assert.Same(jpg, f.Vm.PresetShownAt(2, 0));
+ Assert.Same(webp, f.Vm.PresetShownAt(2, 1));
+ Assert.Same(jpg, f.Vm.PresetAt(2, 0));
+ f.Vm.FitToThisDonut();
+ var donut = f.Window.DonutControl;
+ donut.SetExternalHit(new DonutHit(DonutHitKind.Segment, 0));
+ donut.ExpandPendingGroup(); // as after resting on ring A
+ RenderTests.Pump();
+ RenderTests.Save(f.Window, "donut-fields-groups.png");
+ f.Store.Settings.TurnedOffFields.Clear();
+ f.Load("/p/a.jpg");
+ f.Face(groups.Select(p => p.Id));
+ Assert.Same(ttf, f.Vm.PresetShownAt(0, 1));
+ Assert.Same(woff2, f.Vm.PresetShownAt(1, 1));
+ for (var i = 0; i < groups.Length; i++)
+ Assert.Equal(saved[i], groups[i].SubPresetIds);
+ }
+
+ [AvaloniaFact]
+ public void Outputs_in_the_dropped_files_fields_stay_in_all_three_cases()
+ {
+ using var f = new Fixture();
+ var woff2 = f.Add("woff2");
+ var eot = f.Add("eot");
+ var png = f.Add("png");
+ var group = f.Group("group", woff2.Id, png.Id);
+ var profile = f.Profile("/f/a.ttf");
+ var defaultFonts = profile.PresetIds.Select(f.Store.FindPreset).OfType()
+ .Where(p => FormatRegistry.FindById(p.TargetFormat) is { } format && FormatFields.Of(format).Any(x => x.Id == "font"))
+ .ToArray();
+ Assert.NotEmpty(defaultFonts);
+ profile.PresetIds.AddRange([woff2.Id, eot.Id, group.Id]);
+ var saved = profile.PresetIds.ToArray();
+ f.Load("/f/a.ttf");
+ var baseline = f.Vm.Presets.ToArray();
+ f.Store.Settings.TurnedOffFields = ["font", "legacy"];
+ Assert.False(FormatFields.IsShown("woff2", f.Store.Settings.TurnedOffFieldSet()));
+ Assert.False(FormatFields.IsShown("eot", f.Store.Settings.TurnedOffFieldSet()));
+ f.Load("/f/a.ttf");
+ var effectiveOff = new HashSet(["legacy"], StringComparer.OrdinalIgnoreCase);
+ f.Face(baseline.Where(p => FormatFields.IsShown(p.TargetFormat, effectiveOff)).Select(p => p.Id));
+ foreach (var preset in defaultFonts.Concat([woff2, eot, group]))
+ Assert.Contains(f.Vm.Items, item => item.Id == preset.Id);
+ var shown = Assert.Single(f.Window.DonutControl.Items!, item => item.Id == group.Id);
+ Assert.Equal(new[] { woff2.Id, png.Id }, shown.Children!.Select(p => p.Id));
+ Assert.Equal(saved, profile.PresetIds);
+ Assert.Equal(new[] { woff2.Id, png.Id }, group.SubPresetIds);
+ }
+
+ [AvaloniaFact]
+ public void A_new_install_opens_the_font_donut_with_font_conversions()
+ {
+ using var f = new Fixture();
+ f.Store.Settings.TurnedOffFields = FormatFields.All.Where(x => x.CanTurnOff && !x.OnByDefault).Select(x => x.Id).ToList();
+ var off = f.Store.Settings.TurnedOffFieldSet();
+ var profile = f.Profile("/f/a.ttf");
+ Assert.False(profile.IsFallback);
+ Assert.Contains("ttf", profile.Extensions);
+ var fonts = profile.PresetIds.Select(f.Store.FindPreset).OfType()
+ .Where(p => FormatRegistry.FindById(p.TargetFormat) is { } format && FormatFields.Of(format).Any(x => x.Id == "font"))
+ .ToArray();
+ Assert.NotEmpty(fonts);
+ Assert.Contains(fonts, p => !FormatFields.IsShown(p.TargetFormat, off));
+ f.Load("/f/a.ttf");
+ foreach (var preset in fonts)
+ Assert.Contains(f.Window.DonutControl.Items!, item => item.Id == preset.Id);
+ RenderTests.Save(f.Window, "donut-fields-font-new-install.png");
+ Assert.Equal(f.Loc.Format("donut.files_same", 1, FormatRegistry.Get("ttf").DisplayName), f.Window.DonutControl.CenterTitle);
+ }
+
+ [AvaloniaFact]
+ public void Several_dropped_files_keep_the_fields_of_every_file()
+ {
+ using var f = new Fixture();
+ var dds = f.Add("dds");
+ var profile = f.Profile("/p/a.png", "/p/b.tga");
+ Assert.False(profile.IsFallback);
+ Assert.Same(f.Profile("/p/a.png"), profile);
+ profile.PresetIds.Add(dds.Id);
+ f.Store.Settings.TurnedOffFields = ["game"];
+ f.Load("/p/a.png", "/p/b.tga");
+ Assert.Contains(f.Window.DonutControl.Items!, item => item.Id == dds.Id);
+ f.Load("/p/a.png");
+ Assert.DoesNotContain(f.Window.DonutControl.Items!, item => item.Id == dds.Id);
+ }
+
+ [AvaloniaFact]
+ public void The_mixed_donut_keeps_the_fields_of_every_dropped_file()
+ {
+ using var f = new Fixture();
+ f.Store.Settings.TurnedOffFields = FormatFields.All.Where(x => x.CanTurnOff && !x.OnByDefault).Select(x => x.Id).ToList();
+ string[] files = ["/m/a.jpg", "/m/b.ttf", "/m/c.glb"];
+ var profile = f.Profile(files);
+ Assert.True(profile.IsFallback);
+ profile.PresetIds = ["to-woff2", "to-glb", "to-kml"];
+ f.Load(files);
+ f.Face(["to-woff2", "to-glb"]);
+ RenderTests.Save(f.Window, "donut-fields-mixed.png");
+ }
+
+ [AvaloniaFact]
+ public void A_file_of_a_turned_off_field_is_still_recognized()
+ {
+ using var f = new Fixture();
+ f.Store.Settings.TurnedOffFields = ["video-pro"];
+ Assert.Equal("ass", FormatRegistry.Detect("/s/a.ass")?.Id);
+ Assert.Equal("subtitles", f.Profile("/s/a.ass").Id);
+ f.Load("/s/a.ass");
+ Assert.Equal(f.Loc.Format("donut.files_same", 1, "ASS"), f.Vm.CenterTitle);
+ Assert.Equal(f.Vm.CenterTitle, f.Window.DonutControl.CenterTitle);
+ }
+
+ [AvaloniaFact]
+ public void The_preset_library_leaves_out_turned_off_fields()
+ {
+ using var f = new Fixture();
+ f.Store.Settings.TurnedOffFields = ["font"];
+ var off = f.Store.Settings.TurnedOffFieldSet();
+ var page = new PresetsPageViewModel(f.Store, f.Loc, AppHost.Get());
+ Assert.All(page.Presets, p => Assert.True(FormatFields.IsShown(p.Preset.TargetFormat, off), p.Preset.Id));
+ Assert.Contains(page.Presets, p => p.Preset.Id == "to-eot");
+ page.Search = "woff";
+ Assert.Empty(page.VisiblePresets);
+ Assert.All(page.PlacePresets, p => Assert.True(FormatFields.IsShown(p.Preset.TargetFormat, off)));
+ f.Store.Settings.TurnedOffFields.Clear();
+ var enabled = new PresetsPageViewModel(f.Store, f.Loc, AppHost.Get());
+ Assert.Contains(enabled.Presets, p => p.Preset.Id == "to-woff2");
+ }
+
+ [AvaloniaFact]
+ public void The_editor_target_box_leaves_out_turned_off_fields()
+ {
+ using var f = new Fixture();
+ f.Store.Settings.TurnedOffFields = FormatFields.All.Where(x => x.CanTurnOff && !x.OnByDefault).Select(x => x.Id).ToList();
+ var off = f.Store.Settings.TurnedOffFieldSet();
+ var page = new PresetsPageViewModel(f.Store, f.Loc, AppHost.Get());
+ page.SelectedPreset = page.Presets.Single(p => p.Preset.Id == "to-png");
+ var window = new PresetEditorWindow { DataContext = page.Editor, Height = 900 };
+ window.Show();
+ try
+ {
+ RenderTests.Pump();
+ var box = window.GetVisualDescendants().OfType().Single(c => c.Name == "TargetBox");
+ var targets = box.Items.OfType>().Select(c => c.Value).ToList();
+ Assert.NotEmpty(targets);
+ Assert.All(targets, t => Assert.True(FormatFields.IsShown(t, off), t));
+ Assert.Contains("png", targets);
+ Assert.Contains("*", targets);
+ RenderTests.Save(window, "field-switch-targets.png");
+ }
+ finally
+ {
+ window.Close();
+ }
+ }
+
+ [AvaloniaFact]
+ public async Task A_preset_whose_target_is_hidden_keeps_it_through_an_edit()
+ {
+ using var f = new Fixture();
+ f.Store.Settings.TurnedOffFields = ["font"];
+ var off = f.Store.Settings.TurnedOffFieldSet();
+ var preset = f.Add("ttf");
+ var editor = new PresetEditorViewModel(preset, f.Loc, off);
+ Assert.All(editor.Targets, t => Assert.True(FormatFields.IsShown(t.Value, off), t.Value));
+ Assert.DoesNotContain(editor.Targets, t => t.Value is "ttf" or "woff2" or "otf");
+ var name = FormatRegistry.Get("ttf").DisplayName;
+ Assert.Equal("ttf", editor.Target.Value);
+ Assert.Null(editor.TargetSelection);
+ Assert.Equal(name, editor.HiddenTarget);
+ Assert.Equal(f.Loc.Format("presets.target_hidden", name), editor.HiddenTargetNote);
+ editor.Name = "Renamed font";
+ Assert.True(editor.Apply());
+ Assert.Equal("ttf", preset.TargetFormat);
+
+ var window = new PresetEditorWindow { DataContext = editor, Width = 440, Height = 900 };
+ window.Show();
+ try
+ {
+ RenderTests.Pump();
+ var box = window.GetVisualDescendants().OfType().Single(c => c.Name == "TargetBox");
+ Assert.DoesNotContain(box.Items.OfType>(), t => t.Value == "ttf");
+ Assert.Null(box.SelectedItem);
+ Assert.Equal(name, box.PlaceholderText);
+ var note = Assert.Single(window.GetVisualDescendants().OfType(), t => t.Text == editor.HiddenTargetNote);
+ Assert.True(note.IsVisible);
+ Assert.Equal("ttf", editor.Target.Value);
+ Assert.True(editor.Apply());
+ Assert.Equal("ttf", preset.TargetFormat);
+ RenderTests.Save(window, "preset-editor-hidden-target.png");
+ }
+ finally
+ {
+ window.Close();
+ }
+ var editing = AppHost.Get().EditPresetAsync(preset, f.Window);
+ var popup = Assert.Single(f.Window.OwnedWindows.OfType());
+ try
+ {
+ RenderTests.Pump();
+ var popupEditor = Assert.IsType(popup.DataContext);
+ var box = popup.GetVisualDescendants().OfType().Single(c => c.Name == "TargetBox");
+ Assert.DoesNotContain(box.Items.OfType>(), t => t.Value == "ttf");
+ Assert.Null(box.SelectedItem);
+ Assert.Equal(name, box.PlaceholderText);
+ Assert.Equal("ttf", popupEditor.Target.Value);
+ RenderTests.Save(popup, "field-switch-hidden-target-popup.png");
+ }
+ finally
+ {
+ popup.Close(false);
+ }
+ Assert.False(await editing.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken));
+ var changed = new PresetEditorViewModel(preset, f.Loc, off);
+ changed.TargetSelection = changed.Targets.Single(t => t.Value == "png");
+ Assert.True(changed.Apply());
+ Assert.Equal("png", preset.TargetFormat);
+ Assert.Null(changed.HiddenTarget);
+ Assert.Null(changed.HiddenTargetNote);
+ }
+}
diff --git a/tests/Filee.App.Tests/FieldsPageTests.cs b/tests/Filee.App.Tests/FieldsPageTests.cs
new file mode 100644
index 00000000..9e656eea
--- /dev/null
+++ b/tests/Filee.App.Tests/FieldsPageTests.cs
@@ -0,0 +1,213 @@
+using System.Text.Json.Nodes;
+using Avalonia.Automation;
+using Avalonia.Controls;
+using Avalonia.Headless.XUnit;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+using Filee.App.Services;
+using Filee.App.ViewModels;
+using Filee.App.ViewModels.Pages;
+using Filee.App.Views;
+using Filee.App.Views.Pages;
+using Filee.Core.Formats;
+using Filee.Core.Localization;
+using Filee.Core.Settings;
+
+namespace Filee.App.Tests;
+
+public class FieldsPageTests
+{
+ [AvaloniaFact]
+ public void The_page_shows_the_twelve_fields_with_the_new_install_defaults()
+ {
+ TestServices.EnsureInitialized();
+ var store = AppHost.Get();
+ var before = store.Settings.TurnedOffFields.ToList();
+ var window = new Window { Width = 800, Height = 820 };
+ try
+ {
+ store.Settings.TurnedOffFields = FormatFields.All.Where(f => f.CanTurnOff && !f.OnByDefault)
+ .Select(f => f.Id).ToList();
+ var loc = AppHost.Get();
+ window.Content = new FieldsPage { DataContext = new FieldsPageViewModel(store, loc) };
+ window.Show();
+ RenderTests.Pump();
+
+ var switches = window.GetVisualDescendants().OfType().ToList();
+ Assert.Equal(12, switches.Count);
+ Assert.Equal(FormatFields.All.Where(f => f.CanTurnOff).Select(f => loc[$"fields.name.{f.Id}"]),
+ switches.Select(AutomationProperties.GetName));
+ Assert.Equal(loc["fields.name.developer"], AutomationProperties.GetName(Assert.Single(switches, s => s.IsChecked == true)));
+ Assert.All(switches.Where(s => AutomationProperties.GetName(s) != loc["fields.name.developer"]),
+ s => Assert.False(s.IsChecked));
+
+ // The fields that can't be turned off are named at the end, by their category names.
+ var alwaysShown = FormatFields.All.Where(f => !f.CanTurnOff)
+ .Select(f => loc[$"format.category.{Enum.Parse(f.Id, true)}"]).ToList();
+ Assert.Equal(14, alwaysShown.Count);
+ Assert.Contains(loc["format.category.Image"], alwaysShown);
+ var sentence = Assert.Single(window.GetVisualDescendants().OfType(),
+ t => t.Text == loc.Format("fields.always_shown", string.Join(", ", alwaysShown)));
+ Assert.True(sentence.IsEffectivelyVisible);
+ }
+ finally
+ {
+ window.Close();
+ store.Settings.TurnedOffFields = before;
+ }
+ }
+
+ [AvaloniaFact]
+ public void A_row_lists_only_formats_its_switch_can_hide()
+ {
+ TestServices.EnsureInitialized();
+ var store = AppHost.Get();
+ var loc = AppHost.Get();
+ var vm = new FieldsPageViewModel(store, loc);
+ var prefix = loc.Format("fields.formats", "");
+ // With every switch off, a listed format is hidden; one also in a field that can't be turned off never is.
+ var allOff = FormatFields.All.Where(f => f.CanTurnOff).Select(f => f.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
+ var overlapping = 0;
+ foreach (var row in vm.Fields)
+ {
+ var field = FormatFields.All.Single(f => f.Id == row.Id);
+ var listed = row.Formats.Length == 0 ? []
+ : row.Formats[prefix.Length..].TrimEnd('\u2026').Split(", ");
+ Assert.All(listed, name => Assert.False(FormatFields.IsShown(
+ FormatRegistry.Known.First(f => f.Writable && f.DisplayName == name && FormatFields.Of(f).Contains(field)).Id, allOff)));
+ foreach (var format in FormatRegistry.Known.Where(f => f.Writable && FormatFields.Of(f).Contains(field)
+ && FormatFields.Of(f).Any(x => !x.CanTurnOff)))
+ {
+ overlapping++;
+ Assert.DoesNotContain(format.DisplayName, listed);
+ }
+ }
+ // TGA (Image + Games) and DXF (Vector + CAD) were listed before: the case exists.
+ Assert.True(overlapping > 0);
+ Assert.DoesNotContain("TGA", vm.Fields.Single(r => r.Id == "game").Formats);
+ Assert.DoesNotContain("DXF", vm.Fields.Single(r => r.Id == "cad").Formats);
+ }
+
+ [AvaloniaFact]
+ public void Turning_a_field_off_on_the_page_saves_it_and_survives_a_restart()
+ {
+ TestServices.EnsureInitialized();
+ var directory = Directory.CreateTempSubdirectory("filee-fields-").FullName;
+ var window = new Window { Width = 800, Height = 820 };
+ var restarted = new Window { Width = 800, Height = 820 };
+ try
+ {
+ var store = new UserDataStore(directory);
+ store.Load();
+ store.Settings.TurnedOffFields = ["no-such-field"];
+ store.SaveSettings();
+ var saves = 0;
+ store.SettingsChanged += (_, _) => saves++;
+ var loc = AppHost.Get();
+ window.Content = new FieldsPage { DataContext = new FieldsPageViewModel(store, loc) };
+ window.Show();
+ RenderTests.Pump();
+ Assert.Equal(0, saves);
+ var developer = window.GetVisualDescendants().OfType()
+ .Single(s => AutomationProperties.GetName(s) == loc["fields.name.developer"]);
+ Assert.True(developer.IsChecked);
+
+ developer.IsChecked = false;
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(["no-such-field", "developer"], store.Settings.TurnedOffFields);
+ Assert.Equal(1, saves);
+ var json = JsonNode.Parse(File.ReadAllText(Path.Combine(directory, "settings.json")))!;
+ Assert.Equal(["no-such-field", "developer"], json["turnedOffFields"]!.AsArray().Select(n => n!.GetValue()));
+
+ var reloaded = new UserDataStore(directory);
+ reloaded.Load();
+ Assert.Equal(["no-such-field", "developer"], reloaded.Settings.TurnedOffFields);
+ restarted.Content = new FieldsPage { DataContext = new FieldsPageViewModel(reloaded, loc) };
+ restarted.Show();
+ RenderTests.Pump();
+ var switches = restarted.GetVisualDescendants().OfType().ToList();
+ Assert.Equal(12, switches.Count);
+ Assert.Equal(loc["fields.name.developer"], AutomationProperties.GetName(Assert.Single(switches, s => s.IsChecked == false)));
+ Assert.All(switches.Where(s => AutomationProperties.GetName(s) != loc["fields.name.developer"]),
+ s => Assert.True(s.IsChecked));
+ }
+ finally
+ {
+ restarted.Close();
+ window.Close();
+ Directory.Delete(directory, true);
+ }
+ }
+
+ [AvaloniaFact]
+ public void Turning_a_field_on_keeps_ids_this_version_does_not_know()
+ {
+ TestServices.EnsureInitialized();
+ var directory = Directory.CreateTempSubdirectory("filee-fields-").FullName;
+ var window = new Window { Width = 800, Height = 820 };
+ try
+ {
+ var store = new UserDataStore(directory);
+ store.Load();
+ store.Settings.TurnedOffFields = ["CAD", "no-such-field"];
+ store.SaveSettings();
+ var saves = 0;
+ store.SettingsChanged += (_, _) => saves++;
+ var loc = AppHost.Get();
+ window.Content = new FieldsPage { DataContext = new FieldsPageViewModel(store, loc) };
+ window.Show();
+ RenderTests.Pump();
+ Assert.Equal(0, saves);
+ var cad = window.GetVisualDescendants().OfType()
+ .Single(s => AutomationProperties.GetName(s) == loc["fields.name.cad"]);
+ Assert.False(cad.IsChecked);
+
+ cad.IsChecked = true;
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(["no-such-field"], store.Settings.TurnedOffFields);
+ Assert.Equal(1, saves);
+ var json = JsonNode.Parse(File.ReadAllText(Path.Combine(directory, "settings.json")))!;
+ Assert.Equal(["no-such-field"], json["turnedOffFields"]!.AsArray().Select(n => n!.GetValue()));
+ }
+ finally
+ {
+ window.Close();
+ Directory.Delete(directory, true);
+ }
+ }
+
+ [AvaloniaTheory]
+ [InlineData("en")]
+ [InlineData("ko")]
+ [InlineData("ar")]
+ public void The_fields_page_renders_with_the_new_install_defaults(string language)
+ {
+ TestServices.EnsureInitialized(language);
+ var store = AppHost.Get();
+ var before = store.Settings.TurnedOffFields.ToList();
+ MainWindow? window = null;
+ try
+ {
+ store.Settings.TurnedOffFields = FormatFields.All.Where(f => f.CanTurnOff && !f.OnByDefault)
+ .Select(f => f.Id).ToList();
+ var loc = AppHost.Get();
+ using var vm = new MainWindowViewModel(AppHost.Services, loc, AppHost.Get(), store);
+ vm.Navigate("fields");
+ window = new MainWindow { DataContext = vm, Width = 1080, Height = 820 };
+ window.Show();
+ RenderTests.Pump();
+
+ var switches = window.GetVisualDescendants().OfType().ToList();
+ Assert.Equal(12, switches.Count);
+ Assert.Equal(loc["fields.name.developer"], AutomationProperties.GetName(Assert.Single(switches, s => s.IsChecked == true)));
+ Assert.All(switches.Where(s => AutomationProperties.GetName(s) != loc["fields.name.developer"]),
+ s => Assert.False(s.IsChecked));
+ RenderTests.Save(window, $"page-fields-{language}.png");
+ }
+ finally
+ {
+ window?.Close();
+ store.Settings.TurnedOffFields = before;
+ }
+ }
+}
diff --git a/tests/Filee.App.Tests/LocalizationTests.cs b/tests/Filee.App.Tests/LocalizationTests.cs
index 03accff9..24adbe63 100644
--- a/tests/Filee.App.Tests/LocalizationTests.cs
+++ b/tests/Filee.App.Tests/LocalizationTests.cs
@@ -1,6 +1,7 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using Filee.App.Services;
+using Filee.Core.Formats;
namespace Filee.App.Tests;
@@ -115,6 +116,30 @@ public void Line_breaks_are_real_line_breaks(string code)
Assert.False(value.Contains("\\n"), $"{code}:{key} has a literal \\n");
}
+ [Fact]
+ public void Field_switch_texts_exist_in_every_language()
+ {
+ string[] required =
+ [
+ "nav.fields",
+ "fields.title",
+ "fields.description",
+ "fields.formats",
+ "fields.always_shown",
+ "presets.target_hidden",
+ .. FormatFields.All.Where(f => f.CanTurnOff).Select(f => $"fields.name.{f.Id}")
+ ];
+ foreach (var language in LocalizationService.Languages)
+ {
+ var translations = Load(language.Code);
+ foreach (var key in required)
+ {
+ Assert.True(translations.TryGetValue(key, out var value), $"{language.Code}:{key} is missing");
+ Assert.False(string.IsNullOrWhiteSpace(value), $"{language.Code}:{key} is empty");
+ }
+ }
+ }
+
[Theory]
[MemberData(nameof(Languages))]
public void Format_placeholders_match_english(string code)
diff --git a/tests/Filee.App.Tests/TestApp.cs b/tests/Filee.App.Tests/TestApp.cs
index dc0a3187..4d2ad6dd 100644
--- a/tests/Filee.App.Tests/TestApp.cs
+++ b/tests/Filee.App.Tests/TestApp.cs
@@ -41,6 +41,9 @@ public static void EnsureInitialized(string language = "en")
{
AppHost.Build(DataDirectory);
AppHost.Get().Load();
+ // Existing app tests expect every format, like an existing user's settings.
+ // Field-switch tests set their own switches and restore them afterwards.
+ AppHost.Get().Settings.TurnedOffFields.Clear();
AppHost.Get().Attach(app);
_initializedFor = app;
}
diff --git a/tests/Filee.App.Tests/ToolbarEditorTests.cs b/tests/Filee.App.Tests/ToolbarEditorTests.cs
index d94b947d..052dba77 100644
--- a/tests/Filee.App.Tests/ToolbarEditorTests.cs
+++ b/tests/Filee.App.Tests/ToolbarEditorTests.cs
@@ -1097,6 +1097,64 @@ public void Choosing_a_donut_returns_before_judging_and_the_dispatcher_finishes_
public class ToolbarPaletteModelTests
{
+ [AvaloniaFact]
+ public void The_palette_leaves_out_turned_off_fields_on_every_tab_and_in_search()
+ {
+ using var f = new PresetAvailabilityTests.Fixture();
+ var original = f.Store.Settings.TurnedOffFields.ToList();
+ f.Store.Settings.TurnedOffFields = ["font"];
+ Window? window = null;
+ try
+ {
+ using var vm = f.PageWithPalette();
+ var off = f.Store.Settings.TurnedOffFieldSet();
+ void AssertShown(IEnumerable chips) =>
+ Assert.All(chips, c => Assert.True(FormatFields.IsShown(f.Preset(c.PresetId).TargetFormat, off), c.PresetId));
+ AssertShown(vm.Palette);
+ AssertShown(vm.VisiblePalette);
+ Assert.Contains(vm.Palette, c => c.PresetId == "to-eot");
+ foreach (var tab in vm.Tabs.ToList())
+ {
+ vm.SelectedTab = tab;
+ AssertShown(vm.VisiblePalette);
+ }
+ vm.PaletteSearch = "WOFF";
+ AssertShown(vm.Palette);
+ AssertShown(vm.VisiblePalette);
+ vm.PaletteSearch = "";
+ vm.SelectedTab = vm.Tabs.Single(t => t.Id == PaletteTab.All);
+ Dispatcher.UIThread.RunJobs();
+ AssertShown(vm.VisiblePalette);
+ Assert.Contains(vm.VisiblePalette, c => c.PresetId == "to-eot");
+
+ window = new Window { Width = 1080, Height = 820, Content = new ToolbarPage { DataContext = vm } };
+ window.Show();
+ RenderTests.Pump();
+ IEnumerable RenderedChips() => window.GetVisualDescendants().OfType()
+ .Select(b => b.DataContext).OfType();
+ AssertShown(RenderedChips());
+ Assert.Contains(RenderedChips(), c => c.PresetId == "to-eot");
+ RenderTests.Save(window, "toolbar-palette-font-off.png");
+ vm.SelectedTab = vm.Tabs.Single(t => t.Id == PaletteTab.Font);
+ RenderTests.Pump();
+ RenderTests.Save(window, "toolbar-palette-font-tab-off.png");
+ f.Store.Settings.TurnedOffFields.Clear();
+ vm.PaletteSearch = "WOFF";
+ vm.PaletteSearch = "";
+ vm.SelectedTab = vm.Tabs.Single(t => t.Id == PaletteTab.All);
+ RenderTests.Pump();
+ Assert.Contains(RenderedChips(), c => c.PresetId == "to-woff2");
+ vm.SelectedTab = vm.Tabs.Single(t => t.Id == PaletteTab.Font);
+ RenderTests.Pump();
+ RenderTests.Save(window, "toolbar-palette-font-tab-on.png");
+ }
+ finally
+ {
+ window?.Close();
+ f.Store.Settings.TurnedOffFields = original;
+ }
+ }
+
private static ToolbarProfile Profile(string id, string extension) => new()
{
Id = id,
From 86ed4bc187e68bb594e7cdb8db1a8aaf3d06360d Mon Sep 17 00:00:00 2001
From: SeoJCalive <180237232+SeoJCalive@users.noreply.github.com>
Date: Tue, 3 Nov 2026 03:01:07 +0900
Subject: [PATCH 3/4] docs: the field switches and the new default donuts are
on screen
The status note at the top of FORMAT-RULES.md still said both were to
come.
Part of #806
---
docs/FORMAT-RULES.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/docs/FORMAT-RULES.md b/docs/FORMAT-RULES.md
index 3613d9e7..e6b6256d 100644
--- a/docs/FORMAT-RULES.md
+++ b/docs/FORMAT-RULES.md
@@ -6,8 +6,7 @@ so the everyday conversions stay easy to find as more arrive. They come from [#8
[discussion #862](https://github.com/KnifeLemon/Filee/discussions/862).
> **Status:** the fields and grades are in the code (`src/Filee.Core/Formats/FormatFields.cs`, tested by
-> `FormatFieldsTests`). The field switches and the new default donuts are #806's work and are not on screen yet.
-> Rules 3–6 describe how they behave once they are.
+> `FormatFieldsTests`). The field switches (Settings → Fields) and the new default donuts are on screen.
## Formats
From cf1b25013e4e1cc121d03f5d9f201dc018526500 Mon Sep 17 00:00:00 2001
From: SeoJCalive <180237232+SeoJCalive@users.noreply.github.com>
Date: Tue, 3 Nov 2026 16:06:29 +0900
Subject: [PATCH 4/4] fix(settings): keep the field-switch tests on the current
schema
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The existing-settings test now writes AppSettings.CurrentSchemaVersion, so the
schema 59 step from #995 doesn't rewrite the files it compares. The two new
Korean sentences use the 해요체 of the rest of ko.json, and the Fields page
falls back to fields.name.{id} when an id isn't a format category.
Part of #806
---
src/Filee.App/Assets/i18n/ko.json | 4 ++--
src/Filee.App/ViewModels/Pages/FieldsPageViewModel.cs | 7 +++++--
tests/Filee.Core.Tests/UserDataStoreTests.cs | 4 ++--
3 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json
index bd29810c..9f44107a 100644
--- a/src/Filee.App/Assets/i18n/ko.json
+++ b/src/Filee.App/Assets/i18n/ko.json
@@ -3,7 +3,7 @@
"app.tagline": "끌어서 놓으면, 변환 끝.",
"fields.title": "분야",
- "fields.description": "하지 않는 분야를 끄세요. 꺼진 분야에만 속한 형식은 도넛의 세그먼트, 설정 → 원형 툴바의 “사용 가능한 프리셋”, 설정 → 프리셋의 목록, 프리셋의 “변환 형식” 목록에서 빠집니다. 그 형식의 파일은 그대로 알아보고 명령줄은 그 형식으로도 바꾸며, 놓은 파일은 자기 분야의 형식을 그대로 보여 줍니다.",
+ "fields.description": "쓰지 않는 분야는 꺼 두세요. 꺼진 분야에만 속한 형식은 도넛, 설정 → 원형 툴바의 “사용 가능한 프리셋”, 설정 → 프리셋 목록, 프리셋의 “변환 형식” 목록에서 빠져요. 그 형식의 파일은 계속 알아보고, 명령줄로는 그 형식으로도 변환할 수 있어요. 놓은 파일에는 그 파일 분야의 형식이 그대로 나와요.",
"fields.formats": "형식: {0}",
"fields.always_shown": "항상 보이는 분야: {0}",
"fields.name.cad": "CAD",
@@ -745,7 +745,7 @@
"presets.revert": "되돌리기",
"presets.name": "이름",
"presets.target": "변환 형식",
- "presets.target_hidden": "현재 대상({0})은 꺼진 분야에만 속해 목록에 없습니다. 다른 대상을 고르지 않으면 그대로 저장됩니다.",
+ "presets.target_hidden": "현재 변환 형식({0})은 꺼진 분야에만 속해서 목록에 없어요. 다른 형식을 고르지 않으면 그대로 저장돼요.",
"presets.same_as_source": "원본과 같은 형식",
"presets.cad_version": "AutoCAD 버전",
"presets.cad_version.same": "원본과 같게",
diff --git a/src/Filee.App/ViewModels/Pages/FieldsPageViewModel.cs b/src/Filee.App/ViewModels/Pages/FieldsPageViewModel.cs
index f7b24417..54628811 100644
--- a/src/Filee.App/ViewModels/Pages/FieldsPageViewModel.cs
+++ b/src/Filee.App/ViewModels/Pages/FieldsPageViewModel.cs
@@ -12,9 +12,12 @@ public FieldsPageViewModel(UserDataStore store, ILocalizer loc)
{
Fields = FormatFields.All.Where(f => f.CanTurnOff)
.Select(f => new FieldSwitchViewModel(f, store, loc)).ToList();
- // The fields that can't be turned off are the categories, so their names are the category names.
+ // The fields that can't be turned off are the categories, so their names are the category names; any other
+ // field uses its own name.
AlwaysShown = loc.Format("fields.always_shown", string.Join(", ", FormatFields.All.Where(f => !f.CanTurnOff)
- .Select(f => loc[$"format.category.{Enum.Parse(f.Id, true)}"])));
+ .Select(f => Enum.TryParse(f.Id, true, out var category)
+ ? loc[$"format.category.{category}"]
+ : loc[$"fields.name.{f.Id}"])));
}
public IReadOnlyList Fields { get; }
diff --git a/tests/Filee.Core.Tests/UserDataStoreTests.cs b/tests/Filee.Core.Tests/UserDataStoreTests.cs
index a48b68e1..d65fc8ea 100644
--- a/tests/Filee.Core.Tests/UserDataStoreTests.cs
+++ b/tests/Filee.Core.Tests/UserDataStoreTests.cs
@@ -29,7 +29,7 @@ public void A_new_install_turns_off_every_field_but_developer()
}
[Fact]
- public void Schema_58_settings_start_with_every_field_on_and_keep_the_library()
+ public void Settings_without_turned_off_fields_start_with_every_field_on_and_keep_the_library()
{
using var dir = new TempDir();
var store = new UserDataStore(dir.Path);
@@ -40,7 +40,7 @@ public void Schema_58_settings_start_with_every_field_on_and_keep_the_library()
store.SaveLibrary();
Assert.Equal(slots, store.Profiles.Single(p => p.Id == "images").PresetIds);
Assert.Equal(slots, StoredImageSlots(dir.Path));
- File.WriteAllText(Path.Combine(dir.Path, "settings.json"), """{"schemaVersion":58,"language":"ko"}""");
+ File.WriteAllText(Path.Combine(dir.Path, "settings.json"), $$"""{"schemaVersion":{{AppSettings.CurrentSchemaVersion}},"language":"ko"}""");
var paths = new[] { "settings.json", "presets.json", "profiles.json" }.Select(f => Path.Combine(dir.Path, f)).ToArray();
var before = paths.Select(File.ReadAllBytes).ToArray();
var again = new UserDataStore(dir.Path);