forked from akai/readest
refactor: support custom root dir for readest file system (#2092)
This commit is contained in:
@@ -42,6 +42,22 @@ fn allow_file_in_scopes(app: &AppHandle, files: Vec<PathBuf>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn allow_dir_in_scopes(app: &AppHandle, dir: &PathBuf) {
|
||||
let fs_scope = app.fs_scope();
|
||||
let asset_protocol_scope = app.asset_protocol_scope();
|
||||
if let Err(e) = fs_scope.allow_directory(dir, true) {
|
||||
eprintln!("Failed to allow directory in fs_scope: {e}");
|
||||
} else {
|
||||
println!("Allowed directory in fs_scope: {dir:?}");
|
||||
}
|
||||
if let Err(e) = asset_protocol_scope.allow_directory(dir, true) {
|
||||
eprintln!("Failed to allow directory in asset_protocol_scope: {e}");
|
||||
} else {
|
||||
println!("Allowed directory in asset_protocol_scope: {dir:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn get_files_from_argv(argv: Vec<String>) -> Vec<PathBuf> {
|
||||
let mut files = Vec::new();
|
||||
@@ -87,15 +103,6 @@ fn set_window_open_with_files(app: &AppHandle, files: Vec<PathBuf>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn set_rounded_window(app: &AppHandle, rounded: bool) {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let script = format!("window.IS_ROUNDED = {rounded};");
|
||||
if let Err(e) = window.eval(&script) {
|
||||
eprintln!("Failed to set IS_ROUNDED variable: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn start_server(window: Window) -> Result<u16, String> {
|
||||
start(move |url| {
|
||||
@@ -111,6 +118,15 @@ fn get_environment_variable(name: &str) -> String {
|
||||
std::env::var(String::from(name)).unwrap_or(String::from(""))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_executable_dir() -> String {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(|p| p.to_path_buf()))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
#[allow(dead_code)]
|
||||
struct Payload {
|
||||
@@ -128,6 +144,7 @@ pub fn run() {
|
||||
download_file,
|
||||
upload_file,
|
||||
get_environment_variable,
|
||||
get_executable_dir,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::safari_auth::auth_with_safari,
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -193,6 +210,11 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
allow_dir_in_scopes(app.handle(), &PathBuf::from(get_executable_dir()));
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
app.handle().plugin(tauri_plugin_cli::init())?;
|
||||
@@ -204,13 +226,6 @@ pub fn run() {
|
||||
.eval("window.__READEST_CLI_ACCESS = true;")
|
||||
.expect("Failed to set cli access config");
|
||||
|
||||
set_rounded_window(&app_handle, true);
|
||||
#[cfg(target_os = "windows")]
|
||||
if tauri_plugin_os::version()
|
||||
<= tauri_plugin_os::Version::from_string("10.0.19045")
|
||||
{
|
||||
set_rounded_window(&app_handle, false);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let is_appimage = std::env::var("APPIMAGE").is_ok()
|
||||
|
||||
@@ -50,6 +50,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
<div
|
||||
className={clsx(
|
||||
'sectioninfo absolute flex items-center overflow-hidden',
|
||||
'text-neutral-content font-sans text-xs font-light',
|
||||
isVertical ? 'writing-vertical-rl max-h-[85%]' : 'top-0 h-[44px]',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
)}
|
||||
@@ -75,7 +76,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className={clsx(
|
||||
'text-neutral-content text-center font-sans text-xs font-light',
|
||||
'text-center',
|
||||
isVertical ? '' : 'line-clamp-1',
|
||||
!isVertical && hoveredBookKey == bookKey && 'hidden',
|
||||
)}
|
||||
|
||||
@@ -8,8 +8,6 @@ import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCustomFontStore } from '@/store/customFontStore';
|
||||
import { useFileSelector } from '@/hooks/useFileSelector';
|
||||
import { CustomFont, mountCustomFont } from '@/styles/fonts';
|
||||
import { parseFontInfo } from '@/utils/font';
|
||||
import { getFilename } from '@/utils/path';
|
||||
import { saveViewSettings } from '../../utils/viewSettingsHelper';
|
||||
|
||||
interface CustomFontsProps {
|
||||
@@ -48,28 +46,11 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
|
||||
const handleImportFont = () => {
|
||||
selectFiles({ type: 'fonts', multiple: true }).then(async (result) => {
|
||||
if (result.error || result.files.length === 0) return;
|
||||
if (!(await appService!.fs.exists('', 'Fonts'))) {
|
||||
await appService!.fs.createDir('', 'Fonts');
|
||||
}
|
||||
for (const selectedFile of result.files) {
|
||||
let fontPath: string;
|
||||
let fontFile: File;
|
||||
if (selectedFile.path) {
|
||||
const filePath = selectedFile.path;
|
||||
const fileobj = await appService!.fs.openFile(filePath, 'None');
|
||||
fontPath = fileobj.name || getFilename(filePath);
|
||||
await appService!.fs.copyFile(filePath, fontPath, 'Fonts');
|
||||
fontFile = await appService!.fs.openFile(fontPath, 'Fonts');
|
||||
} else if (selectedFile.file) {
|
||||
const file = selectedFile.file;
|
||||
fontPath = getFilename(file.name);
|
||||
await appService!.fs.writeFile(fontPath, 'Fonts', file);
|
||||
fontFile = file;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
const fontInfo = parseFontInfo(await fontFile.arrayBuffer(), fontPath);
|
||||
const customFont = addFont(fontPath, {
|
||||
const fontInfo = await appService?.importFont(selectedFile.path || selectedFile.file);
|
||||
if (!fontInfo) continue;
|
||||
|
||||
const customFont = addFont(fontInfo.path, {
|
||||
name: fontInfo.name,
|
||||
family: fontInfo.family,
|
||||
style: fontInfo.style,
|
||||
@@ -90,7 +71,7 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
|
||||
for (const font of family.fonts) {
|
||||
if (font) {
|
||||
if (removeFont(font.id)) {
|
||||
appService!.fs.removeFile(font.path, 'Fonts');
|
||||
appService?.deleteFont(font);
|
||||
saveCustomFonts(envConfig);
|
||||
if (getAvailableFonts().length === 0) {
|
||||
setIsDeleteMode(false);
|
||||
|
||||
@@ -167,7 +167,7 @@ const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
});
|
||||
getAllFonts().forEach((font) => {
|
||||
if (removeFont(font.id)) {
|
||||
appService!.fs.removeFile(font.path, 'Fonts');
|
||||
appService!.deleteFont(font);
|
||||
}
|
||||
});
|
||||
saveCustomFonts(envConfig);
|
||||
|
||||
@@ -115,8 +115,10 @@ 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 = await appService.fs.getPrefix('Cache');
|
||||
const apkFilePath = `${cachePrefix}/Readest_${data.version}_${arch}.apk`;
|
||||
const apkFilePath = await appService.resolveFilePath(
|
||||
`Readest_${data.version}_${arch}.apk`,
|
||||
'Cache',
|
||||
);
|
||||
setUpdate({
|
||||
currentVersion,
|
||||
version: data.version,
|
||||
|
||||
@@ -157,10 +157,7 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
if (selectedFile.path && appService) {
|
||||
const filePath = selectedFile.path;
|
||||
metadata.coverImageFile = filePath;
|
||||
const tempName = `cover-${Date.now()}.png`;
|
||||
const cachePrefix = await appService.fs.getPrefix('Cache');
|
||||
await appService.fs.copyFile(filePath, tempName, 'Cache');
|
||||
metadata.coverImageUrl = await appService.fs.getURL(`${cachePrefix}/${tempName}`);
|
||||
metadata.coverImageUrl = await appService.getCachedImageUrl(filePath);
|
||||
setNewCoverImageUrl(metadata.coverImageUrl!);
|
||||
} else if (selectedFile.file) {
|
||||
metadata.coverImageBlobUrl = URL.createObjectURL(selectedFile.file);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { AppPlatform, AppService, OsPlatform } from '@/types/system';
|
||||
import { AppPlatform, AppService, OsPlatform, ResolvedPath } from '@/types/system';
|
||||
import { FileSystem, BaseDir, DeleteAction } from '@/types/system';
|
||||
import {
|
||||
Book,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
getPrimaryLanguage,
|
||||
getLibraryBackupFilename,
|
||||
} from '@/utils/book';
|
||||
import { partialMD5 } from '@/utils/md5';
|
||||
import { md5, partialMD5 } from '@/utils/md5';
|
||||
import { getBaseFilename, getFilename } from '@/utils/path';
|
||||
import { BookDoc, DocumentLoader, EXTS } from '@/libs/document';
|
||||
import {
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
DEFAULT_SCREEN_CONFIG,
|
||||
DEFAULT_TRANSLATOR_CONFIG,
|
||||
DEFAULT_FIXED_LAYOUT_VIEW_SETTINGS,
|
||||
SETTINGS_FILENAME,
|
||||
} from './constants';
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
|
||||
import { getOSPlatform, getTargetLang, isCJKEnv, isContentURI, isValidURL } from '@/utils/misc';
|
||||
@@ -52,13 +53,8 @@ import { ClosableFile } from '@/utils/file';
|
||||
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<string>;
|
||||
fp: string;
|
||||
base: BaseDir;
|
||||
};
|
||||
import { CustomFont, CustomFontInfo } from '@/styles/fonts';
|
||||
import { parseFontInfo } from '@/utils/font';
|
||||
|
||||
export abstract class BaseAppService implements AppService {
|
||||
osPlatform: OsPlatform = getOSPlatform();
|
||||
@@ -71,6 +67,7 @@ export abstract class BaseAppService implements AppService {
|
||||
isAndroidApp = false;
|
||||
isIOSApp = false;
|
||||
isMobileApp = false;
|
||||
isPortableApp = false;
|
||||
hasTrafficLight = false;
|
||||
hasWindow = false;
|
||||
hasWindowBar = false;
|
||||
@@ -80,16 +77,49 @@ export abstract class BaseAppService implements AppService {
|
||||
hasHaptics = false;
|
||||
hasUpdater = false;
|
||||
hasOrientationLock = false;
|
||||
canCustomRootDir = false;
|
||||
distChannel = 'readest';
|
||||
|
||||
abstract fs: FileSystem;
|
||||
protected abstract fs: FileSystem;
|
||||
protected abstract resolvePath(fp: string, base: BaseDir): ResolvedPath;
|
||||
|
||||
abstract resolvePath(fp: string, base: BaseDir): ResolvedPath;
|
||||
abstract getCoverImageUrl(book: Book): string;
|
||||
abstract getCoverImageBlobUrl(book: Book): Promise<string>;
|
||||
abstract selectDirectory(): Promise<string>;
|
||||
abstract selectFiles(name: string, extensions: string[]): Promise<string[]>;
|
||||
|
||||
async init() {
|
||||
this.localBooksDir = await this.fs.getPrefix('Books');
|
||||
}
|
||||
|
||||
async openFile(path: string, base: BaseDir): Promise<File> {
|
||||
return await this.fs.openFile(path, base);
|
||||
}
|
||||
|
||||
async resolveFilePath(path: string, base: BaseDir): Promise<string> {
|
||||
const prefix = await this.fs.getPrefix(base);
|
||||
return `${prefix}/${path}`;
|
||||
}
|
||||
|
||||
getCoverImageUrl = (book: Book): string => {
|
||||
return this.fs.getURL(`${this.localBooksDir}/${getCoverFilename(book)}`);
|
||||
};
|
||||
|
||||
getCoverImageBlobUrl = async (book: Book): Promise<string> => {
|
||||
return this.fs.getBlobURL(`${this.localBooksDir}/${getCoverFilename(book)}`, 'None');
|
||||
};
|
||||
|
||||
async getCachedImageUrl(pathOrUrl: string): Promise<string> {
|
||||
const cachedKey = `img_${md5(pathOrUrl)}`;
|
||||
const cachePrefix = await this.fs.getPrefix('Cache');
|
||||
const cachedPath = `${cachePrefix}/${cachedKey}`;
|
||||
if (await this.fs.exists(cachedPath, 'None')) {
|
||||
return this.fs.getURL(cachedPath);
|
||||
} else {
|
||||
const file = await this.fs.openFile(pathOrUrl, 'None');
|
||||
await this.fs.writeFile(cachedKey, 'Cache', await file.arrayBuffer());
|
||||
return this.fs.getURL(cachedPath);
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultViewSettings(): ViewSettings {
|
||||
return {
|
||||
...DEFAULT_BOOK_LAYOUT,
|
||||
@@ -106,15 +136,13 @@ export abstract class BaseAppService implements AppService {
|
||||
|
||||
async loadSettings(): Promise<SystemSettings> {
|
||||
let settings: SystemSettings;
|
||||
const { fp, base } = this.resolvePath('settings.json', 'Settings');
|
||||
|
||||
try {
|
||||
await this.fs.exists(fp, base);
|
||||
const txt = await this.fs.readFile(fp, base, 'text');
|
||||
await this.fs.exists(SETTINGS_FILENAME, 'Settings');
|
||||
const txt = await this.fs.readFile(SETTINGS_FILENAME, 'Settings', 'text');
|
||||
settings = JSON.parse(txt as string);
|
||||
const version = settings.version ?? 0;
|
||||
if (this.isAppDataSandbox || version < SYSTEM_SETTINGS_VERSION) {
|
||||
settings.localBooksDir = await this.fs.getPrefix('Books');
|
||||
settings.version = SYSTEM_SETTINGS_VERSION;
|
||||
}
|
||||
settings = { ...DEFAULT_SYSTEM_SETTINGS, ...settings };
|
||||
@@ -124,9 +152,10 @@ export abstract class BaseAppService implements AppService {
|
||||
...settings.globalViewSettings,
|
||||
};
|
||||
|
||||
settings.localBooksDir = await this.fs.getPrefix('Books');
|
||||
if (!settings.kosync.deviceId) {
|
||||
settings.kosync.deviceId = uuidv4();
|
||||
await this.fs.writeFile(fp, base, JSON.stringify(settings));
|
||||
await this.saveSettings(settings);
|
||||
}
|
||||
} catch {
|
||||
settings = {
|
||||
@@ -140,10 +169,7 @@ export abstract class BaseAppService implements AppService {
|
||||
},
|
||||
globalViewSettings: this.getDefaultViewSettings(),
|
||||
} as SystemSettings;
|
||||
|
||||
await this.fs.createDir('', 'Books', true);
|
||||
await this.fs.createDir('', base, true);
|
||||
await this.fs.writeFile(fp, base, JSON.stringify(settings));
|
||||
await this.saveSettings(settings);
|
||||
}
|
||||
|
||||
this.localBooksDir = settings.localBooksDir;
|
||||
@@ -151,9 +177,34 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
|
||||
async saveSettings(settings: SystemSettings): Promise<void> {
|
||||
const { fp, base } = this.resolvePath('settings.json', 'Settings');
|
||||
await this.fs.createDir('', base, true);
|
||||
await this.fs.writeFile(fp, base, JSON.stringify(settings));
|
||||
await this.fs.writeFile(SETTINGS_FILENAME, 'Settings', JSON.stringify(settings));
|
||||
}
|
||||
|
||||
async importFont(file?: string | File): Promise<CustomFontInfo | null> {
|
||||
let fontPath: string;
|
||||
let fontFile: File;
|
||||
if (typeof file === 'string') {
|
||||
const filePath = file;
|
||||
const fileobj = await this.fs.openFile(filePath, 'None');
|
||||
fontPath = fileobj.name || getFilename(filePath);
|
||||
await this.fs.copyFile(filePath, fontPath, 'Fonts');
|
||||
fontFile = await this.fs.openFile(fontPath, 'Fonts');
|
||||
} else if (file) {
|
||||
fontPath = getFilename(file.name);
|
||||
await this.fs.writeFile(fontPath, 'Fonts', file);
|
||||
fontFile = file;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
path: fontPath,
|
||||
...parseFontInfo(await fontFile.arrayBuffer(), fontPath),
|
||||
};
|
||||
}
|
||||
|
||||
async deleteFont(font: CustomFont): Promise<void> {
|
||||
await this.fs.removeFile(font.path, 'Fonts');
|
||||
}
|
||||
|
||||
async importBook(
|
||||
@@ -445,7 +496,7 @@ export abstract class BaseAppService implements AppService {
|
||||
const cfp = `${CLOUD_BOOKS_SUBDIR}/${getRemoteBookFilename(book)}`;
|
||||
await this.downloadCloudFile(lfp, cfp, handleProgress);
|
||||
const localFullpath = `${this.localBooksDir}/${lfp}`;
|
||||
bookDownloaded = await this.fs.exists(localFullpath, 'Books');
|
||||
bookDownloaded = await this.fs.exists(localFullpath, 'None');
|
||||
completedFiles.count++;
|
||||
}
|
||||
// some books may not have cover image, so we need to check if the book is downloaded
|
||||
@@ -563,10 +614,11 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
|
||||
private async loadJSONFile(
|
||||
filename: string,
|
||||
path: string,
|
||||
base: BaseDir,
|
||||
): Promise<{ success: boolean; data?: unknown; error?: unknown }> {
|
||||
try {
|
||||
const txt = await this.fs.readFile(filename, 'Books', 'text');
|
||||
const txt = await this.fs.readFile(path, base, 'text');
|
||||
if (!txt || typeof txt !== 'string' || txt.trim().length === 0) {
|
||||
return { success: false, error: 'File is empty or invalid' };
|
||||
}
|
||||
@@ -587,16 +639,19 @@ export abstract class BaseAppService implements AppService {
|
||||
const libraryFilename = getLibraryFilename();
|
||||
const backupFilename = getLibraryBackupFilename();
|
||||
|
||||
const mainResult = await this.loadJSONFile(libraryFilename);
|
||||
if (!(await this.fs.exists('', 'Books'))) {
|
||||
await this.fs.createDir('', 'Books', true);
|
||||
}
|
||||
|
||||
const mainResult = await this.loadJSONFile(libraryFilename, 'Books');
|
||||
if (mainResult.success) {
|
||||
books = mainResult.data as Book[];
|
||||
} else {
|
||||
const backupResult = await this.loadJSONFile(backupFilename);
|
||||
const backupResult = await this.loadJSONFile(backupFilename, 'Books');
|
||||
if (backupResult.success) {
|
||||
books = backupResult.data as Book[];
|
||||
console.warn('Loaded library from backup file:', backupFilename);
|
||||
} else {
|
||||
await this.fs.createDir('', 'Books', true);
|
||||
await this.fs.writeFile(libraryFilename, 'Books', '[]');
|
||||
await this.fs.writeFile(backupFilename, 'Books', '[]');
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ import { stubTranslation as _ } from '@/utils/misc';
|
||||
export const LOCAL_BOOKS_SUBDIR = 'Readest/Books';
|
||||
export const CLOUD_BOOKS_SUBDIR = 'Readest/Books';
|
||||
export const LOCAL_FONTS_SUBDIR = 'Readest/Fonts';
|
||||
export const LOCAL_DATA_SUBDIR = 'Readest/Data';
|
||||
|
||||
export const SETTINGS_FILENAME = 'settings.json';
|
||||
|
||||
export const SUPPORTED_BOOK_EXTS = [
|
||||
'epub',
|
||||
|
||||
@@ -34,7 +34,7 @@ const getNativeAppService = async () => {
|
||||
if (!nativeAppService) {
|
||||
const { NativeAppService } = await import('@/services/nativeAppService');
|
||||
nativeAppService = new NativeAppService();
|
||||
await nativeAppService.loadSettings();
|
||||
await nativeAppService.init();
|
||||
}
|
||||
return nativeAppService;
|
||||
};
|
||||
@@ -44,7 +44,7 @@ const getWebAppService = async () => {
|
||||
if (!webAppService) {
|
||||
const { WebAppService } = await import('@/services/webAppService');
|
||||
webAppService = new WebAppService();
|
||||
await webAppService.loadSettings();
|
||||
await webAppService.init();
|
||||
}
|
||||
return webAppService;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
exists,
|
||||
mkdir,
|
||||
open as openFile,
|
||||
readTextFile,
|
||||
readFile,
|
||||
writeTextFile,
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
BaseDirectory,
|
||||
WriteFileOptions,
|
||||
} from '@tauri-apps/plugin-fs';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { invoke, convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog';
|
||||
import {
|
||||
join,
|
||||
@@ -25,76 +24,136 @@ import {
|
||||
} 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 { FileSystem, BaseDir, AppPlatform, ResolvedPath } from '@/types/system';
|
||||
import { getOSPlatform, isContentURI, isValidURL } from '@/utils/misc';
|
||||
import { getCoverFilename } from '@/utils/book';
|
||||
import { getFilename } from '@/utils/path';
|
||||
import { copyURIToPath } from '@/utils/bridge';
|
||||
import { getDirPath, getFilename } from '@/utils/path';
|
||||
import { NativeFile, RemoteFile } from '@/utils/file';
|
||||
import { copyURIToPath } from '@/utils/bridge';
|
||||
|
||||
import { BaseAppService, ResolvedPath } from './appService';
|
||||
import { LOCAL_BOOKS_SUBDIR, LOCAL_FONTS_SUBDIR } from './constants';
|
||||
import { BaseAppService } from './appService';
|
||||
import {
|
||||
LOCAL_BOOKS_SUBDIR,
|
||||
LOCAL_DATA_SUBDIR,
|
||||
LOCAL_FONTS_SUBDIR,
|
||||
SETTINGS_FILENAME,
|
||||
} from './constants';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
IS_ROUNDED?: boolean;
|
||||
__READEST_UPDATER_DISABLED?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const OS_TYPE = osType();
|
||||
|
||||
// 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, basePrefix: appConfigDir, fp: path, base };
|
||||
case 'Data':
|
||||
return { baseDir: BaseDirectory.AppData, basePrefix: appDataDir, fp: path, base };
|
||||
case 'Cache':
|
||||
return { baseDir: BaseDirectory.AppCache, basePrefix: appCacheDir, fp: path, base };
|
||||
case 'Log':
|
||||
return { baseDir: BaseDirectory.AppLog, basePrefix: appLogDir, fp: path, base };
|
||||
case 'Books':
|
||||
return {
|
||||
baseDir: BaseDirectory.AppData,
|
||||
basePrefix: appDataDir,
|
||||
fp: `${LOCAL_BOOKS_SUBDIR}/${path}`,
|
||||
base,
|
||||
};
|
||||
case 'Fonts':
|
||||
return {
|
||||
baseDir: BaseDirectory.AppData,
|
||||
basePrefix: appDataDir,
|
||||
fp: `${LOCAL_FONTS_SUBDIR}/${path}`,
|
||||
base,
|
||||
};
|
||||
case 'None':
|
||||
return {
|
||||
baseDir: 0,
|
||||
basePrefix: async () => '',
|
||||
fp: path,
|
||||
base,
|
||||
};
|
||||
case 'Temp':
|
||||
default:
|
||||
return {
|
||||
baseDir: BaseDirectory.Temp,
|
||||
basePrefix: tempDir,
|
||||
fp: path,
|
||||
base,
|
||||
};
|
||||
}
|
||||
// Helper function to create a path resolver based on custom root directory and portable mode
|
||||
// 0. If no custom root dir and not portable mode, use default Tauri BaseDirectory
|
||||
// 1. If custom root dir is set, use it as base dir (baseDir = 0)
|
||||
// 2. If portable mode is detected (Settings.json in executable dir), use executable dir as base dir (baseDir = 0)
|
||||
// 3. If both custom root dir and portable mode are set, use custom root dir as base dir (baseDir = 0)
|
||||
// Path Resolver Usage:
|
||||
// - appService.resolvePath and use returned baseDir + fp, when baseDir is 0, fp will be absolute path
|
||||
// - fileSystem.getPrefix and use prefix + path
|
||||
const getPathResolver = ({
|
||||
customRootDir,
|
||||
isPortable,
|
||||
execDir,
|
||||
}: {
|
||||
customRootDir?: string;
|
||||
isPortable?: boolean;
|
||||
execDir?: string;
|
||||
} = {}) => {
|
||||
const customBaseDir = customRootDir ? 0 : undefined;
|
||||
const isCustomBaseDir = Boolean(customRootDir);
|
||||
const getCustomBasePrefixSync = isCustomBaseDir
|
||||
? (baseDir: BaseDir) => {
|
||||
return () =>
|
||||
`${customRootDir}/${['Settings', 'Data', 'Books', 'Fonts'].includes(baseDir) ? '' : baseDir}`;
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const getCustomBasePrefix = getCustomBasePrefixSync
|
||||
? (baseDir: BaseDir) => async () => getCustomBasePrefixSync(baseDir)()
|
||||
: undefined;
|
||||
|
||||
return (path: string, base: BaseDir): ResolvedPath => {
|
||||
const customBasePrefixSync = getCustomBasePrefixSync?.(base);
|
||||
const customBasePrefix = getCustomBasePrefix?.(base);
|
||||
switch (base) {
|
||||
case 'Settings':
|
||||
return {
|
||||
baseDir: isPortable ? 0 : BaseDirectory.AppConfig,
|
||||
basePrefix: isPortable && execDir ? async () => execDir : appConfigDir,
|
||||
fp: isPortable && execDir ? `${execDir}/${path}` : path,
|
||||
base,
|
||||
};
|
||||
case 'Cache':
|
||||
return {
|
||||
baseDir: BaseDirectory.AppCache,
|
||||
basePrefix: appCacheDir,
|
||||
fp: path,
|
||||
base,
|
||||
};
|
||||
case 'Log':
|
||||
return {
|
||||
baseDir: isCustomBaseDir ? 0 : BaseDirectory.AppLog,
|
||||
basePrefix: customBasePrefix ?? appLogDir,
|
||||
fp: customBasePrefixSync ? `${customBasePrefixSync()}/${path}` : path,
|
||||
base,
|
||||
};
|
||||
case 'Data':
|
||||
return {
|
||||
baseDir: customBaseDir ?? BaseDirectory.AppData,
|
||||
basePrefix: customBasePrefix ?? appDataDir,
|
||||
fp: customBasePrefixSync
|
||||
? `${customBasePrefixSync()}/${LOCAL_DATA_SUBDIR}/${path}`
|
||||
: `${LOCAL_DATA_SUBDIR}/${path}`,
|
||||
base,
|
||||
};
|
||||
case 'Books':
|
||||
return {
|
||||
baseDir: customBaseDir ?? BaseDirectory.AppData,
|
||||
basePrefix: customBasePrefix || appDataDir,
|
||||
fp: customBasePrefixSync
|
||||
? `${customBasePrefixSync()}/${LOCAL_BOOKS_SUBDIR}/${path}`
|
||||
: `${LOCAL_BOOKS_SUBDIR}/${path}`,
|
||||
base,
|
||||
};
|
||||
case 'Fonts':
|
||||
return {
|
||||
baseDir: customBaseDir ?? BaseDirectory.AppData,
|
||||
basePrefix: customBasePrefix || appDataDir,
|
||||
fp: customBasePrefixSync
|
||||
? `${customBasePrefixSync()}/${LOCAL_FONTS_SUBDIR}/${path}`
|
||||
: `${LOCAL_FONTS_SUBDIR}/${path}`,
|
||||
base,
|
||||
};
|
||||
case 'None':
|
||||
return {
|
||||
baseDir: 0,
|
||||
basePrefix: async () => '',
|
||||
fp: path,
|
||||
base,
|
||||
};
|
||||
case 'Temp':
|
||||
default:
|
||||
return {
|
||||
baseDir: BaseDirectory.Temp,
|
||||
basePrefix: tempDir,
|
||||
fp: path,
|
||||
base,
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const nativeFileSystem: FileSystem = {
|
||||
resolvePath: getPathResolver(),
|
||||
|
||||
async getPrefix(base: BaseDir) {
|
||||
const { basePrefix, fp } = resolvePath('', base);
|
||||
const { basePrefix, fp, baseDir } = this.resolvePath('', base);
|
||||
const basePath = await basePrefix();
|
||||
return fp ? await join(basePath, fp) : basePath;
|
||||
return fp ? (baseDir === 0 ? fp : await join(basePath, fp)) : basePath;
|
||||
},
|
||||
getURL(path: string) {
|
||||
return isValidURL(path) ? path : convertFileSrc(path);
|
||||
@@ -104,7 +163,7 @@ export const nativeFileSystem: FileSystem = {
|
||||
return URL.createObjectURL(new Blob([content]));
|
||||
},
|
||||
async openFile(path: string, base: BaseDir, name?: string) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
const { fp, baseDir } = this.resolvePath(path, base);
|
||||
let fname = name || getFilename(fp);
|
||||
if (isValidURL(path)) {
|
||||
return await new RemoteFile(path, fname).open();
|
||||
@@ -113,7 +172,7 @@ export const nativeFileSystem: FileSystem = {
|
||||
if (path.includes('com.android.externalstorage')) {
|
||||
// If the URI is from shared internal storage (like /storage/emulated/0),
|
||||
// we can access it directly using the path — no need to copy.
|
||||
return await new NativeFile(fp, fname, base ? baseDir : null).open();
|
||||
return await new NativeFile(fp, fname, baseDir ? baseDir : null).open();
|
||||
} 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.
|
||||
@@ -124,7 +183,7 @@ export const nativeFileSystem: FileSystem = {
|
||||
console.error('Failed to open file:', res);
|
||||
throw new Error('Failed to open file');
|
||||
}
|
||||
return await new NativeFile(dst, fname, base ? baseDir : null).open();
|
||||
return await new NativeFile(dst, fname, baseDir ? baseDir : null).open();
|
||||
}
|
||||
} else {
|
||||
const prefix = await this.getPrefix(base);
|
||||
@@ -136,7 +195,7 @@ export const nativeFileSystem: FileSystem = {
|
||||
// RemoteFile is not usable on Android due to unknown issues of range fetch with Android WebView.
|
||||
return await new RemoteFile(this.getURL(absolutePath), fname).open();
|
||||
} else {
|
||||
return await new NativeFile(fp, fname, base ? baseDir : null).open();
|
||||
return await new NativeFile(fp, fname, baseDir ? baseDir : null).open();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -155,63 +214,57 @@ export const nativeFileSystem: FileSystem = {
|
||||
throw new Error('Failed to copy file');
|
||||
}
|
||||
} else {
|
||||
const { fp, baseDir } = resolvePath(dstPath, base);
|
||||
await copyFile(srcPath, fp, base && { toPathBaseDir: baseDir });
|
||||
const { fp, baseDir } = this.resolvePath(dstPath, base);
|
||||
await copyFile(srcPath, fp, baseDir ? { toPathBaseDir: baseDir } : undefined);
|
||||
}
|
||||
},
|
||||
async readFile(path: string, base: BaseDir, mode: 'text' | 'binary') {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
const { fp, baseDir } = this.resolvePath(path, base);
|
||||
|
||||
return mode === 'text'
|
||||
? (readTextFile(fp, base && { baseDir }) as Promise<string>)
|
||||
: ((await readFile(fp, base && { baseDir })).buffer as ArrayBuffer);
|
||||
? (readTextFile(fp, baseDir ? { baseDir } : undefined) as Promise<string>)
|
||||
: ((await readFile(fp, baseDir ? { baseDir } : undefined)).buffer as ArrayBuffer);
|
||||
},
|
||||
async writeFile(path: string, base: BaseDir, content: string | ArrayBuffer | File) {
|
||||
// NOTE: this could be very slow for large files and might block the UI thread
|
||||
// so do not use this for large files
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
const { fp, baseDir } = this.resolvePath(path, base);
|
||||
if (!(await this.exists(getDirPath(path), base))) {
|
||||
await this.createDir(getDirPath(path), base, true);
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
return writeTextFile(fp, content, base && { baseDir });
|
||||
return writeTextFile(fp, content, baseDir ? { baseDir } : undefined);
|
||||
} else if (content instanceof File) {
|
||||
const writeOptions = { write: true, create: true, baseDir } as WriteFileOptions;
|
||||
// TODO: use writeFile directly when @tauri-apps/plugin-fs@2.2.1 is released
|
||||
// return writeFile(fp, content.stream(), base && writeOptions);
|
||||
const file = await openFile(fp, base && writeOptions);
|
||||
const reader = content.stream().getReader();
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
await file.write(value);
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
await file.close();
|
||||
}
|
||||
const writeOptions = {
|
||||
write: true,
|
||||
create: true,
|
||||
baseDir: baseDir ? baseDir : undefined,
|
||||
} as WriteFileOptions;
|
||||
return await writeFile(fp, content.stream(), writeOptions);
|
||||
} else {
|
||||
return writeFile(fp, new Uint8Array(content), base && { baseDir });
|
||||
return await writeFile(fp, new Uint8Array(content), baseDir ? { baseDir } : undefined);
|
||||
}
|
||||
},
|
||||
async removeFile(path: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
const { fp, baseDir } = this.resolvePath(path, base);
|
||||
|
||||
return remove(fp, base && { baseDir });
|
||||
return remove(fp, baseDir ? { baseDir } : undefined);
|
||||
},
|
||||
async createDir(path: string, base: BaseDir, recursive = false) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
const { fp, baseDir } = this.resolvePath(path, base);
|
||||
|
||||
await mkdir(fp, base && { baseDir, recursive });
|
||||
await mkdir(fp, { baseDir: baseDir ? baseDir : undefined, recursive });
|
||||
},
|
||||
async removeDir(path: string, base: BaseDir, recursive = false) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
const { fp, baseDir } = this.resolvePath(path, base);
|
||||
|
||||
await remove(fp, base && { baseDir, recursive });
|
||||
await remove(fp, { baseDir: baseDir ? baseDir : undefined, recursive });
|
||||
},
|
||||
async readDir(path: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
const { fp, baseDir } = this.resolvePath(path, base);
|
||||
|
||||
const list = await readDir(fp, base && { baseDir });
|
||||
const list = await readDir(fp, baseDir ? { baseDir } : undefined);
|
||||
return list.map((entity) => {
|
||||
return {
|
||||
path: entity.name,
|
||||
@@ -220,10 +273,10 @@ export const nativeFileSystem: FileSystem = {
|
||||
});
|
||||
},
|
||||
async exists(path: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
const { fp, baseDir } = this.resolvePath(path, base);
|
||||
|
||||
try {
|
||||
const res = await exists(fp, base && { baseDir });
|
||||
const res = await exists(fp, baseDir ? { baseDir } : undefined);
|
||||
return res;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -255,10 +308,35 @@ export class NativeAppService extends BaseAppService {
|
||||
// orientation lock is not supported on iPad
|
||||
override hasOrientationLock =
|
||||
(OS_TYPE === 'ios' && getOSPlatform() === 'ios') || OS_TYPE === 'android';
|
||||
override canCustomRootDir = OS_TYPE !== 'ios';
|
||||
override distChannel = process.env['NEXT_PUBLIC_DIST_CHANNEL'] || 'readest';
|
||||
|
||||
override async init() {
|
||||
const execDir = await invoke<string>('get_executable_dir');
|
||||
if (
|
||||
process.env['NEXT_PUBLIC_PORTABLE_APP'] ||
|
||||
(await this.fs.exists(`${execDir}/${SETTINGS_FILENAME}`, 'None'))
|
||||
) {
|
||||
this.isPortableApp = true;
|
||||
this.fs.resolvePath = getPathResolver({
|
||||
customRootDir: execDir,
|
||||
isPortable: this.isPortableApp,
|
||||
execDir,
|
||||
});
|
||||
}
|
||||
const settings = await this.loadSettings();
|
||||
if (settings.customRootDir) {
|
||||
this.fs.resolvePath = getPathResolver({
|
||||
customRootDir: settings.customRootDir,
|
||||
isPortable: this.isPortableApp,
|
||||
execDir,
|
||||
});
|
||||
}
|
||||
await super.init();
|
||||
}
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): ResolvedPath {
|
||||
return resolvePath(fp, base);
|
||||
return this.fs.resolvePath(fp, base);
|
||||
}
|
||||
|
||||
async selectDirectory(): Promise<string> {
|
||||
@@ -276,12 +354,4 @@ export class NativeAppService extends BaseAppService {
|
||||
});
|
||||
return Array.isArray(selected) ? selected : selected ? [selected] : [];
|
||||
}
|
||||
|
||||
getCoverImageUrl = (book: Book): string => {
|
||||
return this.fs.getURL(`${this.localBooksDir}/${getCoverFilename(book)}`);
|
||||
};
|
||||
|
||||
getCoverImageBlobUrl = async (book: Book): Promise<string> => {
|
||||
return this.fs.getBlobURL(`${this.localBooksDir}/${getCoverFilename(book)}`, 'None');
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Book } from '@/types/book';
|
||||
import { FileSystem, BaseDir, AppPlatform } from '@/types/system';
|
||||
import { getCoverFilename } from '@/utils/book';
|
||||
import { FileSystem, BaseDir, AppPlatform, ResolvedPath } from '@/types/system';
|
||||
import { getOSPlatform, isValidURL } from '@/utils/misc';
|
||||
import { RemoteFile } from '@/utils/file';
|
||||
import { isPWA } from './environment';
|
||||
import { BaseAppService, ResolvedPath } from './appService';
|
||||
import { LOCAL_BOOKS_SUBDIR, LOCAL_FONTS_SUBDIR } from './constants';
|
||||
import { BaseAppService } from './appService';
|
||||
import { LOCAL_BOOKS_SUBDIR, LOCAL_DATA_SUBDIR, LOCAL_FONTS_SUBDIR } from './constants';
|
||||
|
||||
const basePrefix = async () => '';
|
||||
|
||||
const resolvePath = (path: string, base: BaseDir): ResolvedPath => {
|
||||
switch (base) {
|
||||
case 'Data':
|
||||
return { baseDir: 0, basePrefix, fp: `${LOCAL_DATA_SUBDIR}/${path}`, base };
|
||||
case 'Books':
|
||||
return { baseDir: 0, basePrefix, fp: `${LOCAL_BOOKS_SUBDIR}/${path}`, base };
|
||||
case 'Fonts':
|
||||
@@ -42,6 +42,13 @@ async function openIndexedDB(): Promise<IDBDatabase> {
|
||||
}
|
||||
|
||||
const indexedDBFileSystem: FileSystem = {
|
||||
resolvePath,
|
||||
async getPrefix(base: BaseDir) {
|
||||
const { basePrefix, fp } = this.resolvePath('', base);
|
||||
const basePath = await basePrefix();
|
||||
const prefix = fp ? (basePath ? `${basePath}/${fp}` : fp) : basePath;
|
||||
return prefix.replace(/\/+$/, '');
|
||||
},
|
||||
getURL(path: string) {
|
||||
if (isValidURL(path)) {
|
||||
return path;
|
||||
@@ -66,7 +73,7 @@ const indexedDBFileSystem: FileSystem = {
|
||||
}
|
||||
},
|
||||
async copyFile(srcPath: string, dstPath: string, base: BaseDir) {
|
||||
const { fp } = resolvePath(dstPath, base);
|
||||
const { fp } = this.resolvePath(dstPath, base);
|
||||
const db = await openIndexedDB();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
@@ -88,7 +95,7 @@ const indexedDBFileSystem: FileSystem = {
|
||||
});
|
||||
},
|
||||
async readFile(path: string, base: BaseDir, mode: 'text' | 'binary') {
|
||||
const { fp } = resolvePath(path, base);
|
||||
const { fp } = this.resolvePath(path, base);
|
||||
const db = await openIndexedDB();
|
||||
|
||||
return new Promise<string | ArrayBuffer>((resolve, reject) => {
|
||||
@@ -121,7 +128,7 @@ const indexedDBFileSystem: FileSystem = {
|
||||
});
|
||||
},
|
||||
async writeFile(path: string, base: BaseDir, content: string | ArrayBuffer | File) {
|
||||
const { fp } = resolvePath(path, base);
|
||||
const { fp } = this.resolvePath(path, base);
|
||||
const db = await openIndexedDB();
|
||||
|
||||
if (content instanceof File) {
|
||||
@@ -138,7 +145,7 @@ const indexedDBFileSystem: FileSystem = {
|
||||
});
|
||||
},
|
||||
async removeFile(path: string, base: BaseDir) {
|
||||
const { fp } = resolvePath(path, base);
|
||||
const { fp } = this.resolvePath(path, base);
|
||||
const db = await openIndexedDB();
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
@@ -158,7 +165,7 @@ const indexedDBFileSystem: FileSystem = {
|
||||
// Directories are virtual in IndexedDB; no-op
|
||||
},
|
||||
async readDir(path: string, base: BaseDir) {
|
||||
const { fp } = resolvePath(path, base);
|
||||
const { fp } = this.resolvePath(path, base);
|
||||
const db = await openIndexedDB();
|
||||
|
||||
return new Promise<{ path: string; isDir: boolean }[]>((resolve, reject) => {
|
||||
@@ -179,7 +186,7 @@ const indexedDBFileSystem: FileSystem = {
|
||||
});
|
||||
},
|
||||
async exists(path: string, base: BaseDir) {
|
||||
const { fp } = resolvePath(path, base);
|
||||
const { fp } = this.resolvePath(path, base);
|
||||
const db = await openIndexedDB();
|
||||
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
@@ -191,11 +198,6 @@ const indexedDBFileSystem: FileSystem = {
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
},
|
||||
async getPrefix(base: BaseDir) {
|
||||
const { basePrefix, fp } = resolvePath('', base);
|
||||
const basePath = await basePrefix();
|
||||
return fp ? `${basePath}/${fp}` : basePath;
|
||||
},
|
||||
};
|
||||
|
||||
export class WebAppService extends BaseAppService {
|
||||
@@ -204,8 +206,13 @@ export class WebAppService extends BaseAppService {
|
||||
override appPlatform = 'web' as AppPlatform;
|
||||
override hasSafeAreaInset = isPWA();
|
||||
|
||||
override async init() {
|
||||
await this.loadSettings();
|
||||
await super.init();
|
||||
}
|
||||
|
||||
override resolvePath(fp: string, base: BaseDir): ResolvedPath {
|
||||
return resolvePath(fp, base);
|
||||
return this.fs.resolvePath(fp, base);
|
||||
}
|
||||
|
||||
async selectDirectory(): Promise<string> {
|
||||
@@ -215,12 +222,4 @@ export class WebAppService extends BaseAppService {
|
||||
async selectFiles(): Promise<string[]> {
|
||||
throw new Error('selectFiles is not supported in browser');
|
||||
}
|
||||
|
||||
getCoverImageUrl = (book: Book): string => {
|
||||
return this.fs.getURL(`${LOCAL_BOOKS_SUBDIR}/${getCoverFilename(book)}`);
|
||||
};
|
||||
|
||||
getCoverImageBlobUrl = async (book: Book): Promise<string> => {
|
||||
return this.fs.getBlobURL(`${LOCAL_BOOKS_SUBDIR}/${getCoverFilename(book)}`, 'None');
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ export const useCustomFontStore = create<FontStoreState>((set, get) => ({
|
||||
});
|
||||
|
||||
const appService = await envConfig.getAppService();
|
||||
const fontFile = await appService.fs.openFile(font.path, 'Fonts');
|
||||
const fontFile = await appService.openFile(font.path, 'Fonts');
|
||||
|
||||
const format = getFontFormat(font.path);
|
||||
const mimeType = getMimeType(format);
|
||||
|
||||
@@ -134,6 +134,9 @@ export interface CustomFont {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type CustomFontInfo = Partial<CustomFont> &
|
||||
Required<Pick<CustomFont, 'path' | 'name' | 'family' | 'style' | 'weight' | 'variable'>>;
|
||||
|
||||
export function getFontName(path: string): string {
|
||||
const fileName = getFilename(path);
|
||||
return fileName.replace(/\.(ttf|otf|woff|woff2)$/i, '');
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface KOSyncSettings {
|
||||
export interface SystemSettings {
|
||||
version: number;
|
||||
localBooksDir: string;
|
||||
customRootDir?: string;
|
||||
|
||||
keepLogin: boolean;
|
||||
autoUpload: boolean;
|
||||
|
||||
@@ -2,13 +2,22 @@ import { SystemSettings } from './settings';
|
||||
import { Book, BookConfig, BookContent, ViewSettings } from './book';
|
||||
import { BookMetadata } from '@/libs/document';
|
||||
import { ProgressHandler } from '@/utils/transfer';
|
||||
import { CustomFont, CustomFontInfo } from '@/styles/fonts';
|
||||
|
||||
export type AppPlatform = 'web' | 'tauri';
|
||||
export type OsPlatform = 'android' | 'ios' | 'macos' | 'windows' | 'linux' | 'unknown';
|
||||
export type BaseDir = 'Books' | 'Settings' | 'Data' | 'Fonts' | 'Log' | 'Cache' | 'Temp' | 'None';
|
||||
export type DeleteAction = 'cloud' | 'local' | 'both';
|
||||
|
||||
export type ResolvedPath = {
|
||||
baseDir: number;
|
||||
basePrefix: () => Promise<string>;
|
||||
fp: string;
|
||||
base: BaseDir;
|
||||
};
|
||||
|
||||
export interface FileSystem {
|
||||
resolvePath(path: string, base: BaseDir): ResolvedPath;
|
||||
getURL(path: string): string;
|
||||
getBlobURL(path: string, base: BaseDir): Promise<string>;
|
||||
openFile(path: string, base: BaseDir, filename?: string): Promise<File>;
|
||||
@@ -24,7 +33,6 @@ export interface FileSystem {
|
||||
}
|
||||
|
||||
export interface AppService {
|
||||
fs: FileSystem;
|
||||
osPlatform: OsPlatform;
|
||||
appPlatform: AppPlatform;
|
||||
hasTrafficLight: boolean;
|
||||
@@ -43,13 +51,21 @@ export interface AppService {
|
||||
isIOSApp: boolean;
|
||||
isMacOSApp: boolean;
|
||||
isLinuxApp: boolean;
|
||||
isPortableApp: boolean;
|
||||
distChannel: string;
|
||||
|
||||
init(): Promise<void>;
|
||||
openFile(path: string, base: BaseDir): Promise<File>;
|
||||
resolveFilePath(path: string, base: BaseDir): Promise<string>;
|
||||
getCachedImageUrl(pathOrUrl: string): Promise<string>;
|
||||
selectDirectory(): Promise<string>;
|
||||
selectFiles(name: string, extensions: string[]): Promise<string[]>;
|
||||
|
||||
getDefaultViewSettings(): ViewSettings;
|
||||
loadSettings(): Promise<SystemSettings>;
|
||||
saveSettings(settings: SystemSettings): Promise<void>;
|
||||
importFont(file?: string | File): Promise<CustomFontInfo | null>;
|
||||
deleteFont(font: CustomFont): Promise<void>;
|
||||
importBook(
|
||||
file: string | File,
|
||||
books: Book[],
|
||||
|
||||
@@ -9,8 +9,16 @@ export const getFilename = (fileOrUri: string) => {
|
||||
const lastPart = parts.pop()!;
|
||||
return lastPart.split('?')[0]!;
|
||||
};
|
||||
|
||||
export const getBaseFilename = (filename: string) => {
|
||||
const normalizedPath = filename.replace(/\\/g, '/');
|
||||
const baseName = normalizedPath.split('/').pop()?.split('.').slice(0, -1).join('.') || '';
|
||||
return baseName;
|
||||
};
|
||||
|
||||
export const getDirPath = (filePath: string) => {
|
||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||
const parts = normalizedPath.split('/');
|
||||
parts.pop();
|
||||
return parts.join('/');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user