"""Install, Update and Remove dialogs. Each only gathers choices and shows what's about to happen; the actual work runs back in Main.py.""" from typing import Callable import flet as ft from Core import ChipButtonStyle, Home, Palette from Installer import ConfigPlan, DefaultAnswers, DefaultOptions, RemovalItem, RemovalPlan, Status from Manifests import Program _MaxListedItems = 12 def _Short(PathText: str) -> str: return PathText.replace(str(Home), "~", 1) if PathText.startswith(str(Home)) else PathText def _Heading(Text: str) -> ft.Text: return ft.Text(Text, size=14, color=Palette.Mauve) def _ItemList(Items: list[RemovalItem]) -> ft.Column: Lines = [] for Item in Items[:_MaxListedItems]: Spans = [ft.TextSpan(_Short(Item.Path), ft.TextStyle(color=Palette.Text))] if Item.Note: Spans.append(ft.TextSpan(f" {Item.Note}", ft.TextStyle(color=Palette.Overlay0))) Lines.append(ft.Text(spans=Spans, size=12, selectable=True)) if len(Items) > _MaxListedItems: Lines.append(ft.Text(f"+ {len(Items) - _MaxListedItems} more", size=12, color=Palette.Overlay0)) if not Items: Lines.append(ft.Text("(nothing)", size=12, color=Palette.Overlay0)) return ft.Column(Lines, spacing=2) def _Dialog(Page: ft.Page, Title: str, Body: list[ft.Control], ConfirmText: str, OnConfirm: Callable[[], None], Danger: bool = False) -> ft.AlertDialog: def Confirm(e) -> None: Page.pop_dialog() OnConfirm() return ft.AlertDialog( title=Title, content=ft.Container( content=ft.Column(Body, spacing=10, scroll=ft.ScrollMode.AUTO, tight=True), width=640, ), actions=[ ft.OutlinedButton(content="Cancel", style=ChipButtonStyle(), on_click=lambda e: Page.pop_dialog()), ft.OutlinedButton( content=ConfirmText, style=ChipButtonStyle(Palette.Red if Danger else ""), on_click=Confirm, ), ], ) def ShowInstallDialog(Page: ft.Page, Program: Program, OnConfirm: Callable[[dict[str, bool], dict[str, str]], None]) -> None: # Start from the user's defaults (Settings → this program's tab), which # fall back to the manifest's. Answers = DefaultAnswers(Program) Ticked = DefaultOptions(Program) Fields = {Question.Id: ft.TextField(label=Question.Prompt, value=Answers[Question.Id]) for Question in Program.Questions} Boxes = {Option.Id: ft.Checkbox(label=Option.Label, value=Ticked[Option.Id]) for Option in Program.Options} Body: list[ft.Control] = [ft.Text(Program.Description, size=13, color=Palette.Overlay0)] Body += list(Fields.values()) if Boxes: Body.append(_Heading("Options")) Body += list(Boxes.values()) Page.show_dialog(_Dialog( Page, f"Install {Program.Name}", Body, "Install", lambda: OnConfirm( {Id: bool(Box.value) for Id, Box in Boxes.items()}, {Id: (Field.value or "").strip() for Id, Field in Fields.items()}, ), )) def ShowOptionsDialog(Page: ft.Page, Program: Program, Current: Status, OnConfirm: Callable[[dict[str, bool]], None]) -> None: """Change an installed program's options. Turning one off takes out what it placed (and puts back any file it had replaced); turning one on places it.""" Recorded = Current.Receipt.Options if Current.Receipt else {} Ticked = DefaultOptions(Program) Boxes = { Option.Id: ft.Checkbox(label=Option.Label, value=bool(Recorded.get(Option.Id, Ticked[Option.Id]))) for Option in Program.Options } Body: list[ft.Control] = list(Boxes.values()) Body.append(ft.Text( "Only what these options control changes; the program stays at the version it's on.", size=12, color=Palette.Overlay0, )) Page.show_dialog(_Dialog( Page, f"{Program.Name} options", Body, "Apply", lambda: OnConfirm({Id: bool(Box.value) for Id, Box in Boxes.items()}), )) def ShowUpdateDialog( Page: ft.Page, Program: Program, Current: Status, NewVersion: str, NewCommit: str, Plans: list[ConfigPlan], OnConfirm: Callable[[], None], ) -> None: Old = Current.Receipt OldLabel = f"{Old.Version} ({Old.Commit[:7]})".strip() if Old else "?" NewLabel = f"{NewVersion} ({NewCommit[:7]})".strip() Body: list[ft.Control] = [ft.Text(f"{OldLabel} -> {NewLabel}", size=14, color=Palette.Text)] if Old is not None and Old.Commit == NewCommit: Body.append(ft.Text( "Same commit: this reapplies the install (e.g. a newer SaraSuite changed how it's set up).", size=12, color=Palette.Overlay0, )) Body.append(ft.Text( "Program files, desktop entries and services are replaced. Configs are merged, so your edits stay.", size=12, color=Palette.Overlay0, )) if Plans: Body.append(_Heading("Configs")) for Plan in Plans: Color = Palette.Peach if Plan.Result.Action == "KeepAndNew" else Palette.Text Body.append(ft.Text(spans=[ ft.TextSpan(_Short(Plan.Target), ft.TextStyle(color=Color)), ft.TextSpan(f" {Plan.Result.Describe()}", ft.TextStyle(color=Palette.Overlay0)), ], size=12)) for Change in Plan.Result.Changes: Body.append(ft.Text(f" {Change}", size=12, color=Palette.Overlay0)) Page.show_dialog(_Dialog(Page, f"Update {Program.Name}", Body, "Update", OnConfirm)) def ShowRemoveDialog(Page: ft.Page, Program: Program, Plan: RemovalPlan, Installed: bool, OnConfirm: Callable[[bool, bool], None]) -> None: ConfigItems = Plan.Group("Config") RuntimeItems = Plan.Group("Runtime") ConfigList = _ItemList(ConfigItems) RuntimeList = _ItemList(RuntimeItems) ConfigList.opacity = 0.5 ConfigBox = ft.Checkbox(label="Remove configs", value=False) RuntimeBox = ft.Checkbox(label="Remove files it made while running", value=True) def Dim(e) -> None: ConfigList.opacity = 1 if ConfigBox.value else 0.5 RuntimeList.opacity = 1 if RuntimeBox.value else 0.5 ConfigList.update() RuntimeList.update() ConfigBox.on_change = Dim RuntimeBox.on_change = Dim Body: list[ft.Control] = [] Always = Plan.Group("Program") + Plan.Group("Integration") if Always: Body += [_Heading("Always removed"), _ItemList(Always)] Body += [ConfigBox, ConfigList, RuntimeBox, RuntimeList] Body.append(ft.Text( "Your own files are never removed: folders that might hold them are only removed when empty.", size=12, color=Palette.Overlay0, )) Page.show_dialog(_Dialog( Page, f"{'Remove' if Installed else 'Clean up'} {Program.Name}", Body, "Remove" if Installed else "Clean up", lambda: OnConfirm(bool(ConfigBox.value), bool(RuntimeBox.value)), Danger=True, ))