From 8e652231bf27de4c7e492f1c9ce95f13b0126c4f Mon Sep 17 00:00:00 2001
From: cre8ive-marketing <168519979+cre8ive-marketing@users.noreply.github.com>
Date: Sun, 25 Oct 2026 11:32:05 +0530
Subject: [PATCH 1/3] Home: undo a conversion, its results go to the Recycle
Bin / Trash (#553)
A recent conversion gets an Undo button while every result is still as the
job left it (same size and write time, saved in history.json). Undo moves the
results to the Recycle Bin / Trash and the card says so. With "Replace the
original" it also says the originals are there; they stay (#332 unchanged).
A source left as it was ("Only if smaller") is never treated as a result.
---
src/Filee.App/Assets/i18n/ar.json | 6 ++
src/Filee.App/Assets/i18n/en.json | 6 ++
src/Filee.App/Assets/i18n/es.json | 6 ++
src/Filee.App/Assets/i18n/fr.json | 6 ++
src/Filee.App/Assets/i18n/hi.json | 6 ++
src/Filee.App/Assets/i18n/ja.json | 6 ++
src/Filee.App/Assets/i18n/ko.json | 6 ++
src/Filee.App/Assets/i18n/ne.json | 6 ++
src/Filee.App/Assets/i18n/pl.json | 6 ++
src/Filee.App/Assets/i18n/pt-BR.json | 6 ++
src/Filee.App/Assets/i18n/pt-PT.json | 6 ++
src/Filee.App/Assets/i18n/ru.json | 6 ++
src/Filee.App/Assets/i18n/th.json | 6 ++
src/Filee.App/Assets/i18n/tr.json | 6 ++
src/Filee.App/Assets/i18n/zh-CN.json | 6 ++
src/Filee.App/Assets/i18n/zh-TW.json | 6 ++
src/Filee.App/Styles/Icons.axaml | 1 +
.../ViewModels/Pages/HomePageViewModel.cs | 49 ++++++++++-
src/Filee.App/Views/Pages/HomePage.axaml | 10 +++
src/Filee.Core/Conversion/ConversionJob.cs | 3 +
src/Filee.Core/Conversion/JobQueue.cs | 4 +-
src/Filee.Core/History/HistoryEntry.cs | 51 +++++++++++
src/Filee.Core/Settings/UserDataStore.cs | 9 +-
.../Filee.App.Tests/RecentConversionTests.cs | 84 ++++++++++++++++++-
tests/Filee.Core.Tests/HistoryEntryTests.cs | 42 ++++++++++
.../Filee.Core.Tests/ReplaceOriginalTests.cs | 28 +++++++
26 files changed, 371 insertions(+), 6 deletions(-)
diff --git a/src/Filee.App/Assets/i18n/ar.json b/src/Filee.App/Assets/i18n/ar.json
index 3de1151ec..b5b4fedeb 100644
--- a/src/Filee.App/Assets/i18n/ar.json
+++ b/src/Filee.App/Assets/i18n/ar.json
@@ -44,6 +44,11 @@
"home.clear_history": "مسح",
"home.files": "عدد الملفات: {0}",
"home.failed_files": "فشل: {0}",
+ "home.undo": "تراجع",
+ "home.undo_hint": "نقل النتائج إلى سلة المحذوفات",
+ "home.undone": "تم التراجع: النتائج في سلة المحذوفات",
+ "home.undone_original": "تم التراجع: النتائج في سلة المحذوفات، وكذلك الملفات الأصلية التي حلّت محلها",
+ "home.undo_failed": "تعذّر نقل {0} إلى سلة المحذوفات: {1}",
"donut.center_drag": "اسحب الملفات إلى هنا",
"donut.files_same": "{0} × {1}",
"donut.files_mixed": "عدد الملفات: {0}",
@@ -119,6 +124,7 @@
"trash.no_folder": "لا يوجد مجلد لسلة المحذوفات",
"trash.failed": "تعذّر نقله إلى سلة المحذوفات",
"trash.cannot_place": "تعذّر حفظ النتيجة في مجلد الأصل",
+ "trash.changed": "تغيّر بعد التحويل",
"notice.empty_spreadsheet_pages_removed": "تمت إزالة صفحات جدول البيانات الفارغة",
"notice.ocr_has_text": "كل الصفحات تحتوي على نص بالفعل، لذا نُسخ الملف كما هو",
"notice.live_photo_pair": "حُفظ كصورة وملف MOV بالاسم نفسه. استوردهما معًا (صور iCloud أو AirDrop أو تطبيق الصور على Mac) لتحصل على Live Photo",
diff --git a/src/Filee.App/Assets/i18n/en.json b/src/Filee.App/Assets/i18n/en.json
index 75d0ee92a..7ed76479d 100644
--- a/src/Filee.App/Assets/i18n/en.json
+++ b/src/Filee.App/Assets/i18n/en.json
@@ -47,6 +47,11 @@
"home.clear_history": "Clear",
"home.files": "{0} files",
"home.failed_files": "{0} failed",
+ "home.undo": "Undo",
+ "home.undo_hint": "Move the results to the Recycle Bin / Trash",
+ "home.undone": "Undone: the results are in the Recycle Bin / Trash",
+ "home.undone_original": "Undone: the results are in the Recycle Bin / Trash, and so are the originals they replaced",
+ "home.undo_failed": "{0} couldn't be moved to the Recycle Bin / Trash: {1}",
"donut.center_drag": "Bring files here",
"donut.files_same": "{0} × {1}",
@@ -125,6 +130,7 @@
"trash.no_folder": "there is no Trash folder",
"trash.failed": "it couldn't be moved to the Recycle Bin / Trash",
"trash.cannot_place": "the result couldn't be saved in the original's folder",
+ "trash.changed": "it was changed after the conversion",
"notice.empty_spreadsheet_pages_removed": "Empty spreadsheet pages were removed",
"notice.ocr_has_text": "Every page already has text, so it was copied as it is",
"notice.live_photo_pair": "Saved as a photo and a MOV with the same name. Import both together (iCloud Photos, AirDrop or Photos on a Mac) to get a Live Photo",
diff --git a/src/Filee.App/Assets/i18n/es.json b/src/Filee.App/Assets/i18n/es.json
index c074a17f9..b43e1cff0 100644
--- a/src/Filee.App/Assets/i18n/es.json
+++ b/src/Filee.App/Assets/i18n/es.json
@@ -44,6 +44,11 @@
"home.clear_history": "Borrar",
"home.files": "{0} archivos",
"home.failed_files": "{0} con error",
+ "home.undo": "Deshacer",
+ "home.undo_hint": "Mover los resultados a la Papelera de reciclaje",
+ "home.undone": "Deshecho: los resultados están en la Papelera de reciclaje",
+ "home.undone_original": "Deshecho: los resultados están en la Papelera de reciclaje, igual que los originales que reemplazaron",
+ "home.undo_failed": "No se pudo mover {0} a la Papelera de reciclaje: {1}",
"donut.center_drag": "Trae los archivos aquí",
"donut.files_same": "{0} × {1}",
"donut.files_mixed": "{0} archivos",
@@ -119,6 +124,7 @@
"trash.no_folder": "no hay carpeta de Papelera",
"trash.failed": "no se pudo mover a la Papelera de reciclaje",
"trash.cannot_place": "no se pudo guardar el resultado en la carpeta del original",
+ "trash.changed": "cambió después de la conversión",
"notice.empty_spreadsheet_pages_removed": "Se eliminaron las páginas vacías de la hoja de cálculo",
"notice.ocr_has_text": "Todas las páginas ya tienen texto, así que se copió tal cual",
"notice.live_photo_pair": "Guardado como una foto y un MOV con el mismo nombre. Impórtalos juntos (Fotos de iCloud, AirDrop o Fotos en un Mac) para obtener una Live Photo",
diff --git a/src/Filee.App/Assets/i18n/fr.json b/src/Filee.App/Assets/i18n/fr.json
index 68101164f..38cc052cf 100644
--- a/src/Filee.App/Assets/i18n/fr.json
+++ b/src/Filee.App/Assets/i18n/fr.json
@@ -47,6 +47,11 @@
"home.clear_history": "Effacer",
"home.files": "{0} fichiers",
"home.failed_files": "{0} en échec",
+ "home.undo": "Annuler",
+ "home.undo_hint": "Déplacer les résultats dans la Corbeille",
+ "home.undone": "Annulé : les résultats sont dans la Corbeille",
+ "home.undone_original": "Annulé : les résultats sont dans la Corbeille, tout comme les originaux qu'ils avaient remplacés",
+ "home.undo_failed": "Impossible de déplacer {0} dans la Corbeille : {1}",
"donut.center_drag": "Amenez les fichiers ici",
"donut.files_same": "{0} × {1}",
@@ -125,6 +130,7 @@
"trash.no_folder": "il n'y a pas de dossier Corbeille",
"trash.failed": "impossible de le déplacer dans la Corbeille",
"trash.cannot_place": "impossible d'enregistrer le résultat dans le dossier de l'original",
+ "trash.changed": "il a été modifié après la conversion",
"notice.empty_spreadsheet_pages_removed": "Les pages vides de la feuille de calcul ont été supprimées",
"notice.ocr_has_text": "Toutes les pages ont déjà du texte : copié tel quel",
"notice.live_photo_pair": "Enregistré en une photo et un MOV de même nom. Importez-les ensemble (Photos iCloud, AirDrop ou Photos sur Mac) pour obtenir une Live Photo",
diff --git a/src/Filee.App/Assets/i18n/hi.json b/src/Filee.App/Assets/i18n/hi.json
index 957153f43..c45698679 100644
--- a/src/Filee.App/Assets/i18n/hi.json
+++ b/src/Filee.App/Assets/i18n/hi.json
@@ -44,6 +44,11 @@
"home.clear_history": "साफ़ करें",
"home.files": "{0} फ़ाइलें",
"home.failed_files": "{0} विफल",
+ "home.undo": "पूर्ववत करें",
+ "home.undo_hint": "परिणामों को रीसायकल बिन / ट्रैश में ले जाएँ",
+ "home.undone": "पूर्ववत किया गया: परिणाम रीसायकल बिन / ट्रैश में हैं",
+ "home.undone_original": "पूर्ववत किया गया: परिणाम रीसायकल बिन / ट्रैश में हैं, और जिन मूल फ़ाइलों की जगह उन्होंने ली थी वे भी वहीं हैं",
+ "home.undo_failed": "{0} को रीसायकल बिन / ट्रैश में नहीं ले जाया जा सका: {1}",
"donut.center_drag": "फ़ाइलें यहाँ लाएँ",
"donut.files_same": "{0} × {1}",
"donut.files_mixed": "{0} फ़ाइलें",
@@ -119,6 +124,7 @@
"trash.no_folder": "ट्रैश फ़ोल्डर नहीं है",
"trash.failed": "इसे रीसायकल बिन / ट्रैश में नहीं ले जाया जा सका",
"trash.cannot_place": "परिणाम मूल फ़ाइल के फ़ोल्डर में सेव नहीं हो सका",
+ "trash.changed": "रूपांतरण के बाद इसे बदला गया",
"notice.empty_spreadsheet_pages_removed": "स्प्रेडशीट के खाली पृष्ठ हटा दिए गए",
"notice.ocr_has_text": "हर पेज में पहले से टेक्स्ट है, इसलिए फ़ाइल जैसी थी वैसी कॉपी की गई",
"notice.live_photo_pair": "एक ही नाम की फ़ोटो और MOV के रूप में सहेजा गया। लाइव फ़ोटो पाने के लिए दोनों को साथ में इम्पोर्ट करें (iCloud Photos, AirDrop या Mac पर Photos)",
diff --git a/src/Filee.App/Assets/i18n/ja.json b/src/Filee.App/Assets/i18n/ja.json
index 5dcc03f93..a86f97dc1 100644
--- a/src/Filee.App/Assets/i18n/ja.json
+++ b/src/Filee.App/Assets/i18n/ja.json
@@ -44,6 +44,11 @@
"home.clear_history": "消去",
"home.files": "{0}個のファイル",
"home.failed_files": "{0}件失敗",
+ "home.undo": "元に戻す",
+ "home.undo_hint": "結果をごみ箱に移動します",
+ "home.undone": "元に戻しました: 結果はごみ箱にあります",
+ "home.undone_original": "元に戻しました: 結果はごみ箱にあり、置き換えられた元のファイルもごみ箱にあります",
+ "home.undo_failed": "{0} をごみ箱に移動できませんでした: {1}",
"donut.center_drag": "ここにファイルをドラッグ",
"donut.files_same": "{0} × {1}",
"donut.files_mixed": "{0}ファイル",
@@ -119,6 +124,7 @@
"trash.no_folder": "ごみ箱フォルダーがありません",
"trash.failed": "ごみ箱に移動できませんでした",
"trash.cannot_place": "元のファイルのフォルダーに結果を保存できませんでした",
+ "trash.changed": "変換後に変更されています",
"notice.empty_spreadsheet_pages_removed": "スプレッドシートの空白ページを削除しました",
"notice.ocr_has_text": "すべてのページにテキストがあるため、元のままコピーしました",
"notice.live_photo_pair": "同じ名前の写真とMOVとして保存しました。両方を一緒に取り込むと(iCloud写真、AirDrop、Macの写真アプリ)Live Photoになります",
diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json
index 5e899b549..3ab3e257c 100644
--- a/src/Filee.App/Assets/i18n/ko.json
+++ b/src/Filee.App/Assets/i18n/ko.json
@@ -47,6 +47,11 @@
"home.clear_history": "지우기",
"home.files": "파일 {0}개",
"home.failed_files": "{0}개 실패",
+ "home.undo": "되돌리기",
+ "home.undo_hint": "결과를 휴지통으로 옮겨요",
+ "home.undone": "되돌렸어요: 결과는 휴지통에 있어요",
+ "home.undone_original": "되돌렸어요: 결과는 휴지통에 있고, 결과가 대신했던 원본도 휴지통에 있어요",
+ "home.undo_failed": "{0}을(를) 휴지통으로 옮기지 못했어요: {1}",
"donut.center_drag": "파일을 이쪽으로",
"donut.files_same": "{1} {0}개",
@@ -125,6 +130,7 @@
"trash.no_folder": "휴지통 폴더가 없어요",
"trash.failed": "휴지통으로 옮기지 못했어요",
"trash.cannot_place": "원본 폴더에 결과를 저장하지 못했어요",
+ "trash.changed": "변환한 뒤에 바뀌었어요",
"notice.empty_spreadsheet_pages_removed": "스프레드시트의 빈 페이지를 제거했어요",
"notice.ocr_has_text": "모든 페이지에 이미 텍스트가 있어서 그대로 복사했어요",
"notice.live_photo_pair": "같은 이름의 사진과 MOV로 저장했어요. 두 파일을 함께 가져오면(iCloud 사진, AirDrop, Mac의 사진 앱) 라이브 포토가 돼요",
diff --git a/src/Filee.App/Assets/i18n/ne.json b/src/Filee.App/Assets/i18n/ne.json
index a9a85890c..8b9d011ad 100644
--- a/src/Filee.App/Assets/i18n/ne.json
+++ b/src/Filee.App/Assets/i18n/ne.json
@@ -44,6 +44,11 @@
"home.clear_history": "खाली गर्नुहोस्",
"home.files": "{0} फाइलहरू",
"home.failed_files": "{0} असफल",
+ "home.undo": "पूर्ववत गर्नुहोस्",
+ "home.undo_hint": "नतिजाहरूलाई रिसाइकल बिन / ट्र्यासमा सार्नुहोस्",
+ "home.undone": "पूर्ववत गरियो: नतिजाहरू रिसाइकल बिन / ट्र्यासमा छन्",
+ "home.undone_original": "पूर्ववत गरियो: नतिजाहरू रिसाइकल बिन / ट्र्यासमा छन्, र तिनले बदलेका मूल फाइलहरू पनि त्यहीँ छन्",
+ "home.undo_failed": "{0} लाई रिसाइकल बिन / ट्र्यासमा सार्न सकिएन: {1}",
"donut.center_drag": "फाइलहरू यहाँ ल्याउनुहोस्",
"donut.files_same": "{0} × {1}",
"donut.files_mixed": "{0} फाइलहरू",
@@ -119,6 +124,7 @@
"trash.no_folder": "ट्र्यास फोल्डर छैन",
"trash.failed": "यसलाई रिसाइकल बिन / ट्र्यासमा सार्न सकिएन",
"trash.cannot_place": "नतिजा मूल फाइलको फोल्डरमा सेभ गर्न सकिएन",
+ "trash.changed": "रूपान्तरणपछि यसलाई परिवर्तन गरिएको छ",
"notice.ocr_has_text": "हरेक पृष्ठमा पहिले नै पाठ छ, त्यसैले जस्तो छ त्यस्तै प्रतिलिपि गरियो",
"notice.empty_spreadsheet_pages_removed": "स्प्रेडसिटका खाली पृष्ठहरू हटाइए",
"notice.live_photo_pair": "एउटै नामको फोटो र MOV का रूपमा सुरक्षित गरियो। लाइभ फोटो पाउन दुवैलाई सँगै आयात गर्नुहोस् (iCloud Photos, AirDrop वा Mac मा Photos)",
diff --git a/src/Filee.App/Assets/i18n/pl.json b/src/Filee.App/Assets/i18n/pl.json
index 72f4aa02f..8aa702f27 100644
--- a/src/Filee.App/Assets/i18n/pl.json
+++ b/src/Filee.App/Assets/i18n/pl.json
@@ -47,6 +47,11 @@
"home.clear_history": "Wyczyść",
"home.files": "Pliki: {0}",
"home.failed_files": "Błędy: {0}",
+ "home.undo": "Cofnij",
+ "home.undo_hint": "Przenieś wyniki do Kosza",
+ "home.undone": "Cofnięto: wyniki są w Koszu",
+ "home.undone_original": "Cofnięto: wyniki są w Koszu, tak jak oryginały, które zastąpiły",
+ "home.undo_failed": "Nie udało się przenieść {0} do Kosza: {1}",
"donut.center_drag": "Przenieś pliki tutaj",
"donut.files_same": "{0} × {1}",
@@ -125,6 +130,7 @@
"trash.no_folder": "nie ma folderu Kosza",
"trash.failed": "nie udało się przenieść go do Kosza",
"trash.cannot_place": "nie udało się zapisać wyniku w folderze oryginału",
+ "trash.changed": "zmieniono go po konwersji",
"notice.empty_spreadsheet_pages_removed": "Usunięto puste strony arkusza kalkulacyjnego",
"notice.ocr_has_text": "Każda strona ma już tekst, więc skopiowano bez zmian",
"notice.live_photo_pair": "Zapisano jako zdjęcie i plik MOV o tej samej nazwie. Zaimportuj je razem (Zdjęcia iCloud, AirDrop lub Zdjęcia na Macu), aby otrzymać Live Photo",
diff --git a/src/Filee.App/Assets/i18n/pt-BR.json b/src/Filee.App/Assets/i18n/pt-BR.json
index 750998136..b15db4303 100644
--- a/src/Filee.App/Assets/i18n/pt-BR.json
+++ b/src/Filee.App/Assets/i18n/pt-BR.json
@@ -47,6 +47,11 @@
"home.clear_history": "Limpar",
"home.files": "{0} arquivos",
"home.failed_files": "{0} falharam",
+ "home.undo": "Desfazer",
+ "home.undo_hint": "Mover os resultados para a Lixeira",
+ "home.undone": "Desfeito: os resultados estão na Lixeira",
+ "home.undone_original": "Desfeito: os resultados estão na Lixeira, assim como os originais que eles substituíram",
+ "home.undo_failed": "Não foi possível mover {0} para a Lixeira: {1}",
"donut.center_drag": "Traga os arquivos para cá",
"donut.files_same": "{0} × {1}",
@@ -125,6 +130,7 @@
"trash.no_folder": "não há pasta de Lixeira",
"trash.failed": "não foi possível movê-lo para a Lixeira",
"trash.cannot_place": "não foi possível salvar o resultado na pasta do original",
+ "trash.changed": "ele foi alterado depois da conversão",
"notice.empty_spreadsheet_pages_removed": "As páginas vazias da planilha foram removidas",
"notice.ocr_has_text": "Todas as páginas já têm texto, então foi copiado como está",
"notice.live_photo_pair": "Salvo como uma foto e um MOV com o mesmo nome. Importe os dois juntos (Fotos do iCloud, AirDrop ou Fotos no Mac) para obter uma Live Photo",
diff --git a/src/Filee.App/Assets/i18n/pt-PT.json b/src/Filee.App/Assets/i18n/pt-PT.json
index e6ca7c15b..e67c02ea7 100644
--- a/src/Filee.App/Assets/i18n/pt-PT.json
+++ b/src/Filee.App/Assets/i18n/pt-PT.json
@@ -47,6 +47,11 @@
"home.clear_history": "Limpar",
"home.files": "{0} ficheiros",
"home.failed_files": "Falhas: {0}",
+ "home.undo": "Anular",
+ "home.undo_hint": "Mover os resultados para a Reciclagem ou para o Lixo",
+ "home.undone": "Anulado: os resultados estão na Reciclagem ou no Lixo",
+ "home.undone_original": "Anulado: os resultados estão na Reciclagem ou no Lixo, tal como os originais que substituíram",
+ "home.undo_failed": "Não foi possível mover {0} para a Reciclagem ou para o Lixo: {1}",
"donut.center_drag": "Arrasta ficheiros para aqui",
"donut.files_same": "{0} × {1}",
@@ -125,6 +130,7 @@
"trash.no_folder": "não existe uma pasta de Lixo",
"trash.failed": "não foi possível movê-lo para a Reciclagem ou para o Lixo",
"trash.cannot_place": "não foi possível guardar o resultado na pasta do original",
+ "trash.changed": "foi alterado depois da conversão",
"notice.empty_spreadsheet_pages_removed": "Foram removidas as páginas vazias das folhas de cálculo",
"notice.ocr_has_text": "Todas as páginas já têm texto, pelo que o ficheiro foi copiado sem alterações",
"notice.live_photo_pair": "Foi guardada uma fotografia e um MOV com o mesmo nome. Importa ambos em conjunto (Fotografias em iCloud, AirDrop ou Fotografias num Mac) para obteres uma Live Photo",
diff --git a/src/Filee.App/Assets/i18n/ru.json b/src/Filee.App/Assets/i18n/ru.json
index 29acdd1e0..45dd75ddc 100644
--- a/src/Filee.App/Assets/i18n/ru.json
+++ b/src/Filee.App/Assets/i18n/ru.json
@@ -47,6 +47,11 @@
"home.clear_history": "Очистить",
"home.files": "Файлов: {0}",
"home.failed_files": "Ошибок: {0}",
+ "home.undo": "Отменить",
+ "home.undo_hint": "Переместить результаты в Корзину",
+ "home.undone": "Отменено: результаты в Корзине",
+ "home.undone_original": "Отменено: результаты в Корзине, как и оригиналы, которые они заменили",
+ "home.undo_failed": "Не удалось переместить {0} в Корзину: {1}",
"donut.center_drag": "Перетащите сюда",
"donut.files_same": "{0} × {1}",
@@ -125,6 +130,7 @@
"trash.no_folder": "нет папки Корзины",
"trash.failed": "не удалось переместить его в Корзину",
"trash.cannot_place": "не удалось сохранить результат в папке оригинала",
+ "trash.changed": "он изменился после преобразования",
"notice.empty_spreadsheet_pages_removed": "Пустые страницы таблицы удалены",
"notice.ocr_has_text": "На всех страницах уже есть текст, поэтому файл скопирован как есть",
"notice.live_photo_pair": "Сохранено как фото и MOV с одинаковым именем. Импортируйте их вместе (Фото iCloud, AirDrop или «Фото» на Mac), чтобы получить Live Photo",
diff --git a/src/Filee.App/Assets/i18n/th.json b/src/Filee.App/Assets/i18n/th.json
index 8c419e6b3..d6d11bc7a 100644
--- a/src/Filee.App/Assets/i18n/th.json
+++ b/src/Filee.App/Assets/i18n/th.json
@@ -44,6 +44,11 @@
"home.clear_history": "ล้างประวัติ",
"home.files": "{0} ไฟล์",
"home.failed_files": "ล้มเหลว {0} ไฟล์",
+ "home.undo": "เลิกทำ",
+ "home.undo_hint": "ย้ายผลลัพธ์ไปที่ถังรีไซเคิล / ถังขยะ",
+ "home.undone": "เลิกทำแล้ว: ผลลัพธ์อยู่ในถังรีไซเคิล / ถังขยะ",
+ "home.undone_original": "เลิกทำแล้ว: ผลลัพธ์อยู่ในถังรีไซเคิล / ถังขยะ และต้นฉบับที่ถูกแทนที่ก็อยู่ที่นั่นด้วย",
+ "home.undo_failed": "ย้าย {0} ไปที่ถังรีไซเคิล / ถังขยะไม่ได้: {1}",
"donut.center_drag": "ลากไฟล์มาที่นี่",
"donut.files_same": "{0} × {1}",
"donut.files_mixed": "{0} ไฟล์",
@@ -119,6 +124,7 @@
"trash.no_folder": "ไม่มีโฟลเดอร์ถังขยะ",
"trash.failed": "ย้ายไปที่ถังรีไซเคิล / ถังขยะไม่ได้",
"trash.cannot_place": "บันทึกผลลัพธ์ในโฟลเดอร์ของไฟล์ต้นฉบับไม่ได้",
+ "trash.changed": "ไฟล์ถูกแก้ไขหลังการแปลง",
"notice.ocr_has_text": "ทุกหน้ามีข้อความอยู่แล้ว จึงคัดลอกไฟล์ตามเดิม",
"notice.empty_spreadsheet_pages_removed": "ลบหน้าว่างของสเปรดชีตออกแล้ว",
"notice.live_photo_pair": "บันทึกเป็นรูปภาพและไฟล์ MOV ที่มีชื่อเดียวกัน นำเข้าทั้งสองไฟล์พร้อมกัน (ผ่านรูปภาพ iCloud, AirDrop หรือ Photos บน Mac) เพื่อใช้งานเป็น Live Photo",
diff --git a/src/Filee.App/Assets/i18n/tr.json b/src/Filee.App/Assets/i18n/tr.json
index 62a8e5e1d..87babf8fe 100644
--- a/src/Filee.App/Assets/i18n/tr.json
+++ b/src/Filee.App/Assets/i18n/tr.json
@@ -44,6 +44,11 @@
"home.clear_history": "Temizle",
"home.files": "{0} dosya",
"home.failed_files": "{0} başarısız",
+ "home.undo": "Geri al",
+ "home.undo_hint": "Sonuçları Geri Dönüşüm Kutusu'na / Çöp Sepeti'ne taşı",
+ "home.undone": "Geri alındı: sonuçlar Geri Dönüşüm Kutusu'nda / Çöp Sepeti'nde",
+ "home.undone_original": "Geri alındı: sonuçlar ve yerini aldıkları orijinaller Geri Dönüşüm Kutusu'nda / Çöp Sepeti'nde",
+ "home.undo_failed": "{0} Geri Dönüşüm Kutusu'na / Çöp Sepeti'ne taşınamadı: {1}",
"donut.center_drag": "Dosyaları buraya getirin",
"donut.files_same": "{0} × {1}",
"donut.files_mixed": "{0} dosya",
@@ -119,6 +124,7 @@
"trash.no_folder": "Çöp Sepeti klasörü yok",
"trash.failed": "Geri Dönüşüm Kutusu'na / Çöp Sepeti'ne taşınamadı",
"trash.cannot_place": "sonuç özgün dosyanın klasörüne kaydedilemedi",
+ "trash.changed": "dönüştürmeden sonra değiştirildi",
"notice.empty_spreadsheet_pages_removed": "Elektronik tablodaki boş sayfalar kaldırıldı",
"notice.ocr_has_text": "Her sayfada zaten metin olduğu için dosya olduğu gibi kopyalandı",
"notice.live_photo_pair": "Aynı adlı fotoğraf ve MOV dosyası olarak kaydedildi. Live Photo elde etmek için ikisini birlikte içe aktarın (iCloud Fotoğrafları, AirDrop veya Mac'teki Fotoğraflar ile)",
diff --git a/src/Filee.App/Assets/i18n/zh-CN.json b/src/Filee.App/Assets/i18n/zh-CN.json
index 340de83b0..4ff4f9ac2 100644
--- a/src/Filee.App/Assets/i18n/zh-CN.json
+++ b/src/Filee.App/Assets/i18n/zh-CN.json
@@ -47,6 +47,11 @@
"home.clear_history": "清除",
"home.files": "{0} 个文件",
"home.failed_files": "{0} 个失败",
+ "home.undo": "撤销",
+ "home.undo_hint": "将结果移到回收站/废纸篓",
+ "home.undone": "已撤销:结果已在回收站/废纸篓中",
+ "home.undone_original": "已撤销:结果已在回收站/废纸篓中,被替换的原文件也在那里",
+ "home.undo_failed": "无法将 {0} 移到回收站/废纸篓:{1}",
"donut.center_drag": "把文件拖到这里",
"donut.files_same": "{0} 个 {1}",
@@ -125,6 +130,7 @@
"trash.no_folder": "没有废纸篓文件夹",
"trash.failed": "无法移到回收站/废纸篓",
"trash.cannot_place": "无法将结果保存到原文件所在的文件夹",
+ "trash.changed": "转换后它被修改过",
"notice.empty_spreadsheet_pages_removed": "已移除电子表格中的空白页面",
"notice.ocr_has_text": "每页都已有文字,已原样复制",
"notice.live_photo_pair": "已保存为同名的照片和 MOV。将两者一起导入(iCloud 照片、隔空投送或 Mac 上的“照片”)即可得到实况照片",
diff --git a/src/Filee.App/Assets/i18n/zh-TW.json b/src/Filee.App/Assets/i18n/zh-TW.json
index ee508a742..d9f1c9792 100644
--- a/src/Filee.App/Assets/i18n/zh-TW.json
+++ b/src/Filee.App/Assets/i18n/zh-TW.json
@@ -47,6 +47,11 @@
"home.clear_history": "清除",
"home.files": "{0} 個檔案",
"home.failed_files": "{0} 個失敗",
+ "home.undo": "復原",
+ "home.undo_hint": "將結果移到資源回收筒/垃圾桶",
+ "home.undone": "已復原:結果已在資源回收筒/垃圾桶中",
+ "home.undone_original": "已復原:結果已在資源回收筒/垃圾桶中,被取代的原始檔也在那裡",
+ "home.undo_failed": "無法將 {0} 移到資源回收筒/垃圾桶:{1}",
"donut.center_drag": "把檔案拖到這裡",
"donut.files_same": "{0} 個 {1}",
@@ -125,6 +130,7 @@
"trash.no_folder": "沒有垃圾桶資料夾",
"trash.failed": "無法移到資源回收筒/垃圾桶",
"trash.cannot_place": "無法將結果儲存到原始檔案所在的資料夾",
+ "trash.changed": "轉換後已被修改",
"notice.empty_spreadsheet_pages_removed": "已移除試算表中的空白頁面",
"notice.ocr_has_text": "每頁都已有文字,已原樣複製",
"notice.live_photo_pair": "已儲存為同名的照片和 MOV。將兩者一起匯入(iCloud 照片、隔空投送或 Mac 上的「照片」)即可得到原況照片",
diff --git a/src/Filee.App/Styles/Icons.axaml b/src/Filee.App/Styles/Icons.axaml
index 21ca4ad72..c8c0588be 100644
--- a/src/Filee.App/Styles/Icons.axaml
+++ b/src/Filee.App/Styles/Icons.axaml
@@ -33,6 +33,7 @@
M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2ZM12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35ZM17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z
+ M12.5,8C9.85,8 7.45,9 5.6,10.6L2,7V16H11L7.38,12.38C8.77,11.22 10.54,10.5 12.5,10.5C16.04,10.5 19.05,12.81 20.1,16L22.47,15.22C21.08,11.03 17.15,8 12.5,8ZM12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7ZM7,17V1H5V5H1V7H5V17A2,2 0 0,0 7,19H17V23H19V19H23V17M17,15H19V7C19,5.89 18.1,5 17,5H9V7H17V15ZM9.64,7.64C9.87,7.14 10,6.59 10,6A4,4 0 0,0 6,2A4,4 0 0,0 2,6A4,4 0 0,0 6,10C6.59,10 7.14,9.87 7.64,9.64L10,12L7.64,14.36C7.14,14.13 6.59,14 6,14A4,4 0 0,0 2,18A4,4 0 0,0 6,22A4,4 0 0,0 10,18C10,17.41 9.87,16.86 9.64,16.36L12,14L19,21H22V20L9.64,7.64M6,8A2,2 0 0,1 4,6A2,2 0 0,1 6,4A2,2 0 0,1 8,6A2,2 0 0,1 6,8M6,20A2,2 0 0,1 4,18A2,2 0 0,1 6,16A2,2 0 0,1 8,18A2,2 0 0,1 6,20M19,3L13,9L15,11L22,4V3H19Z
diff --git a/src/Filee.App/ViewModels/Pages/HomePageViewModel.cs b/src/Filee.App/ViewModels/Pages/HomePageViewModel.cs
index 447facf1f..9d7e1a48e 100644
--- a/src/Filee.App/ViewModels/Pages/HomePageViewModel.cs
+++ b/src/Filee.App/ViewModels/Pages/HomePageViewModel.cs
@@ -18,17 +18,22 @@ namespace Filee.App.ViewModels.Pages;
public sealed partial class HistoryItemViewModel : ObservableObject
{
private readonly HistoryEntry _entry;
+ private readonly ILocalizer _loc;
private readonly IPlatformServices _platform;
private readonly ConversionService? _conversions;
+ private readonly UserDataStore? _store;
/// The first output still on disk (the one the buttons open), or null.
private readonly string? _output;
- public HistoryItemViewModel(HistoryEntry entry, ILocalizer loc, IPlatformServices platform, ConversionService? conversions = null)
+ public HistoryItemViewModel(HistoryEntry entry, ILocalizer loc, IPlatformServices platform, ConversionService? conversions = null,
+ UserDataStore? store = null)
{
_entry = entry;
+ _loc = loc;
_platform = platform;
_conversions = conversions;
+ _store = store;
Title = entry.PresetName;
Subtitle = entry.Sources.Count == 1 ? Path.GetFileName(entry.Sources[0]) : loc.Format("home.files", entry.Sources.Count);
Time = entry.FinishedAt.Date == DateTime.Today ? entry.FinishedAt.ToString("t") : entry.FinishedAt.ToString("d");
@@ -52,6 +57,9 @@ public HistoryItemViewModel(HistoryEntry entry, ILocalizer loc, IPlatformService
ThumbnailSource = picture is not null && HistoryThumbnails.CanShow(picture) ? picture : null;
// Only entries that remember their preset (Filee 1.8 and later) and still have a failed file to convert.
CanRetry = conversions is not null && entry.Preset is not null && entry.Failures.Any(f => ConversionService.Exists(f.Source));
+ // Undo (#553): only while every result is still as the job left it, so a file changed since is never moved.
+ CanUndo = store is not null && !entry.Undone && entry.Undoable.Count > 0 && entry.Undoable.All(o => o.Unchanged());
+ UndoneText = entry.Undone ? loc[entry.OriginalsInTrash ? "home.undone_original" : "home.undone"] : "";
}
public string Title { get; }
@@ -101,6 +109,43 @@ private void Retry()
_conversions.Retry(_entry.Failures.Select(f => f.Source), _entry.Preset, _entry.WatchRuleId);
}
+ public bool CanUndo { get; }
+
+ /// "Undone: the results are in the Recycle Bin / Trash", or empty.
+ public string UndoneText { get; }
+
+ public bool IsUndone => UndoneText.Length > 0;
+
+ /// Why a result couldn't be moved to the Recycle Bin / Trash, or empty.
+ [ObservableProperty, NotifyPropertyChangedFor(nameof(HasUndoError))] private string _undoError = "";
+
+ public bool HasUndoError => UndoError.Length > 0;
+
+ ///
+ /// Moves the job's results to the Recycle Bin / Trash. A result moved by an earlier try is skipped, one changed
+ /// since is kept. "Replace the original" sent the originals there already: they stay, and the card says so.
+ ///
+ [RelayCommand]
+ private void Undo()
+ {
+ if (!CanUndo || _store is null)
+ return;
+ UndoError = "";
+ foreach (var output in _entry.Undoable)
+ {
+ if (!File.Exists(output.Path))
+ continue;
+ var refusal = output.Unchanged() ? _platform.MoveToTrash(output.Path) : new TrashRefusal("trash.changed");
+ if (refusal is not null)
+ {
+ UndoError = _loc.Format("home.undo_failed", Path.GetFileName(output.Path), _loc[refusal.ReasonKey]);
+ return;
+ }
+ }
+ _entry.Undone = true;
+ _store.SaveHistory();
+ }
+
[RelayCommand]
private void OpenFolder()
{
@@ -223,7 +268,7 @@ private void Reload()
.ToList();
History.Clear();
foreach (var entry in entries)
- History.Add(new HistoryItemViewModel(entry, _loc, _platform, _conversions));
+ History.Add(new HistoryItemViewModel(entry, _loc, _platform, _conversions, _store));
IsEmpty = _store.History.Count == 0;
NoMatches = !IsEmpty && History.Count == 0;
LoadThumbnails();
diff --git a/src/Filee.App/Views/Pages/HomePage.axaml b/src/Filee.App/Views/Pages/HomePage.axaml
index a83a3d28d..aae555da6 100644
--- a/src/Filee.App/Views/Pages/HomePage.axaml
+++ b/src/Filee.App/Views/Pages/HomePage.axaml
@@ -113,6 +113,13 @@
+
+
@@ -156,6 +163,9 @@
+
+
+