diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json
index 0cbd894f..86131566 100644
--- a/apps/readest-app/src-tauri/capabilities/default.json
+++ b/apps/readest-app/src-tauri/capabilities/default.json
@@ -43,7 +43,18 @@
"identifier": "fs:allow-cache-read",
"allow": [
{
- "path": "$CACHEDIR/**/*"
+ "path": "$APPCACHE/**/*"
+ },
+ {
+ "path": "/private/var/mobile/Containers/Data/Application/**/*"
+ }
+ ]
+ },
+ {
+ "identifier": "fs:allow-cache-write",
+ "allow": [
+ {
+ "path": "$APPCACHE/**/*"
},
{
"path": "/private/var/mobile/Containers/Data/Application/**/*"
diff --git a/apps/readest-app/src-tauri/tauri.conf.json b/apps/readest-app/src-tauri/tauri.conf.json
index c72cffdc..941d0e0b 100644
--- a/apps/readest-app/src-tauri/tauri.conf.json
+++ b/apps/readest-app/src-tauri/tauri.conf.json
@@ -28,6 +28,7 @@
"allow": [
"$RESOURCE/**",
"$APPDATA/**/*",
+ "$APPCACHE/**/*",
"$TEMP/**/*",
"/private/var/mobile/Containers/Data/Application/**/*"
],
diff --git a/apps/readest-app/src/app/updater/page.tsx b/apps/readest-app/src/app/updater/page.tsx
index 31a868d3..043c96c9 100644
--- a/apps/readest-app/src/app/updater/page.tsx
+++ b/apps/readest-app/src/app/updater/page.tsx
@@ -16,7 +16,7 @@ const UpdaterPage = () => {
}
>
-
+
diff --git a/apps/readest-app/src/components/BookCover.tsx b/apps/readest-app/src/components/BookCover.tsx
index 8a9eee45..1339c4e5 100644
--- a/apps/readest-app/src/components/BookCover.tsx
+++ b/apps/readest-app/src/components/BookCover.tsx
@@ -143,6 +143,7 @@ const BookCover: React.FC
= memo(
return (
prevProps.book.coverImageUrl === nextProps.book.coverImageUrl &&
prevProps.book.metadata?.coverImageUrl === nextProps.book.metadata?.coverImageUrl &&
+ prevProps.book.updatedAt === nextProps.book.updatedAt &&
prevProps.mode === nextProps.mode &&
prevProps.coverFit === nextProps.coverFit &&
prevProps.isPreview === nextProps.isPreview &&
diff --git a/apps/readest-app/src/components/UpdaterWindow.tsx b/apps/readest-app/src/components/UpdaterWindow.tsx
index 636c8e52..9c796967 100644
--- a/apps/readest-app/src/components/UpdaterWindow.tsx
+++ b/apps/readest-app/src/components/UpdaterWindow.tsx
@@ -106,6 +106,7 @@ export const UpdaterContent = ({
}
};
const checkAndroidUpdate = async () => {
+ if (!appService) return;
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
const response = await fetch(READEST_UPDATER_FILE);
const data = await response.json();
@@ -114,7 +115,7 @@ export const UpdaterContent = ({
const platformKey = OS_ARCH === 'aarch64' ? 'android-arm64' : 'android-universal';
const arch = OS_ARCH === 'aarch64' ? 'arm64' : 'universal';
const downloadUrl = data.platforms[platformKey]?.url as string;
- const cachePrefix = appService?.fs.getPrefix('Cache');
+ const cachePrefix = await appService.fs.getPrefix('Cache');
const apkFilePath = `${cachePrefix}/Readest_${data.version}_${arch}.apk`;
setUpdate({
currentVersion,
diff --git a/apps/readest-app/src/components/metadata/BookDetailEdit.tsx b/apps/readest-app/src/components/metadata/BookDetailEdit.tsx
index 2dec4a8e..de1653b4 100644
--- a/apps/readest-app/src/components/metadata/BookDetailEdit.tsx
+++ b/apps/readest-app/src/components/metadata/BookDetailEdit.tsx
@@ -180,8 +180,8 @@ const BookDetailEdit: React.FC = ({
files = (await selectImageFileTauri()) as string[];
if (appService && files.length > 0) {
metadata.coverImageFile = files[0]!;
- const tempName = 'cover-temp.png';
- const cachePrefix = appService.fs.getPrefix('Cache');
+ const tempName = `cover-${Date.now()}.png`;
+ const cachePrefix = await appService.fs.getPrefix('Cache');
await appService.fs.copyFile(files[0]!, tempName, 'Cache');
metadata.coverImageUrl = await appService.fs.getURL(`${cachePrefix}/${tempName}`);
setNewCoverImageUrl(metadata.coverImageUrl!);
@@ -224,7 +224,7 @@ const BookDetailEdit: React.FC = ({
'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',
+ isCoverLocked ? '!text-base-content !bg-base-200' : '!text-gray-500',
)}
title={_('Change cover image')}
>
diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts
index 53daf8ef..654644ca 100644
--- a/apps/readest-app/src/services/appService.ts
+++ b/apps/readest-app/src/services/appService.ts
@@ -46,6 +46,13 @@ import { ProgressHandler } from '@/utils/transfer';
import { TxtToEpubConverter } from '@/utils/txt';
import { BOOK_FILE_NOT_FOUND_ERROR } from './errors';
+export type ResolvedPath = {
+ baseDir: number;
+ basePrefix: () => Promise;
+ fp: string;
+ base: BaseDir;
+};
+
export abstract class BaseAppService implements AppService {
osPlatform: OsPlatform = getOSPlatform();
appPlatform: AppPlatform = 'tauri';
@@ -70,11 +77,9 @@ export abstract class BaseAppService implements AppService {
abstract fs: FileSystem;
- abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string };
+ abstract resolvePath(fp: string, base: BaseDir): ResolvedPath;
abstract getCoverImageUrl(book: Book): string;
abstract getCoverImageBlobUrl(book: Book): Promise;
- abstract getInitBooksDir(): Promise;
- abstract getCacheDir(): Promise;
abstract selectDirectory(): Promise;
abstract selectFiles(name: string, extensions: string[]): Promise;
@@ -102,7 +107,7 @@ export abstract class BaseAppService implements AppService {
settings = JSON.parse(txt as string);
const version = settings.version ?? 0;
if (this.isAppDataSandbox || version < SYSTEM_SETTINGS_VERSION) {
- settings.localBooksDir = await this.getInitBooksDir();
+ settings.localBooksDir = await this.fs.getPrefix('Books');
settings.version = SYSTEM_SETTINGS_VERSION;
}
settings = { ...DEFAULT_SYSTEM_SETTINGS, ...settings };
@@ -115,7 +120,7 @@ export abstract class BaseAppService implements AppService {
settings = {
...DEFAULT_SYSTEM_SETTINGS,
version: SYSTEM_SETTINGS_VERSION,
- localBooksDir: await this.getInitBooksDir(),
+ localBooksDir: await this.fs.getPrefix('Books'),
globalReadSettings: {
...DEFAULT_READSETTINGS,
...(this.isMobile ? DEFAULT_MOBILE_READSETTINGS : {}),
@@ -129,15 +134,6 @@ export abstract class BaseAppService implements AppService {
}
this.localBooksDir = settings.localBooksDir;
- const cacheDir = await this.getCacheDir();
- this.fs.getPrefix = (baseDir: BaseDir) => {
- if (baseDir === 'Books') {
- return this.localBooksDir;
- } else if (baseDir === 'Cache') {
- return cacheDir;
- }
- return null;
- };
return settings;
}
@@ -414,12 +410,15 @@ export abstract class BaseAppService implements AppService {
const lfp = getCoverFilename(book);
const cfp = `${CLOUD_BOOKS_SUBDIR}/${lfp}`;
await this.downloadCloudFile(lfp, cfp, handleProgress);
- completedFiles.count++;
bookCoverDownloaded = true;
}
} catch (error) {
// don't throw error here since some books may not have cover images at all
console.log(`Failed to download cover file for book: '${book.title}'`, error);
+ } finally {
+ if (needDownCover) {
+ completedFiles.count++;
+ }
}
if (needDownBook) {
diff --git a/apps/readest-app/src/services/nativeAppService.ts b/apps/readest-app/src/services/nativeAppService.ts
index 0fa61d44..324a36c2 100644
--- a/apps/readest-app/src/services/nativeAppService.ts
+++ b/apps/readest-app/src/services/nativeAppService.ts
@@ -14,17 +14,25 @@ import {
} from '@tauri-apps/plugin-fs';
import { convertFileSrc } from '@tauri-apps/api/core';
import { open as openDialog } from '@tauri-apps/plugin-dialog';
-import { join, basename, appDataDir, appCacheDir } from '@tauri-apps/api/path';
+import {
+ join,
+ basename,
+ appDataDir,
+ appConfigDir,
+ appCacheDir,
+ appLogDir,
+ tempDir,
+} from '@tauri-apps/api/path';
import { type as osType } from '@tauri-apps/plugin-os';
import { Book } from '@/types/book';
import { FileSystem, BaseDir, AppPlatform } from '@/types/system';
-import { getOSPlatform, isContentURI, isValidURL } from '@/utils/misc';
+import { getOSPlatform, isContentURI, isFileURI, isValidURL } from '@/utils/misc';
import { getCoverFilename, getFilename } from '@/utils/book';
import { copyURIToPath } from '@/utils/bridge';
import { NativeFile, RemoteFile } from '@/utils/file';
-import { BaseAppService } from './appService';
+import { BaseAppService, ResolvedPath } from './appService';
import { LOCAL_BOOKS_SUBDIR } from './constants';
declare global {
@@ -36,38 +44,49 @@ declare global {
const OS_TYPE = osType();
-const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } => {
+// Usage:
+// 1. baseDir + fp
+// 2. basePrefix + fp or the getPrefix util in FileSystem
+const resolvePath = (path: string, base: BaseDir): ResolvedPath => {
switch (base) {
case 'Settings':
- return { baseDir: BaseDirectory.AppConfig, fp, base };
+ return { baseDir: BaseDirectory.AppConfig, basePrefix: appConfigDir, fp: path, base };
case 'Data':
- return { baseDir: BaseDirectory.AppData, fp, base };
+ return { baseDir: BaseDirectory.AppData, basePrefix: appDataDir, fp: path, base };
case 'Cache':
- return { baseDir: BaseDirectory.AppCache, fp, base };
+ return { baseDir: BaseDirectory.AppCache, basePrefix: appCacheDir, fp: path, base };
case 'Log':
- return { baseDir: BaseDirectory.AppLog, fp, base };
+ return { baseDir: BaseDirectory.AppLog, basePrefix: appLogDir, fp: path, base };
case 'Books':
return {
baseDir: BaseDirectory.AppData,
- fp: `${LOCAL_BOOKS_SUBDIR}/${fp}`,
+ basePrefix: appDataDir,
+ fp: `${LOCAL_BOOKS_SUBDIR}/${path}`,
base,
};
case 'None':
return {
baseDir: 0,
- fp,
+ basePrefix: async () => '',
+ fp: path,
base,
};
default:
return {
baseDir: BaseDirectory.Temp,
- fp,
+ basePrefix: tempDir,
+ fp: path,
base,
};
}
};
export const nativeFileSystem: FileSystem = {
+ async getPrefix(base: BaseDir) {
+ const { basePrefix, fp } = resolvePath('', base);
+ const basePath = await basePrefix();
+ return fp ? await join(basePath, fp) : basePath;
+ },
getURL(path: string) {
return isValidURL(path) ? path : convertFileSrc(path);
},
@@ -89,8 +108,8 @@ export const nativeFileSystem: FileSystem = {
} else {
// Otherwise, for content:// URIs (e.g. from MediaStore, Drive, or third-party apps),
// we cannot access the file directly — so we copy it to a temporary cache location.
- const prefix = this.getPrefix('Cache');
- const dst = `${prefix}/${fname}`;
+ const prefix = await this.getPrefix('Cache');
+ const dst = await join(prefix, fname);
const res = await copyURIToPath({ uri: path, dst });
if (!res.success) {
console.error('Failed to open file:', res);
@@ -99,8 +118,11 @@ export const nativeFileSystem: FileSystem = {
return await new NativeFile(dst, fname, base ? baseDir : null).open();
}
} else {
- const prefix = this.getPrefix(base);
- const absolutePath = path.startsWith('/') ? path : prefix ? await join(prefix, path) : null;
+ const prefix = await this.getPrefix(base);
+ if (isFileURI(path)) {
+ path = path.replace(/^file:\/\//, '');
+ }
+ const absolutePath = path.startsWith('/') ? path : await join(prefix, path);
if (absolutePath && OS_TYPE !== 'android') {
// NOTE: RemoteFile currently performs about 2× faster than NativeFile
// due to an unresolved performance issue in Tauri (see tauri-apps/tauri#9190).
@@ -114,13 +136,13 @@ export const nativeFileSystem: FileSystem = {
},
async copyFile(srcPath: string, dstPath: string, base: BaseDir) {
if (isContentURI(srcPath)) {
- const prefix = this.getPrefix(base);
+ const prefix = await this.getPrefix(base);
if (!prefix) {
throw new Error('Invalid base directory');
}
const res = await copyURIToPath({
uri: srcPath,
- dst: `${prefix}/${dstPath}`,
+ dst: await join(prefix, dstPath),
});
if (!res.success) {
console.error('Failed to copy file:', res);
@@ -201,9 +223,6 @@ export const nativeFileSystem: FileSystem = {
return false;
}
},
- getPrefix() {
- return null;
- },
};
export class NativeAppService extends BaseAppService {
@@ -232,18 +251,10 @@ export class NativeAppService extends BaseAppService {
(OS_TYPE === 'ios' && getOSPlatform() === 'ios') || OS_TYPE === 'android';
override distChannel = process.env['NEXT_PUBLIC_DIST_CHANNEL'] || 'readest';
- override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
+ override resolvePath(fp: string, base: BaseDir): ResolvedPath {
return resolvePath(fp, base);
}
- async getInitBooksDir(): Promise {
- return join(await appDataDir(), LOCAL_BOOKS_SUBDIR);
- }
-
- async getCacheDir(): Promise {
- return await appCacheDir();
- }
-
async selectDirectory(): Promise {
const selected = await openDialog({
directory: true,
diff --git a/apps/readest-app/src/services/webAppService.ts b/apps/readest-app/src/services/webAppService.ts
index f7c750f4..812f34c4 100644
--- a/apps/readest-app/src/services/webAppService.ts
+++ b/apps/readest-app/src/services/webAppService.ts
@@ -3,19 +3,20 @@ import { FileSystem, BaseDir, AppPlatform } from '@/types/system';
import { getCoverFilename } from '@/utils/book';
import { getOSPlatform, isValidURL } from '@/utils/misc';
import { RemoteFile } from '@/utils/file';
-
import { isPWA } from './environment';
-import { BaseAppService } from './appService';
+import { BaseAppService, ResolvedPath } from './appService';
import { LOCAL_BOOKS_SUBDIR } from './constants';
-const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } => {
+const basePrefix = async () => '';
+
+const resolvePath = (path: string, base: BaseDir): ResolvedPath => {
switch (base) {
case 'Books':
- return { baseDir: 0, fp: `${LOCAL_BOOKS_SUBDIR}/${fp}`, base };
+ return { baseDir: 0, basePrefix, fp: `${LOCAL_BOOKS_SUBDIR}/${path}`, base };
case 'None':
- return { baseDir: 0, fp, base };
+ return { baseDir: 0, basePrefix, fp: path, base };
default:
- return { baseDir: 0, fp: `${base}/${fp}`, base };
+ return { baseDir: 0, basePrefix, fp: `${base}/${path}`, base };
}
};
@@ -188,8 +189,10 @@ const indexedDBFileSystem: FileSystem = {
request.onerror = () => reject(request.error);
});
},
- getPrefix() {
- return null;
+ async getPrefix(base: BaseDir) {
+ const { basePrefix, fp } = resolvePath('', base);
+ const basePath = await basePrefix();
+ return fp ? `${basePath}/${fp}` : basePath;
},
};
@@ -199,18 +202,10 @@ export class WebAppService extends BaseAppService {
override appPlatform = 'web' as AppPlatform;
override hasSafeAreaInset = isPWA();
- override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
+ override resolvePath(fp: string, base: BaseDir): ResolvedPath {
return resolvePath(fp, base);
}
- async getInitBooksDir(): Promise {
- return LOCAL_BOOKS_SUBDIR;
- }
-
- async getCacheDir(): Promise {
- return 'Cache';
- }
-
async selectDirectory(): Promise {
throw new Error('selectDirectory is not supported in browser');
}
diff --git a/apps/readest-app/src/types/system.ts b/apps/readest-app/src/types/system.ts
index 759ad754..993cac8e 100644
--- a/apps/readest-app/src/types/system.ts
+++ b/apps/readest-app/src/types/system.ts
@@ -19,7 +19,7 @@ export interface FileSystem {
createDir(path: string, base: BaseDir, recursive?: boolean): Promise;
removeDir(path: string, base: BaseDir, recursive?: boolean): Promise;
exists(path: string, base: BaseDir): Promise;
- getPrefix(base: BaseDir): string | null;
+ getPrefix(base: BaseDir): Promise;
}
export interface AppService {