diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json index 9f250e30..7d687da6 100644 --- a/apps/readest-app/src-tauri/capabilities/default.json +++ b/apps/readest-app/src-tauri/capabilities/default.json @@ -159,6 +159,16 @@ "name": "start-readest", "cmd": "cmd", "args": ["/C", "start", "", { "validator": "^.*Readest(.*)\\.exe$" }] + }, + { + "name": "chmod-appimage", + "cmd": "chmod", + "args": ["+x", { "validator": "^.*Readest(.*)\\.AppImage$" }] + }, + { + "name": "launch-appimage", + "cmd": "setsid", + "args": [{ "validator": "^.*Readest(.*)\\.AppImage$" }] } ] }, diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs index 50bff75c..8b3f8554 100644 --- a/apps/readest-app/src-tauri/src/lib.rs +++ b/apps/readest-app/src-tauri/src/lib.rs @@ -253,54 +253,47 @@ pub fn run() { allow_dir_in_scopes(app, path); }); - #[cfg(desktop)] - { - app.handle().plugin(tauri_plugin_cli::init())?; - - let app_handle = app.handle().clone(); - app.listen("window-ready", move |_| { - let webview = app_handle.get_webview_window("main").unwrap(); - webview - .eval("window.__READEST_CLI_ACCESS = true;") - .expect("Failed to set cli access config"); - - #[cfg(target_os = "linux")] - { - let is_appimage = std::env::var("APPIMAGE").is_ok() - || std::env::current_exe() - .map(|path| path.to_string_lossy().contains("/tmp/.mount_")) - .unwrap_or(false); - - let script = - format!("window.__READEST_UPDATER_DISABLED = {};", !is_appimage); - webview - .eval(&script) - .expect("Failed to set updater disabled config"); - } - }); - } - #[cfg(any(target_os = "windows", target_os = "linux"))] { use tauri_plugin_deep_link::DeepLinkExt; let _ = app.deep_link().register_all(); } + #[cfg(desktop)] + { + app.handle().plugin(tauri_plugin_cli::init())?; + } + // Check for e-ink device on Android before building the window #[cfg(target_os = "android")] let is_eink = android::is_eink_device(); #[cfg(not(target_os = "android"))] let is_eink = false; - let eink_script = if is_eink { - "window.__READEST_IS_EINK = true;" - } else { - "" - }; + #[cfg(desktop)] + let cli_access = true; + #[cfg(not(desktop))] + let cli_access = false; + + #[cfg(target_os = "linux")] + let is_appimage = std::env::var("APPIMAGE").is_ok() + || std::env::current_exe() + .map(|path| path.to_string_lossy().contains("/tmp/.mount_")) + .unwrap_or(false); + #[cfg(not(target_os = "linux"))] + let is_appimage = false; + + #[cfg(desktop)] + let updater_disabled = std::env::var("READEST_DISABLE_UPDATER").is_ok(); + #[cfg(not(desktop))] + let updater_disabled = false; let init_script = format!( r#" - {eink_script} + if ({is_eink}) window.__READEST_IS_EINK = true; + if ({cli_access}) window.__READEST_CLI_ACCESS = true; + if ({is_appimage}) window.__READEST_IS_APPIMAGE = true; + if ({updater_disabled}) window.__READEST_UPDATER_DISABLED = true; window.addEventListener('DOMContentLoaded', function() {{ document.documentElement.classList.add('edge-to-edge'); const isTauriLocal = window.location.protocol === 'tauri:' || @@ -322,7 +315,10 @@ pub fn run() { }} }}); "#, - eink_script = eink_script + is_eink = is_eink, + cli_access = cli_access, + is_appimage = is_appimage, + updater_disabled = updater_disabled ); let app_handle = app.handle().clone(); diff --git a/apps/readest-app/src/components/UpdaterWindow.tsx b/apps/readest-app/src/components/UpdaterWindow.tsx index 6c6b05b5..d08439b5 100644 --- a/apps/readest-app/src/components/UpdaterWindow.tsx +++ b/apps/readest-app/src/components/UpdaterWindow.tsx @@ -9,6 +9,7 @@ import { relaunch, exit } from '@tauri-apps/plugin-process'; import { fetch as tauriFetch } from '@tauri-apps/plugin-http'; import { Command } from '@tauri-apps/plugin-shell'; import { invoke } from '@tauri-apps/api/core'; +import { desktopDir } from '@tauri-apps/api/path'; import { isTauriAppPlatform } from '@/services/environment'; import { useTranslator } from '@/hooks/useTranslator'; import { useTranslation } from '@/hooks/useTranslation'; @@ -170,6 +171,41 @@ export const UpdaterContent = ({ } as GenericUpdate); } }; + const downloadWithProgress = ( + downloadUrl: string, + filePath: string, + onEvent?: (progress: DownloadEvent) => void, + ): Promise => { + return new Promise(async (resolve, reject) => { + let downloaded = 0; + let total = 0; + await tauriDownload(downloadUrl, filePath, (progress) => { + if (!onEvent) return; + if (!total && progress.total) { + total = progress.total; + onEvent({ + event: 'Started', + data: { contentLength: total }, + }); + } else if (downloaded > 0 && progress.progress === progress.total) { + console.log('File downloaded to', filePath); + onEvent?.({ event: 'Finished' }); + setTimeout(() => { + resolve(); + }, 1000); + } + + onEvent({ + event: 'Progress', + data: { chunkLength: progress.progress - downloaded }, + }); + downloaded = progress.progress; + }).catch((error) => { + console.error('Download failed:', error); + reject(error); + }); + }); + }; const checkWindowsPortableUpdate = async () => { if (!appService) return; const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch; @@ -190,36 +226,7 @@ export const UpdaterContent = ({ date: data.pub_date, body: data.notes, downloadAndInstall: async (onEvent) => { - await new Promise(async (resolve, reject) => { - let downloaded = 0; - let total = 0; - await tauriDownload(downloadUrl, exeFilePath, (progress) => { - if (!onEvent) return; - if (!total && progress.total) { - total = progress.total; - onEvent({ - event: 'Started', - data: { contentLength: total }, - }); - } else if (downloaded > 0 && progress.progress === progress.total) { - console.log('Portable EXE downloaded to', exeFilePath); - onEvent?.({ event: 'Finished' }); - setTimeout(() => { - resolve(); - }, 1000); - } - - onEvent({ - event: 'Progress', - data: { chunkLength: progress.progress - downloaded }, - }); - downloaded = progress.progress; - }).catch((error) => { - console.error('Download failed:', error); - reject(error); - }); - }); - + await downloadWithProgress(downloadUrl, exeFilePath, onEvent); try { console.log('Launching new executable:', exeFilePath); const command = Command.create('start-readest', ['/C', 'start', '', exeFilePath]); @@ -235,10 +242,53 @@ export const UpdaterContent = ({ } as GenericUpdate); } }; + const checkAppImageUpdate = async () => { + if (!appService) return; + const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch; + const response = await fetch(READEST_UPDATER_FILE); + const data = await response.json(); + if (semver.gt(data.version, currentVersion)) { + const OS_ARCH = osArch(); + const platformKey = + OS_ARCH === 'x86_64' ? 'linux-x86_64-appimage' : 'linux-aarch64-appimage'; + const arch = OS_ARCH === 'x86_64' ? 'x86_64' : 'aarch64'; + const downloadUrl = data.platforms[platformKey]?.url as string; + const appImageFileName = `Readest_${data.version}_${arch}.AppImage`; + const appImageFilePath = await join(await desktopDir(), appImageFileName); + setUpdate({ + currentVersion, + version: data.version, + date: data.pub_date, + body: data.notes, + downloadAndInstall: async (onEvent) => { + await downloadWithProgress(downloadUrl, appImageFilePath, onEvent); + try { + // Make the AppImage executable + const chmodCommand = Command.create('chmod-appimage', ['+x', appImageFilePath]); + await chmodCommand.execute(); + console.log('AppImage made executable:', appImageFilePath); + + // Launch the new AppImage + console.log('Launching new AppImage:', appImageFilePath); + const launchCommand = Command.create('launch-appimage', [appImageFilePath]); + await launchCommand.spawn(); + console.log('New AppImage launched, exiting current app...'); + setTimeout(async () => { + await exit(0); + }, 500); + } catch (error) { + console.error('Failed to launch new AppImage:', error); + } + }, + } as GenericUpdate); + } + }; const checkForUpdates = async () => { const OS_TYPE = osType(); if (appService?.isPortableApp && OS_TYPE === 'windows') { checkWindowsPortableUpdate(); + } else if (appService?.isAppImage) { + checkAppImageUpdate(); } else if (['macos', 'windows', 'linux'].includes(OS_TYPE)) { checkDesktopUpdate(); } else if (OS_TYPE === 'android') { diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index 923230b7..110312f6 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -97,6 +97,7 @@ export abstract class BaseAppService implements AppService { isMobileApp = false; isPortableApp = false; isDesktopApp = false; + isAppImage = false; isEink = false; hasTrafficLight = false; hasWindow = false; diff --git a/apps/readest-app/src/services/nativeAppService.ts b/apps/readest-app/src/services/nativeAppService.ts index 9163dc0d..42cc6153 100644 --- a/apps/readest-app/src/services/nativeAppService.ts +++ b/apps/readest-app/src/services/nativeAppService.ts @@ -52,8 +52,9 @@ import { declare global { interface Window { - __READEST_UPDATER_DISABLED?: boolean; __READEST_IS_EINK?: boolean; + __READEST_IS_APPIMAGE?: boolean; + __READEST_UPDATER_DISABLED?: boolean; } } @@ -399,6 +400,7 @@ export class NativeAppService extends BaseAppService { override isLinuxApp = OS_TYPE === 'linux'; override isMobileApp = ['android', 'ios'].includes(OS_TYPE); override isDesktopApp = ['macos', 'windows', 'linux'].includes(OS_TYPE); + override isAppImage = Boolean(window.__READEST_IS_APPIMAGE); override isEink = Boolean(window.__READEST_IS_EINK); override hasTrafficLight = OS_TYPE === 'macos'; override hasWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android'); diff --git a/apps/readest-app/src/types/system.ts b/apps/readest-app/src/types/system.ts index a348b1ca..7bba8526 100644 --- a/apps/readest-app/src/types/system.ts +++ b/apps/readest-app/src/types/system.ts @@ -85,6 +85,7 @@ export interface AppService { isLinuxApp: boolean; isPortableApp: boolean; isDesktopApp: boolean; + isAppImage: boolean; isEink: boolean; canCustomizeRootDir: boolean; canReadExternalDir: boolean;