From 9856207dd688f8e3a25bd95bd3ca78ed73e55ae3 Mon Sep 17 00:00:00 2001 From: lightscape-jm Date: Tue, 3 Nov 2026 17:27:43 -0600 Subject: [PATCH] feat(tools): Add background tool A palette tool that sets pictures on a gradient, a colour or a picture of one's own, with padding, rounded corners, a soft shadow and a canvas shape (Auto, Square, 16:9, 4:5). The file is drawn with Skia, as the window's preview is, so what the window shows is what the file gets. Settings schema 64 adds the built-in tool-background to the palette (#876). Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 32 + src/Filee.App/Assets/i18n/ar.json | 40 + src/Filee.App/Assets/i18n/de.json | 40 + src/Filee.App/Assets/i18n/en.json | 40 + src/Filee.App/Assets/i18n/es.json | 40 + src/Filee.App/Assets/i18n/fr.json | 40 + src/Filee.App/Assets/i18n/hi.json | 40 + src/Filee.App/Assets/i18n/id.json | 40 + src/Filee.App/Assets/i18n/it.json | 40 + src/Filee.App/Assets/i18n/ja.json | 40 + src/Filee.App/Assets/i18n/ko.json | 40 + src/Filee.App/Assets/i18n/ne.json | 40 + src/Filee.App/Assets/i18n/pl.json | 40 + src/Filee.App/Assets/i18n/pt-BR.json | 40 + src/Filee.App/Assets/i18n/pt-PT.json | 40 + src/Filee.App/Assets/i18n/ru.json | 40 + src/Filee.App/Assets/i18n/th.json | 40 + src/Filee.App/Assets/i18n/tr.json | 40 + src/Filee.App/Assets/i18n/vi.json | 40 + src/Filee.App/Assets/i18n/zh-CN.json | 40 + src/Filee.App/Assets/i18n/zh-TW.json | 40 + src/Filee.App/Services/PresetAvailability.cs | 4 + src/Filee.App/Services/WindowService.cs | 30 + .../ViewModels/BackgroundViewModel.cs | 767 +++++++++++ src/Filee.App/ViewModels/JobViewModel.cs | 5 +- src/Filee.App/Views/BackgroundWindow.axaml | 273 ++++ src/Filee.App/Views/BackgroundWindow.axaml.cs | 135 ++ src/Filee.Core/Conversion/JobQueue.cs | 11 +- src/Filee.Core/History/HistoryEntry.cs | 5 +- src/Filee.Core/Presets/BackgroundTool.cs | 47 + src/Filee.Core/Presets/BuiltInData.cs | 17 + src/Filee.Core/Presets/Preset.cs | 172 ++- src/Filee.Core/Settings/AppSettings.cs | 2 +- src/Filee.Core/Settings/UserDataStore.cs | 16 +- src/Filee.Engines/Magick/BackdropComposer.cs | 639 +++++++++ src/Filee.Engines/Magick/ImageEncoder.cs | 5 + .../Filee.App.Tests/BackgroundWindowTests.cs | 1185 +++++++++++++++++ .../ToolbarPaletteViewTests.cs | 10 +- tests/Filee.Core.Tests/BackgroundToolTests.cs | 257 ++++ tests/Filee.Engines.Tests/BackdropTests.cs | 832 ++++++++++++ 40 files changed, 5224 insertions(+), 20 deletions(-) create mode 100644 src/Filee.App/ViewModels/BackgroundViewModel.cs create mode 100644 src/Filee.App/Views/BackgroundWindow.axaml create mode 100644 src/Filee.App/Views/BackgroundWindow.axaml.cs create mode 100644 src/Filee.Core/Presets/BackgroundTool.cs create mode 100644 src/Filee.Engines/Magick/BackdropComposer.cs create mode 100644 tests/Filee.App.Tests/BackgroundWindowTests.cs create mode 100644 tests/Filee.Core.Tests/BackgroundToolTests.cs create mode 100644 tests/Filee.Engines.Tests/BackdropTests.cs diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4c158e458..9f29e4c34 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -171,6 +171,38 @@ numbers on pictures (`ImageOptions.Annotations`): points are parts of the pictur - `AnnotateTool.ConversionOf` (Core) turns the tool into a conversion in the picture's own format (as Pixelate / blur), run by the job queue; `ReworksPixels` includes the marks. +## Add background tool + +"Add background" (`tool-background`, palette only, settings schema 64) sets pictures on a gradient, a colour or a +picture of one's own, with padding, rounded corners, a shadow and a canvas shape (`BackdropOptions`). Sizes are parts of +the picture (padding of its longer side, corners of its shorter side), so a look suits any picture. + +- `BackgroundTool.ConversionOf` (Core) turns the tool into a PNG conversion with `ImageOptions.Backdrop`, run by the job + queue like Pixelate / blur's areas: `ReworksPixels` includes it, so it is applied once, in the first transform step. + The tool keeps the last look apart, in `ImageOptions.BackdropLook`; an export or import leaves out the path of a + picture of one's own. +- `Engines/Magick/BackdropComposer` composes it in `ImageEncoder.ApplyOptions`, after resizing, background removal and + grayscale and before the density, metadata and alpha steps. The picture is brought to sRGB as it is seen (through its + profile, or by formula for CMYK and grey); the canvas grows round it to the shape (never crops) and stays within a + pixel limit (`PictureSizeOn`). It is drawn by Skia as the window draws it: a linear gradient shader, a colour, or + one's own picture covering (`ReadPictureBehind`: a raster picture of a few kinds, by extension or by its first bytes, + its size checked before it is decoded, read as large as covering the canvas takes and cropped to it, in sRGB, white + where see-through; one that can't be read refuses the file); the picture with its corners cut round in its own pixels; + Skia's drop shadow of what shows of it, blurred at most at a sigma of 100 (a wider one drawn smaller and scaled up, + where Skia's blur stays Gaussian). The canvas is drawn in tiles at most 2048 px wide, a band at a time, as RGB: a blur + is never larger than a tile and the shadow's reach round it (a wider shadow is blurred once, scaled down until its + sigma is 100 or less). The EXIF thumbnail is dropped. +- `BackgroundViewModel` draws the same composition live with Avalonia: the layout and shadow from the composer's + formulas (an Avalonia drop shadow is Skia's), the picture and the one behind read through the composer's functions, + the preview never mirrored in a right-to-left UI. A picture behind is read when it is chosen, and again when it is + picked again; Save waits for one that can be read, and one whose file changed since it was read (time written or + length) is read again and shown before it is saved. The conversion carries that stamp, and the composer refuses the + file (`error.background_picture_changed`, which the toast offers no Retry for) when the picture behind changed since, + before it is read or after: a change after Save gives no file rather than one with a picture behind the window didn't + show. A change that keeps both the time and the length (a copy that keeps the time, of a picture of the same size) + isn't told apart, and the picture itself edited meanwhile isn't looked for (as in Pixelate / blur and Annotate). One + look for all pictures or one each; failures end on a summary, as in Pixelate / blur. + ## Crop overlay and PDF page editor The image Crop tool (`tool-crop`) and PDF Crop (`tool-pdf-crop`) open `Views/CropOverlayWindow` instead of running a job: diff --git a/src/Filee.App/Assets/i18n/ar.json b/src/Filee.App/Assets/i18n/ar.json index 78427faea..e65e9cb7c 100644 --- a/src/Filee.App/Assets/i18n/ar.json +++ b/src/Filee.App/Assets/i18n/ar.json @@ -297,6 +297,8 @@ "preset.hide": "بكسلة / تمويه", "preset.hide.donut": "بكسلة", "error.cannot_hide": "البكسلة والتمويه متاحان للصور فقط", + "error.background_picture": "تعذّرت قراءة الصورة المراد وضعها في الخلفية", + "error.background_picture_changed": "تغيّرت الصورة المراد وضعها في الخلفية بعد عرضها. أضف الخلفية مرة أخرى.", "hide.save": "إخفاء وحفظ", "hide.save_next": "إخفاء والتالي", "hide.pixelate": "بكسلة", @@ -412,6 +414,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "انعكاس", "preset.annotate": "تعليقات", + "preset.background": "إضافة خلفية", "preset.flip_horizontal": "انعكاس أفقي", "preset.flip_horizontal.short": "أفقي", "preset.flip_vertical": "انعكاس عمودي", @@ -1006,6 +1009,43 @@ "annotate.keys": "V للتحديد · Tab لاختيار علامة والأسهم لتحريكها · P A B H T N لاختيار أداة · Del للإزالة · Ctrl+Z للتراجع · Esc للإغلاق", "annotate.save": "حفظ", "annotate.save_next": "حفظ والتالي", + "background.background": "الخلفية", + "background.gradient": "تدرّج", + "background.color": "لون", + "background.picture": "صورة", + "background.from": "اللون الأول", + "background.to": "اللون الثاني", + "background.choose": "اختيار…", + "background.no_picture": "لم تُختر صورة", + "background.padding": "الهامش", + "background.corners": "الزوايا", + "background.shadow": "الظل", + "background.shadow_none": "بلا", + "background.shadow_light": "خفيف", + "background.shadow_soft": "ناعم", + "background.shadow_strong": "قوي", + "background.canvas": "اللوحة", + "background.auto": "تلقائي", + "background.square": "مربع", + "background.landscape": "أفقي (16:9)", + "background.portrait": "عمودي (4:5)", + "background.same_look": "المظهر نفسه لكل الصور", + "background.save": "حفظ", + "background.save_next": "حفظ والتالي", + "background.gradient_sunset": "غروب", + "background.gradient_sky": "سماء", + "background.gradient_mint": "نعناع", + "background.gradient_peach": "خوخ", + "background.gradient_ocean": "محيط", + "background.gradient_lilac": "ليلكي", + "background.color_white": "أبيض", + "background.color_paper": "ورقي", + "background.color_charcoal": "فحمي", + "background.color_navy": "كحلي", + "background.color_sand": "رملي", + "background.color_sage": "مريمية", + "background.picture_unreadable": "تعذّرت قراءة هذه الصورة. اختر صورة أخرى، أو تدرّجًا أو لونًا.", + "background.keys": "Enter يحفظ · Esc يغلق · Tab يتنقّل بين خيارات المظهر · الأسهم تختار أو تحرّك شريط التمرير", "presets.editor_title": "تعديل الإعداد المسبق", "presets.delete_confirm": "هل تريد حذف «{0}»؟ سيُزال أيضًا من جميع أشرطة الأدوات والمجموعات.", "presets.name_required": "أدخل اسمًا", diff --git a/src/Filee.App/Assets/i18n/de.json b/src/Filee.App/Assets/i18n/de.json index 14d4070e0..23d4dd594 100644 --- a/src/Filee.App/Assets/i18n/de.json +++ b/src/Filee.App/Assets/i18n/de.json @@ -297,6 +297,8 @@ "preset.hide": "Verpixeln / Weichzeichnen", "preset.hide.donut": "Verpixeln", "error.cannot_hide": "Verpixeln und Weichzeichnen ist nur bei Bildern möglich", + "error.background_picture": "Das Bild für den Hintergrund kann nicht gelesen werden", + "error.background_picture_changed": "Das Bild für den Hintergrund hat sich geändert, nachdem es angezeigt wurde. Füge den Hintergrund noch einmal hinzu.", "hide.save": "Verbergen & Speichern", "hide.save_next": "Verbergen & Weiter", "hide.pixelate": "Verpixeln", @@ -412,6 +414,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Spiegeln", "preset.annotate": "Beschriften", + "preset.background": "Hintergrund hinzufügen", "preset.flip_horizontal": "Horizontal spiegeln", "preset.flip_horizontal.short": "Horizontal", "preset.flip_vertical": "Vertikal spiegeln", @@ -1006,6 +1009,43 @@ "annotate.keys": "V wählt aus · Tab wählt eine Markierung, Pfeiltasten verschieben sie · P A B H T N wählen ein Werkzeug · Entf entfernt · Strg+Z macht rückgängig · Esc schließt", "annotate.save": "Speichern", "annotate.save_next": "Speichern & weiter", + "background.background": "Hintergrund", + "background.gradient": "Verlauf", + "background.color": "Farbe", + "background.picture": "Bild", + "background.from": "Erste Farbe", + "background.to": "Zweite Farbe", + "background.choose": "Auswählen…", + "background.no_picture": "Kein Bild ausgewählt", + "background.padding": "Abstand", + "background.corners": "Ecken", + "background.shadow": "Schatten", + "background.shadow_none": "Keiner", + "background.shadow_light": "Leicht", + "background.shadow_soft": "Weich", + "background.shadow_strong": "Stark", + "background.canvas": "Leinwand", + "background.auto": "Automatisch", + "background.square": "Quadrat", + "background.landscape": "Querformat (16:9)", + "background.portrait": "Hochformat (4:5)", + "background.same_look": "Gleicher Look für alle Bilder", + "background.save": "Speichern", + "background.save_next": "Speichern & weiter", + "background.gradient_sunset": "Sonnenuntergang", + "background.gradient_sky": "Himmel", + "background.gradient_mint": "Minze", + "background.gradient_peach": "Pfirsich", + "background.gradient_ocean": "Ozean", + "background.gradient_lilac": "Flieder", + "background.color_white": "Weiß", + "background.color_paper": "Papier", + "background.color_charcoal": "Anthrazit", + "background.color_navy": "Marineblau", + "background.color_sand": "Sand", + "background.color_sage": "Salbei", + "background.picture_unreadable": "Dieses Bild kann nicht gelesen werden. Wähle ein anderes, einen Verlauf oder eine Farbe.", + "background.keys": "Eingabe speichert · Esc schließt · Tab geht durch den Look · Pfeiltasten wählen oder bewegen einen Regler", "presets.editor_title": "Vorlage bearbeiten", "presets.delete_confirm": "„{0}“ löschen? Die Vorlage wird auch aus allen Werkzeugleisten und Gruppen entfernt.", "presets.name_required": "Namen eingeben", diff --git a/src/Filee.App/Assets/i18n/en.json b/src/Filee.App/Assets/i18n/en.json index 3d742ecd7..43c91786d 100644 --- a/src/Filee.App/Assets/i18n/en.json +++ b/src/Filee.App/Assets/i18n/en.json @@ -306,6 +306,8 @@ "preset.hide": "Pixelate / blur", "preset.hide.donut": "Pixelate", "error.cannot_hide": "Only pictures can be pixelated or blurred", + "error.background_picture": "The picture to put behind can't be read", + "error.background_picture_changed": "The picture to put behind changed after it was shown. Add the background again.", "hide.save": "Hide & Save", "hide.save_next": "Hide & Next", "hide.pixelate": "Pixelate", @@ -422,6 +424,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Flip", "preset.annotate": "Annotate", + "preset.background": "Add background", "preset.flip_horizontal": "Flip horizontal", "preset.flip_horizontal.short": "Horizontal", "preset.flip_vertical": "Flip vertical", @@ -1021,6 +1024,43 @@ "annotate.keys": "V selects · Tab picks a mark, arrows move it · P A B H T N pick a tool · Del removes · Ctrl+Z undoes · Esc closes", "annotate.save": "Save", "annotate.save_next": "Save & next", + "background.background": "Background", + "background.gradient": "Gradient", + "background.color": "Colour", + "background.picture": "Picture", + "background.from": "First colour", + "background.to": "Second colour", + "background.choose": "Choose…", + "background.no_picture": "No picture chosen", + "background.padding": "Padding", + "background.corners": "Corners", + "background.shadow": "Shadow", + "background.shadow_none": "None", + "background.shadow_light": "Light", + "background.shadow_soft": "Soft", + "background.shadow_strong": "Strong", + "background.canvas": "Canvas", + "background.auto": "Auto", + "background.square": "Square", + "background.landscape": "Landscape (16:9)", + "background.portrait": "Portrait (4:5)", + "background.same_look": "Same look for all pictures", + "background.save": "Save", + "background.save_next": "Save & next", + "background.gradient_sunset": "Sunset", + "background.gradient_sky": "Sky", + "background.gradient_mint": "Mint", + "background.gradient_peach": "Peach", + "background.gradient_ocean": "Ocean", + "background.gradient_lilac": "Lilac", + "background.color_white": "White", + "background.color_paper": "Paper", + "background.color_charcoal": "Charcoal", + "background.color_navy": "Navy", + "background.color_sand": "Sand", + "background.color_sage": "Sage", + "background.picture_unreadable": "This picture can't be read. Choose another one, or a gradient or a colour.", + "background.keys": "Enter saves · Esc closes · Tab goes through the look · arrows choose or move a slider", "presets.editor_title": "Edit preset", "presets.delete_confirm": "Delete “{0}”? It will also be removed from every toolbar and group.", "presets.name_required": "Enter a name", diff --git a/src/Filee.App/Assets/i18n/es.json b/src/Filee.App/Assets/i18n/es.json index f133bd4ad..280629611 100644 --- a/src/Filee.App/Assets/i18n/es.json +++ b/src/Filee.App/Assets/i18n/es.json @@ -297,6 +297,8 @@ "preset.hide": "Pixelar / difuminar", "preset.hide.donut": "Pixelar", "error.cannot_hide": "Solo se pueden pixelar o difuminar imágenes", + "error.background_picture": "No se puede leer la imagen para el fondo", + "error.background_picture_changed": "La imagen para el fondo cambió después de mostrarse. Vuelve a añadir el fondo.", "hide.save": "Ocultar y guardar", "hide.save_next": "Ocultar y seguir", "hide.pixelate": "Pixelar", @@ -412,6 +414,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Voltear", "preset.annotate": "Anotar", + "preset.background": "Añadir fondo", "preset.flip_horizontal": "Volteo horizontal", "preset.flip_horizontal.short": "Horizontal", "preset.flip_vertical": "Volteo vertical", @@ -1006,6 +1009,43 @@ "annotate.keys": "V selecciona · Tab elige una marca, las flechas la mueven · P A B H T N eligen herramienta · Supr quita · Ctrl+Z deshace · Esc cierra", "annotate.save": "Guardar", "annotate.save_next": "Guardar y siguiente", + "background.background": "Fondo", + "background.gradient": "Degradado", + "background.color": "Color", + "background.picture": "Imagen", + "background.from": "Primer color", + "background.to": "Segundo color", + "background.choose": "Elegir…", + "background.no_picture": "No se ha elegido imagen", + "background.padding": "Margen", + "background.corners": "Esquinas", + "background.shadow": "Sombra", + "background.shadow_none": "Ninguna", + "background.shadow_light": "Ligera", + "background.shadow_soft": "Suave", + "background.shadow_strong": "Fuerte", + "background.canvas": "Lienzo", + "background.auto": "Auto", + "background.square": "Cuadrado", + "background.landscape": "Horizontal (16:9)", + "background.portrait": "Vertical (4:5)", + "background.same_look": "Mismo aspecto para todas las imágenes", + "background.save": "Guardar", + "background.save_next": "Guardar y siguiente", + "background.gradient_sunset": "Atardecer", + "background.gradient_sky": "Cielo", + "background.gradient_mint": "Menta", + "background.gradient_peach": "Melocotón", + "background.gradient_ocean": "Océano", + "background.gradient_lilac": "Lila", + "background.color_white": "Blanco", + "background.color_paper": "Papel", + "background.color_charcoal": "Carbón", + "background.color_navy": "Azul marino", + "background.color_sand": "Arena", + "background.color_sage": "Salvia", + "background.picture_unreadable": "No se puede leer esta imagen. Elige otra, o un degradado o un color.", + "background.keys": "Intro guarda · Esc cierra · Tab recorre el aspecto · las flechas eligen o mueven un control deslizante", "presets.editor_title": "Editar ajuste preestablecido", "presets.delete_confirm": "¿Eliminar «{0}»? También se quitará de todos los menús circulares y grupos.", "presets.name_required": "Escribe un nombre", diff --git a/src/Filee.App/Assets/i18n/fr.json b/src/Filee.App/Assets/i18n/fr.json index 7e6b2bfb1..d691ebbaf 100644 --- a/src/Filee.App/Assets/i18n/fr.json +++ b/src/Filee.App/Assets/i18n/fr.json @@ -306,6 +306,8 @@ "preset.hide": "Pixeliser / flouter", "preset.hide.donut": "Pixeliser", "error.cannot_hide": "Seules les images peuvent être pixelisées ou floutées", + "error.background_picture": "Impossible de lire l'image à mettre en arrière-plan", + "error.background_picture_changed": "L'image à mettre en arrière-plan a changé après avoir été affichée. Ajoutez de nouveau l'arrière-plan.", "hide.save": "Masquer et enregistrer", "hide.save_next": "Masquer et suivant", "hide.pixelate": "Pixeliser", @@ -422,6 +424,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Retourner", "preset.annotate": "Annoter", + "preset.background": "Ajouter un fond", "preset.flip_horizontal": "Retourner horizontalement", "preset.flip_horizontal.short": "Horizontal", "preset.flip_vertical": "Retourner verticalement", @@ -1021,6 +1024,43 @@ "annotate.keys": "V sélectionne · Tab choisit une marque, les flèches la déplacent · P A B H T N choisissent un outil · Suppr supprime · Ctrl+Z annule · Échap ferme", "annotate.save": "Enregistrer", "annotate.save_next": "Enregistrer et suivant", + "background.background": "Fond", + "background.gradient": "Dégradé", + "background.color": "Couleur", + "background.picture": "Image", + "background.from": "Première couleur", + "background.to": "Deuxième couleur", + "background.choose": "Choisir…", + "background.no_picture": "Aucune image choisie", + "background.padding": "Marge", + "background.corners": "Coins", + "background.shadow": "Ombre", + "background.shadow_none": "Aucune", + "background.shadow_light": "Légère", + "background.shadow_soft": "Douce", + "background.shadow_strong": "Forte", + "background.canvas": "Toile", + "background.auto": "Auto", + "background.square": "Carré", + "background.landscape": "Paysage (16:9)", + "background.portrait": "Portrait (4:5)", + "background.same_look": "Même apparence pour toutes les images", + "background.save": "Enregistrer", + "background.save_next": "Enregistrer et suivant", + "background.gradient_sunset": "Coucher de soleil", + "background.gradient_sky": "Ciel", + "background.gradient_mint": "Menthe", + "background.gradient_peach": "Pêche", + "background.gradient_ocean": "Océan", + "background.gradient_lilac": "Lilas", + "background.color_white": "Blanc", + "background.color_paper": "Papier", + "background.color_charcoal": "Anthracite", + "background.color_navy": "Bleu marine", + "background.color_sand": "Sable", + "background.color_sage": "Sauge", + "background.picture_unreadable": "Impossible de lire cette image. Choisissez-en une autre, ou un dégradé ou une couleur.", + "background.keys": "Entrée enregistre · Échap ferme · Tab parcourt l'apparence · les flèches choisissent ou déplacent un curseur", "presets.editor_title": "Modifier le préréglage", "presets.delete_confirm": "Supprimer « {0} » ? Il sera aussi retiré de tous les menus circulaires et groupes.", "presets.name_required": "Saisissez un nom", diff --git a/src/Filee.App/Assets/i18n/hi.json b/src/Filee.App/Assets/i18n/hi.json index 3f38eeb4c..91f9b7aac 100644 --- a/src/Filee.App/Assets/i18n/hi.json +++ b/src/Filee.App/Assets/i18n/hi.json @@ -297,6 +297,8 @@ "preset.hide": "पिक्सलेट / धुंधला", "preset.hide.donut": "पिक्सलेट", "error.cannot_hide": "पिक्सलेट और धुंधला केवल इमेज पर काम करता है", + "error.background_picture": "पीछे लगाने वाली तस्वीर पढ़ी नहीं जा सकती", + "error.background_picture_changed": "पीछे लगाने वाली तस्वीर दिखाए जाने के बाद बदल गई। पृष्ठभूमि फिर से जोड़ें।", "hide.save": "छिपाकर सहेजें", "hide.save_next": "छिपाकर अगला", "hide.pixelate": "पिक्सलेट", @@ -412,6 +414,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "पलटें", "preset.annotate": "टिप्पणी", + "preset.background": "पृष्ठभूमि जोड़ें", "preset.flip_horizontal": "आड़ा पलटें", "preset.flip_horizontal.short": "आड़ा", "preset.flip_vertical": "खड़ा पलटें", @@ -1006,6 +1009,43 @@ "annotate.keys": "V चुनता है · Tab निशान चुनता है, तीर उसे खिसकाते हैं · P A B H T N टूल चुनते हैं · Del हटाता है · Ctrl+Z पूर्ववत · Esc बंद करता है", "annotate.save": "सहेजें", "annotate.save_next": "सहेजें और अगला", + "background.background": "पृष्ठभूमि", + "background.gradient": "ग्रेडिएंट", + "background.color": "रंग", + "background.picture": "तस्वीर", + "background.from": "पहला रंग", + "background.to": "दूसरा रंग", + "background.choose": "चुनें…", + "background.no_picture": "कोई तस्वीर नहीं चुनी", + "background.padding": "हाशिया", + "background.corners": "कोने", + "background.shadow": "छाया", + "background.shadow_none": "कोई नहीं", + "background.shadow_light": "हल्की", + "background.shadow_soft": "नरम", + "background.shadow_strong": "गहरी", + "background.canvas": "कैनवास", + "background.auto": "अपने आप", + "background.square": "वर्गाकार", + "background.landscape": "आड़ा (16:9)", + "background.portrait": "खड़ा (4:5)", + "background.same_look": "सभी तस्वीरों के लिए एक जैसा रूप", + "background.save": "सहेजें", + "background.save_next": "सहेजें और अगला", + "background.gradient_sunset": "सूर्यास्त", + "background.gradient_sky": "आसमान", + "background.gradient_mint": "पुदीना", + "background.gradient_peach": "आड़ू", + "background.gradient_ocean": "समुद्र", + "background.gradient_lilac": "बकाइन", + "background.color_white": "सफ़ेद", + "background.color_paper": "कागज़ी", + "background.color_charcoal": "चारकोल", + "background.color_navy": "नेवी", + "background.color_sand": "रेतीला", + "background.color_sage": "सेज", + "background.picture_unreadable": "यह तस्वीर पढ़ी नहीं जा सकती। कोई दूसरी चुनें, या ग्रेडिएंट या रंग।", + "background.keys": "Enter सहेजता है · Esc बंद करता है · Tab रूप के विकल्पों में चलता है · तीर चुनते हैं या स्लाइडर खिसकाते हैं", "presets.editor_title": "प्रीसेट संपादित करें", "presets.delete_confirm": "“{0}” हटाएँ? यह हर टूलबार और समूह से भी हट जाएगा।", "presets.name_required": "नाम दर्ज करें", diff --git a/src/Filee.App/Assets/i18n/id.json b/src/Filee.App/Assets/i18n/id.json index c265c4ba5..279df415d 100644 --- a/src/Filee.App/Assets/i18n/id.json +++ b/src/Filee.App/Assets/i18n/id.json @@ -297,6 +297,8 @@ "preset.hide": "Pikselasi / buramkan", "preset.hide.donut": "Pikselasi", "error.cannot_hide": "Hanya gambar yang dapat dipikselasi atau diburamkan", + "error.background_picture": "Gambar untuk latar tidak bisa dibaca", + "error.background_picture_changed": "Gambar untuk latar berubah setelah ditampilkan. Tambahkan latar lagi.", "hide.save": "Sembunyikan & Simpan", "hide.save_next": "Sembunyikan & Berikutnya", "hide.pixelate": "Pikselasi", @@ -412,6 +414,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Balik", "preset.annotate": "Anotasi", + "preset.background": "Tambah latar", "preset.flip_horizontal": "Balik horizontal", "preset.flip_horizontal.short": "Horizontal", "preset.flip_vertical": "Balik vertikal", @@ -1006,6 +1009,43 @@ "annotate.keys": "V memilih · Tab memilih tanda, panah menggesernya · P A B H T N memilih alat · Del menghapus · Ctrl+Z urungkan · Esc menutup", "annotate.save": "Simpan", "annotate.save_next": "Simpan & Berikutnya", + "background.background": "Latar", + "background.gradient": "Gradasi", + "background.color": "Warna", + "background.picture": "Gambar", + "background.from": "Warna pertama", + "background.to": "Warna kedua", + "background.choose": "Pilih…", + "background.no_picture": "Belum ada gambar dipilih", + "background.padding": "Jarak tepi", + "background.corners": "Sudut", + "background.shadow": "Bayangan", + "background.shadow_none": "Tidak ada", + "background.shadow_light": "Tipis", + "background.shadow_soft": "Lembut", + "background.shadow_strong": "Kuat", + "background.canvas": "Kanvas", + "background.auto": "Otomatis", + "background.square": "Persegi", + "background.landscape": "Lanskap (16:9)", + "background.portrait": "Potret (4:5)", + "background.same_look": "Tampilan sama untuk semua gambar", + "background.save": "Simpan", + "background.save_next": "Simpan & Berikutnya", + "background.gradient_sunset": "Senja", + "background.gradient_sky": "Langit", + "background.gradient_mint": "Mint", + "background.gradient_peach": "Persik", + "background.gradient_ocean": "Samudra", + "background.gradient_lilac": "Lila", + "background.color_white": "Putih", + "background.color_paper": "Kertas", + "background.color_charcoal": "Arang", + "background.color_navy": "Biru dongker", + "background.color_sand": "Pasir", + "background.color_sage": "Hijau sage", + "background.picture_unreadable": "Gambar ini tidak bisa dibaca. Pilih gambar lain, atau gradasi atau warna.", + "background.keys": "Enter menyimpan · Esc menutup · Tab menelusuri tampilan · panah memilih atau menggeser penggeser", "presets.editor_title": "Edit preset", "presets.delete_confirm": "Hapus “{0}”? Preset ini juga akan dihapus dari semua toolbar dan grup.", "presets.name_required": "Masukkan nama", diff --git a/src/Filee.App/Assets/i18n/it.json b/src/Filee.App/Assets/i18n/it.json index 7f3147ee7..824b1cf8a 100644 --- a/src/Filee.App/Assets/i18n/it.json +++ b/src/Filee.App/Assets/i18n/it.json @@ -296,6 +296,8 @@ "preset.hide": "Pixella / sfoca", "preset.hide.donut": "Pixella", "error.cannot_hide": "Solo le immagini possono essere pixellate o sfocate", +"error.background_picture": "Impossibile leggere l'immagine da mettere sullo sfondo", +"error.background_picture_changed": "L'immagine da mettere sullo sfondo è cambiata dopo essere stata mostrata. Aggiungi di nuovo lo sfondo.", "hide.save": "Nascondi e salva", "hide.save_next": "Nascondi e avanti", "hide.pixelate": "Pixella", @@ -411,6 +413,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Capovolgi", "preset.annotate": "Annota", +"preset.background": "Aggiungi sfondo", "preset.flip_horizontal": "Capovolgi orizzontalmente", "preset.flip_horizontal.short": "Orizzontale", "preset.flip_vertical": "Capovolgi verticalmente", @@ -1005,6 +1008,43 @@ "annotate.keys": "V seleziona · Tab sceglie un segno, le frecce lo spostano · P A B H T N scelgono uno strumento · Canc rimuove · Ctrl+Z annulla · Esc chiude", "annotate.save": "Salva", "annotate.save_next": "Salva e avanti", +"background.background": "Sfondo", +"background.gradient": "Sfumatura", +"background.color": "Colore", +"background.picture": "Immagine", +"background.from": "Primo colore", +"background.to": "Secondo colore", +"background.choose": "Scegli…", +"background.no_picture": "Nessuna immagine scelta", +"background.padding": "Margine", +"background.corners": "Angoli", +"background.shadow": "Ombra", +"background.shadow_none": "Nessuna", +"background.shadow_light": "Leggera", +"background.shadow_soft": "Morbida", +"background.shadow_strong": "Forte", +"background.canvas": "Tela", +"background.auto": "Auto", +"background.square": "Quadrato", +"background.landscape": "Orizzontale (16:9)", +"background.portrait": "Verticale (4:5)", +"background.same_look": "Stesso aspetto per tutte le immagini", +"background.save": "Salva", +"background.save_next": "Salva e avanti", +"background.gradient_sunset": "Tramonto", +"background.gradient_sky": "Cielo", +"background.gradient_mint": "Menta", +"background.gradient_peach": "Pesca", +"background.gradient_ocean": "Oceano", +"background.gradient_lilac": "Lilla", +"background.color_white": "Bianco", +"background.color_paper": "Carta", +"background.color_charcoal": "Antracite", +"background.color_navy": "Blu navy", +"background.color_sand": "Sabbia", +"background.color_sage": "Salvia", +"background.picture_unreadable": "Impossibile leggere questa immagine. Scegline un'altra, oppure una sfumatura o un colore.", +"background.keys": "Invio salva · Esc chiude · Tab scorre l'aspetto · le frecce scelgono o spostano un cursore", "presets.editor_title": "Modifica preset", "presets.delete_confirm": "Eliminare “{0}”? Verrà rimosso anche da ogni barra e gruppo.", "presets.name_required": "Inserisci un nome", diff --git a/src/Filee.App/Assets/i18n/ja.json b/src/Filee.App/Assets/i18n/ja.json index 6bd798b3e..3cb7e576e 100644 --- a/src/Filee.App/Assets/i18n/ja.json +++ b/src/Filee.App/Assets/i18n/ja.json @@ -297,6 +297,8 @@ "preset.hide": "モザイク・ぼかし", "preset.hide.donut": "モザイク", "error.cannot_hide": "モザイクとぼかしは画像でのみ使えます", + "error.background_picture": "背景に置く画像を読み込めません", + "error.background_picture_changed": "背景に置く画像が、表示された後に変更されました。もう一度背景を追加してください。", "hide.save": "隠して保存", "hide.save_next": "隠して次へ", "hide.pixelate": "モザイク", @@ -412,6 +414,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "反転", "preset.annotate": "注釈", + "preset.background": "背景を追加", "preset.flip_horizontal": "水平方向に反転", "preset.flip_horizontal.short": "水平", "preset.flip_vertical": "垂直方向に反転", @@ -1006,6 +1009,43 @@ "annotate.keys": "V 選択 · Tab で選択、矢印で移動 · P A B H T N ツール · Del 削除 · Ctrl+Z 元に戻す · Esc 閉じる", "annotate.save": "保存", "annotate.save_next": "保存して次へ", + "background.background": "背景", + "background.gradient": "グラデーション", + "background.color": "色", + "background.picture": "画像", + "background.from": "1 つ目の色", + "background.to": "2 つ目の色", + "background.choose": "選択…", + "background.no_picture": "画像が選ばれていません", + "background.padding": "余白", + "background.corners": "角", + "background.shadow": "影", + "background.shadow_none": "なし", + "background.shadow_light": "薄い", + "background.shadow_soft": "やわらか", + "background.shadow_strong": "濃い", + "background.canvas": "キャンバス", + "background.auto": "自動", + "background.square": "正方形", + "background.landscape": "横長 (16:9)", + "background.portrait": "縦長 (4:5)", + "background.same_look": "すべての画像に同じ見た目", + "background.save": "保存", + "background.save_next": "保存して次へ", + "background.gradient_sunset": "夕焼け", + "background.gradient_sky": "空", + "background.gradient_mint": "ミント", + "background.gradient_peach": "ピーチ", + "background.gradient_ocean": "オーシャン", + "background.gradient_lilac": "ライラック", + "background.color_white": "白", + "background.color_paper": "ペーパー", + "background.color_charcoal": "チャコール", + "background.color_navy": "ネイビー", + "background.color_sand": "サンド", + "background.color_sage": "セージ", + "background.picture_unreadable": "この画像は読み込めません。別の画像か、グラデーションか色を選んでください。", + "background.keys": "Enter で保存 · Esc で閉じる · Tab で見た目の項目を移動 · 矢印キーで選択やスライダーの調整", "presets.editor_title": "プリセットを編集", "presets.delete_confirm": "「{0}」を削除しますか?すべてのドーナツメニューとグループからも削除されます。", "presets.name_required": "名前を入力してください", diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json index 377fda003..59b4bf87c 100644 --- a/src/Filee.App/Assets/i18n/ko.json +++ b/src/Filee.App/Assets/i18n/ko.json @@ -306,6 +306,8 @@ "preset.hide": "모자이크·흐림", "preset.hide.donut": "모자이크", "error.cannot_hide": "모자이크와 흐림은 이미지에만 할 수 있어요", + "error.background_picture": "배경에 넣을 사진을 읽을 수 없습니다", + "error.background_picture_changed": "배경에 넣을 사진이 표시된 뒤에 바뀌었습니다. 배경을 다시 추가하세요.", "hide.save": "가리고 저장", "hide.save_next": "가리고 다음으로", "hide.pixelate": "모자이크", @@ -422,6 +424,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "뒤집기", "preset.annotate": "주석", + "preset.background": "배경 추가", "preset.flip_horizontal": "좌우 반전", "preset.flip_horizontal.short": "좌우", "preset.flip_vertical": "상하 반전", @@ -1021,6 +1024,43 @@ "annotate.keys": "V 선택 · Tab 표시 고르기, 화살표 옮기기 · P A B H T N 도구 고르기 · Del 지우기 · Ctrl+Z 되돌리기 · Esc 닫기", "annotate.save": "저장", "annotate.save_next": "저장하고 다음", + "background.background": "배경", + "background.gradient": "그라데이션", + "background.color": "색", + "background.picture": "사진", + "background.from": "첫째 색", + "background.to": "둘째 색", + "background.choose": "고르기…", + "background.no_picture": "고른 사진 없음", + "background.padding": "여백", + "background.corners": "모서리", + "background.shadow": "그림자", + "background.shadow_none": "없음", + "background.shadow_light": "옅게", + "background.shadow_soft": "부드럽게", + "background.shadow_strong": "진하게", + "background.canvas": "캔버스", + "background.auto": "자동", + "background.square": "정사각형", + "background.landscape": "가로 (16:9)", + "background.portrait": "세로 (4:5)", + "background.same_look": "모든 사진에 같은 모양", + "background.save": "저장", + "background.save_next": "저장하고 다음", + "background.gradient_sunset": "노을", + "background.gradient_sky": "하늘", + "background.gradient_mint": "민트", + "background.gradient_peach": "복숭아", + "background.gradient_ocean": "바다", + "background.gradient_lilac": "라일락", + "background.color_white": "흰색", + "background.color_paper": "종이", + "background.color_charcoal": "차콜", + "background.color_navy": "네이비", + "background.color_sand": "모래", + "background.color_sage": "세이지", + "background.picture_unreadable": "이 사진을 읽을 수 없습니다. 다른 사진이나 그라데이션, 색을 고르세요.", + "background.keys": "Enter 저장 · Esc 닫기 · Tab으로 모양 항목 이동 · 화살표로 선택하거나 슬라이더 이동", "presets.editor_title": "프리셋 편집", "presets.delete_confirm": "“{0}”을(를) 삭제할까요? 모든 툴바와 묶음에서도 빠져요.", "presets.name_required": "이름을 입력하세요", diff --git a/src/Filee.App/Assets/i18n/ne.json b/src/Filee.App/Assets/i18n/ne.json index 6f461a67d..656c8010c 100644 --- a/src/Filee.App/Assets/i18n/ne.json +++ b/src/Filee.App/Assets/i18n/ne.json @@ -297,6 +297,8 @@ "preset.hide": "पिक्सलेट / धमिलो", "preset.hide.donut": "पिक्सलेट", "error.cannot_hide": "पिक्सलेट र धमिलो पार्ने कार्य केवल छविहरूमा मात्र काम गर्छ", + "error.background_picture": "पछाडि राख्ने तस्बिर पढ्न सकिँदैन", + "error.background_picture_changed": "पछाडि राख्ने तस्बिर देखाइएपछि बदलियो। पृष्ठभूमि फेरि थप्नुहोस्।", "hide.save": "लुकाएर सुरक्षित गर्नुहोस्", "hide.save_next": "लुकाएर अर्कोमा जानुहोस्", "hide.pixelate": "पिक्सलेट", @@ -412,6 +414,7 @@ "preset.rotate_180.short": "१८०°", "preset.group_flip": "उल्ट्याउनुहोस्", "preset.annotate": "टिप्पणी", + "preset.background": "पृष्ठभूमि थप्नुहोस्", "preset.flip_horizontal": "तेर्सो उल्ट्याउनुहोस्", "preset.flip_horizontal.short": "तेर्सो", "preset.flip_vertical": "ठाडो उल्ट्याउनुहोस्", @@ -1006,6 +1009,43 @@ "annotate.keys": "V छान्छ · Tab चिन्ह छान्छ, तीरले सार्छ · P A B H T N उपकरण छान्छन् · Del हटाउँछ · Ctrl+Z पूर्ववत · Esc बन्द गर्छ", "annotate.save": "सुरक्षित गर्नुहोस्", "annotate.save_next": "सुरक्षित गरेर अर्को", + "background.background": "पृष्ठभूमि", + "background.gradient": "ग्रेडियन्ट", + "background.color": "रङ", + "background.picture": "तस्बिर", + "background.from": "पहिलो रङ", + "background.to": "दोस्रो रङ", + "background.choose": "छान्नुहोस्…", + "background.no_picture": "कुनै तस्बिर छानिएको छैन", + "background.padding": "किनारा", + "background.corners": "कुना", + "background.shadow": "छाया", + "background.shadow_none": "छैन", + "background.shadow_light": "हल्का", + "background.shadow_soft": "नरम", + "background.shadow_strong": "गाढा", + "background.canvas": "क्यानभास", + "background.auto": "स्वतः", + "background.square": "वर्ग", + "background.landscape": "तेर्सो (16:9)", + "background.portrait": "ठाडो (4:5)", + "background.same_look": "सबै तस्बिरका लागि उस्तै रूप", + "background.save": "सुरक्षित गर्नुहोस्", + "background.save_next": "सुरक्षित गरेर अर्को", + "background.gradient_sunset": "सूर्यास्त", + "background.gradient_sky": "आकाश", + "background.gradient_mint": "पुदिना", + "background.gradient_peach": "आरु", + "background.gradient_ocean": "समुद्र", + "background.gradient_lilac": "लिलाक", + "background.color_white": "सेतो", + "background.color_paper": "कागजी", + "background.color_charcoal": "चारकोल", + "background.color_navy": "नेभी", + "background.color_sand": "बालुवा", + "background.color_sage": "सेज", + "background.picture_unreadable": "यो तस्बिर पढ्न सकिँदैन। अर्को तस्बिर, वा ग्रेडियन्ट वा रङ छान्नुहोस्।", + "background.keys": "Enter ले सेभ गर्छ · Esc ले बन्द गर्छ · Tab ले रूपका विकल्पहरूमा जान्छ · तीरले छान्छ वा स्लाइडर सार्छ", "presets.editor_title": "प्रिसेट सम्पादन गर्नुहोस्", "presets.delete_confirm": "“{0}” हटाउने? यो प्रत्येक टुलबार र समूहबाट पनि हटाइनेछ।", "presets.name_required": "नाम प्रविष्ट गर्नुहोस्", diff --git a/src/Filee.App/Assets/i18n/pl.json b/src/Filee.App/Assets/i18n/pl.json index 51fe0e3bc..2412a143f 100644 --- a/src/Filee.App/Assets/i18n/pl.json +++ b/src/Filee.App/Assets/i18n/pl.json @@ -306,6 +306,8 @@ "preset.hide": "Pikselizacja / rozmycie", "preset.hide.donut": "Mozaika", "error.cannot_hide": "Pikselizować i rozmywać można tylko obrazy", + "error.background_picture": "Nie można odczytać obrazu do umieszczenia w tle", + "error.background_picture_changed": "Obraz do umieszczenia w tle zmienił się po wyświetleniu. Dodaj tło ponownie.", "hide.save": "Ukryj i zapisz", "hide.save_next": "Ukryj i dalej", "hide.pixelate": "Pikselizacja", @@ -422,6 +424,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Odbij", "preset.annotate": "Adnotacje", + "preset.background": "Dodaj tło", "preset.flip_horizontal": "Odbij w poziomie", "preset.flip_horizontal.short": "Poziomo", "preset.flip_vertical": "Odbij w pionie", @@ -1021,6 +1024,43 @@ "annotate.keys": "V zaznacza · Tab wybiera znak, strzałki go przesuwają · P A B H T N wybierają narzędzie · Del usuwa · Ctrl+Z cofa · Esc zamyka", "annotate.save": "Zapisz", "annotate.save_next": "Zapisz i dalej", + "background.background": "Tło", + "background.gradient": "Gradient", + "background.color": "Kolor", + "background.picture": "Obraz", + "background.from": "Pierwszy kolor", + "background.to": "Drugi kolor", + "background.choose": "Wybierz…", + "background.no_picture": "Nie wybrano obrazu", + "background.padding": "Margines", + "background.corners": "Rogi", + "background.shadow": "Cień", + "background.shadow_none": "Brak", + "background.shadow_light": "Lekki", + "background.shadow_soft": "Miękki", + "background.shadow_strong": "Mocny", + "background.canvas": "Płótno", + "background.auto": "Auto", + "background.square": "Kwadrat", + "background.landscape": "Poziomo (16:9)", + "background.portrait": "Pionowo (4:5)", + "background.same_look": "Ten sam wygląd dla wszystkich obrazów", + "background.save": "Zapisz", + "background.save_next": "Zapisz i dalej", + "background.gradient_sunset": "Zachód słońca", + "background.gradient_sky": "Niebo", + "background.gradient_mint": "Mięta", + "background.gradient_peach": "Brzoskwinia", + "background.gradient_ocean": "Ocean", + "background.gradient_lilac": "Liliowy", + "background.color_white": "Biały", + "background.color_paper": "Papier", + "background.color_charcoal": "Grafit", + "background.color_navy": "Granat", + "background.color_sand": "Piasek", + "background.color_sage": "Szałwia", + "background.picture_unreadable": "Nie można odczytać tego obrazu. Wybierz inny albo gradient lub kolor.", + "background.keys": "Enter zapisuje · Esc zamyka · Tab przechodzi przez wygląd · strzałki wybierają lub przesuwają suwak", "presets.editor_title": "Edytuj ustawienie", "presets.delete_confirm": "Usunąć „{0}”? Zostanie też usunięte z każdego paska i grupy.", "presets.name_required": "Podaj nazwę", diff --git a/src/Filee.App/Assets/i18n/pt-BR.json b/src/Filee.App/Assets/i18n/pt-BR.json index d8dd45860..f5bdbb9fa 100644 --- a/src/Filee.App/Assets/i18n/pt-BR.json +++ b/src/Filee.App/Assets/i18n/pt-BR.json @@ -320,6 +320,8 @@ "preset.hide": "Pixelizar / desfocar", "preset.hide.donut": "Pixelizar", "error.cannot_hide": "Só é possível pixelizar ou desfocar imagens", + "error.background_picture": "Não é possível ler a imagem para o fundo", + "error.background_picture_changed": "A imagem para o fundo mudou depois de ser mostrada. Adicione o fundo de novo.", "hide.save": "Ocultar e salvar", "hide.save_next": "Ocultar e avançar", "hide.pixelate": "Pixelizar", @@ -422,6 +424,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Espelhar", "preset.annotate": "Anotar", + "preset.background": "Adicionar fundo", "preset.flip_horizontal": "Espelhar na horizontal", "preset.flip_horizontal.short": "Horizontal", "preset.flip_vertical": "Espelhar na vertical", @@ -1021,6 +1024,43 @@ "annotate.keys": "V seleciona · Tab escolhe uma marca, as setas a movem · P A B H T N escolhem a ferramenta · Del remove · Ctrl+Z desfaz · Esc fecha", "annotate.save": "Salvar", "annotate.save_next": "Salvar e próximo", + "background.background": "Fundo", + "background.gradient": "Degradê", + "background.color": "Cor", + "background.picture": "Imagem", + "background.from": "Primeira cor", + "background.to": "Segunda cor", + "background.choose": "Escolher…", + "background.no_picture": "Nenhuma imagem escolhida", + "background.padding": "Margem", + "background.corners": "Cantos", + "background.shadow": "Sombra", + "background.shadow_none": "Nenhuma", + "background.shadow_light": "Leve", + "background.shadow_soft": "Suave", + "background.shadow_strong": "Forte", + "background.canvas": "Tela", + "background.auto": "Auto", + "background.square": "Quadrado", + "background.landscape": "Paisagem (16:9)", + "background.portrait": "Retrato (4:5)", + "background.same_look": "Mesmo visual para todas as imagens", + "background.save": "Salvar", + "background.save_next": "Salvar e próximo", + "background.gradient_sunset": "Pôr do sol", + "background.gradient_sky": "Céu", + "background.gradient_mint": "Menta", + "background.gradient_peach": "Pêssego", + "background.gradient_ocean": "Oceano", + "background.gradient_lilac": "Lilás", + "background.color_white": "Branco", + "background.color_paper": "Papel", + "background.color_charcoal": "Carvão", + "background.color_navy": "Azul-marinho", + "background.color_sand": "Areia", + "background.color_sage": "Sálvia", + "background.picture_unreadable": "Não é possível ler esta imagem. Escolha outra, ou um degradê ou uma cor.", + "background.keys": "Enter salva · Esc fecha · Tab percorre o visual · as setas escolhem ou movem um controle deslizante", "presets.editor_title": "Editar predefinição", "presets.delete_confirm": "Excluir “{0}”? Também será removida de todo menu e grupo.", "presets.name_required": "Digite um nome", diff --git a/src/Filee.App/Assets/i18n/pt-PT.json b/src/Filee.App/Assets/i18n/pt-PT.json index 004be1cc8..56fb31491 100644 --- a/src/Filee.App/Assets/i18n/pt-PT.json +++ b/src/Filee.App/Assets/i18n/pt-PT.json @@ -306,6 +306,8 @@ "preset.hide": "Pixelizar / desfocar", "preset.hide.donut": "Pixelizar", "error.cannot_hide": "Só é possível pixelizar ou desfocar imagens", + "error.background_picture": "Não é possível ler a imagem para o fundo", + "error.background_picture_changed": "A imagem para o fundo mudou depois de ser mostrada. Adicione o fundo novamente.", "hide.save": "Ocultar e guardar", "hide.save_next": "Ocultar e avançar", "hide.pixelate": "Pixelizar", @@ -422,6 +424,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Inverter", "preset.annotate": "Anotar", + "preset.background": "Adicionar fundo", "preset.flip_horizontal": "Inverter na horizontal", "preset.flip_horizontal.short": "Horizontal", "preset.flip_vertical": "Inverter na vertical", @@ -1021,6 +1024,43 @@ "annotate.keys": "V seleciona · Tab escolhe uma marca, as setas movem-na · P A B H T N escolhem a ferramenta · Del remove · Ctrl+Z anula · Esc fecha", "annotate.save": "Guardar", "annotate.save_next": "Guardar e seguinte", + "background.background": "Fundo", + "background.gradient": "Gradiente", + "background.color": "Cor", + "background.picture": "Imagem", + "background.from": "Primeira cor", + "background.to": "Segunda cor", + "background.choose": "Escolher…", + "background.no_picture": "Nenhuma imagem escolhida", + "background.padding": "Margem", + "background.corners": "Cantos", + "background.shadow": "Sombra", + "background.shadow_none": "Nenhuma", + "background.shadow_light": "Leve", + "background.shadow_soft": "Suave", + "background.shadow_strong": "Forte", + "background.canvas": "Tela", + "background.auto": "Auto", + "background.square": "Quadrado", + "background.landscape": "Paisagem (16:9)", + "background.portrait": "Retrato (4:5)", + "background.same_look": "Mesmo aspeto para todas as imagens", + "background.save": "Guardar", + "background.save_next": "Guardar e seguinte", + "background.gradient_sunset": "Pôr do sol", + "background.gradient_sky": "Céu", + "background.gradient_mint": "Menta", + "background.gradient_peach": "Pêssego", + "background.gradient_ocean": "Oceano", + "background.gradient_lilac": "Lilás", + "background.color_white": "Branco", + "background.color_paper": "Papel", + "background.color_charcoal": "Carvão", + "background.color_navy": "Azul-marinho", + "background.color_sand": "Areia", + "background.color_sage": "Salva", + "background.picture_unreadable": "Não é possível ler esta imagem. Escolha outra, ou um gradiente ou uma cor.", + "background.keys": "Enter guarda · Esc fecha · Tab percorre o aspeto · as setas escolhem ou movem um cursor", "presets.editor_title": "Editar predefinição", "presets.delete_confirm": "Eliminar «{0}»? Também será removida de todas as barras e grupos.", "presets.name_required": "Introduz um nome", diff --git a/src/Filee.App/Assets/i18n/ru.json b/src/Filee.App/Assets/i18n/ru.json index a91f89e6c..2c701edb3 100644 --- a/src/Filee.App/Assets/i18n/ru.json +++ b/src/Filee.App/Assets/i18n/ru.json @@ -306,6 +306,8 @@ "preset.hide": "Пикселизация / размытие", "preset.hide.donut": "Мозаика", "error.cannot_hide": "Пикселизировать и размывать можно только изображения", + "error.background_picture": "Не удаётся прочитать изображение для фона", + "error.background_picture_changed": "Изображение для фона изменилось после показа. Добавьте фон ещё раз.", "hide.save": "Скрыть и сохранить", "hide.save_next": "Скрыть и дальше", "hide.pixelate": "Пикселизация", @@ -422,6 +424,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Отразить", "preset.annotate": "Пометки", + "preset.background": "Добавить фон", "preset.flip_horizontal": "Отразить по горизонтали", "preset.flip_horizontal.short": "По гориз.", "preset.flip_vertical": "Отразить по вертикали", @@ -1021,6 +1024,43 @@ "annotate.keys": "V выбор · Tab выбирает пометку, стрелки двигают · P A B H T N инструмент · Del удаляет · Ctrl+Z отменяет · Esc закрывает", "annotate.save": "Сохранить", "annotate.save_next": "Сохранить и далее", + "background.background": "Фон", + "background.gradient": "Градиент", + "background.color": "Цвет", + "background.picture": "Изображение", + "background.from": "Первый цвет", + "background.to": "Второй цвет", + "background.choose": "Выбрать…", + "background.no_picture": "Изображение не выбрано", + "background.padding": "Поля", + "background.corners": "Углы", + "background.shadow": "Тень", + "background.shadow_none": "Нет", + "background.shadow_light": "Лёгкая", + "background.shadow_soft": "Мягкая", + "background.shadow_strong": "Сильная", + "background.canvas": "Холст", + "background.auto": "Авто", + "background.square": "Квадрат", + "background.landscape": "Альбомный (16:9)", + "background.portrait": "Портретный (4:5)", + "background.same_look": "Одинаковый вид для всех изображений", + "background.save": "Сохранить", + "background.save_next": "Сохранить и далее", + "background.gradient_sunset": "Закат", + "background.gradient_sky": "Небо", + "background.gradient_mint": "Мята", + "background.gradient_peach": "Персик", + "background.gradient_ocean": "Океан", + "background.gradient_lilac": "Сирень", + "background.color_white": "Белый", + "background.color_paper": "Бумага", + "background.color_charcoal": "Графит", + "background.color_navy": "Тёмно-синий", + "background.color_sand": "Песок", + "background.color_sage": "Шалфей", + "background.picture_unreadable": "Не удаётся прочитать это изображение. Выберите другое, градиент или цвет.", + "background.keys": "Enter сохраняет · Esc закрывает · Tab переходит по настройкам вида · стрелки выбирают или двигают ползунок", "presets.editor_title": "Изменить пресет", "presets.delete_confirm": "Удалить «{0}»? Он также пропадёт из всех круговых меню и групп.", "presets.name_required": "Введите название", diff --git a/src/Filee.App/Assets/i18n/th.json b/src/Filee.App/Assets/i18n/th.json index 814019959..c16961f73 100644 --- a/src/Filee.App/Assets/i18n/th.json +++ b/src/Filee.App/Assets/i18n/th.json @@ -297,6 +297,8 @@ "preset.hide": "โมเสก / เบลอ", "preset.hide.donut": "โมเสก", "error.cannot_hide": "ใส่โมเสกหรือเบลอได้เฉพาะไฟล์รูปภาพเท่านั้น", + "error.background_picture": "อ่านรูปที่จะใช้เป็นพื้นหลังไม่ได้", + "error.background_picture_changed": "รูปที่จะใช้เป็นพื้นหลังเปลี่ยนไปหลังจากแสดงแล้ว เพิ่มพื้นหลังอีกครั้ง", "hide.save": "ซ่อนและบันทึก", "hide.save_next": "ซ่อนและต่อไป", "hide.pixelate": "โมเสก", @@ -412,6 +414,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "พลิกภาพ", "preset.annotate": "ใส่คำอธิบาย", + "preset.background": "เพิ่มพื้นหลัง", "preset.flip_horizontal": "พลิกแนวนอน", "preset.flip_horizontal.short": "แนวนอน", "preset.flip_vertical": "พลิกแนวตั้ง", @@ -1006,6 +1009,43 @@ "annotate.keys": "V เลือก · Tab เลือกเครื่องหมาย ลูกศรเลื่อน · P A B H T N เลือกเครื่องมือ · Del ลบ · Ctrl+Z เลิกทำ · Esc ปิด", "annotate.save": "บันทึก", "annotate.save_next": "บันทึกและถัดไป", + "background.background": "พื้นหลัง", + "background.gradient": "ไล่สี", + "background.color": "สี", + "background.picture": "รูปภาพ", + "background.from": "สีแรก", + "background.to": "สีที่สอง", + "background.choose": "เลือก…", + "background.no_picture": "ยังไม่ได้เลือกรูป", + "background.padding": "ระยะขอบ", + "background.corners": "มุม", + "background.shadow": "เงา", + "background.shadow_none": "ไม่มี", + "background.shadow_light": "อ่อน", + "background.shadow_soft": "นุ่ม", + "background.shadow_strong": "เข้ม", + "background.canvas": "ผืนผ้าใบ", + "background.auto": "อัตโนมัติ", + "background.square": "สี่เหลี่ยมจัตุรัส", + "background.landscape": "แนวนอน (16:9)", + "background.portrait": "แนวตั้ง (4:5)", + "background.same_look": "ใช้รูปแบบเดียวกันกับทุกรูป", + "background.save": "บันทึก", + "background.save_next": "บันทึกและถัดไป", + "background.gradient_sunset": "พระอาทิตย์ตก", + "background.gradient_sky": "ท้องฟ้า", + "background.gradient_mint": "มินต์", + "background.gradient_peach": "พีช", + "background.gradient_ocean": "มหาสมุทร", + "background.gradient_lilac": "ไลแลค", + "background.color_white": "ขาว", + "background.color_paper": "กระดาษ", + "background.color_charcoal": "ถ่าน", + "background.color_navy": "กรมท่า", + "background.color_sand": "ทราย", + "background.color_sage": "เสจ", + "background.picture_unreadable": "อ่านรูปนี้ไม่ได้ เลือกรูปอื่น หรือไล่สีหรือสีแทน", + "background.keys": "Enter บันทึก · Esc ปิด · Tab ไล่ไปตามตัวเลือกรูปแบบ · ลูกศรเลือกหรือเลื่อนแถบเลื่อน", "presets.editor_title": "แก้ไขพรีเซ็ต", "presets.delete_confirm": "ลบ “{0}” หรือไม่? พรีเซ็ตนี้จะถูกลบออกจากทุกแถบเครื่องมือและกลุ่มด้วย", "presets.name_required": "โปรดระบุชื่อ", diff --git a/src/Filee.App/Assets/i18n/tr.json b/src/Filee.App/Assets/i18n/tr.json index 307399a58..85be7052d 100644 --- a/src/Filee.App/Assets/i18n/tr.json +++ b/src/Filee.App/Assets/i18n/tr.json @@ -297,6 +297,8 @@ "preset.hide": "Pikselleştir / bulanıklaştır", "preset.hide.donut": "Mozaik", "error.cannot_hide": "Yalnızca görseller pikselleştirilebilir veya bulanıklaştırılabilir", + "error.background_picture": "Arka plana konacak resim okunamıyor", + "error.background_picture_changed": "Arka plana konacak resim gösterildikten sonra değişti. Arka planı yeniden ekleyin.", "hide.save": "Gizle ve kaydet", "hide.save_next": "Gizle ve ilerle", "hide.pixelate": "Pikselleştir", @@ -412,6 +414,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Çevir", "preset.annotate": "Not ekle", + "preset.background": "Arka plan ekle", "preset.flip_horizontal": "Yatay çevir", "preset.flip_horizontal.short": "Yatay", "preset.flip_vertical": "Dikey çevir", @@ -1006,6 +1009,43 @@ "annotate.keys": "V seçer · Tab işaret seçer, oklar taşır · P A B H T N araç seçer · Del kaldırır · Ctrl+Z geri alır · Esc kapatır", "annotate.save": "Kaydet", "annotate.save_next": "Kaydet ve sonraki", + "background.background": "Arka plan", + "background.gradient": "Degrade", + "background.color": "Renk", + "background.picture": "Resim", + "background.from": "Birinci renk", + "background.to": "İkinci renk", + "background.choose": "Seç…", + "background.no_picture": "Resim seçilmedi", + "background.padding": "Kenar boşluğu", + "background.corners": "Köşeler", + "background.shadow": "Gölge", + "background.shadow_none": "Yok", + "background.shadow_light": "Hafif", + "background.shadow_soft": "Yumuşak", + "background.shadow_strong": "Güçlü", + "background.canvas": "Tuval", + "background.auto": "Otomatik", + "background.square": "Kare", + "background.landscape": "Yatay (16:9)", + "background.portrait": "Dikey (4:5)", + "background.same_look": "Tüm resimler için aynı görünüm", + "background.save": "Kaydet", + "background.save_next": "Kaydet ve sonraki", + "background.gradient_sunset": "Gün batımı", + "background.gradient_sky": "Gökyüzü", + "background.gradient_mint": "Nane", + "background.gradient_peach": "Şeftali", + "background.gradient_ocean": "Okyanus", + "background.gradient_lilac": "Leylak", + "background.color_white": "Beyaz", + "background.color_paper": "Kâğıt", + "background.color_charcoal": "Antrasit", + "background.color_navy": "Lacivert", + "background.color_sand": "Kum", + "background.color_sage": "Adaçayı", + "background.picture_unreadable": "Bu resim okunamıyor. Başka bir resim ya da bir degrade veya renk seçin.", + "background.keys": "Enter kaydeder · Esc kapatır · Tab görünüm seçenekleri arasında gezer · oklar seçer veya kaydırıcıyı kaydırır", "presets.editor_title": "Ön ayarı düzenle", "presets.delete_confirm": "“{0}” silinsin mi? Tüm araç çubuklarından ve gruplardan da kaldırılır.", "presets.name_required": "Bir ad girin", diff --git a/src/Filee.App/Assets/i18n/vi.json b/src/Filee.App/Assets/i18n/vi.json index f619e0d39..7cdafa4c8 100644 --- a/src/Filee.App/Assets/i18n/vi.json +++ b/src/Filee.App/Assets/i18n/vi.json @@ -306,6 +306,8 @@ "preset.hide": "Pixel hóa / làm mờ", "preset.hide.donut": "Pixel hóa", "error.cannot_hide": "Chỉ ảnh mới có thể pixel hóa hoặc làm mờ", + "error.background_picture": "Không thể đọc ảnh đặt phía sau", + "error.background_picture_changed": "Ảnh đặt phía sau đã thay đổi sau khi được hiển thị. Hãy thêm nền lại.", "hide.save": "Che & lưu", "hide.save_next": "Che & tiếp", "hide.pixelate": "Pixel hóa", @@ -422,6 +424,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "Lật", "preset.annotate": "Chú thích", + "preset.background": "Thêm nền", "preset.flip_horizontal": "Lật ngang", "preset.flip_horizontal.short": "Ngang", "preset.flip_vertical": "Lật dọc", @@ -1021,6 +1024,43 @@ "annotate.keys": "V để chọn · Tab chọn một dấu, phím mũi tên di chuyển dấu · P A B H T N chọn công cụ · Del để gỡ · Ctrl+Z để hoàn tác · Esc để đóng", "annotate.save": "Lưu", "annotate.save_next": "Lưu & tiếp", + "background.background": "Nền", + "background.gradient": "Chuyển sắc", + "background.color": "Màu", + "background.picture": "Ảnh", + "background.from": "Màu thứ nhất", + "background.to": "Màu thứ hai", + "background.choose": "Chọn…", + "background.no_picture": "Chưa chọn ảnh", + "background.padding": "Khoảng đệm", + "background.corners": "Góc bo", + "background.shadow": "Bóng đổ", + "background.shadow_none": "Không có", + "background.shadow_light": "Nhẹ", + "background.shadow_soft": "Mềm", + "background.shadow_strong": "Đậm", + "background.canvas": "Khổ ảnh", + "background.auto": "Tự động", + "background.square": "Vuông", + "background.landscape": "Ngang (16:9)", + "background.portrait": "Dọc (4:5)", + "background.same_look": "Cùng một kiểu cho mọi ảnh", + "background.save": "Lưu", + "background.save_next": "Lưu & tiếp", + "background.gradient_sunset": "Hoàng hôn", + "background.gradient_sky": "Bầu trời", + "background.gradient_mint": "Bạc hà", + "background.gradient_peach": "Đào", + "background.gradient_ocean": "Đại dương", + "background.gradient_lilac": "Tử đinh hương", + "background.color_white": "Trắng", + "background.color_paper": "Giấy", + "background.color_charcoal": "Than chì", + "background.color_navy": "Xanh hải quân", + "background.color_sand": "Cát", + "background.color_sage": "Xanh xô thơm", + "background.picture_unreadable": "Không thể đọc ảnh này. Hãy chọn ảnh khác, hoặc một chuyển sắc hay một màu.", + "background.keys": "Enter để lưu · Esc để đóng · Tab đi qua các tùy chọn kiểu · phím mũi tên để chọn hoặc di chuyển thanh trượt", "presets.editor_title": "Sửa cấu hình sẵn", "presets.delete_confirm": "Xóa “{0}”? Cấu hình sẵn này cũng sẽ bị gỡ khỏi mọi thanh công cụ và nhóm.", "presets.name_required": "Nhập tên", diff --git a/src/Filee.App/Assets/i18n/zh-CN.json b/src/Filee.App/Assets/i18n/zh-CN.json index 60a442c7f..3575d4b2e 100644 --- a/src/Filee.App/Assets/i18n/zh-CN.json +++ b/src/Filee.App/Assets/i18n/zh-CN.json @@ -306,6 +306,8 @@ "preset.hide": "马赛克/模糊", "preset.hide.donut": "马赛克", "error.cannot_hide": "只能对图片打马赛克或模糊", + "error.background_picture": "无法读取用作背景的图片", + "error.background_picture_changed": "用作背景的图片在显示后已更改。请重新添加背景。", "hide.save": "遮盖并保存", "hide.save_next": "遮盖并下一张", "hide.pixelate": "马赛克", @@ -422,6 +424,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "翻转", "preset.annotate": "批注", + "preset.background": "添加背景", "preset.flip_horizontal": "水平翻转", "preset.flip_horizontal.short": "水平", "preset.flip_vertical": "垂直翻转", @@ -1021,6 +1024,43 @@ "annotate.keys": "V 选择 · Tab 选择标记,方向键移动 · P A B H T N 选择工具 · Del 删除 · Ctrl+Z 撤销 · Esc 关闭", "annotate.save": "保存", "annotate.save_next": "保存并下一个", + "background.background": "背景", + "background.gradient": "渐变", + "background.color": "颜色", + "background.picture": "图片", + "background.from": "第一个颜色", + "background.to": "第二个颜色", + "background.choose": "选择…", + "background.no_picture": "未选择图片", + "background.padding": "边距", + "background.corners": "圆角", + "background.shadow": "阴影", + "background.shadow_none": "无", + "background.shadow_light": "浅", + "background.shadow_soft": "柔和", + "background.shadow_strong": "深", + "background.canvas": "画布", + "background.auto": "自动", + "background.square": "正方形", + "background.landscape": "横向 (16:9)", + "background.portrait": "纵向 (4:5)", + "background.same_look": "所有图片使用相同外观", + "background.save": "保存", + "background.save_next": "保存并下一个", + "background.gradient_sunset": "日落", + "background.gradient_sky": "天空", + "background.gradient_mint": "薄荷", + "background.gradient_peach": "蜜桃", + "background.gradient_ocean": "海洋", + "background.gradient_lilac": "丁香", + "background.color_white": "白色", + "background.color_paper": "纸白", + "background.color_charcoal": "炭灰", + "background.color_navy": "藏青", + "background.color_sand": "沙色", + "background.color_sage": "鼠尾草绿", + "background.picture_unreadable": "无法读取这张图片。请选择另一张图片,或渐变、颜色。", + "background.keys": "Enter 保存 · Esc 关闭 · Tab 在外观选项间移动 · 方向键选择或移动滑块", "presets.editor_title": "编辑预设", "presets.delete_confirm": "删除“{0}”?它也会从所有工具栏和组中移除。", "presets.name_required": "请输入名称", diff --git a/src/Filee.App/Assets/i18n/zh-TW.json b/src/Filee.App/Assets/i18n/zh-TW.json index 5357f4cff..b907b11de 100644 --- a/src/Filee.App/Assets/i18n/zh-TW.json +++ b/src/Filee.App/Assets/i18n/zh-TW.json @@ -306,6 +306,8 @@ "preset.hide": "馬賽克/模糊", "preset.hide.donut": "馬賽克", "error.cannot_hide": "只能對圖片加上馬賽克或模糊", + "error.background_picture": "無法讀取要放在背景的圖片", + "error.background_picture_changed": "要放在背景的圖片在顯示後已變更。請重新加入背景。", "hide.save": "遮蓋並儲存", "hide.save_next": "遮蓋並下一張", "hide.pixelate": "馬賽克", @@ -422,6 +424,7 @@ "preset.rotate_180.short": "180°", "preset.group_flip": "翻轉", "preset.annotate": "註解", + "preset.background": "加入背景", "preset.flip_horizontal": "水平翻轉", "preset.flip_horizontal.short": "水平", "preset.flip_vertical": "垂直翻轉", @@ -1021,6 +1024,43 @@ "annotate.keys": "V 選取 · Tab 選取標記,方向鍵移動 · P A B H T N 選擇工具 · Del 移除 · Ctrl+Z 復原 · Esc 關閉", "annotate.save": "儲存", "annotate.save_next": "儲存並下一個", + "background.background": "背景", + "background.gradient": "漸層", + "background.color": "顏色", + "background.picture": "圖片", + "background.from": "第一個顏色", + "background.to": "第二個顏色", + "background.choose": "選擇…", + "background.no_picture": "未選擇圖片", + "background.padding": "邊距", + "background.corners": "圓角", + "background.shadow": "陰影", + "background.shadow_none": "無", + "background.shadow_light": "淺", + "background.shadow_soft": "柔和", + "background.shadow_strong": "深", + "background.canvas": "畫布", + "background.auto": "自動", + "background.square": "正方形", + "background.landscape": "橫向 (16:9)", + "background.portrait": "直向 (4:5)", + "background.same_look": "所有圖片使用相同外觀", + "background.save": "儲存", + "background.save_next": "儲存並下一個", + "background.gradient_sunset": "日落", + "background.gradient_sky": "天空", + "background.gradient_mint": "薄荷", + "background.gradient_peach": "蜜桃", + "background.gradient_ocean": "海洋", + "background.gradient_lilac": "丁香", + "background.color_white": "白色", + "background.color_paper": "紙白", + "background.color_charcoal": "炭灰", + "background.color_navy": "藏青", + "background.color_sand": "沙色", + "background.color_sage": "鼠尾草綠", + "background.picture_unreadable": "無法讀取這張圖片。請選擇另一張圖片,或漸層、顏色。", + "background.keys": "Enter 儲存 · Esc 關閉 · Tab 在外觀選項間移動 · 方向鍵選擇或移動滑桿", "presets.editor_title": "編輯預設", "presets.delete_confirm": "刪除「{0}」?它也會從所有工具列和群組中移除。", "presets.name_required": "請輸入名稱", diff --git a/src/Filee.App/Services/PresetAvailability.cs b/src/Filee.App/Services/PresetAvailability.cs index 1422d32b8..32d17b141 100644 --- a/src/Filee.App/Services/PresetAvailability.cs +++ b/src/Filee.App/Services/PresetAvailability.cs @@ -137,6 +137,10 @@ private PresetJudgement Evaluate(Preset preset, IReadOnlyList format return formats.Any(f => HideService.CanOpen(f.Id)) ? new(PresetUse.Usable, null) : new(PresetUse.Unusable, _loc["error.images_only"]); + if (preset.IsBackgroundTool) + return formats.Any(f => HideService.CanOpen(f.Id)) + ? new(PresetUse.Usable, null) + : new(PresetUse.Unusable, _loc["error.images_only"]); if (preset.IsHideTool) return formats.Any(f => HideService.CanOpen(f.Id)) ? new(PresetUse.Usable, null) diff --git a/src/Filee.App/Services/WindowService.cs b/src/Filee.App/Services/WindowService.cs index 0809059ce..5c48e9734 100644 --- a/src/Filee.App/Services/WindowService.cs +++ b/src/Filee.App/Services/WindowService.cs @@ -38,11 +38,13 @@ public sealed class WindowService(ILocalizer loc, IServiceProvider services, Mem private ClipWindow? _clipWindow; private HideWindow? _hideWindow; private AnnotateWindow? _annotateWindow; + private BackgroundWindow? _backgroundWindow; private ImageEditWindow? _imageEdit; internal ClipWindow? ClipWindow => _clipWindow; internal HideWindow? HideWindow => _hideWindow; internal AnnotateWindow? AnnotateWindow => _annotateWindow; + internal BackgroundWindow? BackgroundWindow => _backgroundWindow; internal ImageEditWindow? ImageEdit => _imageEdit; private readonly HashSet _spreadsheetStudios = []; @@ -156,6 +158,8 @@ public void ShowTool(IReadOnlyList files, Preset preset, PixelPoint? nea ShowHideWindow(files, preset, near); else if (preset.IsAnnotateTool) ShowAnnotateWindow(files, preset); + else if (preset.IsBackgroundTool) + ShowBackgroundWindow(files, preset); else if (preset.IsImageEditTool) ShowImageEditWindow(files, preset); // Before the crop: the print studio and the PDF page tools have no Tool of their own, which reads as a crop @@ -294,6 +298,32 @@ public void ShowAnnotateWindow(IReadOnlyList files, Preset preset) win.Activate(); } + /// + /// Opens the "Add background" window on the pictures among the files. Each picture is written by an ordinary + /// conversion, shown in the toast. + /// + public void ShowBackgroundWindow(IReadOnlyList files, Preset preset) + { + var pictures = files.Where(f => FormatRegistry.Detect(f) is { } format && HideService.CanOpen(format.Id)).ToList(); + if (pictures.Count == 0) + return; + _backgroundWindow?.Close(); + var conversions = services.GetRequiredService(); + var vm = new BackgroundViewModel(pictures, preset, loc, services.GetRequiredService(), + (path, conversion) => conversions.Start([path], conversion), services.GetService>()); + var win = new BackgroundWindow { DataContext = vm }; + PrepareWindow(win); + win.Closed += (_, _) => + { + if (_backgroundWindow == win) + _backgroundWindow = null; + trimmer.Request(); + }; + _backgroundWindow = win; + win.Show(); + win.Activate(); + } + /// /// The pixel size of a picture as it is seen, read from its headers: turned as its EXIF orientation says, and the /// largest of an icon's sizes (the one converted). Null when ImageMagick can't read it. diff --git a/src/Filee.App/ViewModels/BackgroundViewModel.cs b/src/Filee.App/ViewModels/BackgroundViewModel.cs new file mode 100644 index 000000000..bc17f98da --- /dev/null +++ b/src/Filee.App/ViewModels/BackgroundViewModel.cs @@ -0,0 +1,767 @@ +// State of the "Add background" window: one picture at a time, shown set on the look as the file will be, drawn live +// by the window (the canvas, background, rounded corners and shadow from the same layout and shadow formulas the file +// is composed with, and the picture and the one behind it in the colours the file gets, so moving a slider shows at +// once); the look (background, padding, corners, shadow, canvas), one for all pictures or one each; and the conversion +// that writes each picture, which runs in the toast like any other. The look is kept in the tool for the next time. + +using System.Collections.ObjectModel; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Filee.App.Services; +using Filee.Core.Localization; +using Filee.Core.Presets; +using Filee.Core.Settings; +using Filee.Engines.Magick; +using Microsoft.Extensions.Logging; + +namespace Filee.App.ViewModels; + +public sealed partial class BackgroundViewModel : ObservableObject, IDisposable +{ + /// + /// The gradients offered first (top left → bottom right), with what a screen reader says for each. + /// + public static readonly (string From, string To, string NameKey)[] GradientPresets = + [ + ("#F6B26B", "#E06C9F", "background.gradient_sunset"), ("#8EC5FC", "#E0C3FC", "background.gradient_sky"), + ("#43E97B", "#38F9D7", "background.gradient_mint"), ("#FA709A", "#FEE140", "background.gradient_peach"), + ("#30CFD0", "#330867", "background.gradient_ocean"), ("#A18CD1", "#FBC2EB", "background.gradient_lilac"), + ]; + + /// The colours offered first, with what a screen reader says for each. + public static readonly (string Color, string NameKey)[] ColorPresets = + [ + ("#FFFFFF", "background.color_white"), ("#F2F0EB", "background.color_paper"), ("#1E1E1E", "background.color_charcoal"), + ("#2D3A8C", "background.color_navy"), ("#E8D5C4", "background.color_sand"), ("#C9E4DE", "background.color_sage"), + ]; + + /// The preview of a picture behind is read as large as it takes to cover a square this wide. + private const int PictureBehindSide = 1600; + + /// The preview of a picture behind is at most this many pixels along its longer side. + private const int PictureBehindLongest = 4096; + + private readonly IReadOnlyList _files; + private readonly Preset _tool; + private readonly ILocalizer _loc; + private readonly UserDataStore _store; + private readonly Action _export; + private readonly ILogger? _log; + private readonly BackdropOptions[] _looks; + private int _originalWidth, _originalHeight; + private bool _closed; + /// + /// The pictures behind read so far (null: can't be read), with their files' stamps when they were read. + /// + private readonly Dictionary _backgroundPictures = []; + private bool _syncing; + + /// How each picture went: saved, skipped or failed (none yet while it is open). + private readonly CropItemState?[] _outcomes; + + /// The picture each of is about. + private readonly List _problemFiles = []; + + public BackgroundViewModel(IReadOnlyList files, Preset tool, ILocalizer loc, UserDataStore store, + Action export, ILogger? log = null) + { + _files = files; + _tool = tool; + _loc = loc; + _store = store; + _export = export; + _log = log; + var look = BackgroundTool.LookOf(tool); + _remembered = tool.Image.BackdropLook is null ? null : look.Clone(); + _looks = [.. files.Select(_ => look.Clone())]; + _outcomes = new CropItemState?[files.Count]; + _hasMultipleFiles = files.Count > 1; + Gradients = [.. GradientPresets.Select(g => new BackdropSwatch(g.From, g.To, g.NameKey, this) { Name = loc[g.NameKey] })]; + Colors = [.. ColorPresets.Select(c => new BackdropSwatch(c.Color, c.Color, c.NameKey, this) { Name = loc[c.NameKey] })]; + loc.LanguageChanged += OnLanguageChanged; + ShowLook(); + _ = LoadFileAsync(0); + } + + public IReadOnlyList Gradients { get; } + public IReadOnlyList Colors { get; } + + [ObservableProperty] private int _currentIndex; + [ObservableProperty] private string _currentFileName = ""; + [ObservableProperty] private string _counterText = ""; + [ObservableProperty] private bool _hasMultipleFiles; + + /// + /// The picture itself, preview-sized, in the colours the file gets: the window sets it on the look live. + /// + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveCommand))] + private Bitmap? _pictureImage; + + /// The preview's canvas, in units of a canvas long. + [ObservableProperty] private double _canvasWidth = PreviewSide; + [ObservableProperty] private double _canvasHeight = PreviewSide; + [ObservableProperty] private Avalonia.Thickness _pictureMargin; + [ObservableProperty] private double _pictureWidth; + [ObservableProperty] private double _pictureHeight; + [ObservableProperty] private Avalonia.CornerRadius _pictureCorners; + [ObservableProperty] private IBrush? _backgroundBrush; + + /// The picture's shadow as Avalonia draws it (null for none): the same as the file's. + [ObservableProperty] private IEffect? _shadowEffect; + [ObservableProperty] private string _sizeText = ""; + [ObservableProperty] private string _statusNotice = ""; + + /// + /// The picture to put behind can't be read: the gradient shows meanwhile, and Save waits for another picture or + /// another background (the file would be refused). + /// + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveCommand))] + private bool _pictureUnreadable; + + /// + /// What the window says while the picture behind can't be read ("" otherwise): a text that appears is what a + /// screen reader announces, where a text only shown wasn't. + /// + [ObservableProperty] private string _pictureNotice = ""; + + partial void OnPictureUnreadableChanged(bool value) => PictureNotice = value ? _loc["background.picture_unreadable"] : ""; + + /// + /// All pictures are done and some could not be opened or saved: the window stays open on and + /// until it is closed (as "Pixelate / blur" does). + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsEditing))] + private bool _isFinished; + + [ObservableProperty] private string _summary = ""; + [ObservableProperty] private int _doneCount; + [ObservableProperty] private int _skippedCount; + [ObservableProperty] private int _failedCount; + + /// "file: reason" for each picture that could not be opened or saved. + public ObservableCollection Problems { get; } = []; + + public bool IsEditing => !IsFinished; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveCommand))] + private bool _isBusy; + + /// One look for every picture (on), or one each. + [ObservableProperty] private bool _sameLook = true; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsGradient), nameof(IsColor), nameof(IsPicture))] + [NotifyCanExecuteChangedFor(nameof(SaveCommand))] + private BackdropKind _kind; + + [ObservableProperty] private Color _fromColor; + [ObservableProperty] private Color _toColor; + [ObservableProperty] private Color _fillColor; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(PictureName), nameof(HasPicture))] + [NotifyCanExecuteChangedFor(nameof(SaveCommand))] + private string? _picture; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(PaddingText))] + private int _padding; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CornersText))] + private int _corners; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShadowText))] + private int _shadow; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsAuto), nameof(IsSquare), nameof(IsLandscape), nameof(IsPortrait))] + private BackdropCanvas _canvas; + + public bool IsGradient => Kind == BackdropKind.Gradient; + public bool IsColor => Kind == BackdropKind.Color; + public bool IsPicture => Kind == BackdropKind.Picture; + public bool IsAuto => Canvas == BackdropCanvas.Auto; + public bool IsSquare => Canvas == BackdropCanvas.Square; + public bool IsLandscape => Canvas == BackdropCanvas.Landscape; + public bool IsPortrait => Canvas == BackdropCanvas.Portrait; + public bool HasPicture => !string.IsNullOrEmpty(Picture); + public string PictureName => HasPicture ? Path.GetFileName(Picture!) : _loc["background.no_picture"]; + + public string PaddingText => $"{Padding} %"; + + /// The corner radius in pixels of the file, as it will be. + public string CornersText => _originalWidth == 0 ? $"{Corners} %" : $"{FileLayout().Radius} px"; + + public string ShadowText => Shadow == 0 ? _loc["background.shadow_none"] + : Shadow < 40 ? _loc["background.shadow_light"] : Shadow < 75 ? _loc["background.shadow_soft"] : _loc["background.shadow_strong"]; + + public string SaveText => _loc[CurrentIndex < _files.Count - 1 ? "background.save_next" : "background.save"]; + + public int MaxPadding => BackdropOptions.MaxPadding; + public int MaxCorners => BackdropOptions.MaxCorners; + public int MaxShadow => BackdropOptions.MaxShadow; + + /// The look of the picture shown. + internal BackdropOptions Look => _looks[CurrentIndex]; + + public event Action? CloseRequested; + + /// The name of the pictures the picker offers. + public string PicturesFilterName => _loc["profile.images"]; + + /// Asks for a picture to put behind (the window's file picker); null when none was chosen. + public Func>? PickPicture { get; set; } + + // ───────── The look ───────── + + /// Shows the current picture's look in the controls. + private void ShowLook() + { + _syncing = true; + var look = _looks[CurrentIndex]; + Kind = look.Kind; + FromColor = ParseColor(look.From); + ToColor = ParseColor(look.To); + FillColor = ParseColor(look.Color); + Picture = look.Picture; + Padding = look.Padding; + Corners = look.Corners; + Shadow = look.Shadow; + Canvas = look.Canvas; + _syncing = false; + UpdateSwatches(); + } + + /// + /// Writes a change into the look of the picture shown, or of every picture, and draws the preview again. + /// + private void Change(Action change) + { + if (_syncing) + return; + foreach (var look in SameLook ? _looks : [_looks[CurrentIndex]]) + change(look); + UpdateSwatches(); + OnPropertyChanged(nameof(CornersText)); + ShowPreview(); + } + + partial void OnKindChanged(BackdropKind value) => Change(look => look.Kind = value); + partial void OnFromColorChanged(Color value) => Change(look => look.From = Hex(value)); + partial void OnToColorChanged(Color value) => Change(look => look.To = Hex(value)); + partial void OnFillColorChanged(Color value) => Change(look => look.Color = Hex(value)); + partial void OnPictureChanged(string? value) => Change(look => look.Picture = value); + partial void OnPaddingChanged(int value) => Change(look => look.Padding = value); + partial void OnCornersChanged(int value) => Change(look => look.Corners = value); + partial void OnShadowChanged(int value) => Change(look => look.Shadow = value); + partial void OnCanvasChanged(BackdropCanvas value) => Change(look => look.Canvas = value); + + /// Turning "same look" on gives every picture the look of the one shown. + partial void OnSameLookChanged(bool value) + { + if (!value) + return; + for (var i = 0; i < _looks.Length; i++) + _looks[i] = _looks[CurrentIndex].Clone(); + } + + [RelayCommand] + private void SetKind(BackdropKind kind) => Kind = kind; + + [RelayCommand] + private void SetCanvas(BackdropCanvas canvas) => Canvas = canvas; + + /// A gradient or colour swatch was chosen. + internal void Choose(BackdropSwatch swatch) + { + if (Gradients.Contains(swatch)) + { + Kind = BackdropKind.Gradient; + FromColor = ParseColor(swatch.From); + ToColor = ParseColor(swatch.To); + } + else + { + Kind = BackdropKind.Color; + FillColor = ParseColor(swatch.From); + } + } + + [RelayCommand] + private async Task ChoosePictureAsync() + { + if (PickPicture is not null && await PickPicture() is { } path) + { + // Picked again (fixed, say, since it couldn't be read): read again. + Forget(path); + Picture = path; + Kind = BackdropKind.Picture; + ShowPreview(); + } + } + + private void UpdateSwatches() + { + var look = _looks[CurrentIndex]; + foreach (var swatch in Gradients) + swatch.IsSelected = look.Kind == BackdropKind.Gradient && Same(swatch.From, look.From) && Same(swatch.To, look.To); + foreach (var swatch in Colors) + swatch.IsSelected = look.Kind == BackdropKind.Color && Same(swatch.From, look.Color); + } + + private static bool Same(string a, string b) => string.Equals(a.TrimStart('#'), b.TrimStart('#'), StringComparison.OrdinalIgnoreCase); + + internal static string Hex(Color color) => $"#{color.R:X2}{color.G:X2}{color.B:X2}"; + + private static Color ParseColor(string? text) => Color.TryParse(text, out var color) ? color : Avalonia.Media.Colors.White; + + // ───────── Pictures and the preview ───────── + + /// + /// Opens picture ; past the last one, closes, or shows the summary when a picture could not + /// be opened or saved. + /// + public async Task LoadFileAsync(int index) + { + if (_closed) + return; + if (index >= _files.Count) + { + Finish(); + return; + } + IsBusy = true; + CurrentIndex = index; + var path = _files[index]; + CurrentFileName = Path.GetFileName(path); + CounterText = _loc.Format("crop.page_counter", index + 1, _files.Count); + OnPropertyChanged(nameof(SaveText)); + ShowLook(); + try + { + var (preview, width, height) = await HideService.LoadPreviewAsync(path, _tool); + Bitmap bitmap; + using (preview) + { + bitmap = await Task.Run(() => + { + // In the colours the file gets: a CMYK, greyscale or wide-gamut picture is brought to sRGB as seen. + BackdropComposer.ToSrgb(preview); + return HideService.ToBitmap(preview); + }); + } + if (_closed) + { + bitmap.Dispose(); + return; + } + var old = PictureImage; + (_originalWidth, _originalHeight) = (width, height); + PictureImage = bitmap; + old?.Dispose(); + OnPropertyChanged(nameof(CornersText)); + ShowPreview(); + } + catch (Exception ex) when (ex is not OutOfMemoryException) + { + _log?.LogInformation(ex, "Could not open {File} to add a background", CurrentFileName); + AddProblem(index, ex is NotSupportedException ? "error.unsupported_source" : "error.conversion_failed"); + IsBusy = false; + await LoadFileAsync(index + 1); + return; + } + IsBusy = false; + } + + /// The preview canvas's longer side, in the units the window scales to fit. + internal const double PreviewSide = 1000; + + /// + /// Where everything goes in the file: the picture's size on its canvas (smaller when the canvas would be over the + /// pixel limit), laid out as the file is composed. + /// + private BackdropComposer.Layout FileLayout() + { + var look = Look.Clamped(); + var (width, height) = BackdropComposer.PictureSizeOn(_originalWidth, _originalHeight, look); + return BackdropComposer.LayoutOf(width, height, look); + } + + /// + /// Lays the preview out for the look: the canvas, where the picture goes, its corners and its shadow, from the same + /// layout and shadow formulas the file is composed with (BackdropComposer), scaled to . + /// + private void ShowPreview() + { + if (_originalWidth == 0 || _closed) + return; + var look = Look.Clamped(); + var layout = FileLayout(); + var (pictureWidth, pictureHeight) = BackdropComposer.PictureSizeOn(_originalWidth, _originalHeight, look); + var scale = PreviewSide / Math.Max(layout.Width, layout.Height); + CanvasWidth = layout.Width * scale; + CanvasHeight = layout.Height * scale; + PictureMargin = new Avalonia.Thickness(layout.X * scale, layout.Y * scale, 0, 0); + PictureWidth = pictureWidth * scale; + PictureHeight = pictureHeight * scale; + PictureCorners = new Avalonia.CornerRadius(Math.Min(layout.Radius, Math.Min(pictureWidth, pictureHeight) / 2) * scale); + if (look.Shadow == 0) + ShadowEffect = null; + else + { + var shape = BackdropComposer.ShadowOf(pictureWidth, pictureHeight, look.Shadow); + // Avalonia (Skia) turns a blur radius into a sigma of 0.288675 × radius + 0.5. + ShadowEffect = new DropShadowEffect + { + BlurRadius = Math.Max(0, (shape.Sigma * scale - 0.5) / 0.288675), + OffsetX = 0, + OffsetY = shape.Drop * scale, + Color = Avalonia.Media.Colors.Black, + Opacity = shape.Opacity, + }; + } + BackgroundBrush = BrushOf(look); + // Off the screen now (as the picture itself is disposed once replaced). + foreach (var bitmap in _retired) + bitmap.Dispose(); + _retired.Clear(); + SizeText = $"{layout.Width} × {layout.Height} px · PNG"; + // A redraw can start reading a picture behind, which Save waits for. + SaveCommand.NotifyCanExecuteChanged(); + } + + /// + /// The background as a brush: the gradient corner to corner, the colour, or one's own picture covering. + /// + private IBrush BrushOf(BackdropOptions look) + { + // Worked out once: set false then true again on every redraw, the notice blinked (and a screen reader could + // say it again on every slider step). + var unreadable = false; + var picture = look.Kind == BackdropKind.Picture ? BackgroundPicture(look.Picture, out unreadable) : null; + PictureUnreadable = unreadable; + if (look.Kind == BackdropKind.Color) + return new SolidColorBrush(ParseColor(look.Color)); + if (picture is not null) + return new ImageBrush(picture) { Stretch = Stretch.UniformToFill }; + // While it is read: white, as under its see-through parts, rather than a look not chosen. + if (look.Kind == BackdropKind.Picture && look.Picture is { } reading && _reading.Contains(reading)) + return new SolidColorBrush(Avalonia.Media.Colors.White); + return new LinearGradientBrush + { + StartPoint = new Avalonia.RelativePoint(0, 0, Avalonia.RelativeUnit.Relative), + EndPoint = new Avalonia.RelativePoint(1, 1, Avalonia.RelativeUnit.Relative), + GradientStops = { new GradientStop(ParseColor(look.From), 0), new GradientStop(ParseColor(look.To), 1) }, + }; + } + + /// + /// One's own picture for the preview, read as the file's composer reads it (in sRGB, white where see-through), as + /// large as covering a square across takes, once (and again when it is picked + /// again); null while it is read or when it can't be ( says so), and the window + /// won't save. Its file's stamp as it was read goes with the conversion, which refuses a picture behind changed + /// since. + /// + private Bitmap? BackgroundPicture(string? path, out bool unreadable) + { + unreadable = false; + if (string.IsNullOrEmpty(path)) + return null; + if (_backgroundPictures.TryGetValue(path, out var known)) + { + unreadable = known.Bitmap is null && !_reading.Contains(path); + return known.Bitmap; + } + Prune(); + _backgroundPictures[path] = (null, default); + _reading.Add(path); + _ = Task.Run(() => + { + Bitmap? bitmap = null; + // Before it is read: a change meanwhile makes the stamp an old one, and the file is refused. + var written = StampOf(path); + try + { + using var image = ReadPictureBehind(path, PictureBehindSide, PictureBehindSide); + if (image is not null) + { + // A panorama covers the square only whole: its long side, far past what the preview shows, held + // ten times the pixels. + if (Math.Max(image.Width, image.Height) > PictureBehindLongest) + image.Resize(new ImageMagick.MagickGeometry(PictureBehindLongest, PictureBehindLongest) { Greater = true }); + bitmap = HideService.ToBitmap(image); + } + } + catch (Exception ex) + { + // Whatever went wrong (a picture too large for memory, say), it is one that can't be read: never one + // read for ever. + _log?.LogInformation(ex, "Could not read the picture to put behind"); + } + finally + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + if (_closed) + { + bitmap?.Dispose(); + return; + } + _reading.Remove(path); + _backgroundPictures[path] = (bitmap, written); + ShowPreview(); + SaveCommand.NotifyCanExecuteChanged(); + }); + } + }); + return null; + } + + /// + /// When was last written and how long it is (none when it is gone): a copy that kept the + /// time it was written still differs in length, mostly. + /// + private static (DateTime Written, long Length) StampOf(string path) + { + try + { + var file = new FileInfo(path); + return file.Exists ? (file.LastWriteTimeUtc, file.Length) : (DateTime.MinValue, -1); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException) + { + return (DateTime.MinValue, -1); + } + } + + /// Drops what was read of , unless it is being read. + private void Forget(string path) + { + if (_reading.Contains(path) || !_backgroundPictures.Remove(path, out var known) || known.Bitmap is null) + return; + // The brush on screen still draws it: disposed once another is set (ShowPreview). + _retired.Add(known.Bitmap); + } + + /// Drops the pictures behind no picture's look uses any more (picked once, then another). + private void Prune() + { + foreach (var path in _backgroundPictures.Keys.Where(p => _looks.All(look => look.Picture != p)).ToList()) + Forget(path); + } + + /// Pictures behind let go of, kept until the brush that drew them is replaced. + private readonly List _retired = []; + + /// Reads a picture to put behind (tests make it fail). + internal static Func ReadPictureBehind { get; set; } = + (path, width, height) => BackdropComposer.ReadPictureBehind(path, width, height); + + /// The pictures behind being read. + private readonly HashSet _reading = []; + + /// The picture is loaded and laid out, and any picture behind read (tests wait for it). + internal bool IsPreviewCurrent => PictureImage is not null && BackgroundBrush is not null && _reading.Count == 0; + + // ───────── Saving ───────── + + /// + /// A picture shown, and with "Picture", one chosen that can be read: the file is never written on a background + /// other than the one asked for. + /// + private bool CanSave() => !IsBusy && PictureImage is not null + && (!IsPicture || (HasPicture && !PictureUnreadable && _reading.Count == 0)); + + /// + /// Starts the conversion of the picture on its look (shown in the toast), keeps the look, and opens the next + /// picture or closes. When it can't be started, the picture stays, to save again or skip. + /// + [RelayCommand(CanExecute = nameof(CanSave))] + private async Task SaveAsync() + { + var path = _files[CurrentIndex]; + var look = Look.Clone(); + // The picture behind as the window showed it: changed since, the file is refused, not written with another. + // Changed already (edited in another app since it was read), it is read again and shown first, saved with the + // next press: every picture after it went to a conversion that was refused. + if (look.Kind == BackdropKind.Picture && look.Picture is { } behind && _backgroundPictures.TryGetValue(behind, out var shown)) + { + if (StampOf(behind) != shown.Stamp) + { + Forget(behind); + ShowPreview(); + return; + } + (look.PictureWritten, look.PictureLength) = shown.Stamp; + } + Remember(); + try + { + _export(path, BackgroundTool.ConversionOf(_tool, look)); + } + catch (Exception ex) when (ex is not OutOfMemoryException) + { + _log?.LogError(ex, "Could not start adding a background to {File}", CurrentFileName); + AddProblem(CurrentIndex, "error.conversion_failed"); + return; + } + _outcomes[CurrentIndex] = CropItemState.Done; + ClearProblems(CurrentIndex); + await LoadFileAsync(CurrentIndex + 1); + } + + [RelayCommand] + private async Task SkipAsync() + { + if (IsBusy || IsFinished) + return; + _outcomes[CurrentIndex] ??= CropItemState.Skipped; + await LoadFileAsync(CurrentIndex + 1); + } + + private void AddProblem(int index, string key) + { + _outcomes[index] = CropItemState.Failed; + var line = $"{Path.GetFileName(_files[index])}: {_loc[key]}"; + Problems.Add(line); + _problemFiles.Add(index); + StatusNotice = line; + } + + /// A picture saved after all: what went wrong with it before is no failure any more. + private void ClearProblems(int index) + { + for (var i = _problemFiles.Count - 1; i >= 0; i--) + { + if (_problemFiles[i] != index) + continue; + _problemFiles.RemoveAt(i); + Problems.RemoveAt(i); + } + StatusNotice = Problems.LastOrDefault() ?? ""; + } + + /// + /// Closes after the last picture, or stays open on the summary when a picture could not be opened or saved. + /// + private void Finish() + { + if (Problems.Count == 0) + { + Close(); + return; + } + Remember(); + FailedCount = _outcomes.Count(o => o == CropItemState.Failed); + DoneCount = _outcomes.Count(o => o == CropItemState.Done); + SkippedCount = _outcomes.Count(o => o == CropItemState.Skipped); + Summary = _loc.Format("toast.done_errors", FailedCount, _files.Count); + StatusNotice = ""; + HasMultipleFiles = false; + IsBusy = false; + var old = PictureImage; + PictureImage = null; + old?.Dispose(); + IsFinished = true; + } + + [RelayCommand] + private void Close() + { + Remember(); + CloseRequested?.Invoke(); + } + + /// + /// Keeps the look of the picture shown in the tool, as given to this window and as saved, and writes the library + /// when it changed. The tool given is mostly the library's own: compared with it after the look was put in, it was + /// always the same, and the look was never written. + /// + private void Remember() + { + var look = Look.Clamped(); + if (Same(_remembered, look)) + return; + _remembered = look.Clone(); + BackgroundTool.Remember(_tool, look); + if (_store.FindPreset(_tool.Id) is not { IsBackgroundTool: true } saved) + return; + BackgroundTool.Remember(saved, look); + try + { + _store.SaveLibrary(); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + _log?.LogWarning(ex, "Could not remember the background look"); + } + } + + /// The look last kept in the tool (as it was when the window opened, at first). + private BackdropOptions? _remembered; + + private static bool Same(BackdropOptions? a, BackdropOptions b) => + a is not null && a.Kind == b.Kind && a.From == b.From && a.To == b.To && a.Color == b.Color && a.Picture == b.Picture + && a.Padding == b.Padding && a.Corners == b.Corners && a.Shadow == b.Shadow && a.Canvas == b.Canvas; + + private void OnLanguageChanged(object? sender, EventArgs e) + { + if (CurrentIndex < _files.Count) + CounterText = _loc.Format("crop.page_counter", CurrentIndex + 1, _files.Count); + OnPropertyChanged(nameof(SaveText)); + OnPropertyChanged(nameof(ShadowText)); + OnPropertyChanged(nameof(PictureName)); + if (PictureUnreadable) + PictureNotice = _loc["background.picture_unreadable"]; + foreach (var swatch in Gradients.Concat(Colors)) + swatch.Name = _loc[swatch.NameKey]; + } + + public void Dispose() + { + // Closed with the title bar or Alt+F4 too, while the first picture is still read as well: the look is kept for + // next time (when it changed). + if (!_closed) + Remember(); + _closed = true; + _loc.LanguageChanged -= OnLanguageChanged; + PictureImage?.Dispose(); + PictureImage = null; + foreach (var (bitmap, _) in _backgroundPictures.Values) + bitmap?.Dispose(); + _backgroundPictures.Clear(); + foreach (var bitmap in _retired) + bitmap.Dispose(); + _retired.Clear(); + } +} + +/// A gradient or colour offered as a swatch, named for screen readers and its tooltip. +public sealed partial class BackdropSwatch(string from, string to, string nameKey, BackgroundViewModel owner) : ObservableObject +{ + public string From { get; } = from; + public string To { get; } = to; + public string NameKey { get; } = nameKey; + + [ObservableProperty] private string _name = ""; + + public IBrush Brush { get; } = new LinearGradientBrush + { + StartPoint = new Avalonia.RelativePoint(0, 0, Avalonia.RelativeUnit.Relative), + EndPoint = new Avalonia.RelativePoint(1, 1, Avalonia.RelativeUnit.Relative), + GradientStops = { new GradientStop(Color.Parse(from), 0), new GradientStop(Color.Parse(to), 1) }, + }; + + [ObservableProperty] private bool _isSelected; + + [RelayCommand] + private void Choose() => owner.Choose(this); +} diff --git a/src/Filee.App/ViewModels/JobViewModel.cs b/src/Filee.App/ViewModels/JobViewModel.cs index 8e8cd334f..a0a4771a4 100644 --- a/src/Filee.App/ViewModels/JobViewModel.cs +++ b/src/Filee.App/ViewModels/JobViewModel.cs @@ -113,8 +113,11 @@ public void Refresh() ErrorDetails = failed.Count == 0 ? null : string.Join(Environment.NewLine, failed.Select(f => $"{Path.GetFileName(f.SourcePath)} — {FailureText.Reason(_loc, f.ErrorKey, f.ErrorDetail)}")); // A file from a browser that couldn't be saved, or whose original wasn't kept, is gone: nothing to retry with. + // Not for a picture behind changed since Add background showed it: retried, it is refused again (the look is + // to be added again). CanRetry = _retry is not null && !IsRunning && failed.Count > 0 - && (Job.Fetcher is null || failed.Any(f => File.Exists(f.SourcePath))); + && (Job.Fetcher is null || failed.Any(f => File.Exists(f.SourcePath))) + && failed.Any(f => f.ErrorKey != "error.background_picture_changed"); IReadOnlyList unknown = IsRunning ? [] : FormatRequest.UnknownExtensions( failed.Where(f => f.ErrorKey == "error.unsupported_source").Select(f => f.SourcePath)); if (!unknown.SequenceEqual(UnknownExtensions)) diff --git a/src/Filee.App/Views/BackgroundWindow.axaml b/src/Filee.App/Views/BackgroundWindow.axaml new file mode 100644 index 000000000..e85de5de0 --- /dev/null +++ b/src/Filee.App/Views/BackgroundWindow.axaml @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +