Skip to content

Commit a3d6ddb

Browse files
committed
feat: auto update works
1 parent 82c3be3 commit a3d6ddb

File tree

7 files changed

+172
-66
lines changed

7 files changed

+172
-66
lines changed

src-tauri/capabilities/deafult.json

Lines changed: 0 additions & 42 deletions
This file was deleted.

src-tauri/capabilities/main.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"$schema": "../gen/schemas/desktop-schema.json",
3+
"identifier": "main",
4+
"description": "Default capability set for Milo desktop windows",
5+
"local": true,
6+
"windows": ["*"],
7+
"permissions": [
8+
"core:default",
9+
"core:window:allow-center",
10+
"core:window:allow-set-title",
11+
"core:window:allow-set-position",
12+
"core:window:allow-start-dragging",
13+
"core:window:allow-set-theme",
14+
"core:window:deny-internal-toggle-maximize",
15+
"core:window:allow-set-size",
16+
"core:window:allow-set-focus",
17+
"core:window:allow-show",
18+
"core:window:allow-hide",
19+
"dialog:default",
20+
"notification:default",
21+
"clipboard-manager:default",
22+
"global-shortcut:default",
23+
"shell:default",
24+
{
25+
"identifier": "shell:allow-execute",
26+
"allow": [
27+
{
28+
"name": "bin/ocr",
29+
"sidecar": true,
30+
"args": true
31+
}
32+
]
33+
},
34+
"updater:default",
35+
"updater:allow-check",
36+
"updater:allow-download",
37+
"updater:allow-download-and-install",
38+
"updater:allow-install",
39+
"core:app:default",
40+
"core:app:allow-version"
41+
]
42+
}

src-tauri/src/api.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use tauri::Manager;
1+
use tauri::{AppHandle, Manager};
22

33
use crate::{
44
settings::{api_key_file_path, litellm_api_key_file_path, Settings},
@@ -90,3 +90,11 @@ pub async fn show_settings(window: tauri::Window) -> Result<(), String> {
9090
}
9191
Ok(())
9292
}
93+
94+
#[tauri::command]
95+
pub async fn relaunch_app(app: AppHandle) -> Result<(), String> {
96+
std::thread::spawn(move || {
97+
app.restart();
98+
});
99+
Ok(())
100+
}

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ pub fn run() {
8383
api::save_settings,
8484
api::get_settings,
8585
api::show_settings,
86+
api::relaunch_app,
8687
core::transform_clipboard,
8788
core::transform_clip_with_setting,
8889
shortcuts::get_current_shortcut,

src-tauri/tauri.conf.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
},
2121
"productName": "Milo",
2222
"mainBinaryName": "Milo",
23-
"version": "0.1.15",
23+
"version": "0.1.14",
2424
"identifier": "com.milo.dev",
2525
"plugins": {
2626
"updater": {
27+
"active": true,
2728
"endpoints": [
2829
"https://raw.githubusercontent.com/antoncoding/milo/master/latest.json"
2930
],
31+
"dialog": true,
3032
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVEN0Q0OEYzNDgyOTIxQUEKUldTcUlTbEk4MGg5N1VITktmZ3VBK2QrVzcrRzJYTmhSVTQ0ZjRvSVFxUGsvcmtEdHE0SXhKemIK"
3133
}
3234
},

src/components/InfoPage.tsx

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
import { invoke } from "@tauri-apps/api/core";
22
import { getVersion } from "@tauri-apps/api/app";
3-
import { useEffect, useState } from "react";
3+
import { useEffect, useRef, useState } from "react";
44
import { backendFormatToShortcut, Shortcut } from "../utils/keyboardUtils";
55
import { updateManager, UpdateInfo } from "../utils/updater";
66
import miloLogo from "../assets/icon.png";
77

88
export function InfoPage() {
9+
const isDev = import.meta.env.DEV;
910
const [shortcut, setShortcut] = useState<Shortcut>([]);
1011
const [shortcutEnabled, setShortcutEnabled] = useState(true);
1112
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
1213
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
1314
const [isDownloading, setIsDownloading] = useState(false);
1415
const [downloadProgress, setDownloadProgress] = useState(0);
1516
const [statusMessage, setStatusMessage] = useState<string | null>(null);
17+
const [statusVariant, setStatusVariant] = useState<'neutral' | 'success' | 'error'>('neutral');
1618
const [currentVersion, setCurrentVersion] = useState<string>("");
19+
const totalBytesRef = useRef<number | null>(null);
20+
const downloadedBytesRef = useRef(0);
21+
const initRef = useRef(false);
1722

1823
// Load initial settings and check for updates
1924
useEffect(() => {
25+
if (initRef.current) {
26+
return;
27+
}
28+
initRef.current = true;
29+
2030
const loadSettings = async () => {
2131
try {
2232
console.log("🔄 Frontend: Loading initial settings...");
@@ -57,47 +67,88 @@ export function InfoPage() {
5767
console.error("❌ Failed to get app version:", error);
5868
});
5969
// Check for updates 2 seconds after loading to avoid blocking UI
60-
setTimeout(checkForUpdatesOnLaunch, 2000);
61-
}, []);
70+
if (isDev) {
71+
setStatusVariant('neutral');
72+
setStatusMessage('Auto-updater is disabled in dev builds. Run a packaged app to test updates.');
73+
} else {
74+
setTimeout(checkForUpdatesOnLaunch, 2000);
75+
}
76+
}, [isDev]);
6277

6378
const checkForUpdates = async () => {
79+
if (isDev) {
80+
setStatusVariant('neutral');
81+
setStatusMessage('Auto-updater is disabled in dev builds. Run a packaged app to test updates.');
82+
return;
83+
}
84+
6485
try {
6586
setIsCheckingUpdate(true);
6687
const update = await updateManager.checkForUpdates();
6788
setUpdateInfo(update);
6889
if (!update) {
6990
// Show a message that no updates are available
7091
console.log('No updates available');
92+
setStatusVariant('success');
7193
setStatusMessage(currentVersion ? `Your Milo version v${currentVersion} is up to date!` : 'Milo is up to date!');
7294
} else {
7395
setStatusMessage(null);
7496
}
7597
} catch (error) {
7698
console.error('Failed to check for updates:', error);
99+
setStatusVariant('error');
77100
setStatusMessage('Failed to check for updates. Please try again.');
78101
} finally {
79102
setIsCheckingUpdate(false);
80103
}
81104
};
82105

83106
const downloadUpdate = async () => {
107+
if (isDev) {
108+
setStatusVariant('neutral');
109+
setStatusMessage('Auto-updater is disabled in dev builds. Run a packaged app to test updates.');
110+
return;
111+
}
112+
84113
if (!updateInfo) return;
85114

86115
try {
87116
setIsDownloading(true);
117+
totalBytesRef.current = null;
118+
downloadedBytesRef.current = 0;
119+
setDownloadProgress(0);
120+
console.log('⬇️ Frontend: Starting update download');
88121
updateManager.setProgressCallback((event) => {
122+
console.log('⬇️ Frontend: Download event received', event);
89123
if (event.event === 'Started') {
124+
totalBytesRef.current = event.data.contentLength ?? null;
125+
downloadedBytesRef.current = 0;
90126
setDownloadProgress(0);
91127
} else if (event.event === 'Progress') {
92-
const progress = Math.round((event.data.chunkLength / event.data.contentLength) * 100);
93-
setDownloadProgress(progress);
128+
downloadedBytesRef.current += event.data.chunkLength ?? 0;
129+
if (totalBytesRef.current && totalBytesRef.current > 0) {
130+
const progress = Math.min(100, Math.round((downloadedBytesRef.current / totalBytesRef.current) * 100));
131+
setDownloadProgress(progress);
132+
} else {
133+
setDownloadProgress((prev) => (prev < 99 ? prev + 1 : prev));
134+
}
135+
} else if (event.event === 'Finished') {
136+
setDownloadProgress(100);
137+
setIsDownloading(false);
138+
setUpdateInfo(null);
139+
setStatusVariant('success');
140+
setStatusMessage('Update downloaded. Milo will restart once installation finishes.');
94141
}
95142
});
96143

97144
await updateManager.downloadAndInstall();
98145
} catch (error) {
99146
console.error('Failed to download update:', error);
100147
setIsDownloading(false);
148+
setStatusVariant('error');
149+
setStatusMessage(`Failed to download update. ${error instanceof Error ? error.message : ''}`.trim());
150+
} finally {
151+
downloadedBytesRef.current = 0;
101152
}
102153
};
103154

@@ -207,8 +258,16 @@ export function InfoPage() {
207258
</div>
208259
)}
209260

210-
{statusMessage && !updateInfo && !isDownloading && (
211-
<div className="p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg text-xs text-green-800 dark:text-green-200">
261+
{statusMessage && (
262+
<div
263+
className={`p-3 rounded-lg text-xs ${
264+
statusVariant === 'error'
265+
? 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200'
266+
: statusVariant === 'success'
267+
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-800 dark:text-green-200'
268+
: 'bg-background-secondary/80 border border-border-primary text-text-secondary'
269+
}`}
270+
>
212271
{statusMessage}
213272
</div>
214273
)}

src/utils/updater.ts

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { check } from '@tauri-apps/plugin-updater';
1+
import { invoke } from '@tauri-apps/api/core';
2+
import { check, type DownloadEvent, type Update } from '@tauri-apps/plugin-updater';
23

34
export interface UpdateInfo {
45
version: string;
@@ -8,50 +9,85 @@ export interface UpdateInfo {
89
}
910

1011
export class UpdateManager {
11-
private onProgressCallback?: (event: any) => void;
12+
private onProgressCallback?: (event: DownloadEvent) => void;
13+
private currentUpdate: Update | null = null;
1214

13-
setProgressCallback(callback: (event: any) => void) {
15+
setProgressCallback(callback: (event: DownloadEvent) => void) {
1416
this.onProgressCallback = callback;
1517
}
1618

1719
async checkForUpdates(): Promise<UpdateInfo | null> {
1820
try {
1921
const update = await check();
2022
if (update?.available) {
23+
this.currentUpdate = update;
2124
return {
2225
version: update.version,
2326
currentVersion: update.currentVersion,
2427
body: update.body,
2528
date: update.date
2629
};
2730
}
31+
this.currentUpdate = null;
2832
return null;
2933
} catch (error) {
3034
console.error('Failed to check for updates:', error);
3135
throw new Error('Failed to check for updates');
3236
}
3337
}
3438

39+
private async ensureUpdate(): Promise<Update | null> {
40+
if (this.currentUpdate) {
41+
return this.currentUpdate;
42+
}
43+
44+
const update = await check();
45+
if (update?.available) {
46+
this.currentUpdate = update;
47+
return update;
48+
}
49+
50+
this.currentUpdate = null;
51+
return null;
52+
}
53+
3554
async downloadAndInstall(): Promise<void> {
55+
let update: Update | null = null;
3656
try {
37-
const update = await check();
38-
if (update?.available) {
39-
await update.downloadAndInstall((event) => {
40-
if (this.onProgressCallback) {
41-
this.onProgressCallback(event);
42-
}
43-
});
44-
45-
// The updater will handle the restart automatically
46-
// No need to manually call relaunch() in Tauri 2.0
47-
} else {
57+
update = await this.ensureUpdate();
58+
59+
if (!update) {
4860
throw new Error('No updates available');
4961
}
62+
63+
await update.download((event) => {
64+
console.debug('[Updater] download event', event);
65+
this.onProgressCallback?.(event);
66+
});
67+
68+
// Ensure UI reaches 100% even if the plugin's Finished event is skipped.
69+
this.onProgressCallback?.({ event: 'Finished' } as DownloadEvent);
70+
71+
await update.install();
72+
73+
await update.close();
74+
update = null;
75+
this.currentUpdate = null;
76+
77+
await invoke('relaunch_app');
5078
} catch (error) {
5179
console.error('Failed to download and install update:', error);
5280
throw error;
81+
} finally {
82+
if (update) {
83+
try {
84+
await update.close();
85+
} catch (closeError) {
86+
console.warn('Failed to close update resource:', closeError);
87+
}
88+
}
5389
}
5490
}
5591
}
5692

57-
export const updateManager = new UpdateManager();
93+
export const updateManager = new UpdateManager();

0 commit comments

Comments
 (0)