diff --git a/src/Filee.App/Assets/WhatsNew/2.3.0/annotate.webp b/src/Filee.App/Assets/WhatsNew/2.3.0/annotate.webp new file mode 100644 index 000000000..467033f9c Binary files /dev/null and b/src/Filee.App/Assets/WhatsNew/2.3.0/annotate.webp differ diff --git a/src/Filee.App/Assets/WhatsNew/2.3.0/crop_video.webp b/src/Filee.App/Assets/WhatsNew/2.3.0/crop_video.webp new file mode 100644 index 000000000..9ab05fd9e Binary files /dev/null and b/src/Filee.App/Assets/WhatsNew/2.3.0/crop_video.webp differ diff --git a/src/Filee.App/Assets/WhatsNew/2.3.0/fields.webp b/src/Filee.App/Assets/WhatsNew/2.3.0/fields.webp new file mode 100644 index 000000000..5dcd6c7c8 Binary files /dev/null and b/src/Filee.App/Assets/WhatsNew/2.3.0/fields.webp differ diff --git a/src/Filee.App/Assets/WhatsNew/2.3.0/more.webp b/src/Filee.App/Assets/WhatsNew/2.3.0/more.webp new file mode 100644 index 000000000..2baf529d8 Binary files /dev/null and b/src/Filee.App/Assets/WhatsNew/2.3.0/more.webp differ diff --git a/src/Filee.App/Assets/WhatsNew/2.3.0/sheets.webp b/src/Filee.App/Assets/WhatsNew/2.3.0/sheets.webp new file mode 100644 index 000000000..54edbb6b8 Binary files /dev/null and b/src/Filee.App/Assets/WhatsNew/2.3.0/sheets.webp differ diff --git a/src/Filee.App/Assets/WhatsNew/2.3.0/slides.json b/src/Filee.App/Assets/WhatsNew/2.3.0/slides.json new file mode 100644 index 000000000..7b0aeabd8 --- /dev/null +++ b/src/Filee.App/Assets/WhatsNew/2.3.0/slides.json @@ -0,0 +1,45 @@ +{ + "version": "2.3.0", + "slides": [ + { + "id": "annotate", + "media": "annotate.webp", + "poster": 155, + "title": "whats_new.2_3_0.annotate.title", + "text": "whats_new.2_3_0.annotate.text" + }, + { + "id": "crop_video", + "media": "crop_video.webp", + "poster": 44, + "title": "whats_new.2_3_0.crop_video.title", + "text": "whats_new.2_3_0.crop_video.text" + }, + { + "id": "sheets", + "media": "sheets.webp", + "poster": 88, + "title": "whats_new.2_3_0.sheets.title", + "text": "whats_new.2_3_0.sheets.text" + }, + { + "id": "fields", + "media": "fields.webp", + "poster": 45, + "title": "whats_new.2_3_0.fields.title", + "text": "whats_new.2_3_0.fields.text" + }, + { + "id": "translate", + "media": "translate.webp", + "title": "whats_new.2_3_0.translate.title", + "text": "whats_new.2_3_0.translate.text" + }, + { + "id": "more", + "media": "more.webp", + "title": "whats_new.2_3_0.more.title", + "text": "whats_new.2_3_0.more.text" + } + ] +} diff --git a/src/Filee.App/Assets/WhatsNew/2.3.0/translate.webp b/src/Filee.App/Assets/WhatsNew/2.3.0/translate.webp new file mode 100644 index 000000000..c148bce38 Binary files /dev/null and b/src/Filee.App/Assets/WhatsNew/2.3.0/translate.webp differ diff --git a/src/Filee.App/Assets/i18n/ar.json b/src/Filee.App/Assets/i18n/ar.json index afcc5f611..78427faea 100644 --- a/src/Filee.App/Assets/i18n/ar.json +++ b/src/Filee.App/Assets/i18n/ar.json @@ -1376,6 +1376,18 @@ "whats_new.2_0_0.languages.text": "تتوفر الآن الإسبانية والهندية واليابانية والروسية والعربية والبرتغالية البرازيلية، إلى جانب الإنجليزية والكورية والصينية والبولندية.", "whats_new.2_0_0.more.title": "والمزيد من التحسينات", "whats_new.2_0_0.more.text": "يمكنك إعادة قائمة دائرية واحدة إلى إعداداتها الافتراضية، والبحث في سجل التحويلات بالكامل، وتحويل المستندات الغوجاراتية والهندية والعربية باستخدام LibreOffice.", + "whats_new.2_3_0.annotate.title": "تعليقات على الصور", + "whats_new.2_3_0.annotate.text": "ارسم الأسهم والإطارات والتظليل والنصوص والأرقام مباشرة على الصورة. أضف «تعليقات» إلى قائمة دائرية من الإعدادات ← شريط الأدوات الدائري.", + "whats_new.2_3_0.crop_video.title": "اقتصاص إطار الفيديو", + "whats_new.2_3_0.crop_video.text": "أزل الأشرطة السوداء التي تُكتشف تلقائيًا، أو اقتصص إلى 16:9 أو 9:16 أو 4:3 أو 1:1 أو 21:9. يبقى فيديو HDR بتقنية HDR.", + "whats_new.2_3_0.sheets.title": "صفحات لكل ورقة", + "whats_new.2_3_0.sheets.text": "ضع 2 أو 4 أو 6 أو 8 أو 9 صفحات PDF على كل ورقة. تبقى الصفحات متجهية فيبقى النص واضحًا.", + "whats_new.2_3_0.fields.title": "الصيغ التي تستخدمها فقط", + "whats_new.2_3_0.fields.text": "أوقف المجالات التي لا تعمل فيها، مثل CAD أو النماذج الثلاثية الأبعاد أو الخطوط، من الإعدادات ← المجالات. تختفي صيغها من القائمة الدائرية والقوائم.", + "whats_new.2_3_0.translate.title": "تسميات توضيحية وترجمة على حاسوبك", + "whats_new.2_3_0.translate.text": "أنشئ تسميات توضيحية SRT من الفيديو، وترجم المستندات والكتب الإلكترونية والترجمات بذكاء اصطناعي محلي. لا يغادر أي شيء حاسوبك.", + "whats_new.2_3_0.more.title": "والمزيد من التحسينات", + "whats_new.2_3_0.more.text": "النسخ أو النقل إلى مجلد، والاقتصاص بالمنظور، وملفات CMYK للطباعة، وملفات Krita وMinecraft، إضافة إلى الإيطالية والإندونيسية والفيتنامية.", "preset.extract_motion.donut": "استخراج فيديو Live Photo", "presets.copy_to_folder": "النسخ إلى مجلد", "presets.copy_hint": "نسخ الملفات والمجلدات دون تحويل. تبقى الأصول في مكانها؛ وتُضاف أرقام إلى الأسماء الموجودة.", diff --git a/src/Filee.App/Assets/i18n/de.json b/src/Filee.App/Assets/i18n/de.json index 8ae828ea7..14d4070e0 100644 --- a/src/Filee.App/Assets/i18n/de.json +++ b/src/Filee.App/Assets/i18n/de.json @@ -1375,6 +1375,18 @@ "whats_new.2_0_0.languages.text": "Spanisch, Hindi, Japanisch, Russisch, Arabisch und brasilianisches Portugiesisch ergänzen Englisch, Koreanisch, Chinesisch und Polnisch.", "whats_new.2_0_0.more.title": "Und noch mehr", "whats_new.2_0_0.more.text": "Setze einzelne Ringmenüs zurück, durchsuche den gesamten Verlauf und konvertiere Dokumente auf Gujarati, Hindi oder Arabisch mit LibreOffice.", + "whats_new.2_3_0.annotate.title": "Bilder beschriften", + "whats_new.2_3_0.annotate.text": "Zeichne Pfeile, Rahmen, Markierungen, Text und Nummern direkt aufs Bild. Leg Beschriften unter Einstellungen → Ringmenü auf ein Ringmenü.", + "whats_new.2_3_0.crop_video.title": "Videos zuschneiden", + "whats_new.2_3_0.crop_video.text": "Entferne automatisch erkannte schwarze Balken oder schneide auf 16:9, 9:16, 4:3, 1:1 oder 21:9 zu. HDR-Videos bleiben HDR.", + "whats_new.2_3_0.sheets.title": "Seiten pro Blatt", + "whats_new.2_3_0.sheets.text": "Bring 2, 4, 6, 8 oder 9 PDF-Seiten auf jedes Blatt. Die Seiten bleiben Vektoren, Text bleibt scharf.", + "whats_new.2_3_0.fields.title": "Nur die Formate, die du brauchst", + "whats_new.2_3_0.fields.text": "Schalte unter Einstellungen → Bereiche aus, womit du nicht arbeitest, etwa CAD, 3D oder Schriftarten. Ihre Formate verschwinden aus dem Ringmenü und den Listen.", + "whats_new.2_3_0.translate.title": "Untertitel und Übersetzungen auf deinem PC", + "whats_new.2_3_0.translate.text": "Erstelle SRT-Untertitel aus Videos und übersetze Dokumente, E-Books und Untertitel mit einer lokalen KI. Nichts verlässt deinen PC.", + "whats_new.2_3_0.more.title": "Und noch mehr", + "whats_new.2_3_0.more.text": "In Ordner kopieren oder verschieben, perspektivisch zuschneiden, CMYK-Druckdateien, Krita- und Minecraft-Dateien sowie Italienisch, Indonesisch und Vietnamesisch.", "preset.extract_motion.donut": "Video extrahieren", "presets.copy_to_folder": "In Ordner kopieren", "presets.copy_hint": "Dateien und Ordner ohne Konvertierung kopieren. Die Originale bleiben, wo sie sind; vorhandene Namen bekommen eine Nummer.", diff --git a/src/Filee.App/Assets/i18n/en.json b/src/Filee.App/Assets/i18n/en.json index 5a05a178b..3d742ecd7 100644 --- a/src/Filee.App/Assets/i18n/en.json +++ b/src/Filee.App/Assets/i18n/en.json @@ -1397,6 +1397,18 @@ "whats_new.2_0_0.languages.text": "Spanish, Hindi, Japanese, Russian, Arabic and Brazilian Portuguese join English, Korean, Chinese and Polish.", "whats_new.2_0_0.more.title": "And a little more", "whats_new.2_0_0.more.text": "Reset a single donut to its defaults, search your whole history, and convert Gujarati, Hindi or Arabic documents through LibreOffice.", + "whats_new.2_3_0.annotate.title": "Annotate pictures", + "whats_new.2_3_0.annotate.text": "Draw arrows, boxes, highlights, text and numbers right on a picture. Add Annotate to a donut in Settings → Donut toolbar.", + "whats_new.2_3_0.crop_video.title": "Crop videos", + "whats_new.2_3_0.crop_video.text": "Remove black bars found on their own, or crop to 16:9, 9:16, 4:3, 1:1 or 21:9. HDR video stays HDR.", + "whats_new.2_3_0.sheets.title": "Pages per sheet", + "whats_new.2_3_0.sheets.text": "Put 2, 4, 6, 8 or 9 PDF pages on each sheet. The pages stay vector, so text stays sharp.", + "whats_new.2_3_0.fields.title": "Only the formats you use", + "whats_new.2_3_0.fields.text": "Turn off fields you don't work in, like CAD, 3D or fonts, in Settings → Fields. Their formats leave the donut and the lists.", + "whats_new.2_3_0.translate.title": "Captions and translations on your PC", + "whats_new.2_3_0.translate.text": "Make SRT captions from a video, and translate documents, e-books and subtitles with a local AI. Nothing leaves your PC.", + "whats_new.2_3_0.more.title": "And a little more", + "whats_new.2_3_0.more.text": "Copy or move to a folder, perspective crop, CMYK print files, Krita and Minecraft files, and Italian, Indonesian and Vietnamese.", "preset.extract_motion.donut": "Extract video", "presets.copy_to_folder": "Copy to folder", "presets.copy_hint": "Copy files and folders without conversion. Originals stay where they are; existing names get a number.", diff --git a/src/Filee.App/Assets/i18n/es.json b/src/Filee.App/Assets/i18n/es.json index 245323230..f133bd4ad 100644 --- a/src/Filee.App/Assets/i18n/es.json +++ b/src/Filee.App/Assets/i18n/es.json @@ -1377,6 +1377,18 @@ "whats_new.2_0_0.languages.text": "El español, el hindi, el japonés, el ruso, el árabe y el portugués de Brasil se suman al inglés, el coreano, el chino y el polaco.", "whats_new.2_0_0.more.title": "Y un poco más", "whats_new.2_0_0.more.text": "Restablece un solo menú circular, busca en todo tu historial y convierte documentos en guyaratí, hindi o árabe con LibreOffice.", + "whats_new.2_3_0.annotate.title": "Anota tus imágenes", + "whats_new.2_3_0.annotate.text": "Dibuja flechas, recuadros, resaltados, texto y números directamente sobre la imagen. Añade Anotar a un menú en Ajustes → Menú circular.", + "whats_new.2_3_0.crop_video.title": "Recorta vídeos", + "whats_new.2_3_0.crop_video.text": "Quita las bandas negras detectadas automáticamente o recorta a 16:9, 9:16, 4:3, 1:1 o 21:9. El vídeo HDR sigue en HDR.", + "whats_new.2_3_0.sheets.title": "Páginas por hoja", + "whats_new.2_3_0.sheets.text": "Pon 2, 4, 6, 8 o 9 páginas de un PDF en cada hoja. Las páginas siguen siendo vectoriales y el texto se ve nítido.", + "whats_new.2_3_0.fields.title": "Solo los formatos que usas", + "whats_new.2_3_0.fields.text": "En Ajustes → Áreas, desactiva las que no usas, como CAD, 3D o fuentes. Sus formatos desaparecen del menú circular y de las listas.", + "whats_new.2_3_0.translate.title": "Subtítulos y traducciones en tu equipo", + "whats_new.2_3_0.translate.text": "Crea subtítulos SRT a partir de un vídeo y traduce documentos, libros electrónicos y subtítulos con una IA local. Nada sale de tu equipo.", + "whats_new.2_3_0.more.title": "Y un poco más", + "whats_new.2_3_0.more.text": "Copiar o mover a una carpeta, recorte con perspectiva, archivos CMYK para imprenta, archivos de Krita y Minecraft, e italiano, indonesio y vietnamita.", "preset.extract_motion.donut": "Extraer vídeo de Live Photo", "presets.copy_to_folder": "Copiar a carpeta", "presets.copy_hint": "Copia archivos y carpetas sin convertirlos. Los originales se quedan donde están; los nombres que ya existen reciben un número.", diff --git a/src/Filee.App/Assets/i18n/fr.json b/src/Filee.App/Assets/i18n/fr.json index 93316bbbc..7e6b2bfb1 100644 --- a/src/Filee.App/Assets/i18n/fr.json +++ b/src/Filee.App/Assets/i18n/fr.json @@ -1397,6 +1397,18 @@ "whats_new.2_0_0.languages.text": "L'espagnol, l'hindi, le japonais, le russe, l'arabe et le portugais du Brésil rejoignent l'anglais, le coréen, le chinois et le polonais.", "whats_new.2_0_0.more.title": "Et encore un peu plus", "whats_new.2_0_0.more.text": "Réinitialisez un seul menu circulaire, recherchez dans tout votre historique, et convertissez des documents en gujarati, hindi ou arabe via LibreOffice.", + "whats_new.2_3_0.annotate.title": "Annoter les images", + "whats_new.2_3_0.annotate.text": "Dessinez flèches, cadres, surlignages, texte et numéros directement sur l'image. Ajoutez Annoter à un menu dans Paramètres → Menu circulaire.", + "whats_new.2_3_0.crop_video.title": "Recadrer les vidéos", + "whats_new.2_3_0.crop_video.text": "Supprimez les bandes noires détectées automatiquement ou recadrez en 16:9, 9:16, 4:3, 1:1 ou 21:9. Une vidéo HDR reste en HDR.", + "whats_new.2_3_0.sheets.title": "Pages par feuille", + "whats_new.2_3_0.sheets.text": "Placez 2, 4, 6, 8 ou 9 pages PDF sur chaque feuille. Les pages restent vectorielles et le texte reste net.", + "whats_new.2_3_0.fields.title": "Seulement les formats qui vous servent", + "whats_new.2_3_0.fields.text": "Dans Paramètres → Domaines, désactivez ceux où vous ne travaillez pas, comme la CAO, la 3D ou les polices. Leurs formats quittent le menu circulaire et les listes.", + "whats_new.2_3_0.translate.title": "Sous-titres et traductions sur votre PC", + "whats_new.2_3_0.translate.text": "Créez des sous-titres SRT à partir d'une vidéo et traduisez documents, livres numériques et sous-titres avec une IA locale. Rien ne quitte votre PC.", + "whats_new.2_3_0.more.title": "Et encore un peu plus", + "whats_new.2_3_0.more.text": "Copier ou déplacer dans un dossier, recadrage en perspective, fichiers CMJN pour l'impression, fichiers Krita et Minecraft, ainsi que l'italien, l'indonésien et le vietnamien.", "preset.extract_motion.donut": "Vidéo Live", "presets.copy_to_folder": "Copier dans un dossier", "presets.copy_hint": "Copie les fichiers et dossiers sans conversion. Les originaux restent où ils sont ; les noms déjà pris reçoivent un numéro.", diff --git a/src/Filee.App/Assets/i18n/hi.json b/src/Filee.App/Assets/i18n/hi.json index ec966f1fc..3f38eeb4c 100644 --- a/src/Filee.App/Assets/i18n/hi.json +++ b/src/Filee.App/Assets/i18n/hi.json @@ -1377,6 +1377,18 @@ "whats_new.2_0_0.languages.text": "अंग्रेज़ी, कोरियाई, चीनी और पोलिश के साथ अब स्पैनिश, हिन्दी, जापानी, रूसी, अरबी और ब्राज़ीलियाई पुर्तगाली भी।", "whats_new.2_0_0.more.title": "और भी थोड़ा", "whats_new.2_0_0.more.text": "एक ही डोनट को डिफ़ॉल्ट पर रीसेट करें, पूरा इतिहास खोजें, और गुजराती, हिन्दी या अरबी दस्तावेज़ LibreOffice से बदलें।", + "whats_new.2_3_0.annotate.title": "तस्वीरों पर टिप्पणी", + "whats_new.2_3_0.annotate.text": "तस्वीर पर सीधे तीर, फ़्रेम, हाइलाइट, टेक्स्ट और नंबर बनाएँ। सेटिंग्स → डोनट टूलबार में टिप्पणी को किसी डोनट पर जोड़ें।", + "whats_new.2_3_0.crop_video.title": "वीडियो क्रॉप करें", + "whats_new.2_3_0.crop_video.text": "अपने आप मिली काली पट्टियाँ हटाएँ, या 16:9, 9:16, 4:3, 1:1 या 21:9 में क्रॉप करें। HDR वीडियो HDR ही रहता है।", + "whats_new.2_3_0.sheets.title": "प्रति शीट पेज", + "whats_new.2_3_0.sheets.text": "हर शीट पर PDF के 2, 4, 6, 8 या 9 पेज रखें। पेज वेक्टर ही रहते हैं, इसलिए टेक्स्ट साफ़ दिखता है।", + "whats_new.2_3_0.fields.title": "सिर्फ़ आपके काम के फ़ॉर्मैट", + "whats_new.2_3_0.fields.text": "सेटिंग्स → क्षेत्र में वे क्षेत्र बंद करें जिनमें आप काम नहीं करते, जैसे CAD, 3D या फ़ॉन्ट। उनके फ़ॉर्मैट डोनट और सूचियों से हट जाते हैं।", + "whats_new.2_3_0.translate.title": "आपके PC पर उपशीर्षक और अनुवाद", + "whats_new.2_3_0.translate.text": "वीडियो से SRT उपशीर्षक बनाएँ, और दस्तावेज़, ई-बुक और उपशीर्षक लोकल AI से अनुवाद करें। कुछ भी आपके PC से बाहर नहीं जाता।", + "whats_new.2_3_0.more.title": "और भी थोड़ा", + "whats_new.2_3_0.more.text": "फ़ोल्डर में कॉपी करें या ले जाएँ, परिप्रेक्ष्य क्रॉप, प्रिंट के लिए CMYK फ़ाइलें, Krita और Minecraft फ़ाइलें, और इतालवी, इंडोनेशियाई व वियतनामी भाषा।", "preset.extract_motion.donut": "लाइव फ़ोटो वीडियो निकालें", "presets.copy_to_folder": "फ़ोल्डर में कॉपी करें", "presets.copy_hint": "फ़ाइलें और फ़ोल्डर बिना रूपांतरण के कॉपी करें। मूल फ़ाइलें अपनी जगह रहती हैं; पहले से मौजूद नामों में एक संख्या जुड़ जाती है।", diff --git a/src/Filee.App/Assets/i18n/id.json b/src/Filee.App/Assets/i18n/id.json index 8e9f47d42..c265c4ba5 100644 --- a/src/Filee.App/Assets/i18n/id.json +++ b/src/Filee.App/Assets/i18n/id.json @@ -1375,6 +1375,18 @@ "whats_new.2_0_0.languages.text": "Bahasa Spanyol, Hindi, Jepang, Rusia, Arab, dan Portugis Brasil bergabung dengan bahasa Inggris, Korea, Mandarin, dan Polandia.", "whats_new.2_0_0.more.title": "Dan masih banyak lagi", "whats_new.2_0_0.more.text": "Atur ulang satu menu donut ke bawaan, cari seluruh riwayat Anda, dan konversi dokumen Gujarati, Hindi, atau Arab melalui LibreOffice.", + "whats_new.2_3_0.annotate.title": "Anotasi gambar", + "whats_new.2_3_0.annotate.text": "Gambar panah, bingkai, sorotan, teks, dan nomor langsung di atas gambar. Tambahkan Anotasi ke donut di Pengaturan → Toolbar donut.", + "whats_new.2_3_0.crop_video.title": "Pangkas video", + "whats_new.2_3_0.crop_video.text": "Hapus bilah hitam yang ditemukan otomatis, atau pangkas ke 16:9, 9:16, 4:3, 1:1, atau 21:9. Video HDR tetap HDR.", + "whats_new.2_3_0.sheets.title": "Halaman per lembar", + "whats_new.2_3_0.sheets.text": "Taruh 2, 4, 6, 8, atau 9 halaman PDF di setiap lembar. Halaman tetap vektor, jadi teks tetap tajam.", + "whats_new.2_3_0.fields.title": "Hanya format yang Anda pakai", + "whats_new.2_3_0.fields.text": "Matikan bidang yang tidak Anda kerjakan, seperti CAD, 3D, atau font, di Pengaturan → Bidang. Formatnya hilang dari donut dan daftar.", + "whats_new.2_3_0.translate.title": "Subtitle dan terjemahan di PC Anda", + "whats_new.2_3_0.translate.text": "Buat subtitle SRT dari video, dan terjemahkan dokumen, e-book, serta subtitle dengan AI lokal. Tidak ada yang keluar dari PC Anda.", + "whats_new.2_3_0.more.title": "Dan masih banyak lagi", + "whats_new.2_3_0.more.text": "Salin atau pindahkan ke folder, pangkas perspektif, file CMYK untuk cetak, file Krita dan Minecraft, serta bahasa Italia, Indonesia, dan Vietnam.", "preset.extract_motion.donut": "Ekstrak video", "presets.copy_to_folder": "Salin ke folder", "presets.copy_hint": "Salin file dan folder tanpa konversi. File asli tetap di tempatnya; nama yang sudah ada diberi nomor.", diff --git a/src/Filee.App/Assets/i18n/it.json b/src/Filee.App/Assets/i18n/it.json index f19eda072..7f3147ee7 100644 --- a/src/Filee.App/Assets/i18n/it.json +++ b/src/Filee.App/Assets/i18n/it.json @@ -1374,6 +1374,18 @@ "whats_new.2_0_0.languages.text": "Spagnolo, hindi, giapponese, russo, arabo e portoghese brasiliano si uniscono a inglese, coreano, cinese e polacco.", "whats_new.2_0_0.more.title": "E qualcosa in più", "whats_new.2_0_0.more.text": "Ripristina un singolo donut ai valori predefiniti, cerca in tutta la cronologia e converti documenti in gujarati, hindi o arabo tramite LibreOffice.", +"whats_new.2_3_0.annotate.title": "Annota le immagini", +"whats_new.2_3_0.annotate.text": "Disegna frecce, cornici, evidenziazioni, testo e numeri direttamente sull'immagine. Aggiungi Annota a un donut in Impostazioni → Barra donut.", +"whats_new.2_3_0.crop_video.title": "Ritaglia i video", +"whats_new.2_3_0.crop_video.text": "Rimuovi le bande nere trovate in automatico o ritaglia in 16:9, 9:16, 4:3, 1:1 o 21:9. I video HDR restano HDR.", +"whats_new.2_3_0.sheets.title": "Pagine per foglio", +"whats_new.2_3_0.sheets.text": "Metti 2, 4, 6, 8 o 9 pagine PDF su ogni foglio. Le pagine restano vettoriali e il testo resta nitido.", +"whats_new.2_3_0.fields.title": "Solo i formati che usi", +"whats_new.2_3_0.fields.text": "In Impostazioni → Settori disattiva quelli in cui non lavori, come CAD, 3D o caratteri. I loro formati spariscono dal donut e dagli elenchi.", +"whats_new.2_3_0.translate.title": "Sottotitoli e traduzioni sul tuo PC", +"whats_new.2_3_0.translate.text": "Crea sottotitoli SRT da un video e traduci documenti, ebook e sottotitoli con un'IA locale. Niente lascia il tuo PC.", +"whats_new.2_3_0.more.title": "E qualcosa in più", +"whats_new.2_3_0.more.text": "Copia o sposta in una cartella, ritaglio prospettico, file CMYK per la stampa, file di Krita e Minecraft, e in più italiano, indonesiano e vietnamita.", "preset.extract_motion.donut": "Estrai video", "presets.copy_to_folder": "Copia nella cartella", "presets.copy_hint": "Copia file e cartelle senza conversione. Gli originali restano dove sono; ai nomi già esistenti si aggiunge un numero.", diff --git a/src/Filee.App/Assets/i18n/ja.json b/src/Filee.App/Assets/i18n/ja.json index d56112db9..6bd798b3e 100644 --- a/src/Filee.App/Assets/i18n/ja.json +++ b/src/Filee.App/Assets/i18n/ja.json @@ -1377,6 +1377,18 @@ "whats_new.2_0_0.languages.text": "英語、韓国語、中国語、ポーランド語に、スペイン語、ヒンディー語、日本語、ロシア語、アラビア語、ブラジルポルトガル語が加わりました。", "whats_new.2_0_0.more.title": "ほかにも", "whats_new.2_0_0.more.text": "ドーナツを 1 つだけ既定に戻す、履歴全体を検索する、グジャラート語・ヒンディー語・アラビア語の文書を LibreOffice で変換する、など。", + "whats_new.2_3_0.annotate.title": "画像に注釈", + "whats_new.2_3_0.annotate.text": "矢印、枠、ハイライト、文字、番号を画像に直接描けます。「設定」→「ドーナツメニュー」で注釈をドーナツメニューに追加してください。", + "whats_new.2_3_0.crop_video.title": "動画のトリミング", + "whats_new.2_3_0.crop_video.text": "自動で見つけた黒帯を削除するか、16:9、9:16、4:3、1:1、21:9 にトリミングします。HDR 動画は HDR のままです。", + "whats_new.2_3_0.sheets.title": "ページ割り付け", + "whats_new.2_3_0.sheets.text": "PDF の 2、4、6、8、9 ページを 1 枚に割り付けます。ページはベクターのままなので、文字もくっきりです。", + "whats_new.2_3_0.fields.title": "使う形式だけを表示", + "whats_new.2_3_0.fields.text": "「設定」→「分野」で CAD、3D、フォントなど使わない分野をオフにすると、その形式がドーナツメニューと一覧から消えます。", + "whats_new.2_3_0.translate.title": "字幕と翻訳を PC の中で", + "whats_new.2_3_0.translate.text": "動画から SRT 字幕を作成し、文書、電子書籍、字幕をローカル AI で翻訳します。ファイルが PC の外に出ることはありません。", + "whats_new.2_3_0.more.title": "ほかにも", + "whats_new.2_3_0.more.text": "フォルダーへのコピー・移動、遠近補正のトリミング、印刷用 CMYK ファイル、Krita と Minecraft のファイル、イタリア語、インドネシア語、ベトナム語に対応しました。", "preset.extract_motion.donut": "Live Photoの動画を抽出", "presets.copy_to_folder": "フォルダーにコピー", "presets.copy_hint": "ファイルとフォルダーを変換せずにコピーします。元のファイルはそのまま残り、同じ名前があれば番号が付きます。", diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json index 6616a4744..377fda003 100644 --- a/src/Filee.App/Assets/i18n/ko.json +++ b/src/Filee.App/Assets/i18n/ko.json @@ -1397,6 +1397,18 @@ "whats_new.2_0_0.languages.text": "영어, 한국어, 중국어, 폴란드어에 스페인어, 힌디어, 일본어, 러시아어, 아랍어, 브라질 포르투갈어가 더해졌어요.", "whats_new.2_0_0.more.title": "그리고 조금 더", "whats_new.2_0_0.more.text": "원형 툴바 하나만 기본값으로 초기화하고, 기록 전체를 검색하고, 구자라트어·힌디어·아랍어 문서를 LibreOffice로 변환할 수 있어요.", + "whats_new.2_3_0.annotate.title": "사진에 주석 달기", + "whats_new.2_3_0.annotate.text": "사진 위에 화살표, 상자, 강조, 글자, 번호를 바로 그려요. 설정 → 원형 툴바에서 주석을 툴바에 넣어 보세요.", + "whats_new.2_3_0.crop_video.title": "동영상 화면 자르기", + "whats_new.2_3_0.crop_video.text": "검은 테두리를 알아서 찾아 없애거나 16:9, 9:16, 4:3, 1:1, 21:9로 잘라요. HDR 동영상은 HDR 그대로 남아요.", + "whats_new.2_3_0.sheets.title": "모아찍기", + "whats_new.2_3_0.sheets.text": "PDF 2·4·6·8·9쪽을 한 장에 모아요. 페이지가 벡터 그대로라 글자가 선명해요.", + "whats_new.2_3_0.fields.title": "쓰는 형식만 보기", + "whats_new.2_3_0.fields.text": "설정 → 분야에서 CAD, 3D, 글꼴처럼 쓰지 않는 분야를 끄면 그 형식이 원형 툴바와 목록에서 빠져요.", + "whats_new.2_3_0.translate.title": "내 PC에서 자막과 번역", + "whats_new.2_3_0.translate.text": "동영상에서 SRT 자막을 만들고, 문서·전자책·자막을 내 PC의 AI로 번역해요. 파일은 PC 밖으로 나가지 않아요.", + "whats_new.2_3_0.more.title": "그리고 조금 더", + "whats_new.2_3_0.more.text": "폴더로 복사·이동, 원근 보정 자르기, 인쇄용 CMYK 파일, Krita·마인크래프트 파일, 이탈리아어·인도네시아어·베트남어가 더해졌어요.", "preset.extract_motion.donut": "라이브 포토 동영상 추출", "presets.copy_to_folder": "폴더로 복사", "presets.copy_hint": "파일과 폴더를 변환하지 않고 복사해요. 원본은 그 자리에 남고, 같은 이름이 있으면 번호가 붙어요.", diff --git a/src/Filee.App/Assets/i18n/ne.json b/src/Filee.App/Assets/i18n/ne.json index cf7179243..6f461a67d 100644 --- a/src/Filee.App/Assets/i18n/ne.json +++ b/src/Filee.App/Assets/i18n/ne.json @@ -1377,6 +1377,18 @@ "whats_new.2_0_0.languages.text": "अङ्ग्रेजी, कोरियाली, चिनियाँ र पोलिससँगै अब स्पेनिस, हिन्दी, जापानी, रसियन, अरबी र ब्राजिलियन पोर्चुगिज पनि।", "whats_new.2_0_0.more.title": "र थोरै अरू पनि", "whats_new.2_0_0.more.text": "एउटै डोनटलाई पूर्वनिर्धारितमा रिसेट गर्नुहोस्, सम्पूर्ण इतिहास खोज्नुहोस्, र LibreOffice मार्फत गुजराती, हिन्दी वा अरबी कागजातहरू रूपान्तरण गर्नुहोस्।", + "whats_new.2_3_0.annotate.title": "तस्बिरमा टिप्पणी", + "whats_new.2_3_0.annotate.text": "तस्बिरमै तीर, फ्रेम, हाइलाइट, पाठ र नम्बर कोर्नुहोस्। सेटिङहरू → डोनट टुलबारमा टिप्पणीलाई कुनै डोनटमा थप्नुहोस्।", + "whats_new.2_3_0.crop_video.title": "भिडियो क्रप गर्नुहोस्", + "whats_new.2_3_0.crop_video.text": "आफैँ भेटिएका कालो पट्टी हटाउनुहोस्, वा 16:9, 9:16, 4:3, 1:1 वा 21:9 मा क्रप गर्नुहोस्। HDR भिडियो HDR नै रहन्छ।", + "whats_new.2_3_0.sheets.title": "प्रति पाना पेज", + "whats_new.2_3_0.sheets.text": "हरेक पानामा PDF का 2, 4, 6, 8 वा 9 पेज राख्नुहोस्। पेजहरू भेक्टर नै रहन्छन्, त्यसैले पाठ स्पष्ट देखिन्छ।", + "whats_new.2_3_0.fields.title": "तपाईंले चलाउने ढाँचा मात्र", + "whats_new.2_3_0.fields.text": "सेटिङहरू → क्षेत्रहरूमा तपाईं काम नगर्ने क्षेत्रहरू, जस्तै CAD, 3D वा फन्ट, बन्द गर्नुहोस्। तिनका ढाँचाहरू डोनट र सूचीहरूबाट हट्छन्।", + "whats_new.2_3_0.translate.title": "तपाईंको कम्प्युटरमै उपशीर्षक र अनुवाद", + "whats_new.2_3_0.translate.text": "भिडियोबाट SRT उपशीर्षक बनाउनुहोस्, र कागजात, ई-पुस्तक र उपशीर्षक स्थानीय AI ले अनुवाद गर्नुहोस्। केही पनि तपाईंको कम्प्युटर बाहिर जाँदैन।", + "whats_new.2_3_0.more.title": "र थोरै अरू पनि", + "whats_new.2_3_0.more.text": "फोल्डरमा प्रतिलिपि वा सार्ने, परिप्रेक्ष्य क्रप, मुद्रणका लागि CMYK फाइल, Krita र Minecraft फाइल, साथै इटालियन, इन्डोनेसियाली र भियतनामी भाषा।", "preset.extract_motion.donut": "लाइभ फोटो भिडियो निकाल्नुहोस्", "presets.copy_to_folder": "फोल्डरमा प्रतिलिपि गर्नुहोस्", "presets.copy_hint": "फाइल र फोल्डरहरू रूपान्तरण नगरी प्रतिलिपि गर्नुहोस्। मूल फाइलहरू आफ्नै ठाउँमा रहन्छन्; पहिल्यै भएका नाममा एउटा अङ्क थपिन्छ।", diff --git a/src/Filee.App/Assets/i18n/pl.json b/src/Filee.App/Assets/i18n/pl.json index 93283f353..51fe0e3bc 100644 --- a/src/Filee.App/Assets/i18n/pl.json +++ b/src/Filee.App/Assets/i18n/pl.json @@ -1397,6 +1397,18 @@ "whats_new.2_0_0.languages.text": "Do angielskiego, koreańskiego, chińskiego i polskiego dołączają hiszpański, hindi, japoński, rosyjski, arabski i portugalski brazylijski.", "whats_new.2_0_0.more.title": "I jeszcze trochę", "whats_new.2_0_0.more.text": "Przywróć domyślne ustawienia jednego pierścienia, przeszukuj całą historię i konwertuj dokumenty w gudżarati, hindi lub arabskim przez LibreOffice.", + "whats_new.2_3_0.annotate.title": "Adnotacje na obrazach", + "whats_new.2_3_0.annotate.text": "Rysuj strzałki, ramki, zakreślenia, tekst i numery bezpośrednio na obrazie. Dodaj Adnotacje do pierścienia w Ustawienia → Pasek-pierścień.", + "whats_new.2_3_0.crop_video.title": "Kadrowanie wideo", + "whats_new.2_3_0.crop_video.text": "Usuń automatycznie wykryte czarne pasy albo wykadruj do 16:9, 9:16, 4:3, 1:1 lub 21:9. Wideo HDR zostaje w HDR.", + "whats_new.2_3_0.sheets.title": "Stron na arkusz", + "whats_new.2_3_0.sheets.text": "Umieść 2, 4, 6, 8 lub 9 stron PDF na każdym arkuszu. Strony pozostają wektorowe, więc tekst jest ostry.", + "whats_new.2_3_0.fields.title": "Tylko formaty, których używasz", + "whats_new.2_3_0.fields.text": "Wyłącz w Ustawienia → Dziedziny to, czym się nie zajmujesz, np. CAD, 3D czy czcionki. Ich formaty znikną z pierścienia i z list.", + "whats_new.2_3_0.translate.title": "Napisy i tłumaczenia na twoim komputerze", + "whats_new.2_3_0.translate.text": "Twórz napisy SRT z wideo i tłumacz dokumenty, e-booki oraz napisy lokalną AI. Nic nie opuszcza twojego komputera.", + "whats_new.2_3_0.more.title": "I jeszcze trochę", + "whats_new.2_3_0.more.text": "Kopiowanie lub przenoszenie do folderu, kadrowanie z perspektywą, pliki CMYK do druku, pliki Krity i Minecrafta oraz włoski, indonezyjski i wietnamski.", "preset.extract_motion.donut": "Wyodrębnij wideo z Live Photo", "presets.copy_to_folder": "Kopiuj do folderu", "presets.copy_hint": "Kopiuje pliki i foldery bez konwersji. Oryginały zostają na miejscu; istniejące nazwy dostają numer.", diff --git a/src/Filee.App/Assets/i18n/pt-BR.json b/src/Filee.App/Assets/i18n/pt-BR.json index 64190478a..d8dd45860 100644 --- a/src/Filee.App/Assets/i18n/pt-BR.json +++ b/src/Filee.App/Assets/i18n/pt-BR.json @@ -1397,6 +1397,18 @@ "whats_new.2_0_0.languages.text": "Espanhol, hindi, japonês, russo, árabe e português do Brasil se juntam ao inglês, coreano, chinês e polonês.", "whats_new.2_0_0.more.title": "E um pouco mais", "whats_new.2_0_0.more.text": "Restaure um único menu aos padrões, pesquise todo o histórico e converta documentos em guzerate, hindi ou árabe pelo LibreOffice.", + "whats_new.2_3_0.annotate.title": "Anote suas imagens", + "whats_new.2_3_0.annotate.text": "Desenhe setas, molduras, destaques, texto e números direto na imagem. Adicione Anotar a um menu em Configurações → Menu circular.", + "whats_new.2_3_0.crop_video.title": "Recorte vídeos", + "whats_new.2_3_0.crop_video.text": "Remova as barras pretas encontradas automaticamente ou recorte em 16:9, 9:16, 4:3, 1:1 ou 21:9. Vídeo HDR continua HDR.", + "whats_new.2_3_0.sheets.title": "Páginas por folha", + "whats_new.2_3_0.sheets.text": "Coloque 2, 4, 6, 8 ou 9 páginas de PDF em cada folha. As páginas continuam vetoriais e o texto fica nítido.", + "whats_new.2_3_0.fields.title": "Só os formatos que você usa", + "whats_new.2_3_0.fields.text": "Em Configurações → Áreas, desative as que você não usa, como CAD, 3D ou fontes. Os formatos delas saem do menu circular e das listas.", + "whats_new.2_3_0.translate.title": "Legendas e traduções no seu PC", + "whats_new.2_3_0.translate.text": "Crie legendas SRT a partir de um vídeo e traduza documentos, e-books e legendas com uma IA local. Nada sai do seu PC.", + "whats_new.2_3_0.more.title": "E um pouco mais", + "whats_new.2_3_0.more.text": "Copiar ou mover para uma pasta, recorte com perspectiva, arquivos CMYK para impressão, arquivos do Krita e do Minecraft, além de italiano, indonésio e vietnamita.", "preset.extract_motion.donut": "Extrair vídeo", "presets.copy_to_folder": "Copiar para pasta", "presets.copy_hint": "Copia arquivos e pastas sem conversão. Os originais ficam onde estão; nomes que já existem recebem um número.", diff --git a/src/Filee.App/Assets/i18n/pt-PT.json b/src/Filee.App/Assets/i18n/pt-PT.json index 92e0a62ed..004be1cc8 100644 --- a/src/Filee.App/Assets/i18n/pt-PT.json +++ b/src/Filee.App/Assets/i18n/pt-PT.json @@ -1397,6 +1397,18 @@ "whats_new.2_0_0.languages.text": "O espanhol, o hindi, o japonês, o russo, o árabe e o português do Brasil juntam-se ao inglês, ao coreano, ao chinês e ao polaco.", "whats_new.2_0_0.more.title": "E mais algumas novidades", "whats_new.2_0_0.more.text": "Repõe os valores de origem de uma só barra, pesquisa em todo o histórico e converte documentos em guzerate, hindi ou árabe através do LibreOffice.", + "whats_new.2_3_0.annotate.title": "Anota as tuas imagens", + "whats_new.2_3_0.annotate.text": "Desenha setas, molduras, destaques, texto e números diretamente na imagem. Adiciona Anotar a uma barra em Definições → Barra circular.", + "whats_new.2_3_0.crop_video.title": "Recorta vídeos", + "whats_new.2_3_0.crop_video.text": "Remove as barras pretas detetadas automaticamente ou recorta em 16:9, 9:16, 4:3, 1:1 ou 21:9. O vídeo HDR continua em HDR.", + "whats_new.2_3_0.sheets.title": "Páginas por folha", + "whats_new.2_3_0.sheets.text": "Coloca 2, 4, 6, 8 ou 9 páginas de PDF em cada folha. As páginas continuam vetoriais e o texto fica nítido.", + "whats_new.2_3_0.fields.title": "Só os formatos que usas", + "whats_new.2_3_0.fields.text": "Em Definições → Áreas, desativa as que não usas, como CAD, 3D ou tipos de letra. Os formatos delas saem da barra circular e das listas.", + "whats_new.2_3_0.translate.title": "Legendas e traduções no teu computador", + "whats_new.2_3_0.translate.text": "Cria legendas SRT a partir de um vídeo e traduz documentos, livros digitais e legendas com uma IA local. Nada sai do teu computador.", + "whats_new.2_3_0.more.title": "E mais algumas novidades", + "whats_new.2_3_0.more.text": "Copiar ou mover para uma pasta, recorte em perspetiva, ficheiros CMYK para impressão, ficheiros do Krita e do Minecraft, e ainda italiano, indonésio e vietnamita.", "preset.extract_motion.donut": "Extrair vídeo", "presets.copy_to_folder": "Copiar para pasta", "presets.copy_hint": "Copia ficheiros e pastas sem conversão. Os originais ficam onde estão; os nomes que já existem recebem um número.", diff --git a/src/Filee.App/Assets/i18n/ru.json b/src/Filee.App/Assets/i18n/ru.json index c6ef7e033..a91f89e6c 100644 --- a/src/Filee.App/Assets/i18n/ru.json +++ b/src/Filee.App/Assets/i18n/ru.json @@ -1397,6 +1397,18 @@ "whats_new.2_0_0.languages.text": "К английскому, корейскому, китайскому и польскому добавились испанский, хинди, японский, русский, арабский и бразильский португальский.", "whats_new.2_0_0.more.title": "И ещё немного", "whats_new.2_0_0.more.text": "Сбрасывайте одно круговое меню к настройкам по умолчанию, ищите по всей истории и конвертируйте документы на гуджарати, хинди и арабском через LibreOffice.", + "whats_new.2_3_0.annotate.title": "Пометки на картинках", + "whats_new.2_3_0.annotate.text": "Рисуйте стрелки, рамки, выделение, текст и номера прямо на картинке. Добавьте «Пометки» в меню в «Настройки → Круговое меню».", + "whats_new.2_3_0.crop_video.title": "Обрезка кадра видео", + "whats_new.2_3_0.crop_video.text": "Уберите чёрные полосы, найденные автоматически, или обрежьте кадр до 16:9, 9:16, 4:3, 1:1 или 21:9. HDR-видео остаётся в HDR.", + "whats_new.2_3_0.sheets.title": "Страниц на листе", + "whats_new.2_3_0.sheets.text": "Размещайте 2, 4, 6, 8 или 9 страниц PDF на каждом листе. Страницы остаются векторными, и текст остаётся чётким.", + "whats_new.2_3_0.fields.title": "Только нужные форматы", + "whats_new.2_3_0.fields.text": "Отключите в «Настройки → Области» то, с чем не работаете, например САПР, 3D или шрифты. Их форматы исчезнут из кругового меню и списков.", + "whats_new.2_3_0.translate.title": "Субтитры и перевод на вашем компьютере", + "whats_new.2_3_0.translate.text": "Создавайте субтитры SRT из видео и переводите документы, электронные книги и субтитры локальным ИИ. Ничего не покидает ваш компьютер.", + "whats_new.2_3_0.more.title": "И ещё немного", + "whats_new.2_3_0.more.text": "Копирование и перемещение в папку, обрезка с перспективой, файлы CMYK для печати, файлы Krita и Minecraft, а также итальянский, индонезийский и вьетнамский.", "preset.extract_motion.donut": "Видео из Live Photo", "presets.copy_to_folder": "Копировать в папку", "presets.copy_hint": "Копирует файлы и папки без преобразования. Оригиналы остаются на месте; к уже существующим именам добавляется номер.", diff --git a/src/Filee.App/Assets/i18n/th.json b/src/Filee.App/Assets/i18n/th.json index fd9df7206..814019959 100644 --- a/src/Filee.App/Assets/i18n/th.json +++ b/src/Filee.App/Assets/i18n/th.json @@ -1377,6 +1377,18 @@ "whats_new.2_0_0.languages.text": "เพิ่มภาษาสเปน ฮินดี ญี่ปุ่น รัสเซีย อาหรับ และโปรตุเกสบราซิล ร่วมกับอังกฤษ เกาหลี จีน และโปแลนด์", "whats_new.2_0_0.more.title": "และอีกมากมาย", "whats_new.2_0_0.more.text": "รีเซ็ตโดนัทแต่ละอันกลับเป็นค่าเริ่มต้น ค้นหาประวัติการแปลงไฟล์ทั้งหมด และแปลงเอกสารภาษาคุชราต ฮินดี หรืออาหรับผ่าน LibreOffice", + "whats_new.2_3_0.annotate.title": "ใส่คำอธิบายบนรูปภาพ", + "whats_new.2_3_0.annotate.text": "วาดลูกศร กรอบ ไฮไลต์ ข้อความ และหมายเลขลงบนรูปภาพได้โดยตรง เพิ่ม “ใส่คำอธิบาย” ลงในโดนัทได้ที่การตั้งค่า → แถบโดนัท", + "whats_new.2_3_0.crop_video.title": "ครอบตัดวิดีโอ", + "whats_new.2_3_0.crop_video.text": "ลบแถบดำที่ตรวจพบโดยอัตโนมัติ หรือครอบตัดเป็น 16:9, 9:16, 4:3, 1:1 หรือ 21:9 วิดีโอ HDR ยังคงเป็น HDR", + "whats_new.2_3_0.sheets.title": "หลายหน้าต่อแผ่น", + "whats_new.2_3_0.sheets.text": "วาง PDF 2, 4, 6, 8 หรือ 9 หน้าลงในแต่ละแผ่น หน้ายังคงเป็นเวกเตอร์ ข้อความจึงคมชัด", + "whats_new.2_3_0.fields.title": "เฉพาะรูปแบบที่คุณใช้", + "whats_new.2_3_0.fields.text": "ปิดสาขาที่คุณไม่ได้ใช้ เช่น CAD, 3D หรือแบบอักษร ได้ที่การตั้งค่า → สาขา รูปแบบของสาขานั้นจะหายไปจากโดนัทและรายการ", + "whats_new.2_3_0.translate.title": "คำบรรยายและการแปลบนคอมพิวเตอร์ของคุณ", + "whats_new.2_3_0.translate.text": "สร้างคำบรรยาย SRT จากวิดีโอ และแปลเอกสาร อีบุ๊ก และคำบรรยายด้วย AI ในเครื่อง ไม่มีอะไรออกไปจากคอมพิวเตอร์ของคุณ", + "whats_new.2_3_0.more.title": "และอีกมากมาย", + "whats_new.2_3_0.more.text": "คัดลอกหรือย้ายไปยังโฟลเดอร์ ครอบตัดแบบเปอร์สเปกทีฟ ไฟล์ CMYK สำหรับงานพิมพ์ ไฟล์ Krita และ Minecraft รวมถึงภาษาอิตาลี อินโดนีเซีย และเวียดนาม", "preset.extract_motion.donut": "ดึงวิดีโอจาก Live Photo", "presets.copy_to_folder": "คัดลอกไปยังโฟลเดอร์", "presets.copy_hint": "คัดลอกไฟล์และโฟลเดอร์โดยไม่แปลง ต้นฉบับยังอยู่ที่เดิม ชื่อที่มีอยู่แล้วจะได้หมายเลขต่อท้าย", diff --git a/src/Filee.App/Assets/i18n/tr.json b/src/Filee.App/Assets/i18n/tr.json index 18d1b3bd6..307399a58 100644 --- a/src/Filee.App/Assets/i18n/tr.json +++ b/src/Filee.App/Assets/i18n/tr.json @@ -1375,6 +1375,18 @@ "whats_new.2_0_0.languages.text": "İspanyolca, Hintçe, Japonca, Rusça, Arapça ve Brezilya Portekizcesi; İngilizce, Korece, Çince ve Lehçeye katılıyor.", "whats_new.2_0_0.more.title": "Ve biraz daha fazlası", "whats_new.2_0_0.more.text": "Tek bir halkayı varsayılanlarına sıfırlayın, tüm geçmişte arama yapın ve Guceratça, Hintçe veya Arapça belgeleri LibreOffice ile dönüştürün.", + "whats_new.2_3_0.annotate.title": "Resimlere not ekleyin", + "whats_new.2_3_0.annotate.text": "Resmin üzerine doğrudan ok, çerçeve, vurgu, metin ve numara çizin. Not ekle'yi Ayarlar → Halka araç çubuğu bölümünden bir halkaya ekleyin.", + "whats_new.2_3_0.crop_video.title": "Videoları kırpın", + "whats_new.2_3_0.crop_video.text": "Otomatik bulunan siyah bantları kaldırın ya da 16:9, 9:16, 4:3, 1:1 veya 21:9 oranında kırpın. HDR video HDR olarak kalır.", + "whats_new.2_3_0.sheets.title": "Yaprak başına sayfa", + "whats_new.2_3_0.sheets.text": "Her yaprağa 2, 4, 6, 8 veya 9 PDF sayfası yerleştirin. Sayfalar vektör olarak kalır, metin net görünür.", + "whats_new.2_3_0.fields.title": "Yalnızca kullandığınız biçimler", + "whats_new.2_3_0.fields.text": "CAD, 3B veya yazı tipleri gibi çalışmadığınız alanları Ayarlar → Alanlar bölümünden kapatın. Biçimleri halkadan ve listelerden kalkar.", + "whats_new.2_3_0.translate.title": "Bilgisayarınızda altyazı ve çeviri", + "whats_new.2_3_0.translate.text": "Videodan SRT altyazı oluşturun; belgeleri, e-kitapları ve altyazıları yerel bir yapay zekâyla çevirin. Hiçbir şey bilgisayarınızdan çıkmaz.", + "whats_new.2_3_0.more.title": "Ve biraz daha fazlası", + "whats_new.2_3_0.more.text": "Klasöre kopyalama veya taşıma, perspektif kırpma, baskı için CMYK dosyaları, Krita ve Minecraft dosyaları, ayrıca İtalyanca, Endonezce ve Vietnamca.", "preset.extract_motion.donut": "Live Photo videosunu çıkar", "presets.copy_to_folder": "Klasöre kopyala", "presets.copy_hint": "Dosyaları ve klasörleri dönüştürmeden kopyalar. Orijinaller yerinde kalır; var olan adlara bir numara eklenir.", diff --git a/src/Filee.App/Assets/i18n/vi.json b/src/Filee.App/Assets/i18n/vi.json index e90ce1e5b..f619e0d39 100644 --- a/src/Filee.App/Assets/i18n/vi.json +++ b/src/Filee.App/Assets/i18n/vi.json @@ -1397,6 +1397,18 @@ "whats_new.2_0_0.languages.text": "Tiếng Tây Ban Nha, tiếng Hindi, tiếng Nhật, tiếng Nga, tiếng Ả Rập và tiếng Bồ Đào Nha (Brazil) cùng góp mặt với tiếng Anh, tiếng Hàn, tiếng Trung và tiếng Ba Lan.", "whats_new.2_0_0.more.title": "Và thêm một chút nữa", "whats_new.2_0_0.more.text": "Đặt lại riêng một vòng tròn về mặc định, tìm kiếm toàn bộ lịch sử và chuyển đổi tài liệu tiếng Gujarat, tiếng Hindi hoặc tiếng Ả Rập qua LibreOffice.", + "whats_new.2_3_0.annotate.title": "Chú thích trên ảnh", + "whats_new.2_3_0.annotate.text": "Vẽ mũi tên, khung, tô sáng, chữ và số thứ tự ngay trên ảnh. Thêm Chú thích vào một vòng tròn trong Cài đặt → Thanh công cụ vòng tròn.", + "whats_new.2_3_0.crop_video.title": "Cắt khung video", + "whats_new.2_3_0.crop_video.text": "Gỡ viền đen được tự động tìm thấy, hoặc cắt theo tỉ lệ 16:9, 9:16, 4:3, 1:1 hay 21:9. Video HDR vẫn giữ HDR.", + "whats_new.2_3_0.sheets.title": "Số trang mỗi tờ", + "whats_new.2_3_0.sheets.text": "Đặt 2, 4, 6, 8 hoặc 9 trang PDF lên mỗi tờ. Các trang vẫn là vector nên chữ vẫn sắc nét.", + "whats_new.2_3_0.fields.title": "Chỉ những định dạng bạn dùng", + "whats_new.2_3_0.fields.text": "Tắt các lĩnh vực bạn không làm, như CAD, 3D hay phông chữ, trong Cài đặt → Lĩnh vực. Định dạng của chúng sẽ biến mất khỏi vòng tròn và các danh sách.", + "whats_new.2_3_0.translate.title": "Phụ đề và bản dịch ngay trên máy tính", + "whats_new.2_3_0.translate.text": "Tạo phụ đề SRT từ video, và dịch tài liệu, sách điện tử và phụ đề bằng AI cục bộ. Không có gì rời khỏi máy tính của bạn.", + "whats_new.2_3_0.more.title": "Và thêm một chút nữa", + "whats_new.2_3_0.more.text": "Sao chép hoặc di chuyển vào thư mục, cắt phối cảnh, tệp CMYK để in, tệp Krita và Minecraft, cùng tiếng Ý, tiếng Indonesia và tiếng Việt.", "preset.extract_motion.donut": "Trích xuất video", "presets.copy_to_folder": "Sao chép vào thư mục", "presets.copy_hint": "Sao chép tệp và thư mục mà không chuyển đổi. Bản gốc được giữ tại chỗ; tên đã tồn tại sẽ được thêm số.", diff --git a/src/Filee.App/Assets/i18n/zh-CN.json b/src/Filee.App/Assets/i18n/zh-CN.json index 7b7ff0f73..60a442c7f 100644 --- a/src/Filee.App/Assets/i18n/zh-CN.json +++ b/src/Filee.App/Assets/i18n/zh-CN.json @@ -1397,6 +1397,18 @@ "whats_new.2_0_0.languages.text": "在英语、韩语、中文和波兰语之外,新增西班牙语、印地语、日语、俄语、阿拉伯语和巴西葡萄牙语。", "whats_new.2_0_0.more.title": "还有更多", "whats_new.2_0_0.more.text": "可以只把一个环形工具栏恢复默认、搜索全部历史记录,并通过 LibreOffice 转换古吉拉特语、印地语或阿拉伯语文档。", + "whats_new.2_3_0.annotate.title": "给图片加批注", + "whats_new.2_3_0.annotate.text": "直接在图片上画箭头、方框、高亮、文字和编号。可在“设置 → 环形工具栏”中把批注加到圆环上。", + "whats_new.2_3_0.crop_video.title": "裁剪视频画面", + "whats_new.2_3_0.crop_video.text": "自动找出并去除黑边,或裁剪为 16:9、9:16、4:3、1:1 或 21:9。HDR 视频仍保持 HDR。", + "whats_new.2_3_0.sheets.title": "多页合一", + "whats_new.2_3_0.sheets.text": "每张纸放 2、4、6、8 或 9 页 PDF。页面保持矢量,文字依然清晰。", + "whats_new.2_3_0.fields.title": "只显示您用的格式", + "whats_new.2_3_0.fields.text": "在“设置 → 领域”中关闭不用的领域,比如 CAD、3D 或字体,它们的格式就会从圆环和列表中消失。", + "whats_new.2_3_0.translate.title": "在本机生成字幕和翻译", + "whats_new.2_3_0.translate.text": "从视频生成 SRT 字幕,并用本机 AI 翻译文档、电子书和字幕。文件不会离开您的电脑。", + "whats_new.2_3_0.more.title": "还有更多", + "whats_new.2_3_0.more.text": "复制或移动到文件夹、透视校正裁剪、印刷用 CMYK 文件、Krita 和 Minecraft 文件,以及意大利语、印度尼西亚语和越南语。", "preset.extract_motion.donut": "提取实况/动态照片视频", "presets.copy_to_folder": "复制到文件夹", "presets.copy_hint": "直接复制文件和文件夹,不转换格式。原件保留,重名时自动编号。", diff --git a/src/Filee.App/Assets/i18n/zh-TW.json b/src/Filee.App/Assets/i18n/zh-TW.json index b0eadb8e3..5357f4cff 100644 --- a/src/Filee.App/Assets/i18n/zh-TW.json +++ b/src/Filee.App/Assets/i18n/zh-TW.json @@ -1397,6 +1397,18 @@ "whats_new.2_0_0.languages.text": "在英語、韓語、中文和波蘭語之外,新增西班牙語、印地語、日語、俄語、阿拉伯語和巴西葡萄牙語。", "whats_new.2_0_0.more.title": "還有更多", "whats_new.2_0_0.more.text": "可以只把一個環形工具列還原為初始設定、搜尋全部歷史記錄,並透過 LibreOffice 轉換古吉拉特語、印地語或阿拉伯語文件。", + "whats_new.2_3_0.annotate.title": "為圖片加上註解", + "whats_new.2_3_0.annotate.text": "直接在圖片上畫箭頭、方框、螢光標示、文字和編號。可在「設定 → 環形工具列」中把註解加到圓環上。", + "whats_new.2_3_0.crop_video.title": "裁剪影片畫面", + "whats_new.2_3_0.crop_video.text": "自動找出並移除黑邊,或裁剪為 16:9、9:16、4:3、1:1 或 21:9。HDR 影片仍維持 HDR。", + "whats_new.2_3_0.sheets.title": "多頁合一", + "whats_new.2_3_0.sheets.text": "每張紙放 2、4、6、8 或 9 頁 PDF。頁面保持向量,文字依然清晰。", + "whats_new.2_3_0.fields.title": "只顯示您用的格式", + "whats_new.2_3_0.fields.text": "在「設定 → 領域」中關閉不用的領域,例如 CAD、3D 或字型,它們的格式就會從圓環和清單中消失。", + "whats_new.2_3_0.translate.title": "在本機產生字幕與翻譯", + "whats_new.2_3_0.translate.text": "從影片產生 SRT 字幕,並用本機 AI 翻譯文件、電子書和字幕。檔案不會離開您的電腦。", + "whats_new.2_3_0.more.title": "還有更多", + "whats_new.2_3_0.more.text": "複製或移動到資料夾、透視校正裁剪、印刷用 CMYK 檔案、Krita 和 Minecraft 檔案,以及義大利語、印尼語和越南語。", "preset.extract_motion.donut": "擷取原況/動態相片影片", "presets.copy_to_folder": "複製到資料夾", "presets.copy_hint": "不轉換,直接複製檔案和資料夾。原始檔案留在原處;同名時會加上編號。", diff --git a/tests/Filee.App.Tests/WhatsNewMedia.cs b/tests/Filee.App.Tests/WhatsNewMedia.cs index 2b94cb430..1f4f7d777 100644 --- a/tests/Filee.App.Tests/WhatsNewMedia.cs +++ b/tests/Filee.App.Tests/WhatsNewMedia.cs @@ -44,7 +44,7 @@ public class WhatsNewMedia /// The light theme's PageBrush: the canvas behind every clip. private const string Page = "#F6F5F1"; - /// Where the 2.0.0 slides live in the repository. + /// Where the slides of live in the repository. internal static string Folder(string version) => Path.Combine(RepositoryRoot, "src", "Filee.App", "Assets", "WhatsNew", version); @@ -98,6 +98,33 @@ public void Generate_2_0_0() } } + [AvaloniaFact(Explicit = true)] + public void Generate_2_3_0() + { + TestServices.EnsureInitialized("en"); + var folder = Folder("2.3.0"); + var theme = AppHost.Get(); + Motion.Set(true); + var culture = System.Globalization.CultureInfo.CurrentCulture; + System.Globalization.CultureInfo.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("en-US"); + try + { + theme.Apply(new ThemeSettings { ReduceMotion = false, Mode = ThemeMode.Light }, "en"); + Write(folder, "annotate.webp", Annotate); + Write(folder, "crop_video.webp", CropVideo); + Write(folder, "sheets.webp", Sheets); + Write(folder, "fields.webp", Fields); + Write(folder, "translate.webp", Translate); + Write(folder, "more.webp", AndMore); + } + finally + { + System.Globalization.CultureInfo.CurrentCulture = culture; + theme.Apply(new ThemeSettings(), "en"); + TestServices.EnsureInitialized("en"); + } + } + // ───────────────────────── Scenes ───────────────────────── /// @@ -636,6 +663,1000 @@ private static Clip More(string version) return clip; } + // ───────────────────────── Scenes of 2.3.0 ───────────────────────── + + /// + /// Annotate: a web page screenshot marked up with the tools of the bar, each picked with the pointer: a + /// highlight over the title, a box round the button, an arrow to it, a label typed in place and two numbers. + /// + private static Clip Annotate() + { + var clip = new Clip { Quality = 74 }; + var folder = Directory.CreateTempSubdirectory("filee-whatsnew-annotate-").FullName; + var store = AppHost.Get(); + var tool = store.FindPreset(BuiltInData.AnnotateId) ?? BuiltInData.AnnotatePresets().Single(); + AnnotateTool.Remember(tool, AnnotateTool.Colors[0], AnnotationWidth.Medium); + AnnotateWindow? window = null; + try + { + var picture = Path.Combine(folder, "lisbon.png"); + DrawWebPage(picture); + var vm = new AnnotateViewModel([picture], tool, AppHost.Get(), store, _ => BuiltInData.SameAsSource, (_, _) => { }); + window = new AnnotateWindow { DataContext = vm, Width = 1024, Height = 600 }; + window.Show(); + WaitFor(() => vm.PreviewImage is not null && !vm.IsBusy); + Pump(); + + var bounds = window.AnnotationCanvas.CalculateImageBounds(); + Avalonia.Point On(double x, double y) => + window.AnnotationCanvas.TranslatePoint(new Avalonia.Point(bounds.X + x * bounds.Width, bounds.Y + y * bounds.Height), window)!.Value; + Avalonia.Point Middle(Control control) => + control.TranslatePoint(new Avalonia.Point(control.Bounds.Width / 2, control.Bounds.Height / 2), window)!.Value; + Control Named(string name) => window.GetVisualDescendants().OfType().First(c => c.Name == name); + Avalonia.Rect InWindow(Control control) => new(control.TranslatePoint(default, window)!.Value, control.Bounds.Size); + + // The picture and the bar under it, closer than the whole window shows them (the file name above and the + // keys below are left out). + var shown = new Avalonia.Rect(On(0, 0), On(1, 1)).Union(InWindow(Named("SelectTool"))).Union(InWindow(Named("SaveButton"))).Inflate(14); + var height = Math.Max(shown.Height, shown.Width * CanvasHeight / CanvasWidth); + var region = new Avalonia.Rect(shown.Center.X - height * CanvasWidth / CanvasHeight / 2, shown.Center.Y - height / 2, + height * CanvasWidth / CanvasHeight, height); + var scene = new Scene(window, clip) { FrameLength = 1 / 15.0, Region = region }; + void Pick(string name) + { + scene.MoveTo(Middle(Named(name)) + new Avalonia.Vector(3, 4), 0.45); + scene.Hold(0.1); + scene.Click(); + scene.Hold(0.15); + } + void Swatch(int index) + { + var swatch = window.GetVisualDescendants().OfType().Where(b => b.Classes.Contains("swatch")).ElementAt(index); + scene.MoveTo(Middle(swatch) + new Avalonia.Vector(2, 3), 0.35); + scene.Click(); + scene.Hold(0.15); + } + void Drag((double X, double Y) from, (double X, double Y) to, double seconds) + { + scene.MoveTo(On(from.X, from.Y), 0.4); + scene.Press(); + scene.MoveTo(On(to.X, to.Y), seconds); + scene.Release(); + scene.Hold(0.25); + } + + // Frame 0: the picture open in the window, nothing drawn yet. + scene.Place(On(0.45, 0.42)); + scene.Hold(0.6); + + // A highlight over the title, in yellow. + Pick("HighlightTool"); + Swatch(2); + Drag((0.565, 0.16), (0.905, 0.225), 0.6); + + // A box round the button, in red. + Pick("BoxTool"); + Swatch(0); + Drag((0.558, 0.505), (0.776, 0.632), 0.6); + + // An arrow pointing at it. + Pick("ArrowTool"); + Drag((0.375, 0.775), (0.545, 0.62), 0.5); + + // A label typed where it goes. + Pick("TextTool"); + scene.MoveTo(On(0.2, 0.81), 0.4); + scene.Click(); + WaitFor(() => window.Editor.IsVisible); + scene.Hold(0.2); + foreach (var letter in "Book this one") + { + window.KeyTextInput(letter.ToString()); + scene.Tick(); + if (letter == ' ') + scene.Tick(); + } + scene.Hold(0.3); + scene.Key("Enter", () => window.KeyPress(Key.Enter, RawInputModifiers.None, PhysicalKey.Enter, null)); + window.KeyRelease(Key.Enter, RawInputModifiers.None, PhysicalKey.Enter, null); + scene.Hold(0.3); + scene.HideKey(); + + // Numbers count up: 1 on the picture, 2 on the price. + Pick("NumberTool"); + foreach (var (x, y) in new[] { (0.07, 0.19), (0.553, 0.405) }) + { + scene.MoveTo(On(x, y), 0.45); + scene.Click(); + scene.Hold(0.3); + } + + scene.MoveTo(On(0.47, 0.5), 0.5); + scene.Hold(0.2); + clip.MarkPoster(); + scene.Hold(2.0); + Assert.Equal(6, vm.Marks.Count); + } + finally + { + window?.Close(); + try + { + Directory.Delete(folder, recursive: true); + } + catch (IOException) { } + } + return clip; + } + + /// + /// Crop video (#830): a film with black bars, dragged onto the video donut; Tab turns it to its back face, the + /// "Crop video" slice fans out its ring and the film is dropped on "Auto" (remove black bars); then the frame loses + /// its bars. + /// + private static Clip CropVideo() + { + var clip = new Clip { Quality = 76 }; + var folder = Directory.CreateTempSubdirectory("filee-whatsnew-video-").FullName; + try + { + // A 2.39:1 film in a 16:9 frame. + var still = Path.Combine(folder, "still.jpg"); + DrawLandscape(still, 2); + using var picture = new MagickImage(still); + picture.Crop(new MagickGeometry(0, 140, 1600, 670)); + picture.ResetPage(); + picture.Resize(new MagickGeometry(544, 228) { IgnoreAspectRatio = true }); + var bars = (306 - picture.Height) / 2.0; // 544 × 306 is 16:9 + + // Frame 0: the film with its bars. + Film(clip, picture, bars, 0, 1100); + + using (var drag = new DonutDrag(clip, [@"C:\Videos\trailer.mp4"])) + { + drag.Open(drag.Slice(1, 0.55)); + drag.Flip(); + var group = drag.Index("crop-video"); + drag.Scene.MoveTo(drag.Slice(group, 0.6), 0.4); + drag.Scene.MoveTo(drag.Slice(group, 0.93), 0.25); + drag.Scene.Hold(0.3); + drag.Scene.MoveTo(drag.Sub(group, 0) + new Avalonia.Vector(-2, 10), 0.5); + drag.Scene.Hold(0.5); + Assert.Equal(new DonutHit(DonutHitKind.SubSegment, group, 0), drag.Hit); + // Shown with reduced motion: the ring of ratios, the drop on Auto. + clip.MarkPoster(); + drag.Scene.Hold(0.6); + drag.Drop(0.8, 0.6); + } + + // The bars found are marked and cut away. + Film(clip, picture, bars, 0, 300); + for (var t = 1; t <= 6; t++) + Film(clip, picture, bars, t / 6.0, 50); + clip.Hold(400); + for (var t = 1; t <= 14; t++) + Film(clip, picture, bars * (1 - Scene.EaseInOut(t / 14.0)), 1 - t / 14.0, 50); + clip.Hold(2000); + } + finally + { + try + { + Directory.Delete(folder, recursive: true); + } + catch (IOException) { } + } + return clip; + + // The frame of a video with a play bar under it: the picture with black bars of above and below, and a + // dashed line round the picture (0 to 1) visible. + static void Film(Clip clip, MagickImage picture, double bars, double outline, int milliseconds) + { + var (width, height) = ((double)picture.Width, picture.Height + 2 * bars); + var (left, top) = ((CanvasWidth - width) / 2, (CanvasHeight - height) / 2 - 12); + var canvas = new MagickImage(new MagickColor(Page), CanvasWidth, CanvasHeight); + new Drawables() + .FillColor(new MagickColor("#00000014")).RoundRectangle(left - 2, top + 3, left + width + 2, top + height + 8, 12, 12) + .FillColor(new MagickColor("#0B0B0C")).RoundRectangle(left, top, left + width, top + height, 10, 10) + .Draw(canvas); + canvas.Composite(picture, (int)Math.Round(left), (int)Math.Round(top + bars), CompositeOperator.Over); + var barTop = top + height + 22; + var drawing = new Drawables() + .FillColor(new MagickColor("#1F1E1D")).Polygon(new PointD(left + 4, barTop - 7), new PointD(left + 4, barTop + 7), new PointD(left + 16, barTop)) + .FillColor(new MagickColor("#DAD7CF")).RoundRectangle(left + 32, barTop - 3, left + width, barTop + 3, 3, 3) + .FillColor(new MagickColor("#7F77DD")).RoundRectangle(left + 32, barTop - 3, left + 220, barTop + 3, 3, 3) + .FillColor(new MagickColor("#7F77DD")).Circle(left + 220, barTop, left + 227, barTop); + if (outline > 0) + drawing + .FillColor(MagickColors.Transparent).StrokeColor(new MagickColor(255, 255, 255, (byte)Math.Round(255 * outline))) + .StrokeWidth(2).StrokeDashArray(9, 6) + .Rectangle(left + 1.5, top + bars + 1.5, left + width - 1.5, top + bars + picture.Height - 1.5); + drawing.Draw(canvas); + clip.Add(canvas, milliseconds); + } + } + + /// + /// Pages per sheet (#737): an eight-page PDF dragged onto the PDF donut, the "Pages per sheet" slice fanning out + /// 2, 4, 6, 8 and 9, the drop on 4; then the pages going onto their two sheets. The sheets are the PdfSheets output + /// for that PDF, rendered; the pages fly to where it put them. + /// + private static Clip Sheets() + { + var clip = new Clip { Quality = 78 }; + var folder = Directory.CreateTempSubdirectory("filee-whatsnew-sheets-").FullName; + // Pages per sheet is in the palette, on no donut: put on the PDF donut's front for the clip. + var pdf = AppHost.Get().Profiles.Single(p => p.Id == "pdf"); + var front = pdf.PresetIds.ToList(); + pdf.PresetIds[pdf.PresetIds.IndexOf("to-hwp")] = "pdf-nup"; + var pages = new List(); + var sheets = new List(); + try + { + var source = Path.Combine(folder, "chapters.pdf"); + var output = Path.Combine(folder, "chapters_4.pdf"); + DrawDocument(source, folder); + var options = BuiltInData.CreatePresets().Single(p => p.Id == "pdf-nup-4").Pdf; + Filee.Engines.Pdf.PdfSheets.Write(source, output, options, null, CancellationToken.None); + var input = Filee.Engines.Pdf.PdfPreviewDocument.Open(source); + var result = Filee.Engines.Pdf.PdfPreviewDocument.Open(output); + Assert.Equal((8, 2), (input.PageCount, result.PageCount)); + pages.AddRange(Enumerable.Range(1, 8).Select(n => new MagickImage(input.RenderPage(n, 480).Png))); + sheets.AddRange(Enumerable.Range(1, 2).Select(n => new MagickImage(result.RenderPage(n, 720).Png))); + + // Where everything goes on the canvas: the pages in a row, the two sheets side by side, and in the sheets + // the cells PdfSheets drew the pages in. + var (pageWidth, pageHeight) = (84.0, 84.0 * pages[0].Height / pages[0].Width); + var row = Enumerable.Range(0, 8).Select(k => new Avalonia.Rect( + (CanvasWidth - (8 * pageWidth + 7 * 9)) / 2 + k * (pageWidth + 9), (CanvasHeight - pageHeight) / 2, pageWidth, pageHeight)).ToList(); + var sheetHeight = 356.0; + var sheetWidth = sheetHeight * sheets[0].Width / sheets[0].Height; + var sheetAt = Enumerable.Range(0, 2).Select(k => new Avalonia.Rect( + (CanvasWidth - (2 * sheetWidth + 44)) / 2 + k * (sheetWidth + 44), (CanvasHeight - sheetHeight) / 2, sheetWidth, sheetHeight)).ToList(); + var a4 = new PdfSharp.Drawing.XSize(595.28, 841.89); + var cells = Filee.Engines.Pdf.PdfSheets.Cells(a4, options, 4); + var targets = Enumerable.Range(0, 8).Select(k => + { + var (sheet, cell) = (sheetAt[k / 4], cells[k % 4]); + var scale = sheet.Width / a4.Width; + var fit = Math.Min(cell.Width / pageWidth, cell.Height / pageHeight) * scale; + var (w, h) = (pageWidth * fit, pageHeight * fit); + return new Avalonia.Rect(sheet.X + (cell.X + cell.Width / 2) * scale - w / 2, sheet.Y + (cell.Y + cell.Height / 2) * scale - h / 2, w, h); + }).ToList(); + + // Frame 0: the eight pages. + clip.Add(Layout(pages, row, null, 0), 1100); + + using (var drag = new DonutDrag(clip, [@"C:\Documents\chapters.pdf"])) + { + var group = drag.Index("pdf-nup"); + drag.Open(drag.Slice(group, 0.45)); + drag.Scene.MoveTo(drag.Slice(group, 0.93), 0.3); + drag.Scene.Hold(0.3); + drag.Scene.MoveTo(drag.Sub(group, 1) + new Avalonia.Vector(0, 6), 0.5); + drag.Scene.Hold(0.9); + Assert.Equal(new DonutHit(DonutHitKind.SubSegment, group, 1), drag.Hit); + drag.Drop(0.8, 0.6); + } + + // The pages fly onto the sheets, one after another, as the sheets come in under them. + clip.Add(Layout(pages, row, null, 0), 400); + const int frames = 22; + for (var f = 1; f <= frames; f++) + { + var places = row.Select((from, k) => + { + var u = Scene.EaseInOut(Math.Clamp((f / (double)frames * 1.6) - k * 0.075, 0, 1)); + var to = targets[k]; + return new Avalonia.Rect(from.X + (to.X - from.X) * u, from.Y + (to.Y - from.Y) * u, + from.Width + (to.Width - from.Width) * u, from.Height + (to.Height - from.Height) * u); + }).ToList(); + clip.Add(Layout(pages, places, sheetAt, Math.Min(1, f / (frames * 0.4))), 50); + } + // The sheets as PdfSheets wrote them. + var final = new MagickImage(new MagickColor(Page), CanvasWidth, CanvasHeight); + for (var k = 0; k < 2; k++) + { + DrawPaper(final, sheetAt[k], 1); + using var sheet = (MagickImage)sheets[k].Clone(); + sheet.FilterType = FilterType.Lanczos; + sheet.Resize((uint)Math.Round(sheetAt[k].Width), (uint)Math.Round(sheetAt[k].Height)); + final.Composite(sheet, (int)Math.Round(sheetAt[k].X), (int)Math.Round(sheetAt[k].Y), CompositeOperator.Over); + } + clip.Add(final, 2400); + // Shown with reduced motion: the two sheets. + clip.MarkPoster(); + } + finally + { + pdf.PresetIds = front; + foreach (var image in pages.Concat(sheets)) + image.Dispose(); + try + { + Directory.Delete(folder, recursive: true); + } + catch (IOException) { } + } + return clip; + + // The pages at , over the sheets at ( 0 to 1 of the way in). + static MagickImage Layout(List pages, List places, List? sheetAt, double paper) + { + var canvas = new MagickImage(new MagickColor(Page), CanvasWidth, CanvasHeight); + if (sheetAt is not null && paper > 0) + foreach (var sheet in sheetAt) + DrawPaper(canvas, sheet, paper); + for (var k = 0; k < pages.Count; k++) + { + var at = places[k]; + using var page = (MagickImage)pages[k].Clone(); + page.FilterType = FilterType.Lanczos; + page.Resize((uint)Math.Round(at.Width), (uint)Math.Round(at.Height)); + if (sheetAt is null) + new Drawables().FillColor(new MagickColor("#0000001A")) + .RoundRectangle(at.X - 1, at.Y + 1, at.Right + 1, at.Bottom + 3, 3, 3).Draw(canvas); + canvas.Composite(page, (int)Math.Round(at.X), (int)Math.Round(at.Y), CompositeOperator.Over); + } + return canvas; + } + + // A sheet of paper with a soft shadow, 0 to 1. + static void DrawPaper(MagickImage canvas, Avalonia.Rect sheet, double opacity) + { + var alpha = (byte)Math.Round(255 * opacity); + new Drawables() + .FillColor(new MagickColor(0, 0, 0, (byte)Math.Round(0.08 * alpha))).RoundRectangle(sheet.X - 2, sheet.Y + 2, sheet.Right + 2, sheet.Bottom + 6, 6, 6) + .FillColor(new MagickColor(255, 255, 255, alpha)).Rectangle(sheet.X, sheet.Y, sheet.Right, sheet.Bottom) + .Draw(canvas); + } + } + + /// + /// Settings → Fields (#806): "Design & print" and then "Games" switched off on the page, and the pictures donut + /// beside it opening again without their slices (PSD and EPS, then DDS). The page gets real pointer input; the donut + /// is the one the next drop opens, played on the clip's clock. + /// + private static Clip Fields() + { + var clip = new Clip { Quality = 78 }; + var store = AppHost.Get(); + var loc = AppHost.Get(); + var turnedOff = store.Settings.TurnedOffFields.ToList(); + var images = store.Profiles.Single(p => p.Id == "images"); + var front = images.PresetIds.ToList(); + // Someone who works in design and games: PSD, EPS and DDS on the pictures donut, those fields on. + Preset[] added = + [ + new() { Id = "whatsnew-psd", Name = "PSD", TargetFormat = "psd" }, + new() { Id = "whatsnew-eps", Name = "EPS", TargetFormat = "eps" }, + new() { Id = "whatsnew-dds", Name = "DDS", TargetFormat = "dds" }, + ]; + store.Presets.AddRange(added); + images.PresetIds = ["to-png", "to-jpg", "to-webp", "whatsnew-psd", "whatsnew-eps", "whatsnew-dds", "to-pdf", "trace-svg"]; + store.Settings.TurnedOffFields = Filee.Core.Formats.FormatFields.All + .Where(f => f.CanTurnOff && !f.OnByDefault && f.Id is not ("design" or "game")).Select(f => f.Id).ToList(); + string[] files = [@"C:\Pictures\poster.png"]; + var page = new Window + { + Width = 470, + Height = 1100, + Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse(Page)), + Content = new Border { Padding = new Avalonia.Thickness(24), Child = new FieldsPage { DataContext = new FieldsPageViewModel(store, loc) } }, + }; + var (donutWindow, vm) = OpenDonut(files, clickMode: true); + var donut = donutWindow.DonutControl; + try + { + page.Show(); + Pump(); + ToolTip.SetServiceEnabled(page, false); + Assert.Equal(8, vm.Items.Count); + var switches = page.GetVisualDescendants().OfType().ToList(); + ToggleSwitch Switch(string id) => switches.Single(s => Avalonia.Automation.AutomationProperties.GetName(s) == loc[$"fields.name.{id}"]); + Avalonia.Rect InPage(Control control) => new(control.TranslatePoint(default, page)!.Value, control.Bounds.Size); + + // The card from its top down to the Developer row. + var card = InPage(page.GetVisualDescendants().OfType().First(x => x.Classes.Contains("card"))); + var developer = Switch("developer").FindAncestorOfType()!.GetVisualDescendants().OfType().Max(t => InPage(t).Bottom); + var region = new Avalonia.Rect(card.X - 8, card.Y - 8, card.Width + 16, developer - card.Y + 18); + const double donutSide = 344; + var pageScale = Math.Min(1, Math.Min(404 / region.Width, (CanvasHeight - 12) / region.Height)); + var pageAt = new Avalonia.Point(18, (CanvasHeight - region.Height * pageScale) / 2); + var design = InPage(Switch("design")); + var pointer = new Avalonia.Point(design.X - 120, design.Y + 40); + var pressed = false; + + MagickImage Frame() + { + var canvas = new MagickImage(new MagickColor(Page), CanvasWidth, CanvasHeight); + // Drawn directly: a frame of the render timer would also run the donut's frame loop. + using (var shot = Snapshot(page, direct: true)) + { + shot.Crop(new MagickGeometry((int)region.X, (int)region.Y, (uint)region.Width, (uint)region.Height)); + shot.ResetPage(); + shot.FilterType = FilterType.Lanczos; + shot.Resize((uint)Math.Round(region.Width * pageScale), (uint)Math.Round(region.Height * pageScale)); + canvas.Composite(shot, (int)pageAt.X, (int)Math.Round(pageAt.Y), CompositeOperator.Over); + } + using (var shot = Snapshot(donutWindow)) + { + var scale = Math.Min(1, donutSide / shot.Width); + shot.FilterType = FilterType.Lanczos; + shot.Resize((uint)Math.Round(shot.Width * scale), (uint)Math.Round(shot.Height * scale)); + canvas.Composite(shot, (int)(CanvasWidth - 12 - donutSide + (donutSide - shot.Width) / 2), (int)((CanvasHeight - shot.Height) / 2), CompositeOperator.Over); + } + DrawPointer(canvas, new PointD(pageAt.X + (pointer.X - region.X) * pageScale, pageAt.Y + (pointer.Y - region.Y) * pageScale), pressed); + return canvas; + } + // The render timer plays the switch's transition, but also the donut's own frame loop on wall-clock time: it + // runs only while the donut is at rest. + void Tick(double seconds = 0.05, bool renderTimer = true) + { + for (var t = 0.0; t < seconds - 1e-9; t += 0.05) + { + KeepFramesComing(donut); + donut.Advance(0.05); + KeepFramesComing(donut); + for (var i = 0; i < 3; i++) + { + Dispatcher.UIThread.RunJobs(); + if (renderTimer) + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); + } + clip.Add(Frame(), 50); + } + } + void MoveTo(Avalonia.Point to, double seconds) + { + var from = pointer; + var frames = Math.Max(1, (int)Math.Round(seconds / 0.05)); + var normal = new Avalonia.Vector(from.Y - to.Y, to.X - from.X) * 0.06; + for (var i = 1; i <= frames; i++) + { + var u = Scene.EaseInOut(i / (double)frames); + pointer = new Avalonia.Point(from.X + (to.X - from.X) * u, from.Y + (to.Y - from.Y) * u) + normal * Math.Sin(Math.PI * u); + page.MouseMove(pointer); + Tick(); + } + } + // A switch clicked off, and the donut opening again without that field's slices. + void TurnOff(string id, int slicesLeft) + { + var toggle = Switch(id); + var at = InPage(toggle); + MoveTo(new Avalonia.Point(at.X + 14, at.Center.Y + 2), 0.6); + Tick(0.15); + page.MouseDown(pointer, MouseButton.Left); + pressed = true; + Tick(); + page.MouseUp(pointer, MouseButton.Left); + pressed = false; + Assert.Contains(id, store.Settings.TurnedOffFields); + Tick(0.35); + vm.Load(files, clickMode: true); + Assert.Equal(slicesLeft, vm.Items.Count); + Dispatcher.UIThread.RunJobs(); // the donut takes the new slices before it opens + donut.PlayOpenAnimation(); + Tick(1.2, renderTimer: false); + } + + // Frame 0: Design & print and Games on, PSD, EPS and DDS on the donut. + page.MouseMove(pointer); + donut.FinishAnimations(); + Settle(donut); + clip.Add(Frame(), 1300); + TurnOff("design", 6); + TurnOff("game", 5); + clip.MarkPoster(); + clip.Hold(1600); + } + finally + { + page.Close(); + donutWindow.Close(); + store.Settings.TurnedOffFields = turnedOff; + images.PresetIds = front; + foreach (var preset in added) + store.Presets.Remove(preset); + } + return clip; + } + + /// + /// Captions and translation with a local AI: a video dropped on the "Caption" slice, its SRT written + /// cue by cue, then the same cues translated with their timings kept. Whisper and Ollama can't run here, so the + /// cues are written out; the cards use the app's own styles (card, h2, caption). + /// + private static Clip Translate() + { + var clip = new Clip { Quality = 80 }; + (string Time, string English, string Korean)[] cues = + [ + ("00:00:01,200 --> 00:00:03,800", "Welcome back to the workshop.", "워크숍에 다시 오신 걸 환영해요."), + ("00:00:04,100 --> 00:00:07,000", "Today we turn old photos into prints.", "오늘은 오래된 사진을 인쇄해 볼게요."), + ("00:00:07,400 --> 00:00:10,200", "Let's start with the scanner.", "먼저 스캐너부터 시작해요."), + ]; + var loc = AppHost.Get(); + var blocks = new List<(Control Block, bool Translated)>(); + Control Card(string name, bool translated) + { + var list = new StackPanel { Spacing = 12, Margin = new Avalonia.Thickness(0, 12, 0, 0) }; + for (var i = 0; i < cues.Length; i++) + { + var block = new StackPanel { Spacing = 1 }; + block.Children.Add(new TextBlock { Classes = { "caption" }, Text = $"{i + 1}" }); + block.Children.Add(new TextBlock { Classes = { "caption" }, Text = cues[i].Time }); + block.Children.Add(new TextBlock { FontSize = 16, TextWrapping = Avalonia.Media.TextWrapping.Wrap, Text = translated ? cues[i].Korean : cues[i].English }); + list.Children.Add(block); + blocks.Add((block, translated)); + } + return new Border + { + Classes = { "card" }, + Padding = new Avalonia.Thickness(20, 16), + Width = 316, + Child = new StackPanel { Children = { new TextBlock { Classes = { "h2" }, Text = name }, list } }, + }; + } + var arrow = new StackPanel + { + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Margin = new Avalonia.Thickness(14, 0), + Spacing = 6, + Children = + { + new Border + { + Background = (Avalonia.Media.IBrush)Avalonia.Application.Current!.FindResource("AccentBrush")!, + CornerRadius = new Avalonia.CornerRadius(12), + Padding = new Avalonia.Thickness(10, 4), + Child = new TextBlock { Text = loc["preset.translate"], Foreground = Avalonia.Media.Brushes.White, FontWeight = Avalonia.Media.FontWeight.SemiBold }, + }, + new TextBlock { Text = "→", FontSize = 26, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center }, + }, + }; + var window = new Window + { + Width = CanvasWidth, + Height = CanvasHeight, + Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse(Page)), + Content = new StackPanel + { + Orientation = Avalonia.Layout.Orientation.Horizontal, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, + Children = { Card("interview.srt", false), arrow, Card("interview_ko.srt", true) }, + }, + }; + try + { + window.Show(); + Pump(); + // Frame 0 (reduced motion), and the frame the clip comes back to: both files written. + clip.Add(Capture(window), 1400); + + // As someone with Whisper installed sees it. + using (var drag = new DonutDrag(clip, [@"C:\Videos\interview.mp4"], vm => vm.Items = vm.Items.Select(Enabled).ToList())) + { + var caption = drag.Index("caption-video"); + drag.Open(drag.Slice(caption, 0.6)); + drag.Scene.Hold(0.8); + Assert.Equal(new DonutHit(DonutHitKind.Segment, caption), drag.Hit); + drag.Drop(0.8, 0.5); + } + + // The captions come in cue by cue, then their translation; the arrow shows between them. + void Show(Func shown) + { + for (var i = 0; i < blocks.Count; i++) + { + var amount = shown(i % cues.Length, blocks[i].Translated); + blocks[i].Block.Opacity = amount; + blocks[i].Block.RenderTransform = new Avalonia.Media.TranslateTransform(0, 8 * (1 - amount)); + } + Pump(); + } + double Ease(double t) => Scene.EaseInOut(Math.Clamp(t, 0, 1)); + arrow.Opacity = 0; + Show((_, _) => 0); + clip.Add(Capture(window), 300); + for (var f = 1; f <= 24; f++) + { + Show((cue, translated) => translated ? 0 : Ease(f / 6.0 - cue * 1.2)); + clip.Add(Capture(window), 50); + } + clip.Hold(300); + for (var f = 1; f <= 6; f++) + { + arrow.Opacity = f / 6.0; + Pump(); + clip.Add(Capture(window), 50); + } + for (var f = 1; f <= 24; f++) + { + Show((cue, translated) => translated ? Ease(f / 6.0 - cue * 1.2) : 1); + clip.Add(Capture(window), 50); + } + clip.Hold(2000); + } + finally + { + window.Close(); + } + return clip; + } + + /// + /// "And more": Move to folder and Copy to folder slices on the back of the pictures donut, the perspective crop + /// straightening a photographed page, the donut of a Krita file, and the Home page in Italian and in Indonesian. + /// + private static Clip AndMore() + { + var clip = new Clip { Quality = 80 }; + var store = AppHost.Get(); + var loc = AppHost.Get(); + var images = store.Profiles.Single(p => p.Id == "images"); + var back = images.BackPresetIds?.ToList(); + Preset Folder(string id, string name, string path, bool move) => new() + { + Id = id, + Name = name, + Kind = PresetKind.CopyToFolder, + TargetFormat = Preset.CopyTarget, + Output = new OutputRule { Location = OutputLocation.CustomFolder, CustomFolder = path, MoveInsteadOfCopy = move }, + }; + Preset[] added = + [ + Folder("whatsnew-projects", "Projects", @"C:\Projects", move: true), + Folder("whatsnew-references", "References", @"C:\References", move: false), + ]; + store.Presets.AddRange(added); + images.BackPresetIds = ["tool-crop", "rotate", "remove-background", "whatsnew-projects", "whatsnew-references", "tool-image-edit", "zip-all"]; + var folder = Directory.CreateTempSubdirectory("filee-whatsnew-more-").FullName; + var clock = System.Diagnostics.Stopwatch.GetTimestamp(); + CropCanvas.Clock = () => clock; + try + { + // Frame 0: the folders on the back face, the pointer on Projects (Move to folder). + using (var donut = new DonutDrag(clip, [@"C:\Pictures\sketch.png"], clickMode: true, backFace: true)) + { + donut.Donut.FinishAnimations(); + var (projects, references) = (donut.Index("whatsnew-projects"), donut.Index("whatsnew-references")); + donut.Scene.Place(donut.Slice(projects, 0.9)); + donut.Aim(); + Settle(donut.Donut); + donut.Scene.Tick(); + clip.Hold(1000); + donut.Scene.MoveTo(donut.Slice(references, 0.9), 0.45); + donut.Scene.Hold(1.2); + } + + // Perspective crop: the corners go onto a page photographed at an angle. + var photo = Path.Combine(folder, "notes.jpg"); + var page = DrawTiltedPage(photo); + var preset = BuiltInData.CreatePresets().Single(p => p.Id == "tool-crop"); + var vm = new CropOverlayViewModel([photo], preset, loc, store); + var window = new CropOverlayWindow + { + DataContext = vm, + Width = 889, + Height = 500, + Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse(Page)), + }; + try + { + window.Show(); + WaitFor(() => vm.PreviewImage is not null && !vm.IsBusy); + window.Classes.Set("reduced-motion", true); + vm.IsPerspective = true; + Pump(); + var canvas = window.CropCanvas; + Assert.True(canvas.IsPerspective); + clip.Add(Capture(window), 500); + var from = canvas.NormalizedCorners; + static Avalonia.Point Toward(Avalonia.Point a, Avalonia.Point b, double u) => new(a.X + (b.X - a.X) * u, a.Y + (b.Y - a.Y) * u); + for (var f = 1; f <= 12; f++) + { + var u = Scene.EaseInOut(f / 12.0); + canvas.NormalizedCorners = new CropQuad(Toward(from.TopLeft, page.TopLeft, u), Toward(from.TopRight, page.TopRight, u), + Toward(from.BottomRight, page.BottomRight, u), Toward(from.BottomLeft, page.BottomLeft, u)); + clock += System.Diagnostics.Stopwatch.Frequency / 20; + Pump(); + clip.Add(Capture(window), 50); + } + clip.Hold(1500); + } + finally + { + window.Close(); + } + + // A new format: a Krita picture. + var (krita, kritaVm) = OpenDonut([@"C:\Art\castle.kra"], clickMode: true); + try + { + krita.DonutControl.PlayOpenAnimation(); + Run(clip, krita, 0.4); + Highlight(krita, kritaVm, 0); + clip.Add(Capture(krita), 1400); + } + finally + { + krita.Close(); + } + + // Two new languages. + foreach (var language in new[] { "it", "id" }) + { + TestServices.EnsureInitialized(language); + AppHost.Get().Apply(new ThemeSettings { ReduceMotion = false, Mode = ThemeMode.Light }, language); + var main = new MainWindowViewModel(AppHost.Services, AppHost.Get(), AppHost.Get(), AppHost.Get()); + main.Navigate("home"); + var home = new MainWindow { DataContext = main, Width = 960, Height = 540 }; + home.Show(); + Pump(); + clip.Add(Capture(home, "2.3.0"), 1400); + home.Close(); + main.Dispose(); + } + } + finally + { + CropCanvas.Clock = System.Diagnostics.Stopwatch.GetTimestamp; + TestServices.EnsureInitialized("en"); + AppHost.Get().Apply(new ThemeSettings { ReduceMotion = false, Mode = ThemeMode.Light }, "en"); + images.BackPresetIds = back; + foreach (var preset in added) + store.Presets.Remove(preset); + try + { + Directory.Delete(folder, recursive: true); + } + catch (IOException) { } + } + return clip; + } + + /// + /// A made-up photo of a page lying at an angle on a desk (1600 × 1000), with lines of writing that follow its tilt. + /// Returns where the page's corners are, in parts of the photo. + /// + private static CropQuad DrawTiltedPage(string path) + { + PointD[] corners = [new(360, 150), new(1230, 235), new(1300, 900), new(270, 830)]; + PointD At(double u, double v) + { + var top = new PointD(corners[0].X + (corners[1].X - corners[0].X) * u, corners[0].Y + (corners[1].Y - corners[0].Y) * u); + var bottom = new PointD(corners[3].X + (corners[2].X - corners[3].X) * u, corners[3].Y + (corners[2].Y - corners[3].Y) * u); + return new PointD(top.X + (bottom.X - top.X) * v, top.Y + (bottom.Y - top.Y) * v); + } + using var image = new MagickImage("gradient:#C79A6B-#9C7148", 1600, 1000); + var drawing = new Drawables() + .FillColor(new MagickColor("#00000033")).Polygon(corners.Select(c => new PointD(c.X + 14, c.Y + 18)).ToArray()) + .FillColor(new MagickColor("#FBFAF6")).Polygon(corners) + .FillColor(new MagickColor("#3E63DD")).Polygon(At(0.1, 0.09), At(0.6, 0.09), At(0.6, 0.15), At(0.1, 0.15)); + for (var line = 0; line < 11; line++) + { + var v = 0.24 + line * 0.065; + var end = line % 4 == 3 ? 0.55 : 0.9; + drawing.FillColor(new MagickColor("#B9B6AE")).Polygon(At(0.1, v), At(end, v), At(end, v + 0.022), At(0.1, v + 0.022)); + } + drawing.Draw(image); + image.Quality = 90; + image.Write(path, MagickFormat.Jpeg); + static Avalonia.Point Part(PointD p) => new(p.X / 1600, p.Y / 1000); + return new CropQuad(Part(corners[0]), Part(corners[1]), Part(corners[2]), Part(corners[3])); + } + + /// + /// A made-up eight-page document (A4): each page a coloured heading with its number, a picture and lines of text. + /// The pages are pictures (PDFsharp draws no text without fonts set up); PdfSheets places them as it would any page. + /// + private static void DrawDocument(string path, string folder) + { + var bold = Path.Combine(RepositoryRoot, "src", "Filee.App", "Assets", "Fonts", "NotoSans-Bold.ttf"); + string[] colours = ["#7F77DD", "#3E63DD", "#12A594", "#30A46C", "#F5A524", "#E5484D", "#D6409F", "#8E4EC6"]; + using var document = new PdfSharp.Pdf.PdfDocument(); + for (var n = 0; n < 8; n++) + { + var picture = Path.Combine(folder, $"page{n + 1}.jpg"); + using (var image = new MagickImage(MagickColors.White, 1190, 1684)) + { + var grey = new MagickColor("#DEDDE3"); + var drawing = new Drawables() + .FillColor(new MagickColor(colours[n])).Rectangle(0, 0, 1190, 300) + .Font(bold).FontPointSize(210).FillColor(MagickColors.White).Text(90, 250, (n + 1).ToString(System.Globalization.CultureInfo.InvariantCulture)) + .FillColor(new MagickColor("#FFFFFFB0")).RoundRectangle(420, 120, 1000, 150, 15, 15) + .FillColor(new MagickColor("#FFFFFF80")).RoundRectangle(420, 180, 860, 206, 13, 13); + for (var line = 0; line < 6; line++) + drawing.FillColor(grey).RoundRectangle(90, 400 + line * 52, line == 5 ? 700 : 1100, 424 + line * 52, 12, 12); + drawing.FillColor(new MagickColor(colours[(n + 3) % 8])) + .RoundRectangle(90, 760, 1100, 1180, 24, 24) + .FillColor(new MagickColor("#FFFFFF59")).Circle(800, 900, 800, 960) + .FillColor(new MagickColor("#FFFFFF40")).Polygon(new PointD(90, 1180), new PointD(420, 920), new PointD(700, 1180)); + for (var line = 0; line < 6; line++) + drawing.FillColor(grey).RoundRectangle(90, 1260 + line * 52, line == 5 ? 560 : 1100, 1284 + line * 52, 12, 12); + drawing.Draw(image); + image.Quality = 90; + image.Write(picture, MagickFormat.Jpeg); + } + var page = document.AddPage(); + page.Width = PdfSharp.Drawing.XUnit.FromPoint(595.28); + page.Height = PdfSharp.Drawing.XUnit.FromPoint(841.89); + using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page); + using var ximage = PdfSharp.Drawing.XImage.FromFile(picture); + gfx.DrawImage(ximage, 0, 0, 595.28, 841.89); + } + document.Save(path); + } + + /// + /// Files dragged onto the donut with the gesture key held, as does: the donut opens around + /// the pointer, its own clock stepped with the clip's frames; Tab can turn it over, a group fans out when the pointer + /// heads outward on it, and a drop runs the progress ring to its check mark. + /// + private sealed class DonutDrag : IDisposable + { + private readonly Avalonia.Point _center; + private bool _tracking = true; + + public DonutDrag(Clip clip, IReadOnlyList files, Action? prepare = null, bool clickMode = false, bool backFace = false) + { + (Window, Vm) = OpenDonut(files, clickMode, backFace); + prepare?.Invoke(Vm); + Donut = Window.DonutControl; + _center = new Avalonia.Point(Donut.Bounds.Width / 2, Donut.Bounds.Height / 2); + Scene = new Scene(Window, clip) { FrameLength = 1 / 20.0, RunsRenderTimer = false }; + Scene.Steps.Add(dt => + { + if (_tracking) + Aim(); + KeepFramesComing(Donut); + Donut.Advance(dt); + KeepFramesComing(Donut); + }); + } + + /// Lights up what the pointer is on, and the hint for it in the hub. + public void Aim() + { + var at = Window.TranslatePoint(Scene.Pointer, Donut)!.Value; + Hit = Donut.HitTest(at); + Donut.SetExternalHit(Hit, at); + Vm.ShowHint(Hit.Kind is DonutHitKind.Segment or DonutHitKind.SubSegment ? Hit.Index : -1, + Hit.Kind == DonutHitKind.SubSegment ? Hit.Sub : -1, Hit.Kind == DonutHitKind.Center); + } + + public RadialWindow Window { get; } + public RadialViewModel Vm { get; } + public DonutMenu Donut { get; } + public Scene Scene { get; } + + /// What the pointer was on at the last frame. + public DonutHit Hit { get; private set; } + + public int Index(string presetId) + { + var index = Vm.Items.ToList().FindIndex(i => i.Id == presetId); + Assert.True(index >= 0, $"{presetId} isn't on this face of the donut"); + return index; + } + + /// The middle of slice (in the window), from the hole (0) to the rim (1). + public Avalonia.Point Slice(int index, double depth) + { + var inner = Donut.OuterRadius * Donut.HoleRatio; + return InWindow(DonutGeometry.PointAt(_center, inner + (Donut.OuterRadius - inner) * depth, DonutGeometry.MidAngle(index, Donut.Items!.Count))); + } + + public Avalonia.Point Sub(int group, int sub) => InWindow(Donut.SubSliceCenter(group, sub)); + + private Avalonia.Point InWindow(Avalonia.Point p) => Donut.TranslatePoint(p, Window)!.Value; + + /// + /// The donut opens around the pointer as the drag comes in with the gesture key held, and the pointer goes on to + /// . + /// + public void Open(Avalonia.Point to) + { + Scene.Grab(true); + Scene.Place(InWindow(_center + new Avalonia.Vector(2, 3))); + Scene.ShowKey("Ctrl", down: true); + Donut.PlayOpenAnimation(); + Scene.Tick(); + Scene.MoveTo(to, 0.35); + Scene.Hold(0.35); + Scene.HideKey(); + } + + /// Tab turns the donut over to its other face. + public void Flip() + { + Scene.ShowKey("Tab", down: true); + Assert.True(Donut.Flip()); + Scene.Tick(); + Scene.ShowKey("Tab", down: false); + Scene.Hold(DonutMenu.FlipDuration); + Scene.HideKey(); + Scene.Hold(0.3); + } + + /// + /// The files dropped: the slices go into the centre, the ring fills in and the check + /// shows for seconds. + /// + public void Drop(double seconds, double check) + { + _tracking = false; + Scene.Grab(false); + Donut.BeginProgress(); + var frames = (int)Math.Round(seconds / Scene.FrameLength); + Scene.Hold(0.25); + for (var i = 1; i <= frames; i++) + { + Donut.SetProgress(Scene.EaseInOut(i / (double)frames)); + Scene.Tick(); + } + Scene.Hold(0.3); + Donut.CompleteProgress(DonutProgressOutcome.Succeeded, () => { }); + for (var i = 0; i < 60 && Donut.ShownOutcome is null; i++) + Scene.Tick(); + Scene.Hold(check); + } + + public void Dispose() + { + Window.Close(); + Donut.SetExternalHighlight(-1); + if (Vm.IsBackFace) + Vm.ToggleFace(); + } + } + + /// + /// A made-up web page to mark up (1280 × 720): a travel offer with a photo, a title, a price and a button, and + /// three cards below. Generated, so no picture is kept in the repository. + /// + private static void DrawWebPage(string path) + { + var fonts = Path.Combine(RepositoryRoot, "src", "Filee.App", "Assets", "Fonts"); + var (regular, bold) = (Path.Combine(fonts, "NotoSans-Regular.ttf"), Path.Combine(fonts, "NotoSans-Bold.ttf")); + var photoPath = Path.ChangeExtension(path, ".photo.jpg"); + DrawLandscape(photoPath, 0); + using var image = new MagickImage(new MagickColor("#FFFFFF"), 1280, 720); + using (var photo = new MagickImage(photoPath)) + { + photo.Resize(new MagickGeometry(640, 360) { IgnoreAspectRatio = true }); + image.Composite(photo, 48, 92, CompositeOperator.Over); + } + var grey = new MagickColor("#E4E4E9"); + var drawing = new Drawables() + .FillColor(new MagickColor("#F3F3F5")).Rectangle(0, 0, 1280, 56) + .FillColor(new MagickColor("#3E63DD")).Circle(48, 28, 48, 41) + .FillColor(grey).RoundRectangle(80, 20, 200, 36, 8, 8) + .FillColor(grey).RoundRectangle(900, 18, 980, 38, 10, 10) + .FillColor(grey).RoundRectangle(1000, 18, 1080, 38, 10, 10) + .FillColor(new MagickColor("#1C1C1C")).RoundRectangle(1100, 14, 1232, 42, 14, 14) + .Font(bold).FontPointSize(40).FillColor(new MagickColor("#1C1C1C")).Text(730, 150, "Three days in Lisbon") + .FillColor(grey).RoundRectangle(730, 186, 1200, 200, 7, 7) + .FillColor(grey).RoundRectangle(730, 214, 1160, 228, 7, 7) + .FillColor(grey).RoundRectangle(730, 242, 1010, 256, 7, 7) + .Font(bold).FontPointSize(34).FillColor(new MagickColor("#1C1C1C")).Text(730, 306, "€ 420") + .Font(regular).FontPointSize(20).FillColor(new MagickColor("#6B6B73")).Text(842, 304, "per person") + .FillColor(new MagickColor("#3E63DD")).RoundRectangle(730, 380, 976, 440, 12, 12) + .Font(bold).FontPointSize(24).FillColor(MagickColors.White).TextAlignment(TextAlignment.Center).Text(853, 419, "Book now") + .TextAlignment(TextAlignment.Left); + foreach (var x in new[] { 48, 472, 896 }) + drawing + .FillColor(new MagickColor("#F6F6F8")).RoundRectangle(x, 492, x + 336, 680, 14, 14) + .FillColor(grey).RoundRectangle(x + 24, 528, x + 250, 544, 8, 8) + .FillColor(grey).RoundRectangle(x + 24, 560, x + 300, 572, 6, 6) + .FillColor(grey).RoundRectangle(x + 24, 584, x + 210, 596, 6, 6); + drawing.Draw(image); + image.Write(path, MagickFormat.Png); + } + // ───────────────────────── Helpers ───────────────────────── /// @@ -655,6 +1676,9 @@ private sealed class Scene(TopLevel window, Clip clip, string? version = null) /// Seconds per frame. public double FrameLength { get; init; } = Frame; + /// The part of the window the clip shows (in the window), or null for all of it. + public Avalonia.Rect? Region { get; set; } + /// /// False for a window whose animations the clip steps itself (the donut): the render timer would also run its /// frame loop, on wall-clock time. @@ -684,7 +1708,7 @@ public void Tick(Func? pointer = null) } if (pointer is not null) Pointer = pointer(); - var frame = Capture(window, version, pointer: Pointer, pressed: Pressed || _grabbed); + var frame = Capture(window, version, Region, Pointer, Pressed || _grabbed); var shown = _key.Step(_keyCap.Shown ? 1 : 0, FrameLength); if (shown > 0.01) DrawKeyCap(frame, _keyCap.Label, shown, _keyCap.Down); @@ -775,12 +1799,12 @@ private static double BackEaseOut(double t) return 1 - p * (p * p - Math.Sin(p * Math.PI)); } - private static (RadialWindow Window, RadialViewModel Vm) OpenDonut(IReadOnlyList files, bool clickMode) + private static (RadialWindow Window, RadialViewModel Vm) OpenDonut(IReadOnlyList files, bool clickMode, bool backFace = false) { var vm = AppHost.Get(); if (!clickMode) vm.ResetForDrag(); - vm.Load(files, clickMode); + vm.Load(files, clickMode, backFace); vm.FitToThisDonut(); // The headless platform has no transparent windows: the page colour instead of black around the donut. var window = new RadialWindow { DataContext = vm, Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse(Page)) }; @@ -882,18 +1906,14 @@ private static MagickImage Capture(TopLevel window, string? version = null, Aval /// private static void KeepFramesComing(DonutMenu donut) => LastFrameTick.SetValue(donut, Environment.TickCount64); - private static MagickImage CaptureCore(TopLevel window, string? version, Avalonia.Rect? region, Avalonia.Point? pointer, bool pressed) + /// + /// The window as it is drawn now, at its own size. draws it from its state, as the donut + /// always is, without a frame of the render timer. + /// + private static MagickImage Snapshot(TopLevel window, bool direct = false) { - if (version is not null) - { - foreach (var text in window.GetVisualDescendants().OfType()) - if (text.Text?.Contains(UpdateService.CurrentVersion, StringComparison.Ordinal) == true) - text.Text = text.Text.Replace(UpdateService.CurrentVersion, version, StringComparison.Ordinal); - Dispatcher.UIThread.RunJobs(); - AvaloniaHeadlessPlatform.ForceRenderTimerTick(); - } using var png = new MemoryStream(); - if (window is RadialWindow) + if (direct || window is RadialWindow) { // The donut is drawn straight from its state: the clips step its clock themselves, and the render timer // would run its own frame loop as well. @@ -910,7 +1930,20 @@ private static MagickImage CaptureCore(TopLevel window, string? version, Avaloni frame.Save(png, new Avalonia.Media.Imaging.PngBitmapEncoderOptions()); } png.Position = 0; - using var shot = new MagickImage(png); + return new MagickImage(png); + } + + private static MagickImage CaptureCore(TopLevel window, string? version, Avalonia.Rect? region, Avalonia.Point? pointer, bool pressed) + { + if (version is not null) + { + foreach (var text in window.GetVisualDescendants().OfType()) + if (text.Text?.Contains(UpdateService.CurrentVersion, StringComparison.Ordinal) == true) + text.Text = text.Text.Replace(UpdateService.CurrentVersion, version, StringComparison.Ordinal); + Dispatcher.UIThread.RunJobs(); + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); + } + using var shot = Snapshot(window); var corner = new Avalonia.Point(0, 0); if (region is { } r) { @@ -1022,7 +2055,7 @@ private static void Write(string folder, string name, Func render, bool on clip.SaveFirstFrame(Path.Combine(TestServices.ScreenshotDirectory, $"whatsnew-{language}-" + Path.ChangeExtension(name, ".png"))); Assert.InRange(new FileInfo(path).Length, 1, 400 * 1024); if (language != "en" || clip.Poster is not null) - SetPoster(folder, name, clip.Poster ?? 0); + SetPoster(folder, name, clip.PosterIn(path)); } } @@ -1099,6 +2132,22 @@ public void WriteWebP(string path) public void SaveFirstFrame(string path) => _frames[0].Image.Write(path, MagickFormat.Png); + /// + /// The poster's frame in the file written to (0 without a poster): the encoder can merge + /// a frame into the one before when they come out the same, which moves the frames after it. + /// + public int PosterIn(string path) + { + if (Poster is not { } poster) + return 0; + using var written = new MagickImageCollection(path); + if (written.Count == _frames.Count) + return poster; + written.Coalesce(); + var wanted = _frames[poster].Image; + return Enumerable.Range(0, written.Count).MinBy(i => wanted.Compare(written[i], ErrorMetric.RootMeanSquared)); + } + public void Dispose() { foreach (var (image, _) in _frames) diff --git a/tests/Filee.App.Tests/WhatsNewTests.cs b/tests/Filee.App.Tests/WhatsNewTests.cs index 58acecb0a..95f785a25 100644 --- a/tests/Filee.App.Tests/WhatsNewTests.cs +++ b/tests/Filee.App.Tests/WhatsNewTests.cs @@ -33,19 +33,21 @@ private static SlideDeck Deck(string version) => // ───────────────────────── Manifests ───────────────────────── - [AvaloniaFact] - public void The_2_0_0_deck_ships_with_its_media_and_texts() + [AvaloniaTheory] + [InlineData("2.0.0")] + [InlineData("2.3.0")] + public void A_deck_ships_with_its_media_and_texts(string version) { TestServices.EnsureInitialized("en"); var decks = SlideDeck.LoadAll("Filee"); - var deck = Assert.Single(decks, d => d.Version == V2); + var deck = Assert.Single(decks, d => d.Version == new Version(version)); Assert.InRange(deck.Slides.Count, 5, 7); Assert.Equal(deck.Slides.Count, deck.Slides.Select(s => s.Id).Distinct().Count()); var english = LoadStrings("en"); foreach (var slide in deck.Slides) { Assert.True(AssetLoader.Exists(slide.Media), $"{slide.Media} is missing"); - Assert.StartsWith("avares://Filee/Assets/WhatsNew/2.0.0/", slide.Media.ToString(), StringComparison.Ordinal); + Assert.StartsWith($"avares://Filee/Assets/WhatsNew/{version}/", slide.Media.ToString(), StringComparison.Ordinal); Assert.Contains(slide.TitleKey, english.Keys); Assert.Contains(slide.TextKey, english.Keys); } @@ -318,11 +320,11 @@ public void The_version_updated_from_decides_over_the_remembered_one() // ───────────────────────── The window ───────────────────────── - private static WhatsNewWindow Open(string language = "en", Action? beforeOpening = null) + private static WhatsNewWindow Open(string language = "en", Action? beforeOpening = null, Version? version = null) { TestServices.EnsureInitialized(language); beforeOpening?.Invoke(); - var deck = SlideDeck.LoadAll("Filee").Single(d => d.Version == V2); + var deck = SlideDeck.LoadAll("Filee").Single(d => d.Version == (version ?? V2)); var window = new WhatsNewWindow(deck, AppHost.Get()); window.Show(); window.Activate(); @@ -604,6 +606,55 @@ public void Window_renders(string language, bool dark) } } + /// + /// The 2.3.0 slides in a few languages: every slide's text fits and, with reduced motion, shows its poster frame + /// (what the installer shows too). A screenshot of each slide. + /// + [AvaloniaTheory] + [InlineData("en", false)] + [InlineData("en", true)] + [InlineData("ko", false)] + [InlineData("ar", false)] + [InlineData("de", false)] + [InlineData("ja", false)] + [InlineData("it", false)] + [InlineData("id", false)] + public void The_2_3_0_slides_render_on_their_posters(string language, bool dark) + { + TestServices.EnsureInitialized(language); + var theme = AppHost.Get(); + var motion = Motion.Enabled; + var window = Open(language, () => + { + theme.Apply(new ThemeSettings { Mode = dark ? ThemeMode.Dark : ThemeMode.Light }, language); + Motion.Set(false); + }, new Version(2, 3, 0)); + try + { + var slides = window.SlideShow; + Assert.Equal(6, slides.Slides.Count); + var heights = new HashSet(); + for (var i = 0; i < slides.Slides.Count; i++) + { + slides.GoTo(i); + Pump(); + heights.Add(window.Bounds.Height); + var media = slides.Media; + Assert.True(media.HasPicture, $"slide {i} has no picture"); + Assert.InRange(slides.Slides[i].PosterFrame, 0, media.FrameCount - 1); + Assert.Equal(slides.Slides[i].PosterFrame, media.CurrentFrame); + Save(window, $"whatsnew-2.3.0-{language}-{(dark ? "dark" : "light")}-{i + 1}-{slides.Slides[i].Id}.png"); + } + Assert.Single(heights); + } + finally + { + window.Close(); + Motion.Set(motion); + theme.Apply(new ThemeSettings(), "en"); + } + } + private static Dictionary LoadStrings(string code) => JsonSerializer.Deserialize>(File.ReadAllText( Path.Combine(WhatsNewMedia.RepositoryRoot, "src", "Filee.App", "Assets", "i18n", code + ".json")))!;