diff --git a/fitnesstrax/app/Cargo.toml b/fitnesstrax/app/Cargo.toml
index 470ecea..66f08ef 100644
--- a/fitnesstrax/app/Cargo.toml
+++ b/fitnesstrax/app/Cargo.toml
@@ -11,7 +11,7 @@ emseries = { path = "../../emseries" }
ft-core = { path = "../core" }
gio = { version = "0.18" }
glib = { version = "0.18" }
-gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
+gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] }
tokio = { version = "1.34", features = [ "full" ] }
[build-dependencies]
diff --git a/fitnesstrax/app/resources/style.css b/fitnesstrax/app/resources/style.css
index e69de29..314fd86 100644
--- a/fitnesstrax/app/resources/style.css
+++ b/fitnesstrax/app/resources/style.css
@@ -0,0 +1,21 @@
+.welcome {
+ margin: 64px;
+}
+
+.welcome-title {
+ font-size: larger;
+ padding: 8px;
+}
+
+.welcome-content {
+ padding: 8px;
+}
+
+.welcome-footer {
+}
+
+.dialog-row {
+ margin: 8px 0px 8px 0px;
+ padding: 8px;
+}
+
diff --git a/fitnesstrax/app/src/main.rs b/fitnesstrax/app/src/main.rs
index dcb3748..1eb412e 100644
--- a/fitnesstrax/app/src/main.rs
+++ b/fitnesstrax/app/src/main.rs
@@ -13,8 +13,11 @@ 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 ui;
+
use adw::prelude::*;
-use emseries::Series;
+use emseries::{EmseriesReadError, Series};
use ft_core::TraxRecord;
use gio::resources_lookup_data;
use glib::Object;
@@ -22,36 +25,63 @@ use gtk::{subclass::prelude::*, STYLE_PROVIDER_PRIORITY_USER};
use std::{
cell::RefCell,
env,
+ path::{Path, PathBuf},
+ rc::Rc,
sync::{Arc, RwLock},
};
+use ui::FileChooserRow;
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/";
+/// 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),
+}
+// 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>>>,
}
impl App {
- pub fn new() -> Self {
- Self {
+ pub fn new(settings: gio::Settings) -> Self {
+ let s = Self {
+ settings,
database: Arc::new(RwLock::new(None)),
+ };
+
+ 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 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();
}
}
-/// This is the view to show if the application has not yet been configured. It will walk the user
-/// through the most critical setup steps so that we can move on to the other views in the app.
-pub struct UnconfiguredViewPrivate {}
+pub struct PlaceholderViewPrivate {}
#[glib::object_subclass]
-impl ObjectSubclass for UnconfiguredViewPrivate {
- const NAME: &'static str = "UnconfiguredView";
- type Type = UnconfiguredView;
+impl ObjectSubclass for PlaceholderViewPrivate {
+ const NAME: &'static str = "PlaceholderView";
+ type Type = PlaceholderView;
type ParentType = gtk::Box;
fn new() -> Self {
@@ -59,24 +89,95 @@ impl ObjectSubclass for UnconfiguredViewPrivate {
}
}
-impl ObjectImpl for UnconfiguredViewPrivate {}
-impl WidgetImpl for UnconfiguredViewPrivate {}
-impl BoxImpl for UnconfiguredViewPrivate {}
+impl ObjectImpl for PlaceholderViewPrivate {}
+impl WidgetImpl for PlaceholderViewPrivate {}
+impl BoxImpl for PlaceholderViewPrivate {}
glib::wrapper! {
- pub struct UnconfiguredView(ObjectSubclass) @extends gtk::Box, gtk::Widget;
+ pub struct PlaceholderView(ObjectSubclass) @extends gtk::Box, gtk::Widget;
}
-impl UnconfiguredView {
+impl PlaceholderView {
pub fn new() -> Self {
let s: Self = Object::builder().build();
+ s
+ }
+}
+
+/// This is the view to show if the application has not yet been configured. It will walk the user
+/// through the most critical setup steps so that we can move on to the other views in the app.
+pub struct WelcomeViewPrivate {}
+
+#[glib::object_subclass]
+impl ObjectSubclass for WelcomeViewPrivate {
+ const NAME: &'static str = "WelcomeView";
+ type Type = WelcomeView;
+ type ParentType = gtk::Box;
+
+ fn new() -> Self {
+ Self {}
+ }
+}
+
+impl ObjectImpl for WelcomeViewPrivate {}
+impl WidgetImpl for WelcomeViewPrivate {}
+impl BoxImpl for WelcomeViewPrivate {}
+
+glib::wrapper! {
+ pub struct WelcomeView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
+}
+
+impl WelcomeView {
+ pub fn new(on_save: Box) -> Self
+ where
+ F: Fn(PathBuf) + 'static,
+ {
+ let s: Self = Object::builder().build();
+ s.set_orientation(gtk::Orientation::Vertical);
+ s.set_css_classes(&["welcome"]);
// Replace this with the welcome screen that we set up in the fitnesstrax/unconfigured-page
// branch.
- let label = gtk::Label::builder()
- .label("Database is not configured.")
+ let title = gtk::Label::builder()
+ .label("Welcome to FitnessTrax")
+ .css_classes(["welcome-title"])
.build();
- s.append(&label);
+
+ let content = gtk::Box::builder()
+ .css_classes(["model-content"])
+ .orientation(gtk::Orientation::Vertical)
+ .vexpand(true)
+ .build();
+
+ let save_button = gtk::Button::builder()
+ .label("Save Settings")
+ .sensitive(false)
+ .build();
+
+ // The database selection row should be a box that shows a default database path, along with a
+ // button that triggers a file chooser dialog. Once the dialog returns, the box should be
+ // updated to reflect the chosen path.
+ let db_row = FileChooserRow::new({
+ let save_button = save_button.clone();
+ move |_| save_button.set_sensitive(true)
+ });
+
+ content.append(>k::Label::new(Some("Welcome to FitnessTrax. The application has not yet been configured, so I will walk you through that. Let's start out by selecting your database.")));
+ content.append(&db_row);
+
+ let on_save = on_save;
+ save_button.connect_clicked({
+ move |_| {
+ if let Some(path) = db_row.path() {
+ on_save(path)
+ }
+ }
+ });
+
+ s.append(&title);
+ s.append(&content);
+ s.append(&save_button);
+
s
}
}
@@ -119,11 +220,12 @@ impl HistoricalView {
/// The application window, or the main window, is the main user interface for the app. Almost
/// everything occurs here.
+#[derive(Clone)]
struct AppWindow {
app: App,
window: adw::ApplicationWindow,
layout: gtk::Box,
- current_view: RefCell,
+ current_view: Rc>,
}
impl AppWindow {
@@ -140,12 +242,6 @@ impl AppWindow {
.height_request(600)
.build();
- let current_view = if app.database.read().unwrap().is_none() {
- UnconfiguredView::new().upcast()
- } else {
- HistoricalView::new().upcast()
- };
-
let stylesheet = String::from_utf8(
resources_lookup_data(
&format!("{}style.css", RESOURCE_BASE_PATH),
@@ -169,18 +265,47 @@ impl AppWindow {
let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
+
+ let initial_view = PlaceholderView::new();
+
layout.append(&header);
- layout.append(¤t_view);
+ layout.append(&initial_view);
window.set_content(Some(&layout));
window.present();
let s = Self {
- app,
+ app: app.clone(),
window,
layout,
- current_view: RefCell::new(current_view),
+ 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
}
@@ -215,7 +340,7 @@ fn main() {
println!("database path: {}", settings.string("series-path"));
- let app = App::new();
+ let app = App::new(settings);
/*
let runtime = tokio::runtime::Builder::new_multi_thread()
diff --git a/fitnesstrax/app/src/ui/mod.rs b/fitnesstrax/app/src/ui/mod.rs
new file mode 100644
index 0000000..14f1243
--- /dev/null
+++ b/fitnesstrax/app/src/ui/mod.rs
@@ -0,0 +1,131 @@
+/*
+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 .
+*/
+
+use glib::Object;
+use gtk::{prelude::*, subclass::prelude::*};
+use std::{
+ cell::RefCell,
+ path::{Path, PathBuf},
+ rc::Rc,
+};
+
+pub struct FileChooserRowPrivate {
+ path: RefCell