feat: implement extension installation functionality

This commit adds the ability to install extensions from the UI. It introduces a new `install_extension` command that downloads and extracts the extension files into the local plugins directory. The UI is updated to reflect the installation process, including a loading state for the install button.
This commit is contained in:
ByteAtATime 2025-06-18 12:51:46 -07:00
parent 03b7e44ee7
commit b9259147ba
No known key found for this signature in database
5 changed files with 675 additions and 11 deletions

View file

@ -7,9 +7,15 @@ mod error;
use arboard;
use crate::{app::App, cache::AppCache};
use selection::get_text;
use std::{process::Command, thread, time::Duration};
use std::fs;
use std::io::{self, Cursor};
use std::path::PathBuf;
use std::process::Command;
use std::thread;
use std::time::Duration;
#[cfg(target_os = "linux")]
use std::path::Path;
use tauri::Manager;
#[cfg(target_os = "linux")]
use url::Url;
#[cfg(target_os = "linux")]
@ -309,6 +315,113 @@ fn get_selected_text() -> String {
get_text()
}
#[tauri::command]
async fn install_extension(
app: tauri::AppHandle,
download_url: String,
slug: String,
) -> Result<(), String> {
let data_dir = app
.path()
.app_local_data_dir()
.or_else(|_| Err("Failed to get app local data dir".to_string()))?;
let plugins_dir = data_dir.join("plugins");
let extension_dir = plugins_dir.join(&slug);
if !plugins_dir.exists() {
fs::create_dir_all(&plugins_dir).map_err(|e| e.to_string())?;
}
if extension_dir.exists() {
fs::remove_dir_all(&extension_dir).map_err(|e| e.to_string())?;
}
let response = reqwest::get(&download_url)
.await
.map_err(|e| format!("Failed to download extension: {}", e))?;
if !response.status().is_success() {
return Err(format!(
"Failed to download extension: status code {}",
response.status()
));
}
let content = response
.bytes()
.await
.map_err(|e| format!("Failed to read response bytes: {}", e))?;
let mut archive = zip::ZipArchive::new(Cursor::new(content)).map_err(|e| e.to_string())?;
let prefix_to_strip = {
let file_names: Vec<PathBuf> = archive.file_names().map(PathBuf::from).collect();
if file_names.len() <= 1 {
None
} else {
let first_path = &file_names[0];
if let Some(first_component) = first_path.components().next() {
if file_names.iter().all(|path| path.starts_with(first_component)) {
Some(PathBuf::from(first_component.as_os_str()))
} else {
None
}
} else {
None
}
}
};
for i in 0..archive.len() {
let mut file = archive.by_index(i).map_err(|e| e.to_string())?;
let enclosed_path = match file.enclosed_name() {
Some(path) => path.to_path_buf(),
None => continue,
};
let final_path_part = if let Some(ref prefix) = prefix_to_strip {
enclosed_path
.strip_prefix(prefix)
.unwrap_or(&enclosed_path)
.to_path_buf()
} else {
enclosed_path
};
if final_path_part.as_os_str().is_empty() {
continue;
}
let outpath = extension_dir.join(final_path_part);
if file.name().ends_with('/') {
fs::create_dir_all(&outpath).map_err(|e| e.to_string())?;
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(&p).map_err(|e| e.to_string())?;
}
}
let mut outfile = fs::File::create(&outpath).map_err(|e| e.to_string())?;
io::copy(&mut file, &mut outfile).map_err(|e| e.to_string())?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))
.map_err(|e| e.to_string())?;
}
}
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@ -319,7 +432,8 @@ pub fn run() {
get_installed_apps,
launch_app,
get_selected_text,
get_selected_finder_items
get_selected_finder_items,
install_extension
])
.setup(|_app| {
thread::spawn(|| {
@ -333,4 +447,4 @@ pub fn run() {
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
}