From 7f26e45ae71c48a801ddb61e18b5fd743dd99301 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Thu, 15 Jan 2026 13:50:24 +0100 Subject: [PATCH] feat(sync): cloud deletion in transfer queue (#2964) --- .../public/locales/ar/translation.json | 5 +- .../public/locales/bn/translation.json | 5 +- .../public/locales/bo/translation.json | 5 +- .../public/locales/de/translation.json | 5 +- .../public/locales/el/translation.json | 5 +- .../public/locales/es/translation.json | 5 +- .../public/locales/fa/translation.json | 5 +- .../public/locales/fr/translation.json | 5 +- .../public/locales/hi/translation.json | 5 +- .../public/locales/id/translation.json | 5 +- .../public/locales/it/translation.json | 5 +- .../public/locales/ja/translation.json | 5 +- .../public/locales/ko/translation.json | 5 +- .../public/locales/ms/translation.json | 5 +- .../public/locales/nl/translation.json | 5 +- .../public/locales/pl/translation.json | 5 +- .../public/locales/pt/translation.json | 5 +- .../public/locales/ru/translation.json | 5 +- .../public/locales/si/translation.json | 5 +- .../public/locales/sv/translation.json | 5 +- .../public/locales/ta/translation.json | 5 +- .../public/locales/th/translation.json | 5 +- .../public/locales/tr/translation.json | 5 +- .../public/locales/uk/translation.json | 5 +- .../public/locales/vi/translation.json | 5 +- .../public/locales/zh-CN/translation.json | 5 +- .../public/locales/zh-TW/translation.json | 5 +- .../src/app/library/components/Bookshelf.tsx | 17 ++--- .../library/components/TransferQueuePanel.tsx | 16 +++-- .../src/app/library/hooks/useBooksSync.ts | 17 +++-- apps/readest-app/src/app/library/page.tsx | 37 +++++++++-- .../reader/components/sidebar/BookMenu.tsx | 10 +-- apps/readest-app/src/components/Alert.tsx | 13 +++- apps/readest-app/src/hooks/useSync.ts | 3 +- apps/readest-app/src/libs/sync.ts | 2 + apps/readest-app/src/pages/api/sync.ts | 55 +++++++++++----- .../src/services/transferManager.ts | 66 +++++++++++++------ apps/readest-app/src/store/transferStore.ts | 10 ++- 38 files changed, 281 insertions(+), 100 deletions(-) diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 457aecdb..89b0b34c 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -886,5 +886,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "تعذر تحميل المقال. حاول البحث مباشرة على {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "تعذر تحميل الكلمة. حاول البحث مباشرة على {{link}}.", "Date Published": "تاريخ النشر", - "Only for TTS:": "فقط لـ TTS:" + "Only for TTS:": "فقط لـ TTS:", + "Uploaded": "تم الرفع", + "Downloaded": "تم التنزيل", + "Deleted": "تم الحذف" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 04e4250a..0c8df43d 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -846,5 +846,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "নিবন্ধ লোড করতে অক্ষম। সরাসরি {{link}} এ অনুসন্ধান করার চেষ্টা করুন।", "Unable to load the word. Try searching directly on {{link}}.": "শব্দ লোড করতে অক্ষম। সরাসরি {{link}} এ অনুসন্ধান করার চেষ্টা করুন।", "Date Published": "প্রকাশনার তারিখ", - "Only for TTS:": "শুধুমাত্র TTS-এর জন্য:" + "Only for TTS:": "শুধুমাত্র TTS-এর জন্য:", + "Uploaded": "আপলোড হয়েছে", + "Downloaded": "ডাউনলোড হয়েছে", + "Deleted": "মুছে ফেলা হয়েছে" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index ee1a1dd9..899c3f26 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -836,5 +836,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "རྩོམ་ཡིག་མངོན་མི་ཐུབ། {{link}} ཐད་ཀར་འཚོལ་བ་བྱོས།", "Unable to load the word. Try searching directly on {{link}}.": "ཚིག་མངོན་མི་ཐུབ། {{link}} ཐད་ཀར་འཚོལ་བ་བྱོས།", "Date Published": "པར་སྐྲུན་ཚེས་གྲངས།", - "Only for TTS:": "TTS ལ་ཁོ་ན།:" + "Only for TTS:": "TTS ལ་ཁོ་ན།:", + "Uploaded": "ཡར་སྐྱེལ་བྱས་ཟིན།", + "Downloaded": "མར་ལེན་བྱས་ཟིན།", + "Deleted": "བསུབས་ཟིན།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 518fb2e0..80a46dfa 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -846,5 +846,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Artikel kann nicht geladen werden. Versuchen Sie die direkte Suche auf {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Wort kann nicht geladen werden. Versuchen Sie die direkte Suche auf {{link}}.", "Date Published": "Veröffentlichungsdatum", - "Only for TTS:": "Nur für TTS:" + "Only for TTS:": "Nur für TTS:", + "Uploaded": "Hochgeladen", + "Downloaded": "Heruntergeladen", + "Deleted": "Gelöscht" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index eb96f1c3..f5230dba 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -846,5 +846,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Αδυναμία φόρτωσης του άρθρου. Δοκιμάστε να αναζητήσετε απευθείας στο {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Αδυναμία φόρτωσης της λέξης. Δοκιμάστε να αναζητήσετε απευθείας στο {{link}}.", "Date Published": "Ημερομηνία δημοσίευσης", - "Only for TTS:": "Μόνο για TTS:" + "Only for TTS:": "Μόνο για TTS:", + "Uploaded": "Μεταφορτώθηκε", + "Downloaded": "Λήφθηκε", + "Deleted": "Διαγράφηκε" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index d98962d3..88976bcc 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -856,5 +856,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "No se puede cargar el artículo. Intenta buscar directamente en {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "No se puede cargar la palabra. Intenta buscar directamente en {{link}}.", "Date Published": "Fecha de publicación", - "Only for TTS:": "Solo para TTS:" + "Only for TTS:": "Solo para TTS:", + "Uploaded": "Subido", + "Downloaded": "Descargado", + "Deleted": "Eliminado" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 2085cc9e..a339b5f3 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -846,5 +846,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "بارگذاری مقاله امکان‌پذیر نیست. سعی کنید مستقیماً در {{link}} جستجو کنید.", "Unable to load the word. Try searching directly on {{link}}.": "بارگذاری کلمه امکان‌پذیر نیست. سعی کنید مستقیماً در {{link}} جستجو کنید.", "Date Published": "تاریخ انتشار", - "Only for TTS:": "فقط برای TTS:" + "Only for TTS:": "فقط برای TTS:", + "Uploaded": "بارگذاری شد", + "Downloaded": "دانلود شد", + "Deleted": "حذف شد" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 3af62562..9930cf7a 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -856,5 +856,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Impossible de charger l'article. Essayez de rechercher directement sur {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Impossible de charger le mot. Essayez de rechercher directement sur {{link}}.", "Date Published": "Date de publication", - "Only for TTS:": "Uniquement pour TTS :" + "Only for TTS:": "Uniquement pour TTS :", + "Uploaded": "Téléversé", + "Downloaded": "Téléchargé", + "Deleted": "Supprimé" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 9ceb3b47..0d8592fa 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -846,5 +846,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "लेख लोड करने में असमर्थ। सीधे {{link}} पर खोजने का प्रयास करें।", "Unable to load the word. Try searching directly on {{link}}.": "शब्द लोड करने में असमर्थ। सीधे {{link}} पर खोजने का प्रयास करें।", "Date Published": "प्रकाशन तिथि", - "Only for TTS:": "केवल TTS के लिए:" + "Only for TTS:": "केवल TTS के लिए:", + "Uploaded": "अपलोड किया गया", + "Downloaded": "डाउनलोड किया गया", + "Deleted": "हटाया गया" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 30509594..d067993a 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -836,5 +836,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Tidak dapat memuat artikel. Coba cari langsung di {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Tidak dapat memuat kata. Coba cari langsung di {{link}}.", "Date Published": "Tanggal terbit", - "Only for TTS:": "Hanya untuk TTS:" + "Only for TTS:": "Hanya untuk TTS:", + "Uploaded": "Diunggah", + "Downloaded": "Diunduh", + "Deleted": "Dihapus" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 16a44e8b..826b6f07 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -856,5 +856,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Impossibile caricare l'articolo. Prova a cercare direttamente su {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Impossibile caricare la parola. Prova a cercare direttamente su {{link}}.", "Date Published": "Data di pubblicazione", - "Only for TTS:": "Solo per TTS:" + "Only for TTS:": "Solo per TTS:", + "Uploaded": "Caricato", + "Downloaded": "Scaricato", + "Deleted": "Eliminato" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 464af482..2fd17f2f 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -836,5 +836,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "記事を読み込めません。{{link}}で直接検索してみてください。", "Unable to load the word. Try searching directly on {{link}}.": "単語を読み込めません。{{link}}で直接検索してみてください。", "Date Published": "出版日", - "Only for TTS:": "TTSのみ:" + "Only for TTS:": "TTSのみ:", + "Uploaded": "アップロード済み", + "Downloaded": "ダウンロード済み", + "Deleted": "削除済み" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index cf0ed5ae..a2b20235 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -836,5 +836,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "문서를 로드할 수 없습니다. {{link}}에서 직접 검색해 보세요.", "Unable to load the word. Try searching directly on {{link}}.": "단어를 로드할 수 없습니다. {{link}}에서 직접 검색해 보세요.", "Date Published": "출판일", - "Only for TTS:": "TTS 전용:" + "Only for TTS:": "TTS 전용:", + "Uploaded": "업로드됨", + "Downloaded": "다운로드됨", + "Deleted": "삭제됨" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index fd5b653c..23260e64 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -836,5 +836,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Tidak dapat memuatkan artikel. Cuba cari terus di {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Tidak dapat memuatkan perkataan. Cuba cari terus di {{link}}.", "Date Published": "Tarikh diterbitkan", - "Only for TTS:": "Hanya untuk TTS:" + "Only for TTS:": "Hanya untuk TTS:", + "Uploaded": "Dimuat naik", + "Downloaded": "Dimuat turun", + "Deleted": "Dipadam" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index bdb48af2..45f3bd03 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -846,5 +846,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Kan het artikel niet laden. Probeer direct te zoeken op {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Kan het woord niet laden. Probeer direct te zoeken op {{link}}.", "Date Published": "Publicatiedatum", - "Only for TTS:": "Alleen voor TTS:" + "Only for TTS:": "Alleen voor TTS:", + "Uploaded": "Geüpload", + "Downloaded": "Gedownload", + "Deleted": "Verwijderd" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index df58e466..cdc3e8ec 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -866,5 +866,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Nie można załadować artykułu. Spróbuj wyszukać bezpośrednio na {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Nie można załadować słowa. Spróbuj wyszukać bezpośrednio na {{link}}.", "Date Published": "Data wydania", - "Only for TTS:": "Tylko dla TTS:" + "Only for TTS:": "Tylko dla TTS:", + "Uploaded": "Przesłano", + "Downloaded": "Pobrano", + "Deleted": "Usunięto" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 4a0fdf32..83f138d3 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -856,5 +856,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Não foi possível carregar o artigo. Tente pesquisar diretamente em {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Não foi possível carregar a palavra. Tente pesquisar diretamente em {{link}}.", "Date Published": "Data de publicação", - "Only for TTS:": "Apenas para TTS:" + "Only for TTS:": "Apenas para TTS:", + "Uploaded": "Enviado", + "Downloaded": "Baixado", + "Deleted": "Excluído" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 41c15d89..d778cb0c 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -866,5 +866,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Не удалось загрузить статью. Попробуйте искать напрямую на {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Не удалось загрузить слово. Попробуйте искать напрямую на {{link}}.", "Date Published": "Дата публикации", - "Only for TTS:": "Только для TTS:" + "Only for TTS:": "Только для TTS:", + "Uploaded": "Загружено", + "Downloaded": "Скачано", + "Deleted": "Удалено" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index aaba3175..5657a0cd 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -846,5 +846,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "ලිපිය පූරණය කළ නොහැක. {{link}} හි කෙලින්ම සෙවීමට උත්සාහ කරන්න.", "Unable to load the word. Try searching directly on {{link}}.": "වචනය පූරණය කළ නොහැක. {{link}} හි කෙලින්ම සෙවීමට උත්සාහ කරන්න.", "Date Published": "ප්‍රකාශන දිනය", - "Only for TTS:": "TTS සඳහා පමණක්:" + "Only for TTS:": "TTS සඳහා පමණක්:", + "Uploaded": "උඩුගත කරන ලදී", + "Downloaded": "බාගත කරන ලදී", + "Deleted": "මකා දමන ලදී" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 4574c2b2..3cb1f507 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -846,5 +846,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Kan inte ladda artikeln. Försök söka direkt på {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Kan inte ladda ordet. Försök söka direkt på {{link}}.", "Date Published": "Publiceringsdatum", - "Only for TTS:": "Endast för TTS:" + "Only for TTS:": "Endast för TTS:", + "Uploaded": "Uppladdad", + "Downloaded": "Nedladdad", + "Deleted": "Borttagen" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index e77df677..5427c12a 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -846,5 +846,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "கட்டுரையை ஏற்ற முடியவில்லை. நேரடியாக {{link}} இல் தேடவும்.", "Unable to load the word. Try searching directly on {{link}}.": "சொல்லை ஏற்ற முடியவில்லை. நேரடியாக {{link}} இல் தேடவும்.", "Date Published": "வெளியீட்டு தேதி", - "Only for TTS:": "TTS க்கு மட்டும்:" + "Only for TTS:": "TTS க்கு மட்டும்:", + "Uploaded": "பதிவேற்றப்பட்டது", + "Downloaded": "பதிவிறக்கப்பட்டது", + "Deleted": "நீக்கப்பட்டது" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index efabb3b5..a31df248 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -836,5 +836,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "ไม่สามารถโหลดบทความได้ ลองค้นหาโดยตรงที่ {{link}}", "Unable to load the word. Try searching directly on {{link}}.": "ไม่สามารถโหลดคำได้ ลองค้นหาโดยตรงที่ {{link}}", "Date Published": "วันที่เผยแพร่", - "Only for TTS:": "สำหรับ TTS เท่านั้น:" + "Only for TTS:": "สำหรับ TTS เท่านั้น:", + "Uploaded": "อัปโหลดแล้ว", + "Downloaded": "ดาวน์โหลดแล้ว", + "Deleted": "ลบแล้ว" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 7e07ce20..ec0b144a 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -846,5 +846,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Makale yüklenemiyor. Doğrudan {{link}} üzerinde arama yapmayı deneyin.", "Unable to load the word. Try searching directly on {{link}}.": "Kelime yüklenemiyor. Doğrudan {{link}} üzerinde arama yapmayı deneyin.", "Date Published": "Yayın tarihi", - "Only for TTS:": "Sadece TTS için:" + "Only for TTS:": "Sadece TTS için:", + "Uploaded": "Yüklendi", + "Downloaded": "İndirildi", + "Deleted": "Silindi" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 529d2289..ebd636eb 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -866,5 +866,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Не вдалося завантажити статтю. Спробуйте шукати безпосередньо на {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Не вдалося завантажити слово. Спробуйте шукати безпосередньо на {{link}}.", "Date Published": "Дата публікації", - "Only for TTS:": "Тільки для TTS:" + "Only for TTS:": "Тільки для TTS:", + "Uploaded": "Завантажено", + "Downloaded": "Звантажено", + "Deleted": "Видалено" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index af03638f..44e8cd7a 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -836,5 +836,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "Không thể tải bài viết. Hãy thử tìm kiếm trực tiếp trên {{link}}.", "Unable to load the word. Try searching directly on {{link}}.": "Không thể tải từ. Hãy thử tìm kiếm trực tiếp trên {{link}}.", "Date Published": "Ngày xuất bản", - "Only for TTS:": "Chỉ dành cho TTS:" + "Only for TTS:": "Chỉ dành cho TTS:", + "Uploaded": "Đã tải lên", + "Downloaded": "Đã tải xuống", + "Deleted": "Đã xóa" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 8fad4107..9c766d36 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -836,5 +836,8 @@ "Error": "错误", "Unable to load the article. Try searching directly on {{link}}.": "无法加载文章。请直接在 {{link}} 上搜索。", "Unable to load the word. Try searching directly on {{link}}.": "无法加载词汇。请直接在 {{link}} 上搜索。", - "Date Published": "出版日期" + "Date Published": "出版日期", + "Uploaded": "已上传", + "Downloaded": "已下载", + "Deleted": "已删除" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 0287084d..9e74ce55 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -836,5 +836,8 @@ "Unable to load the article. Try searching directly on {{link}}.": "無法載入文章。請直接在 {{link}} 上搜尋。", "Unable to load the word. Try searching directly on {{link}}.": "無法載入詞彙。請直接在 {{link}} 上搜尋。", "Date Published": "出版日期", - "Only for TTS:": "僅用於TTS:" + "Only for TTS:": "僅用於TTS:", + "Uploaded": "已上傳", + "Downloaded": "已下載", + "Deleted": "已刪除" } diff --git a/apps/readest-app/src/app/library/components/Bookshelf.tsx b/apps/readest-app/src/app/library/components/Bookshelf.tsx index 18d32b26..bcac89fd 100644 --- a/apps/readest-app/src/app/library/components/Bookshelf.tsx +++ b/apps/readest-app/src/app/library/components/Bookshelf.tsx @@ -10,7 +10,6 @@ import { useThemeStore } from '@/store/themeStore'; import { useAutoFocus } from '@/hooks/useAutoFocus'; import { useSettingsStore } from '@/store/settingsStore'; import { useLibraryStore } from '@/store/libraryStore'; -import { useTransferStore } from '@/store/transferStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { navigateToLibrary, navigateToReader, showReaderWindow } from '@/utils/nav'; @@ -24,7 +23,6 @@ import ModalPortal from '@/components/ModalPortal'; import BookshelfItem, { generateBookshelfItems } from './BookshelfItem'; import SelectModeActions from './SelectModeActions'; import GroupingModal from './GroupingModal'; -import TransferQueuePanel from './TransferQueuePanel'; interface BookshelfProps { libraryBooks: Book[]; @@ -79,6 +77,7 @@ const Bookshelf: React.FC = ({ const [showGroupingModal, setShowGroupingModal] = useState(false); const [importBookUrl] = useState(searchParams?.get('url') || ''); + const abortDeletionRef = useRef(false); const isImportingBook = useRef(false); const iconSize15 = useResponsiveSize(15); const autofocusRef = useAutoFocus(); @@ -87,8 +86,6 @@ const Bookshelf: React.FC = ({ const { setSelectedBooks, getSelectedBooks, toggleSelectedBook } = useLibraryStore(); const { getGroupName } = useLibraryStore(); - const { isTransferQueueOpen } = useTransferStore(); - const uiLanguage = localStorage?.getItem('i18nextLng') || ''; const updateUrlParams = useCallback( @@ -223,9 +220,13 @@ const Bookshelf: React.FC = ({ const confirmDelete = async () => { const books = getBooksToDelete(); - const concurrency = 4; + const concurrency = 20; for (let i = 0; i < books.length; i += concurrency) { + if (abortDeletionRef.current) { + abortDeletionRef.current = false; + break; + } const batch = books.slice(i, i + concurrency); await Promise.all(batch.map((book) => handleBookDelete(book, false))); } @@ -383,11 +384,6 @@ const Bookshelf: React.FC = ({ /> )} - {isTransferQueueOpen && ( - - - - )} {showDeleteAlert && (
= ({ count: getBooksToDelete().length, })} onCancel={() => { + abortDeletionRef.current = true; setShowDeleteAlert(false); setShowSelectModeActions(true); }} diff --git a/apps/readest-app/src/app/library/components/TransferQueuePanel.tsx b/apps/readest-app/src/app/library/components/TransferQueuePanel.tsx index fe01a17c..b75968a1 100644 --- a/apps/readest-app/src/app/library/components/TransferQueuePanel.tsx +++ b/apps/readest-app/src/app/library/components/TransferQueuePanel.tsx @@ -52,7 +52,7 @@ const formatDateTime = (timestamp: number): string => { const StatusIcon: React.FC<{ status: TransferStatus; - type: 'upload' | 'download'; + type: 'upload' | 'download' | 'delete'; size: number; }> = ({ status, type, size }) => { switch (status) { @@ -67,6 +67,8 @@ const StatusIcon: React.FC<{ default: return type === 'upload' ? ( + ) : type === 'delete' ? ( + ) : ( ); @@ -81,6 +83,12 @@ const TransferItemRow: React.FC<{ }> = ({ transfer, onCancel, onRetry, iconSize }) => { const _ = useTranslation(); + const completedLabel = { + upload: _('Uploaded'), + download: _('Downloaded'), + delete: _('Deleted'), + }; + return (
@@ -100,7 +108,7 @@ const TransferItemRow: React.FC<{ {transfer.status === 'failed' && ( {transfer.error || _('Failed')} )} - {transfer.status === 'completed' && _('Completed')} + {transfer.status === 'completed' && (completedLabel[transfer.type] || _('Completed'))} {transfer.status === 'cancelled' && _('Cancelled')} {' · '} {formatDateTime(transfer.completedAt || transfer.startedAt || transfer.createdAt)} @@ -167,9 +175,7 @@ const TransferQueuePanel: React.FC = () => { const onClose = () => setIsOpen(false); const divRef = useKeyDownActions({ onCancel: onClose, onConfirm: onClose }); - const booksToUpload = getVisibleLibrary().filter( - (book) => book.downloadedAt && !book.uploadedAt, - ); + const booksToUpload = getVisibleLibrary().filter((book) => book.downloadedAt && !book.uploadedAt); const booksToDownload = getVisibleLibrary().filter( (book) => book.uploadedAt && !book.downloadedAt, ); diff --git a/apps/readest-app/src/app/library/hooks/useBooksSync.ts b/apps/readest-app/src/app/library/hooks/useBooksSync.ts index 79598804..64027a17 100644 --- a/apps/readest-app/src/app/library/hooks/useBooksSync.ts +++ b/apps/readest-app/src/app/library/hooks/useBooksSync.ts @@ -11,7 +11,7 @@ export const useBooksSync = () => { const { user } = useAuth(); const { appService } = useEnv(); const { library, isSyncing, setLibrary, setIsSyncing, setSyncProgress } = useLibraryStore(); - const { syncedBooks, syncBooks, lastSyncedAtBooks } = useSync(); + const { useSyncInited, syncedBooks, syncBooks, lastSyncedAtBooks } = useSync(); const isPullingRef = useRef(false); const getNewBooks = useCallback(() => { @@ -64,8 +64,7 @@ export const useBooksSync = () => { return; } handleAutoSync(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [library, handleAutoSync]); + }, [user, library, handleAutoSync]); const pushLibrary = useCallback(async () => { if (!user) return; @@ -76,9 +75,9 @@ export const useBooksSync = () => { }, [user, syncBooks, getNewBooks]); useEffect(() => { - if (!user) return; + if (!user || !useSyncInited) return; pullLibrary(); - }, [user, pullLibrary]); + }, [user, useSyncInited, pullLibrary]); const updateLibrary = async () => { if (isSyncing) return; @@ -107,13 +106,16 @@ export const useBooksSync = () => { return oldBook; }; - const oldBooksBatchSize = 20; + const oldBooksBatchSize = 100; for (let i = 0; i < oldBooksNeedsDownload.length; i += oldBooksBatchSize) { const batch = oldBooksNeedsDownload.slice(i, i + oldBooksBatchSize); await appService?.downloadBookCovers(batch); } const updatedLibrary = await Promise.all(library.map(processOldBook)); + setLibrary(updatedLibrary); + appService?.saveLibraryBooks(updatedLibrary); + const bookHashesInLibrary = new Set(updatedLibrary.map((book) => book.hash)); const newBooks = syncedBooks.filter( (newBook) => @@ -140,7 +142,8 @@ export const useBooksSync = () => { setLibrary([...updatedLibrary]); appService?.saveLibraryBooks(updatedLibrary); } - setLibrary(updatedLibrary); + } catch (err) { + console.error('Error updating new books:', err); } finally { if (newBooks.length > 0) { setIsSyncing(false); diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index 15124c8c..583209ec 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -36,6 +36,7 @@ import { useUICSS } from '@/hooks/useUICSS'; import { useDemoBooks } from './hooks/useDemoBooks'; import { useBooksSync } from './hooks/useBooksSync'; import { useBookDataStore } from '@/store/bookDataStore'; +import { useTransferStore } from '@/store/transferStore'; import { useScreenWakeLock } from '@/hooks/useScreenWakeLock'; import { useOpenWithBooks } from '@/hooks/useOpenWithBooks'; import { SelectedFile, useFileSelector } from '@/hooks/useFileSelector'; @@ -65,6 +66,8 @@ import Bookshelf from './components/Bookshelf'; import useShortcuts from '@/hooks/useShortcuts'; import DropIndicator from '@/components/DropIndicator'; import SettingsDialog from '@/components/settings/SettingsDialog'; +import ModalPortal from '@/components/ModalPortal'; +import TransferQueuePanel from './components/TransferQueuePanel'; const LibraryPageWithSearchParams = () => { const searchParams = useSearchParams(); @@ -95,6 +98,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP const { clearBookData } = useBookDataStore(); const { settings, setSettings, saveSettings } = useSettingsStore(); const { isSettingsDialogOpen, setSettingsDialogOpen } = useSettingsStore(); + const { isTransferQueueOpen } = useTransferStore(); const [showCatalogManager, setShowCatalogManager] = useState( searchParams?.get('opds') === 'true', ); @@ -558,14 +562,32 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP cloud: _('Failed to delete cloud backup of the book: {{title}}', { title: book.title }), local: _('Failed to delete local copy of the book: {{title}}', { title: book.title }), }; + try { - await appService?.deleteBook(book, deleteAction); - await updateBook(envConfig, book); - clearBookData(book.hash); - if (syncBooks) pushLibrary(); + // Handle local deletion immediately + if (deleteAction === 'local' || deleteAction === 'both') { + await appService?.deleteBook(book, 'local'); + if (deleteAction === 'both') { + book.deletedAt = Date.now(); + book.downloadedAt = null; + book.coverDownloadedAt = null; + } + await updateBook(envConfig, book); + clearBookData(book.hash); + if (syncBooks) pushLibrary(); + } + + // Queue cloud deletion + if (deleteAction === 'cloud' || deleteAction === 'both') { + const transferId = transferManager.queueDelete(book, 1, true); + if (!transferId) { + throw new Error('Failed to queue cloud deletion'); + } + } + eventDispatcher.dispatch('toast', { type: 'info', - timeout: 2000, + timeout: 1000, message: deletionMessages[deleteAction], }); return true; @@ -849,6 +871,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP handleBookMetadataUpdate={handleUpdateMetadata} /> )} + {isTransferQueueOpen && ( + + + + )} diff --git a/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx b/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx index 6700580f..c844a673 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx @@ -3,7 +3,7 @@ import React from 'react'; import Image from 'next/image'; import { MdCheck } from 'react-icons/md'; -import { setAboutDialogVisible } from '@/components/AboutWindow'; +import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; import { useLibraryStore } from '@/store/libraryStore'; import { useSidebarStore } from '@/store/sidebarStore'; @@ -12,10 +12,11 @@ import { useSettingsStore } from '@/store/settingsStore'; import { useParallelViewStore } from '@/store/parallelViewStore'; import { isWebAppPlatform } from '@/services/environment'; import { eventDispatcher } from '@/utils/event'; +import { FIXED_LAYOUT_FORMATS } from '@/types/book'; import { DOWNLOAD_READEST_URL } from '@/services/constants'; import { setKOSyncSettingsWindowVisible } from '@/app/reader/components/KOSyncSettings'; import { setProofreadRulesVisibility } from '@/app/reader/components/ProofreadRules'; -import { FIXED_LAYOUT_FORMATS } from '@/types/book'; +import { setAboutDialogVisible } from '@/components/AboutWindow'; import useBooksManager from '../../hooks/useBooksManager'; import MenuItem from '@/components/MenuItem'; import Menu from '@/components/Menu'; @@ -27,8 +28,9 @@ interface BookMenuProps { const BookMenu: React.FC = ({ menuClassName, setIsDropdownOpen }) => { const _ = useTranslation(); + const { envConfig } = useEnv(); const { settings } = useSettingsStore(); - const { bookKeys, getViewSettings, setViewSettings } = useReaderStore(); + const { bookKeys, recreateViewer, getViewSettings, setViewSettings } = useReaderStore(); const { getVisibleLibrary } = useLibraryStore(); const { openParallelView } = useBooksManager(); const { sideBarBookKey } = useSidebarStore(); @@ -64,8 +66,8 @@ const BookMenu: React.FC = ({ menuClassName, setIsDropdownOpen }) const viewSettings = getViewSettings(sideBarBookKey)!; viewSettings.sortedTOC = !isSortedTOC; setViewSettings(sideBarBookKey, viewSettings); + recreateViewer(envConfig, sideBarBookKey); } - setTimeout(() => window.location.reload(), 100); }; const handleSetParallel = () => { setParallel(bookKeys); diff --git a/apps/readest-app/src/components/Alert.tsx b/apps/readest-app/src/components/Alert.tsx index af7cf9df..69f3231e 100644 --- a/apps/readest-app/src/components/Alert.tsx +++ b/apps/readest-app/src/components/Alert.tsx @@ -10,6 +10,7 @@ const Alert: React.FC<{ onConfirm: () => void; }> = ({ title, message, onCancel, onConfirm }) => { const _ = useTranslation(); + const [isProcessing, setIsProcessing] = React.useState(false); const divRef = useKeyDownActions({ onCancel, onConfirm }); return ( @@ -39,15 +40,21 @@ const Alert: React.FC<{ >
-

{title}

-
{message}
+

{title}

+
{message}
-
diff --git a/apps/readest-app/src/hooks/useSync.ts b/apps/readest-app/src/hooks/useSync.ts index e3d1aa03..67ee2db3 100644 --- a/apps/readest-app/src/hooks/useSync.ts +++ b/apps/readest-app/src/hooks/useSync.ts @@ -181,7 +181,7 @@ export function useSync(bookKey?: string) { await pushChanges({ books }); } if (op === 'pull' || op === 'both') { - await pullChanges('books', lastSyncedAtBooks, setLastSyncedAtBooks, setSyncingBooks); + await pullChanges('books', lastSyncedAtBooks + 1, setLastSyncedAtBooks, setSyncingBooks); } }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -258,6 +258,7 @@ export function useSync(bookKey?: string) { lastSyncedAtBooks, lastSyncedAtNotes, lastSyncedAtConfigs, + useSyncInited: lastSyncedAtInited, pullChanges, pushChanges, syncBooks, diff --git a/apps/readest-app/src/libs/sync.ts b/apps/readest-app/src/libs/sync.ts index 247bc6e5..78b4e1c7 100644 --- a/apps/readest-app/src/libs/sync.ts +++ b/apps/readest-app/src/libs/sync.ts @@ -18,6 +18,8 @@ export interface SyncResult { configs: BookConfigRecord[] | null; } +export type SyncRecord = BookRecord & BookConfigRecord & BookNoteRecord; + export interface SyncData { books?: Partial[]; notes?: Partial[]; diff --git a/apps/readest-app/src/pages/api/sync.ts b/apps/readest-app/src/pages/api/sync.ts index fecd604a..94110678 100644 --- a/apps/readest-app/src/pages/api/sync.ts +++ b/apps/readest-app/src/pages/api/sync.ts @@ -7,7 +7,7 @@ import { transformBookConfigToDB } from '@/utils/transform'; import { transformBookNoteToDB } from '@/utils/transform'; import { transformBookToDB } from '@/utils/transform'; import { runMiddleware, corsAllMethods } from '@/utils/cors'; -import { SyncData, SyncResult, SyncType } from '@/libs/sync'; +import { SyncData, SyncRecord, SyncResult, SyncType } from '@/libs/sync'; import { validateUserAndToken } from '@/utils/access'; import { DBBook, DBBookConfig } from '@/types/records'; @@ -60,21 +60,44 @@ export async function GET(req: NextRequest) { }; const queryTables = async (table: TableName, dedupeKeys?: (keyof BookDataRecord)[]) => { - let query = supabase.from(table).select('*').eq('user_id', user.id); - if (bookParam && metaHashParam) { - query.or(`book_hash.eq.${bookParam},meta_hash.eq.${metaHashParam}`); - } else if (bookParam) { - query.eq('book_hash', bookParam); - } else if (metaHashParam) { - query.eq('meta_hash', metaHashParam); + const PAGE_SIZE = 1000; + let allRecords: SyncRecord[] = []; + let offset = 0; + let hasMore = true; + + while (hasMore) { + let query = supabase + .from(table) + .select('*') + .eq('user_id', user.id) + .range(offset, offset + PAGE_SIZE - 1); + + if (bookParam && metaHashParam) { + query = query.or(`book_hash.eq.${bookParam},meta_hash.eq.${metaHashParam}`); + } else if (bookParam) { + query = query.eq('book_hash', bookParam); + } else if (metaHashParam) { + query = query.eq('meta_hash', metaHashParam); + } + + query = query.or(`updated_at.gt.${sinceIso},deleted_at.gt.${sinceIso}`); + query = query.order('updated_at', { ascending: false }); + + console.log('Querying table:', table, 'since:', sinceIso, 'offset:', offset); + + const { data, error } = await query; + if (error) throw { table, error } as DBError; + + if (data && data.length > 0) { + allRecords = allRecords.concat(data); + offset += PAGE_SIZE; + hasMore = data.length === PAGE_SIZE; + } else { + hasMore = false; + } } - query = query.or(`updated_at.gt.${sinceIso},deleted_at.gt.${sinceIso}`); - query = query.order('updated_at', { ascending: false }); - console.log('Querying table:', table, 'since:', sinceIso); - const { data, error } = await query; - if (error) throw { table, error } as DBError; - let records = data; + let records = allRecords; if (dedupeKeys && dedupeKeys.length > 0) { const seen = new Set(); records = records.filter((rec) => { @@ -167,8 +190,8 @@ export async function POST(req: NextRequest) { // Fetch existing records for this batch const orConditions = matchConditions .map((cond) => { - const parts = Object.entries(cond).map(([key, val]) => `and(${key}.eq.${val})`); - return parts.join(','); + const parts = Object.entries(cond).map(([key, val]) => `${key}.eq.${val}`); + return `and(${parts.join(',')})`; }) .join(','); diff --git a/apps/readest-app/src/services/transferManager.ts b/apps/readest-app/src/services/transferManager.ts index 9bc858aa..3c1e4ff6 100644 --- a/apps/readest-app/src/services/transferManager.ts +++ b/apps/readest-app/src/services/transferManager.ts @@ -94,6 +94,25 @@ class TransferManager { return transferId; } + queueDelete(book: Book, priority: number = 10, isBackground: boolean = false): string | null { + if (!this.isReady()) { + console.warn('TransferManager not initialized'); + return null; + } + + const store = useTransferStore.getState(); + + const existing = store.getTransferByBookHash(book.hash, 'delete'); + if (existing) { + return existing.id; + } + + const transferId = store.addTransfer(book.hash, book.title, 'delete', priority, isBackground); + this.persistQueue(); + this.processQueue(); + return transferId; + } + queueBatchUploads(books: Book[], priority: number = 10): string[] { return books .map((book) => this.queueUpload(book, priority)) @@ -221,23 +240,31 @@ class TransferManager { if (transfer.type === 'upload') { await this.appService.uploadBook(book, progressHandler); book.uploadedAt = Date.now(); - } else { + await this.updateBook(book); + } else if (transfer.type === 'download') { await this.appService.downloadBook(book, false, false, progressHandler); book.downloadedAt = Date.now(); + await this.updateBook(book); + } else if (transfer.type === 'delete') { + await this.appService.deleteBook(book, 'cloud'); + await this.updateBook(book); } - await this.updateBook(book); - useTransferStore.getState().setTransferStatus(transfer.id, 'completed'); - eventDispatcher.dispatch('toast', { - type: 'info', - timeout: 2000, - message: - transfer.type === 'upload' - ? _('Book uploaded: {{title}}', { title: transfer.bookTitle }) - : _('Book downloaded: {{title}}', { title: transfer.bookTitle }), - }); + const successMessages = { + upload: _('Book uploaded: {{title}}', { title: transfer.bookTitle }), + download: _('Book downloaded: {{title}}', { title: transfer.bookTitle }), + delete: _('Deleted cloud backup of the book: {{title}}', { title: transfer.bookTitle }), + }; + + if (!transfer.isBackground) { + eventDispatcher.dispatch('toast', { + type: 'info', + timeout: 2000, + message: successMessages[transfer.type], + }); + } } catch (error) { if (abortController.signal.aborted) { // Already cancelled, don't update status @@ -273,16 +300,17 @@ class TransferManager { message: _('Insufficient storage quota'), }); } else { + const errorMessages = { + upload: _('Failed to upload book: {{title}}', { title: transfer.bookTitle }), + download: _('Failed to download book: {{title}}', { title: transfer.bookTitle }), + delete: _('Failed to delete cloud backup of the book: {{title}}', { + title: transfer.bookTitle, + }), + }; + eventDispatcher.dispatch('toast', { type: 'error', - message: - transfer.type === 'upload' - ? _('Failed to upload book: {{title}}', { - title: transfer.bookTitle, - }) - : _('Failed to download book: {{title}}', { - title: transfer.bookTitle, - }), + message: errorMessages[transfer.type], }); } diff --git a/apps/readest-app/src/store/transferStore.ts b/apps/readest-app/src/store/transferStore.ts index 6a913058..e546f609 100644 --- a/apps/readest-app/src/store/transferStore.ts +++ b/apps/readest-app/src/store/transferStore.ts @@ -1,6 +1,6 @@ import { create } from 'zustand'; -export type TransferType = 'upload' | 'download'; +export type TransferType = 'upload' | 'download' | 'delete'; export type TransferStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'cancelled'; export interface TransferItem { @@ -20,6 +20,7 @@ export interface TransferItem { startedAt?: number; completedAt?: number; priority: number; // Lower = higher priority + isBackground: boolean; } interface TransferState { @@ -38,6 +39,7 @@ interface TransferState { bookTitle: string, type: TransferType, priority?: number, + isBackground?: boolean, ) => string; removeTransfer: (transferId: string) => void; updateTransferProgress: ( @@ -92,7 +94,7 @@ export const useTransferStore = create((set, get) => ({ setIsTransferQueueOpen: (isOpen) => set({ isTransferQueueOpen: isOpen }), - addTransfer: (bookHash, bookTitle, type, priority = 10) => { + addTransfer: (bookHash, bookTitle, type, priority = 10, isBackground = false) => { const id = generateTransferId(); const transfer: TransferItem = { id, @@ -108,6 +110,7 @@ export const useTransferStore = create((set, get) => ({ maxRetries: 3, createdAt: Date.now(), priority, + isBackground, }; set((state) => ({ @@ -257,7 +260,8 @@ export const useTransferStore = create((set, get) => ({ getTransferByBookHash: (bookHash, type) => { return Object.values(get().transfers).find( - (t) => t.bookHash === bookHash && t.type === type && ['pending', 'in_progress'].includes(t.status), + (t) => + t.bookHash === bookHash && t.type === type && ['pending', 'in_progress'].includes(t.status), ); },