From 806568cbde8483e3041d7a5e5c1bffa7b05be7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Wagner?= Date: Wed, 26 Oct 2022 16:23:55 +0200 Subject: [PATCH] First usable version --- Cargo.lock | 314 ++++------------------------ meson.build | 30 ++- src/data/Cargo.toml | 1 + src/data/src/lib.rs | 157 ++++++++------ src/gui/Cargo.toml | 9 +- src/gui/src/application.rs | 33 +-- src/gui/src/bigger.css | 7 + src/gui/src/config.rs | 1 - src/gui/src/gtimelog4.gresource.xml | 2 + src/gui/src/main.rs | 2 +- src/gui/src/style.css | 8 + src/gui/src/timeentry.rs | 100 ++++++--- src/gui/src/window.rs | 200 ++++++++++++++---- src/gui/src/window.ui | 74 +++++-- 14 files changed, 487 insertions(+), 451 deletions(-) create mode 100644 src/gui/src/bigger.css create mode 100644 src/gui/src/style.css diff --git a/Cargo.lock b/Cargo.lock index cc7fe72..c626ead 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,17 +111,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "clipboard-win" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219" -dependencies = [ - "error-code", - "str-buf", - "winapi", -] - [[package]] name = "codespan-reporting" version = "0.11.1" @@ -188,6 +177,7 @@ version = "0.1.0" dependencies = [ "chrono", "diesel", + "diesel_migrations", ] [[package]] @@ -214,92 +204,14 @@ dependencies = [ ] [[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-next" +name = "diesel_migrations" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +checksum = "e9ae22beef5e9d6fab9225ddb073c1c6c1a7a6ded5019d5da11d1e5c5adc34e2" dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "error-code" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" -dependencies = [ - "libc", - "str-buf", -] - -[[package]] -name = "fd-lock" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11dcc7e4d79a8c89b9ab4c6f5c30b1fc4a83c420792da3542fd31179ed5f517" -dependencies = [ - "cfg-if", - "rustix", - "windows-sys", + "diesel", + "migrations_internals", + "migrations_macros", ] [[package]] @@ -434,17 +346,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "getrandom" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - [[package]] name = "gettext-rs" version = "0.7.0" @@ -621,11 +522,13 @@ dependencies = [ name = "gtimelog4" version = "0.1.0" dependencies = [ + "anyhow", + "chrono", + "data", "gettext-rs", "glib-build-tools", "gtk4", "libadwaita", - "rtimelog", ] [[package]] @@ -714,12 +617,6 @@ dependencies = [ "cxx-build", ] -[[package]] -name = "io-lifetimes" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e481ccbe3dea62107216d0d1138bb8ad8e5e5c43009a098bd1990272c497b0" - [[package]] name = "js-sys" version = "0.3.60" @@ -795,12 +692,6 @@ dependencies = [ "cc", ] -[[package]] -name = "linux-raw-sys" -version = "0.0.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" - [[package]] name = "locale_config" version = "0.3.0" @@ -848,23 +739,24 @@ dependencies = [ ] [[package]] -name = "nibble_vec" -version = "0.1.0" +name = "migrations_internals" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +checksum = "c493c09323068c01e54c685f7da41a9ccf9219735c3766fbfd6099806ea08fbc" dependencies = [ - "smallvec", + "serde", + "toml", ] [[package]] -name = "nix" -version = "0.24.2" +name = "migrations_macros" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" +checksum = "8a8ff27a350511de30cdabb77147501c36ef02e0451d957abea2f30caffb2b58" dependencies = [ - "bitflags", - "cfg-if", - "libc", + "migrations_internals", + "proc-macro2", + "quote", ] [[package]] @@ -1028,36 +920,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall", - "thiserror", -] - [[package]] name = "regex" version = "1.6.0" @@ -1075,16 +937,6 @@ version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" -[[package]] -name = "rtimelog" -version = "0.1.0" -source = "git+https://github.com/martinpitt/rtimelog/#a8f74baa66194bff8b5b177d35f0e98b71ce6dfb" -dependencies = [ - "chrono", - "dirs", - "rustyline", -] - [[package]] name = "rustc_version" version = "0.3.3" @@ -1094,49 +946,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.35.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb2fda4666def1433b1b05431ab402e42a1084285477222b72d6c564c417cef" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustyline" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1cd5ae51d3f7bf65d7969d579d502168ef578f289452bd8ccc91de28fda20e" -dependencies = [ - "bitflags", - "cfg-if", - "clipboard-win", - "dirs-next", - "fd-lock", - "libc", - "log", - "memchr", - "nix", - "radix_trie", - "scopeguard", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "winapi", -] - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - [[package]] name = "scratch" version = "1.0.2" @@ -1166,6 +975,20 @@ name = "serde" version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "slab" @@ -1182,12 +1005,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" - [[package]] name = "syn" version = "1.0.103" @@ -1254,7 +1071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi", "winapi", ] @@ -1279,24 +1096,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" -[[package]] -name = "unicode-segmentation" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" - [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "utf8parse" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" - [[package]] name = "vcpkg" version = "0.2.15" @@ -1321,12 +1126,6 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - [[package]] name = "wasm-bindgen" version = "0.2.83" @@ -1411,46 +1210,3 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" diff --git a/meson.build b/meson.build index e9202f9..9488695 100644 --- a/meson.build +++ b/meson.build @@ -10,9 +10,37 @@ gnome = import('gnome') subdir('data') -subdir('src') +#subdir('src') subdir('po') +cargo_bin = find_program('cargo') +cargo_opt = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ] +cargo_opt += [ '--target-dir', meson.project_build_root() / 'src' ] +cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ] + +if get_option('buildtype') == 'release' + cargo_options += [ '--release' ] + rust_target = 'release' +else + rust_target = 'debug' +endif + +cargo_build = custom_target( + 'cargo-build', + build_by_default: true, + build_always_stale: true, + output: meson.project_name(), + console: true, + install: true, + install_dir: get_option('bindir'), + command: [ + 'env', cargo_env, + cargo_bin, 'build', + cargo_opt, '&&', 'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@', + ] +) + + gnome.post_install( glib_compile_schemas: true, gtk_update_icon_cache: true, diff --git a/src/data/Cargo.toml b/src/data/Cargo.toml index 639ca29..bc957b5 100644 --- a/src/data/Cargo.toml +++ b/src/data/Cargo.toml @@ -7,4 +7,5 @@ edition = "2021" [dependencies] diesel = { version = "2.0.2", features = ["chrono", "sqlite", "returning_clauses_for_sqlite_3_35"] } +diesel_migrations = "2.0.0" chrono = "0.4.22" diff --git a/src/data/src/lib.rs b/src/data/src/lib.rs index acc8901..321a663 100644 --- a/src/data/src/lib.rs +++ b/src/data/src/lib.rs @@ -1,17 +1,23 @@ -use chrono::{Datelike, NaiveDateTime}; -use diesel::{AsChangeset, Connection, Identifiable, Insertable, Queryable, RunQueryDsl, SqliteConnection}; +use chrono::{Datelike, Duration, NaiveDateTime}; +use diesel::{ + AsChangeset, Connection, Identifiable, Insertable, Queryable, RunQueryDsl, SqliteConnection, +}; +use diesel_migrations::MigrationHarness; use schema::entries; mod schema; -#[derive(Clone, Queryable, Identifiable, AsChangeset)] +pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = + diesel_migrations::embed_migrations!(); + +#[derive(Clone, Default, Debug, Queryable, Identifiable, AsChangeset)] #[diesel(table_name = entries)] pub struct Entry { id: i32, - start: Option, - stop: Option, - task: Option, + pub start: Option, + pub stop: Option, + pub task: Option, } #[derive(Insertable)] @@ -23,83 +29,116 @@ pub struct NewEntry { } pub struct Timelog { - entries: Vec, + conn: SqliteConnection, +} + +impl Default for Timelog { + fn default() -> Self { + let mut conn = get_connection(); + conn.run_pending_migrations(MIGRATIONS).unwrap(); + Self { conn } + } } impl Timelog { pub fn new() -> Self { - use schema::entries::dsl::*; - let mut conn = get_connection(); - - let items: Vec = entries.load(&mut conn).expect("Could not load entries"); - - - Self { - entries: items - } + conn.run_pending_migrations(MIGRATIONS).unwrap(); + Self { conn } } - pub fn add_entry>(&mut self, task_name: P) { + pub fn add_entry>(&mut self, task_name: P) -> Entry { use schema::entries::dsl::*; - let mut conn = get_connection(); + let now = chrono::Local::now().naive_local(); + let mut start_time = None; + // update last entry + let mut today = self.get_today(); + if let Some(last) = today.last_mut() { + start_time = last.stop; + } + + // create new entry let entry = NewEntry { - start: Some(chrono::Local::now().naive_local()), - stop: None, + start: start_time, + stop: Some(now), task: Some(task_name.as_ref().to_string()), }; - let saved_entry = diesel::insert_into(entries).values(&entry).get_result::(&mut conn).expect("Error saving entry"); - - self.entries.push(saved_entry); + diesel::insert_into(entries) + .values(&entry) + .get_result::(&mut self.conn) + .expect("Error saving entry") } - pub fn update_entry(&self, item: &Entry) { - let mut conn = get_connection(); - diesel::update(item).set(item).execute(&mut conn).expect("Could not update Entry"); + pub fn update_entry(&mut self, item: &Entry) { + diesel::update(item) + .set(item) + .execute(&mut self.conn) + .expect("Could not update Entry"); } - pub fn get_today(&self) -> Vec<&Entry> { + pub fn get_today(&mut self) -> Vec { + let e: Vec = entries::table + .load(&mut self.conn) + .expect("Could not load entries"); + let now = chrono::Local::now(); - let today_entries: Vec<_> = self.entries.iter().filter(|entry| { - if let Some(start) = entry.start { - if start.day() == now.day() { - return true; + let mut today_entries: Vec<_> = e + .into_iter() + .filter(|entry| { + if let Some(stop) = entry.stop { + if stop.day() == now.day() { + return true; + } } - } - return false; - }).collect(); + return false; + }) + .collect(); + today_entries.sort_by_key(|entry| entry.stop); return today_entries; } + + pub fn get_work_time_today(&mut self) -> Duration { + let entries_today = self.get_today(); + + let mut work = Duration::zero(); + for entry in entries_today { + if let Some(task) = entry.task { + if !task.starts_with("**") { + if let (Some(start), Some(stop)) = (entry.start, entry.stop) { + let duration = stop - start; + work = work + duration; + } + } + } + } + + work + } + + pub fn get_slack_time_today(&mut self) -> Duration { + let entries_today = self.get_today(); + + let mut slack = Duration::zero(); + for entry in entries_today { + if let Some(task) = entry.task { + if task.contains("**") { + if let (Some(start), Some(stop)) = (entry.start, entry.stop) { + let duration = stop - start; + slack = slack + duration; + } + } + } + } + + slack + } } pub fn get_connection() -> SqliteConnection { let database_url = "sqlite://data.db"; - SqliteConnection::establish(database_url).unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) + SqliteConnection::establish(database_url) + .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_save_entry() { - let mut timelog = Timelog::new(); - - timelog.add_entry("MyTask"); - } - - #[test] - fn test_update_entry() { - let mut timelog = Timelog::new(); - - let e: &Entry = { - let e = timelog.entries.first_mut().unwrap(); - e.task = Some(String::from("Changed taskname")); - timelog.entries.first().unwrap() - }; - timelog.update_entry(e); - } -} \ No newline at end of file diff --git a/src/gui/Cargo.toml b/src/gui/Cargo.toml index 6dd9398..b384d3a 100644 --- a/src/gui/Cargo.toml +++ b/src/gui/Cargo.toml @@ -7,12 +7,15 @@ edition = "2021" gettext-rs = { version = "0.7", features = ["gettext-system"] } gtk = { version = "0.5.0", package = "gtk4" } #rtimelog = { path = "/home/gunibert/Projekte/rtimelog" } -rtimelog = { git = "https://github.com/martinpitt/rtimelog/"} +#rtimelog = { git = "https://github.com/martinpitt/rtimelog/"} +data = { path = "../data"} +chrono = "0.4.22" +anyhow = "1.0.66" [dependencies.adw] package = "libadwaita" -version = "0.2.0-alpha.2" -# features = ["v1_2"] +version = "0.2.0" +features = ["v1_2"] [build-dependencies] glib-build-tools = "0.16.0" \ No newline at end of file diff --git a/src/gui/src/application.rs b/src/gui/src/application.rs index 2852755..504809a 100644 --- a/src/gui/src/application.rs +++ b/src/gui/src/application.rs @@ -18,16 +18,15 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +use crate::config::VERSION; +use adw::subclass::prelude::*; use glib::clone; use gtk::prelude::*; -use adw::subclass::prelude::*; use gtk::{gio, glib}; -use crate::config::VERSION; use crate::Gtimelog4Window; mod imp { - use std::path::PathBuf; use super::*; #[derive(Debug, Default)] @@ -47,7 +46,6 @@ mod imp { obj.setup_gactions(); obj.set_accels_for_action("app.quit", &["q"]); - } } @@ -62,7 +60,7 @@ mod imp { let window = if let Some(window) = application.active_window() { window } else { - let timelog = rtimelog::store::Timelog::new_from_file(&PathBuf::from("/home/gunibert/.local/share/gtimelog/timelog.txt")); + let timelog = data::Timelog::new(); let window = Gtimelog4Window::new(&*application); window.set_timelog(timelog); window.upcast() @@ -85,7 +83,10 @@ glib::wrapper! { impl Gtimelog4Application { pub fn new(application_id: &str, flags: &gio::ApplicationFlags) -> Self { - glib::Object::new::(&[("application-id", &application_id), ("flags", flags)]) + glib::Object::new::(&[ + ("application-id", &application_id), + ("flags", flags), + ]) } fn setup_gactions(&self) { @@ -104,16 +105,16 @@ impl Gtimelog4Application { fn show_about(&self) { let window = self.active_window().unwrap(); - // let about = adw::AboutWindow::builder() - // .transient_for(&window) - // .application_name("gtimelog4") - // .application_icon("de.gunibert.gtimelog4") - // .developer_name("Günther Wagner") - // .version(VERSION) - // .developers(vec!["Günther Wagner".into()]) - // .copyright("© 2022 Günther Wagner") - // .build(); + let about = adw::AboutWindow::builder() + .transient_for(&window) + .application_name("gtimelog4") + .application_icon("de.gunibert.gtimelog4") + .developer_name("Günther Wagner") + .version(VERSION) + .developers(vec!["Günther Wagner".into()]) + .copyright("© 2022 Günther Wagner") + .build(); - // about.present(); + about.present(); } } diff --git a/src/gui/src/bigger.css b/src/gui/src/bigger.css new file mode 100644 index 0000000..10c4bdc --- /dev/null +++ b/src/gui/src/bigger.css @@ -0,0 +1,7 @@ +.data-table { + font-size: 1.3em; +} + +header label { + font-size: 1.4em; +} \ No newline at end of file diff --git a/src/gui/src/config.rs b/src/gui/src/config.rs index 441fb75..2db33ab 100644 --- a/src/gui/src/config.rs +++ b/src/gui/src/config.rs @@ -1,4 +1,3 @@ pub static VERSION: &str = "0.1.0"; pub static GETTEXT_PACKAGE: &str = "gtimelog4"; pub static LOCALEDIR: &str = "/usr/local/share/locale"; -pub static PKGDATADIR: &str = "/usr/local/share/gtimelog4"; diff --git a/src/gui/src/gtimelog4.gresource.xml b/src/gui/src/gtimelog4.gresource.xml index dca43d0..4a6aeaf 100644 --- a/src/gui/src/gtimelog4.gresource.xml +++ b/src/gui/src/gtimelog4.gresource.xml @@ -3,5 +3,7 @@ window.ui gtk/help-overlay.ui + bigger.css + style.css diff --git a/src/gui/src/main.rs b/src/gui/src/main.rs index a1cce89..346d1e7 100644 --- a/src/gui/src/main.rs +++ b/src/gui/src/main.rs @@ -26,7 +26,7 @@ mod timeentry; use self::application::Gtimelog4Application; use self::window::Gtimelog4Window; -use config::{GETTEXT_PACKAGE, LOCALEDIR, PKGDATADIR}; +use config::{GETTEXT_PACKAGE, LOCALEDIR}; use gettextrs::{bind_textdomain_codeset, bindtextdomain, textdomain}; use gtk::gio; use gtk::prelude::*; diff --git a/src/gui/src/style.css b/src/gui/src/style.css new file mode 100644 index 0000000..6818e8e --- /dev/null +++ b/src/gui/src/style.css @@ -0,0 +1,8 @@ + +.slackrow { + color: grey; +} + +.timelbl { + font-family: monospace; +} diff --git a/src/gui/src/timeentry.rs b/src/gui/src/timeentry.rs index 0974cd3..f2b768e 100644 --- a/src/gui/src/timeentry.rs +++ b/src/gui/src/timeentry.rs @@ -1,25 +1,24 @@ -use glib::Boxed; use gtk::glib; use gtk::glib::ToValue; -use gtk::subclass::prelude::{ObjectSubclassExt, ObjectSubclassIsExt}; +use gtk::subclass::prelude::ObjectSubclassIsExt; #[derive(glib::Boxed, Clone, Default)] #[boxed_type(name = "Entry")] -pub struct Entry(rtimelog::store::Entry); +pub struct Entry(pub data::Entry); -impl From for Entry { - fn from(rentry: rtimelog::store::Entry) -> Self { +impl From for Entry { + fn from(rentry: data::Entry) -> Self { Self(rentry) } } mod imp { - use std::cell::RefCell; - use std::rc::Rc; - use gtk::glib::once_cell::sync::Lazy; use super::*; - use gtk::subclass::prelude::{ObjectImpl, ObjectSubclass}; use crate::gio::glib::{ParamSpec, Value}; + use chrono::NaiveDateTime; + use gtk::glib::once_cell::sync::Lazy; + use gtk::subclass::prelude::{ObjectImpl, ObjectSubclass}; + use std::cell::RefCell; #[derive(Default)] pub struct TimeEntry { @@ -40,7 +39,9 @@ mod imp { static PROPERTIES: Lazy> = Lazy::new(|| { vec![ glib::ParamSpecBoxed::builder::("entry").build(), - glib::ParamSpecString::builder("task").build() + glib::ParamSpecString::builder("task").build(), + glib::ParamSpecString::builder("start").build(), + glib::ParamSpecString::builder("stop").build(), ] }); PROPERTIES.as_ref() @@ -53,22 +54,64 @@ mod imp { self.entry.replace(e); } "task" => { - let e = value.get().expect("Something went wrong"); + let e: String = value.get().expect("Something went wrong"); println!("Set Task: {}", e); - self.entry.borrow_mut().0.task = e; + self.entry.borrow_mut().0.task = Some(e); } - _ => unimplemented!() + "start" => { + let e: String = value.get().expect("Something went wront"); + + let start = NaiveDateTime::parse_from_str(&e, "%s").unwrap(); + + self.entry.borrow_mut().0.start = Some(start); + } + "stop" => { + let e: String = value.get().expect("Something went wront"); + + if let Ok(stop) = NaiveDateTime::parse_from_str(&e, "%s") { + self.entry.borrow_mut().0.stop = Some(stop); + } else if let Ok(stop) = chrono::NaiveTime::parse_from_str(&e, "%T") { + let mut entry = self.entry.borrow_mut(); + if let Some(old_stop) = entry.0.stop { + let new_stop = NaiveDateTime::new(old_stop.date(), stop); + entry.0.stop = Some(new_stop); + } + } + } + _ => unimplemented!(), } } fn property(&self, _id: usize, pspec: &ParamSpec) -> Value { match pspec.name() { "entry" => self.entry.borrow().to_value(), - "task" => { - println!("Task: {}", self.entry.borrow().0.task); - self.entry.borrow().0.task.to_value() - }, - _ => unimplemented!() + "task" => self + .entry + .borrow() + .0 + .task + .clone() + .unwrap_or_default() + .to_value(), + "start" => { + let e = self.entry.borrow(); + + if let Some(datetime) = e.0.start { + datetime.format("%T").to_string().to_value() + } else { + "-".to_value() + } + } + "stop" => { + let e = self.entry.borrow(); + + if let Some(datetime) = e.0.stop { + datetime.format("%T").to_string().to_value() + } else { + "-".to_value() + } + } + _ => unimplemented!(), } } } @@ -79,22 +122,25 @@ glib::wrapper! { } impl TimeEntry { - pub fn new(entry: rtimelog::store::Entry) -> Self { + pub fn new(entry: data::Entry) -> Self { let e: Entry = entry.into(); glib::Object::new(&[("entry", &e.to_value())]) } pub fn get_task(&self) -> String { - self.imp().entry.borrow().0.task.clone() - } - - pub fn get_time(&self) -> String { - self.imp().entry.borrow().0.stop.format("%T").to_string() + self.imp() + .entry + .borrow() + .0 + .task + .clone() + .unwrap_or_default() + .clone() } } -impl From for TimeEntry { - fn from(entry: rtimelog::store::Entry) -> Self { +impl From for TimeEntry { + fn from(entry: data::Entry) -> Self { TimeEntry::new(entry) } -} \ No newline at end of file +} diff --git a/src/gui/src/window.rs b/src/gui/src/window.rs index 0652104..67972ab 100644 --- a/src/gui/src/window.rs +++ b/src/gui/src/window.rs @@ -18,23 +18,24 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -use gtk::prelude::*; -use adw::subclass::prelude::*; -use gtk::{gio, glib, CompositeTemplate}; use crate::timeentry::TimeEntry; +use adw::subclass::prelude::*; +use gtk::prelude::*; +use gtk::{gio, glib, CompositeTemplate}; mod imp { - use std::cell::RefCell; - use std::rc::Rc; - use gtk::{ListItem, SignalListItemFactory}; - use gtk::glib::clone; - use crate::gio::glib::signal::Inhibit; use super::*; + use anyhow::anyhow; + use gtk::glib::clone; + use gtk::{ListItem, NoSelection, SelectionModel, SignalListItemFactory}; + use std::cell::RefCell; + use std::ops::Deref; + use std::rc::Rc; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Default, CompositeTemplate)] #[template(resource = "/de/gunibert/gtimelog4/window.ui")] pub struct Gtimelog4Window { - pub timelog: Rc>, + pub timelog: Rc>, // Template widgets #[template_child] @@ -42,11 +43,17 @@ mod imp { #[template_child] pub columnview: TemplateChild, #[template_child] - pub time_column: TemplateChild, + pub start_column: TemplateChild, + #[template_child] + pub stop_column: TemplateChild, #[template_child] pub task_column: TemplateChild, #[template_child] pub taskentry: TemplateChild, + #[template_child] + pub statistics: TemplateChild, + + pub provider: RefCell, } impl Gtimelog4Window { @@ -55,6 +62,59 @@ mod imp { lbl.set_xalign(0.0f32); item.set_child(Some(&lbl)); } + + fn setup_time_lbl(factory: &SignalListItemFactory, item: &ListItem) { + Gtimelog4Window::setup_lbl(factory, item); + let lbl = item.child(); + if let Some(widget) = lbl { + widget.add_css_class("timelbl"); + } + } + + fn get_model(&self) -> Result { + let model: SelectionModel = self + .columnview + .model() + .ok_or(anyhow!("Columnview has not model set"))?; + let selection = model + .downcast::() + .map_err(|_| anyhow!("Cannot downcast SelectionModel"))?; + let model = selection + .model() + .ok_or(anyhow!("SelectionModel has no model set"))?; + model + .downcast::() + .map_err(|_| anyhow!("ListModel is no ListStore")) + } + + fn update_statistics(&self) { + let work = self.timelog.borrow_mut().get_work_time_today(); + let work_seconds = work.num_seconds() % 60; + let work_minutes = (work.num_seconds() / 60) % 60; + let work_hours = (work.num_seconds() / 60 / 60) % 24; + + let slack = self.timelog.borrow_mut().get_slack_time_today(); + let slack_seconds = slack.num_seconds() % 60; + let slack_minutes = (slack.num_seconds() / 60) % 60; + let slack_hours = (slack.num_seconds() / 60 / 60) % 24; + + self.statistics.set_text(&format!( + "Work: {} hours, {} minutes, {} seconds\nSlack: {} hours, {} minutes, {} seconds", + work_hours, work_minutes, work_seconds, slack_hours, slack_minutes, slack_seconds + )); + } + + fn time_factory_bind(item: &ListItem, src_prop: &str) { + if let (Some(lbl), Some(entry)) = (item.child(), item.item()) { + let mylbl = lbl.downcast::().unwrap(); + let entry = entry.downcast::().unwrap(); + entry + .bind_property(src_prop, &mylbl, "text") + .sync_create() + .bidirectional() + .build(); + } + } } #[glib::object_subclass] @@ -76,55 +136,94 @@ mod imp { fn constructed(&self) { self.parent_constructed(); + self.provider.replace(gtk::CssProvider::new()); + self.provider + .borrow_mut() + .load_from_resource("/de/gunibert/gtimelog4/bigger.css"); + + self.update_statistics(); + let task_factory = SignalListItemFactory::new(); task_factory.connect_setup(Gtimelog4Window::setup_lbl); - task_factory.connect_bind(|_factory, item| { + task_factory.connect_bind(clone!(@weak self as this => move |_factory, item| { if let (Some(lbl), Some(entry)) = (item.child(), item.item()) { let mylbl = lbl.downcast::().unwrap(); + let entry = entry.downcast::().unwrap(); - println!("Initial Task: {}", entry.property::("task")); entry.bind_property("task", &mylbl, "text").sync_create().bidirectional().build(); + + if entry.get_task().starts_with("**") { + mylbl.add_css_class("slackrow"); + } + + mylbl.connect_changed(clone!(@weak this => move |_| { + let v: crate::timeentry::Entry = entry.property("entry"); + this.timelog.borrow_mut().update_entry(&v.0); + })); + } - }); + })); self.task_column.set_factory(Some(&task_factory)); - let time_factory = SignalListItemFactory::new(); - time_factory.connect_setup(Gtimelog4Window::setup_lbl); - time_factory.connect_bind(|_factory, item| { - if let (Some(lbl), Some(entry)) = (item.child(), item.item()) { - let mylbl = lbl.downcast::().unwrap(); - let entry = entry.downcast::().unwrap(); - mylbl.set_text(&entry.get_time()); + let start_factory = SignalListItemFactory::new(); + start_factory.connect_setup(Gtimelog4Window::setup_time_lbl); + start_factory.connect_bind(|_factory, item| Self::time_factory_bind(item, "start")); + + self.start_column.set_factory(Some(&start_factory)); + + let stop_factory = SignalListItemFactory::new(); + stop_factory.connect_setup(Gtimelog4Window::setup_time_lbl); + stop_factory.connect_bind(|_factory, item| Self::time_factory_bind(item, "stop")); + + self.stop_column.set_factory(Some(&stop_factory)); + + self.taskentry.connect_activate(clone!(@weak self as this, @strong self.timelog as timelog, @strong self.columnview as columnview => move |entry| { + let item = timelog.borrow_mut().add_entry(entry.text()); + + if let Ok(model) = this.get_model() { + let n_items = model.n_items(); + if n_items > 0 { + let last_item = model.item(n_items - 1); + if let (Some(last_item), Some(start)) = (last_item, item.start) { + last_item.set_property("stop", start.format("%s").to_string().to_value()); + } + } + + let timeentry: TimeEntry = item.into(); + + model.append(&timeentry); + this.update_statistics(); + entry.set_text(""); } - }); - - self.time_column.set_factory(Some(&time_factory)); - - self.taskentry.connect_activate(clone!(@strong self.timelog as timelog, @strong self.columnview as columnview => move |entry| { - timelog.borrow_mut().add(entry.text().to_string()); - timelog.borrow_mut().save(); - - let entries: Vec = timelog.borrow().get_today().to_vec().into_iter().map(|entry| entry.into()).collect(); - let model = gio::ListStore::new(TimeEntry::static_type()); - for entry in entries { - model.append(&entry); - } - columnview.set_model(Some(>k::NoSelection::new(Some(&model)))); })); } } - impl WidgetImpl for Gtimelog4Window {} - - impl WindowImpl for Gtimelog4Window { - fn close_request(&self) -> Inhibit { - println!("Close request"); - self.timelog.borrow_mut().save().expect("TODO: panic message"); - Inhibit(false) + impl WidgetImpl for Gtimelog4Window { + fn size_allocate(&self, width: i32, height: i32, baseline: i32) { + self.parent_size_allocate(width, height, baseline); + if width > 800 { + if let Some(display) = gtk::gdk::Display::default() { + gtk::StyleContext::add_provider_for_display( + &display, + self.provider.borrow().deref(), + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + } + } else { + if let Some(display) = gtk::gdk::Display::default() { + gtk::StyleContext::remove_provider_for_display( + &display, + self.provider.borrow().deref(), + ); + } + } } } + impl WindowImpl for Gtimelog4Window {} + impl ApplicationWindowImpl for Gtimelog4Window {} impl AdwApplicationWindowImpl for Gtimelog4Window {} @@ -136,18 +235,27 @@ glib::wrapper! { } impl Gtimelog4Window { - pub fn new>(application: &P) -> Self { + pub fn new>(application: &P) -> Self { glib::Object::new(&[("application", application)]) } - pub fn set_timelog(&self, timelog: rtimelog::store::Timelog) { + pub fn set_timelog(&self, timelog: data::Timelog) { self.imp().timelog.replace(timelog); - let entries: Vec = self.imp().timelog.borrow().get_this_week().to_vec().into_iter().map(|entry| entry.into()).collect(); + let entries: Vec = self + .imp() + .timelog + .borrow_mut() + .get_today() + .into_iter() + .map(|entry| entry.into()) + .collect(); let model = gio::ListStore::new(TimeEntry::static_type()); for entry in entries { model.append(&entry); } - self.imp().columnview.set_model(Some(>k::NoSelection::new(Some(&model)))); + self.imp() + .columnview + .set_model(Some(>k::NoSelection::new(Some(&model)))); } } diff --git a/src/gui/src/window.ui b/src/gui/src/window.ui index da8053d..4d0a3d7 100644 --- a/src/gui/src/window.ui +++ b/src/gui/src/window.ui @@ -19,30 +19,68 @@ - - True - + - - Time - - - - - Task - True + + True + + + + Start + 110 + true + + + + + Stop + 110 + true + + + + + Task + True + true + + + - - 6 - 6 - 6 - 6 + + vertical + + + test + center + 0.0 + + + + + 6 + 6 + 6 + 6 + 6 + + + true + + + + + Send to Personio... + + + +