feat(desktop): stablize updater ux and window state (#4798)

This commit is contained in:
Shreyas 2025-03-03 15:00:04 +05:30 committed by GitHub
parent 8bfa8c98f5
commit fac3bec988
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 462 additions and 212 deletions

View file

@ -1,7 +1,7 @@
{
"name": "hoppscotch-desktop",
"private": true,
"version": "0.1.0",
"version": "25.2.0-0",
"type": "module",
"scripts": {
"dev": "vite",
@ -19,8 +19,10 @@
"@hoppscotch/plugin-appload": "github:CuriousCorrelation/tauri-plugin-appload",
"@hoppscotch/ui": "0.2.1",
"@tauri-apps/api": "2.1.1",
"@tauri-apps/plugin-process": "2.2.0",
"@tauri-apps/plugin-shell": "2.0.1",
"@tauri-apps/plugin-store": "2.2.0",
"@tauri-apps/plugin-updater": "2.5.1",
"@vueuse/core": "11.1.0",
"fp-ts": "2.16.9",
"vue": "^3.3.4",

View file

@ -2023,7 +2023,7 @@ dependencies = [
[[package]]
name = "hoppscotch-desktop"
version = "0.1.0"
version = "25.2.0"
dependencies = [
"axum",
"portpicker",
@ -2035,6 +2035,7 @@ dependencies = [
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-process",
"tauri-plugin-relay",
"tauri-plugin-shell",
"tauri-plugin-store",
@ -5066,6 +5067,16 @@ dependencies = [
"uuid",
]
[[package]]
name = "tauri-plugin-process"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40cc553ab29581c8c43dfa5fb0c9d5aee8ba962ad3b42908eea26c79610441b7"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-relay"
version = "0.1.0"

View file

@ -1,6 +1,6 @@
[package]
name = "hoppscotch-desktop"
version = "0.1.0"
version = "25.2.0"
description = "Desktop App for hoppscotch.io"
authors = ["CuriousCorrelation"]
edition = "2021"
@ -34,9 +34,8 @@ axum = "0.8.1"
tower-http = { version = "0.6.2", features = ["cors"] }
portpicker = "0.1.1"
tokio = "1.43.0"
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
tauri-plugin-updater = "2.3.1"
tauri-plugin-process = "2.2.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = "2.3.1"
tauri-plugin-window-state = "2.2.1"

View file

@ -14,6 +14,7 @@
"shell:allow-open",
"store:default",
"dialog:default",
"process:default",
"updater:default",
"fs:allow-write-text-file",
"deep-link:default",

View file

@ -6,6 +6,7 @@
"linux"
],
"permissions": [
"updater:default",
"window-state:default"
]
}

View file

@ -6,6 +6,7 @@ use std::sync::OnceLock;
use tauri::Emitter;
use tauri_plugin_appload::VendorConfigBuilder;
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_window_state::StateFlags;
static SERVER_PORT: OnceLock<u16> = OnceLock::new();
@ -19,16 +20,22 @@ pub fn run() {
tracing::info!("Starting Hoppscotch Desktop v{}", env!("CARGO_PKG_VERSION"));
tauri::Builder::default()
.plugin(tauri_plugin_window_state::Builder::new().build())
.plugin(
tauri_plugin_window_state::Builder::new()
.with_state_flags(
StateFlags::SIZE
| StateFlags::POSITION
| StateFlags::MAXIMIZED
| StateFlags::FULLSCREEN,
)
.with_denylist(&["main"])
.build(),
)
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
let _ = app
.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?;
Ok(())
})
.setup(|app| {
let handle = app.handle().clone();
tracing::info!(app_name = %app.package_info().name, "Configuring deep link handler");

View file

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Hoppscotch",
"version": "25.1.0-0",
"version": "25.2.0",
"identifier": "io.hoppscotch.desktop",
"build": {
"beforeDevCommand": "pnpm dev",
@ -12,14 +12,12 @@
"app": {
"windows": [
{
"title": "hoppscotch-desktop",
"decorations": false,
"title": "main",
"width": 500,
"height": 600,
"decorations": false,
"alwaysOnTop": true,
"resizable": false,
"visible": false,
"shadow": true
"resizable": false
}
],
"security": {
@ -34,13 +32,6 @@
}
}
},
"plugins": {
"deep-link": {
"desktop": {
"schemes": ["io.hoppscotch.desktop"]
}
}
},
"bundle": {
"active": true,
"targets": "all",
@ -54,6 +45,11 @@
]
},
"plugins": {
"deep-link": {
"desktop": {
"schemes": ["io.hoppscotch.desktop"]
}
},
"updater": {
"active": true,
"endpoints": ["https://releases.hoppscotch.com/hoppscotch-selfhost-desktop.json"],

View file

@ -8,6 +8,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
LayoutHeader: typeof import('./components/layout/LayoutHeader.vue')['default']
LayoutSidebar: typeof import('./components/layout/LayoutSidebar.vue')['default']

View file

@ -8,3 +8,31 @@ export interface RecentInstance {
export interface StoreSchema {
recentInstances: RecentInstance[]
}
export enum UpdateStatus {
IDLE = "idle",
CHECKING = "checking",
AVAILABLE = "available",
NOT_AVAILABLE = "not_available",
DOWNLOADING = "downloading",
INSTALLING = "installing",
READY_TO_RESTART = "ready_to_restart",
ERROR = "error"
}
export enum CheckResult {
AVAILABLE,
NOT_AVAILABLE,
TIMEOUT,
ERROR
}
export interface UpdateState {
status: UpdateStatus;
version?: string;
message?: string;
progress?: {
downloaded: number;
total?: number;
};
}

View file

@ -0,0 +1,156 @@
import { check, type DownloadEvent } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
import { type LazyStore } from "@tauri-apps/plugin-store";
import { UpdateStatus, CheckResult, UpdateState } from "~/types";
export class UpdaterService {
constructor(private store: LazyStore) {}
async initialize(): Promise<void> {
await this.saveUpdateState({
status: UpdateStatus.IDLE
});
}
async checkForUpdates(timeout = 2000): Promise<CheckResult> {
try {
await this.saveUpdateState({
status: UpdateStatus.CHECKING
});
// Create a timeout promise, this is just to make sure we don't keep checking for updates indefinitely
// NOTE: Also `checkUpdate` tends to hang indefinitely in dev mode, but works in build
const timeoutPromise = new Promise<null>((resolve) => {
setTimeout(() => {
console.log("Update check timeout reached, proceeding with app load");
resolve(null);
}, timeout);
});
const updateResult = await Promise.race([
check({ timeout }),
timeoutPromise
]);
// If we got a timeout (null), we treat it as no update available
// NOTE: We could maybe show more info but for now this works fine
if (!updateResult) {
console.log("Update check timed out or no update available");
await this.saveUpdateState({
status: UpdateStatus.NOT_AVAILABLE
});
return CheckResult.TIMEOUT;
}
const hasUpdates = updateResult.available;
await this.saveUpdateState(
hasUpdates
? {
status: UpdateStatus.AVAILABLE,
version: updateResult.version,
message: updateResult.body
}
: {
status: UpdateStatus.NOT_AVAILABLE
}
);
console.log("Update check result:", {
available: updateResult.available,
currentVersion: updateResult.currentVersion,
version: updateResult.version
});
return hasUpdates ? CheckResult.AVAILABLE : CheckResult.NOT_AVAILABLE;
} catch (error) {
console.error("Error checking for updates:", error);
await this.saveUpdateState({
status: UpdateStatus.ERROR,
message: String(error)
});
return CheckResult.ERROR;
}
}
async downloadAndInstall(): Promise<void> {
try {
const updateResult = await check();
if (!updateResult) {
throw new Error("No update available to install");
}
await this.saveUpdateState({
status: UpdateStatus.DOWNLOADING,
progress: {
downloaded: 0,
total: undefined
}
});
let totalBytes: number | undefined;
let downloadedBytes = 0;
await updateResult.downloadAndInstall(
(event: DownloadEvent) => {
if (event.event === 'Started') {
totalBytes = event.data.contentLength;
this.saveUpdateState({
status: UpdateStatus.DOWNLOADING,
progress: {
downloaded: 0,
total: totalBytes
}
});
} else if (event.event === 'Progress') {
downloadedBytes += event.data.chunkLength;
this.saveUpdateState({
status: UpdateStatus.DOWNLOADING,
progress: {
downloaded: downloadedBytes,
total: totalBytes
}
});
} else if (event.event === 'Finished') {
this.saveUpdateState({
status: UpdateStatus.INSTALLING
});
}
}
);
// If we reach here, it means the app hasn't restarted automatically
// Mark as ready to restart
await this.saveUpdateState({
status: UpdateStatus.READY_TO_RESTART
});
} catch (error) {
console.error("Error installing updates:", error);
await this.saveUpdateState({
status: UpdateStatus.ERROR,
message: String(error)
});
throw error;
}
}
async restartApp(): Promise<void> {
try {
await relaunch();
} catch (error) {
console.error("Failed to restart app:", error);
throw error;
}
}
private async saveUpdateState(state: UpdateState): Promise<void> {
try {
await this.store.set("updateState", state);
await this.store.save();
} catch (error) {
console.error("Failed to save update state:", error);
}
}
}

View file

@ -1,26 +1,91 @@
<template>
<div class="flex flex-col items-center justify-center w-full h-screen bg-primary">
<div class="flex flex-col items-center space-y-6">
<div class="flex flex-col items-center space-y-6 max-w-md text-center">
<div class="flex items-center space-x-4">
<IconLucideCloud class="h-16 w-16 text-secondaryDark" />
<div class="flex flex-col">
<img src="/logo.svg" alt="Hoppscotch" class="h-7 w-7" />
<div class="flex flex-col items-start">
<h1 class="text-2xl font-semibold text-secondaryDark">Hoppscotch</h1>
<p class="text-secondary text-sm">Desktop</p>
</div>
</div>
<div v-if="isLoading" class="flex flex-col items-center space-y-4">
<div v-if="appState === AppState.LOADING" class="flex flex-col items-center space-y-4">
<HoppSmartSpinner />
<p class="text-secondaryDark">{{ statusMessage }}</p>
</div>
<div v-else-if="error" class="flex flex-col items-center space-y-4">
<IconLucideAlertCircle class="h-10 w-10 text-red-500" />
<p class="text-red-500">{{ error }}</p>
<div
v-else-if="appState === AppState.UPDATE_AVAILABLE || appState === AppState.UPDATE_IN_PROGRESS || appState === AppState.UPDATE_READY"
class="flex flex-col items-center space-y-4"
>
<IconLucideDownload class="h-16 w-16 text-accent" />
<div class="text-center">
<h2 class="text-xl font-semibold text-secondaryDark">Update Available</h2>
<p class="text-secondary mt-1">{{ updateMessage || 'A new version of Hoppscotch is available, downloading...' }}</p>
</div>
<div v-if="downloadProgress.total && downloadProgress.downloaded" class="w-full">
<div class="w-full bg-primaryLight rounded-full h-2.5">
<div
class="bg-accent h-2.5 rounded-full"
:style="{ width: `${(downloadProgress.downloaded / downloadProgress.total) * 100}%` }"
></div>
</div>
<div class="flex justify-between text-sm text-secondaryLight mt-1">
<span>{{ Math.round((downloadProgress.downloaded / downloadProgress.total) * 100) }}%</span>
<span class="text-xs">
{{ formatBytes(downloadProgress.downloaded) }} / {{ formatBytes(downloadProgress.total) }}
</span>
</div>
</div>
<div v-else-if="downloadProgress.downloaded > 0" class="w-full">
<div class="w-full bg-primaryLight rounded-full h-2.5">
<div class="bg-accent h-2.5 rounded-full animate-pulse" style="width: 100%"></div>
</div>
<p class="text-sm text-secondaryLight text-center mt-1">
Downloaded {{ formatBytes(downloadProgress.downloaded) }}
</p>
</div>
<div class="flex space-x-2">
<HoppButtonPrimary
v-if="appState === AppState.UPDATE_AVAILABLE"
label="Install Update"
:icon="IconLucideDownload"
@click="installUpdate"
/>
<HoppButtonPrimary
v-else-if="appState === AppState.UPDATE_READY"
label="Restart Now"
:icon="IconLucideRefreshCw"
@click="restartApp"
/>
<HoppButtonSecondary
v-if="appState === AppState.UPDATE_AVAILABLE"
label="Later"
outline
@click="skipUpdate"
/>
</div>
</div>
<div v-else-if="appState === AppState.ERROR" class="flex flex-col items-center space-y-4">
<IconLucideAlertCircle class="h-16 w-16 text-red-500" />
<div class="text-center">
<h2 class="text-xl font-semibold text-secondaryDark">Something went wrong</h2>
<p class="text-red-500 mt-2">{{ error }}</p>
</div>
<HoppButtonPrimary
label="Try Again"
:icon="IconLucideRefreshCw"
@click="loadVendored"
@click="initialize"
/>
</div>
<div class="text-secondaryLight text-xs mt-4">
<p>Version {{ appVersion }}</p>
</div>
</div>
</div>
</template>
@ -29,14 +94,25 @@
import { ref, onMounted } from "vue";
import { LazyStore } from '@tauri-apps/plugin-store';
import { load } from "@hoppscotch/plugin-appload";
import { invoke } from "@tauri-apps/api/core";
import { getVersion } from '@tauri-apps/api/app';
import IconLucideCloud from "~icons/lucide/cloud"
import IconLucideAlertCircle from "~icons/lucide/alert-circle"
import IconLucideRefreshCw from "~icons/lucide/refresh-cw"
import { UpdateStatus, CheckResult, UpdateState } from "~/types";
import { UpdaterService } from "~/utils/updater";
const HOME_STORE_PATH = "hopp.store.json";
const APP_STORE_PATH = "hoppscotch.hoppscotch.store";
import IconLucideAlertCircle from "~icons/lucide/alert-circle";
import IconLucideRefreshCw from "~icons/lucide/refresh-cw";
import IconLucideDownload from "~icons/lucide/download";
const APP_STORE_PATH = "hoppscotch-desktop.store";
enum AppState {
LOADING = "loading",
UPDATE_AVAILABLE = "update_available",
UPDATE_IN_PROGRESS = "update_in_progress",
UPDATE_READY = "update_ready",
ERROR = "error",
LOADED = "loaded"
}
interface VendoredInstance {
type: "vendored";
@ -51,188 +127,115 @@ interface ConnectionState {
message?: string;
}
interface StoredData {
schemaVersion: number;
metadata: {
createdAt: string;
updatedAt: string;
namespace: string;
};
data: unknown;
}
interface StoreData {
data: {
[namespace: string]: {
[key: string]: StoredData;
};
};
}
const home_store = new LazyStore(HOME_STORE_PATH);
const app_store = new LazyStore(APP_STORE_PATH);
const isLoading = ref(true);
const appStore = new LazyStore(APP_STORE_PATH);
const appState = ref<AppState>(AppState.LOADING);
const updateStatus = ref("");
const updateMessage = ref("");
const downloadProgress = ref<{ downloaded: number; total?: number }>({ downloaded: 0 });
const error = ref("");
const statusMessage = ref("Initializing...");
const appVersion = ref("...");
const updaterService = new UpdaterService(appStore);
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const saveConnectionState = async (state: ConnectionState) => {
try {
await home_store.set("connectionState", state);
await home_store.save();
await appStore.set("connectionState", state);
await appStore.save();
} catch (err) {
console.error("Failed to save connection state:", err);
}
};
const migrateFromLocalStorage = async () => {
const keyMappings = {
settings: "settings",
collections: "restCollections",
collectionsGraphql: "gqlCollections",
environments: "environments",
history: "restHistory",
graphqlHistory: "gqlHistory",
WebsocketRequest: "websocket",
SocketIORequest: "socketio",
SSERequest: "sse",
MQTTRequest: "mqtt",
globalEnv: "globalEnv",
restTabState: "restTabs",
gqlTabState: "gqlTabs",
secretEnvironments: "secretEnvironments"
};
const setupUpdateStateWatcher = async () => {
const unsubscribe = await appStore.onKeyChange<UpdateState>("updateState", (newValue) => {
if (!newValue) return;
console.log("Starting migration from localStorage to home_store...");
let migratedCount = 0;
updateStatus.value = newValue.status;
updateMessage.value = newValue.message || "";
const storeData: StoreData = {
data: {
"persistence.v1": {}
if (newValue.progress) {
downloadProgress.value = newValue.progress;
}
};
console.log("Current localStorage keys:", Object.keys(localStorage));
for (const [oldKey, newKey] of Object.entries(keyMappings)) {
const data = localStorage.getItem(oldKey);
if (data) {
try {
console.log(`Migrating ${oldKey} to persistence.v1.${newKey}...`);
let parsedData;
try {
parsedData = JSON.parse(data);
} catch (e) {
console.error(`Failed to parse ${oldKey} data:`, e);
continue;
}
const storedData: StoredData = {
schemaVersion: 1,
metadata: {
createdAt: new Date().toISOString(),
namespace: "persistence.v1",
updatedAt: new Date().toISOString(),
},
data: parsedData
};
storeData.data["persistence.v1"][newKey] = storedData;
localStorage.removeItem(oldKey);
migratedCount++;
console.log(`Successfully migrated ${oldKey}`);
} catch (err) {
console.error(`Failed to migrate ${oldKey}:`, err);
}
if (newValue.status === UpdateStatus.AVAILABLE) {
appState.value = AppState.UPDATE_AVAILABLE;
} else if (newValue.status === UpdateStatus.ERROR) {
error.value = newValue.message || "Unknown error";
appState.value = AppState.ERROR;
} else if (newValue.status === UpdateStatus.DOWNLOADING || newValue.status === UpdateStatus.INSTALLING) {
appState.value = AppState.UPDATE_IN_PROGRESS;
} else if (newValue.status === UpdateStatus.READY_TO_RESTART) {
appState.value = AppState.UPDATE_READY;
}
}
});
await app_store.set('data', storeData.data);
await app_store.save();
console.log(`Migration complete. Migrated ${migratedCount} items.`);
return unsubscribe;
};
interface UpdateCheckResult {
status: "completed" | "timeout";
hasUpdates?: boolean;
}
const installUpdate = async () => {
try {
appState.value = AppState.UPDATE_IN_PROGRESS;
await updaterService.downloadAndInstall();
// In a rare occurance where we reach here but automatic restart didn't happen,
// we'll just show a restart button instead
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
error.value = `Failed to install update: ${errorMessage}`;
appState.value = AppState.ERROR;
}
};
const skipUpdate = async () => {
await loadVendored();
};
const restartApp = async () => {
try {
await updaterService.restartApp();
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
error.value = `Failed to restart app: ${errorMessage}`;
appState.value = AppState.ERROR;
}
};
const loadVendored = async () => {
isLoading.value = true;
error.value = "";
try {
console.log("Initializing home_store and starting migration process");
await home_store.init();
await app_store.init();
statusMessage.value = "Loading application...";
console.log(`Using home_store path: ${HOME_STORE_PATH}`);
console.log("home_Store initialized successfully");
const vendoredInstance: VendoredInstance = {
type: "vendored",
displayName: "Vendored",
version: "vendored"
};
try {
await migrateFromLocalStorage();
console.log("Migration completed successfully");
} catch (migrationError) {
console.error("Migration error:", migrationError);
await saveConnectionState({
status: "connected",
instance: vendoredInstance
});
console.log("Loading vendored app...");
const loadResp = await load({
bundleName: "Hoppscotch",
window: { title: "Hoppscotch" }
});
if (!loadResp.success) {
throw new Error("Failed to load Hoppscotch Vendored");
}
let shouldProceedWithLoad = true;
try {
console.log("Checking for updates before loading app...");
const timeoutPromise: Promise<UpdateCheckResult> = new Promise((resolve) => {
setTimeout(() => {
console.log("Update check timeout reached, proceeding with app load");
resolve({ status: "timeout" });
}, 2000); // TODO: 2s shoud be good?
});
const result = await Promise.race([
invoke('check_updates_available').then(hasUpdates => ({ status: "completed", hasUpdates })),
timeoutPromise
]);
console.log("Update check result:", result);
if (result.status === "completed" && result.hasUpdates) {
console.log("Updates available, handling before loading app");
shouldProceedWithLoad = false;
await invoke('install_updates_and_restart');
// This point would only be reached if install_updates_and_restart
// doesn't actually restart the app
return;
}
} catch (updateError) {
console.error("Update check error:", updateError);
// Continue with loading the app despite update check errors
}
if (shouldProceedWithLoad) {
const vendoredInstance: VendoredInstance = {
type: "vendored",
displayName: "Vendored",
version: "vendored"
};
await saveConnectionState({
status: "connected",
instance: vendoredInstance
});
console.log("Loading vendored app...");
const loadResp = await load({
bundleName: "Hoppscotch",
window: { title: "Hoppscotch" }
});
if (!loadResp.success) {
throw new Error("Failed to load Hoppscotch Vendored");
}
console.log("Vendored app loaded successfully");
}
console.log("Vendored app loaded successfully");
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.error("Error loading vendored app:", errorMessage);
@ -244,23 +247,48 @@ const loadVendored = async () => {
message: errorMessage
});
isLoading.value = false;
appState.value = AppState.ERROR;
}
};
const initialize = async () => {
appState.value = AppState.LOADING;
error.value = "";
downloadProgress.value = { downloaded: 0 };
try {
try {
appVersion.value = await getVersion();
} catch (error) {
console.error("Failed to get app version:", error);
appVersion.value = "unknown";
}
statusMessage.value = "Initializing stores...";
await appStore.init();
await updaterService.initialize();
await setupUpdateStateWatcher();
statusMessage.value = "Checking for updates...";
const checkResult = await updaterService.checkForUpdates();
if (checkResult === CheckResult.AVAILABLE) {
console.log("Updates available, prompting for install");
appState.value = AppState.UPDATE_AVAILABLE;
return;
}
await loadVendored();
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.error("Initialization error:", errorMessage);
error.value = errorMessage;
appState.value = AppState.ERROR;
}
};
onMounted(() => {
loadVendored();
initialize();
});
</script>
<style scoped>
.loading-spinner {
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View file

@ -984,12 +984,18 @@ importers:
'@tauri-apps/api':
specifier: 2.1.1
version: 2.1.1
'@tauri-apps/plugin-process':
specifier: 2.2.0
version: 2.2.0
'@tauri-apps/plugin-shell':
specifier: 2.0.1
version: 2.0.1
'@tauri-apps/plugin-store':
specifier: 2.2.0
version: 2.2.0
'@tauri-apps/plugin-updater':
specifier: 2.5.1
version: 2.5.1
'@vueuse/core':
specifier: 11.1.0
version: 11.1.0(vue@3.5.12(typescript@5.7.2))
@ -5247,6 +5253,9 @@ packages:
'@tauri-apps/plugin-fs@2.0.2':
resolution: {integrity: sha512-4YZaX2j7ta81M5/DL8aN10kTnpUkEpkPo1FTYPT8Dd0ImHe3azM8i8MrtjrDGoyBYLPO3zFv7df/mSCYF8oA0Q==}
'@tauri-apps/plugin-process@2.2.0':
resolution: {integrity: sha512-uypN2Crmyop9z+KRJr3zl71OyVFgTuvHFjsJ0UxxQ/J5212jVa5w4nPEYjIewcn8bUEXacRebwE6F7owgrbhSw==}
'@tauri-apps/plugin-shell@2.0.0':
resolution: {integrity: sha512-OpW2+ycgJLrEoZityWeWYk+6ZWP9VyiAfbO+N/O8VfLkqyOym8kXh7odKDfINx9RAotkSGBtQM4abyKfJDkcUg==}
@ -5256,6 +5265,9 @@ packages:
'@tauri-apps/plugin-store@2.2.0':
resolution: {integrity: sha512-hJTRtuJis4w5fW1dkcgftsYxKXK0+DbAqurZ3CURHG5WkAyyZgbxpeYctw12bbzF9ZbZREXZklPq8mocCC3Sgg==}
'@tauri-apps/plugin-updater@2.5.1':
resolution: {integrity: sha512-7fNJraKRbKkxguMY5lG2W20pBvAUkLu+cqnbu0UcK7DqeZgrAnNECcGBIDG6fJ6C+0fAp2V2dMIgznhffOBCcg==}
'@trysound/sax@0.2.0':
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
@ -17430,6 +17442,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/plugin-process@2.2.0':
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/plugin-shell@2.0.0':
dependencies:
'@tauri-apps/api': 2.0.2
@ -17442,6 +17458,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.1.1
'@tauri-apps/plugin-updater@2.5.1':
dependencies:
'@tauri-apps/api': 2.1.1
'@trysound/sax@0.2.0':
optional: true