usawa

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

commit f8d3bdb1d0dad727132590b4d025801d1e4daf98
parent b49271fd252daca2424701499990e422886d8e13
Author: Carlosokumu <carlosokumu254@gmail.com>
Date:   Thu, 19 Feb 2026 19:42:40 +0300

initialize gui

Diffstat:
Adummy/usawa/gui/__init__.py | 37+++++++++++++++++++++++++++++++++++++
Adummy/usawa/gui/controllers/entry_controller.py | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adummy/usawa/gui/main_window.py | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adummy/usawa/gui/models/__init__.py | 0
Adummy/usawa/gui/models/entry_item.py | 32++++++++++++++++++++++++++++++++
Adummy/usawa/gui/views/create_entry_view.py | 527+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adummy/usawa/gui/views/entry_details_view.py | 386+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adummy/usawa/gui/views/entry_list_view.py | 607+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adummy/usawa/runnable/gui.py | 11+++++++++++
9 files changed, 1732 insertions(+), 0 deletions(-)

diff --git a/dummy/usawa/gui/__init__.py b/dummy/usawa/gui/__init__.py @@ -0,0 +1,36 @@ +import logging +import signal +import gi + +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +from gi.repository import Gtk, Adw,Gio +from .main_window import UsawaMainWindow +logg = logging.getLogger("gui.app") + +class Usawa(Adw.Application): + def __init__(self, *args, **kwargs): + super().__init__(flags= Gio.ApplicationFlags.HANDLES_COMMAND_LINE,*args, **kwargs) + self.win = None + self.ledger_file = None + self.connect('activate', self.on_activate) + signal.signal(signal.SIGINT, self._handle_sigint) + + + def on_activate(self, app): + self.win = UsawaMainWindow(application=app,ledger_path=self.ledger_file) + self.win.present() + + + def _handle_sigint(self,*_): + logg.debug("shutdown") + self.quit() + + def do_command_line(self, cli): + args = cli.get_arguments() + + if len(args) > 1: + self.ledger_file = args[1] + + self.activate() + return 0 +\ No newline at end of file diff --git a/dummy/usawa/gui/controllers/entry_controller.py b/dummy/usawa/gui/controllers/entry_controller.py @@ -0,0 +1,53 @@ +import logging +from typing import Optional + +from usawa.core.entry_service import EntryService +from ...core.models import LedgerEntry + +logg = logging.getLogger("gui.entry_controller") + + +class EntryController: + + """Handles entry creation logic""" + def __init__(self,entry_service: EntryService): + self.entry_service = entry_service + + def collect_entry_data(self, view) -> Optional[LedgerEntry]: + """Collect data from the view and create an entry""" + try: + entry = LedgerEntry( + external_reference=view.ref_entry.get_text().strip() or None, + description=view.desc_entry.get_text().strip() or None, + amount=float(view.amount_entry.get_text() or "0"), + source_unit="BTC", + source_type=view.get_source_type(), + source_path=view.source_path_entry.get_text().strip(), + dest_unit="BTC", + dest_type=view.get_dest_type(), + dest_path=view.dest_path_entry.get_text().strip(), + ) + + is_valid, error_msg = entry.validate() + if not is_valid: + logg.error(f"Validation failed: {error_msg}") + return None + return entry + + except ValueError as e: + logg.error(f"Failed to collect entry data: {e}") + return None + + def finalize_entry(self, entry: LedgerEntry) -> bool: + """Save the entry to the ledger""" + try: + logg.debug("Entry: %s", entry) + success = self.entry_service.save_entry(entry) + if success: + logg.info(f"Entry saved successfully") + else: + logg.error("Failed to save entry") + return True + except Exception as e: + logg.error(f"Failed to save entry: {e}") + return False +\ No newline at end of file diff --git a/dummy/usawa/gui/main_window.py b/dummy/usawa/gui/main_window.py @@ -0,0 +1,78 @@ +import base64 +import logging +from usawa.crypto import ACL +from usawa.service import UnixClient +from usawa.core.entry_service import EntryService +from usawa.ledger import Ledger +from usawa.storage.ledger_repository import LedgerRepository +from usawa.store import LedgerStore +from gi.repository import Adw, Gtk +from whee.valkey import ValkeyStore + +from usawa.gui.controllers.entry_controller import EntryController +from usawa.gui.models.entry_item import EntryItem +from usawa.gui.views.entry_list_view import EntryListView +from usawa import Ledger, DemoWallet, load + +logg = logging.getLogger("gui.mainwindow") + +class UsawaMainWindow(Gtk.ApplicationWindow): + + + def __init__(self, application,ledger_path=None, **kwargs): + super().__init__(application=application, **kwargs) + + self.set_title("Usawa") + self.set_default_size(1000, 600) + + # Main box + main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + self.set_child(main_box) + + ledger_tree = load(ledger_path) + ledger = Ledger.from_tree(ledger_tree) + + + db = ValkeyStore('') + self.client = UnixClient(path="") + + store = LedgerStore(db, ledger) + pk = store.get_key() + wallet = DemoWallet(privatekey=pk) + logg.debug("wallet pk: %s pubk: %s", wallet.privkey().hex(), wallet.pubkey().hex()) + ledger.set_wallet(wallet) + + + ledger.acl = ACL.from_wallet(wallet) + store.load(acl=ledger.acl) + + logg.debug("Ledger has %d entries", len(ledger.entries)) + + for serial, entry in ledger.entries.items(): + logg.debug("Entry serial: %s", serial) + + + repository = LedgerRepository(ledger_store=store,unix_client=self.client,wallet=wallet) + + entry_service = EntryService(repository=repository,unixClient= self.client) + self.entry_controller = EntryController(entry_service=entry_service) + + # Navigation view + self.nav_view = Adw.NavigationView() + main_box.append(self.nav_view) + + entry_list_page = self._create_entry_list_page() + self.nav_view.add(entry_list_page) + + + def _create_entry_list_page(self): + page = Adw.NavigationPage( + title="Ledger Entries", + tag="entry-list" + ) + + entry_list_view = EntryListView(nav_view=self.nav_view,entry_controller=self.entry_controller) + page.set_child(entry_list_view) + + return page + diff --git a/dummy/usawa/gui/models/__init__.py b/dummy/usawa/gui/models/__init__.py diff --git a/dummy/usawa/gui/models/entry_item.py b/dummy/usawa/gui/models/entry_item.py @@ -0,0 +1,31 @@ +from gi.repository import Adw, Gtk, Gio,GObject + + +class EntryItem(GObject.Object): + """Data model for a ledger entry""" + + serial = GObject.Property(type=int, default=0) + tx_date = GObject.Property(type=str, default="") + description = GObject.Property(type=str, default="") + auth_state = GObject.Property(type=str, default="unsigned") + source_unit = GObject.Property(type=str, default="") + source_type = GObject.Property(type=str, default="") + source_path = GObject.Property(type=str, default="") + dest_unit = GObject.Property(type=str, default="") + dest_type = GObject.Property(type=str, default="") + dest_path = GObject.Property(type=str, default="") + + def __init__(self, serial=0, tx_date="", description="", auth_state="unsigned", + source_unit="", source_type="", source_path="", + dest_unit="", dest_type="", dest_path=""): + super().__init__() + self.serial = serial + self.tx_date = tx_date + self.description = description + self.auth_state = auth_state + self.source_unit = source_unit + self.source_type = source_type + self.source_path = source_path + self.dest_unit = dest_unit + self.dest_type = dest_type + self.dest_path = dest_path +\ No newline at end of file diff --git a/dummy/usawa/gui/views/create_entry_view.py b/dummy/usawa/gui/views/create_entry_view.py @@ -0,0 +1,526 @@ +import logging +from gi.repository import Gtk, Adw,Gio,Pango +from pathlib import Path +import mimetypes + +logg = logging.getLogger("gui.create_entry_view") + + +def create_entry_page(nav_view, controller): + """Create a new entry page""" + page = Adw.NavigationPage( + title="Create New Entry", + tag="create-entry" + ) + + view = CreateEntryView(nav_view, controller) + page.set_child(view) + + return page + + +class CreateEntryView(Gtk.Box): + """Create entry view - UI ONLY""" + + def __init__(self, nav_view, controller): + super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0) + + self.nav_view = nav_view + self.controller = controller + # self.attachment_list = [] + self.attachment_paths: list[str] = [] + + # Build UI + self._build_ui(nav_view) + + def _build_ui(self,nav_view): + """Build the UI""" + header = self._create_header(nav_view=nav_view) + self.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=16) + content.set_margin_top(16) + content.set_margin_bottom(16) + content.set_margin_start(16) + content.set_margin_end(16) + + # Sections + basic_section = self._create_basic_section() + content.append(basic_section) + + transaction_section = self._create_transaction_section() + content.append(transaction_section) + + attachments_section = self._create_attachments_section() + content.append(attachments_section) + + warning = self._create_warning() + content.append(warning) + + scrolled.set_child(content) + self.append(scrolled) + + # Action bar + action_bar = self._create_action_bar() + self.append(action_bar) + + + def _create_header(self,nav_view): + """Create the header with back button and serial number""" + 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) + + back_btn = Gtk.Button() + back_btn.set_icon_name("go-previous-symbolic") + back_btn.add_css_class("flat") + back_btn.connect("clicked", lambda b: nav_view.pop()) + header_box.append(back_btn) + + title = Gtk.Label(label="Create new Entry") + title.add_css_class("title-2") + header_box.append(title) + + serial_badge = Gtk.Label(label="Next Serial: #0002") + serial_badge.add_css_class("caption") + serial_badge.add_css_class("accent") + 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_basic_section(self): + """Create basic details section - UI ONLY""" + section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + + + header = Gtk.Label(label="BASIC DETAILS") + header.set_halign(Gtk.Align.START) + header.add_css_class("heading") + section_box.append(header) + + + grid = Gtk.Grid() + grid.set_column_spacing(12) + grid.set_row_spacing(12) + + ref_label = Gtk.Label(label="External reference (optional)") + ref_label.set_halign(Gtk.Align.START) + ref_label.add_css_class("dim-label") + grid.attach(ref_label, 0, 0, 2, 1) + + self.ref_entry = Gtk.Entry() + self.ref_entry.set_hexpand(True) + grid.attach(self.ref_entry, 0, 1, 2, 1) + + desc_label = Gtk.Label(label="Description (optional)") + desc_label.set_halign(Gtk.Align.START) + desc_label.add_css_class("dim-label") + grid.attach(desc_label, 0, 2, 2, 1) + + self.desc_entry = Gtk.Entry() + self.desc_entry.set_placeholder_text("Brief description of the entry") + self.desc_entry.set_hexpand(True) + grid.attach(self.desc_entry, 0, 3, 2, 1) + + section_box.append(grid) + return section_box + + def _create_transaction_section(self): + """Create transaction details section with per-side currencies""" + 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_label = Gtk.Label(label="Amount") + amount_label.set_halign(Gtk.Align.START) + amount_label.add_css_class("dim-label") + section_box.append(amount_label) + + self.amount_entry = Gtk.Entry() + self.amount_entry.set_placeholder_text("0.00") + section_box.append(self.amount_entry) + + ledger_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16) + ledger_box.set_homogeneous(True) + + + source_card = self._create_ledger_side_card("Source", is_source=True) + ledger_box.append(source_card) + + + dest_card = self._create_ledger_side_card("Destination", 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) + + + header = Gtk.Label(label="ATTACHMENTS") + header.set_halign(Gtk.Align.START) + header.add_css_class("heading") + section_box.append(header) + + attachments_card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + attachments_card.add_css_class("card") + attachments_card.set_margin_top(8) + attachments_card.set_margin_bottom(8) + attachments_card.set_margin_start(8) + attachments_card.set_margin_end(8) + + + header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + header_box.set_margin_top(8) + header_box.set_margin_start(8) + header_box.set_margin_end(8) + + attach_label = Gtk.Label(label="Add supporting Documents") + attach_label.set_halign(Gtk.Align.START) + attach_label.set_hexpand(True) + header_box.append(attach_label) + + add_btn = Gtk.Button() + add_btn.set_icon_name("list-add-symbolic") + add_btn.set_label("Add File") + add_btn.add_css_class("flat") + add_btn.connect("clicked", self.on_add_attachment) + header_box.append(add_btn) + + attachments_card.append(header_box) + + self.attachment_list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + self.attachment_list.set_margin_start(8) + self.attachment_list.set_margin_end(8) + self.attachment_list.set_margin_bottom(8) + + attachments_card.append(self.attachment_list) + + section_box.append(attachments_card) + + return section_box + + + def _create_attachment_row(self, filename: str, file_path: str, metadata: str): + """Create a single attachment row""" + row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + row.add_css_class("card") + row.set_margin_top(4) + row.set_margin_bottom(4) + row.set_margin_start(4) + row.set_margin_end(4) + + # Store file path as data + row.file_path = file_path + + # File icon based on MIME type + icon = self._get_icon_for_file(filename, metadata) + row.append(icon) + + # File info + info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) + info_box.set_hexpand(True) + + 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) + info_box.append(name_label) + + meta_label = Gtk.Label(label=metadata) + meta_label.set_halign(Gtk.Align.START) + meta_label.add_css_class("dim-label") + meta_label.add_css_class("caption") + info_box.append(meta_label) + + row.append(info_box) + + remove_btn = Gtk.Button(label="Remove") + remove_btn.add_css_class("destructive-action") + remove_btn.connect("clicked", lambda b: self._on_remove_attachment(row)) + row.append(remove_btn) + + return row + + + + def _create_ledger_side_card( self,title, is_source=True): + """Create a card for source or destination with its own unit""" + card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + card.add_css_class("card") + card.set_margin_top(8) + card.set_margin_bottom(8) + card.set_margin_start(8) + card.set_margin_end(8) + + 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 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + fields.set_margin_start(8) + fields.set_margin_end(8) + fields.set_margin_bottom(8) + + # Unit dropdown + unit_label = Gtk.Label(label="Unit") + unit_label.set_halign(Gtk.Align.START) + unit_label.add_css_class("caption") + fields.append(unit_label) + + units = Gtk.StringList() + units.append("Bitcoin(BTC)") + units.append("USD") + units.append("EUR") + + unit_dropdown = Gtk.DropDown(model=units) + unit_dropdown.set_selected(0 if is_source else 1) + fields.append(unit_dropdown) + + # Account Type dropdown + type_label = Gtk.Label(label="Account type") + type_label.set_halign(Gtk.Align.START) + type_label.add_css_class("caption") + fields.append(type_label) + + types = Gtk.StringList() + if is_source: + types.append("Expense") + types.append("Asset") + types.append("Liability") + types.append("Income") + else: + types.append("Asset") + types.append("Expense") + types.append("Liability") + types.append("Income") + + type_dropdown = Gtk.DropDown(model=types) + fields.append(type_dropdown) + + # Account Path entry + path_label = Gtk.Label(label="Account path") + path_label.set_halign(Gtk.Align.START) + path_label.add_css_class("caption") + fields.append(path_label) + + path_entry = Gtk.Entry() + path_entry.set_text("general") + fields.append(path_entry) + + card.append(fields) + + # Store references + if is_source: + self.source_unit_dropdown = unit_dropdown + self.source_type_dropdown = type_dropdown + self.source_path_entry = path_entry + else: + self.dest_unit_dropdown = unit_dropdown + self.dest_type_dropdown = type_dropdown + self.dest_path_entry = path_entry + + return card + + + def _create_warning(self): + """Create warning note""" + warning = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + warning.add_css_class("card") + warning.add_css_class("warning") + warning.set_margin_top(8) + warning.set_margin_bottom(8) + warning.set_margin_start(8) + warning.set_margin_end(8) + + icon = Gtk.Image.new_from_icon_name("dialog-warning-symbolic") + warning.append(icon) + + text = Gtk.Label() + text.set_markup("<b>Note:</b> Entries cannot be saved as drafts. You must either Finalize or Discard.") + text.set_wrap(True) + text.set_hexpand(True) + text.set_halign(Gtk.Align.START) + warning.append(text) + + return warning + + def _create_action_bar(self): + """Create action bar with buttons""" + action_bar = Gtk.ActionBar() + + discard_btn = Gtk.Button(label="Discard") + discard_btn.add_css_class("destructive-action") + discard_btn.connect("clicked", self._on_discard) + action_bar.pack_start(discard_btn) + + finalize_btn = Gtk.Button(label="Finalize Entry") + finalize_btn.add_css_class("suggested-action") + finalize_btn.connect("clicked", self._on_finalize) + action_bar.pack_end(finalize_btn) + + return action_bar + + # Helper methods to get dropdown values + def get_source_unit(self) -> str: + """Get selected source unit""" + selected_idx = self.source_unit_dropdown.get_selected() + model = self.source_unit_dropdown.get_model() + return model.get_string(selected_idx) + + def get_source_type(self) -> str: + """Get selected source type""" + selected_idx = self.source_type_dropdown.get_selected() + model = self.source_type_dropdown.get_model() + return model.get_string(selected_idx) + + def get_dest_unit(self) -> str: + """Get selected dest unit""" + selected_idx = self.dest_unit_dropdown.get_selected() + model = self.dest_unit_dropdown.get_model() + return model.get_string(selected_idx) + + def get_dest_type(self) -> str: + """Get selected dest type""" + selected_idx = self.dest_type_dropdown.get_selected() + model = self.dest_type_dropdown.get_model() + return model.get_string(selected_idx) + + # Event handlers + def _on_discard(self, button): + """Handle discard button""" + self.nav_view.pop() + + + def on_add_attachment(self, button): + """Handle add attachment button - multiple files""" + logg.info("Add attachment clicked") + + # Create file chooser dialog + file_dialog = Gtk.FileDialog() + file_dialog.set_title("Select Attachments") + + + filters = Gio.ListStore.new(Gtk.FileFilter) + all_filter = Gtk.FileFilter() + all_filter.set_name("All Files") + all_filter.add_pattern("*") + filters.append(all_filter) + file_dialog.set_filters(filters) + + file_dialog.open_multiple( + parent=self.get_root(), + callback=self._on_files_selected + ) + + def _on_files_selected(self, dialog, result): + """Handle multiple file selection""" + try: + files = dialog.open_multiple_finish(result) + + if files: + for i in range(files.get_n_items()): + file = files.get_item(i) + file_path = file.get_path() + logg.info(f"File selected: {file_path}") + self.attachment_paths.append(file_path) + self._add_attachment_to_list(file_path) + + except Exception as e: + logg.debug(f"File selection cancelled or failed: {e}") + + + def _get_icon_for_file(self, filename: str, metadata: str): + """Get appropriate icon for file type""" + icon_name = "text-x-generic-symbolic" # Default + + if "pdf" in metadata.lower(): + icon_name = "application-pdf-symbolic" + elif "image" in metadata.lower(): + icon_name = "image-x-generic-symbolic" + elif "text" in metadata.lower(): + icon_name = "text-x-generic-symbolic" + elif "video" in metadata.lower(): + icon_name = "video-x-generic-symbolic" + elif "audio" in metadata.lower(): + icon_name = "audio-x-generic-symbolic" + + icon = Gtk.Image.new_from_icon_name(icon_name) + return icon + + def _add_attachment_to_list(self, file_path: str): + path = Path(file_path) + + # Get file metadata + filename = path.name + file_size = path.stat().st_size + mime_type, _ = mimetypes.guess_type(file_path) + + if mime_type is None: + mime_type = "application/octet-stream" + + size_str = self._format_file_size(file_size) + + attachment_row = self._create_attachment_row( + filename=filename, + file_path=file_path, + metadata=f"{mime_type} • {size_str}" + ) + + self.attachment_list.append(attachment_row) + logg.info(f"Added attachment: {filename} ({size_str})") + + def _format_file_size(self, size_bytes: int) -> str: + """Format file size in human-readable form""" + for unit in ['B', 'KB', 'MB', 'GB']: + if size_bytes < 1024.0: + return f"{size_bytes:.1f} {unit}" + size_bytes /= 1024.0 + return f"{size_bytes:.1f} TB" + + def _on_finalize(self, button): + """Handle finalize button - delegates to controller""" + entry = self.controller.collect_entry_data(self) + # entry.attachments.extend(self.attachment_paths) + + if entry is None: + self._show_error_dialog("Invalid Input", + "Please check your entries and try again.") + return + + success = self.controller.finalize_entry(entry) + if success: + self.nav_view.pop() + else: + self._show_error_dialog("Save Failed", + "Could not save the entry. Please try again.") + + def _show_error_dialog(self, title, message): + """Show error dialog""" + dialog = Adw.MessageDialog( + transient_for=self.get_root(), + heading=title, + body=message + ) + dialog.add_response("ok", "OK") + dialog.present() +\ No newline at end of file diff --git a/dummy/usawa/gui/views/entry_details_view.py b/dummy/usawa/gui/views/entry_details_view.py @@ -0,0 +1,385 @@ +import logging +from gi.repository import Gtk, Adw,Pango + +logg = logging.getLogger("gui.entry_details") + +def create_entry_details_page(entry,nav_view): + """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 content (your existing content) + 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) + + 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""" + 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)", + "a3f7c8d9-4e2b-1a5c-9d8e-7f6a5b4c3d2e", 0, 1) + _add_field_to_grid(grid, "Transaction date", entry.tx_date, 1, 0) + _add_field_to_grid(grid, "Date registered", "2024-02-11 14:31:23 UTC", 1, 1) + + parent_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) + parent_label = Gtk.Label(label="Parent Digest") + parent_label.set_halign(Gtk.Align.START) + parent_label.add_css_class("dim-label") + parent_label.add_css_class("caption") + parent_box.append(parent_label) + + parent_value = Gtk.Label( + label="0x7f8e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e" + ) + 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.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.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.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): + """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 + field_box.append(value) + + grid.attach(field_box, col, row, 1, 1) + +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) + badge.set_margin_bottom(4) + 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("✓") + text.set_text("Trusted") + elif auth_state == "not_trusted": + badge.add_css_class("auth-not-trusted") + icon.set_text("⚠") + text.set_text("Not Trusted") + elif auth_state == "unknown": + badge.add_css_class("auth-unknown") + icon.set_text("?") + text.set_text("Unknown Key") + elif auth_state == "invalid": + badge.add_css_class("auth-invalid") + icon.set_text("✗") + text.set_text("Invalid") + else: + 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) + card.add_css_class("card") + card.set_margin_top(8) + 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): + """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 + 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") + meta_label.add_css_class("caption") + meta_label.set_margin_start(12) + meta_label.set_margin_bottom(12) + card.append(meta_label) + + return card +\ No newline at end of file diff --git a/dummy/usawa/gui/views/entry_list_view.py b/dummy/usawa/gui/views/entry_list_view.py @@ -0,0 +1,607 @@ +import logging +from gi.repository import Adw, Gtk, Gio,Pango + +from usawa.gui.models.entry_item import EntryItem +from usawa.gui.views.create_entry_view import create_entry_page +from usawa.gui.views.entry_details_view import create_entry_details_page + +logg = logging.getLogger("gui.mainwindow") + +class EntryListView(Gtk.Box): + + """The entry list view with filters, table, and FAB""" + def __init__(self, nav_view,entry_controller, **kwargs): + super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0, **kwargs) + + self.nav_view = nav_view + self.entry_controller = entry_controller + + # Overlay for FAB + overlay = Gtk.Overlay() + self.append(overlay) + + # Main content + content = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, + spacing=12, + ) + content.set_margin_top(12) + content.set_margin_bottom(12) + content.set_margin_start(12) + content.set_margin_end(12) + overlay.set_child(content) + + # Sort Section + sort_section = self._create_sort_section() + content.append(sort_section) + + # Filter Section + filter_section = self._create_filter_section() + content.append(filter_section) + + # Table Section + table_section = self._create_table_section() + content.append(table_section) + + # Floating Action Button + fab = Gtk.Button() + fab.set_icon_name("list-add-symbolic") + fab.add_css_class("circular") + fab.add_css_class("suggested-action") + fab.set_halign(Gtk.Align.END) + fab.set_valign(Gtk.Align.END) + fab.set_margin_end(24) + fab.set_margin_bottom(24) + overlay.add_overlay(fab) + fab.connect("clicked", self.on_fab_clicked) + + + def _create_sort_section(self): + """Create the Sort by section with toggle buttons""" + sort_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + + sort_label = Gtk.Label(label="Sort by") + sort_label.set_halign(Gtk.Align.START) + sort_label.add_css_class("heading") + sort_box.append(sort_label) + + button_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + button_container.set_margin_top(4) + button_container.set_margin_bottom(4) + button_container.set_margin_start(8) + button_container.set_margin_end(8) + button_container.add_css_class("card") + + self.sort_serial_btn = Gtk.ToggleButton(label="Serial Number") + self.sort_serial_btn.set_active(True) + self.sort_serial_btn.connect("toggled", self.on_sort_changed, "serial") + button_container.append(self.sort_serial_btn) + + self.sort_datetime_btn = Gtk.ToggleButton(label="Transaction Datetime") + self.sort_datetime_btn.connect("toggled", self.on_sort_changed, "datetime") + button_container.append(self.sort_datetime_btn) + + # Group the toggle buttons so only one can be active + self.sort_serial_btn.set_group(self.sort_datetime_btn) + + sort_box.append(button_container) + + return sort_box + + def _create_filter_section(self): + """Create the Filter section with account type, keyword, and date range""" + filter_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + + # Label + filter_label = Gtk.Label(label="Filter") + filter_label.set_halign(Gtk.Align.START) + filter_label.add_css_class("heading") + filter_box.append(filter_label) + + # Filter container with card styling + filter_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16) + filter_container.set_margin_top(8) + filter_container.set_margin_bottom(8) + filter_container.set_margin_start(8) + filter_container.set_margin_end(8) + filter_container.add_css_class("card") + + # Account Type Column + account_type_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + account_type_box.set_hexpand(True) + account_type_box.set_margin_start(8) + account_type_label = Gtk.Label(label="Account type") + account_type_label.set_halign(Gtk.Align.START) + account_type_label.add_css_class("caption") + account_type_label.set_margin_top(4) + account_type_box.set_margin_bottom(4) + account_type_box.append(account_type_label) + + # Dropdown for account type + account_types = Gtk.StringList() + account_types.append("All types") + account_types.append("Asset") + account_types.append("Liability") + account_types.append("Income") + account_types.append("Expense") + + self.account_type_dropdown = Gtk.DropDown(model=account_types) + self.account_type_dropdown.set_selected(1) # Default to "Asset" + self.account_type_dropdown.connect("notify::selected", self.on_filter_changed) + account_type_box.append(self.account_type_dropdown) + + filter_container.append(account_type_box) + + # Keyword or Account Path Column + keyword_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + keyword_box.set_hexpand(True) + + keyword_label = Gtk.Label(label="Keyword or Account path") + keyword_label.set_halign(Gtk.Align.START) + keyword_label.add_css_class("caption") + keyword_label.set_margin_top(4) + keyword_box.set_margin_bottom(4) + keyword_box.append(keyword_label) + + self.keyword_entry = Gtk.Entry() + self.keyword_entry.set_placeholder_text("Search in description or path...") + self.keyword_entry.set_text("rent/apartment") + self.keyword_entry.connect("changed", self.on_filter_changed) + keyword_label.set_margin_top(4) + keyword_box.append(self.keyword_entry) + + filter_container.append(keyword_box) + + # Date Range Column + date_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + date_box.set_hexpand(True) + + date_label = Gtk.Label(label="Date range") + date_label.set_halign(Gtk.Align.START) + date_label.add_css_class("caption") + date_label.set_margin_top(4) + date_box.set_margin_bottom(4) + date_box.append(date_label) + + # Date range container + date_range_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + + self.date_start_entry = Gtk.Entry() + self.date_start_entry.set_placeholder_text("MM-DD") + self.date_start_entry.set_text("02-10") + self.date_start_entry.set_max_width_chars(10) + self.date_start_entry.connect("changed", self.on_filter_changed) + date_range_box.append(self.date_start_entry) + + to_label = Gtk.Label(label="to") + to_label.add_css_class("dim-label") + date_range_box.append(to_label) + + self.date_end_entry = Gtk.Entry() + self.date_end_entry.set_placeholder_text("MM-DD") + self.date_end_entry.set_text("02-11") + self.date_end_entry.set_max_width_chars(10) + self.date_end_entry.connect("changed", self.on_filter_changed) + date_range_box.append(self.date_end_entry) + + calendar_btn = Gtk.Button() + calendar_btn.set_icon_name("x-office-calendar-symbolic") + calendar_btn.add_css_class("flat") + calendar_btn.connect("clicked", self.on_calendar_clicked) + date_range_box.append(calendar_btn) + + date_box.append(date_range_box) + + filter_container.append(date_box) + + filter_box.append(filter_container) + + return filter_box + + def on_filter_changed(self, widget, *args): + """Handle filter changes""" + logg.info("Filter changed") + # Get current filter values + account_type_idx = self.account_type_dropdown.get_selected() + keyword = self.keyword_entry.get_text() + date_start = self.date_start_entry.get_text() + date_end = self.date_end_entry.get_text() + + logg.debug(f"Filters - Type: {account_type_idx}, Keyword: {keyword}, " + f"Dates: {date_start} to {date_end}") + self.refresh_data() + + def on_calendar_clicked(self, button): + """Show calendar popup for date range selection""" + logg.info("Calendar button clicked - showing date range picker") + + # Create the dialog + dialog = Gtk.Dialog(transient_for=self, modal=True) + dialog.set_title("Select Date Range") + dialog.set_default_size(400, 450) + + content = dialog.get_content_area() + content.set_spacing(16) + content.set_margin_top(12) + content.set_margin_bottom(12) + content.set_margin_start(12) + content.set_margin_end(12) + + + main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) + content.append(main_box) + + instruction = Gtk.Label(label="Click to select start date, then click again for end date") + instruction.add_css_class("dim-label") + instruction.set_wrap(True) + instruction.set_halign(Gtk.Align.START) + main_box.append(instruction) + + calendar_card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + calendar_card.add_css_class("card") + calendar_card.set_margin_top(8) + calendar_card.set_margin_bottom(8) + + calendar = Gtk.Calendar() + calendar.set_margin_start(12) + calendar.set_margin_end(12) + calendar.set_margin_top(8) + calendar.set_margin_bottom(8) + calendar_card.append(calendar) + + main_box.append(calendar_card) + + selection_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) + selection_box.set_halign(Gtk.Align.CENTER) + selection_box.add_css_class("card") + selection_box.set_margin_top(8) + selection_box.set_margin_bottom(8) + selection_box.set_margin_start(12) + selection_box.set_margin_end(12) + + start_label = Gtk.Label() + start_label.set_markup("<b>Start:</b> --") + selection_box.append(start_label) + + arrow_label = Gtk.Label(label="→") + selection_box.append(arrow_label) + + end_label = Gtk.Label() + end_label.set_markup("<b>End:</b> --") + selection_box.append(end_label) + + main_box.append(selection_box) + dialog.present() + + + + + + def on_sort_changed(self, button, sort_type): + """Handle sort option changes""" + if button.get_active(): + logg.info(f"Sort changed to: {sort_type}") + + def on_fab_clicked(self, button): + logg.info("FAB clicked - opening create entry window") + # create_page = create_entry_page(self.nav_view) + create_page = create_entry_page(self.nav_view, self.entry_controller) + + # Push onto navigation stack + self.nav_view.push(create_page) + + + def refresh_data(self): + """Refresh the entry list based on current sort and filter settings""" + # TODO: Implement actual data refresh logic + logg.info("Refreshing data with current sort/filter settings") + + + def on_create_window_closed(self, window): + # Refresh the entry list + logg.info("Create entry window closed") + return False + + + def _create_table_section(self): + """Create the entry list table with all columns""" + table_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + + self.entry_store = Gio.ListStore.new(EntryItem) + selection_model = Gtk.SingleSelection.new(self.entry_store) + + column_view = Gtk.ColumnView(model=selection_model) + column_view.add_css_class("data-table") + column_view.set_show_row_separators(True) + column_view.set_show_column_separators(True) + + serial_factory = Gtk.SignalListItemFactory() + serial_factory.connect("setup", self._on_serial_setup) + serial_factory.connect("bind", self._on_serial_bind) + serial_col = Gtk.ColumnViewColumn(title="Serial No", factory=serial_factory) + serial_col.set_fixed_width(70) + column_view.append_column(serial_col) + + # Transaction date column - FIXED + date_factory = Gtk.SignalListItemFactory() + date_factory.connect("setup", self._on_date_setup) + date_factory.connect("bind", self._on_date_bind) + date_col = Gtk.ColumnViewColumn(title="Transaction date", factory=date_factory) + date_col.set_fixed_width(140) + column_view.append_column(date_col) + + # Description column - EXPAND (flexible) + desc_factory = Gtk.SignalListItemFactory() + desc_factory.connect("setup", self._on_desc_setup) + desc_factory.connect("bind", self._on_desc_bind) + desc_col = Gtk.ColumnViewColumn(title="Description", factory=desc_factory) + desc_col.set_expand(True) + column_view.append_column(desc_col) + + auth_factory = Gtk.SignalListItemFactory() + auth_factory.connect("setup", self._on_auth_setup) + auth_factory.connect("bind", self._on_auth_bind) + auth_col = Gtk.ColumnViewColumn(title="Auth state", factory=auth_factory) + auth_col.set_fixed_width(120) + column_view.append_column(auth_col) + + # Source column -(flexible) + source_factory = Gtk.SignalListItemFactory() + source_factory.connect("setup", self._on_source_setup) + source_factory.connect("bind", self._on_source_bind) + source_col = Gtk.ColumnViewColumn(title="Source", factory=source_factory) + source_col.set_expand(True) + column_view.append_column(source_col) + + # Destination column - (flexible) + dest_factory = Gtk.SignalListItemFactory() + dest_factory.connect("setup", self._on_dest_setup) + dest_factory.connect("bind", self._on_dest_bind) + dest_col = Gtk.ColumnViewColumn(title="Destination", factory=dest_factory) + dest_col.set_expand(True) + column_view.append_column(dest_col) + + + action_factory = Gtk.SignalListItemFactory() + action_factory.connect("setup", self._on_action_setup) + action_factory.connect("bind", self._on_action_bind) + action_col = Gtk.ColumnViewColumn(title="Action", factory=action_factory) + action_col.set_fixed_width(80) + column_view.append_column(action_col) + + scrolled = Gtk.ScrolledWindow() + scrolled.set_vexpand(True) + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled.set_child(column_view) + + table_box.append(scrolled) + self._populate_sample_data() + + return table_box + + + + def _on_serial_setup(self, factory, list_item): + label = Gtk.Label() + label.set_halign(Gtk.Align.START) + label.add_css_class("monospace") + list_item.set_child(label) + + def _on_serial_bind(self, factory, list_item): + entry = list_item.get_item() + label = list_item.get_child() + label.set_text(str(entry.serial)) + + def _on_date_setup(self, factory, list_item): + label = Gtk.Label() + label.set_halign(Gtk.Align.START) + label.add_css_class("monospace") + list_item.set_child(label) + + def _on_date_bind(self, factory, list_item): + entry = list_item.get_item() + label = list_item.get_child() + label.set_text(entry.tx_date) + + def _on_desc_setup(self, factory, list_item): + label = Gtk.Label() + label.set_halign(Gtk.Align.START) + label.set_ellipsize(Pango.EllipsizeMode.END) + list_item.set_child(label) + + def _on_desc_bind(self, factory, list_item): + entry = list_item.get_item() + label = list_item.get_child() + label.set_text(entry.description) + + def _on_auth_setup(self, factory, list_item): + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) + box.set_halign(Gtk.Align.CENTER) + + # Badge container + badge = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) + badge.set_margin_top(4) + badge.set_margin_bottom(4) + badge.set_margin_start(8) + badge.set_margin_end(8) + badge.add_css_class("badge") + + icon = Gtk.Label() + badge.append(icon) + + text = Gtk.Label() + text.add_css_class("caption") + badge.append(text) + + box.append(badge) + list_item.set_child(box) + + def _on_auth_bind(self, factory, list_item): + """Bind auth state data with styling""" + entry = list_item.get_item() + box = list_item.get_child() + badge = box.get_first_child() + + badge.remove_css_class("auth-trusted") + badge.remove_css_class("auth-not-trusted") + badge.remove_css_class("auth-unknown") + badge.remove_css_class("auth-invalid") + badge.remove_css_class("auth-unsigned") + + icon_label = badge.get_first_child() + text_label = icon_label.get_next_sibling() + + # Set content based on auth state + auth_state = entry.auth_state + if auth_state == "trusted": + badge.add_css_class("auth-trusted") + icon_label.set_text("✓") + text_label.set_text("Trusted") + elif auth_state == "not_trusted": + badge.add_css_class("auth-not-trusted") + icon_label.set_text("⚠") + text_label.set_text("Not Trusted") + elif auth_state == "unknown": + badge.add_css_class("auth-unknown") + icon_label.set_text("?") + text_label.set_text("Unknown Key") + elif auth_state == "invalid": + badge.add_css_class("auth-invalid") + icon_label.set_text("✗") + text_label.set_text("Invalid") + else: # unsigned + badge.add_css_class("auth-unsigned") + icon_label.set_text("○") + text_label.set_text("No Key") + + def _on_source_setup(self, factory, list_item): + """Setup source cell (compressed format)""" + label = Gtk.Label() + label.set_halign(Gtk.Align.START) + label.add_css_class("monospace") + label.add_css_class("source-cell") + label.set_margin_start(8) + list_item.set_child(label) + + def _on_source_bind(self, factory, list_item): + """Bind source data in compressed format: [BTC] Expense.deposit/rent""" + entry = list_item.get_item() + label = list_item.get_child() + + # Format: [UNIT] Type.path + formatted = f"[{entry.source_unit}]\n{entry.source_type}.{entry.source_path}" + label.set_text(formatted) + + def _on_dest_setup(self, factory, list_item): + """Setup destination cell (compressed format)""" + label = Gtk.Label() + label.set_halign(Gtk.Align.START) + label.add_css_class("monospace") + label.add_css_class("dest-cell") + label.set_margin_start(8) + list_item.set_child(label) + + def _on_dest_bind(self, factory, list_item): + """Bind destination data in compressed format""" + entry = list_item.get_item() + label = list_item.get_child() + + # Format: [UNIT] Type.path + formatted = f"[{entry.dest_unit}]\n{entry.dest_type}.{entry.dest_path}" + label.set_text(formatted) + + + def _on_action_setup(self, factory, list_item): + """Setup action cell""" + button = Gtk.Button(label="View") + button.add_css_class("link") + button.add_css_class("accent") + button.set_halign(Gtk.Align.CENTER) + button.handler_id = None + list_item.set_child(button) + + def _on_action_bind(self, factory, list_item): + """Bind action button""" + entry = list_item.get_item() + button = list_item.get_child() + + if hasattr(button, 'handler_id') and button.handler_id is not None: + button.disconnect(button.handler_id) + + button.handler_id = button.connect("clicked", self._on_view_entry, entry) + + def _on_view_entry(self, button, entry): + """Handle view button click""" + logg.info(f"View entry clicked: {entry.serial}") + details_page = create_entry_details_page(entry,self.nav_view) + self.nav_view.push(details_page) + + + def _populate_sample_data(self): + """Add sample entries to the table""" + sample_entries = [ + EntryItem( + serial=0, + tx_date="2024-02-11 14:30:00", + description="Monthly rent payment", + auth_state="trusted", + source_unit="BTC", + source_type="Expense", + source_path="deposit/rent", + dest_unit="BTC", + dest_type="Asset", + dest_path="rent/apartment" + ), + EntryItem( + serial=1, + tx_date="2024-02-11 09:15:00", + description="Utility deposit", + auth_state="not_trusted", + source_unit="BTC", + source_type="Asset", + source_path="bank/saving", + dest_unit="BTC", + dest_type="Asset", + dest_path="deposits/utils" + ), + EntryItem( + serial=2, + tx_date="2024-02-11 11:15:00", + description="Late payment fine", + auth_state="unknown", + source_unit="BTC", + source_type="Liability", + source_path="fees/late-pay", + dest_unit="BTC", + dest_type="Income", + dest_path="bank/checking" + ), + EntryItem( + serial=3, + tx_date="2024-02-11 4:15:00", + description="Security deposit", + auth_state="invalid", + source_unit="BTC", + source_type="Income", + source_path="bank/income", + dest_unit="BTC", + dest_type="Expense", + dest_path="bank/expense" + ), + EntryItem( + serial=4, + tx_date="2024-02-11 4:16:00", + description="Grocery purchase", + auth_state="unsigned", + source_unit="BTC", + source_type="Asset", + source_path="bank/checking", + dest_unit="BTC", + dest_type="Expense", + dest_path="food/groceries" + ), + ] + + for entry in sample_entries: + self.entry_store.append(entry) + diff --git a/dummy/usawa/runnable/gui.py b/dummy/usawa/runnable/gui.py @@ -0,0 +1,11 @@ +import logging +import sys + +from usawa.gui import Usawa + +logging.basicConfig(level=logging.DEBUG) + + +def main(): + app = Usawa(application_id='org.usawa.app') + app.run(sys.argv)