Compare commits

..

No commits in common. "806568cbde8483e3041d7a5e5c1bffa7b05be7d4" and "dbcecd9352d0050c2cea9604a0e57300b4247f53" have entirely different histories.

31 changed files with 558 additions and 2377 deletions

2
.gitignore vendored
View File

@ -1,3 +1 @@
/target/ /target/
/src/data/target/
/src/gui/target/

611
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,17 @@
[workspace] [package]
members = [ name = "gtimelog4"
"src/gui", version = "0.1.0"
"src/data", 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" }

View File

@ -1,6 +1,7 @@
use gtk::gio;
fn main() { fn main() {
glib_build_tools::compile_resources( gio::compile_resources(
"src", "src",
"src/gtimelog4.gresource.xml", "src/gtimelog4.gresource.xml",
"gtimelog4.gresource", "gtimelog4.gresource",

View File

@ -10,37 +10,9 @@ 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

@ -18,12 +18,12 @@
* 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 {
@ -40,12 +40,13 @@ mod imp {
} }
impl ObjectImpl for Gtimelog4Application { impl ObjectImpl for Gtimelog4Application {
fn constructed(&self) { fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(); self.parent_constructed(obj);
let obj = self.instance();
obj.setup_gactions(); obj.setup_gactions();
obj.set_accels_for_action("app.quit", &["<primary>q"]); 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 // has been launched. Additionally, this callback notifies us when the user
// tries to launch a "second instance" of the application. When they try // tries to launch a "second instance" of the application. When they try
// to do that, we'll just present any existing window. // to do that, we'll just present any existing window.
fn activate(&self) { fn activate(&self, application: &Self::Type) {
let application = self.instance();
// Get the current window or create one if necessary // Get the current window or create one if necessary
let window = if let Some(window) = application.active_window() { let window = if let Some(window) = application.active_window() {
window window
} else { } else {
let timelog = data::Timelog::new(); let window = Gtimelog4Window::new(application);
let window = Gtimelog4Window::new(&*application);
window.set_timelog(timelog);
window.upcast() window.upcast()
}; };
@ -83,10 +81,8 @@ 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>(&[ glib::Object::new(&[("application-id", &application_id), ("flags", flags)])
("application-id", &application_id), .expect("Failed to create Gtimelog4Application")
("flags", flags),
])
} }
fn setup_gactions(&self) { fn setup_gactions(&self) {
@ -105,16 +101,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();
} }
} }

View File

@ -1,3 +1,4 @@
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";

7
src/data/Cargo.lock generated
View File

@ -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"

View File

@ -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"

Binary file not shown.

View File

@ -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"

View File

@ -1,3 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE entries;

View File

@ -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
);

View File

@ -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))
}

View File

@ -1,10 +0,0 @@
// @generated automatically by Diesel CLI.
diesel::table! {
entries (id) {
id -> Integer,
start -> Nullable<Timestamp>,
stop -> Nullable<Timestamp>,
task -> Nullable<Text>,
}
}

View File

@ -3,7 +3,5 @@
<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>

1311
src/gui/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

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

View File

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

View File

@ -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)
}
}

View File

@ -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(&gtk::NoSelection::new(Some(&model))));
}
}

View File

@ -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>

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}; use config::{GETTEXT_PACKAGE, LOCALEDIR, PKGDATADIR};
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::*;

47
src/timeentry.rs Normal file
View 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
View 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
View 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>