Atualização pelo próprio app (Tauri updater) com cartão de aviso
Ao abrir, o app consulta o latest.json da release no GitHub; havendo versão nova, um cartão oferece Atualizar agora: baixa, confere a assinatura com a chave pública embutida, instala e reabre (AppImage substituído no lugar; Windows e macOS pelo instalador). Instalação por .deb ou .rpm só avisa. Configurações ganha a opção de desligar a consulta e o botão Verificar agora. A release assina os pacotes com a chave guardada no ambiente 'release' do GitHub (só tags v*) e o latest.json é montado de uma vez por scripts/make-latest-json.py, depois que todos os jobs de empacotamento terminam. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
17b0e89e03
commit
daa12ba117
20 files changed
+341
-7
No files matched your search
@@ -17,6 +17,8 @@ tauri-build = { version = "2", features = [] }
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
guara-core = { path = "../../../crates/guara-core" }
|
||||
|
||||
|
||||
@@ -15,6 +15,73 @@ async fn rpc(state: tauri::State<'_, State>, method: String, params: serde_json:
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
/// Versão nova encontrada na release do GitHub.
|
||||
#[derive(serde::Serialize)]
|
||||
struct UpdateInfo {
|
||||
current: String,
|
||||
version: String,
|
||||
notes: String,
|
||||
/// O app consegue se atualizar sozinho (AppImage, Windows e macOS). Instalação por pacote
|
||||
/// (.deb/.rpm) só avisa e leva à página da release.
|
||||
auto: bool,
|
||||
url: String,
|
||||
}
|
||||
|
||||
fn self_update_possible() -> bool {
|
||||
!cfg!(target_os = "linux") || std::env::var_os("APPIMAGE").is_some()
|
||||
}
|
||||
|
||||
/// Procura uma versão mais nova. `None` = já está na última.
|
||||
#[tauri::command]
|
||||
async fn update_check(app: tauri::AppHandle) -> Result<Option<UpdateInfo>, String> {
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
let found = app.updater().map_err(|e| e.to_string())?.check().await.map_err(|e| e.to_string())?;
|
||||
Ok(found.map(|u| UpdateInfo {
|
||||
current: u.current_version.clone(),
|
||||
notes: u.body.clone().unwrap_or_default(),
|
||||
url: format!("https://github.com/lucasrguerra/guara/releases/tag/v{}", u.version),
|
||||
version: u.version,
|
||||
auto: self_update_possible(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Baixa, confere a assinatura (chave pública embutida no app), instala e reabre o app.
|
||||
/// O progresso sai no evento `update-progress`.
|
||||
#[tauri::command]
|
||||
async fn update_install(app: tauri::AppHandle) -> Result<(), String> {
|
||||
use tauri::Emitter;
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
if !self_update_possible() {
|
||||
return Err("Esta instalação não se atualiza sozinha (pacote do sistema). Baixe a versão nova na página da release.".into());
|
||||
}
|
||||
let update = app
|
||||
.updater()
|
||||
.map_err(|e| e.to_string())?
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or("Já está na versão mais recente.")?;
|
||||
let (a, b) = (app.clone(), app.clone());
|
||||
let mut got = 0u64;
|
||||
update
|
||||
.download_and_install(
|
||||
move |chunk, total| {
|
||||
got += chunk as u64;
|
||||
let _ = a.emit("update-progress", serde_json::json!({ "downloaded": got, "total": total }));
|
||||
},
|
||||
move || {
|
||||
let _ = b.emit("update-progress", serde_json::json!({ "installing": true }));
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Não foi possível atualizar: {e}"))?;
|
||||
// Rodando como administrador, o arquivo novo nasce do root: devolve ao usuário que abriu o app.
|
||||
if let Some(p) = std::env::var_os("APPIMAGE") {
|
||||
guara_core::owner::give_to_user(std::path::Path::new(&p));
|
||||
}
|
||||
app.restart()
|
||||
}
|
||||
|
||||
/// Reabre o app com privilégios de administrador (Linux/macOS). No Windows o
|
||||
/// manifesto já exige elevação. O app novo encerra este quando abre; se a
|
||||
/// autorização falhar, a janela continua aqui e a interface mostra o motivo.
|
||||
@@ -155,6 +222,7 @@ fn main() {
|
||||
let for_protocol = engine.clone();
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.manage(State(engine))
|
||||
.register_asynchronous_uri_scheme_protocol("guara", move |_ctx, request, responder| {
|
||||
let eng = for_protocol.clone();
|
||||
@@ -176,7 +244,7 @@ fn main() {
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![rpc, relaunch_elevated])
|
||||
.invoke_handler(tauri::generate_handler![rpc, relaunch_elevated, update_check, update_install])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("erro ao iniciar o Guará");
|
||||
}
|
||||
@@ -34,6 +34,18 @@
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
],
|
||||
"createUpdaterArtifacts": true
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDhENzRCRUU5NEI3QTAxQzgKUldUSUFYcEw2YjUwamFzMnpXM3ZKTEZPN1JIeXdLL3Q5SEF1M1FxQ284WFM0UWNsdlAyNUloWTEK",
|
||||
"endpoints": [
|
||||
"https://github.com/lucasrguerra/guara/releases/latest/download/latest.json"
|
||||
],
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -518,3 +518,12 @@ dialog.lightbox[open] { display: grid; }
|
||||
|
||||
/* logo inline: segue o tema escolhido no app, não o do sistema */
|
||||
.logo .a { stroke: var(--accent); } .logo .b { stroke: var(--accent-2); } .logo .t { fill: var(--text); }
|
||||
|
||||
/* atualização do app */
|
||||
.update-card { position: fixed; right: 18px; bottom: 18px; z-index: 60; width: min(400px, calc(100vw - 36px)); display: grid; gap: 10px; padding: 16px 18px; border-radius: 18px; background: var(--surface); box-shadow: var(--shadow); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); }
|
||||
.update-card .upd-head { display: grid; gap: 2px; }
|
||||
.update-card .upd-ver { font-size: 12.5px; color: var(--text-2); }
|
||||
.update-card .upd-url { word-break: break-all; font-size: 12.5px; }
|
||||
.update-card .upd-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.update-card .sub { margin: 0; }
|
||||
pre.upd-notes { max-height: 50vh; overflow: auto; white-space: pre-wrap; font-size: 13px; }
|
||||
+64
-1
@@ -53,7 +53,7 @@ document.querySelectorAll('[data-theme-pick]').forEach((b) => (b.onclick = async
|
||||
|
||||
// ------------------------------------------------------------------ configurações
|
||||
// (declarado antes do uso: `S` é definido mais abaixo, por isso o objeto fica aqui)
|
||||
const SETTINGS = { theme: 'auto', lang: 'auto', ui_lang: null, notify: true, maps: false };
|
||||
const SETTINGS = { theme: 'auto', lang: 'auto', ui_lang: null, notify: true, maps: false, updates: true };
|
||||
async function saveSettings(patch) {
|
||||
Object.assign(SETTINGS, patch);
|
||||
try { await rpc('settings_set', SETTINGS); } catch { /* segue só com o armazenamento local */ }
|
||||
@@ -745,11 +745,14 @@ $('#settingsBtn')?.addEventListener('click', () => {
|
||||
<label class="fld"><span>Idioma</span><select class="select" name="lang"><option value="auto">Auto</option><option value="pt">Português</option><option value="en">English</option></select></label>
|
||||
<label class="check"><input type="checkbox" name="notify" ${SETTINGS.notify ? 'checked' : ''}><span><b>Notificações</b><br><span class="sub">Avisar quando uma tarefa longa terminar</span></span></label>
|
||||
<label class="check"><input type="checkbox" name="maps" ${SETTINGS.maps ? 'checked' : ''}><span><b>Mapas online</b><br><span class="sub">Mostrar mapas das fotos com GPS (usa a internet: baixa blocos do OpenStreetMap)</span></span></label>
|
||||
${tauri?.core?.invoke ? `<label class="check"><input type="checkbox" name="updates" ${SETTINGS.updates ? 'checked' : ''}><span><b>Atualizações automáticas</b><br><span class="sub">Ao abrir, consulta o GitHub para ver se há versão nova (uma consulta, sem enviar dados seus)</span><br><button type="button" class="btn sm" id="updNow">Verificar agora</button></span></label>` : ''}
|
||||
<menu><button class="btn" value="cancel">Cancelar</button><button class="btn primary" value="ok">Salvar</button></menu></form>`;
|
||||
d.querySelector('[name=lang]').value = SETTINGS.lang;
|
||||
d.querySelector('#updNow')?.addEventListener('click', (e) => { e.preventDefault(); checkUpdate(true); });
|
||||
d.onclose = () => {
|
||||
if (d.returnValue !== 'ok') return;
|
||||
const patch = { lang: d.querySelector('[name=lang]').value, notify: d.querySelector('[name=notify]').checked, maps: d.querySelector('[name=maps]').checked };
|
||||
if (d.querySelector('[name=updates]')) patch.updates = d.querySelector('[name=updates]').checked;
|
||||
setLang(patch.lang);
|
||||
patch.ui_lang = currentLang();
|
||||
saveSettings(patch);
|
||||
@@ -759,6 +762,64 @@ $('#settingsBtn')?.addEventListener('click', () => {
|
||||
d.showModal();
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ atualização do app
|
||||
// O núcleo do app (Tauri) consulta o `latest.json` da release no GitHub, confere a assinatura do
|
||||
// pacote com a chave pública embutida, instala e reabre. No navegador (`guara ui`) não há nada a fazer.
|
||||
let UPD = null;
|
||||
|
||||
async function checkUpdate(manual) {
|
||||
if (!tauri?.core?.invoke) return;
|
||||
try {
|
||||
UPD = await tauri.core.invoke('update_check');
|
||||
renderUpdateCard();
|
||||
if (manual) toast(UPD ? `Nova versão ${UPD.version} disponível` : 'Você já está na versão mais recente');
|
||||
} catch (e) {
|
||||
if (manual) toast(`Não foi possível verificar atualizações: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
function renderUpdateCard(state) {
|
||||
let el = $('#updateCard');
|
||||
if (!UPD) { el?.remove(); return; }
|
||||
if (!el) { el = document.createElement('div'); el.id = 'updateCard'; el.className = 'update-card'; el.setAttribute('role', 'status'); document.body.appendChild(el); }
|
||||
if (state === 'busy') return;
|
||||
el.innerHTML = `<div class="upd-head"><b>Nova versão do Guará disponível</b><span class="mono upd-ver">v${esc(UPD.current)} → v${esc(UPD.version)}</span></div>
|
||||
${UPD.auto ? '' : '<p class="sub">Esta instalação vem de um pacote do sistema e não se atualiza sozinha. Baixe a versão nova na página da release:</p><p class="mono upd-url">' + esc(UPD.url) + '</p>'}
|
||||
<div class="upd-actions">
|
||||
${UPD.auto ? '<button class="btn primary sm" id="updGo">Atualizar agora</button>' : ''}
|
||||
${UPD.notes ? '<button class="btn sm" id="updNotes">Novidades</button>' : ''}
|
||||
<button class="btn sm" id="updLater">Depois</button>
|
||||
</div>`;
|
||||
$('#updGo')?.addEventListener('click', installUpdate);
|
||||
$('#updLater')?.addEventListener('click', () => { UPD = null; el.remove(); });
|
||||
$('#updNotes')?.addEventListener('click', () => info({ title: `Guará ${UPD.version}`, html: `<pre class="upd-notes">${esc(UPD.notes)}</pre>` }));
|
||||
}
|
||||
|
||||
async function installUpdate() {
|
||||
const el = $('#updateCard');
|
||||
el.innerHTML = `<div class="upd-head"><b>Atualizando o Guará…</b></div>
|
||||
<div class="bar indet" id="updBar"><i style="width:0"></i></div><p class="sub" id="updMsg">Baixando a atualização…</p>`;
|
||||
renderUpdateCard('busy');
|
||||
try {
|
||||
await tauri.core.invoke('update_install');
|
||||
} catch (e) {
|
||||
toast(String(e));
|
||||
renderUpdateCard();
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgress(p) {
|
||||
const bar = $('#updBar'); const msg = $('#updMsg');
|
||||
if (!bar || !msg) return;
|
||||
if (p.installing) { bar.classList.add('indet'); msg.textContent = 'Instalando… o Guará vai reabrir sozinho.'; return; }
|
||||
if (p.total) {
|
||||
const pct = Math.min(100, Math.round((p.downloaded / p.total) * 100));
|
||||
bar.classList.remove('indet');
|
||||
bar.firstElementChild.style.width = `${pct}%`;
|
||||
msg.textContent = `Baixando… ${pct}%`;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ fechar com varredura em andamento
|
||||
function askClose() {
|
||||
return new Promise((resolve) => {
|
||||
@@ -807,6 +868,8 @@ function watchClose() {
|
||||
setLang(SETTINGS.lang);
|
||||
if (SETTINGS.ui_lang !== currentLang()) saveSettings({ ui_lang: currentLang() });
|
||||
watchClose();
|
||||
tauri?.event?.listen?.('update-progress', (e) => updateProgress(e.payload));
|
||||
if (SETTINGS.updates !== false) checkUpdate(false);
|
||||
tauri?.event?.listen?.('elevate-failed', (e) => { toast(String(e.payload || 'Não foi possível reiniciar como administrador')); });
|
||||
} catch (e) {
|
||||
main.innerHTML = `<div class="page"><div class="notice bad">${warnSvg}<div>Não foi possível falar com o núcleo do Guará: ${esc(e.message)}. Se abriu esta página manualmente, use o endereço completo exibido pelo comando <code>guara ui</code> (ele inclui o token).</div></div></div>`;
|
||||
|
||||
@@ -575,4 +575,19 @@ export const EN = {
|
||||
'{n} · {n}{n} · confiança {n} · {n} de {n}': '{n} · {n}{n} · confidence {n} · {n} of {n}',
|
||||
'e mais {n}…': 'and {n} more…',
|
||||
'abre no seu player: o navegador embutido do Linux não traz os codecs e travaria a janela.': 'opens in your player: the embedded Linux browser does not include the codecs and would freeze the window.',
|
||||
'Nova versão do Guará disponível': 'New Guará version available',
|
||||
'Atualizar agora': 'Update now',
|
||||
'Novidades': 'What\'s new',
|
||||
'Depois': 'Later',
|
||||
'Atualizando o Guará…': 'Updating Guará…',
|
||||
'Baixando a atualização…': 'Downloading the update…',
|
||||
'Baixando… {n}%': 'Downloading… {n}%',
|
||||
'Instalando… o Guará vai reabrir sozinho.': 'Installing… Guará will reopen by itself.',
|
||||
'Esta instalação vem de um pacote do sistema e não se atualiza sozinha. Baixe a versão nova na página da release:': 'This install comes from a system package and cannot update itself. Download the new version from the release page:',
|
||||
'Atualizações automáticas': 'Automatic updates',
|
||||
'Ao abrir, consulta o GitHub para ver se há versão nova (uma consulta, sem enviar dados seus)': 'On startup, checks GitHub for a new version (one request, nothing of yours is sent)',
|
||||
'Verificar agora': 'Check now',
|
||||
'Nova versão {n} disponível': 'New version {n} available',
|
||||
'Você já está na versão mais recente': 'You are already on the latest version',
|
||||
'Não foi possível verificar atualizações: {n}': 'Could not check for updates: {n}',
|
||||
};
|
||||
Reference in new issue
Block a user