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