commit f8d3bdb1d0dad727132590b4d025801d1e4daf98
parent b49271fd252daca2424701499990e422886d8e13
Author: Carlosokumu <carlosokumu254@gmail.com>
Date: Thu, 19 Feb 2026 19:42:40 +0300
initialize gui
Diffstat:
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)