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