ungana

Unnamed repository; edit this file 'description' to name the repository.
Info | Log | Files | Refs | README

commit 995f34c06c222129bf0e738d4f0fb4c6b65b2900
parent 7e07000abbaed9c1b1ff72fd5f4f2300ffc46b47
Author: lash <dev@holbrook.no>
Date:   Mon,  1 Sep 2025 01:22:27 +0100

Replace main window with event listings

Diffstat:
Mpyproject.toml | 1+
Mungana/config.py | 4++++
Mungana/data/config.ini | 1+
Aungana/gui/event.py | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mungana/gui/win.py | 252+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
5 files changed, 270 insertions(+), 108 deletions(-)

diff --git a/pyproject.toml b/pyproject.toml @@ -10,6 +10,7 @@ requires-python = ">=3.8" dependencies = [ "icalendar>=5.0.0", "confini~=0.6.5", + "pyxdg~=0.26", ] [project.optional-dependencies] diff --git a/ungana/config.py b/ungana/config.py @@ -1,9 +1,13 @@ import os import confini +from xdg.BaseDirectory import save_data_path __datadir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") def load(): cfg = confini.Config(__datadir) cfg.process() + if not cfg.have('BASE_DATAPATH'): + datapath = save_data_path('ungana') + cfg.add(datapath, 'BASE_DATAPATH') return cfg diff --git a/ungana/data/config.ini b/ungana/data/config.ini @@ -1,3 +1,4 @@ [base] domain = nondominium.org zone = America/El_Salvador +datapath = diff --git a/ungana/gui/event.py b/ungana/gui/event.py @@ -0,0 +1,120 @@ +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +from gi.repository import Gtk, Adw, Gio + + +class EventWindow(Gtk.Window): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.cal = None + self.box_main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.set_child(self.box_main) + + title = Gtk.Label(label="UUID") + self.box_main.append(title) + entry = Gtk.Entry() + entry.set_editable(False) + entry.set_sensitive(False) + self.entry_uuid = entry + self.box_main.append(entry) + + title = Gtk.Label(label="Domain") + self.box_main.append(title) + entry = Gtk.Entry() + self.entry_domain = entry + self.box_main.append(entry) + + title = Gtk.Label(label="Summary") + self.box_main.append(title) + entry = Gtk.Entry() + self.entry_summary = entry + self.box_main.append(entry) + + title = Gtk.Label(label="Description") + self.box_main.append(title) + entry = Gtk.Entry() + self.box_main.append(entry) + + title = Gtk.Label(label="Time zone") + self.box_main.append(title) + entry = Gtk.DropDown() + entry.set_enable_search(True) + zones = Gtk.StringList() + entry.props.model = zones + zonedata = zoneinfo.available_timezones() + i = 0 + for v in sorted(zonedata): + zones.append(v) + if v == self.get_application().cfg.get('BASE_ZONE'): + entry.props.selected = i + logg.debug("timezone default: {}".format(v)) + i += 1 + self.box_main.append(entry) + + title = Gtk.Label(label="Start") + self.box_main.append(title) + entry = Gtk.Entry(placeholder_text="YYYY-MM-DD HH:MM") + self.box_main.append(entry) + + title = Gtk.Label(label="Duration") + self.box_main.append(title) + entry = Gtk.Entry(placeholder_text="") + self.box_main.append(entry) + + title = Gtk.Label(label="Banner") + self.box_main.append(title) + + + fltr = Gtk.FileFilter() + fltr.set_name("Images") + fltr.add_mime_type("image/*") + fltrs = Gio.ListStore.new(Gtk.FileFilter) + fltrs.append(fltr) + self.filedialog = Gtk.FileDialog.new() + self.filedialog.set_filters(fltrs) + self.filedialog.set_default_filter(fltr) + + img = Gtk.Image() + self.img = img + self.box_main.append(img) + self.img.set_size_request(0, 200) + + entry = Gtk.Button(label="Open") + entry.connect('clicked', self.show_open_dialog) + self.box_main.append(entry) + + self.status = Gtk.Statusbar() + self.box_main.append(self.status) + + + def show_open_dialog(self, button): + self.filedialog.open(self, None, self.open_dialog_callback) + + + def open_dialog_callback(self, dialog, result): + try: + f = dialog.open_finish(result) + if f is not None: + self.img.set_from_file(f.get_path()) + except GLib.Error as e: + logg.error("error open: {}".format(e)) + + def load(self, fp): + self.cal = ICalManager() + self.cal.load_ical_file(fp) + ev = self.cal.get_first_event(self.cal.calendar) + + v = ev.get('summary') + self.entry_summary.set_text(v) + + v = ev.get('uid') + (uid, domain) = v.split('@') + self.entry_uuid.set_text(uid) + self.entry_domain.set_text(domain) + + status_id = self.status.get_context_id("ical") + self.status.push(status_id, "{} {:s}: {}@{}".format(_("Loaded event from ical file"), fp, uid, domain)) + + diff --git a/ungana/gui/win.py b/ungana/gui/win.py @@ -1,129 +1,165 @@ import logging import zoneinfo import markdown +import datetime +import mimetypes +import os +import uuid +from pathlib import Path +import icalendar import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') -from gi.repository import Gtk, Adw, Gio +from gi.repository import Gtk, Adw, Gio, GObject from ungana.i18n import _ from ungana.ical import ICalManager logg = logging.getLogger("gui.win") + +class WindowEvent(GObject.GObject): + def __init__(self, ev, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ev = ev + v = self.ev.decoded('uid') + r = v.split(b'@') + logg.debug(type(r[0])) + self.__uuid = uuid.UUID(r[0].decode('utf-8')) + self.__domain = r[1].decode('utf-8') + #v = self.summary.decode('utf-8') + #self.set_text(v) + #label = Gtk.Label() + #label.set_text(v) + #self.set_child(label) + #self.set_header(label) + + def summary(self): + v = self.ev.decoded('summary') + return v.decode('utf-8') + + + def uuid(self): + return self.__uuid + + + def domain(self): + return self.__domain + + + def when(self): + return "fii" + + +def event_item_setup(widget, item): + cell = Gtk.Label() + item.set_child(cell) + logg.debug("setup") + + +def event_head_bind(widget, item): + label = item.get_child() + o = item.get_item() + label.set_text(o.summary()) + logg.debug("bind") + + +def event_uuid_bind(widget, item): + label = item.get_child() + o = item.get_item() + label.set_text(str(o.uuid())) + logg.debug("bind") + + +def event_domain_bind(widget, item): + label = item.get_child() + o = item.get_item() + label.set_text(o.domain()) + logg.debug("bind") + + +def event_date_bind(widget, item): + label = item.get_child() + o = item.get_item() + label.set_text(o.when()) + logg.debug("bind") + + class MainWindow(Gtk.ApplicationWindow): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.props.show_menubar = True - self.cal = None + self.box_main = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.set_child(self.box_main) - title = Gtk.Label(label="UUID") - self.box_main.append(title) - entry = Gtk.Entry() - entry.set_editable(False) - entry.set_sensitive(False) - self.entry_uuid = entry - self.box_main.append(entry) - - title = Gtk.Label(label="Domain") - self.box_main.append(title) - entry = Gtk.Entry() - self.entry_domain = entry - self.box_main.append(entry) - - title = Gtk.Label(label="Summary") - self.box_main.append(title) - entry = Gtk.Entry() - self.entry_summary = entry - self.box_main.append(entry) - - title = Gtk.Label(label="Description") - self.box_main.append(title) - entry = Gtk.Entry() - self.box_main.append(entry) - - title = Gtk.Label(label="Time zone") - self.box_main.append(title) - entry = Gtk.DropDown() - entry.set_enable_search(True) - zones = Gtk.StringList() - entry.props.model = zones - zonedata = zoneinfo.available_timezones() - i = 0 - for v in sorted(zonedata): - zones.append(v) - if v == self.get_application().cfg.get('BASE_ZONE'): - entry.props.selected = i - logg.debug("timezone default: {}".format(v)) - i += 1 - self.box_main.append(entry) - - title = Gtk.Label(label="Start") - self.box_main.append(title) - entry = Gtk.Entry(placeholder_text="YYYY-MM-DD HH:MM") - self.box_main.append(entry) - - title = Gtk.Label(label="Duration") - self.box_main.append(title) - entry = Gtk.Entry(placeholder_text="") - self.box_main.append(entry) - - title = Gtk.Label(label="Banner") - self.box_main.append(title) - - - fltr = Gtk.FileFilter() - fltr.set_name("Images") - fltr.add_mime_type("image/*") - fltrs = Gio.ListStore.new(Gtk.FileFilter) - fltrs.append(fltr) - self.filedialog = Gtk.FileDialog.new() - self.filedialog.set_filters(fltrs) - self.filedialog.set_default_filter(fltr) - - img = Gtk.Image() - self.img = img - self.box_main.append(img) - self.img.set_size_request(0, 200) - - entry = Gtk.Button(label="Open") - entry.connect('clicked', self.show_open_dialog) - self.box_main.append(entry) - - self.status = Gtk.Statusbar() - self.box_main.append(self.status) - - - def show_open_dialog(self, button): - self.filedialog.open(self, None, self.open_dialog_callback) - - - def open_dialog_callback(self, dialog, result): - try: - f = dialog.open_finish(result) - if f is not None: - self.img.set_from_file(f.get_path()) - except GLib.Error as e: - logg.error("error open: {}".format(e)) - - def load(self, fp): - self.cal = ICalManager() - self.cal.load_ical_file(fp) - ev = self.cal.get_first_event(self.cal.calendar) - - v = ev.get('summary') - self.entry_summary.set_text(v) - - v = ev.get('uid') - (uid, domain) = v.split('@') - self.entry_uuid.set_text(uid) - self.entry_domain.set_text(domain) - - status_id = self.status.get_context_id("ical") - self.status.push(status_id, "{} {:s}: {}@{}".format(_("Loaded event from ical file"), fp, uid, domain)) - + #self.store = Gtk.TreeListModel(WindowEvent) + #self.store = Gtk.ListStore(str) + self.store = Gio.ListStore.new(WindowEvent) + self.sel = Gtk.SingleSelection() + self.sel.set_model(self.store) + + self.view = Gtk.ColumnView() + self.view.set_model(self.sel) + + factory_head = Gtk.SignalListItemFactory() + factory_head.connect("setup", event_item_setup) + factory_head.connect("bind", event_head_bind) + + factory_uuid = Gtk.SignalListItemFactory() + factory_uuid.connect("setup", event_item_setup) + factory_uuid.connect("bind", event_uuid_bind) + + factory_domain = Gtk.SignalListItemFactory() + factory_domain.connect("setup", event_item_setup) + factory_domain.connect("bind", event_domain_bind) + + factory_date = Gtk.SignalListItemFactory() + factory_date.connect("setup", event_item_setup) + factory_date.connect("bind", event_date_bind) + + rendererTxt = Gtk.CellRendererText() + # TODO: is there a row equivalent to set up? + col = Gtk.ColumnViewColumn.new(_("Event"), factory_head) + #col = Gtk.TreeViewColumn(_("Events"), rendererTxt, text=0) + self.view.append_column(col) + col = Gtk.ColumnViewColumn.new(_("UUID"), factory_uuid) + self.view.append_column(col) + col = Gtk.ColumnViewColumn.new(_("Domain"), factory_domain) + self.view.append_column(col) + col = Gtk.ColumnViewColumn.new(_("Date"), factory_date) + self.view.append_column(col) + self.box_main.append(self.view) + + + def add_event(self, ev): + logg.info("adding event {}".format(str(ev))) + o = WindowEvent(ev) + self.store.append(o) + #self.store.append([ev.decoded('summary').decode('utf-8')]) + + + def load(self, v, skip_past=True): + cfg = self.get_application().cfg + now = datetime.datetime.now(datetime.UTC) + events = {} + logg.debug("processing {}".format(cfg.get('BASE_DATAPATH'))) + for (r, d, files) in os.walk(cfg.get('BASE_DATAPATH')): + for f in files: + t = mimetypes.guess_file_type(f) + if t[0] != 'text/calendar': + continue + fp = os.path.join(r, f) + logg.debug("processing: {}".format(fp)) + # TODO: accommodate this load use-case in internal icalendar module + cal = icalendar.Calendar.from_ical(Path(fp).read_bytes()) + for ev in cal.events: + for d in ev.decoded('dtstart'): + zone = d.tzinfo.tzname(d) + if zone == 'UTC': + if skip_past and d < now: + logg.debug("skip past event: ".format(ev)) + continue + self.add_event(ev)