mirror of
https://git.srhtnin.garden/sarah/SaraSuite.git
synced 2026-10-14 02:28:11 +02:00
Program icons at 128px, no frame
This commit is contained in:
1 parent
e8c524531e
commit
d9da8438c2
8 files changed
+128
-71
No files matched your search
@@ -237,10 +237,14 @@ def BuildTheme() -> ft.Theme:
|
||||
|
||||
def ChipButtonStyle(Accent: str = "") -> ft.ButtonStyle:
|
||||
"""SaraView's button look: SurfaceAlt fill, 1px accent border, small radius.
|
||||
Accent defaults to Mauve; Remove buttons pass Palette.Red."""
|
||||
Accent defaults to Mauve; Remove buttons pass Palette.Red. Disabled buttons
|
||||
are greyed out (muted text, plain border, no fill) so it's obvious they
|
||||
can't be pressed."""
|
||||
Disabled = ft.ControlState.DISABLED
|
||||
Default = ft.ControlState.DEFAULT
|
||||
return ft.ButtonStyle(
|
||||
color=Palette.Text,
|
||||
bgcolor=Palette.Surface0,
|
||||
side=ft.BorderSide(1, Accent or Palette.Mauve),
|
||||
color={Disabled: Palette.Overlay0, Default: Palette.Text},
|
||||
bgcolor={Disabled: Palette.Mantle, Default: Palette.Surface0},
|
||||
side={Disabled: ft.BorderSide(1, Palette.Surface1), Default: ft.BorderSide(1, Accent or Palette.Mauve)},
|
||||
shape=ft.RoundedRectangleBorder(radius=4),
|
||||
)
|
||||
+3
-1
@@ -152,7 +152,9 @@ def ManifestHash(Program: Program) -> str:
|
||||
AppImage), so that shows up as an update even with no new commit."""
|
||||
Hasher = hashlib.sha256()
|
||||
for ManifestFile in sorted(Program.Folder.rglob("*")):
|
||||
if ManifestFile.is_file() and "__pycache__" not in ManifestFile.parts:
|
||||
# Icon.png is only the card's picture before installing; changing it
|
||||
# shouldn't make installed programs show an update.
|
||||
if ManifestFile.is_file() and "__pycache__" not in ManifestFile.parts and ManifestFile.name != "Icon.png":
|
||||
Hasher.update(str(ManifestFile.relative_to(Program.Folder)).encode())
|
||||
Hasher.update(ManifestFile.read_bytes())
|
||||
return Hasher.hexdigest()
|
||||
|
||||
@@ -129,6 +129,22 @@ class Program:
|
||||
def AppDir(self) -> Path:
|
||||
return self.ProgramDir / "App"
|
||||
|
||||
@property
|
||||
def IconPath(self) -> Optional[Path]:
|
||||
"""The installed copy's own icon if it's there (always current; the
|
||||
Icon= line of the program's desktop entry template says where), otherwise
|
||||
the copy bundled next to the manifest (Programs/<Name>/Icon.png), so the
|
||||
card has an icon before installing too."""
|
||||
for Template in self.Templates:
|
||||
if Template.Target.endswith(".desktop"):
|
||||
for Line in (self.Folder / "Templates" / Template.Source).read_text().splitlines():
|
||||
if Line.startswith("Icon="):
|
||||
Installed = Path(Render(Line.removeprefix("Icon="), self.Context()))
|
||||
if Installed.is_file():
|
||||
return Installed
|
||||
Bundled = self.Folder / "Icon.png"
|
||||
return Bundled if Bundled.is_file() else None
|
||||
|
||||
@property
|
||||
def ConfigDir(self) -> Path:
|
||||
return SharedConfigDir / self.Name
|
||||
|
||||
+101
-66
@@ -1,4 +1,7 @@
|
||||
"""One card per program: name, status, versions, and its action buttons."""
|
||||
"""One card per program: its icon on the left; name, description, install
|
||||
info and install status in the middle; and three fixed buttons stacked on
|
||||
the right (Install/Update, Options, Remove). A button that doesn't apply
|
||||
right now stays in place, greyed out, rather than disappearing."""
|
||||
|
||||
from typing import Callable, Optional
|
||||
|
||||
@@ -9,6 +12,11 @@ from Installer import RemovalPlan, Status
|
||||
from Manifests import Program
|
||||
|
||||
ChildIndent = 40
|
||||
# The icons are 32x32 pixel art (stored 10x, as 320x320). 128 keeps every art
|
||||
# pixel a whole number of screen pixels: 4 at 100% scale and 3 at 75%, the
|
||||
# scale of Sarah's screens. Sizes that aren't (like 80) make pixels uneven.
|
||||
IconSize = 128
|
||||
ButtonWidth = 120
|
||||
|
||||
|
||||
class ProgramCard(ft.Container):
|
||||
@@ -22,50 +30,56 @@ class ProgramCard(ft.Container):
|
||||
) -> None:
|
||||
self.Program = Program
|
||||
self.CurrentStatus: Optional[Status] = None
|
||||
self.OnInstall = OnInstall
|
||||
self.OnUpdate = OnUpdate
|
||||
# Which buttons apply right now (set by ShowStatus); while busy they're
|
||||
# all disabled, and afterwards only these come back.
|
||||
self.Allowed = {"Primary": False, "Options": False, "Remove": False}
|
||||
|
||||
# No frame of its own: the icons are drawn with their own border.
|
||||
self.IconBox = ft.Container(
|
||||
width=IconSize, height=IconSize, alignment=ft.Alignment.CENTER, content=self._IconContent(),
|
||||
)
|
||||
|
||||
self.InfoText = ft.Text("", size=12, color=Palette.Overlay0, selectable=True)
|
||||
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.NoteText = ft.Text("", size=12, color=Palette.Overlay0, selectable=True)
|
||||
self.Spinner = ft.ProgressRing(width=14, height=14, 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),
|
||||
# Install, Update or Repair, depending on the state.
|
||||
self.PrimaryButton = ft.OutlinedButton(
|
||||
content="Install", style=ChipButtonStyle(), width=ButtonWidth, disabled=True,
|
||||
on_click=lambda e: self._OnPrimary(),
|
||||
)
|
||||
self.OptionsButton = ft.OutlinedButton(
|
||||
content="Options", style=ChipButtonStyle(), visible=False, on_click=lambda e: OnOptions(self),
|
||||
content="Options", style=ChipButtonStyle(), width=ButtonWidth, disabled=True,
|
||||
on_click=lambda e: OnOptions(self),
|
||||
)
|
||||
self.RemoveButton = ft.OutlinedButton(
|
||||
content="Remove", style=ChipButtonStyle(Palette.Red), visible=False, on_click=lambda e: OnRemove(self),
|
||||
content="Remove", style=ChipButtonStyle(Palette.Red), width=ButtonWidth, disabled=True,
|
||||
on_click=lambda e: OnRemove(self),
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
content=ft.Column(
|
||||
spacing=6,
|
||||
content=ft.Row(
|
||||
spacing=16,
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
controls=[
|
||||
ft.Row(
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
self.IconBox,
|
||||
ft.Column(
|
||||
expand=True,
|
||||
spacing=6,
|
||||
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.Name, size=22, color=Palette.Mauve),
|
||||
ft.Text(Program.Description, size=14, color=Palette.Text),
|
||||
self.InfoText,
|
||||
ft.Row([self.Spinner, self.StatusText, self.NoteText], spacing=8, wrap=True),
|
||||
],
|
||||
),
|
||||
ft.Text(Program.Description, size=13, color=Palette.Text),
|
||||
self.DetailText,
|
||||
ft.Column(
|
||||
spacing=8,
|
||||
controls=[self.PrimaryButton, self.OptionsButton, self.RemoveButton],
|
||||
),
|
||||
],
|
||||
),
|
||||
bgcolor=Palette.Mantle,
|
||||
@@ -76,58 +90,79 @@ class ProgramCard(ft.Container):
|
||||
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 _IconContent(self) -> ft.Control:
|
||||
Path = self.Program.IconPath
|
||||
if Path is None:
|
||||
# No icon (e.g. SaraSortd, a background service).
|
||||
return ft.Icon(ft.Icons.APPS, size=IconSize // 2, color=Palette.Overlay0)
|
||||
return ft.Image(
|
||||
src=str(Path),
|
||||
width=IconSize,
|
||||
height=IconSize,
|
||||
fit=ft.BoxFit.CONTAIN,
|
||||
filter_quality=ft.FilterQuality.NONE, # pixel art stays crisp when scaled
|
||||
)
|
||||
|
||||
def _OnPrimary(self) -> None:
|
||||
if self.CurrentStatus is not None and self.CurrentStatus.Installed:
|
||||
self.OnUpdate(self)
|
||||
else:
|
||||
self.OnInstall(self)
|
||||
|
||||
def ShowStatus(self, Current: Status, Leftovers: RemovalPlan, Busy: bool) -> None:
|
||||
self.CurrentStatus = Current
|
||||
Receipt = Current.Receipt
|
||||
|
||||
if Current.Broken:
|
||||
self._SetChip("Incomplete", Palette.Peach)
|
||||
Text, Color = "Incomplete install", Palette.Peach
|
||||
elif Current.UpdateAvailable:
|
||||
self._SetChip("Update available", Palette.Yellow)
|
||||
Text, Color = "Update available", Palette.Yellow
|
||||
elif Current.Installed:
|
||||
self._SetChip("Installed", Palette.Green)
|
||||
Text, Color = "Installed, up to date", Palette.Green
|
||||
elif Current.Error:
|
||||
self._SetChip("Unreachable", Palette.Red)
|
||||
Text, Color = "Unreachable", Palette.Red
|
||||
else:
|
||||
self._SetChip("Not installed", Palette.Overlay0)
|
||||
Text, Color = "Not installed", Palette.Overlay0
|
||||
self.StatusText.value = Text
|
||||
self.StatusText.color = Color
|
||||
|
||||
Details = []
|
||||
Info = []
|
||||
if Receipt is not None:
|
||||
Installed = f"Installed {Receipt.Version} ({Receipt.Commit[:7]})".replace(" ", " ")
|
||||
Details.append(f"{Installed} on {Receipt.UpdatedAt.replace('T', ' ')}")
|
||||
Info.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)
|
||||
Info.append(f"latest commit {Current.LatestCommit[:7]}")
|
||||
Info.append(f"from {self.Program.GitUrl}")
|
||||
self.InfoText.value = " · ".join(Info)
|
||||
|
||||
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
|
||||
Notes = []
|
||||
if Current.Error:
|
||||
Notes.append(Current.Error)
|
||||
if Receipt is None and Leftovers.Items:
|
||||
Notes.append(f"{len(Leftovers.Items)} file(s)/folder(s) it made are still around")
|
||||
self.NoteText.value = " · ".join(Notes)
|
||||
|
||||
# Install when not installed; Update (Repair if broken) when it is,
|
||||
# greyed out while there's nothing to update.
|
||||
if not Current.Installed:
|
||||
self.PrimaryButton.content = "Install"
|
||||
self.Allowed["Primary"] = bool(Current.LatestCommit)
|
||||
else:
|
||||
self.PrimaryButton.content = "Repair" if Current.Broken else "Update"
|
||||
self.Allowed["Primary"] = Current.UpdateAvailable and bool(Current.LatestCommit)
|
||||
# Options reapplies the installed commit, so it needs the git host too.
|
||||
self.Allowed["Options"] = Current.Installed and bool(self.Program.Options) and bool(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
|
||||
self.RemoveButton.content = "Remove" if Current.Installed or not Leftovers.Items else "Clean up"
|
||||
self.Allowed["Remove"] = Current.Installed or bool(Leftovers.Items)
|
||||
|
||||
self.IconBox.content = self._IconContent() # installing/removing switches the icon's source
|
||||
self.SetBusy(Busy)
|
||||
|
||||
def SetBusy(self, Busy: bool, Working: bool = False) -> None:
|
||||
"""Busy greys out the buttons (only one operation runs at a time);
|
||||
"""Busy greys out every button (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.PrimaryButton.disabled = Busy or not self.Allowed["Primary"]
|
||||
self.OptionsButton.disabled = Busy or not self.Allowed["Options"]
|
||||
self.RemoveButton.disabled = Busy or not self.Allowed["Remove"]
|
||||
self.Spinner.visible = Working
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 991 B |
Reference in new issue
Block a user