From afbfc59603ca0f698bf8cc629b1cc7b2fac00ba9 Mon Sep 17 00:00:00 2001 From: jess Date: Tue, 31 Mar 2026 15:56:51 -0700 Subject: [PATCH] wire up App entry point state machine --- au-o2-gui/src/entry.rs | 270 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 265 insertions(+), 5 deletions(-) diff --git a/au-o2-gui/src/entry.rs b/au-o2-gui/src/entry.rs index c56a8ec..f4bc8a3 100644 --- a/au-o2-gui/src/entry.rs +++ b/au-o2-gui/src/entry.rs @@ -64,13 +64,18 @@ pub enum Message { pub struct App { pub state: AppState, + pub global_config: crate::config::AudioOxideConfig, } impl Default for App { fn default() -> Self { - Self { - state: AppState::ProjectView(ProjectViewState::Splash), - } + let config = crate::first_run::load_or_initialize_config(); + let state = if config.first_run { + AppState::FirstRun { project_dir: config.project_dir.clone() } + } else { + AppState::ProjectView(ProjectViewState::Splash) + }; + Self { state, global_config: config } } } @@ -79,12 +84,267 @@ pub fn main() -> iced::Result { .run() } +fn scan_recent_projects(project_dir: &PathBuf) -> Vec { + let mut projects = Vec::new(); + let entries = match std::fs::read_dir(project_dir) { + Ok(e) => e, + Err(_) => return projects, + }; + for entry in entries.flatten() { + let path = entry.path(); + if !path.is_dir() { + continue; + } + let config_path = path.join("project.toml"); + if !config_path.exists() { + continue; + } + let name = path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("Untitled") + .to_string(); + let modified = std::fs::metadata(&config_path) + .and_then(|m| m.modified()) + .ok() + .and_then(|t| { + let duration = t.duration_since(std::time::UNIX_EPOCH).ok()?; + chrono::DateTime::from_timestamp(duration.as_secs() as i64, 0) + .map(|dt| dt.naive_local()) + }) + .unwrap_or_default(); + projects.push(ProjectInfo { name, path, modified }); + } + projects.sort_by(|a, b| b.modified.cmp(&a.modified)); + projects +} + +fn parse_time_signature(s: &str) -> Option<(u8, u8)> { + let parts: Vec<&str> = s.split('/').collect(); + if parts.len() == 2 { + let num = parts[0].trim().parse::().ok()?; + let den = parts[1].trim().parse::().ok()?; + Some((num, den)) + } else { + None + } +} + +fn apply_tempo_to_state(state: &mut AppState, bpm: f32) { + if let AppState::NewProject(np) = state { + np.config.tempo = bpm; + } +} + +fn apply_time_signature_to_state(state: &mut AppState, num: u8, den: u8) { + if let AppState::NewProject(np) = state { + np.config.time_signature_numerator = num; + np.config.time_signature_denominator = den; + } +} + impl App { - fn update(&mut self, _message: Message) -> Task { + fn update(&mut self, message: Message) -> Task { + match message { + Message::ViewRecentProjects => { + let projects = scan_recent_projects(&self.global_config.project_dir); + self.state = AppState::ProjectView(ProjectViewState::Recent { projects }); + } + Message::ViewFindProject => { + self.state = AppState::ProjectView(ProjectViewState::Find { + path_input: String::new(), + }); + } + Message::ViewNewProject => { + self.state = AppState::NewProject(crate::gui::new_project::State::default()); + } + Message::ViewTimeUtility => { + let return_state = std::mem::replace( + &mut self.state, + AppState::ProjectView(ProjectViewState::Splash), + ); + self.state = AppState::TimeUtility { + tapper_state: time_utility::State::default(), + return_state: Box::new(return_state), + }; + } + + Message::FindPathChanged(s) => { + if let AppState::ProjectView(ProjectViewState::Find { ref mut path_input }) = self.state { + *path_input = s; + } + } + + Message::OpenProject(path) => { + let (editor, task) = crate::editor::Editor::new(path); + self.state = AppState::Editor(editor); + return task.map(Message::EditorMessage); + } + Message::CreateProject => { + if let AppState::NewProject(ref np_state) = self.state { + let config = &np_state.config; + let project_dir = self.global_config.project_dir.join(&config.name); + let _ = std::fs::create_dir_all(&project_dir); + let config_path = project_dir.join("project.toml"); + if let Ok(toml_str) = toml::to_string_pretty(config) { + let _ = std::fs::write(&config_path, toml_str); + } + let (editor, task) = crate::editor::Editor::new(project_dir); + self.state = AppState::Editor(editor); + return task.map(Message::EditorMessage); + } + } + + Message::ProjectNameChanged(s) => { + if let AppState::NewProject(ref mut np) = self.state { + np.config.name = s; + } + } + Message::SampleRateSelected(sr) => { + if let AppState::NewProject(ref mut np) = self.state { + np.config.sample_rate = sr; + } + } + Message::OutputBufferSizeSelected(bs) => { + if let AppState::NewProject(ref mut np) = self.state { + np.config.output_buffer_size = bs; + } + } + Message::InputBufferSizeSelected(bs) => { + if let AppState::NewProject(ref mut np) = self.state { + np.config.input_buffer_size = bs; + } + } + Message::AudioDeviceSelected(d) => { + if let AppState::NewProject(ref mut np) = self.state { + np.config.audio_device = d; + } + } + Message::InputDeviceSelected(d) => { + if let AppState::NewProject(ref mut np) = self.state { + np.config.audio_input_device = d; + } + } + Message::TempoChanged(t) => { + if let AppState::NewProject(ref mut np) = self.state { + np.config.tempo = t; + } + } + Message::TimeSignatureNumeratorChanged(s) => { + if let AppState::NewProject(ref mut np) = self.state { + if let Ok(v) = s.parse::() { + np.config.time_signature_numerator = v; + } + } + } + Message::TimeSignatureDenominatorChanged(s) => { + if let AppState::NewProject(ref mut np) = self.state { + if let Ok(v) = s.parse::() { + np.config.time_signature_denominator = v; + } + } + } + + Message::FirstRunProjectDirChanged(s) => { + if let AppState::FirstRun { ref mut project_dir } = self.state { + *project_dir = PathBuf::from(s); + } + } + Message::FirstRunComplete => { + if let AppState::FirstRun { ref project_dir } = self.state { + self.global_config.first_run = false; + self.global_config.project_dir = project_dir.clone(); + let _ = std::fs::create_dir_all(&self.global_config.project_dir); + crate::first_run::save_config(&self.global_config); + } + self.state = AppState::ProjectView(ProjectViewState::Splash); + } + + Message::TimeUtilityTapPressed => { + if let AppState::TimeUtility { ref mut tapper_state, .. } = self.state { + time_utility::handle_tap_pressed(tapper_state); + } + } + Message::TimeUtilityTapReleased => { + if let AppState::TimeUtility { ref mut tapper_state, .. } = self.state { + return time_utility::handle_tap_released(tapper_state); + } + } + Message::RunTimeUtilityAnalysis => { + if let AppState::TimeUtility { ref mut tapper_state, .. } = self.state { + tapper_state.result = time_utility::run_analysis(&tapper_state.tap_events); + } + } + Message::TimeUtilitySet(bpm) => { + if let Some(mut rs) = self.take_time_utility_return() { + apply_tempo_to_state(&mut rs, bpm as f32); + self.state = rs; + } + } + Message::TimeUtilitySetTimeSignature(sig) => { + if let Some(mut rs) = self.take_time_utility_return() { + if let Some((num, den)) = parse_time_signature(&sig) { + apply_time_signature_to_state(&mut rs, num, den); + } + self.state = rs; + } + } + Message::TimeUtilitySetBoth(bpm, sig) => { + if let Some(mut rs) = self.take_time_utility_return() { + apply_tempo_to_state(&mut rs, bpm as f32); + if let Some((num, den)) = parse_time_signature(&sig) { + apply_time_signature_to_state(&mut rs, num, den); + } + self.state = rs; + } + } + Message::TimeUtilityCancel => { + if let Some(rs) = self.take_time_utility_return() { + self.state = rs; + } else { + self.state = AppState::ProjectView(ProjectViewState::Splash); + } + } + + Message::EditorMessage(editor_msg) => { + if let AppState::Editor(ref mut editor) = self.state { + return editor.update(editor_msg).map(Message::EditorMessage); + } + } + } Task::none() } fn view(&self) -> Element<'_, Message> { - crate::gui::splash::view() + match &self.state { + AppState::ProjectView(ProjectViewState::Splash) => { + crate::gui::splash::view() + } + AppState::ProjectView(pv_state) => { + crate::gui::project_viewer::view(pv_state) + } + AppState::FirstRun { project_dir } => { + crate::gui::first_run_wizard::view(project_dir) + } + AppState::NewProject(np_state) => { + crate::gui::new_project::view(np_state) + } + AppState::TimeUtility { tapper_state, .. } => { + time_utility::view(tapper_state) + } + AppState::Editor(editor) => { + editor.view().map(Message::EditorMessage) + } + } + } + + fn take_time_utility_return(&mut self) -> Option { + let placeholder = AppState::ProjectView(ProjectViewState::Splash); + let old = std::mem::replace(&mut self.state, placeholder); + if let AppState::TimeUtility { return_state, .. } = old { + Some(*return_state) + } else { + self.state = old; + None + } } }