usawa

Signed, immutable accounting.
Info | Log | Files | Refs | Submodules | LICENSE

commit 2b66b8201f908eff5144c307c0014a87d05ce4ff
parent 1e23655e08cd2e158bdd58e04c1ea2ba7f801a42
Author: Carlosokumu <carlosokumu254@gmail.com>
Date:   Mon,  2 Mar 2026 08:56:06 +0300

implement attachment viewing

Diffstat:
Mdummy/usawa/gui/views/entry_details_view.py | 472++++++++++++++++++++++++++++++++++++++++++-------------------------------------
1 file changed, 252 insertions(+), 220 deletions(-)

diff --git a/dummy/usawa/gui/views/entry_details_view.py b/dummy/usawa/gui/views/entry_details_view.py @@ -1,101 +1,88 @@ import logging -from gi.repository import Gtk, Adw,Pango +import threading +from gi.repository import Gtk, Adw,Pango,Gdk,GdkPixbuf,GLib +import threading +import tempfile +import subprocess +import logging + +logg = logging.getLogger("gui.entry_details_view") -logg = logging.getLogger("gui.entry_details") -def create_entry_details_page(entry,nav_view): +def create_entry_details_page(entry, nav_view,fetch_fn): """Create an entry details page for the navigation stack""" - - # Create navigation page page = Adw.NavigationPage( title="Entry Details", tag=f"entry-{entry.serial}" ) - - main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) - - header = _create_header(nav_view) - main_box.append(header) - - - scrolled = Gtk.ScrolledWindow() - scrolled.set_vexpand(True) - scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - - content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20) - content.set_margin_top(20) - content.set_margin_bottom(20) - content.set_margin_start(20) - content.set_margin_end(20) - - # Entry Details Section - entry_details = _create_entry_details_section(entry) - content.append(entry_details) - - # Transaction Details Section - transaction_details = _create_transaction_section(entry) - content.append(transaction_details) - - # Attachments Section - attachments_section = _create_attachments_section(entry) - content.append(attachments_section) - - scrolled.set_child(content) - main_box.append(scrolled) - page.set_child(main_box) - + view = EntryDetailsView(entry, nav_view,fetch_fn) + page.set_child(view) return page -def _create_header(nav_view): - """Create header with back button and serial badge""" - header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) - header_box.set_margin_top(12) - header_box.set_margin_bottom(12) - header_box.set_margin_start(12) - header_box.set_margin_end(12) - header_box.add_css_class("toolbar") - - back_btn = Gtk.Button() - back_btn.set_icon_name("go-previous-symbolic") - back_btn.set_label("Back to List") - back_btn.add_css_class("flat") - back_btn.connect("clicked", lambda b: nav_view.pop()) - header_box.append(back_btn) - header_box.append(back_btn) - - serial_badge = Gtk.Label(label=f"#{"0001"}") - serial_badge.add_css_class("accent") - serial_badge.add_css_class("caption") - serial_badge.set_margin_start(8) - serial_badge.set_margin_end(8) - serial_badge.set_margin_top(4) - serial_badge.set_margin_bottom(4) - header_box.append(serial_badge) - - return header_box - -def _create_entry_details_section(entry): - """Create the entry metadata section""" +class EntryDetailsView(Gtk.Box): + """Entry details view""" + + def __init__(self, entry, nav_view,fetch_fn): + super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0) + self.entry = entry + self.nav_view = nav_view + self.fetch_fn = fetch_fn + self._build_ui() + + def _build_ui(self): + self.append(self._create_header()) + + scrolled = Gtk.ScrolledWindow() + scrolled.set_vexpand(True) + scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + + content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=20) + content.set_margin_top(20) + content.set_margin_bottom(20) + content.set_margin_start(20) + content.set_margin_end(20) + + content.append(self._create_entry_details_section()) + content.append(self._create_transaction_section()) + content.append(self._create_attachments_section()) + + scrolled.set_child(content) + self.append(scrolled) + + def _create_header(self): + header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + header_box.set_margin_top(12) + header_box.set_margin_bottom(12) + header_box.set_margin_start(12) + header_box.set_margin_end(12) + header_box.add_css_class("toolbar") + + back_btn = Gtk.Button() + back_btn.set_icon_name("go-previous-symbolic") + back_btn.add_css_class("flat") + back_btn.connect("clicked", lambda b: self.nav_view.pop()) + header_box.append(back_btn) + + return header_box + + def _create_entry_details_section(self): section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) - + header = Gtk.Label(label="ENTRY DETAILS") header.set_halign(Gtk.Align.START) header.add_css_class("heading") section_box.append(header) - grid = Gtk.Grid() grid.set_column_spacing(16) grid.set_row_spacing(12) grid.set_column_homogeneous(True) - - _add_field_to_grid(grid, "Serial number", str(entry.serial), 0, 0) - _add_field_to_grid(grid, "Transaction reference(uuid)", - entry.tx_ref, 0, 1) - _add_field_to_grid(grid, "Transaction date", entry.tx_date, 1, 0) - _add_field_to_grid(grid, "Date registered", entry.tx_date_rg, 1, 1) + _add_field_to_grid(grid, "Serial number", str(self.entry.serial), 0, 0) + _add_field_to_grid(grid, "Transaction reference(uuid)", self.entry.tx_ref, 0, 1) + _add_field_to_grid(grid, "Transaction date", self.entry.tx_date, 1, 0) + _add_field_to_grid(grid, "Date registered", self.entry.tx_date_rg, 1, 1) parent_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) parent_label = Gtk.Label(label="Parent Digest") @@ -104,158 +91,137 @@ def _create_entry_details_section(entry): parent_label.add_css_class("caption") parent_box.append(parent_label) - parent_value = Gtk.Label( - label=entry.parent_digest - ) + parent_value = Gtk.Label(label=self.entry.parent_digest) parent_value.set_halign(Gtk.Align.START) - parent_value.set_selectable(True) - parent_value.set_wrap(False) - parent_value.set_ellipsize(Pango.EllipsizeMode.END) - parent_value.set_max_width_chars(70) + parent_value.set_selectable(True) + parent_value.set_wrap(False) + parent_value.set_ellipsize(Pango.EllipsizeMode.END) + parent_value.set_max_width_chars(70) parent_value.add_css_class("monospace") parent_box.append(parent_value) grid.attach(parent_box, 0, 2, 2, 1) _add_field_to_grid(grid, "Unix Index", "1707665400", 3, 0) - - # Auth state with badge auth_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) auth_label = Gtk.Label(label="Authentication State") auth_label.set_halign(Gtk.Align.START) auth_label.add_css_class("dim-label") auth_label.add_css_class("caption") auth_box.append(auth_label) - - auth_badge = _create_auth_badge(entry.auth_state) + + auth_badge = _create_auth_badge(self.entry.auth_state) auth_badge.set_halign(Gtk.Align.START) auth_box.append(auth_badge) - - grid.attach(auth_box, 1, 3, 1, 1) - - + desc_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) desc_label = Gtk.Label(label="Description") desc_label.set_halign(Gtk.Align.START) desc_label.add_css_class("dim-label") desc_label.add_css_class("caption") desc_box.append(desc_label) - - desc_value = Gtk.Label(label=entry.description) + + desc_value = Gtk.Label(label=self.entry.description) desc_value.set_halign(Gtk.Align.START) desc_value.set_wrap(True) desc_box.append(desc_value) - grid.attach(desc_box, 0, 4, 2, 1) - + section_box.append(grid) - return section_box - -def _create_transaction_section(entry): - """Create the transaction details section""" - section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) - - # Section header - header = Gtk.Label(label="TRANSACTION DETAILS") - header.set_halign(Gtk.Align.START) - header.add_css_class("heading") - section_box.append(header) - - # Amount field - amount_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - amount_label = Gtk.Label(label="Amount") - amount_label.set_halign(Gtk.Align.START) - amount_label.add_css_class("dim-label") - amount_label.add_css_class("caption") - amount_box.append(amount_label) - - amount_value = Gtk.Label(label="0.045") - amount_value.set_halign(Gtk.Align.START) - amount_value.add_css_class("title-1") - amount_box.append(amount_value) - - section_box.append(amount_box) - - # Source and Destination cards - ledger_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16) - ledger_box.set_homogeneous(True) - - # Source card - source_card = _create_ledger_card( - "Source", - entry.source_unit, - entry.source_type, - entry.source_path, - is_source=True - ) - ledger_box.append(source_card) - - # Destination card - dest_card = _create_ledger_card( - "Destination", - entry.dest_unit, - entry.dest_type, - entry.dest_path, - is_source=False - ) - ledger_box.append(dest_card) - - section_box.append(ledger_box) - - return section_box - -def _create_attachments_section(self): - """Create the attachments section""" - section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) - - # Section header - header = Gtk.Label(label="ATTACHMENTS") - header.set_halign(Gtk.Align.START) - header.add_css_class("heading") - section_box.append(header) - - # Attachments container - attachments_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) - - # Attachment 1 - attach1 = _create_attachment_card( - "payment_receipt.txt", - "text/plain • 3 KB" - ) - attachments_box.append(attach1) - - # Attachment 2 - attach2 = _create_attachment_card( - "payment_receipt.png", - "image/png • 1.2 MB" - ) - attachments_box.append(attach2) - - section_box.append(attachments_box) - - return section_box -def _add_field_to_grid( grid, label_text, value_text, row, col): + def _create_transaction_section(self): + section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + + header = Gtk.Label(label="TRANSACTION DETAILS") + header.set_halign(Gtk.Align.START) + header.add_css_class("heading") + section_box.append(header) + + amount_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + amount_label = Gtk.Label(label="Amount") + amount_label.set_halign(Gtk.Align.START) + amount_label.add_css_class("dim-label") + amount_label.add_css_class("caption") + amount_box.append(amount_label) + + amount_value = Gtk.Label(label=self.entry.amount) + amount_value.set_halign(Gtk.Align.START) + amount_value.add_css_class("title-1") + amount_box.append(amount_value) + section_box.append(amount_box) + + ledger_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16) + ledger_box.set_homogeneous(True) + + logg.debug("Created EntryItem in TX Section: %s", self.entry) + + ledger_box.append(_create_ledger_card( + "Source", + self.entry.source_unit, + self.entry.source_type, + self.entry.source_path, + is_source=True + )) + ledger_box.append(_create_ledger_card( + "Destination", + self.entry.dest_unit, + self.entry.dest_type, + self.entry.dest_path, + is_source=False + )) + + section_box.append(ledger_box) + return section_box + + def _create_attachments_section(self): + section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + + header = Gtk.Label(label="ATTACHMENTS") + header.set_halign(Gtk.Align.START) + header.add_css_class("heading") + section_box.append(header) + + attachments_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + + if self.entry.attachments_raw: + for asset in self.entry.attachments_raw: + attach_card = _create_attachment_card( + asset.slug or "Unnamed", + asset.mime or "unknown", + on_click=lambda f, a=asset: _open_attachment_viewer( + self.get_root(), + a, + fetch_fn=self.fetch_fn + ) + ) + attachments_box.append(attach_card) + + section_box.append(attachments_box) + return section_box + + +def _add_field_to_grid(grid, label_text, value_text, row, col): """Helper to add a field to the grid""" field_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) - + label = Gtk.Label(label=label_text) label.set_halign(Gtk.Align.START) label.add_css_class("dim-label") label.add_css_class("caption") field_box.append(label) - + value = Gtk.Label(label=value_text) value.set_halign(Gtk.Align.START) value.add_css_class("monospace") - value.set_selectable(True) # Allow copying + value.set_selectable(True) field_box.append(value) - + grid.attach(field_box, col, row, 1, 1) - -def _create_auth_badge( auth_state): + + +def _create_auth_badge(auth_state): """Create an authentication state badge""" badge = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) badge.set_margin_top(4) @@ -263,11 +229,11 @@ def _create_auth_badge( auth_state): badge.set_margin_start(8) badge.set_margin_end(8) badge.add_css_class("badge") - + icon = Gtk.Label() text = Gtk.Label() text.add_css_class("caption") - + if auth_state == "trusted": badge.add_css_class("auth-trusted") icon.set_text("✓") @@ -288,12 +254,12 @@ def _create_auth_badge( auth_state): badge.add_css_class("auth-unsigned") icon.set_text("○") text.set_text("No Key") - + badge.append(icon) badge.append(text) - return badge - + + def _create_ledger_card(title, unit, account_type, path, is_source=True): """Create a source or destination card""" card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) @@ -302,77 +268,75 @@ def _create_ledger_card(title, unit, account_type, path, is_source=True): card.set_margin_bottom(8) card.set_margin_start(8) card.set_margin_end(8) - - # Header + header = Gtk.Label(label=title) header.set_halign(Gtk.Align.START) header.add_css_class("heading") header.set_margin_top(8) header.set_margin_start(8) card.append(header) - - # Fields + fields_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) fields_box.set_margin_start(8) fields_box.set_margin_end(8) fields_box.set_margin_bottom(8) - - # Account Unit + _add_label_value_pair(fields_box, "Account Unit:", unit) - - # Account Type _add_label_value_pair(fields_box, "Account Type:", account_type) - - # Account Path _add_label_value_pair(fields_box, "Account Path:", path) - + card.append(fields_box) - return card - + + def _add_label_value_pair(container, label_text, value_text): """Add a label:value pair to a container""" row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) - + label = Gtk.Label(label=label_text) label.set_halign(Gtk.Align.START) label.add_css_class("dim-label") row.append(label) - + value = Gtk.Label(label=value_text) value.set_halign(Gtk.Align.START) value.add_css_class("monospace") value.set_hexpand(True) row.append(value) - + container.append(row) - -def _create_attachment_card( filename, metadata): + + +def _create_attachment_card(filename, metadata, on_click=None): """Create an attachment card""" card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) card.add_css_class("card") card.set_margin_top(8) card.set_margin_bottom(8) card.set_size_request(200, -1) - - # Icon and filename + + if on_click: + gesture = Gtk.GestureClick.new() + gesture.connect("pressed", lambda gesture, n_press, x, y: on_click(filename)) + card.add_controller(gesture) + card.set_cursor(Gdk.Cursor.new_from_name("pointer")) + header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) header_box.set_margin_top(12) header_box.set_margin_start(12) header_box.set_margin_end(12) - + icon = Gtk.Image.new_from_icon_name("text-x-generic-symbolic") header_box.append(icon) - + name_label = Gtk.Label(label=filename) name_label.set_halign(Gtk.Align.START) name_label.add_css_class("caption") name_label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) header_box.append(name_label) - + card.append(header_box) - - # Metadata + meta_label = Gtk.Label(label=metadata) meta_label.set_halign(Gtk.Align.START) meta_label.add_css_class("dim-label") @@ -380,5 +344,74 @@ def _create_attachment_card( filename, metadata): meta_label.set_margin_start(12) meta_label.set_margin_bottom(12) card.append(meta_label) - - return card -\ No newline at end of file + + return card + +def _open_attachment_viewer(parent_window, asset, fetch_fn): + """Fetch bytes in a background thread then open the appropriate viewer.""" + + def on_fetched(bytes_data): + mime = asset.mime or "" + if mime.startswith("image/"): + _show_image_viewer(parent_window, asset.slug, bytes_data) + elif mime == "application/pdf": + _show_pdf_viewer(parent_window, asset.slug, bytes_data) + elif mime.startswith("text/") or mime in ("application/json", "application/xml"): + _show_text_viewer(parent_window, asset.slug, bytes_data) + else: + _show_unsupported_dialog(parent_window, asset.slug, mime) + + threading.Thread( + target=lambda: GLib.idle_add(on_fetched, fetch_fn(asset.digest)), + daemon=True + ).start() + + +def _show_image_viewer(parent, title, data): + dialog = Gtk.Window(title=title) + dialog.set_transient_for(parent) + dialog.set_modal(True) + dialog.set_default_size(800, 600) + + loader = GdkPixbuf.PixbufLoader() + loader.write(data) + loader.close() + pixbuf = loader.get_pixbuf() + + scroll = Gtk.ScrolledWindow() + image = Gtk.Picture.new_for_pixbuf(pixbuf) + image.set_content_fit(Gtk.ContentFit.CONTAIN) + scroll.set_child(image) + dialog.set_child(scroll) + dialog.present() + + +def _show_text_viewer(parent, title, data): + dialog = Gtk.Window(title=title) + dialog.set_transient_for(parent) + dialog.set_modal(True) + dialog.set_default_size(800, 600) + + scroll = Gtk.ScrolledWindow() + text_view = Gtk.TextView() + text_view.set_editable(False) + text_view.set_monospace(True) + text_view.get_buffer().set_text(data.decode("utf-8", errors="replace")) + scroll.set_child(text_view) + dialog.set_child(scroll) + dialog.present() + + +def _show_pdf_viewer(parent, title, data): + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f: + f.write(data) + tmp_path = f.name + subprocess.Popen(["evince", tmp_path]) + + +def _show_unsupported_dialog(parent, filename, mime): + dialog = Gtk.AlertDialog() + dialog.set_message(f"Cannot preview '{filename}'") + dialog.set_detail(f"No viewer available for type: {mime}") + dialog.show(parent) +