156 lines
5.9 KiB
Python
156 lines
5.9 KiB
Python
from rich.markup import escape
|
|
from textual import on
|
|
from textual.app import ComposeResult
|
|
from textual.binding import Binding
|
|
from textual.containers import Horizontal, VerticalScroll
|
|
from textual.widgets import DataTable, Static
|
|
|
|
import portfolio
|
|
from fields import get_path, summary_text, to_form_text
|
|
from portfolio import Collection
|
|
from tui.content_pane import ContentPane
|
|
from tui.dialogs import ConfirmDialog
|
|
from tui.record_editor import RecordEditor
|
|
|
|
|
|
def record_details(collection: Collection, record: dict) -> str:
|
|
lines = [f"[b]{escape(portfolio.record_title(collection, record))}[/b]", ""]
|
|
for spec in collection.fields[1:]:
|
|
value = get_path(record, spec.path)
|
|
if value in (None, "", [], {}, False):
|
|
continue
|
|
text = "yes" if value is True else to_form_text(spec.kind, value)
|
|
if "\n" in text or len(text) > 60:
|
|
lines += [f"[dim]{escape(spec.label)}[/dim]", escape(text), ""]
|
|
else:
|
|
lines.append(f"[dim]{escape(spec.label):<14}[/dim]{escape(text)}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
class RecordListPane(ContentPane):
|
|
BINDINGS = [
|
|
Binding("a", "add", "Add"),
|
|
Binding("e", "edit", "Edit"),
|
|
Binding("d", "delete", "Delete"),
|
|
Binding("f", "toggle_featured", "Featured"),
|
|
Binding("K", "move(-1)", "Move up"),
|
|
Binding("J", "move(1)", "Move down"),
|
|
Binding("r", "reload", "Reload"),
|
|
]
|
|
|
|
def __init__(self, collection: Collection, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.collection = collection
|
|
self.set_content_file(collection.file)
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield from self.compose_header()
|
|
with Horizontal(classes="split-layout"):
|
|
yield DataTable(cursor_type="row", zebra_stripes=True)
|
|
with VerticalScroll(classes="detail-scroll"):
|
|
yield Static("", classes="record-detail")
|
|
|
|
def compose_header(self) -> ComposeResult:
|
|
yield from ()
|
|
|
|
def on_mount(self):
|
|
self.build_columns()
|
|
self.reload()
|
|
|
|
def build_columns(self):
|
|
table = self.table()
|
|
table.clear(columns=True)
|
|
table.add_column("#", key="number")
|
|
for label, path in self.collection.columns:
|
|
table.add_column(label, key=path)
|
|
|
|
def records(self) -> list[dict]:
|
|
return portfolio.load_records(self.collection)
|
|
|
|
def reload(self, focus_key: str | None = None):
|
|
table = self.table()
|
|
table.clear()
|
|
for index, record in enumerate(self.records()):
|
|
cells = [summary_text(get_path(record, path)) for _, path in self.collection.columns]
|
|
table.add_row(str(index + 1), *cells, key=str(index))
|
|
self.restore_cursor(focus_key)
|
|
self.show_details(self.selected_key())
|
|
|
|
def selected_index(self) -> int | None:
|
|
key = self.selected_key()
|
|
return int(key) if key is not None else None
|
|
|
|
def show_details(self, key: str | None):
|
|
records = self.records()
|
|
detail = self.query_one(".record-detail", Static)
|
|
if key is None or int(key) >= len(records):
|
|
detail.update(f"No {self.collection.title.lower()} yet. Press [b]a[/b] to add one.")
|
|
return
|
|
detail.update(record_details(self.collection, records[int(key)]))
|
|
|
|
@on(DataTable.RowHighlighted)
|
|
def highlight_changed(self, event: DataTable.RowHighlighted):
|
|
self.show_details(event.row_key.value if event.row_key else None)
|
|
|
|
@on(DataTable.RowSelected)
|
|
def row_selected(self):
|
|
self.action_edit()
|
|
|
|
def open_editor(self, index: int | None, record: dict):
|
|
title = f"Edit {portfolio.record_title(self.collection, record)}" if index is not None else f"New {self.collection.title.lower()} entry"
|
|
|
|
def saved(result: dict | None):
|
|
if result is None:
|
|
return
|
|
self.run_now(
|
|
lambda: portfolio.save_record(self.collection, index, result),
|
|
f"Saved {portfolio.record_title(self.collection, result)}",
|
|
str(index) if index is not None else str(len(self.records())),
|
|
)
|
|
|
|
self.app.push_screen(RecordEditor(title, self.collection.fields, record, image_folder=self.image_folder()), saved)
|
|
|
|
def image_folder(self) -> str:
|
|
return ""
|
|
|
|
def action_add(self):
|
|
self.open_editor(None, {})
|
|
|
|
def action_edit(self):
|
|
index = self.selected_index()
|
|
if index is not None:
|
|
self.open_editor(index, self.records()[index])
|
|
|
|
def action_delete(self):
|
|
index = self.selected_index()
|
|
if index is None:
|
|
return
|
|
title = portfolio.record_title(self.collection, self.records()[index])
|
|
|
|
def confirmed(should_delete: bool | None):
|
|
if should_delete:
|
|
self.run_now(lambda: portfolio.remove_record(self.collection, index), f"Removed {title}", str(max(0, index - 1)))
|
|
|
|
self.app.push_screen(ConfirmDialog(f"Remove {title}?"), confirmed)
|
|
|
|
def action_move(self, step: int):
|
|
index = self.selected_index()
|
|
if index is None:
|
|
return
|
|
target = index + step
|
|
self.run_now(lambda: portfolio.move_record(self.collection, index, step), "Moved", str(target))
|
|
|
|
def action_toggle_featured(self):
|
|
index = self.selected_index()
|
|
if index is None:
|
|
return
|
|
if not any(spec.path == "featured" for spec in self.collection.fields):
|
|
self.notify(f"{self.collection.title} entries have no featured flag", severity="warning")
|
|
return
|
|
record = self.records()[index]
|
|
record["featured"] = not record.get("featured", False)
|
|
if not record["featured"]:
|
|
record.pop("featured")
|
|
state = "Featured" if record.get("featured") else "Unfeatured"
|
|
self.run_now(lambda: portfolio.save_record(self.collection, index, record), f"{state} {portfolio.record_title(self.collection, record)}", str(index))
|