First usable version

This commit is contained in:
Günther Wagner 2022-10-26 16:23:55 +02:00
parent 19c7824a0e
commit 806568cbde
14 changed files with 487 additions and 451 deletions

314
Cargo.lock generated
View File

@ -111,17 +111,6 @@ dependencies = [
"winapi", "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]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.11.1" version = "0.11.1"
@ -188,6 +177,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"diesel", "diesel",
"diesel_migrations",
] ]
[[package]] [[package]]
@ -214,92 +204,14 @@ dependencies = [
] ]
[[package]] [[package]]
name = "dirs" name = "diesel_migrations"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-next"
version = "2.0.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" checksum = "e9ae22beef5e9d6fab9225ddb073c1c6c1a7a6ded5019d5da11d1e5c5adc34e2"
dependencies = [ dependencies = [
"cfg-if", "diesel",
"dirs-sys-next", "migrations_internals",
] "migrations_macros",
[[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",
] ]
[[package]] [[package]]
@ -434,17 +346,6 @@ dependencies = [
"system-deps", "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]] [[package]]
name = "gettext-rs" name = "gettext-rs"
version = "0.7.0" version = "0.7.0"
@ -621,11 +522,13 @@ dependencies = [
name = "gtimelog4" name = "gtimelog4"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"chrono",
"data",
"gettext-rs", "gettext-rs",
"glib-build-tools", "glib-build-tools",
"gtk4", "gtk4",
"libadwaita", "libadwaita",
"rtimelog",
] ]
[[package]] [[package]]
@ -714,12 +617,6 @@ dependencies = [
"cxx-build", "cxx-build",
] ]
[[package]]
name = "io-lifetimes"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e481ccbe3dea62107216d0d1138bb8ad8e5e5c43009a098bd1990272c497b0"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.60" version = "0.3.60"
@ -795,12 +692,6 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d"
[[package]] [[package]]
name = "locale_config" name = "locale_config"
version = "0.3.0" version = "0.3.0"
@ -848,23 +739,24 @@ dependencies = [
] ]
[[package]] [[package]]
name = "nibble_vec" name = "migrations_internals"
version = "0.1.0" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" checksum = "c493c09323068c01e54c685f7da41a9ccf9219735c3766fbfd6099806ea08fbc"
dependencies = [ dependencies = [
"smallvec", "serde",
"toml",
] ]
[[package]] [[package]]
name = "nix" name = "migrations_macros"
version = "0.24.2" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" checksum = "8a8ff27a350511de30cdabb77147501c36ef02e0451d957abea2f30caffb2b58"
dependencies = [ dependencies = [
"bitflags", "migrations_internals",
"cfg-if", "proc-macro2",
"libc", "quote",
] ]
[[package]] [[package]]
@ -1028,36 +920,6 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "regex" name = "regex"
version = "1.6.0" version = "1.6.0"
@ -1075,16 +937,6 @@ version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]]
name = "rtimelog"
version = "0.1.0"
source = "git+https://github.com/martinpitt/rtimelog/#a8f74baa66194bff8b5b177d35f0e98b71ce6dfb"
dependencies = [
"chrono",
"dirs",
"rustyline",
]
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.3.3" version = "0.3.3"
@ -1094,49 +946,6 @@ dependencies = [
"semver", "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]] [[package]]
name = "scratch" name = "scratch"
version = "1.0.2" version = "1.0.2"
@ -1166,6 +975,20 @@ name = "serde"
version = "1.0.147" version = "1.0.147"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" 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]] [[package]]
name = "slab" name = "slab"
@ -1182,12 +1005,6 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "str-buf"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.103" version = "1.0.103"
@ -1254,7 +1071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [ dependencies = [
"libc", "libc",
"wasi 0.10.0+wasi-snapshot-preview1", "wasi",
"winapi", "winapi",
] ]
@ -1279,24 +1096,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "unicode-segmentation"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.10" version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "utf8parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 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]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.83" version = "0.2.83"
@ -1411,46 +1210,3 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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"

View File

@ -10,9 +10,37 @@ gnome = import('gnome')
subdir('data') subdir('data')
subdir('src') #subdir('src')
subdir('po') 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( gnome.post_install(
glib_compile_schemas: true, glib_compile_schemas: true,
gtk_update_icon_cache: true, gtk_update_icon_cache: true,

View File

@ -7,4 +7,5 @@ edition = "2021"
[dependencies] [dependencies]
diesel = { version = "2.0.2", features = ["chrono", "sqlite", "returning_clauses_for_sqlite_3_35"] } diesel = { version = "2.0.2", features = ["chrono", "sqlite", "returning_clauses_for_sqlite_3_35"] }
diesel_migrations = "2.0.0"
chrono = "0.4.22" chrono = "0.4.22"

View File

@ -1,17 +1,23 @@
use chrono::{Datelike, NaiveDateTime}; use chrono::{Datelike, Duration, NaiveDateTime};
use diesel::{AsChangeset, Connection, Identifiable, Insertable, Queryable, RunQueryDsl, SqliteConnection}; use diesel::{
AsChangeset, Connection, Identifiable, Insertable, Queryable, RunQueryDsl, SqliteConnection,
};
use diesel_migrations::MigrationHarness;
use schema::entries; use schema::entries;
mod schema; 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)] #[diesel(table_name = entries)]
pub struct Entry { pub struct Entry {
id: i32, id: i32,
start: Option<NaiveDateTime>, pub start: Option<NaiveDateTime>,
stop: Option<NaiveDateTime>, pub stop: Option<NaiveDateTime>,
task: Option<String>, pub task: Option<String>,
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -23,83 +29,116 @@ pub struct NewEntry {
} }
pub struct Timelog { pub struct Timelog {
entries: Vec<Entry>, conn: SqliteConnection,
}
impl Default for Timelog {
fn default() -> Self {
let mut conn = get_connection();
conn.run_pending_migrations(MIGRATIONS).unwrap();
Self { conn }
}
} }
impl Timelog { impl Timelog {
pub fn new() -> Self { pub fn new() -> Self {
use schema::entries::dsl::*;
let mut conn = get_connection(); let mut conn = get_connection();
conn.run_pending_migrations(MIGRATIONS).unwrap();
let items: Vec<Entry> = entries.load(&mut conn).expect("Could not load entries"); Self { conn }
Self {
entries: items
}
} }
pub fn add_entry<P: AsRef<str>>(&mut self, task_name: P) { pub fn add_entry<P: AsRef<str>>(&mut self, task_name: P) -> Entry {
use schema::entries::dsl::*; 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 { let entry = NewEntry {
start: Some(chrono::Local::now().naive_local()), start: start_time,
stop: None, stop: Some(now),
task: Some(task_name.as_ref().to_string()), task: Some(task_name.as_ref().to_string()),
}; };
let saved_entry = diesel::insert_into(entries).values(&entry).get_result::<Entry>(&mut conn).expect("Error saving entry"); diesel::insert_into(entries)
.values(&entry)
self.entries.push(saved_entry); .get_result::<Entry>(&mut self.conn)
.expect("Error saving entry")
} }
pub fn update_entry(&self, item: &Entry) { pub fn update_entry(&mut self, item: &Entry) {
let mut conn = get_connection(); diesel::update(item)
diesel::update(item).set(item).execute(&mut conn).expect("Could not update Entry"); .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<Entry> {
let e: Vec<Entry> = entries::table
.load(&mut self.conn)
.expect("Could not load entries");
let now = chrono::Local::now(); let now = chrono::Local::now();
let today_entries: Vec<_> = self.entries.iter().filter(|entry| { let mut today_entries: Vec<_> = e
if let Some(start) = entry.start { .into_iter()
if start.day() == now.day() { .filter(|entry| {
return true; if let Some(stop) = entry.stop {
if stop.day() == now.day() {
return true;
}
} }
} return false;
return false; })
}).collect(); .collect();
today_entries.sort_by_key(|entry| entry.stop);
return today_entries; 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 { pub fn get_connection() -> SqliteConnection {
let database_url = "sqlite://data.db"; 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);
}
}

View File

@ -7,12 +7,15 @@ edition = "2021"
gettext-rs = { version = "0.7", features = ["gettext-system"] } gettext-rs = { version = "0.7", features = ["gettext-system"] }
gtk = { version = "0.5.0", package = "gtk4" } gtk = { version = "0.5.0", package = "gtk4" }
#rtimelog = { path = "/home/gunibert/Projekte/rtimelog" } #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] [dependencies.adw]
package = "libadwaita" package = "libadwaita"
version = "0.2.0-alpha.2" version = "0.2.0"
# features = ["v1_2"] features = ["v1_2"]
[build-dependencies] [build-dependencies]
glib-build-tools = "0.16.0" glib-build-tools = "0.16.0"

View File

@ -18,16 +18,15 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
use crate::config::VERSION;
use adw::subclass::prelude::*;
use glib::clone; use glib::clone;
use gtk::prelude::*; use gtk::prelude::*;
use adw::subclass::prelude::*;
use gtk::{gio, glib}; use gtk::{gio, glib};
use crate::config::VERSION;
use crate::Gtimelog4Window; use crate::Gtimelog4Window;
mod imp { mod imp {
use std::path::PathBuf;
use super::*; use super::*;
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -47,7 +46,6 @@ mod imp {
obj.setup_gactions(); obj.setup_gactions();
obj.set_accels_for_action("app.quit", &["<primary>q"]); obj.set_accels_for_action("app.quit", &["<primary>q"]);
} }
} }
@ -62,7 +60,7 @@ mod imp {
let window = if let Some(window) = application.active_window() { let window = if let Some(window) = application.active_window() {
window window
} else { } 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); let window = Gtimelog4Window::new(&*application);
window.set_timelog(timelog); window.set_timelog(timelog);
window.upcast() window.upcast()
@ -85,7 +83,10 @@ glib::wrapper! {
impl Gtimelog4Application { impl Gtimelog4Application {
pub fn new(application_id: &str, flags: &gio::ApplicationFlags) -> Self { pub fn new(application_id: &str, flags: &gio::ApplicationFlags) -> Self {
glib::Object::new::<Gtimelog4Application>(&[("application-id", &application_id), ("flags", flags)]) glib::Object::new::<Gtimelog4Application>(&[
("application-id", &application_id),
("flags", flags),
])
} }
fn setup_gactions(&self) { fn setup_gactions(&self) {
@ -104,16 +105,16 @@ impl Gtimelog4Application {
fn show_about(&self) { fn show_about(&self) {
let window = self.active_window().unwrap(); let window = self.active_window().unwrap();
// let about = adw::AboutWindow::builder() let about = adw::AboutWindow::builder()
// .transient_for(&window) .transient_for(&window)
// .application_name("gtimelog4") .application_name("gtimelog4")
// .application_icon("de.gunibert.gtimelog4") .application_icon("de.gunibert.gtimelog4")
// .developer_name("Günther Wagner") .developer_name("Günther Wagner")
// .version(VERSION) .version(VERSION)
// .developers(vec!["Günther Wagner".into()]) .developers(vec!["Günther Wagner".into()])
// .copyright("© 2022 Günther Wagner") .copyright("© 2022 Günther Wagner")
// .build(); .build();
// about.present(); about.present();
} }
} }

7
src/gui/src/bigger.css Normal file
View File

@ -0,0 +1,7 @@
.data-table {
font-size: 1.3em;
}
header label {
font-size: 1.4em;
}

View File

@ -1,4 +1,3 @@
pub static VERSION: &str = "0.1.0"; pub static VERSION: &str = "0.1.0";
pub static GETTEXT_PACKAGE: &str = "gtimelog4"; pub static GETTEXT_PACKAGE: &str = "gtimelog4";
pub static LOCALEDIR: &str = "/usr/local/share/locale"; pub static LOCALEDIR: &str = "/usr/local/share/locale";
pub static PKGDATADIR: &str = "/usr/local/share/gtimelog4";

View File

@ -3,5 +3,7 @@
<gresource prefix="/de/gunibert/gtimelog4"> <gresource prefix="/de/gunibert/gtimelog4">
<file preprocess="xml-stripblanks">window.ui</file> <file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file> <file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
<file>bigger.css</file>
<file>style.css</file>
</gresource> </gresource>
</gresources> </gresources>

View File

@ -26,7 +26,7 @@ mod timeentry;
use self::application::Gtimelog4Application; use self::application::Gtimelog4Application;
use self::window::Gtimelog4Window; use self::window::Gtimelog4Window;
use config::{GETTEXT_PACKAGE, LOCALEDIR, PKGDATADIR}; use config::{GETTEXT_PACKAGE, LOCALEDIR};
use gettextrs::{bind_textdomain_codeset, bindtextdomain, textdomain}; use gettextrs::{bind_textdomain_codeset, bindtextdomain, textdomain};
use gtk::gio; use gtk::gio;
use gtk::prelude::*; use gtk::prelude::*;

8
src/gui/src/style.css Normal file
View File

@ -0,0 +1,8 @@
.slackrow {
color: grey;
}
.timelbl {
font-family: monospace;
}

View File

@ -1,25 +1,24 @@
use glib::Boxed;
use gtk::glib; use gtk::glib;
use gtk::glib::ToValue; use gtk::glib::ToValue;
use gtk::subclass::prelude::{ObjectSubclassExt, ObjectSubclassIsExt}; use gtk::subclass::prelude::ObjectSubclassIsExt;
#[derive(glib::Boxed, Clone, Default)] #[derive(glib::Boxed, Clone, Default)]
#[boxed_type(name = "Entry")] #[boxed_type(name = "Entry")]
pub struct Entry(rtimelog::store::Entry); pub struct Entry(pub data::Entry);
impl From<rtimelog::store::Entry> for Entry { impl From<data::Entry> for Entry {
fn from(rentry: rtimelog::store::Entry) -> Self { fn from(rentry: data::Entry) -> Self {
Self(rentry) Self(rentry)
} }
} }
mod imp { mod imp {
use std::cell::RefCell;
use std::rc::Rc;
use gtk::glib::once_cell::sync::Lazy;
use super::*; use super::*;
use gtk::subclass::prelude::{ObjectImpl, ObjectSubclass};
use crate::gio::glib::{ParamSpec, Value}; 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)] #[derive(Default)]
pub struct TimeEntry { pub struct TimeEntry {
@ -40,7 +39,9 @@ mod imp {
static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| { static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
vec![ vec![
glib::ParamSpecBoxed::builder::<Entry>("entry").build(), glib::ParamSpecBoxed::builder::<Entry>("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() PROPERTIES.as_ref()
@ -53,22 +54,64 @@ mod imp {
self.entry.replace(e); self.entry.replace(e);
} }
"task" => { "task" => {
let e = value.get().expect("Something went wrong"); let e: String = value.get().expect("Something went wrong");
println!("Set Task: {}", e); 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 { fn property(&self, _id: usize, pspec: &ParamSpec) -> Value {
match pspec.name() { match pspec.name() {
"entry" => self.entry.borrow().to_value(), "entry" => self.entry.borrow().to_value(),
"task" => { "task" => self
println!("Task: {}", self.entry.borrow().0.task); .entry
self.entry.borrow().0.task.to_value() .borrow()
}, .0
_ => unimplemented!() .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 { impl TimeEntry {
pub fn new(entry: rtimelog::store::Entry) -> Self { pub fn new(entry: data::Entry) -> Self {
let e: Entry = entry.into(); let e: Entry = entry.into();
glib::Object::new(&[("entry", &e.to_value())]) glib::Object::new(&[("entry", &e.to_value())])
} }
pub fn get_task(&self) -> String { pub fn get_task(&self) -> String {
self.imp().entry.borrow().0.task.clone() self.imp()
} .entry
.borrow()
pub fn get_time(&self) -> String { .0
self.imp().entry.borrow().0.stop.format("%T").to_string() .task
.clone()
.unwrap_or_default()
.clone()
} }
} }
impl From<rtimelog::store::Entry> for TimeEntry { impl From<data::Entry> for TimeEntry {
fn from(entry: rtimelog::store::Entry) -> Self { fn from(entry: data::Entry) -> Self {
TimeEntry::new(entry) TimeEntry::new(entry)
} }
} }

View File

@ -18,23 +18,24 @@
* SPDX-License-Identifier: GPL-3.0-or-later * 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 crate::timeentry::TimeEntry;
use adw::subclass::prelude::*;
use gtk::prelude::*;
use gtk::{gio, glib, CompositeTemplate};
mod imp { 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 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")] #[template(resource = "/de/gunibert/gtimelog4/window.ui")]
pub struct Gtimelog4Window { pub struct Gtimelog4Window {
pub timelog: Rc<RefCell<rtimelog::store::Timelog>>, pub timelog: Rc<RefCell<data::Timelog>>,
// Template widgets // Template widgets
#[template_child] #[template_child]
@ -42,11 +43,17 @@ mod imp {
#[template_child] #[template_child]
pub columnview: TemplateChild<gtk::ColumnView>, pub columnview: TemplateChild<gtk::ColumnView>,
#[template_child] #[template_child]
pub time_column: TemplateChild<gtk::ColumnViewColumn>, pub start_column: TemplateChild<gtk::ColumnViewColumn>,
#[template_child]
pub stop_column: TemplateChild<gtk::ColumnViewColumn>,
#[template_child] #[template_child]
pub task_column: TemplateChild<gtk::ColumnViewColumn>, pub task_column: TemplateChild<gtk::ColumnViewColumn>,
#[template_child] #[template_child]
pub taskentry: TemplateChild<gtk::Entry>, pub taskentry: TemplateChild<gtk::Entry>,
#[template_child]
pub statistics: TemplateChild<gtk::Label>,
pub provider: RefCell<gtk::CssProvider>,
} }
impl Gtimelog4Window { impl Gtimelog4Window {
@ -55,6 +62,59 @@ mod imp {
lbl.set_xalign(0.0f32); lbl.set_xalign(0.0f32);
item.set_child(Some(&lbl)); 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<gio::ListStore, anyhow::Error> {
let model: SelectionModel = self
.columnview
.model()
.ok_or(anyhow!("Columnview has not model set"))?;
let selection = model
.downcast::<NoSelection>()
.map_err(|_| anyhow!("Cannot downcast SelectionModel"))?;
let model = selection
.model()
.ok_or(anyhow!("SelectionModel has no model set"))?;
model
.downcast::<gio::ListStore>()
.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::<gtk::EditableLabel>().unwrap();
let entry = entry.downcast::<TimeEntry>().unwrap();
entry
.bind_property(src_prop, &mylbl, "text")
.sync_create()
.bidirectional()
.build();
}
}
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -76,55 +136,94 @@ mod imp {
fn constructed(&self) { fn constructed(&self) {
self.parent_constructed(); 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(); let task_factory = SignalListItemFactory::new();
task_factory.connect_setup(Gtimelog4Window::setup_lbl); 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()) { if let (Some(lbl), Some(entry)) = (item.child(), item.item()) {
let mylbl = lbl.downcast::<gtk::EditableLabel>().unwrap(); let mylbl = lbl.downcast::<gtk::EditableLabel>().unwrap();
let entry = entry.downcast::<TimeEntry>().unwrap(); let entry = entry.downcast::<TimeEntry>().unwrap();
println!("Initial Task: {}", entry.property::<String>("task"));
entry.bind_property("task", &mylbl, "text").sync_create().bidirectional().build(); 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)); self.task_column.set_factory(Some(&task_factory));
let time_factory = SignalListItemFactory::new(); let start_factory = SignalListItemFactory::new();
time_factory.connect_setup(Gtimelog4Window::setup_lbl); start_factory.connect_setup(Gtimelog4Window::setup_time_lbl);
time_factory.connect_bind(|_factory, item| { start_factory.connect_bind(|_factory, item| Self::time_factory_bind(item, "start"));
if let (Some(lbl), Some(entry)) = (item.child(), item.item()) {
let mylbl = lbl.downcast::<gtk::EditableLabel>().unwrap(); self.start_column.set_factory(Some(&start_factory));
let entry = entry.downcast::<TimeEntry>().unwrap();
mylbl.set_text(&entry.get_time()); 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<TimeEntry> = 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(&gtk::NoSelection::new(Some(&model))));
})); }));
} }
} }
impl WidgetImpl for Gtimelog4Window {} impl WidgetImpl for Gtimelog4Window {
fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
impl WindowImpl for Gtimelog4Window { self.parent_size_allocate(width, height, baseline);
fn close_request(&self) -> Inhibit { if width > 800 {
println!("Close request"); if let Some(display) = gtk::gdk::Display::default() {
self.timelog.borrow_mut().save().expect("TODO: panic message"); gtk::StyleContext::add_provider_for_display(
Inhibit(false) &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 ApplicationWindowImpl for Gtimelog4Window {}
impl AdwApplicationWindowImpl for Gtimelog4Window {} impl AdwApplicationWindowImpl for Gtimelog4Window {}
@ -136,18 +235,27 @@ glib::wrapper! {
} }
impl Gtimelog4Window { impl Gtimelog4Window {
pub fn new<P: glib::IsA<gtk::Application>>(application: &P) -> Self { pub fn new<P: IsA<gtk::Application>>(application: &P) -> Self {
glib::Object::new(&[("application", application)]) 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); self.imp().timelog.replace(timelog);
let entries: Vec<TimeEntry> = self.imp().timelog.borrow().get_this_week().to_vec().into_iter().map(|entry| entry.into()).collect(); let entries: Vec<TimeEntry> = self
.imp()
.timelog
.borrow_mut()
.get_today()
.into_iter()
.map(|entry| entry.into())
.collect();
let model = gio::ListStore::new(TimeEntry::static_type()); let model = gio::ListStore::new(TimeEntry::static_type());
for entry in entries { for entry in entries {
model.append(&entry); model.append(&entry);
} }
self.imp().columnview.set_model(Some(&gtk::NoSelection::new(Some(&model)))); self.imp()
.columnview
.set_model(Some(&gtk::NoSelection::new(Some(&model))));
} }
} }

View File

@ -19,30 +19,68 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkColumnView" id="columnview"> <object class="GtkScrolledWindow">
<property name="vexpand">True</property>
<style>
<class name="data-table"/>
</style>
<child> <child>
<object class="GtkColumnViewColumn" id="time_column"> <object class="GtkColumnView" id="columnview">
<property name="title">Time</property> <property name="vexpand">True</property>
</object> <style>
</child> <class name="data-table"/>
<child> </style>
<object class="GtkColumnViewColumn" id="task_column"> <child>
<property name="title">Task</property> <object class="GtkColumnViewColumn" id="start_column">
<property name="expand">True</property> <property name="title">Start</property>
<property name="fixed-width">110</property>
<property name="resizable">true</property>
</object>
</child>
<child>
<object class="GtkColumnViewColumn" id="stop_column">
<property name="title">Stop</property>
<property name="fixed-width">110</property>
<property name="resizable">true</property>
</object>
</child>
<child>
<object class="GtkColumnViewColumn" id="task_column">
<property name="title">Task</property>
<property name="expand">True</property>
<property name="resizable">true</property>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkEntry" id="taskentry"> <object class="GtkBox">
<property name="margin-bottom">6</property> <property name="orientation">vertical</property>
<property name="margin-top">6</property> <child>
<property name="margin-start">6</property> <object class="GtkLabel" id="statistics">
<property name="margin-end">6</property> <property name="label">test</property>
<property name="halign">center</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="margin-bottom">6</property>
<property name="margin-top">6</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="spacing">6</property>
<child>
<object class="GtkEntry" id="taskentry">
<property name="hexpand">true</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="label">Send to Personio...</property>
</object>
</child>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>