Files
website/tools/content/tui/record_pane.py
T

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))