feat: toggle finished manually (#3091)

This commit is contained in:
Mohammed Efaz
2026-01-28 06:26:57 +01:00
committed by GitHub
parent 09c62d442f
commit 01b4e3530d
38 changed files with 516 additions and 37 deletions
@@ -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;
+19 -1
View File
@@ -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);
+2
View File
@@ -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;
+1
View File
@@ -10,6 +10,7 @@ export interface DBBook {
group_name?: string;
tags?: string[];
progress?: [number, number];
reading_status?: string;
metadata?: string | null;
created_at?: string;
+5
View File
@@ -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(),