feat: pré-visualização completa, tema, nova identidade e correções (0.2.0)
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
- Preview de imagem, vídeo/MP4, áudio, PDF (pdf.js), texto, Office/ODF, ZIP e hex, lido sob demanda com Range; tela cheia com navegação por teclado. - Seletor de tema Auto/Claro/Escuro persistido (vale também como administrador). - Identidade verde-azulada com logo de pixels que formam um lobo. - Corrige reinício como administrador no AppImage (usa $APPIMAGE) e mostra o motivo. - Corrige barra inferior piscando (render idempotente, barra criada uma vez). - Corrige FAT32 em mídias pequenas não reconhecido. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
510f1bc48e
commit
c5c94a9956
32 files changed
+1666
-227
No files matched your search
+125
-21
@@ -16,40 +16,144 @@ async fn rpc(state: tauri::State<'_, State>, method: String, params: serde_json:
|
||||
}
|
||||
|
||||
/// Reabre o app com privilégios de administrador (Linux/macOS). No Windows o
|
||||
/// manifesto já exige elevaçã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.
|
||||
#[tauri::command]
|
||||
fn relaunch_elevated() -> Result<(), String> {
|
||||
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
|
||||
#[cfg(target_os = "linux")]
|
||||
fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
{
|
||||
let mut cmd = std::process::Command::new("pkexec");
|
||||
cmd.arg("env");
|
||||
for k in ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS"] {
|
||||
if let Ok(v) = std::env::var(k) {
|
||||
cmd.arg(format!("{k}={v}"));
|
||||
use tauri::Emitter;
|
||||
let exe = elevated_exe()?;
|
||||
let cfg = guara_core::settings::config_dir();
|
||||
let log = std::env::temp_dir().join(format!("guara-elevate-{}.log", std::process::id()));
|
||||
let _ = std::fs::write(&log, b"");
|
||||
let out = std::fs::OpenOptions::new().append(true).open(&log).map_err(|e| e.to_string())?;
|
||||
let err = out.try_clone().map_err(|e| e.to_string())?;
|
||||
let pid = std::process::id().to_string();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let mut cmd = {
|
||||
let mut c = std::process::Command::new("pkexec");
|
||||
c.arg("/usr/bin/env");
|
||||
for k in ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS", "GDK_BACKEND"] {
|
||||
if let Ok(v) = std::env::var(k) {
|
||||
c.arg(format!("{k}={v}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd.arg(exe);
|
||||
cmd.spawn().map_err(|e| format!("pkexec indisponível: {e}"))?;
|
||||
std::process::exit(0);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let script = format!("do shell script \"'{}' &> /dev/null &\" with administrator privileges", exe.display());
|
||||
std::process::Command::new("osascript").args(["-e", &script]).spawn().map_err(|e| e.to_string())?;
|
||||
std::process::exit(0);
|
||||
c.arg(format!("GUARA_CONFIG_DIR={}", cfg.display()));
|
||||
c.arg(format!("GUARA_REPLACE_PID={pid}"));
|
||||
c.arg(&exe);
|
||||
c
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut cmd = {
|
||||
let shell = format!(
|
||||
"GUARA_CONFIG_DIR='{}' GUARA_REPLACE_PID={} '{}' >/dev/null 2>&1 &",
|
||||
cfg.display(),
|
||||
pid,
|
||||
exe.display()
|
||||
);
|
||||
let script = format!("do shell script \"{}\" with administrator privileges", shell.replace('\\', "\\\\").replace('"', "\\\""));
|
||||
let mut c = std::process::Command::new("osascript");
|
||||
c.args(["-e", &script]);
|
||||
c
|
||||
};
|
||||
cmd.stdout(out).stderr(err);
|
||||
let mut child = cmd.spawn().map_err(|e| format!("não foi possível pedir privilégios: {e}"))?;
|
||||
std::thread::spawn(move || {
|
||||
// No Linux o pkexec só termina com sucesso quando o app elevado é fechado (e o app
|
||||
// elevado já terá encerrado este processo). Qualquer falha antes disso vira aviso.
|
||||
let status = child.wait();
|
||||
if matches!(&status, Ok(s) if s.success()) {
|
||||
let _ = std::fs::remove_file(&log);
|
||||
return;
|
||||
}
|
||||
let detail = std::fs::read_to_string(&log).unwrap_or_default();
|
||||
let msg = match status.ok().and_then(|s| s.code()) {
|
||||
Some(126) => "Autorização cancelada.".to_string(),
|
||||
Some(127) => "Não foi possível autenticar como administrador.".to_string(),
|
||||
_ => format!("Não foi possível reiniciar como administrador. {}", detail.lines().last().unwrap_or("").trim()),
|
||||
};
|
||||
let _ = app.emit("elevate-failed", msg);
|
||||
let _ = std::fs::remove_file(&log);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = exe;
|
||||
let _ = app;
|
||||
Err("o Guará já roda como administrador no Windows".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Caminho que o root consegue executar. No AppImage, `current_exe` aponta para
|
||||
/// dentro de um ponto de montagem FUSE do usuário, inacessível ao root: usa o
|
||||
/// próprio arquivo `.AppImage` (variável `APPIMAGE`).
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn elevated_exe() -> Result<std::path::PathBuf, String> {
|
||||
if let Some(p) = std::env::var_os("APPIMAGE").map(std::path::PathBuf::from).filter(|p| p.is_file()) {
|
||||
return Ok(p);
|
||||
}
|
||||
std::env::current_exe().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Serve os arquivos recuperados para pré-visualização (`guara://localhost/file/<job>/<id>`),
|
||||
/// com suporte a Range para vídeo/áudio/PDF, lendo direto da origem.
|
||||
fn serve_file(eng: &Engine, req: &tauri::http::Request<Vec<u8>>) -> tauri::http::Response<Vec<u8>> {
|
||||
use tauri::http::Response;
|
||||
const CAP: u64 = 8 * 1024 * 1024;
|
||||
let mut it = req.uri().path().trim_start_matches("/file/").split('/');
|
||||
let (job, id) = (it.next().unwrap_or(""), it.next().and_then(|s| s.parse::<u64>().ok()));
|
||||
let err = |code: u16| Response::builder().status(code).header("Access-Control-Allow-Origin", "*").body(Vec::new()).unwrap();
|
||||
let Some(id) = id else { return err(400) };
|
||||
let range = req.headers().get("range").and_then(|v| v.to_str().ok());
|
||||
let slice = match eng.file_slice(job, id, range) {
|
||||
Ok(s) => s,
|
||||
Err(code) => return err(code),
|
||||
};
|
||||
let total = slice.total;
|
||||
let start = slice.start;
|
||||
let want = slice.len();
|
||||
let mut headers = slice.headers();
|
||||
let partial = slice.partial || want > CAP;
|
||||
let Ok(data) = slice.read_all(CAP) else { return err(500) };
|
||||
let mut b = Response::builder().status(if partial { 206 } else { 200 });
|
||||
headers.retain(|(k, _)| *k != "Content-Range");
|
||||
for (k, v) in &headers {
|
||||
b = b.header(*k, v);
|
||||
}
|
||||
if partial && !data.is_empty() {
|
||||
b = b.header("Content-Range", format!("bytes {}-{}/{}", start, start + data.len() as u64 - 1, total));
|
||||
}
|
||||
b.body(data).unwrap_or_else(|_| err(500))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let engine = Arc::new(Engine::new());
|
||||
let for_protocol = engine.clone();
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.manage(State(Arc::new(Engine::new())))
|
||||
.manage(State(engine))
|
||||
.register_asynchronous_uri_scheme_protocol("guara", move |_ctx, request, responder| {
|
||||
let eng = for_protocol.clone();
|
||||
std::thread::spawn(move || responder.respond(serve_file(&eng, &request)));
|
||||
})
|
||||
.setup(|_app| {
|
||||
// Quando abrimos elevados a pedido do app normal, ele fecha o app antigo.
|
||||
#[cfg(unix)]
|
||||
if let Some(pid) = std::env::var("GUARA_REPLACE_PID").ok().and_then(|p| p.parse::<i32>().ok()) {
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(1200));
|
||||
// SAFETY: só envia SIGTERM ao processo anterior do próprio usuário.
|
||||
unsafe {
|
||||
if libc::geteuid() == 0 {
|
||||
libc::kill(pid, libc::SIGTERM);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![rpc, relaunch_elevated])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("erro ao iniciar o Guará");
|
||||
|
||||
Reference in new issue
Block a user