diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5132b441..64dc64e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -204,11 +204,11 @@ jobs: jq --arg url "$universial_apk_url" \ --arg sig "$universial_sig" \ - '.platforms["android-universal"] = {url: $url, signature: $sig}' latest.json > tmp.$$.json && mv tmp.$$.json latest.json + '.platforms["android-universal"] = {signature: $sig, url: $url}' latest.json > tmp.$$.json && mv tmp.$$.json latest.json jq --arg url "$arm64_apk_url" \ --arg sig "$arm64_sig" \ - '.platforms["android-arm64"] = {url: $url, signature: $sig}' latest.json > tmp.$$.json && mv tmp.$$.json latest.json + '.platforms["android-arm64"] = {signature: $sig, url: $url}' latest.json > tmp.$$.json && mv tmp.$$.json latest.json echo "Uploading updated latest.json to GitHub release" gh release upload ${{ needs.get-release.outputs.release_tag }} latest.json --clobber diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index 7b69cd9c..31d9439e 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -1,6 +1,6 @@ { "name": "@readest/readest-app", - "version": "0.9.33", + "version": "0.9.35", "private": true, "scripts": { "dev": "dotenv -e .env.tauri -- next dev", diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 6a78604c..856150c7 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -282,5 +282,6 @@ "Download finished": "اكتمل التنزيل", "DOWNLOAD & INSTALL": "تنزيل وتثبيت", "Changelog:": "سجل التغييرات:", - "Redownload": "إعادة التنزيل" + "Redownload": "إعادة التنزيل", + "Software Update": "تحديث البرنامج" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index bbd571f0..1f9434fc 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -282,5 +282,6 @@ "Download finished": "Download abgeschlossen", "DOWNLOAD & INSTALL": "HERUNTERLADEN & INSTALLIEREN", "Changelog:": "Änderungsprotokoll:", - "Redownload": "Neu herunterladen" + "Redownload": "Neu herunterladen", + "Software Update": "Software-Update" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index f0a67d81..8545395f 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -282,5 +282,6 @@ "Download finished": "Η λήψη ολοκληρώθηκε", "DOWNLOAD & INSTALL": "ΛΗΨΗ & ΕΓΚΑΤΑΣΤΑΣΗ", "Changelog:": "Αλλαγές:", - "Redownload": "Επαναφόρτωση" + "Redownload": "Επαναφόρτωση", + "Software Update": "Ενημέρωση λογισμικού" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index a38b8615..3c64ce81 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -282,5 +282,6 @@ "Download finished": "Descarga finalizada", "DOWNLOAD & INSTALL": "DESCARGAR E INSTALAR", "Changelog:": "Notas de la versión:", - "Redownload": "Volver a descargar" + "Redownload": "Volver a descargar", + "Software Update": "Actualización de software" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 524d6826..41515ad6 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -282,5 +282,6 @@ "Download finished": "Téléchargement terminé", "DOWNLOAD & INSTALL": "TÉLÉCHARGER ET INSTALLER", "Changelog:": "Journal des modifications :", - "Redownload": "Télécharger à nouveau" + "Redownload": "Télécharger à nouveau", + "Software Update": "Mise à jour logicielle" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 13706af3..9ef3709a 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -282,5 +282,6 @@ "Download finished": "डाउनलोड पूरा हुआ", "DOWNLOAD & INSTALL": "डाउनलोड और इंस्टॉल करें", "Changelog:": "चेंजलॉग:", - "Redownload": "री-डाउनलोड करें" + "Redownload": "री-डाउनलोड करें", + "Software Update": "सॉफ़्टवेयर अपडेट" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 4070d6a5..4156defd 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -282,5 +282,6 @@ "Download finished": "Unduhan selesai", "DOWNLOAD & INSTALL": "UNDUH & INSTAL", "Changelog:": "Catatan Perubahan:", - "Redownload": "Unduh Ulang" + "Redownload": "Unduh Ulang", + "Software Update": "Pembaruan Perangkat Lunak" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 5b720a16..e088b140 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -282,5 +282,6 @@ "Download finished": "Download completato", "DOWNLOAD & INSTALL": "SCARICA E INSTALLA", "Changelog:": "Note di rilascio:", - "Redownload": "Scarica di nuovo" + "Redownload": "Scarica di nuovo", + "Software Update": "Aggiornamento software" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index ebaa22b1..d6bea268 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -282,5 +282,6 @@ "Download finished": "ダウンロード完了", "DOWNLOAD & INSTALL": "更新", "Changelog:": "変更ログ:", - "Redownload": "再ダウンロード" + "Redownload": "再ダウンロード", + "Software Update": "ソフトウェアの更新" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 091aaa75..ecce4d0e 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -282,5 +282,6 @@ "Download finished": "다운로드 완료", "DOWNLOAD & INSTALL": "다운로드 및 설치", "Changelog:": "변경 로그:", - "Redownload": "다시 다운로드" + "Redownload": "다시 다운로드", + "Software Update": "소프트웨어 업데이트" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 80129293..556a6cc1 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -282,5 +282,6 @@ "Download finished": "Pobieranie zakończone", "DOWNLOAD & INSTALL": "POBIERZ I ZAINSTALUJ", "Changelog:": "Dziennik zmian:", - "Redownload": "Pobierz ponownie" + "Redownload": "Pobierz ponownie", + "Software Update": "Aktualizacja oprogramowania" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 057e5f69..0ba82c3e 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -282,5 +282,6 @@ "Download finished": "Download concluído", "DOWNLOAD & INSTALL": "BAIXAR E INSTALAR", "Changelog:": "Notas de versão:", - "Redownload": "Baixar novamente" + "Redownload": "Baixar novamente", + "Software Update": "Atualização de Software" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 89cfa003..cef5a3a3 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -282,5 +282,6 @@ "Download finished": "Загрузка завершена", "DOWNLOAD & INSTALL": "СКАЧАТЬ И УСТАНОВИТЬ", "Changelog:": "Изменения:", - "Redownload": "Переустановить" + "Redownload": "Переустановить", + "Software Update": "Обновление программного обеспечения" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index cdf6b4ea..a8976c00 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -282,5 +282,6 @@ "Download finished": "İndirme tamamlandı", "DOWNLOAD & INSTALL": "İNDİR & YÜKLE", "Changelog:": "Değişiklik Günlüğü:", - "Redownload": "Yeniden İndir" + "Redownload": "Yeniden İndir", + "Software Update": "Yazılım Güncellemesi" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 8f5713d3..4dcebac4 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -282,5 +282,6 @@ "Download finished": "Завантаження завершено", "DOWNLOAD & INSTALL": "ЗАВАНТАЖИТИ І ВСТАНОВИТИ", "Changelog:": "Журнал змін:", - "Redownload": "Завантажити знову" + "Redownload": "Завантажити знову", + "Software Update": "Оновлення програмного забезпечення" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index c39d66cb..b8f3b23a 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -282,5 +282,6 @@ "Download finished": "Tải xuống hoàn tất", "DOWNLOAD & INSTALL": "TẢI XUỐNG & CÀI ĐẶT", "Changelog:": "Thay đổi:", - "Redownload": "Tải lại" + "Redownload": "Tải lại", + "Software Update": "Cập nhật phần mềm" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 65bab670..a5d227c8 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -282,5 +282,6 @@ "Download finished": "下载完成", "DOWNLOAD & INSTALL": "下载并安装", "Changelog:": "更新日志:", - "Redownload": "重新下载" + "Redownload": "重新下载", + "Software Update": "软件更新" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 3a6b23b9..982c1fce 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -282,5 +282,6 @@ "Download finished": "下載完成", "DOWNLOAD & INSTALL": "下載並安裝", "Changelog:": "更新日誌:", - "Redownload": "重新下載" + "Redownload": "重新下載", + "Software Update": "軟體更新" } diff --git a/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml index 4037d3d7..49626f08 100644 --- a/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml +++ b/apps/readest-app/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt index b6a06519..62d3d1df 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.util.Log +import androidx.core.content.FileProvider import androidx.browser.customtabs.CustomTabsIntent import app.tauri.annotation.Command import app.tauri.annotation.InvokeArg @@ -24,6 +25,11 @@ class CopyURIRequestArgs { var dst: String? = null } +@InvokeArg +class InstallPackageRequestArgs { + var path: String? = null +} + @TauriPlugin class NativeBridgePlugin(private val activity: Activity): Plugin(activity) { private val implementation = NativeBridge() @@ -74,4 +80,34 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) { } invoke.resolve(ret) } + + @Command + fun install_package(invoke: Invoke) { + val args = invoke.parseArgs(InstallPackageRequestArgs::class.java) + val ret = JSObject() + try { + val file = File(args.path ?: "") + if (file.exists()) { + val intent = Intent(Intent.ACTION_VIEW) + val apkUri = FileProvider.getUriForFile(activity, "${activity.packageName}.fileprovider", file) + intent.setDataAndType(apkUri, "application/vnd.android.package-archive") + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK) + val packageManager = activity.packageManager + val resolveInfos = packageManager.queryIntentActivities(intent, 0) + for (resolveInfo in resolveInfos) { + val packageName = resolveInfo.activityInfo.packageName + activity.grantUriPermission(packageName, apkUri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + activity.startActivity(intent) + ret.put("success", true) + } else { + ret.put("success", false) + ret.put("error", "File does not exist") + } + } catch (e: Exception) { + ret.put("success", false) + ret.put("error", e.message) + } + invoke.resolve(ret) + } } diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs index e97e777e..da470368 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs @@ -3,6 +3,7 @@ const COMMANDS: &[&str] = &[ "auth_with_custom_tab", "copy_uri_to_path", "use_background_audio", + "install_package", ]; fn main() { diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/install_package.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/install_package.toml new file mode 100644 index 00000000..9071b84d --- /dev/null +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/install_package.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-install-package" +description = "Enables the install_package command without any pre-configured scope." +commands.allow = ["install_package"] + +[[permission]] +identifier = "deny-install-package" +description = "Denies the install_package command without any pre-configured scope." +commands.deny = ["install_package"] diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md index 9fc50314..78689406 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md @@ -8,6 +8,7 @@ Default permissions for the plugin - `allow-auth-with-custom-tab` - `allow-copy-uri-to-path` - `allow-use-background-audio` +- `allow-install-package` ## Permission Table @@ -99,6 +100,32 @@ Denies the copy_uri_to_path command without any pre-configured scope. +`native-bridge:allow-install-package` + + + + +Enables the install_package command without any pre-configured scope. + + + + + + + +`native-bridge:deny-install-package` + + + + +Denies the install_package command without any pre-configured scope. + + + + + + + `native-bridge:allow-use-background-audio` diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml index d9ea6498..f26dd027 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml @@ -1,3 +1,9 @@ [default] description = "Default permissions for the plugin" -permissions = ["allow-auth-with-safari", "allow-auth-with-custom-tab", "allow-copy-uri-to-path", "allow-use-background-audio"] +permissions = [ + "allow-auth-with-safari", + "allow-auth-with-custom-tab", + "allow-copy-uri-to-path", + "allow-use-background-audio", + "allow-install-package", +] diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json index b8204693..7eb0bcb7 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json @@ -330,6 +330,18 @@ "const": "deny-copy-uri-to-path", "markdownDescription": "Denies the copy_uri_to_path command without any pre-configured scope." }, + { + "description": "Enables the install_package command without any pre-configured scope.", + "type": "string", + "const": "allow-install-package", + "markdownDescription": "Enables the install_package command without any pre-configured scope." + }, + { + "description": "Denies the install_package command without any pre-configured scope.", + "type": "string", + "const": "deny-install-package", + "markdownDescription": "Denies the install_package command without any pre-configured scope." + }, { "description": "Enables the use_background_audio command without any pre-configured scope.", "type": "string", @@ -343,10 +355,10 @@ "markdownDescription": "Denies the use_background_audio command without any pre-configured scope." }, { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`", + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`", "type": "string", "const": "default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`" + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`" } ] } diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs index 6fa250f2..4f21bafc 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs @@ -35,3 +35,11 @@ pub(crate) async fn use_background_audio( ) -> Result<()> { app.native_bridge().use_background_audio(payload) } + +#[command] +pub(crate) async fn install_package( + app: AppHandle, + payload: InstallPackageRequest, +) -> Result { + app.native_bridge().install_package(payload) +} diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs index 3c112603..c2836023 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs @@ -29,4 +29,11 @@ impl NativeBridge { pub fn use_background_audio(&self, _payload: UseBackgroundAudioRequest) -> crate::Result<()> { Err(crate::Error::UnsupportedPlatformError) } + + pub fn install_package( + &self, + _payload: InstallPackageRequest, + ) -> crate::Result { + Err(crate::Error::UnsupportedPlatformError) + } } diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs index 8a364c26..9bbb6b45 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs @@ -41,6 +41,7 @@ pub fn init() -> TauriPlugin { commands::auth_with_custom_tab, commands::copy_uri_to_path, commands::use_background_audio, + commands::install_package, ]) .setup(|app, api| { #[cfg(mobile)] diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs index 18f077b9..9ded15c6 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs @@ -55,3 +55,14 @@ impl NativeBridge { .map_err(Into::into) } } + +impl NativeBridge { + pub fn install_package( + &self, + payload: InstallPackageRequest, + ) -> crate::Result { + self.0 + .run_mobile_plugin("install_package", payload) + .map_err(Into::into) + } +} diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs index 7f32c3e7..fe539884 100644 --- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs +++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs @@ -31,3 +31,16 @@ pub struct CopyURIResponse { pub struct UseBackgroundAudioRequest { pub enabled: bool, } + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InstallPackageRequest { + pub path: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InstallPackageResponse { + pub success: bool, + pub error: Option, +} diff --git a/apps/readest-app/src-tauri/src/lib.rs b/apps/readest-app/src-tauri/src/lib.rs index 9befd136..62a1da9b 100644 --- a/apps/readest-app/src-tauri/src/lib.rs +++ b/apps/readest-app/src-tauri/src/lib.rs @@ -201,13 +201,17 @@ pub fn run() { let app_handle = app.handle().clone(); app.listen("window-ready", move |_| { - app_handle.get_webview_window("main").unwrap() - .eval("window.__READEST_CLI_ACCESS = true; window.__READEST_UPDATER_ACCESS = true;") + app_handle + .get_webview_window("main") + .unwrap() + .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") { + if tauri_plugin_os::version() + <= tauri_plugin_os::Version::from_string("10.0.19045") + { set_rounded_window(&app_handle, false); } }); @@ -228,9 +232,7 @@ pub fn run() { let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()); #[cfg(desktop)] - let win_builder = win_builder - .inner_size(800.0, 600.0) - .resizable(true); + let win_builder = win_builder.inner_size(800.0, 600.0).resizable(true); #[cfg(target_os = "macos")] let win_builder = win_builder @@ -247,7 +249,9 @@ pub fn run() { .title("Readest"); if cfg!(target_os = "windows") { - if tauri_plugin_os::version() <= tauri_plugin_os::Version::from_string("10.0.19045") { + if tauri_plugin_os::version() + <= tauri_plugin_os::Version::from_string("10.0.19045") + { win_builder = win_builder.shadow(false); } else { win_builder = win_builder.shadow(true); diff --git a/apps/readest-app/src/app/library/components/SettingsMenu.tsx b/apps/readest-app/src/app/library/components/SettingsMenu.tsx index a973cd43..6b5af692 100644 --- a/apps/readest-app/src/app/library/components/SettingsMenu.tsx +++ b/apps/readest-app/src/app/library/components/SettingsMenu.tsx @@ -6,7 +6,7 @@ import { PiUserCircleCheck } from 'react-icons/pi'; import { MdCheck } from 'react-icons/md'; import { setAboutDialogVisible } from '@/components/AboutWindow'; -import { hasUpdater, isTauriAppPlatform, isWebAppPlatform } from '@/services/environment'; +import { isTauriAppPlatform, isWebAppPlatform } from '@/services/environment'; import { DOWNLOAD_READEST_URL } from '@/services/constants'; import { useAuth } from '@/context/AuthContext'; import { useEnv } from '@/context/EnvContext'; @@ -177,7 +177,7 @@ const SettingsMenu: React.FC = ({ setIsDropdownOpen }) => { onClick={toggleAutoImportBooksOnOpen} /> )} - {hasUpdater() && ( + {appService?.hasUpdater && ( : undefined} diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index 5c5509f8..d6df6826 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -13,7 +13,7 @@ import { eventDispatcher } from '@/utils/event'; import { ProgressPayload } from '@/utils/transfer'; import { throttle } from '@/utils/throttle'; import { parseOpenWithFiles } from '@/helpers/cli'; -import { isTauriAppPlatform, hasUpdater } from '@/services/environment'; +import { isTauriAppPlatform } from '@/services/environment'; import { checkForAppUpdates } from '@/helpers/updater'; import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants'; import { impactFeedback } from '@tauri-apps/plugin-haptics'; @@ -34,6 +34,7 @@ import { useOpenWithBooks } from '@/hooks/useOpenWithBooks'; import { tauriHandleSetAlwaysOnTop, tauriQuitApp } from '@/utils/window'; import { AboutWindow } from '@/components/AboutWindow'; +import { UpdaterWindow } from '@/components/UpdaterWindow'; import { Toast } from '@/components/Toast'; import Spinner from '@/components/Spinner'; import LibraryHeader from './components/LibraryHeader'; @@ -100,7 +101,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP useEffect(() => { const doCheckAppUpdates = async () => { - if (hasUpdater() && settings.autoCheckUpdates) { + if (appService?.hasUpdater && settings.autoCheckUpdates) { await checkForAppUpdates(_); } }; @@ -577,6 +578,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP /> )} + ); diff --git a/apps/readest-app/src/app/reader/components/Reader.tsx b/apps/readest-app/src/app/reader/components/Reader.tsx index 6318a5ca..2b980d74 100644 --- a/apps/readest-app/src/app/reader/components/Reader.tsx +++ b/apps/readest-app/src/app/reader/components/Reader.tsx @@ -8,12 +8,13 @@ import { useEnv } from '@/context/EnvContext'; import { useTheme } from '@/hooks/useTheme'; import { useThemeStore } from '@/store/themeStore'; import { useLibraryStore } from '@/store/libraryStore'; +import { useSidebarStore } from '@/store/sidebarStore'; import { useSettingsStore } from '@/store/settingsStore'; import { useScreenWakeLock } from '@/hooks/useScreenWakeLock'; import { AboutWindow } from '@/components/AboutWindow'; +import { UpdaterWindow } from '@/components/UpdaterWindow'; import { Toast } from '@/components/Toast'; import ReaderContent from './ReaderContent'; -import { useSidebarStore } from '@/store/sidebarStore'; const Reader: React.FC<{ ids?: string }> = ({ ids }) => { const { envConfig, appService } = useEnv(); @@ -53,6 +54,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => { + diff --git a/apps/readest-app/src/app/reader/components/settings/ThemeEditor.tsx b/apps/readest-app/src/app/reader/components/settings/ThemeEditor.tsx index f1cf3e52..51a7101f 100644 --- a/apps/readest-app/src/app/reader/components/settings/ThemeEditor.tsx +++ b/apps/readest-app/src/app/reader/components/settings/ThemeEditor.tsx @@ -42,13 +42,13 @@ const ThemeEditor: React.FC = ({ customTheme, onSave, onDelete
-

+

{_( "All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare", )} diff --git a/apps/readest-app/src/app/reader/page.tsx b/apps/readest-app/src/app/reader/page.tsx index 8776cd94..77af032c 100644 --- a/apps/readest-app/src/app/reader/page.tsx +++ b/apps/readest-app/src/app/reader/page.tsx @@ -1,22 +1,23 @@ 'use client'; import { useEffect } from 'react'; -import { hasUpdater } from '@/services/environment'; -import { checkForAppUpdates } from '@/helpers/updater'; +import { useEnv } from '@/context/EnvContext'; import { useTranslation } from '@/hooks/useTranslation'; import { useOpenWithBooks } from '@/hooks/useOpenWithBooks'; import { useSettingsStore } from '@/store/settingsStore'; +import { checkForAppUpdates } from '@/helpers/updater'; import Reader from './components/Reader'; export default function Page() { const _ = useTranslation(); + const { appService } = useEnv(); const { settings } = useSettingsStore(); useOpenWithBooks(); useEffect(() => { const doCheckAppUpdates = async () => { - if (hasUpdater() && settings.autoCheckUpdates) { + if (appService?.hasUpdater && settings.autoCheckUpdates) { await checkForAppUpdates(_); } }; diff --git a/apps/readest-app/src/app/updater/page.tsx b/apps/readest-app/src/app/updater/page.tsx index 5a661ecd..c36914b2 100644 --- a/apps/readest-app/src/app/updater/page.tsx +++ b/apps/readest-app/src/app/updater/page.tsx @@ -1,238 +1,8 @@ 'use client'; -import clsx from 'clsx'; -import semver from 'semver'; -import Image from 'next/image'; -import { Suspense, useEffect, useState } from 'react'; -import { check, Update } from '@tauri-apps/plugin-updater'; -import { relaunch } from '@tauri-apps/plugin-process'; -import { fetch } from '@tauri-apps/plugin-http'; -import { useTranslation } from '@/hooks/useTranslation'; -import { useSearchParams } from 'next/navigation'; -import { useTheme } from '@/hooks/useTheme'; -import packageJson from '../../../package.json'; +import { Suspense } from 'react'; import Spinner from '@/components/Spinner'; - -const CHANGELOG_URL = - 'https://github.com/readest/readest/releases/latest/download/release-notes.json'; - -interface ReleaseNotes { - releases: Record< - string, - { - date: string; - notes: string[]; - } - >; -} - -interface Changelog { - version: string; - date: string; - notes: string[]; -} - -const UpdaterDialog = () => { - const _ = useTranslation(); - const searchParams = useSearchParams(); - const currentVersion = packageJson.version; - const version = searchParams?.get('version') || ''; - const [update, setUpdate] = useState(null); - const [isMounted, setIsMounted] = useState(false); - const [newVersion, setNewVersion] = useState(version); - const [changelogs, setChangelogs] = useState([]); - const [progress, setProgress] = useState(null); - const [contentLength, setContentLength] = useState(null); - const [downloaded, setDownloaded] = useState(null); - - useEffect(() => { - const fetchChangelogs = async ( - fromVersion: string, - toVersion: string, - ): Promise => { - try { - const res = await fetch(CHANGELOG_URL); - const data: ReleaseNotes = await res.json(); - const releases = data.releases; - - const entries = Object.entries(releases) - .filter(([ver]) => semver.gt(ver, fromVersion) && semver.lte(ver, toVersion)) - .sort(([a], [b]) => semver.rcompare(a, b)) - .map(([version, info]) => ({ - version, - date: new Date(info.date).toDateString(), - notes: info.notes, - })); - - return entries; - } catch (error) { - console.error('Failed to fetch changelog:', error); - return []; - } - }; - const parseNumberedList = (input: string): string[] => { - return input - .split(/(?:^|\s)\d+\.\s/) - .map((item) => item.trim()) - .filter((item) => item.length > 0); - }; - const checkForUpdates = async () => { - const update = await check(); - setUpdate(update); - if (update) { - setNewVersion(update.version); - const changelogs = await fetchChangelogs(currentVersion, update.version); - if (changelogs.length > 0) { - setChangelogs(changelogs); - } else { - setChangelogs([ - { - version: update.version, - date: new Date(update.date!).toDateString(), - notes: parseNumberedList(update.body ?? ''), - }, - ]); - } - } - }; - checkForUpdates(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useTheme(); - - useEffect(() => { - setIsMounted(true); - }, []); - - const handleDownloadInstall = async () => { - if (!update) { - return; - } - console.log(`found update ${update.version} from ${update.date} with notes ${update.body}`); - let downloaded = 0; - let contentLength = 0; - let lastLogged = 0; - setProgress(0); - await update.downloadAndInstall((event) => { - switch (event.event) { - case 'Started': - contentLength = event.data.contentLength!; - setContentLength(contentLength); - console.log(`started downloading ${event.data.contentLength} bytes`); - break; - case 'Progress': - downloaded += event.data.chunkLength; - setDownloaded(downloaded); - const percent = Math.floor((downloaded / contentLength) * 100); - setProgress(percent); - if (downloaded - lastLogged >= 1 * 1024 * 1024) { - console.log(`downloaded ${downloaded} bytes from ${contentLength}`); - lastLogged = downloaded; - } - break; - case 'Finished': - console.log('download finished'); - setProgress(100); - break; - } - }); - console.log('update installed'); - await relaunch(); - }; - - if (!isMounted) { - return null; - } - - return ( -

-
-
-
-
-
- Logo -
-
- -
-

{_('A new version of Readest is Available!')}

-

- {_('Readest {{newVersion}} is available (installed version {{currentVersion}}).', { - newVersion, - currentVersion, - })} -

-

{_('Download and install now?')}

- -
- {progress !== null && ( -
- -

- {progress < 100 - ? _('Downloading {{downloaded}} of {{contentLength}}', { - downloaded: downloaded - ? `${Math.floor(downloaded / 1024 / 1024)} MB` - : '0 MB', - contentLength: contentLength - ? `${Math.floor(contentLength / 1024 / 1024)} MB` - : '0 MB', - }) - : _('Download finished')} -

-
- )} - -
- -
-
-
-
-
-
-

{_('Changelog:')}

-
- {changelogs.length > 0 ? ( - changelogs.map((entry: Changelog) => ( -
-

- {entry.version} ({entry.date}) -

-
    - {entry.notes.map((note: string, i: number) => ( -
  • {note}
  • - ))} -
-
- )) - ) : ( -
-
-
-
-
- )} -
-
-
-
- ); -}; +import { UpdaterContent } from '@/components/UpdaterWindow'; const UpdaterPage = () => { return ( @@ -243,7 +13,9 @@ const UpdaterPage = () => {
} > - +
+ +
); }; diff --git a/apps/readest-app/src/components/AboutWindow.tsx b/apps/readest-app/src/components/AboutWindow.tsx index 2e2927c3..b5a54b79 100644 --- a/apps/readest-app/src/components/AboutWindow.tsx +++ b/apps/readest-app/src/components/AboutWindow.tsx @@ -3,7 +3,6 @@ import Image from 'next/image'; import packageJson from '../../package.json'; import { useEnv } from '@/context/EnvContext'; import { useTranslation } from '@/hooks/useTranslation'; -import { hasUpdater } from '@/services/environment'; import { checkForAppUpdates } from '@/helpers/updater'; import { parseWebViewVersion } from '@/utils/ua'; import Dialog from './Dialog'; @@ -30,82 +29,77 @@ export const AboutWindow = () => { const handleCheckUpdate = async () => { const update = await checkForAppUpdates(_, false); - if (!update) { + if (update) { + setAboutDialogVisible(false); + } else { setIsUpdated(true); } }; return ( - <> - setAboutDialogVisible(false)} - boxClassName='sm:!w-96 sm:h-auto' - > -
-
-
- App Logo -
-
-

Readest

-

- {_('Version {{version}}', { version: packageJson.version })} {`(${browserInfo})`} -

-
-
- {hasUpdater() && !isUpdated && ( - - {_('Check Update')} - - )} - {isUpdated && ( -

- {_('Already the latest version')} -

- )} -
+ setAboutDialogVisible(false)} + boxClassName='sm:!w-96 sm:h-auto' + > +
+
+
+ App Logo
- -
- -
+
+

Readest

- © {new Date().getFullYear()} Bilingify LLC. All rights reserved. -

-

- This software is licensed under the{' '} - - GNU Affero General Public License v3.0 - - . You are free to use, modify, and distribute this software under the terms of the - AGPL v3 license. Please see the license for more details. -

-

- Source code is available at{' '} - - GitHub - - . + {_('Version {{version}}', { version: packageJson.version })} {`(${browserInfo})`}

+
+ {appService?.hasUpdater && !isUpdated && ( + + {_('Check Update')} + + )} + {isUpdated && ( +

{_('Already the latest version')}

+ )} +
-
- + +
+ +
+

+ © {new Date().getFullYear()} Bilingify LLC. All rights reserved. +

+

+ This software is licensed under the{' '} + + GNU Affero General Public License v3.0 + + . You are free to use, modify, and distribute this software under the terms of the AGPL + v3 license. Please see the license for more details. +

+

+ Source code is available at{' '} + + GitHub + + . +

+
+
+
); }; diff --git a/apps/readest-app/src/components/UpdaterWindow.tsx b/apps/readest-app/src/components/UpdaterWindow.tsx new file mode 100644 index 00000000..ffb024e8 --- /dev/null +++ b/apps/readest-app/src/components/UpdaterWindow.tsx @@ -0,0 +1,372 @@ +import clsx from 'clsx'; +import semver from 'semver'; +import Image from 'next/image'; +import { useEnv } from '@/context/EnvContext'; +import { useEffect, useState } from 'react'; +import { type as osType, arch as osArch } from '@tauri-apps/plugin-os'; +import { check, Update } from '@tauri-apps/plugin-updater'; +import { relaunch } from '@tauri-apps/plugin-process'; +import { fetch } from '@tauri-apps/plugin-http'; +import { useTranslation } from '@/hooks/useTranslation'; +import { useSearchParams } from 'next/navigation'; +import { useTheme } from '@/hooks/useTheme'; +import { tauriDownload } from '@/utils/transfer'; +import { READEST_UPDATER_FILE, READEST_CHANGELOG_FILE } from '@/services/constants'; +import packageJson from '../../package.json'; +import Dialog from '@/components/Dialog'; +import { installPackage } from '@/utils/bridge'; + +interface ReleaseNotes { + releases: Record< + string, + { + date: string; + notes: string[]; + } + >; +} + +interface Changelog { + version: string; + date: string; + notes: string[]; +} + +type DownloadEvent = + | { + event: 'Started'; + data: { + contentLength?: number; + }; + } + | { + event: 'Progress'; + data: { + chunkLength: number; + }; + } + | { + event: 'Finished'; + }; + +interface GenericUpdate { + currentVersion: string; + version: string; + date?: string; + body?: string; + downloadAndInstall?(onEvent?: (progress: DownloadEvent) => void): Promise; +} + +export const UpdaterContent = ({ version }: { version?: string }) => { + const _ = useTranslation(); + const { appService } = useEnv(); + const searchParams = useSearchParams(); + const currentVersion = packageJson.version; + const resolvedVersion = version ?? searchParams?.get('version') ?? ''; + const [update, setUpdate] = useState(null); + const [isMounted, setIsMounted] = useState(false); + const [newVersion, setNewVersion] = useState(resolvedVersion); + const [changelogs, setChangelogs] = useState([]); + const [progress, setProgress] = useState(null); + const [contentLength, setContentLength] = useState(null); + const [downloaded, setDownloaded] = useState(null); + + useEffect(() => { + const checkDesktopUpdate = async () => { + const update = await check(); + if (update) { + setUpdate(update); + } + }; + const checkAndroidUpdate = async () => { + const response = await fetch(READEST_UPDATER_FILE); + const data = await response.json(); + if (semver.gt(data.version, packageJson.version)) { + const OS_ARCH = osArch(); + 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 apkFilePath = `${cachePrefix}/Readest_${data.version}_${arch}.apk`; + setUpdate({ + currentVersion: packageJson.version, + version: data.version, + date: data.date, + body: data.notes, + downloadAndInstall: async (onEvent) => { + await new Promise(async (resolve, reject) => { + let downloaded = 0; + let total = 0; + await tauriDownload(downloadUrl, apkFilePath, (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('APK downloaded to', apkFilePath); + 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 res = await installPackage({ + path: apkFilePath, + }); + if (res.success) { + console.log('APK installed successfully'); + } else { + console.error('Failed to install APK:', res.error); + } + }, + } as GenericUpdate); + } + }; + const checkForUpdates = async () => { + const OS_TYPE = osType(); + if (['macos', 'windows', 'linux'].includes(OS_TYPE)) { + checkDesktopUpdate(); + } else if (OS_TYPE === 'android') { + checkAndroidUpdate(); + } + }; + checkForUpdates(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const fetchChangelogs = async ( + fromVersion: string, + toVersion: string, + ): Promise => { + try { + const res = await fetch(READEST_CHANGELOG_FILE); + const data: ReleaseNotes = await res.json(); + const releases = data.releases; + + const entries = Object.entries(releases) + .filter(([ver]) => semver.gt(ver, fromVersion) && semver.lte(ver, toVersion)) + .sort(([a], [b]) => semver.rcompare(a, b)) + .map(([version, info]) => ({ + version, + date: new Date(info.date).toDateString(), + notes: info.notes, + })); + + return entries; + } catch (error) { + console.error('Failed to fetch changelog:', error); + return []; + } + }; + const parseNumberedList = (input: string): string[] => { + return input + .split(/(?:^|\s)\d+\.\s/) + .map((item) => item.trim()) + .filter((item) => item.length > 0); + }; + const updateChangelogs = async (update: GenericUpdate) => { + setNewVersion(update.version); + const changelogs = await fetchChangelogs(currentVersion, update.version); + if (changelogs.length > 0) { + setChangelogs(changelogs); + } else { + setChangelogs([ + { + version: update.version, + date: new Date(update.date!).toDateString(), + notes: parseNumberedList(update.body ?? ''), + }, + ]); + } + }; + if (update) { + updateChangelogs(update); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [update]); + + useTheme(); + + useEffect(() => { + setIsMounted(true); + }, []); + + const handleDownloadInstall = async () => { + if (!update) { + return; + } + let downloaded = 0; + let contentLength = 0; + let lastLogged = 0; + setProgress(0); + await update.downloadAndInstall?.((event) => { + switch (event.event) { + case 'Started': + contentLength = event.data.contentLength!; + setContentLength(contentLength); + break; + case 'Progress': + downloaded += event.data.chunkLength; + setDownloaded(downloaded); + const percent = Math.floor((downloaded / contentLength) * 100); + setProgress(percent); + if (downloaded - lastLogged >= 1 * 1024 * 1024) { + console.log(`downloaded ${downloaded} bytes from ${contentLength}`); + lastLogged = downloaded; + } + break; + case 'Finished': + console.log('download finished'); + setProgress(100); + break; + } + }); + console.log('package installed'); + if (!appService?.isAndroidApp && process.env.NODE_ENV === 'production') { + await relaunch(); + } + }; + + if (!isMounted) { + return null; + } + + return ( +
+
+
+
+ Logo +
+ +
+

+ {_('A new version of Readest is Available!')} +

+

+ {_('Readest {{newVersion}} is available (installed version {{currentVersion}}).', { + newVersion, + currentVersion, + })} +

+

{_('Download and install now?')}

+ +
+ {progress !== null && ( +
+ +

+ {progress < 100 + ? _('Downloading {{downloaded}} of {{contentLength}}', { + downloaded: downloaded + ? `${Math.floor(downloaded / 1024 / 1024)} MB` + : '0 MB', + contentLength: contentLength + ? `${Math.floor(contentLength / 1024 / 1024)} MB` + : '0 MB', + }) + : _('Download finished')} +

+
+ )} + +
+ +
+
+
+
+
+

{_('Changelog:')}

+
+ {changelogs.length > 0 ? ( + changelogs.map((entry: Changelog) => ( +
+

+ {entry.version} ({entry.date}) +

+
    + {entry.notes.map((note: string, i: number) => ( +
  • {note}
  • + ))} +
+
+ )) + ) : ( +
+
+
+
+
+ )} +
+
+
+
+ ); +}; + +export const setUpdaterWindowVisible = (visible: boolean, newVersion?: string) => { + if (newVersion) { + localStorage.setItem('newVersion', newVersion); + window.dispatchEvent(new CustomEvent('new-version-detected')); + } + const dialog = document.getElementById('updater_window'); + if (visible) { + (dialog as HTMLDialogElement)?.showModal(); + } else { + (dialog as HTMLDialogElement)?.close(); + } +}; + +export const UpdaterWindow = () => { + const _ = useTranslation(); + const [newVersion, setNewVersion] = useState(localStorage.getItem('newVersion') || ''); + + useEffect(() => { + const handler = () => { + setNewVersion(localStorage.getItem('newVersion') || ''); + }; + window.addEventListener('new-version-detected', handler); + return () => window.removeEventListener('new-version-detected', handler); + }, []); + + return ( + setUpdaterWindowVisible(false)} + boxClassName='sm:!w-[80%] sm:h-auto' + > + + + ); +}; diff --git a/apps/readest-app/src/helpers/updater.ts b/apps/readest-app/src/helpers/updater.ts index 69d751bd..0e079f70 100644 --- a/apps/readest-app/src/helpers/updater.ts +++ b/apps/readest-app/src/helpers/updater.ts @@ -1,35 +1,64 @@ +import semver from 'semver'; import { check } from '@tauri-apps/plugin-updater'; +import { type as osType } from '@tauri-apps/plugin-os'; +import { fetch } from '@tauri-apps/plugin-http'; import { WebviewWindow } from '@tauri-apps/api/webviewWindow'; -import { CHECK_UPDATE_INTERVAL_SEC } from '@/services/constants'; import { TranslationFunc } from '@/hooks/useTranslation'; +import { setUpdaterWindowVisible } from '@/components/UpdaterWindow'; +import { CHECK_UPDATE_INTERVAL_SEC, READEST_UPDATER_FILE } from '@/services/constants'; +import packageJson from '../../package.json'; const LAST_CHECK_KEY = 'lastAppUpdateCheck'; -export const checkForAppUpdates = async (_: TranslationFunc, autoCheck = true) => { +const showUpdateWindow = (newVersion: string) => { + const win = new WebviewWindow('updater', { + url: `/updater?version=${newVersion}`, + title: 'Software Update', + width: 626, + height: 406, + center: true, + resizable: true, + }); + win.once('tauri://created', () => { + console.log('new window created'); + }); + win.once('tauri://error', (e) => { + console.error('error creating window', e); + }); +}; + +export const checkForAppUpdates = async ( + _: TranslationFunc, + autoCheck = true, +): Promise => { const lastCheck = localStorage.getItem(LAST_CHECK_KEY); const now = Date.now(); if (autoCheck && lastCheck && now - parseInt(lastCheck, 10) < CHECK_UPDATE_INTERVAL_SEC * 1000) - return; + return false; localStorage.setItem(LAST_CHECK_KEY, now.toString()); console.log('Checking for updates'); - const update = await check(); - console.log('Update found', update); - if (update) { - const win = new WebviewWindow('updater', { - url: `/updater?version=${update.version}`, - title: 'Software Update', - width: 626, - height: 406, - center: true, - resizable: true, - }); - win.once('tauri://created', () => { - console.log('new window created'); - }); - win.once('tauri://error', (e) => { - console.error('error creating window', e); - }); + const OS_TYPE = osType(); + if (['macos', 'windows', 'linux'].includes(OS_TYPE)) { + const update = await check(); + if (update) { + showUpdateWindow(update.version); + } + return !!update; + } else if (OS_TYPE === 'android') { + try { + const response = await fetch(READEST_UPDATER_FILE); + const data = await response.json(); + const isNewer = semver.gt(data.version, packageJson.version); + if (isNewer && ('android-arm64' in data.platforms || 'android-universal' in data.platforms)) { + setUpdaterWindowVisible(true, data.version); + } + return isNewer; + } catch (err) { + console.warn('Failed to fetch Android update info', err); + return false; + } } - return update; + + return false; }; diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index e42be072..ec92ace7 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -57,6 +57,7 @@ export abstract class BaseAppService implements AppService { hasSafeAreaInset = false; hasHaptics = false; hasSysFontsList = false; + hasUpdater = false; abstract fs: FileSystem; diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index d6038b48..6a40f3ec 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -468,6 +468,12 @@ export const DOWNLOAD_READEST_URL = 'https://readest.com?utm_source=readest_web' export const READEST_WEB_BASE_URL = 'https://web.readest.com'; +export const GITHUB_LATEST_DOWNLOAD = 'https://github.com/readest/readest/releases/latest/download'; + +export const READEST_UPDATER_FILE = `${GITHUB_LATEST_DOWNLOAD}/latest.json`; + +export const READEST_CHANGELOG_FILE = `${GITHUB_LATEST_DOWNLOAD}/release-notes.json`; + export const SYNC_PROGRESS_INTERVAL_SEC = 60; export const SYNC_NOTES_INTERVAL_SEC = 10; export const SYNC_BOOKS_INTERVAL_SEC = 10; diff --git a/apps/readest-app/src/services/environment.ts b/apps/readest-app/src/services/environment.ts index a01a2a8b..cf849308 100644 --- a/apps/readest-app/src/services/environment.ts +++ b/apps/readest-app/src/services/environment.ts @@ -4,14 +4,11 @@ import { READEST_WEB_BASE_URL } from './constants'; declare global { interface Window { __READEST_CLI_ACCESS?: boolean; - __READEST_UPDATER_ACCESS?: boolean; } } export const isTauriAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'tauri'; export const isWebAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'web'; -export const hasUpdater = () => - window.__READEST_UPDATER_ACCESS === true && !process.env['NEXT_PUBLIC_DISABLE_UPDATER']; export const hasCli = () => window.__READEST_CLI_ACCESS === true; export const isPWA = () => window.matchMedia('(display-mode: standalone)').matches; diff --git a/apps/readest-app/src/services/nativeAppService.ts b/apps/readest-app/src/services/nativeAppService.ts index 35e2092d..ab764caf 100644 --- a/apps/readest-app/src/services/nativeAppService.ts +++ b/apps/readest-app/src/services/nativeAppService.ts @@ -221,6 +221,7 @@ export class NativeAppService extends BaseAppService { override hasSafeAreaInset = OS_TYPE === 'ios' || OS_TYPE === 'android'; override hasHaptics = OS_TYPE === 'ios' || OS_TYPE === 'android'; override hasSysFontsList = !(OS_TYPE === 'ios' || OS_TYPE === 'android'); + override hasUpdater = OS_TYPE !== 'ios' && !process.env['NEXT_PUBLIC_DISABLE_UPDATER']; override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } { return resolvePath(fp, base); diff --git a/apps/readest-app/src/types/system.ts b/apps/readest-app/src/types/system.ts index feeb76e1..2c576dce 100644 --- a/apps/readest-app/src/types/system.ts +++ b/apps/readest-app/src/types/system.ts @@ -34,6 +34,7 @@ export interface AppService { hasSafeAreaInset: boolean; hasHaptics: boolean; hasSysFontsList: boolean; + hasUpdater: boolean; isMobile: boolean; isAppDataSandbox: boolean; isAndroidApp: boolean; diff --git a/apps/readest-app/src/utils/bridge.ts b/apps/readest-app/src/utils/bridge.ts index f83cc0bb..82e76867 100644 --- a/apps/readest-app/src/utils/bridge.ts +++ b/apps/readest-app/src/utils/bridge.ts @@ -14,6 +14,15 @@ export interface UseBackgroundAudioRequest { enabled: boolean; } +export interface InstallPackageRequest { + path: string; +} + +export interface InstallPackageResponse { + success: boolean; + error?: string; +} + export async function copyURIToPath(request: CopyURIRequest): Promise { const result = await invoke('plugin:native-bridge|copy_uri_to_path', { payload: request, @@ -27,3 +36,12 @@ export async function invokeUseBackgroundAudio(request: UseBackgroundAudioReques payload: request, }); } + +export async function installPackage( + request: InstallPackageRequest, +): Promise { + const result = await invoke('plugin:native-bridge|install_package', { + payload: request, + }); + return result; +}