"""Update: bring Books in line with their Lexicons, for every Record or one. "Preview only" runs the same thing without writing, to see what would change.""" from pathlib import Path import flet as ft from Core import ChipButton, DisplayPath, Heading, Note, Palette from History import RecordUpdaterRun, UpdaterState from Library import DataError from Records import ListRecords, Record from Updater import DestructiveModes, Modes, UpdateRecord, UpdateResult from Widgets import App, Panel, Plural, SectionTitle AllRecords = "All" class UpdatePage(ft.Container): def __init__(self, TheApp: App): super().__init__(expand=True, padding=16) self.App = TheApp self.Records: list[Record] = [] self.Running = False State = UpdaterState() self.ModePicker = ft.RadioGroup( ft.Column([ ft.Column([ft.Radio(value=Name, label=Label), ft.Container(Note(Help), padding=ft.Padding.only(left=48))], spacing=0) for Name, (Label, Help) in Modes.items() ], spacing=6), value=State.get("LastMode") if State.get("LastMode") in Modes else "Expand", ) self.DryRun = ft.Switch(label="Preview only (change nothing, just show what would change)", value=bool(State.get("LastDryRun", False))) self.ScopePicker = ft.Dropdown(label="Which Records", width=360, dense=True) self.LastScope = State.get("LastScope", "") self.RunButton = ChipButton("Run", lambda e: self.Run(), ft.Icons.PLAY_ARROW) self.Progress = ft.Row([ft.ProgressRing(width=16, height=16), Note("")], visible=False) self.Results = ft.Column(spacing=12) Options = ft.Column([ Heading("Update Books"), Note("Only directories with a Lexicon are touched. Files never change; only their Books do."), Panel(self.ModePicker), self.DryRun, ft.Row([self.ScopePicker, self.RunButton, self.Progress], spacing=12, vertical_alignment=ft.CrossAxisAlignment.CENTER), ], spacing=12, width=560) self.content = ft.Column([Options, ft.Divider(height=1, color=Palette.Surface1), self.Results], spacing=16, scroll=ft.ScrollMode.AUTO, expand=True) def Refresh(self, Scope: str = "") -> None: self.page.on_keyboard_event = None try: self.Records = [Item for Item in ListRecords()[0] if Item.Enabled] except DataError as Error: self.App.Notify(str(Error), "error") self.Records = [] Options = [ft.DropdownOption(key=AllRecords, text=f"All enabled Records ({len(self.Records)})")] Options += [ft.DropdownOption(key=str(Item.Directory), text=Item.Name) for Item in self.Records] self.ScopePicker.options = Options Wanted = Scope or self.ScopePicker.value or self.LastScope Keys = {Option.key for Option in Options} self.ScopePicker.value = Wanted if Wanted in Keys else AllRecords self.update() def Leave(self) -> None: pass def _Chosen(self) -> list[Record]: if self.ScopePicker.value == AllRecords: return self.Records return [Item for Item in self.Records if str(Item.Directory) == self.ScopePicker.value] def Run(self) -> None: if self.Running: return Mode = self.ModePicker.value or "Expand" Chosen = self._Chosen() if not Chosen: self.App.Notify("There are no enabled Records to update.", "warning") return if Mode in DestructiveModes and not self.DryRun.value: Label = Modes[Mode][0] self.App.Confirm(f"{Label}?", f"{Modes[Mode][1]} This rewrites Books in {Plural(len(Chosen), 'Record')} and " "can't be undone. Tip: run it with Preview only first.", lambda: self._Start(Mode, Chosen), "Run", Accent=Palette.Yellow) return self._Start(Mode, Chosen) def _Start(self, Mode: str, Chosen: list[Record]) -> None: self.Running = True self.RunButton.disabled = True self.Progress.visible = True self.Results.controls = [] self.update() self.page.run_thread(self._Work, Mode, bool(self.DryRun.value), Chosen) def _Work(self, Mode: str, DryRun: bool, Chosen: list[Record]) -> None: Label: ft.Text = self.Progress.controls[1] Results: list[tuple[Record, UpdateResult]] = [] def Progress(Directory: Path) -> None: Label.value = DisplayPath(str(Directory)) Label.update() try: for Item in Chosen: Results.append((Item, UpdateRecord(Item.Directory, Item.LexiconPath, Mode, DryRun, Progress))) Changed = [BookPath for _, Result in Results for BookPath in Result.ChangedBooks] RecordUpdaterRun(Mode, DryRun, AllRecords if len(Chosen) > 1 else str(Chosen[0].Directory), Changed) finally: self.Running = False self.RunButton.disabled = False self.Progress.visible = False self._ShowResults(Mode, DryRun, Results) def _ShowResults(self, Mode: str, DryRun: bool, Results: list[tuple[Record, UpdateResult]]) -> None: Total = UpdateResult(Path("/"), Mode, DryRun) for _, Result in Results: Total.Merge(Result) Verb = "would change" if DryRun else "changed" Headline = f"{Modes[Mode][0]}{' (preview)' if DryRun else ''}: {Plural(len(Total.ChangedBooks), 'Book')} {Verb}." Rows = [ft.Row([ft.Text(Name, size=13, width=200), ft.Text(str(Count), size=13, color=Palette.Red if Count and Name in ("Errors", "Invalid values") else Palette.Text)]) for Name, Count in Total.Summary() if Count or Name in ("Files scanned", "Books created", "Books updated")] Controls: list[ft.Control] = [ SectionTitle(Headline), Panel(ft.Column(Rows, spacing=2), width=560), ] if len(Results) > 1: PerRecord = [ft.Row([ft.Text(Item.Name, size=13, expand=True), Note(f"{len(Result.ChangedBooks)} {Verb}" + (f", {Plural(len(Result.Errors), 'error')}" if Result.Errors else ""))]) for Item, Result in Results] Controls.append(self._Expander("Per Record", PerRecord)) for Title, Items, Color in ( ("Errors", Total.Errors, Palette.Red), ("Invalid values", Total.InvalidValues, Palette.Yellow), ("Orphaned Books (their file is gone)", [str(Item) for Item in Total.OrphanedBooks], Palette.Peach), (f"Books that {Verb}", [str(Item) for Item in Total.ChangedBooks], Palette.Text), ): if Items: Controls.append(self._Expander(f"{Title} ({len(Items)})", [ ft.Text(DisplayPath(Text), size=12, color=Color, selectable=True) for Text in Items[:500] ] + ([Note(f"…and {len(Items) - 500} more")] if len(Items) > 500 else []), Open=Color == Palette.Red)) self.Results.controls = Controls self.update() self.App.Notify(Headline, "error" if Total.Errors else "success") @staticmethod def _Expander(Title: str, Controls: list[ft.Control], Open: bool = False) -> ft.Control: return ft.ExpansionTile( title=ft.Text(Title, size=14), expanded=Open, controls=[ft.Container(ft.Column(Controls, spacing=2), padding=12)], collapsed_icon_color=Palette.Mauve, icon_color=Palette.Mauve, )