forked from akai/readest
feat(sync): cloud deletion in transfer queue (#2964)
This commit is contained in:
@@ -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": "تم الحذف"
|
||||
}
|
||||
|
||||
@@ -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": "মুছে ফেলা হয়েছে"
|
||||
}
|
||||
|
||||
@@ -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": "བསུབས་ཟིན།"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Διαγράφηκε"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "حذف شد"
|
||||
}
|
||||
|
||||
@@ -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é"
|
||||
}
|
||||
|
||||
@@ -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": "हटाया गया"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "削除済み"
|
||||
}
|
||||
|
||||
@@ -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": "삭제됨"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Удалено"
|
||||
}
|
||||
|
||||
@@ -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": "මකා දමන ලදී"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "நீக்கப்பட்டது"
|
||||
}
|
||||
|
||||
@@ -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": "ลบแล้ว"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "Видалено"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "已删除"
|
||||
}
|
||||
|
||||
@@ -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": "已刪除"
|
||||
}
|
||||
|
||||
@@ -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<BookshelfProps> = ({
|
||||
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<HTMLDivElement>();
|
||||
@@ -87,8 +86,6 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
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<BookshelfProps> = ({
|
||||
|
||||
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<BookshelfProps> = ({
|
||||
/>
|
||||
</ModalPortal>
|
||||
)}
|
||||
{isTransferQueueOpen && (
|
||||
<ModalPortal>
|
||||
<TransferQueuePanel />
|
||||
</ModalPortal>
|
||||
)}
|
||||
{showDeleteAlert && (
|
||||
<div
|
||||
className={clsx('delete-alert fixed bottom-0 left-0 right-0 z-50 flex justify-center')}
|
||||
@@ -401,6 +397,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
count: getBooksToDelete().length,
|
||||
})}
|
||||
onCancel={() => {
|
||||
abortDeletionRef.current = true;
|
||||
setShowDeleteAlert(false);
|
||||
setShowSelectModeActions(true);
|
||||
}}
|
||||
|
||||
@@ -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' ? (
|
||||
<MdCloudUpload className='text-primary' size={size} />
|
||||
) : type === 'delete' ? (
|
||||
<MdDeleteSweep className='text-primary' size={size} />
|
||||
) : (
|
||||
<MdCloudDownload className='text-primary' size={size} />
|
||||
);
|
||||
@@ -81,6 +83,12 @@ const TransferItemRow: React.FC<{
|
||||
}> = ({ transfer, onCancel, onRetry, iconSize }) => {
|
||||
const _ = useTranslation();
|
||||
|
||||
const completedLabel = {
|
||||
upload: _('Uploaded'),
|
||||
download: _('Downloaded'),
|
||||
delete: _('Deleted'),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='hover:bg-base-200 flex items-center gap-3 rounded-lg p-3'>
|
||||
<StatusIcon status={transfer.status} type={transfer.type} size={iconSize} />
|
||||
@@ -100,7 +108,7 @@ const TransferItemRow: React.FC<{
|
||||
{transfer.status === 'failed' && (
|
||||
<span className='text-error'>{transfer.error || _('Failed')}</span>
|
||||
)}
|
||||
{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,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 && (
|
||||
<ModalPortal>
|
||||
<TransferQueuePanel />
|
||||
</ModalPortal>
|
||||
)}
|
||||
<AboutWindow />
|
||||
<UpdaterWindow />
|
||||
<MigrateDataWindow />
|
||||
|
||||
@@ -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<BookMenuProps> = ({ 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<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
|
||||
const viewSettings = getViewSettings(sideBarBookKey)!;
|
||||
viewSettings.sortedTOC = !isSortedTOC;
|
||||
setViewSettings(sideBarBookKey, viewSettings);
|
||||
recreateViewer(envConfig, sideBarBookKey);
|
||||
}
|
||||
setTimeout(() => window.location.reload(), 100);
|
||||
};
|
||||
const handleSetParallel = () => {
|
||||
setParallel(bookKeys);
|
||||
|
||||
@@ -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<{
|
||||
></path>
|
||||
</svg>
|
||||
<div className='flex flex-col gap-y-2'>
|
||||
<h3 className='text-start text-sm sm:text-center'>{title}</h3>
|
||||
<div className='text-start text-xs sm:text-center'>{message}</div>
|
||||
<h3 className='text-start text-sm font-medium sm:text-center'>{title}</h3>
|
||||
<div className='text-start text-sm sm:text-center'>{message}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='buttons flex flex-wrap items-center justify-end gap-2 self-end sm:max-w-[20vw] sm:self-center'>
|
||||
<button className='btn btn-sm btn-neutral' onClick={onCancel}>
|
||||
{_('Cancel')}
|
||||
</button>
|
||||
<button className='btn btn-sm btn-warning' onClick={onConfirm}>
|
||||
<button
|
||||
className={clsx('btn btn-sm btn-warning', { 'btn-disabled': isProcessing })}
|
||||
onClick={() => {
|
||||
setIsProcessing(true);
|
||||
onConfirm();
|
||||
}}
|
||||
>
|
||||
{_('Confirm')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface SyncResult {
|
||||
configs: BookConfigRecord[] | null;
|
||||
}
|
||||
|
||||
export type SyncRecord = BookRecord & BookConfigRecord & BookNoteRecord;
|
||||
|
||||
export interface SyncData {
|
||||
books?: Partial<BookRecord>[];
|
||||
notes?: Partial<BookNoteRecord>[];
|
||||
|
||||
@@ -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<string>();
|
||||
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(',');
|
||||
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TransferState>((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<TransferState>((set, get) => ({
|
||||
maxRetries: 3,
|
||||
createdAt: Date.now(),
|
||||
priority,
|
||||
isBackground,
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
@@ -257,7 +260,8 @@ export const useTransferStore = create<TransferState>((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),
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user