diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs index eb28ae5..ed28429 100644 --- a/fitnesstrax/app/src/main.rs +++ b/fitnesstrax/app/src/main.rs @@ -40,11 +40,14 @@ const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/fitnesstrax/"; #[derive(Debug)] enum AppInvocation { OpenDatabase(PathBuf), + RequestRecords, } #[derive(Debug)] enum AppResponse { - DatabaseChanged, + NoDatabase, + Records, + DatabaseChanged(PathBuf), } // Note that I have not yet figured out the communication channel or how the central dispatcher @@ -53,32 +56,38 @@ enum AppResponse { /// The real, headless application. This is where all of the logic will reside. #[derive(Clone)] struct App { - settings: gio::Settings, database: Arc>>>, } impl App { - pub fn new(settings: gio::Settings) -> Self { + pub fn new(db_path: Option) -> Self { + let database = db_path.map(|path| Series::open(path).unwrap()); let s = Self { - settings, - database: Arc::new(RwLock::new(None)), + database: Arc::new(RwLock::new(database)), }; - if !s.settings.string("series-path").is_empty() { - let path = PathBuf::from(s.settings.string("series-path")); - let db = Series::open(path).unwrap(); - *s.database.write().unwrap() = Some(db); - } - s } + pub async fn process_invocation(&self, invocation: AppInvocation) -> AppResponse { + match invocation { + AppInvocation::OpenDatabase(db_path) => { + self.open_db(&db_path); + AppResponse::DatabaseChanged(db_path) + } + AppInvocation::RequestRecords => { + if self.database.read().unwrap().is_none() { + AppResponse::NoDatabase + } else { + AppResponse::Records + } + } + } + } + pub fn open_db(&self, path: &Path) { let db = Series::open(path).unwrap(); *self.database.write().unwrap() = Some(db); - self.settings - .set_string("series-path", path.to_str().unwrap()) - .unwrap(); } } @@ -224,6 +233,29 @@ impl HistoricalView { } } +#[derive(Clone, Debug, PartialEq)] +enum ViewName { + Placeholder, + Welcome, + Historical, +} + +enum View { + Placeholder(gtk::Widget), + Welcome(gtk::Widget), + Historical(gtk::Widget), +} + +impl View { + fn widget<'a>(&'a self) -> &'a gtk::Widget { + match self { + View::Placeholder(widget) => widget, + View::Welcome(widget) => widget, + View::Historical(widget) => widget, + } + } +} + /// The application window, or the main window, is the main user interface for the app. Almost /// everything occurs here. #[derive(Clone)] @@ -231,7 +263,8 @@ struct AppWindow { app_tx: Sender, window: adw::ApplicationWindow, layout: gtk::Box, - current_view: Rc>, + current_view: Rc>, + settings: gio::Settings, } impl AppWindow { @@ -241,7 +274,7 @@ impl AppWindow { /// otherwise we don't use this. /// /// app is a core [App] object which encapsulates all of the basic logic. - fn new(adw_app: &adw::Application, app_tx: Sender) -> AppWindow { + fn new(app_id: &str, adw_app: &adw::Application, app_tx: Sender) -> AppWindow { let window = adw::ApplicationWindow::builder() .application(adw_app) .width_request(800) @@ -272,10 +305,10 @@ impl AppWindow { .orientation(gtk::Orientation::Vertical) .build(); - let initial_view = PlaceholderView::new(); + let initial_view = View::Placeholder(PlaceholderView::new().upcast()); layout.append(&header); - layout.append(&initial_view); + layout.append(initial_view.widget()); window.set_content(Some(&layout)); window.present(); @@ -284,58 +317,62 @@ impl AppWindow { app_tx, window, layout, - current_view: Rc::new(RefCell::new(initial_view.upcast())), + current_view: Rc::new(RefCell::new(initial_view)), + settings: gio::Settings::new(app_id), }; - let initial_view = if true { - WelcomeView::new({ - let s = s.clone(); - Box::new(move |path: PathBuf| { - // The user has selected a path. Perhaps the path is new, perhaps it already - // exists. - // - // If the file exists already, attempt to read it. Fail if that doesn't work. - // A should show to the user something that indicates that the file exists but is - // not already a database. - // - // If the file does not exist, create a new one. Again, show the user an error if - // some kind of error occurs. - // app.open_db(&path); - // s.change_view(HistoricalView::new().upcast()); - // let s = s.clone(); - /* - glib::spawn_future_local(async move { - s.app_tx - .send(AppInvocation::OpenDatabase(path)) - .await - .unwrap(); - }); - */ - s.app_tx - .send_blocking(AppInvocation::OpenDatabase(path)) - .unwrap(); - }) - }) - .upcast() - } else { - HistoricalView::new().upcast() - }; - - s.change_view(initial_view); - s } + pub fn change_view(&self, view: ViewName) { + self.swap_main(self.construct_view(view)); + } + + pub fn process_response(&self, response: AppResponse) { + match response { + AppResponse::DatabaseChanged(db_path) => { + self.settings + .set_string("series-path", db_path.to_str().unwrap()) + .unwrap(); + self.change_view(ViewName::Historical); + } + AppResponse::NoDatabase => { + self.change_view(ViewName::Welcome); + } + AppResponse::Records => { + self.change_view(ViewName::Historical); + } + } + } + // Switch views. // // This function only replaces the old view with the one which matches the current view state. // It is responsible for ensuring that the new view goes into the layout in the correct // position. - fn change_view(&self, view: gtk::Widget) { - let mut current_view = self.current_view.borrow_mut(); - self.layout.remove(&*current_view); - *current_view = view; - self.layout.append(&*current_view); + fn swap_main(&self, view: View) { + let mut current_widget = self.current_view.borrow_mut(); + self.layout.remove(&*current_widget.widget()); + *current_widget = view; + self.layout.append(&*current_widget.widget()); + } + + fn construct_view(&self, view: ViewName) -> View { + match view { + ViewName::Placeholder => View::Placeholder(PlaceholderView::new().upcast()), + ViewName::Welcome => View::Welcome( + WelcomeView::new({ + let s = self.clone(); + Box::new(move |path: PathBuf| { + s.app_tx + .send_blocking(AppInvocation::OpenDatabase(path)) + .unwrap(); + }) + }) + .upcast(), + ), + ViewName::Historical => View::Historical(HistoricalView::new().upcast()), + } } } @@ -354,10 +391,14 @@ fn main() { }; let settings = gio::Settings::new(app_id); - - println!("database path: {}", settings.string("series-path")); - - let app = App::new(settings); + let app = App::new({ + let path = settings.string("series-path"); + if path.is_empty() { + None + } else { + Some(PathBuf::from(path)) + } + }); let adw_app = adw::Application::builder() .application_id(app_id) @@ -373,17 +414,26 @@ fn main() { let (gtk_tx, gtk_rx) = async_channel::unbounded::(); let (app_tx, app_rx) = async_channel::unbounded::(); - AppWindow::new(adw_app, app_tx.clone()); + let window = AppWindow::new(app_id, adw_app, app_tx.clone()); glib::spawn_future_local(async move { + // The app requests data to start with. This kicks everything off. The response from + // the app will cause the window to be updated shortly. + let _ = app_tx.send(AppInvocation::RequestRecords).await; + while let Ok(response) = gtk_rx.recv().await { println!("response received: {:?}", response); + window.process_response(response); } }); - runtime.spawn(async move { - while let Ok(invocation) = app_rx.recv().await { - println!("Received an invocation: {:?}", invocation); + runtime.spawn({ + let app = app.clone(); + async move { + while let Ok(invocation) = app_rx.recv().await { + let response = app.process_invocation(invocation).await; + let _ = gtk_tx.send(response).await; + } } }); });