Files
SaraTag/UpdatePage.py
T

154 lines
7.5 KiB
Python

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