diff --git a/src/components/timeline/events_view.rs b/src/components/timeline/events_view.rs index 058418a..8e526f1 100644 --- a/src/components/timeline/events_view.rs +++ b/src/components/timeline/events_view.rs @@ -1,3 +1,4 @@ +use crate::managers::timeline_manager::get_timeline_manager; use crate::models::timeline::{LifePeriodEvent, Yaml}; use chrono::{Duration, Local, NaiveDate}; use dioxus::prelude::*; @@ -5,72 +6,110 @@ use uuid::Uuid; #[component] pub fn EventView(selected_life_period_id: Uuid) -> Element { - let yaml_state = use_context::>(); + let mut events = use_signal(|| None::, String>>); + let mut timeline = use_signal(|| None::>); - let life_period = use_memo(move || { - yaml_state() - .life_periods - .iter() - .find(|p| p.id == Some(selected_life_period_id)) - .cloned() - }); + // Load data effect + { + let mut events = events.clone(); + let mut timeline = timeline.clone(); + use_effect(move || { + spawn(async move { + let events_result = get_timeline_manager() + .get_period_events(selected_life_period_id) + .await; + events.set(Some(events_result)); - match life_period() { - Some(period) => { - let events = &period.events; - if events.is_empty() { - return rsx! { - div { class: "event-view-empty", - h2 { "No events in this life period" } - p { "This life period from {period.start} currently has no events." } - p { "You can add events to this period to track important moments or milestones." } - } - }; - } + let timeline_result = get_timeline_manager().get_timeline().await; + timeline.set(Some(timeline_result)); + }); + }); + } - let start_date = events - .iter() - .filter_map(|event| NaiveDate::parse_from_str(&event.start, "%Y-%m-%d").ok()) - .min() - .unwrap_or_else(|| { - NaiveDate::parse_from_str(&period.start, "%Y-%m") - .unwrap_or(Local::now().date_naive()) - }); + let events_ref = events.read(); + let timeline_ref = timeline.read(); - let end_date = yaml_state() + match (&*events_ref, &*timeline_ref) { + (Some(Ok(events)), Some(Ok(yaml))) => { + let period = yaml .life_periods .iter() - .find(|p| p.start > period.start) - .and_then(|next_period| { - NaiveDate::parse_from_str(&format!("{}-01", next_period.start), "%Y-%m-%d").ok() - }) - .unwrap_or_else(|| Local::now().date_naive()); + .find(|p| p.id == Some(selected_life_period_id)) + .cloned(); - let total_days = (end_date - start_date).num_days() as usize; - let cols = 28; - - rsx! { - div { - class: "event-view", - style: "grid-template-columns: repeat({cols}, 1fr);", - {(0..total_days).map(|day| { - let date = start_date + Duration::days(day as i64); - let color = get_color_for_event(&date, events, &end_date); - rsx! { - div { - key: "{day}", - class: "event-cell", - style: "background-color: {color};", - title: "{date}" + match period { + Some(period) => { + if events.is_empty() { + return rsx! { + div { class: "event-view-empty", + h2 { "No events in this life period" } + p { "This life period from {period.start} currently has no events." } + p { "You can add events to this period to track important moments or milestones." } } + }; + } + + let start_date = events + .iter() + .filter_map(|event| { + NaiveDate::parse_from_str(&event.start, "%Y-%m-%d").ok() + }) + .min() + .unwrap_or_else(|| { + NaiveDate::parse_from_str(&period.start, "%Y-%m") + .unwrap_or(Local::now().date_naive()) + }); + + let end_date = yaml + .life_periods + .iter() + .find(|p| p.start > period.start) + .and_then(|next_period| { + NaiveDate::parse_from_str( + &format!("{}-01", next_period.start), + "%Y-%m-%d", + ) + .ok() + }) + .unwrap_or_else(|| Local::now().date_naive()); + + let total_days = (end_date - start_date).num_days() as usize; + let cols = 28; + + rsx! { + div { + class: "event-view", + style: "grid-template-columns: repeat({cols}, 1fr);", + {(0..total_days).map(|day| { + let date = start_date + Duration::days(day as i64); + let color = get_color_for_event(&date, &events, &end_date); + rsx! { + div { + key: "{day}", + class: "event-cell", + style: "background-color: {color};", + title: "{date}" + } + } + })} } - })} + } } + None => rsx! { + div { class: "event-view-not-found", + "Selected life period not found." + } + }, } } - None => rsx! { - div { class: "event-view-not-found", - "Selected life period not found." + (Some(Err(ref e)), _) | (_, Some(Err(ref e))) => rsx! { + div { class: "error-message", + "Failed to load data: {e}" + } + }, + _ => rsx! { + div { class: "loading-message", + "Loading..." } }, } diff --git a/src/components/timeline/top_panel.rs b/src/components/timeline/top_panel.rs index 2937193..a8ccfce 100644 --- a/src/components/timeline/top_panel.rs +++ b/src/components/timeline/top_panel.rs @@ -6,7 +6,7 @@ use arboard::Clipboard; use dioxus::prelude::*; use qrcode::render::svg; use qrcode::QrCode; -use tracing::error; +use tracing::{debug, error}; #[cfg(all(target_os = "linux", not(target_arch = "wasm32")))] use wl_clipboard_rs::copy::{MimeType, Options as WlOptions, Source}; @@ -14,54 +14,86 @@ use wl_clipboard_rs::copy::{MimeType, Options as WlOptions, Source}; use crate::utils::compression::compress_and_encode; #[cfg(target_arch = "wasm32")] use crate::utils::screenshot::share_screenshot; - #[component] fn YamlSelector( app_state: Signal, yaml_state: Signal, available_timelines: Signal>, ) -> Element { + // Add a loading state to prevent multiple selections while loading + let mut is_switching = use_signal(|| false); + // Add state to track the actual current timeline + let mut current_timeline = use_signal(|| app_state().selected_yaml.clone()); + rsx! { - select { - value: "{app_state().selected_yaml}", - onchange: { - move |evt: Event| { + div { + class: "yaml-selector-container", + select { + disabled: is_switching(), + value: "{current_timeline()}", + onchange: move |evt: Event| { let selected_yaml = evt.value().to_string(); - use_future(move || { - let selected_yaml = selected_yaml.clone(); - async move { - app_state.write().selected_yaml = selected_yaml.clone(); - if let Ok(new_yaml) = get_timeline_manager().get_timeline_by_name(&selected_yaml).await { - yaml_state.set(new_yaml.clone()); - // Also update the timeline in the manager - if let Err(e) = get_timeline_manager().update_timeline(&new_yaml.clone()).await { - error!("Failed to update timeline: {}", e); - } + let previous_yaml = current_timeline(); + + if selected_yaml == previous_yaml { + return; + } + + is_switching.set(true); + + spawn(async move { + let timeline_manager = get_timeline_manager(); + debug!("Attempting to switch from '{}' to '{}'", previous_yaml, selected_yaml); + + match timeline_manager.select_timeline(&selected_yaml).await { + Ok(new_yaml) => { + debug!("Successfully switched to '{}'", selected_yaml); + // Update both states only after successful switch + app_state.write().selected_yaml = selected_yaml.clone(); + yaml_state.set(new_yaml); + current_timeline.set(selected_yaml); + } + Err(e) => { + error!("Failed to switch to timeline '{}': {}", selected_yaml, e); + // Keep the previous selection on failure + current_timeline.set(previous_yaml); } } + + is_switching.set(false); }); + }, + if available_timelines().is_empty() { + option { + value: "default", + "default" + } + + } else { + + { available_timelines.read().iter().map(|name| { + rsx! { + option { + value: "{name}", + selected: name == ¤t_timeline(), + "{name}" + } + } + })} + } - }, - if available_timelines().is_empty() { - option { - value: "default", - "default" + } + // Optional: Add loading indicator + {if is_switching() { + rsx! { + span { class: "loading-indicator", "⟳" } } } else { - { available_timelines.read().iter().map(|name| { - rsx! { - option { - value: "{name}", - selected: name == &app_state().selected_yaml, - "{name}" - } - } - })} - } + rsx! {} + }} } } } - #[component] pub fn TopPanel(y: String) -> Element { let mut app_state = use_context::>(); @@ -74,17 +106,54 @@ pub fn TopPanel(y: String) -> Element { let available_timelines = use_signal(Vec::new); // Load timeline functionality + let load_timeline = move |_| { + let mut yaml_state = yaml_state.clone(); + let mut app_state = app_state.clone(); + use_future(move || async move { if let Some((name, new_yaml)) = get_timeline_manager().import_timeline().await { + // Update both states atomically yaml_state.set(new_yaml.clone()); - app_state.write().selected_yaml = name; + app_state.write().selected_yaml = name.clone(); + + // Make sure to select the imported timeline + if let Err(e) = get_timeline_manager().select_timeline(&name).await { + error!("Failed to switch to imported timeline: {}", e); + } } else { error!("Failed to import timeline"); } }); }; + // Update the life expectancy handler + let life_expectancy_handler = move |evt: Event| { + let mut yaml_state = yaml_state.clone(); + + if let Ok(value) = evt.value().parse() { + yaml_state.write().life_expectancy = value; + + // Update timeline after changing life expectancy + use_future(move || async move { + if let Err(e) = get_timeline_manager().update_timeline(&yaml_state()).await { + error!("Failed to update timeline: {}", e); + } + }); + } else { + error!("Failed to parse life expectancy: {}", evt.value()); + } + }; + + use_effect(move || { + to_owned![available_timelines]; + spawn(async move { + let timelines = get_timeline_manager().get_available_timelines().await; + available_timelines.set(timelines); + }); + (|| ())() + }); + // Export timeline functionality let export_timeline = move |_| { use_future(move || async move { @@ -219,19 +288,7 @@ pub fn TopPanel(y: String) -> Element { }, select { value: "{yaml_state().life_expectancy}", - onchange: move |evt| { - if let Ok(value) = evt.value().parse() { - yaml_state.write().life_expectancy = value; - // Update timeline after changing life expectancy - use_future(move || async move { - if let Err(e) = get_timeline_manager().update_timeline(&yaml_state()).await { - error!("Failed to update timeline: {}", e); - } - }); - } else { - error!("Failed to parse life expectancy: {}", evt.value()); - } - }, + onchange: life_expectancy_handler, { (40..=120).map(|year| { rsx! { @@ -245,6 +302,7 @@ pub fn TopPanel(y: String) -> Element { } } } + } } // Screenshot Modal diff --git a/src/managers/timeline_manager.rs b/src/managers/timeline_manager.rs index c55d8dc..6157788 100644 --- a/src/managers/timeline_manager.rs +++ b/src/managers/timeline_manager.rs @@ -1,8 +1,12 @@ use crate::models::timeline::{LifePeriod, LifePeriodEvent, Yaml}; -use crate::storage::{get_path_manager, StorageConfig, YamlStorage}; +use crate::storage::{get_path_manager, StorageConfig, StorageError, YamlStorage}; use chrono::{Local, NaiveDate}; use once_cell::sync::Lazy; use rfd::FileDialog; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::SystemTime; +use tokio::sync::RwLock; use tracing::{debug, error}; use uuid::Uuid; @@ -34,8 +38,17 @@ life_periods: routines: [] "#; +impl From for String { + fn from(error: StorageError) -> Self { + error.to_string() + } +} + pub struct TimelineManager { - storage: YamlStorage, + current_name: Arc>, + storage: Arc>>, + + last_modified: Arc>, } impl TimelineManager { @@ -48,26 +61,29 @@ impl TimelineManager { extension: String::from("yaml"), ..Default::default() }; - debug!("Storage config: {:?}", config); - let path = get_path_manager().timeline_file("default"); - debug!("Timeline file path: {:?}", path); + let current_name = "default".to_string(); + let path = get_path_manager().timeline_file(¤t_name); + + // Get the initial last modified time + let last_modified = path + .metadata() + .ok() + .and_then(|m| m.modified().ok()) + .unwrap_or_else(SystemTime::now); - let storage = - YamlStorage::with_config_and_default(path.clone(), config, Some(default_yaml)) - .map_err(|e| { - error!("Failed to create storage at {:?}: {}", path, e); - e.to_string() - })?; + let storage = YamlStorage::with_config_and_default(path, config, Some(default_yaml))?; - debug!("Storage created successfully"); - Ok(Self { storage }) + Ok(Self { + current_name: Arc::new(RwLock::new(current_name)), + storage: Arc::new(RwLock::new(storage)), + last_modified: Arc::new(RwLock::new(last_modified)), + }) } pub async fn get_available_timelines(&self) -> Vec { #[cfg(not(target_arch = "wasm32"))] { if let Ok(entries) = std::fs::read_dir(get_path_manager().timelines_dir()) { - // Changed from timeline_dir to timelines_dir entries .filter_map(|entry| { let entry = entry.ok()?; @@ -90,38 +106,138 @@ impl TimelineManager { } } - // Add this new function pub async fn get_timeline_by_name(&self, name: &str) -> Result { let path = get_path_manager().timeline_file(name); + debug!("Loading timeline '{}' from: {:?}", name, path); + + if !path.exists() { + error!("Timeline file does not exist at path: {:?}", path); + return Err(format!("Timeline file '{}' does not exist", name)); + } + + match std::fs::read_to_string(&path) { + Ok(content) => { + debug!( + "Successfully read file content for '{}', content length: {}", + name, + content.len() + ); + match serde_yaml::from_str(&content) { + Ok(yaml) => { + debug!("Successfully parsed YAML for timeline '{}'", name); + Ok(yaml) + } + Err(e) => { + error!("Failed to parse YAML for timeline '{}': {}", name, e); + Err(format!("Failed to parse timeline '{}': {}", name, e)) + } + } + } + Err(e) => { + error!("Failed to read timeline file '{}': {}", name, e); + Err(format!("Failed to read timeline '{}': {}", name, e)) + } + } + } + + pub async fn select_timeline(&self, name: &str) -> Result { + debug!("Switching storage to timeline: {}", name); let config = StorageConfig { extension: String::from("yaml"), ..Default::default() }; - let storage = - YamlStorage::with_config_and_default(path.clone(), config, None).map_err(|e| { - error!("Failed to create storage for {}: {}", name, e); - e.to_string() - })?; + let path = get_path_manager().timeline_file(name); + debug!("New storage path: {:?}", path); - storage.get_data().await.map_err(|e| e.to_string()) + // Get or create the yaml content + let yaml = match self.get_timeline_by_name(name).await { + Ok(yaml) => yaml, + Err(_) => { + let mut default: Yaml = serde_yaml::from_str(DEFAULT_TIMELINE) + .map_err(|e| format!("Failed to parse default timeline: {}", e))?; + default.name = name.to_string(); + default + } + }; + + // Update last modified time + if let Ok(metadata) = path.metadata() { + if let Ok(modified) = metadata.modified() { + let mut last_modified = self.last_modified.write().await; + *last_modified = modified; + } + } + + // Create new storage + let new_storage = YamlStorage::with_config_and_default(path, config, Some(yaml.clone()))?; + + // Update the manager state + { + let mut current_name = self.current_name.write().await; + *current_name = name.to_string(); + } + { + let mut storage = self.storage.write().await; + *storage = new_storage; + } + + Ok(yaml) } + pub async fn check_for_file_changes(&self) -> Result, String> { + let current_name = self.current_name.read().await; + let path = get_path_manager().timeline_file(¤t_name); + + let file_modified = path.metadata().ok().and_then(|m| m.modified().ok()); + + if let Some(file_time) = file_modified { + let last_check = *self.last_modified.read().await; + + if file_time > last_check { + debug!("File change detected for timeline: {}", current_name); + + // Load the new content + let new_yaml = self.get_timeline_by_name(¤t_name).await?; + + // Update the storage and last modified time + { + let storage = self.storage.read().await; + storage.write(|store| *store = new_yaml.clone()).await?; + } + { + let mut last_modified = self.last_modified.write().await; + *last_modified = file_time; + } + + return Ok(Some(new_yaml)); + } + } + + Ok(None) + } pub async fn get_timeline(&self) -> Result { - self.storage.get_data().await.map_err(|e| e.to_string()) + let storage = self.storage.read().await; + storage.get_data().await.map_err(|e| e.to_string()) } pub async fn update_timeline(&self, yaml: &Yaml) -> Result<(), String> { - debug!("Updating timeline"); - self.storage + let storage = self.storage.read().await; + debug!( + "Updating timeline {} at {:?}", + self.current_name.read().await, + storage.file_path() + ); + storage .write(|store| *store = yaml.clone()) .await .map_err(|e| e.to_string()) } pub async fn add_life_period(&self, period: LifePeriod) -> Result<(), String> { + let storage = self.storage.read().await; debug!("Adding life period: {:?}", period); - self.storage + storage .write(|store| { store.life_periods.push(period); store.life_periods.sort_by(|a, b| a.start.cmp(&b.start)); @@ -131,8 +247,9 @@ impl TimelineManager { } pub async fn update_life_period(&self, period: LifePeriod) -> Result<(), String> { + let storage = self.storage.read().await; debug!("Updating life period: {:?}", period); - self.storage + storage .write(|store| { if let Some(existing) = store.life_periods.iter_mut().find(|p| p.id == period.id) { *existing = period; @@ -144,8 +261,9 @@ impl TimelineManager { } pub async fn delete_life_period(&self, id: Uuid) -> Result<(), String> { + let storage = self.storage.read().await; debug!("Deleting life period: {}", id); - self.storage + storage .write(|store| { store.life_periods.retain(|p| p.id != Some(id)); }) @@ -154,8 +272,9 @@ impl TimelineManager { } pub async fn add_event(&self, period_id: Uuid, event: LifePeriodEvent) -> Result<(), String> { + let storage = self.storage.read().await; debug!("Adding event to period {}: {:?}", period_id, event); - self.storage + storage .write(|store| { if let Some(period) = store .life_periods @@ -175,8 +294,9 @@ impl TimelineManager { period_id: Uuid, event: LifePeriodEvent, ) -> Result<(), String> { + let storage = self.storage.read().await; debug!("Updating event in period {}: {:?}", period_id, event); - self.storage + storage .write(|store| { if let Some(period) = store .life_periods @@ -194,8 +314,9 @@ impl TimelineManager { } pub async fn delete_event(&self, period_id: Uuid, event_id: Uuid) -> Result<(), String> { + let storage = self.storage.read().await; debug!("Deleting event {} from period {}", event_id, period_id); - self.storage + storage .write(|store| { if let Some(period) = store .life_periods @@ -209,9 +330,9 @@ impl TimelineManager { .map_err(|e| e.to_string()) } - // Helper methods for event view pub async fn get_period_events(&self, period_id: Uuid) -> Result, String> { - self.storage + let storage = self.storage.read().await; + storage .read(|store| { store .life_periods @@ -225,31 +346,30 @@ impl TimelineManager { } pub async fn force_save(&self) -> Result<(), String> { - self.storage.force_save().await.map_err(|e| e.to_string()) + let storage = self.storage.read().await; + storage.force_save().await.map_err(|e| e.to_string()) } pub async fn reload(&self) -> Result<(), String> { - self.storage.reload().await.map_err(|e| e.to_string()) + let storage = self.storage.read().await; + storage.reload().await.map_err(|e| e.to_string()) } pub async fn import_timeline(&self) -> Option<(String, Yaml)> { #[cfg(target_arch = "wasm32")] { - // Web import logic - // You'll need to implement this based on your requirements None } #[cfg(not(target_arch = "wasm32"))] { - // Desktop import logic using file dialog if let Some(file_path) = FileDialog::new() .add_filter("YAML", &["yaml", "yml"]) .pick_file() { let content = std::fs::read_to_string(&file_path).ok()?; let yaml: Yaml = serde_yaml::from_str(&content).ok()?; - let name = file_path.file_name()?.to_str()?.to_string(); + let name = file_path.file_stem()?.to_str()?.to_string(); Some((name, yaml)) } else { None @@ -260,14 +380,11 @@ impl TimelineManager { pub async fn export_timeline(&self, yaml: &Yaml) -> Result<(), String> { #[cfg(target_arch = "wasm32")] { - // Web export logic - // Implement based on your requirements Ok(()) } #[cfg(not(target_arch = "wasm32"))] { - // Desktop export logic if let Some(file_path) = FileDialog::new() .set_file_name("timeline.yaml") .add_filter("YAML", &["yaml", "yml"]) @@ -276,7 +393,7 @@ impl TimelineManager { let content = serde_yaml::to_string(yaml).map_err(|e| e.to_string())?; std::fs::write(file_path, content).map_err(|e| e.to_string()) } else { - Ok(()) // User cancelled + Ok(()) } } } diff --git a/src/storage/storage_manager.rs b/src/storage/storage_manager.rs index 5d72614..68e80ec 100644 --- a/src/storage/storage_manager.rs +++ b/src/storage/storage_manager.rs @@ -40,6 +40,10 @@ where Self::with_config_and_default(file_path, StorageConfig::default(), None) } + pub fn file_path(&self) -> &PathBuf { + &self.file_path + } + pub fn with_config(file_path: PathBuf, config: StorageConfig) -> StorageResult { Self::with_config_and_default(file_path, config, None) } @@ -80,17 +84,17 @@ where error!("Failed to serialize default data: {}", e); StorageError::Serialization(e.to_string()) })?; - + std::fs::write(&file_path, &content).map_err(|e| { error!("Failed to write default data to file: {}", e); StorageError::Io(e) })?; - + debug!("Successfully wrote default data to file: {:?}", file_path); } data }; - + Ok(Self { file_path, data: Arc::new(RwLock::new(data)), @@ -179,4 +183,4 @@ where *guard = new_data; Ok(()) } -} \ No newline at end of file +} diff --git a/src/views/timeline.rs b/src/views/timeline.rs index 179dc80..90e00bf 100644 --- a/src/views/timeline.rs +++ b/src/views/timeline.rs @@ -1,10 +1,11 @@ use crate::components::timeline::bottom_panel::BottomPanel; use crate::components::timeline::central_panel::CentralPanel; use crate::components::timeline::top_panel::TopPanel; +use crate::managers::timeline_manager::get_timeline_manager; use crate::state::life_state::initialize_state; use dioxus::prelude::*; -use tracing::debug; - +use tokio::time::Duration; +use tracing::{debug, error}; const TIMELINE_VIEW_CSS: Asset = asset!("/assets/styling/timeline_view.css"); const TIMELINE_ITEMS_CSS: Asset = asset!("/assets/styling/timeline_items.css"); const TIMELINE_MODAL_CSS: Asset = asset!("/assets/styling/timeline_modal.css"); @@ -15,6 +16,8 @@ pub fn TimelinePage(y: String) -> Element { let yaml_state = use_signal(Default::default); let app_state = use_signal(Default::default); let y_two = y.clone(); + // Initialize state using use_future + // Initialize state using use_future use_future(move || { to_owned![y, yaml_state, app_state, loading]; @@ -28,6 +31,24 @@ pub fn TimelinePage(y: String) -> Element { } }); + use_effect(move || { + to_owned![yaml_state]; + spawn(async move { + let timeline_manager = get_timeline_manager(); + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + match timeline_manager.check_for_file_changes().await { + Ok(Some(new_yaml)) => { + debug!("File changes detected, updating state"); + yaml_state.set(new_yaml); + } + Ok(None) => (), + Err(e) => error!("Error checking for file changes: {}", e), + } + } + }); + (|| ())() + }); // Show loading state while initializing if loading() { return rsx! { @@ -45,17 +66,17 @@ pub fn TimelinePage(y: String) -> Element { rsx! { // Stylesheet links - document::Link { - rel: "stylesheet", - href: TIMELINE_VIEW_CSS + document::Link { + rel: "stylesheet", + href: TIMELINE_VIEW_CSS } - document::Link { - rel: "stylesheet", - href: TIMELINE_ITEMS_CSS + document::Link { + rel: "stylesheet", + href: TIMELINE_ITEMS_CSS } - document::Link { - rel: "stylesheet", - href: TIMELINE_MODAL_CSS + document::Link { + rel: "stylesheet", + href: TIMELINE_MODAL_CSS } // Main app container @@ -71,8 +92,8 @@ pub fn TimelinePage(y: String) -> Element { #[component] pub fn TimelinePageNoParam() -> Element { rsx! { - TimelinePage { - y: String::new() + TimelinePage { + y: String::new() } } }