From 1febea2f685ce36048f3b58aee8416a08355e132 Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Tue, 21 Apr 2026 21:27:58 +0300 Subject: [PATCH] feat: update notification for game update --- src-tauri/src/lib.rs | 34 +++++++++++++++++++++++++++++++-- src/context/LauncherContext.tsx | 2 +- src/hooks/useGameManager.ts | 25 ++++++++++++++++++++++++ src/pages/App.tsx | 11 +++++++++++ src/services/TauriService.ts | 4 ++++ 5 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8ab9a60..4b72b01 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -670,7 +670,7 @@ async fn download_and_install(app: AppHandle, state: State<'_, DownloadState>, u let file_name = entry.file_name(); let name_str = file_name.to_string_lossy(); - let keep_list = ["Windows64", "Windows64Media", "uid.dat", "username.txt", "settings.dat", "servers.dat", "servers.txt", "server.properties", "options.txt", "servers.db", "workshop_files.json", "screenshots"]; + let keep_list = ["Windows64", "Windows64Media", "uid.dat", "username.txt", "settings.dat", "servers.dat", "servers.txt", "server.properties", "options.txt", "servers.db", "workshop_files.json", "screenshots", "update_timestamp.txt"]; let entry_path_str = entry.path().to_string_lossy().to_string(); let is_workshop_file = workshop_files.iter().any(|wf| entry_path_str.starts_with(wf) || wf.starts_with(&entry_path_str)); if !keep_list.contains(&name_str.as_ref()) && !is_workshop_file { @@ -691,6 +691,14 @@ async fn download_and_install(app: AppHandle, state: State<'_, DownloadState>, u return Err(format!("Download failed: {}", response.status())); } + let last_modified = response.headers().get(reqwest::header::LAST_MODIFIED) + .and_then(|h| h.to_str().ok()) + .unwrap_or("") + .to_string(); + if !last_modified.is_empty() { + let _ = fs::write(instance_dir.join("update_timestamp.txt"), last_modified); + } + let total_size = response.content_length().unwrap_or(0) as f64; let mut file = fs::File::create(&zip_path).map_err(|e| e.to_string())?; let mut downloaded = 0.0; @@ -1107,6 +1115,28 @@ fn workshop_list_installed(app: AppHandle) -> Vec { result } +#[tauri::command] +async fn check_game_update(app: AppHandle, instance_id: String, url: String) -> Result { + let instance_dir = get_app_dir(&app).join("instances").join(&instance_id); + let timestamp_file = instance_dir.join("update_timestamp.txt"); + + let local_timestamp = fs::read_to_string(×tamp_file).unwrap_or_default(); + if local_timestamp.is_empty() { + return Ok(false); + } + + let response = reqwest::Client::new().head(&url).send().await.map_err(|e| e.to_string())?; + if let Some(remote_header) = response.headers().get(reqwest::header::LAST_MODIFIED) { + if let Ok(remote_timestamp) = remote_header.to_str() { + if remote_timestamp != local_timestamp { + return Ok(true); + } + } + } + + Ok(false) +} + #[tauri::command] #[allow(non_snake_case)] async fn launch_game(app: AppHandle, state: State<'_, GameState>, instance_id: String, servers: Vec) -> Result<(), String> { @@ -1459,7 +1489,7 @@ pub fn run() { } } }) - .invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, workshop_uninstall, workshop_list_installed, get_screenshots, delete_screenshot, save_global_skin_pck]) + .invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, workshop_uninstall, workshop_list_installed, get_screenshots, delete_screenshot, save_global_skin_pck, check_game_update]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/context/LauncherContext.tsx b/src/context/LauncherContext.tsx index accee14..f0c879e 100644 --- a/src/context/LauncherContext.tsx +++ b/src/context/LauncherContext.tsx @@ -73,7 +73,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) { gameRaw.installs, gameRaw.isGameRunning, gameRaw.downloadProgress, gameRaw.downloadingId, gameRaw.editions, gameRaw.isRunnerDownloading, gameRaw.runnerDownloadProgress, gameRaw.error, gameRaw.updateCustomEdition, - gameRaw.handleUninstall, gameRaw.handleCancelDownload, configRaw.profile + gameRaw.handleUninstall, gameRaw.handleCancelDownload, gameRaw.gameUpdateMessage, configRaw.profile ]); const audio = useMemo(() => audioRaw, [ diff --git a/src/hooks/useGameManager.ts b/src/hooks/useGameManager.ts index 752f765..548792f 100644 --- a/src/hooks/useGameManager.ts +++ b/src/hooks/useGameManager.ts @@ -60,6 +60,7 @@ export function useGameManager({ number | null >(null); const [error, setError] = useState(null); + const [gameUpdateMessage, setGameUpdateMessage] = useState(null); const editions = useMemo( () => [...BASE_EDITIONS, ...customEditions], @@ -76,6 +77,28 @@ export function useGameManager({ setInstalls(results.filter((id): id is string => id !== null)); }, [editions]); + const checkForGameUpdates = useCallback(async () => { + if (!profile) return; + const edition = editions.find(e => e.id === profile); + if (!edition) return; + try { + const isUpdate = await TauriService.checkGameUpdate(profile, edition.url); + if (isUpdate) { + setGameUpdateMessage(`An update is available for ${edition.name}!`); + } else { + setGameUpdateMessage(null); + } + } catch(e) { + console.error(e); + } + }, [profile, editions]); + + useEffect(() => { + if (installs.includes(profile)) { + checkForGameUpdates(); + } + }, [profile, installs, checkForGameUpdates]); + useEffect(() => { checkInstalls(); const unlistenDownload = TauriService.onDownloadProgress((p) => @@ -236,5 +259,7 @@ export function useGameManager({ updateCustomEdition, downloadRunner, checkInstalls, + gameUpdateMessage, + setGameUpdateMessage, }; } diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 33e2e62..ea0d6b1 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -164,6 +164,17 @@ export default function App() { variant="update" /> + game.setGameUpdateMessage(null)} + onClick={() => { + game.setGameUpdateMessage(null); + game.toggleInstall(config.profile); + }} + title="Game Update Available!" + variant="update" + /> + {showSetup ? ( { return invoke("save_global_skin_pck", { pckData: Array.from(pckData) }); } + + static async checkGameUpdate(instanceId: string, url: string): Promise { + return invoke("check_game_update", { instanceId, url }); + } }