2023-12-18 23:36:22 +00:00
|
|
|
/*
|
|
|
|
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
|
|
|
|
|
|
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 <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
2023-12-19 02:14:08 +00:00
|
|
|
|
2023-12-22 19:54:38 +00:00
|
|
|
mod app_window;
|
|
|
|
mod components;
|
|
|
|
mod views;
|
2023-12-19 02:14:08 +00:00
|
|
|
|
2023-12-18 23:30:41 +00:00
|
|
|
use adw::prelude::*;
|
2023-12-22 19:54:38 +00:00
|
|
|
use app_window::AppWindow;
|
2023-12-19 23:02:35 +00:00
|
|
|
use async_channel::{Receiver, Sender};
|
2023-12-19 15:46:53 +00:00
|
|
|
use emseries::{EmseriesReadError, Series};
|
2023-12-18 23:30:41 +00:00
|
|
|
use ft_core::TraxRecord;
|
|
|
|
use std::{
|
|
|
|
env,
|
2023-12-19 15:59:33 +00:00
|
|
|
path::{Path, PathBuf},
|
2023-12-19 05:31:36 +00:00
|
|
|
rc::Rc,
|
2023-12-18 23:30:41 +00:00
|
|
|
sync::{Arc, RwLock},
|
|
|
|
};
|
2023-11-20 03:47:36 +00:00
|
|
|
|
2023-12-07 14:45:56 +00:00
|
|
|
const APP_ID_DEV: &str = "com.luminescent-dreams.fitnesstrax.dev";
|
2023-12-07 14:56:10 +00:00
|
|
|
const APP_ID_PROD: &str = "com.luminescent-dreams.fitnesstrax";
|
2023-12-07 14:45:56 +00:00
|
|
|
|
2023-12-18 16:59:56 +00:00
|
|
|
const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/fitnesstrax/";
|
2023-12-07 14:45:56 +00:00
|
|
|
|
2023-12-22 19:28:23 +00:00
|
|
|
/// Invocations are how parts of the application, primarily the UI, will send requests to the core.
|
2023-12-19 23:02:35 +00:00
|
|
|
#[derive(Debug)]
|
2023-12-22 19:54:38 +00:00
|
|
|
pub enum AppInvocation {
|
2023-12-22 19:28:23 +00:00
|
|
|
/// Tell the core to try to open a database.
|
2023-12-19 23:02:35 +00:00
|
|
|
OpenDatabase(PathBuf),
|
2023-12-22 19:28:23 +00:00
|
|
|
|
|
|
|
/// Request a set of records from the core.
|
|
|
|
// Note: this will require a time range, but doesn't yet.
|
2023-12-22 19:08:16 +00:00
|
|
|
RequestRecords,
|
2023-12-19 15:46:53 +00:00
|
|
|
}
|
2023-12-19 23:02:35 +00:00
|
|
|
|
2023-12-22 19:28:23 +00:00
|
|
|
/// 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].
|
2023-12-19 23:02:35 +00:00
|
|
|
#[derive(Debug)]
|
2023-12-22 19:54:38 +00:00
|
|
|
pub enum AppResponse {
|
2023-12-22 19:28:23 +00:00
|
|
|
/// No database is available. The UI should typically display a placeholder, such as the
|
|
|
|
/// welcome view.
|
2023-12-22 19:08:16 +00:00
|
|
|
NoDatabase,
|
2023-12-22 19:28:23 +00:00
|
|
|
|
|
|
|
/// 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.
|
2023-12-22 19:08:16 +00:00
|
|
|
Records,
|
2023-12-22 19:28:23 +00:00
|
|
|
|
|
|
|
/// 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.
|
2023-12-22 19:08:16 +00:00
|
|
|
DatabaseChanged(PathBuf),
|
2023-12-19 23:02:35 +00:00
|
|
|
}
|
|
|
|
|
2023-12-19 15:46:53 +00:00
|
|
|
// 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.
|
|
|
|
|
2023-12-18 23:30:41 +00:00
|
|
|
/// The real, headless application. This is where all of the logic will reside.
|
|
|
|
#[derive(Clone)]
|
|
|
|
struct App {
|
|
|
|
database: Arc<RwLock<Option<Series<TraxRecord>>>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl App {
|
2023-12-22 19:08:16 +00:00
|
|
|
pub fn new(db_path: Option<PathBuf>) -> Self {
|
|
|
|
let database = db_path.map(|path| Series::open(path).unwrap());
|
2023-12-19 15:59:33 +00:00
|
|
|
let s = Self {
|
2023-12-22 19:08:16 +00:00
|
|
|
database: Arc::new(RwLock::new(database)),
|
2023-12-19 15:59:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
s
|
|
|
|
}
|
|
|
|
|
2023-12-22 19:08:16 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-19 15:59:33 +00:00
|
|
|
pub fn open_db(&self, path: &Path) {
|
|
|
|
let db = Series::open(path).unwrap();
|
|
|
|
*self.database.write().unwrap() = Some(db);
|
2023-12-18 23:30:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-13 13:52:10 +00:00
|
|
|
fn main() {
|
2023-12-18 16:59:56 +00:00
|
|
|
// 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
|
2023-12-07 14:56:10 +00:00
|
|
|
} else {
|
2023-12-18 16:59:56 +00:00
|
|
|
APP_ID_PROD
|
2023-12-07 14:56:10 +00:00
|
|
|
};
|
2023-12-07 14:45:56 +00:00
|
|
|
|
|
|
|
let settings = gio::Settings::new(app_id);
|
2023-12-22 19:08:16 +00:00
|
|
|
let app = App::new({
|
|
|
|
let path = settings.string("series-path");
|
|
|
|
if path.is_empty() {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(PathBuf::from(path))
|
|
|
|
}
|
|
|
|
});
|
2023-11-20 03:47:36 +00:00
|
|
|
|
2023-12-18 23:30:41 +00:00
|
|
|
let adw_app = adw::Application::builder()
|
2023-12-18 16:59:56 +00:00
|
|
|
.application_id(app_id)
|
|
|
|
.resource_base_path(RESOURCE_BASE_PATH)
|
2023-11-20 03:47:36 +00:00
|
|
|
.build();
|
|
|
|
|
2023-12-19 23:02:35 +00:00
|
|
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
|
|
|
.enable_all()
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
|
2023-12-18 23:30:41 +00:00
|
|
|
adw_app.connect_activate(move |adw_app| {
|
2023-12-22 19:28:23 +00:00
|
|
|
// 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::<AppResponse>();
|
|
|
|
|
|
|
|
// 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.
|
2023-12-19 23:02:35 +00:00
|
|
|
let (app_tx, app_rx) = async_channel::unbounded::<AppInvocation>();
|
|
|
|
|
2023-12-22 19:54:38 +00:00
|
|
|
let window = AppWindow::new(app_id, RESOURCE_BASE_PATH, adw_app, app_tx.clone());
|
2023-12-19 23:02:35 +00:00
|
|
|
|
2023-12-22 19:28:23 +00:00
|
|
|
// 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`.
|
2023-12-19 23:02:35 +00:00
|
|
|
glib::spawn_future_local(async move {
|
2023-12-22 19:08:16 +00:00
|
|
|
// 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;
|
|
|
|
|
2023-12-22 19:28:23 +00:00
|
|
|
while let Ok(response) = ui_rx.recv().await {
|
2023-12-22 19:08:16 +00:00
|
|
|
window.process_response(response);
|
2023-12-19 23:02:35 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-12-22 19:28:23 +00:00
|
|
|
// 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`.
|
2023-12-22 19:08:16 +00:00
|
|
|
runtime.spawn({
|
|
|
|
let app = app.clone();
|
|
|
|
async move {
|
|
|
|
while let Ok(invocation) = app_rx.recv().await {
|
|
|
|
let response = app.process_invocation(invocation).await;
|
2023-12-22 19:28:23 +00:00
|
|
|
let _ = ui_tx.send(response).await;
|
2023-12-22 19:08:16 +00:00
|
|
|
}
|
2023-12-19 23:02:35 +00:00
|
|
|
}
|
|
|
|
});
|
2023-11-20 03:47:36 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
let args: Vec<String> = env::args().collect();
|
2023-12-18 23:30:41 +00:00
|
|
|
ApplicationExtManual::run_with_args(&adw_app, &args);
|
2023-11-13 13:52:10 +00:00
|
|
|
}
|