Files
SaraSuite/ProgramCard.py
T
sarahandClaude Opus 5.5 6e4c571e38 Add SaraSort, listed under SaraSortd
Manifests can name a Parent program; it's listed (indented) right under it,
in the window and in `list`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 22:57:49 +02:00

134 lines
5.8 KiB
Python

"""One card per program: name, status, versions, and its action buttons."""
from typing import Callable, Optional
import flet as ft
from Core import ChipButtonStyle, Palette
from Installer import RemovalPlan, Status
from Manifests import Program
ChildIndent = 40
class ProgramCard(ft.Container):
def __init__(
self,
Program: Program,
OnInstall: Callable[["ProgramCard"], None],
OnUpdate: Callable[["ProgramCard"], None],
OnRemove: Callable[["ProgramCard"], None],
OnOptions: Callable[["ProgramCard"], None],
) -> None:
self.Program = Program
self.CurrentStatus: Optional[Status] = None
self.StatusText = ft.Text("Checking...", size=12, color=Palette.Overlay0)
self.StatusChip = ft.Container(
content=self.StatusText,
bgcolor=Palette.Surface0,
border=ft.Border.all(1, Palette.Overlay0),
border_radius=4,
padding=ft.Padding.symmetric(horizontal=8, vertical=2),
)
self.DetailText = ft.Text("", size=12, color=Palette.Overlay0, selectable=True)
self.Spinner = ft.ProgressRing(width=16, height=16, stroke_width=2, visible=False)
self.InstallButton = ft.OutlinedButton(
content="Install", style=ChipButtonStyle(), visible=False, on_click=lambda e: OnInstall(self),
)
self.UpdateButton = ft.OutlinedButton(
content="Update", style=ChipButtonStyle(), visible=False, on_click=lambda e: OnUpdate(self),
)
self.OptionsButton = ft.OutlinedButton(
content="Options", style=ChipButtonStyle(), visible=False, on_click=lambda e: OnOptions(self),
)
self.RemoveButton = ft.OutlinedButton(
content="Remove", style=ChipButtonStyle(Palette.Red), visible=False, on_click=lambda e: OnRemove(self),
)
super().__init__(
content=ft.Column(
spacing=6,
controls=[
ft.Row(
vertical_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
ft.Text(Program.Name, size=20, color=Palette.Mauve),
self.StatusChip,
ft.Container(expand=True),
self.Spinner,
self.InstallButton,
self.UpdateButton,
self.OptionsButton,
self.RemoveButton,
],
),
ft.Text(Program.Description, size=13, color=Palette.Text),
self.DetailText,
],
),
bgcolor=Palette.Mantle,
border=ft.Border.all(1, Palette.Surface1),
border_radius=8,
padding=16,
# A program that belongs to another (Parent) sits indented under it.
margin=ft.Margin.only(left=ChildIndent) if Program.Parent else None,
)
def _SetChip(self, Text: str, Color: str) -> None:
self.StatusText.value = Text
self.StatusText.color = Color
self.StatusChip.border = ft.Border.all(1, Color)
def ShowStatus(self, Current: Status, Leftovers: RemovalPlan, Busy: bool) -> None:
self.CurrentStatus = Current
Receipt = Current.Receipt
if Current.Broken:
self._SetChip("Incomplete", Palette.Peach)
elif Current.UpdateAvailable:
self._SetChip("Update available", Palette.Yellow)
elif Current.Installed:
self._SetChip("Installed", Palette.Green)
elif Current.Error:
self._SetChip("Unreachable", Palette.Red)
else:
self._SetChip("Not installed", Palette.Overlay0)
Details = []
if Receipt is not None:
Installed = f"Installed {Receipt.Version} ({Receipt.Commit[:7]})".replace(" ", " ")
Details.append(f"{Installed} on {Receipt.UpdatedAt.replace('T', ' ')}")
if Current.LatestCommit and (Receipt is None or Current.LatestCommit != Receipt.Commit):
Details.append(f"Latest commit {Current.LatestCommit[:7]}")
Details.append(f"From {self.Program.GitUrl}")
if Current.Error:
Details.append(Current.Error)
if Receipt is None and Leftovers.Items:
Details.append(f"{len(Leftovers.Items)} file(s)/folder(s) it made are still around")
self.DetailText.value = " · ".join(Details)
self.InstallButton.visible = not Current.Installed
self.InstallButton.disabled = Busy or not Current.LatestCommit
# Only there when there's something to update (or repair).
self.UpdateButton.visible = Current.UpdateAvailable
self.UpdateButton.disabled = Busy or not Current.LatestCommit
self.UpdateButton.content = "Repair" if Current.Broken else "Update"
# Change options (e.g. the file picker) after installing. Needs the
# git host, since it reapplies the installed commit.
self.OptionsButton.visible = Current.Installed and bool(self.Program.Options)
self.OptionsButton.disabled = Busy or not Current.LatestCommit
# Not installed but configs/runtime files were kept: still removable.
self.RemoveButton.visible = Current.Installed or bool(Leftovers.Items)
self.RemoveButton.content = "Remove" if Current.Installed else "Clean up"
self.RemoveButton.disabled = Busy
self.Spinner.visible = False
def SetBusy(self, Busy: bool, Working: bool = False) -> None:
"""Busy greys out the buttons (only one operation runs at a time);
Working also shows this card's spinner."""
for Button in (self.InstallButton, self.UpdateButton, self.OptionsButton, self.RemoveButton):
Button.disabled = Busy
self.Spinner.visible = Working