Files
SaraSuite/Dialogs.py
T

168 lines
6.8 KiB
Python

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