/* Copyright 2023, Savanni D'Gerinel This file is part of FitnessTrax. FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ mod app_window; mod components; mod views; use adw::prelude::*; use app_window::AppWindow; use async_channel::{Receiver, Sender}; use emseries::{EmseriesReadError, Series}; use ft_core::TraxRecord; use std::{ env, path::{Path, PathBuf}, rc::Rc, sync::{Arc, RwLock}, }; const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev"; const APP_ID_PROD: &str = "com.luminescent-dreams.fitnesstrax"; const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/fitnesstrax/"; /// Invocations are how parts of the application, primarily the UI, will send requests to the core. #[derive(Debug)] pub enum AppInvocation { /// Tell the core to try to open a database. OpenDatabase(PathBuf), /// Request a set of records from the core. // Note: this will require a time range, but doesn't yet. RequestRecords, } /// Responses are messages that the core sends to the UI. Though they are called responses, the /// could actually be pre-emptively sent, such as notifications. The UI will need to be able to /// process those any time they arrive. /// /// A typical use would be for the UI to send an [AppInvocation::RequestRecords] request and /// receive [AppResponse::Records]. #[derive(Debug)] pub enum AppResponse { /// No database is available. The UI should typically display a placeholder, such as the /// welcome view. NoDatabase, /// The database is open and here is a set of records. Typically, the set of records will be /// all of the records within a time frame, but this can actually be any set of records. Records, /// The database has been changed. This message is useful for telling the UI that a significant /// change has happened. Further, the UI needs to save PathBuf to settings, because the /// gio::Settings system can't be run in the fully async background. DatabaseChanged(PathBuf), } // Note that I have not yet figured out the communication channel or how the central dispatcher // should work. There's a dance between the App and the AppWindow that I haven't figured out yet. /// The real, headless application. This is where all of the logic will reside. #[derive(Clone)] struct App { database: Arc>>>, } impl App { pub fn new(db_path: Option) -> Self { let database = db_path.map(|path| Series::open(path).unwrap()); let s = Self { database: Arc::new(RwLock::new(database)), }; 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); } } fn main() { // I still don't fully understand gio resources. resources_register_include! is convenient // because I don't have to deal with filesystem locations at runtime. However, I think other // GTK applications do that rather than compiling the resources directly into the app. So, I'm // unclear as to how I want to handle this. gio::resources_register_include!("com.luminescent-dreams.fitnesstrax.gresource") .expect("to register resources"); let app_id = if std::env::var_os("ENV") == Some("dev".into()) { APP_ID_DEV } else { APP_ID_PROD }; let settings = gio::Settings::new(app_id); 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) .resource_base_path(RESOURCE_BASE_PATH) .build(); let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap(); adw_app.connect_activate(move |adw_app| { // These channels are used to send messages to the UI. Anything that needs to send a // message to the UI will send it via `ui_tx`. We will have one single process that owns // `ui_rx`. That process will read messages coming in and send them to AppWindow for proper // processing. // // The core app will usually only send messages in response to a request, but this channel // can also be used to tell the UI that something happened in the background, such as // detecting a watch, detecting new tracks to import, and so forth. let (ui_tx, ui_rx) = async_channel::unbounded::(); // These channels are used for communicating with the app. Already I can see that a lot of // different event handlers will need copies of app_tx in order to send requests into the // UI. let (app_tx, app_rx) = async_channel::unbounded::(); let window = AppWindow::new(app_id, RESOURCE_BASE_PATH, adw_app, app_tx.clone()); // Spawn a future where the UI will receive messages for the app window. Previously, this // would have been done by creating a glib::MainContext::channel(), but that has been // deprecated since gtk 4.10 in favor of using `async_channel`. 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) = ui_rx.recv().await { window.process_response(response); } }); // The tokio runtime starts up here and will handle all of the asynchronous operations that // the application needs to do. Messages arrive on `app_rx` and responses will be sent via // `ui_tx`. runtime.spawn({ let app = app.clone(); async move { while let Ok(invocation) = app_rx.recv().await { let response = app.process_invocation(invocation).await; let _ = ui_tx.send(response).await; } } }); }); let args: Vec = env::args().collect(); ApplicationExtManual::run_with_args(&adw_app, &args); }