mirror of
https://git.srhtnin.garden/sarah/SaraTag.git
synced 2026-10-13 18:28:16 +02:00
154 lines
7.5 KiB
Python
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,
|
|
)
|