diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 9afbcc6..4db3b1d 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -32,6 +32,21 @@ }, { "path": "$HOME/.ezpplauncher/**/*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*/**" + }, + { + "path": "$TEMP" + }, + { + "path": "$TEMP/*" + }, + { + "path": "$TEMP/**/*" } ] }, @@ -46,6 +61,21 @@ }, { "path": "$HOME/.ezpplauncher/**/*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*/**" + }, + { + "path": "$TEMP" + }, + { + "path": "$TEMP/*" + }, + { + "path": "$TEMP/**/*" } ] }, @@ -60,6 +90,21 @@ }, { "path": "$HOME/.ezpplauncher/**/*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*/**" + }, + { + "path": "$TEMP" + }, + { + "path": "$TEMP/*" + }, + { + "path": "$TEMP/**/*" } ] }, @@ -74,6 +119,21 @@ }, { "path": "$HOME/.ezpplauncher/**/*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*/**" + }, + { + "path": "$TEMP" + }, + { + "path": "$TEMP/*" + }, + { + "path": "$TEMP/**/*" } ] }, @@ -88,6 +148,21 @@ }, { "path": "$HOME/.ezpplauncher/**/*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*/**" + }, + { + "path": "$TEMP" + }, + { + "path": "$TEMP/*" + }, + { + "path": "$TEMP/**/*" } ] }, @@ -102,6 +177,21 @@ }, { "path": "$HOME/.ezpplauncher/**/*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*/**" + }, + { + "path": "$TEMP" + }, + { + "path": "$TEMP/*" + }, + { + "path": "$TEMP/**/*" } ] }, @@ -116,6 +206,21 @@ }, { "path": "$HOME/.ezpplauncher/**/*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*/**" + }, + { + "path": "$TEMP" + }, + { + "path": "$TEMP/*" + }, + { + "path": "$TEMP/**/*" } ] }, @@ -130,6 +235,21 @@ }, { "path": "$HOME/.ezpplauncher/**/*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*/**" + }, + { + "path": "$TEMP" + }, + { + "path": "$TEMP/*" + }, + { + "path": "$TEMP/**/*" } ] }, @@ -144,6 +264,21 @@ }, { "path": "$HOME/.ezpplauncher/**/*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*" + }, + { + "path": "$HOME/.ezpplauncher/**/.*/**" + }, + { + "path": "$TEMP" + }, + { + "path": "$TEMP/*" + }, + { + "path": "$TEMP/**/*" } ] } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index a81282a..a253608 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1552,6 +1552,120 @@ pub async fn download_and_extract_theme( Ok(()) } +#[tauri::command] +pub async fn download_and_extract_community_theme( + app: AppHandle, + download_url: String, + theme_folder: String, + theme_name: String, +) -> Result<(), String> { + let client = reqwest::Client::new(); + let response = client + .get(&download_url) + .header( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + ) + .send() + .await + .map_err(|e| format!("Failed to download theme: {}", e))?; + + if !response.status().is_success() { + return Err(format!("Download failed (HTTP {})", response.status())); + } + + let total = response.content_length().unwrap_or(0); + let mut received: u64 = 0; + let mut chunks: Vec = Vec::new(); + + let mut stream = response.bytes_stream(); + while let Some(chunk) = futures_util::StreamExt::next(&mut stream).await { + let chunk = chunk.map_err(|e| format!("Failed to read chunk: {}", e))?; + received += chunk.len() as u64; + chunks.extend_from_slice(&chunk); + let _ = app.emit( + "download_progress", + DownloadProgress { + theme_name: theme_name.clone(), + received, + total, + progress: if total > 0 { + received as f64 / total as f64 + } else { + 0.0 + }, + }, + ); + } + + // Parse zip on blocking thread + let entries: Vec<(String, Vec, bool)> = tokio::task::spawn_blocking(move || { + let cursor = std::io::Cursor::new(chunks); + let mut archive = zip::ZipArchive::new(cursor) + .map_err(|e| format!("Failed to read zip archive: {}", e))?; + + let mut entries = Vec::new(); + for i in 0..archive.len() { + let mut zip_entry = archive + .by_index(i) + .map_err(|e| format!("Failed to read zip entry: {}", e))?; + + let name = match zip_entry.enclosed_name() { + Some(p) => p.to_string_lossy().to_string(), + None => continue, + }; + + let is_dir = zip_entry.is_dir(); + let mut buf = Vec::new(); + if !is_dir { + std::io::copy(&mut zip_entry, &mut buf) + .map_err(|e| format!("Failed to read zip entry data: {}", e))?; + } + entries.push((name, buf, is_dir)); + } + + Ok::<_, String>(entries) + }) + .await + .map_err(|e| format!("Thread error: {}", e))??; + + let total_files = entries.len(); + let mut extracted = 0; + + for (name, buf, is_dir) in entries { + let entry_path = std::path::Path::new(&theme_folder).join(&name); + + if is_dir { + tokio::fs::create_dir_all(&entry_path) + .await + .map_err(|e| format!("Failed to create directory: {}", e))?; + } else { + if let Some(parent) = entry_path.parent() { + tokio::fs::create_dir_all(parent) + .await + .map_err(|e| format!("Failed to create parent directory: {}", e))?; + } + tokio::fs::write(&entry_path, &buf) + .await + .map_err(|e| format!("Failed to write file: {}", e))?; + } + + extracted += 1; + let _ = app.emit( + "extract_progress", + ExtractProgress { + theme_name: theme_name.clone(), + total: total_files, + extracted, + progress: extracted as f64 / total_files as f64, + current_file: name, + }, + ); + } + + Ok(()) +} + #[tauri::command] pub async fn extract_theme( app: AppHandle, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 18573da..3e530a5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -19,7 +19,7 @@ pub mod utils; use crate::{ commands::{ backup_theme_folder, check_for_corruption, close_presence_editor, close_theme_editor, config_all, config_clear, config_delete, - config_exists, config_get, config_set, download_and_extract_theme, + config_exists, config_get, config_set, download_and_extract_community_theme, download_and_extract_theme, download_ezpp_launcher_update, download_ezpp_launcher_update_files, encrypt_string, exit, export_theme, extract_theme, file_size, find_osu_installation, focus_presence_editor, focus_theme_editor, get_beatmapsets_count, get_ezpp_launcher_update_files, get_hwid, get_launcher_version, get_osu_release_stream, @@ -192,6 +192,7 @@ pub fn run() { restore_theme_backup, extract_theme, download_and_extract_theme, + download_and_extract_community_theme, save_theme_project, export_theme, hash_file_sha256, diff --git a/src/lib/communityThemes.ts b/src/lib/communityThemes.ts index 1b25685..dee432e 100644 --- a/src/lib/communityThemes.ts +++ b/src/lib/communityThemes.ts @@ -1,4 +1,5 @@ import { invoke } from '@tauri-apps/api/core'; +import { listen } from '@tauri-apps/api/event'; import * as path from '@tauri-apps/api/path'; import * as fs from '@tauri-apps/plugin-fs'; import { config } from './config'; @@ -424,7 +425,7 @@ export async function publishThemeToWorkshop(options: { onProgress('Packaging theme archive...', 5); const baseThemesDir = await themesDirectory(); - const tmpDir = await path.join(baseThemesDir, '.tmp'); + const tmpDir = await path.join(baseThemesDir, 'export_temp'); if (!(await fs.exists(tmpDir))) { await fs.mkdir(tmpDir, { recursive: true }); } @@ -555,53 +556,41 @@ export async function downloadAndInstallCommunityTheme( reportProgress('Downloading theme package...', 10); - let res: Response; - try { - res = await fetch(theme.downloadUrl); - if (!res.ok && (theme.apiVersion === '1.0' || theme.tags?.includes('legacy'))) { - res = await fetch( - `https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/${theme.id}.ezpplauncher-theme`, - { headers: { 'User-Agent': 'EZPPLauncher' } } - ); + const unlistenDownload = await listen<{ received: number; total: number; progress: number }>( + 'download_progress', + (event) => { + reportProgress('Downloading theme package...', Math.round(10 + event.payload.progress * 50)); } - } catch (err) { - if (theme.apiVersion === '1.0' || theme.tags?.includes('legacy')) { - res = await fetch( - `https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/${theme.id}.ezpplauncher-theme`, - { headers: { 'User-Agent': 'EZPPLauncher' } } - ); - } else { - throw err; + ); + + const unlistenExtract = await listen<{ extracted: number; total: number; progress: number }>( + 'extract_progress', + (event) => { + reportProgress('Extracting theme files...', Math.round(60 + event.payload.progress * 38)); } - } - - if (!res.ok) { - throw new Error(`Download failed (HTTP ${res.status})`); - } - - const tmpDir = await path.join(baseThemesDir, '.tmp'); - if (!(await fs.exists(tmpDir))) { - await fs.mkdir(tmpDir, { recursive: true }); - } - - const tempFile = await path.join(tmpDir, `${folderName}_download.ezpplauncher-theme`); + ); try { - const arrayBuffer = await res.arrayBuffer(); - await fs.writeFile(tempFile, new Uint8Array(arrayBuffer)); - - reportProgress('Extracting theme files...', 70); - - if (!(await fs.exists(targetFolder))) { - await fs.mkdir(targetFolder, { recursive: true }); + try { + await invoke('download_and_extract_community_theme', { + downloadUrl: theme.downloadUrl, + themeFolder: targetFolder, + themeName: theme.name, + }); + } catch (err) { + // Fallback for legacy themes directly to git repo + if (theme.apiVersion === '1.0' || theme.tags?.includes('legacy')) { + const fallbackUrl = `https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/${theme.id}.ezpplauncher-theme`; + await invoke('download_and_extract_community_theme', { + downloadUrl: fallbackUrl, + themeFolder: targetFolder, + themeName: theme.name, + }); + } else { + throw err; + } } - await invoke('extract_theme', { - filePath: tempFile, - themeFolder: targetFolder, - themeName: theme.name, - }); - reportProgress('Theme installed!', 100); // Refresh themes in launcher state @@ -609,8 +598,7 @@ export async function downloadAndInstallCommunityTheme( return { success: true, folderName }; } finally { - if (await fs.exists(tempFile)) { - await fs.remove(tempFile).catch(() => {}); - } + unlistenDownload(); + unlistenExtract(); } }