feat: toggle finished manually (#3091)
This commit is contained in:
@@ -998,5 +998,13 @@
|
||||
"Ollama Model": "نموذج Ollama",
|
||||
"AI Gateway Model": "نموذج بوابة الذكاء الاصطناعي",
|
||||
"Actions": "الإجراءات",
|
||||
"Navigation": "التنقل"
|
||||
"Navigation": "التنقل",
|
||||
"Set status for {{count}} book(s)_one": "تعيين الحالة لـ {{count}} كتاب",
|
||||
"Set status for {{count}} book(s)_other": "تعيين الحالة لـ {{count}} كتب",
|
||||
"Mark as Unread": "تحديد كغير مقروء",
|
||||
"Mark as Finished": "تحديد كمنتهى",
|
||||
"Finished": "منتهى",
|
||||
"Unread": "غير مقروء",
|
||||
"Clear Status": "مسح الحالة",
|
||||
"Status": "الحالة"
|
||||
}
|
||||
|
||||
@@ -958,5 +958,13 @@
|
||||
"Ollama Model": "Ollama মডেল",
|
||||
"AI Gateway Model": "AI গেটওয়ে মডেল",
|
||||
"Actions": "অ্যাকশন",
|
||||
"Navigation": "নেভিগেশন"
|
||||
"Navigation": "নেভিগেশন",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} টি বইয়ের স্ট্যাটাস সেট করুন",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} টি বইয়ের স্ট্যাটাস সেট করুন",
|
||||
"Mark as Unread": "পড়া হয়নি হিসেবে চিহ্নিত করুন",
|
||||
"Mark as Finished": "পড়া শেষ হিসেবে চিহ্নিত করুন",
|
||||
"Finished": "শেষ হয়েছে",
|
||||
"Unread": "অপঠিত",
|
||||
"Clear Status": "স্থিতি মুছুন",
|
||||
"Status": "অবস্থা"
|
||||
}
|
||||
|
||||
@@ -948,5 +948,13 @@
|
||||
"Ollama Model": "Ollama དཔེ་གཞི།",
|
||||
"AI Gateway Model": "AI Gateway དཔེ་གཞི།",
|
||||
"Actions": "བྱ་འགུལ།",
|
||||
"Navigation": "འགྲུལ་བཞུད།"
|
||||
"Navigation": "འགྲུལ་བཞུད།",
|
||||
"Set status for {{count}} book(s)_one": "དེབ་ {{count}} གི་གནས་སྟངས་གཏན་འཁེལ་བྱེད།",
|
||||
"Set status for {{count}} book(s)_other": "དེབ་ {{count}} གི་གནས་སྟངས་གཏན་འཁེལ་བྱེད།",
|
||||
"Mark as Unread": "མ་ཀློག་པར་རྟགས་རྒྱག",
|
||||
"Mark as Finished": "ཚར་བར་རྟགས་རྒྱག",
|
||||
"Finished": "ཚར་སོང་།",
|
||||
"Unread": "མ་ཀློག་པ།",
|
||||
"Clear Status": "གནས་སྟངས་གཙང་མ།",
|
||||
"Status": "གནས་སྟངས།"
|
||||
}
|
||||
|
||||
@@ -958,5 +958,13 @@
|
||||
"Ollama Model": "Ollama-Modell",
|
||||
"AI Gateway Model": "AI Gateway-Modell",
|
||||
"Actions": "Aktionen",
|
||||
"Navigation": "Navigation"
|
||||
"Navigation": "Navigation",
|
||||
"Set status for {{count}} book(s)_one": "Status für {{count}} Buch festlegen",
|
||||
"Set status for {{count}} book(s)_other": "Status für {{count}} Bücher festlegen",
|
||||
"Mark as Unread": "Als ungelesen markieren",
|
||||
"Mark as Finished": "Als beendet markieren",
|
||||
"Finished": "Beendet",
|
||||
"Unread": "Ungelesen",
|
||||
"Clear Status": "Status löschen",
|
||||
"Status": "Status"
|
||||
}
|
||||
|
||||
@@ -958,5 +958,13 @@
|
||||
"Ollama Model": "Μοντέλο Ollama",
|
||||
"AI Gateway Model": "Μοντέλο AI Gateway",
|
||||
"Actions": "Ενέργειες",
|
||||
"Navigation": "Πλοήγηση"
|
||||
"Navigation": "Πλοήγηση",
|
||||
"Set status for {{count}} book(s)_one": "Ορισμός κατάστασης για {{count}} βιβλίο",
|
||||
"Set status for {{count}} book(s)_other": "Ορισμός κατάστασης για {{count}} βιβλία",
|
||||
"Mark as Unread": "Σήμανση ως μη αναγνωσμένο",
|
||||
"Mark as Finished": "Σήμανση ως ολοκληρωμένο",
|
||||
"Finished": "Ολοκληρώθηκε",
|
||||
"Unread": "Μη αναγνωσμένο",
|
||||
"Clear Status": "Εκκαθάριση κατάστασης",
|
||||
"Status": "Κατάσταση"
|
||||
}
|
||||
|
||||
@@ -19,5 +19,13 @@
|
||||
"Are you sure to delete {{count}} selected file(s)?_one": "Are you sure to delete {{count}} selected file?",
|
||||
"Are you sure to delete {{count}} selected file(s)?_other": "Are you sure to delete {{count}} selected files?",
|
||||
"Successfully imported {{count}} book(s)_one": "Successfully imported {{count}} book",
|
||||
"Successfully imported {{count}} book(s)_other": "Successfully imported {{count}} books"
|
||||
"Successfully imported {{count}} book(s)_other": "Successfully imported {{count}} books",
|
||||
"Set status for {{count}} book(s)_one": "Set status for {{count}} book",
|
||||
"Set status for {{count}} book(s)_other": "Set status for {{count}} books",
|
||||
"Mark as Finished": "Mark as Finished",
|
||||
"Mark as Unread": "Mark as Unread",
|
||||
"Finished": "Finished",
|
||||
"Unread": "Unread",
|
||||
"Clear Status": "Clear Status",
|
||||
"Status": "Status"
|
||||
}
|
||||
|
||||
@@ -968,5 +968,13 @@
|
||||
"Ollama Model": "Modelo de Ollama",
|
||||
"AI Gateway Model": "Modelo de AI Gateway",
|
||||
"Actions": "Acciones",
|
||||
"Navigation": "Navegación"
|
||||
"Navigation": "Navegación",
|
||||
"Set status for {{count}} book(s)_one": "Establecer estado para {{count}} libro",
|
||||
"Set status for {{count}} book(s)_other": "Establecer estado para {{count}} libros",
|
||||
"Mark as Unread": "Marcar como no leído",
|
||||
"Mark as Finished": "Marcar como terminado",
|
||||
"Finished": "Terminado",
|
||||
"Unread": "Sin leer",
|
||||
"Clear Status": "Borrar estado",
|
||||
"Status": "Estado"
|
||||
}
|
||||
|
||||
@@ -958,5 +958,13 @@
|
||||
"Ollama Model": "مدل Ollama",
|
||||
"AI Gateway Model": "مدل AI Gateway",
|
||||
"Actions": "عملیات",
|
||||
"Navigation": "ناوبری"
|
||||
"Navigation": "ناوبری",
|
||||
"Set status for {{count}} book(s)_one": "تعیین وضعیت برای {{count}} کتاب",
|
||||
"Set status for {{count}} book(s)_other": "تعیین وضعیت برای {{count}} کتاب",
|
||||
"Mark as Unread": "علامتگذاری بهعنوان خواندهنشده",
|
||||
"Mark as Finished": "علامتگذاری بهعنوان تمامشده",
|
||||
"Finished": "تمامشده",
|
||||
"Unread": "خواندهنشده",
|
||||
"Clear Status": "پاک کردن وضعیت",
|
||||
"Status": "وضعیت"
|
||||
}
|
||||
|
||||
@@ -968,5 +968,13 @@
|
||||
"Ollama Model": "Modèle Ollama",
|
||||
"AI Gateway Model": "Modèle AI Gateway",
|
||||
"Actions": "Actions",
|
||||
"Navigation": "Navigation"
|
||||
"Navigation": "Navigation",
|
||||
"Set status for {{count}} book(s)_one": "Définir le statut pour {{count}} livre",
|
||||
"Set status for {{count}} book(s)_other": "Définir le statut pour {{count}} livres",
|
||||
"Mark as Unread": "Marquer comme non lu",
|
||||
"Mark as Finished": "Marquer comme terminé",
|
||||
"Finished": "Terminé",
|
||||
"Unread": "Non lu",
|
||||
"Clear Status": "Effacer le statut",
|
||||
"Status": "Statut"
|
||||
}
|
||||
|
||||
@@ -958,5 +958,13 @@
|
||||
"Ollama Model": "Ollama मॉडल",
|
||||
"AI Gateway Model": "AI गेटवे मॉडल",
|
||||
"Actions": "क्रियाएं",
|
||||
"Navigation": "नेविगेशन"
|
||||
"Navigation": "नेविगेशन",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} किताब की स्थिति सेट करें",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} किताबों की स्थिति सेट करें",
|
||||
"Mark as Unread": "अपठित के रूप में चिह्नित करें",
|
||||
"Mark as Finished": "समाप्त के रूप में चिह्नित करें",
|
||||
"Finished": "समाप्त",
|
||||
"Unread": "अपठित",
|
||||
"Clear Status": "स्थिति साफ़ करें",
|
||||
"Status": "स्थिति"
|
||||
}
|
||||
|
||||
@@ -948,5 +948,13 @@
|
||||
"Ollama Model": "Model Ollama",
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Tindakan",
|
||||
"Navigation": "Navigasi"
|
||||
"Navigation": "Navigasi",
|
||||
"Set status for {{count}} book(s)_one": "Atur status untuk {{count}} buku",
|
||||
"Set status for {{count}} book(s)_other": "Atur status untuk {{count}} buku",
|
||||
"Mark as Unread": "Tandai sebagai Belum Dibaca",
|
||||
"Mark as Finished": "Tandai sebagai Selesai",
|
||||
"Finished": "Selesai",
|
||||
"Unread": "Belum Dibaca",
|
||||
"Clear Status": "Hapus Status",
|
||||
"Status": "Status"
|
||||
}
|
||||
|
||||
@@ -968,5 +968,13 @@
|
||||
"Ollama Model": "Modello Ollama",
|
||||
"AI Gateway Model": "Modello AI Gateway",
|
||||
"Actions": "Azioni",
|
||||
"Navigation": "Navigazione"
|
||||
"Navigation": "Navigazione",
|
||||
"Set status for {{count}} book(s)_one": "Imposta stato per {{count}} libro",
|
||||
"Set status for {{count}} book(s)_other": "Imposta stato per {{count}} libri",
|
||||
"Mark as Unread": "Segna come non letto",
|
||||
"Mark as Finished": "Segna come finito",
|
||||
"Finished": "Finito",
|
||||
"Unread": "Non letto",
|
||||
"Clear Status": "Cancella stato",
|
||||
"Status": "Stato"
|
||||
}
|
||||
|
||||
@@ -948,5 +948,13 @@
|
||||
"Ollama Model": "Ollamaモデル",
|
||||
"AI Gateway Model": "AI Gatewayモデル",
|
||||
"Actions": "アクション",
|
||||
"Navigation": "ナビゲーション"
|
||||
"Navigation": "ナビゲーション",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} 冊の本のステータスを設定",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} 冊の本のステータスを設定",
|
||||
"Mark as Unread": "未読にする",
|
||||
"Mark as Finished": "読了にする",
|
||||
"Finished": "読了",
|
||||
"Unread": "未読",
|
||||
"Clear Status": "ステータスをクリア",
|
||||
"Status": "ステータス"
|
||||
}
|
||||
|
||||
@@ -948,5 +948,13 @@
|
||||
"Ollama Model": "Ollama 모델",
|
||||
"AI Gateway Model": "AI 게이트웨이 모델",
|
||||
"Actions": "작업",
|
||||
"Navigation": "탐색"
|
||||
"Navigation": "탐색",
|
||||
"Set status for {{count}} book(s)_one": "책 {{count}}권 상태 설정",
|
||||
"Set status for {{count}} book(s)_other": "책 {{count}}권 상태 설정",
|
||||
"Mark as Unread": "읽지 않음으로 표시",
|
||||
"Mark as Finished": "완료로 표시",
|
||||
"Finished": "완료",
|
||||
"Unread": "읽지 않음",
|
||||
"Clear Status": "상태 지우기",
|
||||
"Status": "상태"
|
||||
}
|
||||
|
||||
@@ -948,5 +948,13 @@
|
||||
"Ollama Model": "Model Ollama",
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Tindakan",
|
||||
"Navigation": "Navigasi"
|
||||
"Navigation": "Navigasi",
|
||||
"Set status for {{count}} book(s)_one": "Tetapkan status untuk {{count}} buku",
|
||||
"Set status for {{count}} book(s)_other": "Tetapkan status untuk {{count}} buku",
|
||||
"Mark as Unread": "Tandakan sebagai Belum Dibaca",
|
||||
"Mark as Finished": "Tandakan sebagai Selesai",
|
||||
"Finished": "Selesai",
|
||||
"Unread": "Belum Dibaca",
|
||||
"Clear Status": "Kosongkan Status",
|
||||
"Status": "Status"
|
||||
}
|
||||
|
||||
@@ -958,5 +958,13 @@
|
||||
"Ollama Model": "Ollama-model",
|
||||
"AI Gateway Model": "AI Gateway-model",
|
||||
"Actions": "Acties",
|
||||
"Navigation": "Navigatie"
|
||||
"Navigation": "Navigatie",
|
||||
"Set status for {{count}} book(s)_one": "Stel status in voor {{count}} boek",
|
||||
"Set status for {{count}} book(s)_other": "Stel status in voor {{count}} boeken",
|
||||
"Mark as Unread": "Markeren als ongelezen",
|
||||
"Mark as Finished": "Markeren als voltooid",
|
||||
"Finished": "Voltooid",
|
||||
"Unread": "Ongelezen",
|
||||
"Clear Status": "Status wissen",
|
||||
"Status": "Status"
|
||||
}
|
||||
|
||||
@@ -978,5 +978,13 @@
|
||||
"Ollama Model": "Model Ollama",
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Akcje",
|
||||
"Navigation": "Nawigacja"
|
||||
"Navigation": "Nawigacja",
|
||||
"Set status for {{count}} book(s)_one": "Ustaw status dla {{count}} książki",
|
||||
"Set status for {{count}} book(s)_other": "Ustaw status dla {{count}} książek",
|
||||
"Mark as Unread": "Oznacz jako nieprzeczytane",
|
||||
"Mark as Finished": "Oznacz jako ukończone",
|
||||
"Finished": "Ukończone",
|
||||
"Unread": "Nieprzeczytane",
|
||||
"Clear Status": "Wyczyść status",
|
||||
"Status": "Status"
|
||||
}
|
||||
|
||||
@@ -968,5 +968,13 @@
|
||||
"Ollama Model": "Modelo do Ollama",
|
||||
"AI Gateway Model": "Modelo de AI Gateway",
|
||||
"Actions": "Ações",
|
||||
"Navigation": "Navegação"
|
||||
"Navigation": "Navegação",
|
||||
"Set status for {{count}} book(s)_one": "Definir estado para {{count}} livro",
|
||||
"Set status for {{count}} book(s)_other": "Definir estado para {{count}} livros",
|
||||
"Mark as Unread": "Marcar como não lido",
|
||||
"Mark as Finished": "Marcar como concluído",
|
||||
"Finished": "Concluído",
|
||||
"Unread": "Não lido",
|
||||
"Clear Status": "Limpar estado",
|
||||
"Status": "Estado"
|
||||
}
|
||||
|
||||
@@ -978,5 +978,13 @@
|
||||
"Ollama Model": "Модель Ollama",
|
||||
"AI Gateway Model": "Модель AI Gateway",
|
||||
"Actions": "Действия",
|
||||
"Navigation": "Навигация"
|
||||
"Navigation": "Навигация",
|
||||
"Set status for {{count}} book(s)_one": "Установить статус для {{count}} книги",
|
||||
"Set status for {{count}} book(s)_other": "Установить статус для {{count}} книг",
|
||||
"Mark as Unread": "Отметить как непрочитанное",
|
||||
"Mark as Finished": "Отметить как прочитанное",
|
||||
"Finished": "Прочитано",
|
||||
"Unread": "Не прочитано",
|
||||
"Clear Status": "Очистить статус",
|
||||
"Status": "Статус"
|
||||
}
|
||||
|
||||
@@ -958,5 +958,13 @@
|
||||
"Ollama Model": "Ollama ආකෘතිය",
|
||||
"AI Gateway Model": "AI ਗੇਟਵੇ ਆਕෘතිය",
|
||||
"Actions": "ක්රියා",
|
||||
"Navigation": "සංචාලනය"
|
||||
"Navigation": "සංචාලනය",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} පොත සඳහා තත්ත්වය සකසන්න",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} පොත් සඳහා තත්ත්වය සකසන්න",
|
||||
"Mark as Unread": "නොකියවූ ලෙස සලකුණු කරන්න",
|
||||
"Mark as Finished": "අවසන් වූ ලෙස සලකුණු කරන්න",
|
||||
"Finished": "අවසන්",
|
||||
"Unread": "නොකියවූ",
|
||||
"Clear Status": "තත්ත්වය හිස් කරන්න",
|
||||
"Status": "තත්ත්වය"
|
||||
}
|
||||
|
||||
@@ -958,5 +958,13 @@
|
||||
"Ollama Model": "Ollama-modell",
|
||||
"AI Gateway Model": "AI Gateway-modell",
|
||||
"Actions": "Åtgärder",
|
||||
"Navigation": "Navigering"
|
||||
"Navigation": "Navigering",
|
||||
"Set status for {{count}} book(s)_one": "Ange status för {{count}} bok",
|
||||
"Set status for {{count}} book(s)_other": "Ange status för {{count}} böcker",
|
||||
"Mark as Unread": "Markera som oläst",
|
||||
"Mark as Finished": "Markera som läst",
|
||||
"Finished": "Läst",
|
||||
"Unread": "Oläst",
|
||||
"Clear Status": "Rensa status",
|
||||
"Status": "Status"
|
||||
}
|
||||
|
||||
@@ -958,5 +958,13 @@
|
||||
"Ollama Model": "Ollama மாதிரி",
|
||||
"AI Gateway Model": "AI கேட்வே மாதிரி",
|
||||
"Actions": "செயல்கள்",
|
||||
"Navigation": "வழிசெலுத்தல்"
|
||||
"Navigation": "வழிசெலுத்தல்",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} புத்தகத்திற்கான நிலையை அமைக்கவும்",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} புத்தகங்களுக்கான நிலையை அமைக்கவும்",
|
||||
"Mark as Unread": "படிக்காததாகக் குறிக்கவும்",
|
||||
"Mark as Finished": "முடித்ததாகக் குறிக்கவும்",
|
||||
"Finished": "முடிந்தது",
|
||||
"Unread": "படிக்கவில்லை",
|
||||
"Clear Status": "நிலையை அழி",
|
||||
"Status": "நிலை"
|
||||
}
|
||||
|
||||
@@ -948,5 +948,13 @@
|
||||
"Ollama Model": "โมเดล Ollama",
|
||||
"AI Gateway Model": "โมเดล AI Gateway",
|
||||
"Actions": "การกระทำ",
|
||||
"Navigation": "การนำทาง"
|
||||
"Navigation": "การนำทาง",
|
||||
"Set status for {{count}} book(s)_one": "ตั้งสถานะสำหรับหนังสือ {{count}} เล่ม",
|
||||
"Set status for {{count}} book(s)_other": "ตั้งสถานะสำหรับหนังสือ {{count}} เล่ม",
|
||||
"Mark as Unread": "ทำเครื่องหมายว่ายังไม่อ่าน",
|
||||
"Mark as Finished": "ทำเครื่องหมายว่าอ่านจบแล้ว",
|
||||
"Finished": "อ่านจบแล้ว",
|
||||
"Unread": "ยังไม่ได้อ่าน",
|
||||
"Clear Status": "ล้างสถานะ",
|
||||
"Status": "สถานะ"
|
||||
}
|
||||
|
||||
@@ -958,5 +958,13 @@
|
||||
"Ollama Model": "Ollama Modeli",
|
||||
"AI Gateway Model": "AI Gateway Modeli",
|
||||
"Actions": "Eylemler",
|
||||
"Navigation": "Gezين"
|
||||
"Navigation": "Gezinme",
|
||||
"Set status for {{count}} book(s)_one": "{{count}} kitap için durumu ayarla",
|
||||
"Set status for {{count}} book(s)_other": "{{count}} kitap için durumu ayarla",
|
||||
"Mark as Unread": "Okunmadı olarak işaretle",
|
||||
"Mark as Finished": "Bitirildi olarak işaretle",
|
||||
"Finished": "Bitirildi",
|
||||
"Unread": "Okunmadı",
|
||||
"Clear Status": "Durumu Temizle",
|
||||
"Status": "Durum"
|
||||
}
|
||||
|
||||
@@ -978,5 +978,13 @@
|
||||
"Ollama Model": "Модель Ollama",
|
||||
"AI Gateway Model": "Модель AI Gateway",
|
||||
"Actions": "Дії",
|
||||
"Navigation": "Навігація"
|
||||
"Navigation": "Навігація",
|
||||
"Set status for {{count}} book(s)_one": "Встановити статус для {{count}} книги",
|
||||
"Set status for {{count}} book(s)_other": "Встановити статус для {{count}} книг",
|
||||
"Mark as Unread": "Позначити як непрочитане",
|
||||
"Mark as Finished": "Позначити як завершене",
|
||||
"Finished": "Завершено",
|
||||
"Unread": "Непрочитане",
|
||||
"Clear Status": "Очистити статус",
|
||||
"Status": "Статус"
|
||||
}
|
||||
|
||||
@@ -948,5 +948,13 @@
|
||||
"Ollama Model": "Mô hình Ollama",
|
||||
"AI Gateway Model": "Mô hình AI Gateway",
|
||||
"Actions": "Hành động",
|
||||
"Navigation": "Điều hướng"
|
||||
"Navigation": "Điều hướng",
|
||||
"Set status for {{count}} book(s)_one": "Đặt trạng thái cho {{count}} cuốn sách",
|
||||
"Set status for {{count}} book(s)_other": "Đặt trạng thái cho {{count}} cuốn sách",
|
||||
"Mark as Unread": "Đánh dấu là chưa đọc",
|
||||
"Mark as Finished": "Đánh dấu là đã hoàn thành",
|
||||
"Finished": "Đã hoàn thành",
|
||||
"Unread": "Chưa đọc",
|
||||
"Clear Status": "Xóa trạng thái",
|
||||
"Status": "Trạng thái"
|
||||
}
|
||||
|
||||
@@ -948,5 +948,13 @@
|
||||
"Ollama Model": "Ollama 模型",
|
||||
"AI Gateway Model": "AI Gateway 模型",
|
||||
"Actions": "操作",
|
||||
"Navigation": "导航"
|
||||
"Navigation": "导航",
|
||||
"Set status for {{count}} book(s)_one": "设置 {{count}} 本书的状态",
|
||||
"Set status for {{count}} book(s)_other": "设置 {{count}} 本书的状态",
|
||||
"Mark as Unread": "标记为未读",
|
||||
"Mark as Finished": "标记为已读",
|
||||
"Finished": "已读",
|
||||
"Unread": "未读",
|
||||
"Clear Status": "清除状态",
|
||||
"Status": "状态"
|
||||
}
|
||||
|
||||
@@ -948,5 +948,13 @@
|
||||
"Ollama Model": "Ollama 模型",
|
||||
"AI Gateway Model": "AI Gateway 模型",
|
||||
"Actions": "操作",
|
||||
"Navigation": "導覽"
|
||||
"Navigation": "導覽",
|
||||
"Set status for {{count}} book(s)_one": "設定 {{count}} 本書的狀態",
|
||||
"Set status for {{count}} book(s)_other": "設定 {{count}} 本書的狀態",
|
||||
"Mark as Unread": "標示為未讀",
|
||||
"Mark as Finished": "標示為已讀",
|
||||
"Finished": "已讀",
|
||||
"Unread": "未讀",
|
||||
"Clear Status": "清除狀態",
|
||||
"Status": "狀態"
|
||||
}
|
||||
|
||||
@@ -118,13 +118,16 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
</h4>
|
||||
)}
|
||||
<div
|
||||
className={clsx('flex items-center', book.progress ? 'justify-between' : 'justify-end')}
|
||||
className={clsx(
|
||||
'flex items-center',
|
||||
book.progress || book.readingStatus ? 'justify-between' : 'justify-end',
|
||||
)}
|
||||
style={{
|
||||
height: `${iconSize15}px`,
|
||||
minHeight: `${iconSize15}px`,
|
||||
}}
|
||||
>
|
||||
{book.progress && <ReadingProgress book={book} />}
|
||||
{(book.progress || book.readingStatus) && <ReadingProgress book={book} />}
|
||||
<div className='flex items-center justify-center gap-x-2'>
|
||||
{!appService?.isMobile && (
|
||||
<button
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { Book } from '@/types/book';
|
||||
import { Book, ReadingStatus } from '@/types/book';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
@@ -59,7 +59,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { appService } = useEnv();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { safeAreaInsets } = useThemeStore();
|
||||
|
||||
@@ -74,6 +74,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const [showSelectModeActions, setShowSelectModeActions] = useState(false);
|
||||
const [bookIdsToDelete, setBookIdsToDelete] = useState<string[]>([]);
|
||||
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
|
||||
const [showStatusAlert, setShowStatusAlert] = useState(false);
|
||||
const [showGroupingModal, setShowGroupingModal] = useState(false);
|
||||
const [importBookUrl] = useState(searchParams?.get('url') || '');
|
||||
|
||||
@@ -82,7 +83,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const iconSize15 = useResponsiveSize(15);
|
||||
const autofocusRef = useAutoFocus<HTMLDivElement>();
|
||||
|
||||
const { setCurrentBookshelf, setLibrary } = useLibraryStore();
|
||||
const { setCurrentBookshelf, setLibrary, updateBooks } = useLibraryStore();
|
||||
const { setSelectedBooks, getSelectedBooks, toggleSelectedBook } = useLibraryStore();
|
||||
const { getGroupName } = useLibraryStore();
|
||||
|
||||
@@ -247,6 +248,39 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
setShowGroupingModal(true);
|
||||
};
|
||||
|
||||
const showStatusSelection = () => {
|
||||
setShowSelectModeActions(false);
|
||||
setShowStatusAlert(true);
|
||||
};
|
||||
|
||||
const updateBooksStatus = async (status: ReadingStatus | undefined) => {
|
||||
const selectedIds = getSelectedBooks();
|
||||
const booksToUpdate: Book[] = [];
|
||||
|
||||
for (const id of selectedIds) {
|
||||
const book = filteredBooks.find((b) => b.hash === id);
|
||||
if (book) {
|
||||
booksToUpdate.push({ ...book, readingStatus: status, updatedAt: Date.now() });
|
||||
}
|
||||
}
|
||||
|
||||
if (booksToUpdate.length > 0) {
|
||||
await updateBooks(envConfig, booksToUpdate);
|
||||
}
|
||||
|
||||
setSelectedBooks([]);
|
||||
setShowStatusAlert(false);
|
||||
setShowSelectModeActions(true);
|
||||
};
|
||||
|
||||
const handleUpdateReadingStatus = useCallback(
|
||||
async (book: Book, status: ReadingStatus | undefined) => {
|
||||
const updatedBook = { ...book, readingStatus: status, updatedAt: Date.now() };
|
||||
await updateBooks(envConfig, [updatedBook]);
|
||||
},
|
||||
[envConfig, updateBooks],
|
||||
);
|
||||
|
||||
const handleDeleteBooksIntent = (event: CustomEvent) => {
|
||||
const { ids } = event.detail;
|
||||
setBookIdsToDelete(ids);
|
||||
@@ -318,6 +352,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
handleBookDelete={handleBookDelete}
|
||||
handleSetSelectMode={handleSetSelectMode}
|
||||
handleShowDetailsBook={handleShowDetailsBook}
|
||||
handleUpdateReadingStatus={handleUpdateReadingStatus}
|
||||
transferProgress={
|
||||
'hash' in item ? booksTransferProgress[(item as Book).hash] || null : null
|
||||
}
|
||||
@@ -363,6 +398,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
onOpen={openSelectedBooks}
|
||||
onGroup={groupSelectedBooks}
|
||||
onDetails={openBookDetails}
|
||||
onStatus={showStatusSelection}
|
||||
onDelete={deleteSelectedBooks}
|
||||
onCancel={() => handleSetSelectMode(false)}
|
||||
/>
|
||||
@@ -405,6 +441,90 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showStatusAlert && (
|
||||
<div
|
||||
className={clsx('status-alert fixed bottom-0 left-0 right-0 z-50 flex justify-center')}
|
||||
style={{
|
||||
paddingBottom: `${(safeAreaInsets?.bottom || 0) + 16}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center justify-between',
|
||||
'bg-base-200/95 rounded-2xl p-4 backdrop-blur-sm',
|
||||
'border-base-content/10 border',
|
||||
'shadow-lg',
|
||||
'w-auto max-w-[90vw]',
|
||||
'flex-col gap-4 sm:flex-row',
|
||||
)}
|
||||
>
|
||||
<div className='text-sm font-medium'>
|
||||
{_('Set status for {{count}} book(s)', { count: getSelectedBooks().length })}
|
||||
</div>
|
||||
<div className='flex flex-wrap items-center justify-end gap-2'>
|
||||
<button
|
||||
className={clsx(
|
||||
'flex items-center gap-2 rounded-full px-4 py-2',
|
||||
'bg-base-300 text-base-content',
|
||||
'hover:bg-base-content/10',
|
||||
'border-base-content/10 border',
|
||||
'shadow-sm',
|
||||
'transition-all duration-200 ease-out',
|
||||
'active:scale-[0.97]',
|
||||
)}
|
||||
onClick={() => {
|
||||
setShowStatusAlert(false);
|
||||
setShowSelectModeActions(true);
|
||||
}}
|
||||
>
|
||||
<span className='text-sm font-medium'>{_('Cancel')}</span>
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
'flex items-center gap-2 rounded-full px-4 py-2',
|
||||
'bg-amber-500/15 text-amber-600 dark:text-amber-400',
|
||||
'hover:bg-amber-500/25',
|
||||
'border border-amber-500/20',
|
||||
'shadow-sm',
|
||||
'transition-all duration-200 ease-out',
|
||||
'active:scale-[0.97]',
|
||||
)}
|
||||
onClick={() => updateBooksStatus('unread')}
|
||||
>
|
||||
<span className='text-sm font-medium'>{_('Mark as Unread')}</span>
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
'flex items-center gap-2 rounded-full px-4 py-2',
|
||||
'bg-success/15 text-success',
|
||||
'hover:bg-success/25',
|
||||
'border-success/20 border',
|
||||
'shadow-sm',
|
||||
'transition-all duration-200 ease-out',
|
||||
'active:scale-[0.97]',
|
||||
)}
|
||||
onClick={() => updateBooksStatus('finished')}
|
||||
>
|
||||
<span className='text-sm font-medium'>{_('Mark as Finished')}</span>
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
'flex items-center gap-2 rounded-full px-4 py-2',
|
||||
'bg-base-300 text-base-content',
|
||||
'hover:bg-base-content/10',
|
||||
'border-base-content/10 border',
|
||||
'shadow-sm',
|
||||
'transition-all duration-200 ease-out',
|
||||
'active:scale-[0.97]',
|
||||
)}
|
||||
onClick={() => updateBooksStatus(undefined)}
|
||||
>
|
||||
<span className='text-sm font-medium'>{_('Clear Status')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import { throttle } from '@/utils/throttle';
|
||||
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
|
||||
import { BOOK_UNGROUPED_ID, BOOK_UNGROUPED_NAME } from '@/services/constants';
|
||||
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
|
||||
import { Book, BooksGroup } from '@/types/book';
|
||||
import { Book, BooksGroup, ReadingStatus } from '@/types/book';
|
||||
import { md5Fingerprint } from '@/utils/md5';
|
||||
import BookItem from './BookItem';
|
||||
import GroupItem from './GroupItem';
|
||||
@@ -99,6 +99,7 @@ interface BookshelfItemProps {
|
||||
handleBookDelete: (book: Book, syncBooks?: boolean) => Promise<boolean>;
|
||||
handleSetSelectMode: (selectMode: boolean) => void;
|
||||
handleShowDetailsBook: (book: Book) => void;
|
||||
handleUpdateReadingStatus: (book: Book, status: ReadingStatus | undefined) => void;
|
||||
}
|
||||
|
||||
const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
@@ -115,6 +116,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
handleBookDownload,
|
||||
handleSetSelectMode,
|
||||
handleShowDetailsBook,
|
||||
handleUpdateReadingStatus,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -210,6 +212,24 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
handleGroupBooks();
|
||||
},
|
||||
});
|
||||
const markAsFinishedMenuItem = await MenuItem.new({
|
||||
text: _('Mark as Finished'),
|
||||
action: async () => {
|
||||
handleUpdateReadingStatus(book, 'finished');
|
||||
},
|
||||
});
|
||||
const markAsUnreadMenuItem = await MenuItem.new({
|
||||
text: _('Mark as Unread'),
|
||||
action: async () => {
|
||||
handleUpdateReadingStatus(book, 'unread');
|
||||
},
|
||||
});
|
||||
const clearStatusMenuItem = await MenuItem.new({
|
||||
text: _('Clear Status'),
|
||||
action: async () => {
|
||||
handleUpdateReadingStatus(book, undefined);
|
||||
},
|
||||
});
|
||||
const showBookInFinderMenuItem = await MenuItem.new({
|
||||
text: _(fileRevealLabel),
|
||||
action: async () => {
|
||||
@@ -244,6 +264,15 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
const menu = await Menu.new();
|
||||
menu.append(selectBookMenuItem);
|
||||
menu.append(groupBooksMenuItem);
|
||||
if (book.readingStatus === 'finished') {
|
||||
menu.append(markAsUnreadMenuItem);
|
||||
} else {
|
||||
menu.append(markAsFinishedMenuItem);
|
||||
}
|
||||
// show "Clear Status" option when book has an explicit status set
|
||||
if (book.readingStatus === 'finished' || book.readingStatus === 'unread') {
|
||||
menu.append(clearStatusMenuItem);
|
||||
}
|
||||
menu.append(showBookDetailsMenuItem);
|
||||
menu.append(showBookInFinderMenuItem);
|
||||
if (book.uploadedAt && !book.downloadedAt) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type React from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import type { Book } from '@/types/book';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import StatusBadge from './StatusBadge';
|
||||
|
||||
interface ReadingProgressProps {
|
||||
book: Book;
|
||||
@@ -19,8 +21,25 @@ const getProgressPercentage = (book: Book) => {
|
||||
|
||||
const ReadingProgress: React.FC<ReadingProgressProps> = memo(
|
||||
({ book }) => {
|
||||
const _ = useTranslation();
|
||||
const progressPercentage = useMemo(() => getProgressPercentage(book), [book]);
|
||||
|
||||
if (book.readingStatus === 'finished') {
|
||||
return (
|
||||
<div className='flex justify-start'>
|
||||
<StatusBadge status={book.readingStatus}>{_('Finished')}</StatusBadge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (book.readingStatus === 'unread') {
|
||||
return (
|
||||
<div className='flex justify-start'>
|
||||
<StatusBadge status={book.readingStatus}>{_('Unread')}</StatusBadge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (progressPercentage === null || Number.isNaN(progressPercentage)) {
|
||||
return null;
|
||||
}
|
||||
@@ -38,7 +57,8 @@ const ReadingProgress: React.FC<ReadingProgressProps> = memo(
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.book.hash === nextProps.book.hash &&
|
||||
prevProps.book.updatedAt === nextProps.book.updatedAt
|
||||
prevProps.book.updatedAt === nextProps.book.updatedAt &&
|
||||
prevProps.book.readingStatus === nextProps.book.readingStatus
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import clsx from 'clsx';
|
||||
import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-icons/md';
|
||||
import {
|
||||
MdDelete,
|
||||
MdOpenInNew,
|
||||
MdOutlineCancel,
|
||||
MdInfoOutline,
|
||||
MdCheckCircleOutline,
|
||||
} from 'react-icons/md';
|
||||
import { LuFolderPlus } from 'react-icons/lu';
|
||||
import { useKeyDownActions } from '@/hooks/useKeyDownActions';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
@@ -11,6 +17,7 @@ interface SelectModeActionsProps {
|
||||
onOpen: () => void;
|
||||
onGroup: () => void;
|
||||
onDetails: () => void;
|
||||
onStatus: () => void;
|
||||
onDelete: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
@@ -21,6 +28,7 @@ const SelectModeActions: React.FC<SelectModeActionsProps> = ({
|
||||
onOpen,
|
||||
onGroup,
|
||||
onDetails,
|
||||
onStatus,
|
||||
onDelete,
|
||||
onCancel,
|
||||
}) => {
|
||||
@@ -66,6 +74,16 @@ const SelectModeActions: React.FC<SelectModeActionsProps> = ({
|
||||
<LuFolderPlus />
|
||||
<div>{_('Group')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={onStatus}
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-1',
|
||||
(!hasSelection || !hasValidBooks) && 'btn-disabled opacity-50',
|
||||
)}
|
||||
>
|
||||
<MdCheckCircleOutline />
|
||||
<div>{_('Status')}</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={onDetails}
|
||||
className={clsx(
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import clsx from 'clsx';
|
||||
import type { ReadingStatus } from '@/types/book';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status?: ReadingStatus;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const StatusBadge: React.FC<StatusBadgeProps> = ({ status, children, className }) => {
|
||||
if (status !== 'finished' && status !== 'unread') return null;
|
||||
|
||||
const isFinished = status === 'finished';
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center',
|
||||
'rounded-[1px] px-0.5',
|
||||
'text-[8px] font-bold uppercase leading-none tracking-wider',
|
||||
'h-3.5',
|
||||
// finished: green/emerald
|
||||
isFinished && 'bg-emerald-100 dark:bg-emerald-900/90',
|
||||
isFinished && 'border border-emerald-300/50 dark:border-emerald-700/50',
|
||||
isFinished && 'text-emerald-700 dark:text-emerald-300',
|
||||
// unread: pastel yellow/amber
|
||||
!isFinished && 'bg-amber-100 dark:bg-amber-900/80',
|
||||
!isFinished && 'border border-amber-300/50 dark:border-amber-700/50',
|
||||
!isFinished && 'text-amber-700 dark:text-amber-300',
|
||||
className,
|
||||
)}
|
||||
role='status'
|
||||
>
|
||||
<span className='relative top-[0.5px]'>{children}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusBadge;
|
||||
@@ -291,15 +291,33 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
const pagePressInfo = bookData.isFixedLayout ? section : pageinfo;
|
||||
const progress: [number, number] = [pagePressInfo.current + 1, pagePressInfo.total];
|
||||
|
||||
// Update library book progress
|
||||
// calculate progress percentage
|
||||
const progressPercentage = Math.round((progress[0] / progress[1]) * 100);
|
||||
|
||||
// update library book progress
|
||||
const { library, setLibrary } = useLibraryStore.getState();
|
||||
const bookIndex = library.findIndex((b) => b.hash === id);
|
||||
if (bookIndex !== -1) {
|
||||
const updatedLibrary = [...library];
|
||||
const existingBook = updatedLibrary[bookIndex]!;
|
||||
|
||||
// determine new reading status
|
||||
let newReadingStatus = existingBook.readingStatus;
|
||||
|
||||
// auto-clear 'unread' status when user starts reading (progress changes)
|
||||
if (existingBook.readingStatus === 'unread') {
|
||||
newReadingStatus = undefined;
|
||||
}
|
||||
|
||||
// auto mark as 'finished' when progress reaches 100%
|
||||
if (progressPercentage >= 100 && existingBook.readingStatus !== 'finished') {
|
||||
newReadingStatus = 'finished';
|
||||
}
|
||||
|
||||
updatedLibrary[bookIndex] = {
|
||||
...existingBook,
|
||||
progress,
|
||||
readingStatus: newReadingStatus,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
setLibrary(updatedLibrary);
|
||||
|
||||
@@ -14,6 +14,7 @@ export type BookFormat =
|
||||
| 'TXT'
|
||||
| 'MD';
|
||||
export type BookNoteType = 'bookmark' | 'annotation' | 'excerpt';
|
||||
export type ReadingStatus = 'unread' | 'reading' | 'finished';
|
||||
export type HighlightStyle = 'highlight' | 'underline' | 'squiggly';
|
||||
// Predefined highlight colors, can be extended with custom hex colors
|
||||
export type HighlightColor = 'red' | 'yellow' | 'green' | 'blue' | 'violet' | string;
|
||||
@@ -51,6 +52,7 @@ export interface Book {
|
||||
|
||||
lastUpdated?: number; // deprecated in favor of updatedAt
|
||||
progress?: [number, number]; // Add progress field: [current, total], 1-based page number
|
||||
readingStatus?: ReadingStatus;
|
||||
primaryLanguage?: string;
|
||||
|
||||
metadata?: BookMetadata;
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface DBBook {
|
||||
group_name?: string;
|
||||
tags?: string[];
|
||||
progress?: [number, number];
|
||||
reading_status?: string;
|
||||
|
||||
metadata?: string | null;
|
||||
created_at?: string;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
BookNoteType,
|
||||
HighlightColor,
|
||||
HighlightStyle,
|
||||
ReadingStatus,
|
||||
} from '@/types/book';
|
||||
import { DBBookConfig, DBBook, DBBookNote } from '@/types/records';
|
||||
import { sanitizeString } from './sanitize';
|
||||
@@ -70,6 +71,7 @@ export const transformBookToDB = (book: unknown, userId: string): DBBook => {
|
||||
groupName,
|
||||
tags,
|
||||
progress,
|
||||
readingStatus,
|
||||
metadata,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
@@ -88,6 +90,7 @@ export const transformBookToDB = (book: unknown, userId: string): DBBook => {
|
||||
group_name: sanitizeString(groupName),
|
||||
tags: tags,
|
||||
progress: progress,
|
||||
reading_status: readingStatus,
|
||||
source_title: sanitizeString(sourceTitle),
|
||||
metadata: metadata ? sanitizeString(JSON.stringify(metadata)) : null,
|
||||
created_at: new Date(createdAt ?? Date.now()).toISOString(),
|
||||
@@ -108,6 +111,7 @@ export const transformBookFromDB = (dbBook: DBBook): Book => {
|
||||
group_name,
|
||||
tags,
|
||||
progress,
|
||||
reading_status,
|
||||
source_title,
|
||||
metadata,
|
||||
created_at,
|
||||
@@ -126,6 +130,7 @@ export const transformBookFromDB = (dbBook: DBBook): Book => {
|
||||
groupName: group_name,
|
||||
tags: tags,
|
||||
progress: progress,
|
||||
readingStatus: reading_status as ReadingStatus,
|
||||
sourceTitle: source_title,
|
||||
metadata: metadata ? JSON.parse(metadata) : null,
|
||||
createdAt: new Date(created_at!).getTime(),
|
||||
|
||||
Reference in New Issue
Block a user