Compare commits
No commits in common. "806568cbde8483e3041d7a5e5c1bffa7b05be7d4" and "dbcecd9352d0050c2cea9604a0e57300b4247f53" have entirely different histories.
806568cbde
...
dbcecd9352
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1 @@
|
||||
/target/
|
||||
/src/data/target/
|
||||
/src/gui/target/
|
||||
|
||||
611
Cargo.lock
generated
611
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@ -1,5 +1,17 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"src/gui",
|
||||
"src/data",
|
||||
]
|
||||
[package]
|
||||
name = "gtimelog4"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
gettext-rs = { version = "0.7", features = ["gettext-system"] }
|
||||
gtk = { version = "0.4.8", package = "gtk4" }
|
||||
rtimelog = { path = "/home/gunibert/Projekte/rtimelog" }
|
||||
|
||||
[dependencies.adw]
|
||||
package = "libadwaita"
|
||||
version = "0.2.0-alpha.2"
|
||||
# features = ["v1_2"]
|
||||
|
||||
[build-dependencies]
|
||||
gtk = { version = "0.4.8", package = "gtk4" }
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use gtk::gio;
|
||||
|
||||
fn main() {
|
||||
glib_build_tools::compile_resources(
|
||||
gio::compile_resources(
|
||||
"src",
|
||||
"src/gtimelog4.gresource.xml",
|
||||
"gtimelog4.gresource",
|
||||
30
meson.build
30
meson.build
@ -10,37 +10,9 @@ 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,
|
||||
|
||||
@ -18,12 +18,12 @@
|
||||
* 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 {
|
||||
@ -40,12 +40,13 @@ mod imp {
|
||||
}
|
||||
|
||||
impl ObjectImpl for Gtimelog4Application {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.instance();
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
self.parent_constructed(obj);
|
||||
|
||||
obj.setup_gactions();
|
||||
obj.set_accels_for_action("app.quit", &["<primary>q"]);
|
||||
|
||||
let timelog = rtimelog::store::Timelog::new_from_default_file();
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,15 +55,12 @@ mod imp {
|
||||
// has been launched. Additionally, this callback notifies us when the user
|
||||
// tries to launch a "second instance" of the application. When they try
|
||||
// to do that, we'll just present any existing window.
|
||||
fn activate(&self) {
|
||||
let application = self.instance();
|
||||
fn activate(&self, application: &Self::Type) {
|
||||
// Get the current window or create one if necessary
|
||||
let window = if let Some(window) = application.active_window() {
|
||||
window
|
||||
} else {
|
||||
let timelog = data::Timelog::new();
|
||||
let window = Gtimelog4Window::new(&*application);
|
||||
window.set_timelog(timelog);
|
||||
let window = Gtimelog4Window::new(application);
|
||||
window.upcast()
|
||||
};
|
||||
|
||||
@ -83,10 +81,8 @@ glib::wrapper! {
|
||||
|
||||
impl Gtimelog4Application {
|
||||
pub fn new(application_id: &str, flags: &gio::ApplicationFlags) -> Self {
|
||||
glib::Object::new::<Gtimelog4Application>(&[
|
||||
("application-id", &application_id),
|
||||
("flags", flags),
|
||||
])
|
||||
glib::Object::new(&[("application-id", &application_id), ("flags", flags)])
|
||||
.expect("Failed to create Gtimelog4Application")
|
||||
}
|
||||
|
||||
fn setup_gactions(&self) {
|
||||
@ -105,16 +101,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();
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
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";
|
||||
7
src/data/Cargo.lock
generated
7
src/data/Cargo.lock
generated
@ -1,7 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "data"
|
||||
version = "0.1.0"
|
||||
@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "data"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
diesel = { version = "2.0.2", features = ["chrono", "sqlite", "returning_clauses_for_sqlite_3_35"] }
|
||||
diesel_migrations = "2.0.0"
|
||||
chrono = "0.4.22"
|
||||
BIN
src/data/data.db
BIN
src/data/data.db
Binary file not shown.
@ -1,8 +0,0 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
|
||||
[migrations_directory]
|
||||
dir = "migrations"
|
||||
@ -1,3 +0,0 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
|
||||
DROP TABLE entries;
|
||||
@ -1,8 +0,0 @@
|
||||
-- Your SQL goes here
|
||||
|
||||
CREATE TABLE IF NOT EXISTS entries (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
start TIMESTAMP,
|
||||
stop TIMESTAMP,
|
||||
task TEXT
|
||||
);
|
||||
@ -1,144 +0,0 @@
|
||||
use chrono::{Datelike, Duration, NaiveDateTime};
|
||||
use diesel::{
|
||||
AsChangeset, Connection, Identifiable, Insertable, Queryable, RunQueryDsl, SqliteConnection,
|
||||
};
|
||||
use diesel_migrations::MigrationHarness;
|
||||
|
||||
use schema::entries;
|
||||
|
||||
mod schema;
|
||||
|
||||
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,
|
||||
pub start: Option<NaiveDateTime>,
|
||||
pub stop: Option<NaiveDateTime>,
|
||||
pub task: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = entries)]
|
||||
pub struct NewEntry {
|
||||
start: Option<NaiveDateTime>,
|
||||
stop: Option<NaiveDateTime>,
|
||||
task: Option<String>,
|
||||
}
|
||||
|
||||
pub struct Timelog {
|
||||
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 {
|
||||
let mut conn = get_connection();
|
||||
conn.run_pending_migrations(MIGRATIONS).unwrap();
|
||||
Self { conn }
|
||||
}
|
||||
|
||||
pub fn add_entry<P: AsRef<str>>(&mut self, task_name: P) -> Entry {
|
||||
use schema::entries::dsl::*;
|
||||
|
||||
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: start_time,
|
||||
stop: Some(now),
|
||||
task: Some(task_name.as_ref().to_string()),
|
||||
};
|
||||
|
||||
diesel::insert_into(entries)
|
||||
.values(&entry)
|
||||
.get_result::<Entry>(&mut self.conn)
|
||||
.expect("Error saving 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(&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 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();
|
||||
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))
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
entries (id) {
|
||||
id -> Integer,
|
||||
start -> Nullable<Timestamp>,
|
||||
stop -> Nullable<Timestamp>,
|
||||
task -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,5 @@
|
||||
<gresource prefix="/de/gunibert/gtimelog4">
|
||||
<file preprocess="xml-stripblanks">window.ui</file>
|
||||
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
|
||||
<file>bigger.css</file>
|
||||
<file>style.css</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
1311
src/gui/Cargo.lock
generated
1311
src/gui/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "gtimelog4"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
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/"}
|
||||
data = { path = "../data"}
|
||||
chrono = "0.4.22"
|
||||
anyhow = "1.0.66"
|
||||
|
||||
[dependencies.adw]
|
||||
package = "libadwaita"
|
||||
version = "0.2.0"
|
||||
features = ["v1_2"]
|
||||
|
||||
[build-dependencies]
|
||||
glib-build-tools = "0.16.0"
|
||||
@ -1,7 +0,0 @@
|
||||
.data-table {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
header label {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
|
||||
.slackrow {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.timelbl {
|
||||
font-family: monospace;
|
||||
}
|
||||
@ -1,146 +0,0 @@
|
||||
use gtk::glib;
|
||||
use gtk::glib::ToValue;
|
||||
use gtk::subclass::prelude::ObjectSubclassIsExt;
|
||||
|
||||
#[derive(glib::Boxed, Clone, Default)]
|
||||
#[boxed_type(name = "Entry")]
|
||||
pub struct Entry(pub data::Entry);
|
||||
|
||||
impl From<data::Entry> for Entry {
|
||||
fn from(rentry: data::Entry) -> Self {
|
||||
Self(rentry)
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
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 {
|
||||
pub(crate) entry: RefCell<Entry>,
|
||||
}
|
||||
|
||||
impl TimeEntry {}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for TimeEntry {
|
||||
const NAME: &'static str = "TimeEntry";
|
||||
type Type = super::TimeEntry;
|
||||
type ParentType = glib::Object;
|
||||
}
|
||||
|
||||
impl ObjectImpl for TimeEntry {
|
||||
fn properties() -> &'static [ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpecBoxed::builder::<Entry>("entry").build(),
|
||||
glib::ParamSpecString::builder("task").build(),
|
||||
glib::ParamSpecString::builder("start").build(),
|
||||
glib::ParamSpecString::builder("stop").build(),
|
||||
]
|
||||
});
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &Value, pspec: &ParamSpec) {
|
||||
match pspec.name() {
|
||||
"entry" => {
|
||||
let e = value.get().expect("Something went wrong");
|
||||
self.entry.replace(e);
|
||||
}
|
||||
"task" => {
|
||||
let e: String = value.get().expect("Something went wrong");
|
||||
println!("Set Task: {}", e);
|
||||
self.entry.borrow_mut().0.task = Some(e);
|
||||
}
|
||||
"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" => 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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct TimeEntry(ObjectSubclass<imp::TimeEntry>);
|
||||
}
|
||||
|
||||
impl TimeEntry {
|
||||
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()
|
||||
.unwrap_or_default()
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<data::Entry> for TimeEntry {
|
||||
fn from(entry: data::Entry) -> Self {
|
||||
TimeEntry::new(entry)
|
||||
}
|
||||
}
|
||||
@ -1,261 +0,0 @@
|
||||
/* window.rs
|
||||
*
|
||||
* Copyright 2022 Günther Wagner
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
use crate::timeentry::TimeEntry;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{gio, glib, CompositeTemplate};
|
||||
|
||||
mod imp {
|
||||
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(Default, CompositeTemplate)]
|
||||
#[template(resource = "/de/gunibert/gtimelog4/window.ui")]
|
||||
pub struct Gtimelog4Window {
|
||||
pub timelog: Rc<RefCell<data::Timelog>>,
|
||||
|
||||
// Template widgets
|
||||
#[template_child]
|
||||
pub header_bar: TemplateChild<gtk::HeaderBar>,
|
||||
#[template_child]
|
||||
pub columnview: TemplateChild<gtk::ColumnView>,
|
||||
#[template_child]
|
||||
pub start_column: TemplateChild<gtk::ColumnViewColumn>,
|
||||
#[template_child]
|
||||
pub stop_column: TemplateChild<gtk::ColumnViewColumn>,
|
||||
#[template_child]
|
||||
pub task_column: TemplateChild<gtk::ColumnViewColumn>,
|
||||
#[template_child]
|
||||
pub taskentry: TemplateChild<gtk::Entry>,
|
||||
#[template_child]
|
||||
pub statistics: TemplateChild<gtk::Label>,
|
||||
|
||||
pub provider: RefCell<gtk::CssProvider>,
|
||||
}
|
||||
|
||||
impl Gtimelog4Window {
|
||||
fn setup_lbl(_: &SignalListItemFactory, item: &ListItem) {
|
||||
let lbl = gtk::EditableLabel::new("");
|
||||
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<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]
|
||||
impl ObjectSubclass for Gtimelog4Window {
|
||||
const NAME: &'static str = "Gtimelog4Window";
|
||||
type Type = super::Gtimelog4Window;
|
||||
type ParentType = adw::ApplicationWindow;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for Gtimelog4Window {
|
||||
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(clone!(@weak self as this => move |_factory, item| {
|
||||
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("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 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("");
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Gtimelog4Window(ObjectSubclass<imp::Gtimelog4Window>)
|
||||
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow, @implements gio::ActionGroup, gio::ActionMap;
|
||||
}
|
||||
|
||||
impl Gtimelog4Window {
|
||||
pub fn new<P: IsA<gtk::Application>>(application: &P) -> Self {
|
||||
glib::Object::new(&[("application", application)])
|
||||
}
|
||||
|
||||
pub fn set_timelog(&self, timelog: data::Timelog) {
|
||||
self.imp().timelog.replace(timelog);
|
||||
|
||||
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());
|
||||
for entry in entries {
|
||||
model.append(&entry);
|
||||
}
|
||||
self.imp()
|
||||
.columnview
|
||||
.set_model(Some(>k::NoSelection::new(Some(&model))));
|
||||
}
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="Adw" version="1.0"/>
|
||||
<template class="Gtimelog4Window" parent="AdwApplicationWindow">
|
||||
<property name="default-width">600</property>
|
||||
<property name="default-height">300</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkHeaderBar" id="header_bar">
|
||||
<child type="end">
|
||||
<object class="GtkMenuButton">
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
<property name="menu-model">primary_menu</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<child>
|
||||
<object class="GtkColumnView" id="columnview">
|
||||
<property name="vexpand">True</property>
|
||||
<style>
|
||||
<class name="data-table"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkColumnViewColumn" id="start_column">
|
||||
<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>
|
||||
</child>
|
||||
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="statistics">
|
||||
<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>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
<menu id="primary_menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Preferences</attribute>
|
||||
<attribute name="action">app.preferences</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
|
||||
<attribute name="action">win.show-help-overlay</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_About Gtimelog4</attribute>
|
||||
<attribute name="action">app.about</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
</interface>
|
||||
@ -26,7 +26,7 @@ mod timeentry;
|
||||
use self::application::Gtimelog4Application;
|
||||
use self::window::Gtimelog4Window;
|
||||
|
||||
use config::{GETTEXT_PACKAGE, LOCALEDIR};
|
||||
use config::{GETTEXT_PACKAGE, LOCALEDIR, PKGDATADIR};
|
||||
use gettextrs::{bind_textdomain_codeset, bindtextdomain, textdomain};
|
||||
use gtk::gio;
|
||||
use gtk::prelude::*;
|
||||
47
src/timeentry.rs
Normal file
47
src/timeentry.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use gtk::glib;
|
||||
use gtk::subclass::prelude::ObjectSubclassExt;
|
||||
use rtimelog::store::Entry;
|
||||
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use super::*;
|
||||
use gtk::subclass::prelude::{ObjectImpl, ObjectSubclass};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TimeEntry {
|
||||
pub(crate) entry: Rc<RefCell<rtimelog::store::Entry>>,
|
||||
}
|
||||
|
||||
impl TimeEntry {
|
||||
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for TimeEntry {
|
||||
const NAME: &'static str = "TimeEntry";
|
||||
type Type = super::TimeEntry;
|
||||
type ParentType = glib::Object;
|
||||
}
|
||||
|
||||
impl ObjectImpl for TimeEntry {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct TimeEntry(ObjectSubclass<imp::TimeEntry>);
|
||||
}
|
||||
|
||||
impl TimeEntry {
|
||||
pub fn new(entry: rtimelog::store::Entry) -> Self {
|
||||
let obj = glib::Object::new(&[]).expect("Could not create TimeEntry");
|
||||
let imp = imp::TimeEntry::from_instance(&obj);
|
||||
imp.entry.replace(entry);
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rtimelog::store::Entry> for TimeEntry {
|
||||
fn from(entry: Entry) -> Self {
|
||||
TimeEntry::new(entry)
|
||||
}
|
||||
}
|
||||
72
src/window.rs
Normal file
72
src/window.rs
Normal file
@ -0,0 +1,72 @@
|
||||
/* window.rs
|
||||
*
|
||||
* Copyright 2022 Günther Wagner
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
use gtk::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::{gio, glib, CompositeTemplate};
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)]
|
||||
#[template(resource = "/de/gunibert/gtimelog4/window.ui")]
|
||||
pub struct Gtimelog4Window {
|
||||
// Template widgets
|
||||
#[template_child]
|
||||
pub header_bar: TemplateChild<gtk::HeaderBar>,
|
||||
#[template_child]
|
||||
pub columnview: TemplateChild<gtk::ColumnView>,
|
||||
#[template_child]
|
||||
pub task_column: TemplateChild<gtk::ColumnViewColumn>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Gtimelog4Window {
|
||||
const NAME: &'static str = "Gtimelog4Window";
|
||||
type Type = super::Gtimelog4Window;
|
||||
type ParentType = adw::ApplicationWindow;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
Self::bind_template(klass);
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for Gtimelog4Window {}
|
||||
impl WidgetImpl for Gtimelog4Window {}
|
||||
impl WindowImpl for Gtimelog4Window {}
|
||||
impl ApplicationWindowImpl for Gtimelog4Window {}
|
||||
impl AdwApplicationWindowImpl for Gtimelog4Window {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Gtimelog4Window(ObjectSubclass<imp::Gtimelog4Window>)
|
||||
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow, @implements gio::ActionGroup, gio::ActionMap;
|
||||
}
|
||||
|
||||
impl Gtimelog4Window {
|
||||
pub fn new<P: glib::IsA<gtk::Application>>(application: &P) -> Self {
|
||||
glib::Object::new(&[("application", application)])
|
||||
.expect("Failed to create Gtimelog4Window")
|
||||
}
|
||||
}
|
||||
49
src/window.ui
Normal file
49
src/window.ui
Normal file
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="Adw" version="1.0"/>
|
||||
<template class="Gtimelog4Window" parent="AdwApplicationWindow">
|
||||
<property name="default-width">600</property>
|
||||
<property name="default-height">300</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkHeaderBar" id="header_bar">
|
||||
<child type="end">
|
||||
<object class="GtkMenuButton">
|
||||
<property name="icon-name">open-menu-symbolic</property>
|
||||
<property name="menu-model">primary_menu</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkColumnView" id="columnview">
|
||||
<child>
|
||||
<object class="GtkColumnViewColumn" id="task_column">
|
||||
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
<menu id="primary_menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Preferences</attribute>
|
||||
<attribute name="action">app.preferences</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
|
||||
<attribute name="action">win.show-help-overlay</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_About Gtimelog4</attribute>
|
||||
<attribute name="action">app.about</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
</interface>
|
||||
Loading…
x
Reference in New Issue
Block a user