feat: update notification for game update

This commit is contained in:
neoapps-dev 2026-04-21 21:27:58 +03:00
parent 2fe2749d62
commit 1febea2f68
5 changed files with 73 additions and 3 deletions

View file

@ -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<InstalledPackageEntry> {
result
}
#[tauri::command]
async fn check_game_update(app: AppHandle, instance_id: String, url: String) -> Result<bool, String> {
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(&timestamp_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<McServer>) -> 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");
}

View file

@ -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, [

View file

@ -60,6 +60,7 @@ export function useGameManager({
number | null
>(null);
const [error, setError] = useState<string | null>(null);
const [gameUpdateMessage, setGameUpdateMessage] = useState<string | null>(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,
};
}

View file

@ -164,6 +164,17 @@ export default function App() {
variant="update"
/>
<AchievementToast
message={game.gameUpdateMessage}
onClose={() => game.setGameUpdateMessage(null)}
onClick={() => {
game.setGameUpdateMessage(null);
game.toggleInstall(config.profile);
}}
title="Game Update Available!"
variant="update"
/>
<AnimatePresence>
{showSetup ? (
<SetupView

View file

@ -194,4 +194,8 @@ export class TauriService {
static async saveGlobalSkinPck(pckData: Uint8Array): Promise<void> {
return invoke("save_global_skin_pck", { pckData: Array.from(pckData) });
}
static async checkGameUpdate(instanceId: string, url: string): Promise<boolean> {
return invoke("check_game_update", { instanceId, url });
}
}