use chrono::TimeDelta; use leptos::prelude::*; use wasm_bindgen::closure::Closure; use wasm_bindgen::JsCast; use crate::components::modal::ModalState; use crate::prelude::*; use crate::state::persist::{ apply_import, clear_all, export_download }; use crate::state::undo::HistoryHandle; /// Campaign header: editable name, campaign image, in-game date and /// session steppers, the persistence controls, and undo/redo. /// /// Save downloads the dashboard as `dashboard.json`, Load imports one via /// a hidden file picker (with a confirm), Clear wipes to the blank /// campaign (with a confirm). Changes also autosave to localStorage /// (see `crate::state::persist`) and feed the undo history (see /// `crate::state::undo`); the undo/redo buttons and the Ctrl+Z / /// Ctrl+Shift+Z / Ctrl+Y shortcuts revert the last edit bursts. #[component] pub fn Header () -> impl IntoView { let state = expect_context::(); let modal = expect_context::(); let history = expect_context::(); let undo_depth = history.undo_depth(); let redo_depth = history.redo_depth(); let file_input = NodeRef::::new(); let campaign = state.campaign(); let date = state.date(); let session = state.session(); let image = state.campaign_image(); // Shifts the in-game date by ±1 day (saturating, via checked_add_signed). let adjust_day = move |adjustment: i64| date.update(|d| { *d = d.checked_add_signed(TimeDelta::days(adjustment)).unwrap(); }); // Shifts the session counter by ±1, guarding against underflow. let adjust_session = move |adjustment: isize| session.update(|s| { if let Some(new) = s.checked_add_signed(adjustment) { *s = new; } }); // Save: downloads the current dashboard as a JSON file (backup). let on_save = { let state = state.clone(); move |_| export_download(&state) }; // Load: opens the hidden file picker; the picked file is imported. let on_load = { let file_input = file_input.clone(); move |_| { if let Some(input) = file_input.get() { let _ = input.click(); } } }; // Clear: asks for confirmation, then replaces everything with the // blank dashboard. let on_clear = { let state = state.clone(); let modal = modal.clone(); move |_| { modal.confirm( "Очистить всё?", "Текущее состояние будет заменено пустой кампанией. Это действие нельзя отменить.", "Очистить", move || clear_all(&state), ); } }; // Import: reads the picked file, applies it, reports errors in a modal. let on_file = { let state = state.clone(); let modal = modal.clone(); move |ev: Event| { let Some(target) = ev.target() else { return }; let input = target.unchecked_into::(); let Some(file) = input.files().and_then(|files| files.item(0)) else { return; }; let Ok(reader) = web_sys::FileReader::new() else { return }; let reader_c = reader.clone(); let modal = modal.clone(); let onload = Closure::once(move || { let text = reader_c .result() .ok() .and_then(|value| value.as_string()) .unwrap_or_default(); match apply_import(&state, &text) { Ok(()) => {} Err(err) => modal.error("Ошибка импорта", err.to_string()), } }); reader.set_onloadend(Some(onload.as_ref().unchecked_ref())); onload.forget(); let _ = reader.read_as_text(&file); input.set_value(""); } }; view! {
aex v{env!("CARGO_PKG_VERSION")} by mk

{move || format!("{}", date.get().format("%d/%m/%Y"))}

Сессия #{move || format!("{}", session.get())}

} }