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-18 23:30:41 +00:00
use adw ::prelude ::* ;
use emseries ::Series ;
use ft_core ::TraxRecord ;
2023-12-18 16:59:56 +00:00
use gio ::resources_lookup_data ;
2023-12-18 23:30:41 +00:00
use glib ::Object ;
2023-12-19 00:08:32 +00:00
use gtk ::{ subclass ::prelude ::* , STYLE_PROVIDER_PRIORITY_USER } ;
2023-12-18 23:30:41 +00:00
use std ::{
cell ::RefCell ,
env ,
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-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 {
pub fn new ( ) -> Self {
Self {
database : Arc ::new ( RwLock ::new ( None ) ) ,
}
}
}
2023-12-18 23:36:22 +00:00
/// 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.
2023-12-19 01:04:55 +00:00
pub struct WelcomeViewPrivate { }
2023-12-18 23:30:41 +00:00
#[ glib::object_subclass ]
2023-12-19 01:04:55 +00:00
impl ObjectSubclass for WelcomeViewPrivate {
const NAME : & 'static str = " WelcomeView " ;
type Type = WelcomeView ;
2023-12-18 23:30:41 +00:00
type ParentType = gtk ::Box ;
fn new ( ) -> Self {
Self { }
}
}
2023-12-19 01:04:55 +00:00
impl ObjectImpl for WelcomeViewPrivate { }
impl WidgetImpl for WelcomeViewPrivate { }
impl BoxImpl for WelcomeViewPrivate { }
2023-12-18 23:30:41 +00:00
glib ::wrapper! {
2023-12-19 01:04:55 +00:00
pub struct WelcomeView ( ObjectSubclass < WelcomeViewPrivate > ) @ extends gtk ::Box , gtk ::Widget , @ implements gtk ::Orientable ;
2023-12-18 23:30:41 +00:00
}
2023-12-19 01:04:55 +00:00
impl WelcomeView {
2023-12-18 23:30:41 +00:00
pub fn new ( ) -> Self {
let s : Self = Object ::builder ( ) . build ( ) ;
2023-12-19 01:04:55 +00:00
s . set_orientation ( gtk ::Orientation ::Vertical ) ;
2023-12-18 23:30:41 +00:00
2023-12-18 23:36:22 +00:00
// Replace this with the welcome screen that we set up in the fitnesstrax/unconfigured-page
// branch.
2023-12-18 23:30:41 +00:00
let label = gtk ::Label ::builder ( )
2023-12-19 01:04:55 +00:00
. label ( " Welcome to FitnessTrax " )
2023-12-18 23:30:41 +00:00
. build ( ) ;
2023-12-19 01:04:55 +00:00
2023-12-18 23:30:41 +00:00
s . append ( & label ) ;
2023-12-19 01:04:55 +00:00
s . append ( & gtk ::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. " ) ) ) ;
// 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 = gtk ::Box ::builder ( )
. orientation ( gtk ::Orientation ::Horizontal )
. build ( ) ;
db_row . append (
& gtk ::Label ::builder ( )
. label ( " No Path Selected " )
. hexpand ( true )
. build ( ) ,
) ;
db_row . append ( & gtk ::Button ::builder ( ) . label ( " Select Database " ) . build ( ) ) ;
s . append ( & db_row ) ;
s . append ( & gtk ::Button ::builder ( ) . label ( " Save Settings " ) . build ( ) ) ;
2023-12-18 23:30:41 +00:00
s
}
}
2023-12-18 23:36:22 +00:00
/// The historical view will show a window into the main database. It will show some version of
/// daily summaries, daily details, and will provide all functions the user may need for editing
/// records.
2023-12-18 23:30:41 +00:00
pub struct HistoricalViewPrivate { }
#[ glib::object_subclass ]
impl ObjectSubclass for HistoricalViewPrivate {
const NAME : & 'static str = " HistoricalView " ;
type Type = HistoricalView ;
type ParentType = gtk ::Box ;
fn new ( ) -> Self {
Self { }
}
}
2023-11-20 03:47:36 +00:00
2023-12-18 23:30:41 +00:00
impl ObjectImpl for HistoricalViewPrivate { }
impl WidgetImpl for HistoricalViewPrivate { }
impl BoxImpl for HistoricalViewPrivate { }
glib ::wrapper! {
pub struct HistoricalView ( ObjectSubclass < HistoricalViewPrivate > ) @ extends gtk ::Box , gtk ::Widget ;
}
impl HistoricalView {
pub fn new ( ) -> Self {
let s : Self = Object ::builder ( ) . build ( ) ;
let label = gtk ::Label ::builder ( )
. label ( " Database has been configured and now it is time to show data " )
. build ( ) ;
s . append ( & label ) ;
s
}
}
/// The application window, or the main window, is the main user interface for the app. Almost
/// everything occurs here.
2023-11-20 03:47:36 +00:00
struct AppWindow {
2023-12-18 23:30:41 +00:00
app : App ,
2023-11-20 03:47:36 +00:00
window : adw ::ApplicationWindow ,
2023-12-19 00:08:32 +00:00
layout : gtk ::Box ,
current_view : RefCell < gtk ::Widget > ,
2023-12-18 23:30:41 +00:00
}
impl AppWindow {
/// Construct a new App Window.
///
/// adw_app is an Adwaita application. Application windows need to have access to this, but
/// 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 : App ) -> AppWindow {
let window = adw ::ApplicationWindow ::builder ( )
. application ( adw_app )
. width_request ( 800 )
. height_request ( 600 )
. build ( ) ;
let current_view = if app . database . read ( ) . unwrap ( ) . is_none ( ) {
2023-12-19 01:04:55 +00:00
WelcomeView ::new ( ) . upcast ( )
2023-12-18 23:30:41 +00:00
} else {
2023-12-19 00:08:32 +00:00
HistoricalView ::new ( ) . upcast ( )
2023-12-18 23:30:41 +00:00
} ;
let stylesheet = String ::from_utf8 (
resources_lookup_data (
& format! ( " {} style.css " , RESOURCE_BASE_PATH ) ,
gio ::ResourceLookupFlags ::NONE ,
)
. expect ( " stylesheet must be available in the resources " )
. to_vec ( ) ,
)
. expect ( " to parse stylesheet " ) ;
let provider = gtk ::CssProvider ::new ( ) ;
provider . load_from_data ( & stylesheet ) ;
let context = window . style_context ( ) ;
context . add_provider ( & provider , STYLE_PROVIDER_PRIORITY_USER ) ;
2023-12-19 00:08:32 +00:00
let header = adw ::HeaderBar ::builder ( )
. title_widget ( & gtk ::Label ::new ( Some ( " FitnessTrax " ) ) )
. build ( ) ;
let layout = gtk ::Box ::builder ( )
. orientation ( gtk ::Orientation ::Vertical )
. build ( ) ;
layout . append ( & header ) ;
layout . append ( & current_view ) ;
window . set_content ( Some ( & layout ) ) ;
2023-12-18 23:30:41 +00:00
window . present ( ) ;
let s = Self {
app ,
window ,
2023-12-19 00:08:32 +00:00
layout ,
2023-12-18 23:30:41 +00:00
current_view : RefCell ::new ( current_view ) ,
} ;
s
}
2023-12-18 23:36:22 +00:00
// Switch views.
//
// This function only replaces the old view with the one which matches the current view state.
2023-12-19 00:08:32 +00:00
// 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 ) ;
2023-12-18 23:30:41 +00:00
}
2023-11-20 03:47:36 +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 ) ;
println! ( " database path: {} " , settings . string ( " series-path " ) ) ;
2023-12-18 23:30:41 +00:00
let app = App ::new ( ) ;
2023-11-20 03:47:36 +00:00
/*
let runtime = tokio ::runtime ::Builder ::new_multi_thread ( )
. enable_all ( )
. build ( )
. unwrap ( ) ;
* /
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-18 23:30:41 +00:00
adw_app . connect_activate ( move | adw_app | {
AppWindow ::new ( adw_app , app . clone ( ) ) ;
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
}