Compare commits
No commits in common. "dedcc76df09dd2abfa11a93fb56c15d0130361b3" and "87994012fa5f97b77938148ff0c848fd19211a98" have entirely different histories.
dedcc76df0
...
87994012fa
987
Cargo.lock
generated
987
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,6 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
||||
async-channel = { version = "2.1" }
|
||||
emseries = { path = "../../emseries" }
|
||||
ft-core = { path = "../core" }
|
||||
gio = { version = "0.18" }
|
||||
|
@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License along with Fit
|
||||
mod ui;
|
||||
|
||||
use adw::prelude::*;
|
||||
use async_channel::{Receiver, Sender};
|
||||
use emseries::{EmseriesReadError, Series};
|
||||
use ft_core::TraxRecord;
|
||||
use gio::resources_lookup_data;
|
||||
@ -37,77 +36,43 @@ 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)]
|
||||
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,
|
||||
/// A set of events that can occur at the global application level. These events should represent
|
||||
/// significant state changes that should go through a central dispatcher.
|
||||
enum Events {
|
||||
DatabaseChanged(Series<TraxRecord>),
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
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 {
|
||||
settings: gio::Settings,
|
||||
database: Arc<RwLock<Option<Series<TraxRecord>>>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(db_path: Option<PathBuf>) -> Self {
|
||||
let database = db_path.map(|path| Series::open(path).unwrap());
|
||||
pub fn new(settings: gio::Settings) -> Self {
|
||||
let s = Self {
|
||||
database: Arc::new(RwLock::new(database)),
|
||||
settings,
|
||||
database: Arc::new(RwLock::new(None)),
|
||||
};
|
||||
|
||||
s
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,38 +218,14 @@ 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)]
|
||||
struct AppWindow {
|
||||
app_tx: Sender<AppInvocation>,
|
||||
app: App,
|
||||
window: adw::ApplicationWindow,
|
||||
layout: gtk::Box,
|
||||
current_view: Rc<RefCell<View>>,
|
||||
settings: gio::Settings,
|
||||
current_view: Rc<RefCell<gtk::Widget>>,
|
||||
}
|
||||
|
||||
impl AppWindow {
|
||||
@ -294,7 +235,7 @@ impl AppWindow {
|
||||
/// otherwise we don't use this.
|
||||
///
|
||||
/// app is a core [App] object which encapsulates all of the basic logic.
|
||||
fn new(app_id: &str, adw_app: &adw::Application, app_tx: Sender<AppInvocation>) -> AppWindow {
|
||||
fn new(adw_app: &adw::Application, app: App) -> AppWindow {
|
||||
let window = adw::ApplicationWindow::builder()
|
||||
.application(adw_app)
|
||||
.width_request(800)
|
||||
@ -325,74 +266,59 @@ impl AppWindow {
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.build();
|
||||
|
||||
let initial_view = View::Placeholder(PlaceholderView::new().upcast());
|
||||
let initial_view = PlaceholderView::new();
|
||||
|
||||
layout.append(&header);
|
||||
layout.append(initial_view.widget());
|
||||
layout.append(&initial_view);
|
||||
|
||||
window.set_content(Some(&layout));
|
||||
window.present();
|
||||
|
||||
let s = Self {
|
||||
app_tx,
|
||||
app: app.clone(),
|
||||
window,
|
||||
layout,
|
||||
current_view: Rc::new(RefCell::new(initial_view)),
|
||||
settings: gio::Settings::new(app_id),
|
||||
current_view: Rc::new(RefCell::new(initial_view.upcast())),
|
||||
};
|
||||
|
||||
let initial_view = if app.database.read().unwrap().is_none() {
|
||||
WelcomeView::new({
|
||||
let app = app.clone();
|
||||
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());
|
||||
})
|
||||
})
|
||||
.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 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()),
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -411,68 +337,25 @@ fn main() {
|
||||
};
|
||||
|
||||
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))
|
||||
}
|
||||
});
|
||||
|
||||
println!("database path: {}", settings.string("series-path"));
|
||||
|
||||
let app = App::new(settings);
|
||||
|
||||
/*
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
*/
|
||||
|
||||
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::<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.
|
||||
let (app_tx, app_rx) = async_channel::unbounded::<AppInvocation>();
|
||||
|
||||
let window = AppWindow::new(app_id, 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
AppWindow::new(adw_app, app.clone());
|
||||
});
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
Loading…
Reference in New Issue
Block a user