mirror of
https://git.srhtnin.garden/sarah/SaraTag.git
synced 2026-10-13 18:28:16 +02:00
191 lines
9.1 KiB
Python
191 lines
9.1 KiB
Python
"""Libraries: named groups of Records, browsed as one list of files (each
|
|
Record's top-level files, with a Record column to sort and filter by)."""
|
|
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import flet as ft
|
|
|
|
from Core import ChipButton, Heading, Note, Palette
|
|
from Library import DataError
|
|
from Records import CreateLibrary, DeleteLibrary, DirectorySize, LibraryEntry, ListLibraries, ListRecords, Record, SaveLibrary
|
|
from Widgets import App, EmptyState, HumanSize, Panel, Plural, SectionTitle, TextInput
|
|
|
|
|
|
class LibrariesPage(ft.Container):
|
|
def __init__(self, TheApp: App):
|
|
super().__init__(expand=True)
|
|
self.App = TheApp
|
|
self.Libraries: list[LibraryEntry] = []
|
|
self.Records: list[Record] = []
|
|
self.Selected: Optional[str] = None
|
|
self.Sizes: dict[Path, tuple[int, int]] = {}
|
|
self.SizeLabels: dict[Path, ft.Text] = {}
|
|
self.TotalLabel: Optional[ft.Text] = None
|
|
|
|
self.List = ft.ListView(spacing=4, expand=True, padding=8)
|
|
Left = ft.Container(
|
|
ft.Column([
|
|
ft.Container(SectionTitle("Libraries", [ChipButton("New", lambda e: self.NewDialog(), ft.Icons.ADD)]),
|
|
padding=ft.Padding.only(left=12, right=8, top=8)),
|
|
self.List,
|
|
], spacing=0, expand=True),
|
|
width=300, bgcolor=Palette.Mantle, border=ft.Border.only(right=ft.BorderSide(1, Palette.Surface1)),
|
|
)
|
|
self.Details = ft.Container(expand=True, padding=16)
|
|
self.content = ft.Row([Left, self.Details], spacing=0, expand=True, vertical_alignment=ft.CrossAxisAlignment.STRETCH)
|
|
|
|
def Refresh(self) -> None:
|
|
self.page.on_keyboard_event = None
|
|
try:
|
|
self.Libraries = ListLibraries()
|
|
self.Records = ListRecords()[0]
|
|
except DataError as Error:
|
|
self.App.Notify(str(Error), "error")
|
|
self.Libraries, self.Records = [], []
|
|
if self.Selected not in {Item.Name for Item in self.Libraries}:
|
|
self.Selected = self.Libraries[0].Name if self.Libraries else None
|
|
self._Build()
|
|
self.page.run_thread(self._MeasureSizes)
|
|
|
|
def Leave(self) -> None:
|
|
pass
|
|
|
|
def _Current(self) -> Optional[LibraryEntry]:
|
|
return next((Item for Item in self.Libraries if Item.Name == self.Selected), None)
|
|
|
|
def _Build(self) -> None:
|
|
Controls = []
|
|
for Item in self.Libraries:
|
|
Selected = Item.Name == self.Selected
|
|
Controls.append(ft.Container(
|
|
ft.Column([
|
|
ft.Row([ft.Icon(ft.Icons.COLLECTIONS_BOOKMARK_OUTLINED, size=18,
|
|
color=Palette.Mauve if Item.Enabled else Palette.Overlay0),
|
|
ft.Text(Item.Name, size=14, expand=True, color=Palette.Text if Item.Enabled else Palette.Overlay0)]),
|
|
Note(Plural(len(Item.Members), "Record") + ("" if Item.Enabled else " · disabled"), Size=11),
|
|
], spacing=2, tight=True),
|
|
padding=10, border_radius=6, ink=True, bgcolor=Palette.Surface0 if Selected else None,
|
|
border=ft.Border.all(1, Palette.Mauve if Selected else Palette.Surface0),
|
|
on_click=lambda e, Name=Item.Name: self._Select(Name),
|
|
))
|
|
self.List.controls = Controls or [Note("No Libraries yet. A Library lets you browse several Records as one list.")]
|
|
self.Details.content = self._DetailView(self._Current())
|
|
self.update()
|
|
|
|
def _Select(self, Name: str) -> None:
|
|
self.Selected = Name
|
|
self._Build()
|
|
|
|
def _MeasureSizes(self) -> None:
|
|
for Item in self.Records:
|
|
if Item.Directory not in self.Sizes and Item.Directory.is_dir():
|
|
self.Sizes[Item.Directory] = DirectorySize(Item.Directory)
|
|
# Only the size labels change, so half-made edits in the form survive.
|
|
for Directory, Label in self.SizeLabels.items():
|
|
Label.value = self._SizeText(Directory)
|
|
if self.TotalLabel is not None:
|
|
self.TotalLabel.value = self._TotalText(self._Current())
|
|
if self.page is not None:
|
|
self.update()
|
|
|
|
def _SizeText(self, Directory: Path) -> str:
|
|
Size = self.Sizes.get(Directory)
|
|
return f"{HumanSize(Size[0])} · {Plural(Size[1], 'file')}" if Size else "…"
|
|
|
|
def _TotalText(self, Item: Optional[LibraryEntry]) -> str:
|
|
if Item is None:
|
|
return ""
|
|
Members = [Record for Record in self.Records if Record.Directory in Item.Members]
|
|
Sizes = [self.Sizes.get(Record.Directory) for Record in Members]
|
|
Total = (f"{HumanSize(sum(Size[0] for Size in Sizes))} in {Plural(sum(Size[1] for Size in Sizes), 'file')}"
|
|
if Sizes and all(Sizes) else "measuring…")
|
|
return f"{Plural(len(Members), 'Record')} · {Total}"
|
|
|
|
def _DetailView(self, Item: Optional[LibraryEntry]) -> ft.Control:
|
|
if Item is None:
|
|
self.SizeLabels, self.TotalLabel = {}, None
|
|
return EmptyState("No Library selected.", "Make one with the New button.", ft.Icons.COLLECTIONS_BOOKMARK_OUTLINED)
|
|
NameField = TextInput(self.App, Label="Name", Value=Item.Name, Width=320)
|
|
EnabledSwitch = ft.Switch(label="Enabled (shown on the Browse page)", value=Item.Enabled)
|
|
Boxes: dict[Path, ft.Checkbox] = {}
|
|
Rows: list[ft.Control] = []
|
|
Known = {Member.Directory for Member in self.Records}
|
|
self.SizeLabels = {}
|
|
for Member in self.Records:
|
|
Boxes[Member.Directory] = ft.Checkbox(value=Member.Directory in Item.Members)
|
|
self.SizeLabels[Member.Directory] = Note(self._SizeText(Member.Directory), Size=11)
|
|
Rows.append(ft.Row([Boxes[Member.Directory], ft.Text(Member.Name, size=13, expand=True),
|
|
self.SizeLabels[Member.Directory]]))
|
|
Missing = [Member for Member in Item.Members if Member not in Known]
|
|
for Member in Missing:
|
|
Rows.append(ft.Row([ft.Icon(ft.Icons.WARNING_AMBER, size=16, color=Palette.Yellow),
|
|
ft.Text(f"{Member} isn't a Record any more (dropped on save)", size=12, color=Palette.Yellow)]))
|
|
self.TotalLabel = Note(self._TotalText(Item))
|
|
|
|
def Save(e) -> None:
|
|
Chosen = [Directory for Directory, Box in Boxes.items() if Box.value]
|
|
try:
|
|
SaveLibrary(Item.Index, NameField.value or "", Chosen, EnabledSwitch.value)
|
|
except DataError as Error:
|
|
self.App.Notify(str(Error), "error")
|
|
return
|
|
self.Selected = (NameField.value or "").strip() or "Unnamed library"
|
|
self.App.Notify("Library saved.", "success")
|
|
self.Refresh()
|
|
|
|
def Delete(e) -> None:
|
|
def Yes() -> None:
|
|
try:
|
|
DeleteLibrary(Item.Index)
|
|
except DataError as Error:
|
|
self.App.Notify(str(Error), "error")
|
|
return
|
|
self.Selected = None
|
|
self.App.Notify(f"Deleted {Item.Name}.", "success")
|
|
self.Refresh()
|
|
|
|
self.App.Confirm("Delete Library", f"Delete the Library {Item.Name}? Its Records aren't touched.", Yes, "Delete")
|
|
|
|
return ft.Column([
|
|
Heading(Item.Name),
|
|
self.TotalLabel,
|
|
ft.Row([
|
|
ChipButton("Browse", lambda e: self.App.Navigate("Browse", Library=Item.Name), ft.Icons.FOLDER_OPEN,
|
|
Disabled=not Item.Enabled),
|
|
ChipButton("Save", Save, ft.Icons.SAVE_OUTLINED),
|
|
ChipButton("Delete", Delete, ft.Icons.DELETE_OUTLINE, Accent=Palette.Red),
|
|
], spacing=8),
|
|
ft.Divider(height=1, color=Palette.Surface1),
|
|
NameField, EnabledSwitch,
|
|
SectionTitle("Records in it"),
|
|
Panel(ft.Column(Rows, spacing=0), padding=8) if Rows else Note("There are no Records yet."),
|
|
], spacing=12, scroll=ft.ScrollMode.AUTO, expand=True)
|
|
|
|
def NewDialog(self) -> None:
|
|
NameField = TextInput(self.App, Label="Name", Autofocus=True)
|
|
Boxes = {Record.Directory: ft.Checkbox(label=Record.Name, value=False) for Record in self.Records}
|
|
|
|
def Create(e=None) -> None:
|
|
Chosen = [Directory for Directory, Box in Boxes.items() if Box.value]
|
|
if not Chosen:
|
|
self.App.Notify("Tick at least one Record.", "warning")
|
|
return
|
|
try:
|
|
CreateLibrary(NameField.value or "", Chosen)
|
|
except DataError as Error:
|
|
self.App.Notify(str(Error), "error")
|
|
return
|
|
self.App.Close()
|
|
self.Selected = (NameField.value or "").strip() or "New library"
|
|
self.App.Notify(f"Created {self.Selected}.", "success")
|
|
self.Refresh()
|
|
|
|
self.App.ShowDialog("New Library", ft.Column([
|
|
NameField, Note("Records in it:"),
|
|
ft.Column(list(Boxes.values()), spacing=0, scroll=ft.ScrollMode.AUTO, height=min(360, 40 * max(1, len(Boxes)))),
|
|
], tight=True, spacing=8), [
|
|
ChipButton("Cancel", lambda e: self.App.Close()),
|
|
ChipButton("Create", Create, ft.Icons.ADD),
|
|
], Width=460)
|