fix: save custom cover images in apps (#1588)

This commit is contained in:
Huang Xin
2025-07-16 01:33:13 +08:00
committed by GitHub
parent e3c411f043
commit 33515f1f09
30 changed files with 129 additions and 69 deletions
@@ -464,5 +464,6 @@
"Keep manual input": "الاحتفاظ بالإدخال اليدوي",
"Google Books": "كتب جوجل",
"Open Library": "المكتبة المفتوحة",
"Fiction, Science, History": "خيال، علوم، تاريخ"
"Fiction, Science, History": "خيال، علوم، تاريخ",
"Select Cover Image": "اختر صورة الغلاف"
}
@@ -448,5 +448,6 @@
"Keep manual input": "Manuelle Eingabe beibehalten",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Belletristik, Wissenschaft, Geschichte"
"Fiction, Science, History": "Belletristik, Wissenschaft, Geschichte",
"Select Cover Image": "Coverbild auswählen"
}
@@ -448,5 +448,6 @@
"Keep manual input": "Διατήρηση χειροκίνητης εισαγωγής",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Μυθοπλασία, Επιστήμη, Ιστορία"
"Fiction, Science, History": "Μυθοπλασία, Επιστήμη, Ιστορία",
"Select Cover Image": "Επιλέξτε εικόνα εξωφύλλου"
}
@@ -452,5 +452,6 @@
"Keep manual input": "Mantener entrada manual",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Ficción, Ciencia, Historia"
"Fiction, Science, History": "Ficción, Ciencia, Historia",
"Select Cover Image": "Seleccionar imagen de portada"
}
@@ -452,5 +452,6 @@
"Keep manual input": "Conserver la saisie manuelle",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Fiction, Science, Histoire"
"Fiction, Science, History": "Fiction, Science, Histoire",
"Select Cover Image": "Sélectionner l'image de couverture"
}
@@ -448,5 +448,6 @@
"Keep manual input": "मैन्युअल इनपुट रखें",
"Google Books": "गूगल बुक्स",
"Open Library": "ओपन लाइब्रेरी",
"Fiction, Science, History": "काल्पनिक, विज्ञान, इतिहास"
"Fiction, Science, History": "काल्पनिक, विज्ञान, इतिहास",
"Select Cover Image": "कवर इमेज चुनें"
}
@@ -444,5 +444,6 @@
"Keep manual input": "Pertahankan input manual",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Fiksi, Sains, Sejarah"
"Fiction, Science, History": "Fiksi, Sains, Sejarah",
"Select Cover Image": "Pilihan Gambar Sampul"
}
@@ -452,5 +452,6 @@
"Keep manual input": "Mantieni input manuale",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Narrativa, Scienza, Storia"
"Fiction, Science, History": "Narrativa, Scienza, Storia",
"Select Cover Image": "Seleziona immagine di copertina"
}
@@ -444,5 +444,6 @@
"Keep manual input": "手動入力を保持",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "小説、科学、歴史"
"Fiction, Science, History": "小説、科学、歴史",
"Select Cover Image": "表紙画像を選択"
}
@@ -444,5 +444,6 @@
"Keep manual input": "수동 입력 유지",
"Google Books": "구글 북스",
"Open Library": "오픈 라이브러리",
"Fiction, Science, History": "소설, 과학, 역사"
"Fiction, Science, History": "소설, 과학, 역사",
"Select Cover Image": "표지 이미지 선택"
}
@@ -448,5 +448,6 @@
"Keep manual input": "Handmatige invoer behouden",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Fictie, Wetenschap, Geschiedenis"
"Fiction, Science, History": "Fictie, Wetenschap, Geschiedenis",
"Select Cover Image": "Selecteer omslagafbeelding"
}
@@ -456,5 +456,6 @@
"Keep manual input": "Zachowaj wprowadzenie ręczne",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Beletrystyka, Nauka, Historia"
"Fiction, Science, History": "Beletrystyka, Nauka, Historia",
"Select Cover Image": "Zaznacz obraz okładki"
}
@@ -452,5 +452,6 @@
"Keep manual input": "Manter entrada manual",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Ficção, Ciência, História"
"Fiction, Science, History": "Ficção, Ciência, História",
"Select Cover Image": "Selecionar imagem da capa"
}
@@ -456,5 +456,6 @@
"Keep manual input": "Сохранить ручной ввод",
"Google Books": "Google Книги",
"Open Library": "Open Library",
"Fiction, Science, History": "Художественная литература, Наука, История"
"Fiction, Science, History": "Художественная литература, Наука, История",
"Select Cover Image": "Выбрать изображение обложки"
}
@@ -444,5 +444,6 @@
"Keep manual input": "เก็บข้อมูลที่ป้อนด้วยตนเอง",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "นิยาย, วิทยาศาสตร์, ประวัติศาสตร์"
"Fiction, Science, History": "นิยาย, วิทยาศาสตร์, ประวัติศาสตร์",
"Select Cover Image": "เลือกภาพปก"
}
@@ -448,5 +448,6 @@
"Keep manual input": "Manuel girişi koru",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Kurgu, Bilim, Tarih"
"Fiction, Science, History": "Kurgu, Bilim, Tarih",
"Select Cover Image": "Kapak Resmi Seç"
}
@@ -456,5 +456,6 @@
"Keep manual input": "Зберегти ручне введення",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Художня література, Наука, Історія"
"Fiction, Science, History": "Художня література, Наука, Історія",
"Select Cover Image": "Оберіть зображення обкладинки"
}
@@ -444,5 +444,6 @@
"Keep manual input": "Giữ nhập liệu thủ công",
"Google Books": "Google Books",
"Open Library": "Open Library",
"Fiction, Science, History": "Tiểu thuyết, Khoa học, Lịch sử"
"Fiction, Science, History": "Tiểu thuyết, Khoa học, Lịch sử",
"Select Cover Image": "Chọn ảnh bìa"
}
@@ -444,5 +444,6 @@
"Keep manual input": "保留手动输入",
"Google Books": "谷歌图书",
"Open Library": "开放图书馆",
"Fiction, Science, History": "小说,科学,历史"
"Fiction, Science, History": "小说,科学,历史",
"Select Cover Image": "选择封面图片"
}
@@ -444,5 +444,6 @@
"Keep manual input": "保留手動輸入",
"Google Books": "Google 圖書",
"Open Library": "開放圖書館",
"Fiction, Science, History": "小說,科學,歷史"
"Fiction, Science, History": "小說,科學,歷史",
"Select Cover Image": "選擇封面圖片"
}
+18 -6
View File
@@ -21,7 +21,7 @@ import { eventDispatcher } from '@/utils/event';
import { ProgressPayload } from '@/utils/transfer';
import { throttle } from '@/utils/throttle';
import { parseOpenWithFiles } from '@/helpers/openWith';
import { isTauriAppPlatform } from '@/services/environment';
import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment';
import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater';
import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants';
import { impactFeedback } from '@tauri-apps/plugin-haptics';
@@ -565,17 +565,29 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
book.title = formatTitle(metadata.title);
book.author = formatAuthors(metadata.author);
book.primaryLanguage = getPrimaryLanguage(metadata.language);
if (metadata.coverImageBlobUrl || metadata.coverImageUrl) {
book.updatedAt = Date.now();
if (metadata.coverImageBlobUrl || metadata.coverImageUrl || metadata.coverImageFile) {
book.coverImageUrl = metadata.coverImageBlobUrl || metadata.coverImageUrl;
try {
appService?.updateCoverImage(
await appService?.updateCoverImage(
book,
(metadata.coverImageBlobUrl || metadata.coverImageUrl) as string,
metadata.coverImageBlobUrl || metadata.coverImageUrl,
metadata.coverImageFile,
);
} catch (error) {
console.warn('Failed to update cover image:', error);
}
}
if (isWebAppPlatform()) {
// Clear HTTP cover image URL if cover is updated with a local file
if (metadata.coverImageBlobUrl) {
metadata.coverImageUrl = undefined;
}
} else {
metadata.coverImageUrl = undefined;
}
metadata.coverImageBlobUrl = undefined;
metadata.coverImageFile = undefined;
await updateBook(envConfig, book);
};
@@ -585,9 +597,9 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
let files;
if (isTauriAppPlatform()) {
files = (await selectFilesTauri()) as [string];
files = (await selectFilesTauri()) as string[];
} else {
files = (await selectFilesWeb()) as [File];
files = (await selectFilesWeb()) as File[];
}
importBooks(files);
};
@@ -37,9 +37,7 @@ const BookCover: React.FC<BookCoverProps> = ({
<div className={clsx('book-cover-container relative flex h-full w-full', className)}>
{coverFit === 'crop' ? (
<Image
src={
book.metadata?.coverImageUrl || book.metadata?.coverImageBlobUrl || book.coverImageUrl!
}
src={book.metadata?.coverImageUrl || book.coverImageUrl!}
alt={book.title}
fill={true}
className={clsx('crop-cover-img object-cover', imageClassName)}
@@ -53,7 +51,7 @@ const BookCover: React.FC<BookCoverProps> = ({
)}
>
<Image
src={book.coverImageUrl!}
src={book.metadata?.coverImageUrl || book.coverImageUrl!}
alt={book.title}
width={0}
height={0}
@@ -4,9 +4,12 @@ import { MdEdit, MdLock, MdLockOpen, MdOutlineSearch } from 'react-icons/md';
import { Book } from '@/types/book';
import { BookMetadata } from '@/libs/document';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { formatAuthors, formatTitle } from '@/utils/book';
import { FormField } from './FormField';
import { IMAGE_ACCEPT_FORMATS, SUPPORTED_IMAGE_EXTS } from '@/services/constants';
import { isTauriAppPlatform } from '@/services/environment';
import BookCover from '@/components/BookCover';
interface BookDetailEditProps {
@@ -43,20 +46,18 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
onSave,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const hasLockedFields = Object.values(lockedFields).some((locked) => locked);
const allFieldsLocked = Object.values(lockedFields).every((locked) => locked);
const isCoverLocked = lockedFields['coverImageUrl'] || false;
const [coverImageUpdated, setCoverImageUpdated] = useState(false);
const [newCoverImageUrl, setNewCoverImageUrl] = useState<string | null>(null);
useEffect(() => {
if (metadata.coverImageUrl) {
if (book.metadata) {
book.metadata.coverImageUrl = metadata.coverImageUrl;
}
setCoverImageUpdated(true);
setNewCoverImageUrl(metadata.coverImageUrl);
}
}, [book.metadata, metadata.coverImageUrl]);
}, [metadata.coverImageUrl]);
const titleAuthorFields = [
{
@@ -147,12 +148,11 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
},
];
const selectImageFile = () => {
if (coverImageUpdated) setCoverImageUpdated(false);
const selectImageFileWeb = () => {
return new Promise((resolve) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.png, .jpg, .jpeg';
fileInput.accept = IMAGE_ACCEPT_FORMATS;
fileInput.multiple = false;
fileInput.click();
@@ -162,27 +162,50 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
});
};
const selectImageFileTauri = async () => {
const exts = appService?.isMobileApp ? [] : SUPPORTED_IMAGE_EXTS;
const files = (await appService?.selectFiles(_('Select Cover Image'), exts)) || [];
if (appService?.isIOSApp) {
return files.filter((file) => {
const fileExt = file.split('.').pop()?.toLowerCase() || 'unknown';
return SUPPORTED_IMAGE_EXTS.includes(fileExt);
});
}
return files;
};
const handleSelectLocalImage = async () => {
const files = (await selectImageFile()) as [File];
if (files && files.length > 0) {
const file = files[0];
metadata.coverImageBlobUrl = URL.createObjectURL(file);
setCoverImageUpdated(true);
let files;
if (isTauriAppPlatform()) {
files = (await selectImageFileTauri()) as string[];
if (files.length > 0) {
metadata.coverImageFile = files[0]!;
metadata.coverImageUrl = appService?.fs.getURL(files[0]!);
setNewCoverImageUrl(metadata.coverImageUrl!);
}
} else {
files = (await selectImageFileWeb()) as File[];
if (files.length > 0) {
metadata.coverImageBlobUrl = URL.createObjectURL(files[0]!);
setNewCoverImageUrl(metadata.coverImageBlobUrl!);
}
}
};
return (
<div className='bg-base-100 relative w-full rounded-lg'>
<div className='mb-6 flex items-start gap-4'>
<div className='flex w-[40%] max-w-36 flex-col gap-2'>
<div className='flex w-[30%] max-w-32 flex-col gap-2'>
<div className='aspect-[28/41] h-full shadow-md'>
<BookCover
mode='list'
book={{
...book,
...(metadata.coverImageUrl || metadata.coverImageBlobUrl
? { coverImageUrl: metadata.coverImageUrl || metadata.coverImageBlobUrl }
: {}),
metadata: {
...metadata,
coverImageUrl: newCoverImageUrl || metadata.coverImageUrl,
},
...(newCoverImageUrl ? { coverImageUrl: newCoverImageUrl } : {}),
}}
/>
</div>
@@ -191,14 +214,15 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
onClick={handleSelectLocalImage}
disabled={isCoverLocked}
className={clsx(
'flex flex-1 items-center justify-center gap-1 rounded px-2 py-1 text-xs',
'flex flex-1 items-center justify-center gap-1 rounded px-2 py-1',
'border border-gray-300 bg-white hover:bg-gray-50',
'disabled:cursor-not-allowed disabled:opacity-50',
'text-sm sm:text-xs',
isCoverLocked && '!text-base-content !bg-base-200',
)}
title={_('Change cover image')}
>
<MdEdit className='h-3 w-3' />
<MdEdit className='h-5 w-5 sm:h-4 sm:w-4' />
<span className='hidden sm:inline'>{_('Replace')}</span>
</button>
@@ -99,7 +99,7 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
const handleSaveMetadata = () => {
if (editedMeta && handleBookMetadataUpdate) {
setBookMeta(editedMeta);
setBookMeta({ ...editedMeta });
handleBookMetadataUpdate(book, editedMeta);
setEditMode(false);
}
@@ -48,15 +48,7 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
<div className='relative w-full rounded-lg'>
<div className='mb-6 me-4 flex h-32 items-start'>
<div className='me-10 aspect-[28/41] h-32 shadow-lg'>
<BookCover
mode='list'
book={{
...book,
...(metadata.coverImageUrl || metadata.coverImageBlobUrl
? { coverImageUrl: metadata.coverImageUrl || metadata.coverImageBlobUrl }
: {}),
}}
/>
<BookCover mode='list' book={book} />
</div>
<div className='title-author flex h-32 flex-col justify-between'>
<div>
@@ -39,7 +39,6 @@ export const useMetadataEdit = (metadata: BookMetadata | null) => {
useEffect(() => {
if (metadata) {
console.log('initial metadata:', metadata);
setEditedMeta({ ...metadata });
}
}, [metadata]);
+2
View File
@@ -75,6 +75,8 @@ export type BookMetadata = {
series?: string;
seriesIndex?: number;
seriesTotal?: number;
coverImageFile?: string;
coverImageUrl?: string;
coverImageBlobUrl?: string;
};
+17 -7
View File
@@ -539,26 +539,36 @@ export abstract class BaseAppService implements AppService {
await this.fs.writeFile(getLibraryFilename(), 'Books', JSON.stringify(libraryBooks));
}
private imageToArrayBuffer(imageUrl: string): Promise<ArrayBuffer> {
private imageToArrayBuffer(imageUrl?: string, imageFile?: string): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
if (imageUrl.startsWith('blob:')) {
if (!imageUrl && !imageFile) {
reject(new Error('No image URL or file provided'));
return;
}
if (this.appPlatform === 'web' && imageUrl && imageUrl.startsWith('blob:')) {
fetch(imageUrl)
.then((response) => response.arrayBuffer())
.then((buffer) => resolve(buffer))
.catch((error) => reject(error));
} else if (this.appPlatform === 'web') {
reject(new Error('Skip non-blob URL'));
} else if (this.appPlatform === 'tauri') {
} else if (this.appPlatform === 'tauri' && imageFile) {
this.fs
.openFile(imageFile, 'None')
.then((file) => file.arrayBuffer())
.then((buffer) => resolve(buffer))
.catch((error) => reject(error));
} else if (this.appPlatform === 'tauri' && imageUrl) {
tauriFetch(imageUrl, { method: 'GET' })
.then((response) => response.arrayBuffer())
.then((buffer) => resolve(buffer))
.catch((error) => reject(error));
} else {
reject(new Error('Unsupported platform or missing image data'));
}
});
}
async updateCoverImage(book: Book, imageUrl: string): Promise<void> {
const arrayBuffer = await this.imageToArrayBuffer(imageUrl);
async updateCoverImage(book: Book, imageUrl?: string, imageFile?: string): Promise<void> {
const arrayBuffer = await this.imageToArrayBuffer(imageUrl, imageFile);
await this.fs.writeFile(getCoverFilename(book), 'Books', arrayBuffer);
}
}
@@ -33,6 +33,9 @@ export const FILE_ACCEPT_FORMATS = SUPPORTED_FILE_EXTS.map((ext) => `.${ext}`).j
export const BOOK_UNGROUPED_NAME = '';
export const BOOK_UNGROUPED_ID = '';
export const SUPPORTED_IMAGE_EXTS = ['png', 'jpg', 'jpeg'];
export const IMAGE_ACCEPT_FORMATS = SUPPORTED_IMAGE_EXTS.map((ext) => `.${ext}`).join(', ');
export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
keepLogin: false,
autoUpload: true,
+1 -1
View File
@@ -71,5 +71,5 @@ export interface AppService {
getCoverImageUrl(book: Book): string;
getCoverImageBlobUrl(book: Book): Promise<string>;
generateCoverImageUrl(book: Book): Promise<string>;
updateCoverImage(book: Book, imageUrl: string): Promise<void>;
updateCoverImage(book: Book, imageUrl?: string, imageFile?: string): Promise<void>;
}