Files
SaraTag/LibrariesPage.py
T

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)