mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Add basic Toga GUI
This commit is contained in:
@@ -32,3 +32,4 @@ requests>=2.32
|
|||||||
python-multipart
|
python-multipart
|
||||||
orjson
|
orjson
|
||||||
argon2-cffi
|
argon2-cffi
|
||||||
|
toga-core>=0.5.2
|
||||||
|
@@ -27,3 +27,4 @@ requests>=2.32
|
|||||||
python-multipart
|
python-multipart
|
||||||
orjson
|
orjson
|
||||||
argon2-cffi
|
argon2-cffi
|
||||||
|
toga-core>=0.5.2
|
||||||
|
11
src/seedpass_gui/__init__.py
Normal file
11
src/seedpass_gui/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"""Graphical user interface for SeedPass."""
|
||||||
|
|
||||||
|
from .app import SeedPassApp
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Launch the GUI application."""
|
||||||
|
SeedPassApp().main_loop()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["SeedPassApp", "main"]
|
199
src/seedpass_gui/app.py
Normal file
199
src/seedpass_gui/app.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import toga
|
||||||
|
from toga.style import Pack
|
||||||
|
from toga.style.pack import COLUMN, ROW
|
||||||
|
|
||||||
|
from seedpass.core.manager import PasswordManager
|
||||||
|
from seedpass.core.api import (
|
||||||
|
VaultService,
|
||||||
|
EntryService,
|
||||||
|
UnlockRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LockScreenWindow(toga.Window):
|
||||||
|
"""Window prompting for the master password."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, app: SeedPassApp, vault: VaultService, entries: EntryService
|
||||||
|
) -> None:
|
||||||
|
super().__init__("Unlock Vault")
|
||||||
|
self.app = app
|
||||||
|
self.vault = vault
|
||||||
|
self.entries = entries
|
||||||
|
|
||||||
|
self.password_input = toga.PasswordInput(style=Pack(flex=1))
|
||||||
|
self.message = toga.Label("", style=Pack(color="red"))
|
||||||
|
unlock_button = toga.Button(
|
||||||
|
"Unlock", on_press=self.handle_unlock, style=Pack(padding_top=10)
|
||||||
|
)
|
||||||
|
|
||||||
|
box = toga.Box(style=Pack(direction=COLUMN, padding=20))
|
||||||
|
box.add(toga.Label("Master Password:"))
|
||||||
|
box.add(self.password_input)
|
||||||
|
box.add(unlock_button)
|
||||||
|
box.add(self.message)
|
||||||
|
self.content = box
|
||||||
|
|
||||||
|
def handle_unlock(self, widget: toga.Widget) -> None:
|
||||||
|
password = self.password_input.value or ""
|
||||||
|
try:
|
||||||
|
self.vault.unlock(UnlockRequest(password=password))
|
||||||
|
except Exception as exc: # pragma: no cover - GUI error handling
|
||||||
|
self.message.text = str(exc)
|
||||||
|
return
|
||||||
|
main = MainWindow(self.app, self.vault, self.entries)
|
||||||
|
self.app.main_window = main
|
||||||
|
main.show()
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(toga.Window):
|
||||||
|
"""Main application window showing vault entries."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, app: SeedPassApp, vault: VaultService, entries: EntryService
|
||||||
|
) -> None:
|
||||||
|
super().__init__("SeedPass")
|
||||||
|
self.app = app
|
||||||
|
self.vault = vault
|
||||||
|
self.entries = entries
|
||||||
|
|
||||||
|
self.table = toga.Table(
|
||||||
|
headings=["ID", "Label", "Username", "URL"], style=Pack(flex=1)
|
||||||
|
)
|
||||||
|
|
||||||
|
add_button = toga.Button("Add", on_press=self.add_entry)
|
||||||
|
edit_button = toga.Button("Edit", on_press=self.edit_entry)
|
||||||
|
search_button = toga.Button("Search", on_press=self.search_entries)
|
||||||
|
|
||||||
|
button_box = toga.Box(style=Pack(direction=ROW, padding_top=5))
|
||||||
|
button_box.add(add_button)
|
||||||
|
button_box.add(edit_button)
|
||||||
|
button_box.add(search_button)
|
||||||
|
|
||||||
|
box = toga.Box(style=Pack(direction=COLUMN, padding=10))
|
||||||
|
box.add(self.table)
|
||||||
|
box.add(button_box)
|
||||||
|
self.content = box
|
||||||
|
|
||||||
|
self.refresh_entries()
|
||||||
|
|
||||||
|
def refresh_entries(self) -> None:
|
||||||
|
self.table.data = []
|
||||||
|
for idx, label, username, url, _arch in self.entries.list_entries():
|
||||||
|
self.table.data.append((idx, label, username or "", url or ""))
|
||||||
|
|
||||||
|
# --- Button handlers -------------------------------------------------
|
||||||
|
def add_entry(self, widget: toga.Widget) -> None:
|
||||||
|
dlg = EntryDialog(self, None)
|
||||||
|
dlg.show()
|
||||||
|
|
||||||
|
def edit_entry(self, widget: toga.Widget) -> None:
|
||||||
|
if self.table.selection is None:
|
||||||
|
return
|
||||||
|
entry_id = int(self.table.selection[0])
|
||||||
|
dlg = EntryDialog(self, entry_id)
|
||||||
|
dlg.show()
|
||||||
|
|
||||||
|
def search_entries(self, widget: toga.Widget) -> None:
|
||||||
|
dlg = SearchDialog(self)
|
||||||
|
dlg.show()
|
||||||
|
|
||||||
|
|
||||||
|
class EntryDialog(toga.Window):
|
||||||
|
"""Dialog for adding or editing an entry."""
|
||||||
|
|
||||||
|
def __init__(self, main: MainWindow, entry_id: int | None) -> None:
|
||||||
|
title = "Add Entry" if entry_id is None else "Edit Entry"
|
||||||
|
super().__init__(title)
|
||||||
|
self.main = main
|
||||||
|
self.entry_id = entry_id
|
||||||
|
|
||||||
|
self.label_input = toga.TextInput(style=Pack(flex=1))
|
||||||
|
self.username_input = toga.TextInput(style=Pack(flex=1))
|
||||||
|
self.url_input = toga.TextInput(style=Pack(flex=1))
|
||||||
|
self.length_input = toga.NumberInput(
|
||||||
|
min_value=8, max_value=128, style=Pack(width=80), value=16
|
||||||
|
)
|
||||||
|
|
||||||
|
save_button = toga.Button(
|
||||||
|
"Save", on_press=self.save, style=Pack(padding_top=10)
|
||||||
|
)
|
||||||
|
|
||||||
|
box = toga.Box(style=Pack(direction=COLUMN, padding=20))
|
||||||
|
box.add(toga.Label("Label"))
|
||||||
|
box.add(self.label_input)
|
||||||
|
box.add(toga.Label("Username"))
|
||||||
|
box.add(self.username_input)
|
||||||
|
box.add(toga.Label("URL"))
|
||||||
|
box.add(self.url_input)
|
||||||
|
box.add(toga.Label("Length"))
|
||||||
|
box.add(self.length_input)
|
||||||
|
box.add(save_button)
|
||||||
|
self.content = box
|
||||||
|
|
||||||
|
if entry_id is not None:
|
||||||
|
entry = self.main.entries.retrieve_entry(entry_id)
|
||||||
|
if entry:
|
||||||
|
self.label_input.value = entry.get("label", "")
|
||||||
|
self.username_input.value = entry.get("username", "") or ""
|
||||||
|
self.url_input.value = entry.get("url", "") or ""
|
||||||
|
self.length_input.value = entry.get("length", 16)
|
||||||
|
|
||||||
|
def save(self, widget: toga.Widget) -> None:
|
||||||
|
label = self.label_input.value or ""
|
||||||
|
username = self.username_input.value or None
|
||||||
|
url = self.url_input.value or None
|
||||||
|
length = int(self.length_input.value or 16)
|
||||||
|
|
||||||
|
if self.entry_id is None:
|
||||||
|
self.main.entries.add_entry(label, length, username=username, url=url)
|
||||||
|
else:
|
||||||
|
self.main.entries.modify_entry(
|
||||||
|
self.entry_id, username=username, url=url, label=label
|
||||||
|
)
|
||||||
|
self.main.refresh_entries()
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
class SearchDialog(toga.Window):
|
||||||
|
"""Dialog for searching entries."""
|
||||||
|
|
||||||
|
def __init__(self, main: MainWindow) -> None:
|
||||||
|
super().__init__("Search Entries")
|
||||||
|
self.main = main
|
||||||
|
self.query_input = toga.TextInput(style=Pack(flex=1))
|
||||||
|
search_button = toga.Button(
|
||||||
|
"Search", on_press=self.do_search, style=Pack(padding_top=10)
|
||||||
|
)
|
||||||
|
box = toga.Box(style=Pack(direction=COLUMN, padding=20))
|
||||||
|
box.add(toga.Label("Query"))
|
||||||
|
box.add(self.query_input)
|
||||||
|
box.add(search_button)
|
||||||
|
self.content = box
|
||||||
|
|
||||||
|
def do_search(self, widget: toga.Widget) -> None:
|
||||||
|
query = self.query_input.value or ""
|
||||||
|
results = self.main.entries.search_entries(query)
|
||||||
|
self.main.table.data = []
|
||||||
|
for idx, label, username, url, _arch in results:
|
||||||
|
self.main.table.data.append((idx, label, username or "", url or ""))
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
def build() -> SeedPassApp:
|
||||||
|
return SeedPassApp()
|
||||||
|
|
||||||
|
|
||||||
|
class SeedPassApp(toga.App):
|
||||||
|
def startup(self) -> None: # pragma: no cover - GUI bootstrap
|
||||||
|
pm = PasswordManager()
|
||||||
|
self.vault_service = VaultService(pm)
|
||||||
|
self.entry_service = EntryService(pm)
|
||||||
|
self.lock_window = LockScreenWindow(
|
||||||
|
self, self.vault_service, self.entry_service
|
||||||
|
)
|
||||||
|
self.main_window = None
|
||||||
|
self.lock_window.show()
|
Reference in New Issue
Block a user