mirror of
https://git.srhtnin.garden/sarah/SaraSort.git
synced 2026-10-13 18:28:18 +02:00
Look like SaraFem and SaraSuite: no custom title bar
The window keeps its own title bar (or none, per HideWindowTitleBar), with SaraSuite's header (name, Add to app menu for AppImages, gear). Settings is now a dialog laid out like SaraFem's, with export/import on the SaraSuite tab, and notifications use SaraFem's tinted toast style. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f7ed23edf9
commit
e90f59bc16
6 files changed
+338
-219
No files matched your search
@@ -277,6 +277,23 @@ def BuildTheme() -> ft.Theme:
|
||||
)
|
||||
|
||||
|
||||
def BlendHex(BaseHex: str, AccentHex: str, Ratio: float) -> str:
|
||||
"""Solid colour mixing Base and Accent (0 = pure Base, 1 = pure Accent),
|
||||
for tinted-but-still-dark backgrounds. Same as SaraFem's."""
|
||||
BaseRGB = tuple(int(BaseHex.lstrip("#")[i : i + 2], 16) for i in (0, 2, 4))
|
||||
AccentRGB = tuple(int(AccentHex.lstrip("#")[i : i + 2], 16) for i in (0, 2, 4))
|
||||
Mixed = (round(B + (A - B) * Ratio) for B, A in zip(BaseRGB, AccentRGB))
|
||||
return "#" + "".join(f"{C:02x}" for C in Mixed)
|
||||
|
||||
|
||||
def NotificationStyle(Kind: str) -> tuple[str, str]:
|
||||
"""(Background, AccentColor) for a toast, from the theme's own colours like
|
||||
SaraFem's: "success" green, "error" red, plus "warning" yellow and "info"
|
||||
mauve for SaraSort's restart-needed and plain notes."""
|
||||
Accent = {"success": Palette.Green, "error": Palette.Red, "warning": Palette.Yellow}.get(Kind, Palette.Mauve)
|
||||
return BlendHex(Palette.Mantle, Accent, 0.12), Accent
|
||||
|
||||
|
||||
def ChipButtonStyle(Accent: str = "") -> ft.ButtonStyle:
|
||||
"""SaraView's button look: SurfaceAlt fill, 1px accent border, small radius."""
|
||||
return ft.ButtonStyle(
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
"""SaraSort: a GUI for configuring SaraSortd. It doesn't sort anything itself;
|
||||
it reads and edits SaraSortd's configs and controls its systemd service."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import flet as ft
|
||||
|
||||
from ConfigPage import ConfigPage
|
||||
from Core import (
|
||||
AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, EffectiveSettings, LoadConfig, Palette,
|
||||
AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, ChipButtonStyle, DataHome, EffectiveSettings,
|
||||
LoadConfig, Palette,
|
||||
)
|
||||
from FlowPage import FlowPage
|
||||
from ServicePage import ServicePage
|
||||
from SettingsPage import SettingsPage
|
||||
from Settings import ShowSettingsDialog
|
||||
from TemplatesPage import TemplatesPage
|
||||
from ToolsPage import ToolsPage
|
||||
from Widgets import App
|
||||
@@ -20,10 +24,31 @@ Destinations = [
|
||||
("Configs", ft.Icons.TUNE_OUTLINED, ft.Icons.TUNE),
|
||||
("Templates", ft.Icons.DASHBOARD_CUSTOMIZE_OUTLINED, ft.Icons.DASHBOARD_CUSTOMIZE),
|
||||
("Tools", ft.Icons.BUILD_OUTLINED, ft.Icons.BUILD),
|
||||
("Settings", ft.Icons.SETTINGS_OUTLINED, ft.Icons.SETTINGS),
|
||||
|
||||
]
|
||||
Names = [Name for Name, _, _ in Destinations]
|
||||
|
||||
# When running as an AppImage, SaraSort can put itself in the app menu (like SaraSuite).
|
||||
_AppImage = os.environ.get("APPIMAGE", "")
|
||||
_MenuEntry = DataHome / "applications" / "SaraSort.desktop"
|
||||
_MenuIcon = DataHome / "SaraSort" / "SaraSort.png"
|
||||
|
||||
|
||||
def _SetMenuEntry(Enabled: bool) -> None:
|
||||
if not Enabled:
|
||||
_MenuEntry.unlink(missing_ok=True)
|
||||
_MenuIcon.unlink(missing_ok=True)
|
||||
return
|
||||
_MenuIcon.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(AssetsDir / "Icon.png", _MenuIcon)
|
||||
_MenuEntry.parent.mkdir(parents=True, exist_ok=True)
|
||||
_MenuEntry.write_text(
|
||||
"[Desktop Entry]\nType=Application\nName=SaraSort\n"
|
||||
"Comment=Configure SaraSortd, the file sorting daemon\n"
|
||||
f"Exec={_AppImage}\nIcon={_MenuIcon}\nTerminal=false\n"
|
||||
"Categories=Utility;\nStartupNotify=true\n"
|
||||
)
|
||||
|
||||
|
||||
def Main(Page: ft.Page) -> None:
|
||||
Config = LoadConfig()
|
||||
@@ -70,7 +95,6 @@ def Main(Page: ft.Page) -> None:
|
||||
"Configs": ConfigPage(TheApp),
|
||||
"Templates": TemplatesPage(TheApp),
|
||||
"Tools": ToolsPage(TheApp),
|
||||
"Settings": SettingsPage(TheApp, OnSaved=lambda: None),
|
||||
})
|
||||
|
||||
Rail = ft.NavigationRail(
|
||||
@@ -84,23 +108,37 @@ def Main(Page: ft.Page) -> None:
|
||||
on_change=lambda e: Navigate(Names[e.control.selected_index]),
|
||||
)
|
||||
|
||||
async def CloseWindow(e) -> None:
|
||||
await Page.window.close()
|
||||
def ToggleMenuEntry(e) -> None:
|
||||
_SetMenuEntry(not _MenuEntry.exists())
|
||||
MenuButton.content = "Remove from app menu" if _MenuEntry.exists() else "Add to app menu"
|
||||
MenuButton.update()
|
||||
|
||||
TitleRow = [ft.WindowDragArea(
|
||||
ft.Container(ft.Text(AppName, size=18, color=Palette.Mauve), padding=ft.Padding.only(left=16, top=6, bottom=6)),
|
||||
expand=True,
|
||||
)]
|
||||
if Appearance.HideWindowTitleBar:
|
||||
TitleRow += [
|
||||
ft.IconButton(ft.Icons.MINIMIZE, icon_size=16, tooltip="Minimize",
|
||||
on_click=lambda e: (setattr(Page.window, "minimized", True), Page.update())),
|
||||
ft.IconButton(ft.Icons.CLOSE, icon_size=16, tooltip="Close", on_click=CloseWindow),
|
||||
]
|
||||
MenuButton = ft.OutlinedButton(
|
||||
content="Remove from app menu" if _MenuEntry.exists() else "Add to app menu",
|
||||
style=ChipButtonStyle(),
|
||||
visible=bool(_AppImage),
|
||||
on_click=ToggleMenuEntry,
|
||||
)
|
||||
SettingsButton = ft.IconButton(
|
||||
icon=ft.Icons.SETTINGS_OUTLINED,
|
||||
icon_color=Palette.Mauve,
|
||||
tooltip="Settings",
|
||||
# SaraSortd's paths may have changed, so reload the page afterwards.
|
||||
on_click=lambda e: ShowSettingsDialog(TheApp, lambda: Navigate(State["Current"])),
|
||||
)
|
||||
|
||||
Page.add(ft.Column([
|
||||
ft.Container(ft.Row(TitleRow, spacing=0), bgcolor=Palette.Mantle,
|
||||
border=ft.Border.only(bottom=ft.BorderSide(1, Palette.Surface1))),
|
||||
ft.Container(
|
||||
content=ft.Row([
|
||||
ft.Text(AppName, size=24, color=Palette.Mauve),
|
||||
ft.Container(expand=True),
|
||||
MenuButton,
|
||||
SettingsButton,
|
||||
]),
|
||||
padding=ft.Padding.symmetric(horizontal=16, vertical=8),
|
||||
bgcolor=Palette.Mantle,
|
||||
border=ft.Border.only(bottom=ft.BorderSide(1, Palette.Surface1)),
|
||||
),
|
||||
ft.Row([Rail, ft.VerticalDivider(width=1, color=Palette.Surface1), Body], expand=True, spacing=0,
|
||||
vertical_alignment=ft.CrossAxisAlignment.STRETCH),
|
||||
], expand=True, spacing=0))
|
||||
|
||||
@@ -26,8 +26,11 @@ Needs SaraSortd **7.1** or newer for arrows with their own patterns (per-entry `
|
||||
- **Tools**: warnings about the setup (Unset values, version mismatches, outputs nothing reaches, inputs
|
||||
without outputs, entries shadowed by a catch-all), a filename tester that shows where a file would go
|
||||
and why, and the files in FailedDir with a retry button.
|
||||
- **Settings**: the shared SaraSuite appearance, SaraSort's own settings, where SaraSortd lives, and
|
||||
export/import in the same file format as SaraFem's (`[SaraSuite]` plus `[SaraSort]`).
|
||||
|
||||
The gear button opens **Settings**, laid out like SaraFem's and SaraSuite's: the shared SaraSuite
|
||||
appearance and export/import (same file format as SaraFem's, `[SaraSuite]` plus `[SaraSort]`), SaraSort's
|
||||
own settings, and where SaraSortd lives. Run as an AppImage, the header also has an "Add to app menu"
|
||||
button, like SaraSuite's.
|
||||
|
||||
After a change to `GlobalConf.toml` (new inputs or outputs) SaraSort restarts SaraSortd, since SaraSortd
|
||||
only reads that file when it starts. That can be turned off in Settings.
|
||||
@@ -60,5 +63,6 @@ only reads that file when it starts. That can be turned off in Settings.
|
||||
| `Sortd.py` | SaraSortd's configs: the graph, edits, the filename simulator, warnings |
|
||||
| `Service.py` | systemctl and journalctl |
|
||||
| `Templates.py` | built-in and your own templates |
|
||||
| `Settings.py` | the Settings dialog |
|
||||
| `Widgets.py` | the shared App context and small controls |
|
||||
| `*Page.py` | one per page |
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
"""Settings dialog: the shared SaraSuite settings (with export/import),
|
||||
SaraSort's own, and where SaraSortd lives. Laid out like SaraFem's and
|
||||
SaraSuite's Settings (a category list on the left, the chosen category on the
|
||||
right)."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import flet as ft
|
||||
import tomlkit
|
||||
|
||||
from Core import (
|
||||
AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButtonStyle, ExportSettings, Home,
|
||||
ImportSettings, LoadSharedSettings, Palette, SharedSettings,
|
||||
)
|
||||
from Widgets import App
|
||||
|
||||
DialogWidth = 700
|
||||
DialogHeight = 520
|
||||
CategoryListWidth = 150
|
||||
RestartNote = "Theme, font and title bar changes apply the next time SaraSort starts."
|
||||
StartPages = ["Flow", "Service", "Configs", "Templates", "Tools"]
|
||||
|
||||
|
||||
def _Note(Text: str) -> ft.Text:
|
||||
return ft.Text(Text, color=Palette.Overlay0, size=12)
|
||||
|
||||
|
||||
def _Fonts() -> list[str]:
|
||||
"""Installed font families (fontconfig), with the bundled font first."""
|
||||
try:
|
||||
Result = subprocess.run(
|
||||
["fc-list", ":", "family"], capture_output=True, text=True, timeout=5, env=ChildEnvironment(),
|
||||
)
|
||||
Families = {Line.split(",")[0].strip() for Line in Result.stdout.splitlines() if Line.strip()}
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
Families = set()
|
||||
Families.discard(BundledFontFamily)
|
||||
return [BundledFontFamily] + sorted(Families, key=str.lower)
|
||||
|
||||
|
||||
def _NumberField(Label: str, Value: int) -> ft.TextField:
|
||||
return ft.TextField(label=Label, value=str(Value), keyboard_type=ft.KeyboardType.NUMBER)
|
||||
|
||||
|
||||
def _ReadInt(Field: ft.TextField, Fallback: int, Minimum: int) -> int:
|
||||
try:
|
||||
return max(Minimum, int(Field.value))
|
||||
except (TypeError, ValueError):
|
||||
return Fallback # keep the previous value rather than crash on bad input
|
||||
|
||||
|
||||
def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[], None]) -> None:
|
||||
Page = TheApp.Page
|
||||
Config: AppConfig = TheApp.Config
|
||||
Themes = AvailableThemes()
|
||||
Fonts = _Fonts()
|
||||
|
||||
def ThemePicker(Value: str) -> ft.Dropdown:
|
||||
Names = Themes if Value in Themes else [Value] + Themes
|
||||
return ft.Dropdown(value=Value, label="Theme", options=[ft.DropdownOption(key=Name, text=Name) for Name in Names])
|
||||
|
||||
def FontPicker(Value: str) -> ft.Dropdown:
|
||||
# Type to filter the long list.
|
||||
Names = Fonts if Value in Fonts else [Value] + Fonts
|
||||
return ft.Dropdown(
|
||||
value=Value, label="Font", editable=True, enable_filter=True, menu_height=320,
|
||||
options=[ft.DropdownOption(key=Name, text=Name) for Name in Names],
|
||||
)
|
||||
|
||||
# SaraSuite: ~/.config/SaraSuite/Config.toml, read by every SaraSuite program.
|
||||
Shared = LoadSharedSettings(Config)
|
||||
UseSharedSwitch = ft.Switch(label="SaraSort uses these shared settings", value=Config.UseSharedSettings)
|
||||
SharedTheme = ThemePicker(Shared.Theme)
|
||||
SharedFont = FontPicker(Shared.FontFamily)
|
||||
SharedTitleBar = ft.Switch(label="Hide window title bar", value=Shared.HideWindowTitleBar)
|
||||
SettingsFileField = ft.TextField(value=str(Home / "SaraSort-Settings.toml"), label="Settings file path")
|
||||
|
||||
# SaraSort's own appearance, only used with shared settings off.
|
||||
OwnTheme = ThemePicker(Config.Theme)
|
||||
OwnFont = FontPicker(Config.FontFamily)
|
||||
OwnTitleBar = ft.Switch(label="Hide window title bar", value=Config.HideWindowTitleBar)
|
||||
OwnAppearance = ft.Column(
|
||||
[_Note("SaraSort's own appearance, used when shared settings are off"), OwnTheme, OwnFont, OwnTitleBar],
|
||||
spacing=12,
|
||||
disabled=Config.UseSharedSettings,
|
||||
opacity=0.5 if Config.UseSharedSettings else 1,
|
||||
)
|
||||
|
||||
def OnUseSharedChanged(e) -> None:
|
||||
OwnAppearance.disabled = UseSharedSwitch.value
|
||||
OwnAppearance.opacity = 0.5 if UseSharedSwitch.value else 1
|
||||
OwnAppearance.update()
|
||||
|
||||
UseSharedSwitch.on_change = OnUseSharedChanged
|
||||
|
||||
StartPage = ft.Dropdown(
|
||||
label="Start page", value=Config.StartPage if Config.StartPage in StartPages else "Flow",
|
||||
options=[ft.DropdownOption(key=Name, text=Name) for Name in StartPages],
|
||||
)
|
||||
LabelLength = _NumberField("Arrow label length before it's shortened", Config.LabelLength)
|
||||
|
||||
ConfigDirField = ft.TextField(label="SaraSortd config folder", value=Config.SortdConfigDir)
|
||||
AppDirField = ft.TextField(label="SaraSortd program folder", value=Config.SortdAppDir)
|
||||
UnitField = ft.TextField(label="systemd user unit", value=Config.ServiceName)
|
||||
AutoRestartSwitch = ft.Switch(label="Restart SaraSortd after changes to GlobalConf.toml", value=Config.AutoRestart)
|
||||
RefreshField = _NumberField("Service page refresh (seconds)", Config.RefreshSeconds)
|
||||
LogLinesField = _NumberField("Log lines to show", Config.LogLines)
|
||||
|
||||
def OnExport(e) -> None:
|
||||
PathText = (SettingsFileField.value or "").strip()
|
||||
if not PathText:
|
||||
return
|
||||
try:
|
||||
ExportSettings(Config, Path(PathText).expanduser())
|
||||
except OSError as Error:
|
||||
TheApp.Notify(f"Couldn't export settings: {Error}", "error")
|
||||
return
|
||||
TheApp.Notify("Settings exported.", "success")
|
||||
|
||||
def OnImport(e) -> None:
|
||||
PathText = (SettingsFileField.value or "").strip()
|
||||
if not PathText:
|
||||
return
|
||||
try:
|
||||
ImportSettings(Config, Path(PathText).expanduser())
|
||||
except (OSError, tomlkit.exceptions.TOMLKitError) as Error:
|
||||
TheApp.Notify(f"Couldn't import settings: {Error}", "error")
|
||||
return
|
||||
# The open dialog still shows the pre-import values; closing it
|
||||
# rather than leaving a stale Save that would overwrite the import.
|
||||
Page.pop_dialog()
|
||||
OnSaved()
|
||||
TheApp.Notify("Settings imported. Reopen Settings to see them; restart for Theme/Font/Title bar.", "success")
|
||||
|
||||
Categories: dict[str, ft.Control] = {
|
||||
"SaraSuite": ft.Column(
|
||||
[
|
||||
_Note("Shared by every SaraSuite program (~/.config/SaraSuite/Config.toml). "
|
||||
"Other programs pick changes up the next time they start."),
|
||||
UseSharedSwitch,
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
SharedTheme,
|
||||
SharedFont,
|
||||
SharedTitleBar,
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
_Note("Export/import settings as one file: this tab's shared settings plus everything else "
|
||||
"in Settings. Same format as SaraFem's."),
|
||||
SettingsFileField,
|
||||
ft.Row([
|
||||
ft.OutlinedButton(content="Export", style=ChipButtonStyle(), on_click=OnExport),
|
||||
ft.OutlinedButton(content="Import", style=ChipButtonStyle(), on_click=OnImport),
|
||||
]),
|
||||
],
|
||||
spacing=12,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
),
|
||||
"General": ft.Column(
|
||||
[
|
||||
StartPage,
|
||||
LabelLength,
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
OwnAppearance,
|
||||
],
|
||||
spacing=12,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
),
|
||||
"SaraSortd": ft.Column(
|
||||
[
|
||||
_Note("Where SaraSortd lives. The defaults are where SaraSuite installs it."),
|
||||
ConfigDirField,
|
||||
AppDirField,
|
||||
UnitField,
|
||||
AutoRestartSwitch,
|
||||
_Note("SaraSortd only reads GlobalConf.toml when it starts."),
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
RefreshField,
|
||||
LogLinesField,
|
||||
],
|
||||
spacing=12,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
),
|
||||
}
|
||||
|
||||
DetailArea = ft.Container(Categories["General"], expand=True, padding=ft.Padding.only(left=12))
|
||||
CategoryTiles: dict[str, ft.ListTile] = {}
|
||||
|
||||
def SelectCategory(Name: str) -> None:
|
||||
for CategoryName, Tile in CategoryTiles.items():
|
||||
Tile.selected = CategoryName == Name
|
||||
DetailArea.content = Categories[Name]
|
||||
DetailArea.update()
|
||||
CategoryList.update()
|
||||
|
||||
for Name in Categories:
|
||||
CategoryTiles[Name] = ft.ListTile(
|
||||
title=Name,
|
||||
selected=Name == "General",
|
||||
selected_tile_color=Palette.Surface1,
|
||||
selected_color=Palette.Mauve,
|
||||
on_click=lambda e, n=Name: SelectCategory(n),
|
||||
)
|
||||
CategoryList = ft.Column(list(CategoryTiles.values()), spacing=0, scroll=ft.ScrollMode.AUTO)
|
||||
|
||||
def Save(e) -> None:
|
||||
Config.UseSharedSettings = UseSharedSwitch.value
|
||||
Config.Theme = OwnTheme.value or Config.Theme
|
||||
Config.FontFamily = OwnFont.value or Config.FontFamily
|
||||
Config.HideWindowTitleBar = OwnTitleBar.value
|
||||
Config.StartPage = StartPage.value or Config.StartPage
|
||||
Config.LabelLength = _ReadInt(LabelLength, Config.LabelLength, 6)
|
||||
Config.SortdConfigDir = (ConfigDirField.value or "").strip() or Config.SortdConfigDir
|
||||
Config.SortdAppDir = (AppDirField.value or "").strip() or Config.SortdAppDir
|
||||
Config.ServiceName = (UnitField.value or "").strip() or Config.ServiceName
|
||||
Config.AutoRestart = AutoRestartSwitch.value
|
||||
Config.RefreshSeconds = _ReadInt(RefreshField, Config.RefreshSeconds, 1)
|
||||
Config.LogLines = _ReadInt(LogLinesField, Config.LogLines, 10)
|
||||
Config.Save()
|
||||
SharedSettings(
|
||||
Theme=SharedTheme.value,
|
||||
FontFamily=SharedFont.value or Shared.FontFamily,
|
||||
HideWindowTitleBar=SharedTitleBar.value,
|
||||
).Save()
|
||||
Page.pop_dialog()
|
||||
OnSaved()
|
||||
TheApp.Notify("Settings saved.", "success")
|
||||
|
||||
Page.show_dialog(ft.AlertDialog(
|
||||
title="Settings",
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Row(
|
||||
[
|
||||
ft.Container(
|
||||
CategoryList, width=CategoryListWidth, bgcolor=Palette.Surface0, border_radius=4, padding=4,
|
||||
),
|
||||
DetailArea,
|
||||
],
|
||||
expand=True,
|
||||
vertical_alignment=ft.CrossAxisAlignment.STRETCH,
|
||||
),
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
_Note(RestartNote),
|
||||
],
|
||||
width=DialogWidth,
|
||||
height=DialogHeight,
|
||||
),
|
||||
actions=[
|
||||
ft.OutlinedButton(content="Cancel", style=ChipButtonStyle(), on_click=lambda e: Page.pop_dialog()),
|
||||
ft.OutlinedButton(content="Save", style=ChipButtonStyle(), on_click=Save),
|
||||
],
|
||||
))
|
||||
-196
@@ -1,196 +0,0 @@
|
||||
"""SaraSort's own settings (not SaraSortd's): the shared SaraSuite appearance,
|
||||
SaraSort's own, where SaraSortd lives, and exporting/importing settings in
|
||||
the same file format SaraFem uses ([SaraSuite] + one section per program)."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import flet as ft
|
||||
import tomlkit
|
||||
|
||||
from Core import (
|
||||
AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButton, Heading, LoadSharedSettings, Note,
|
||||
Palette, SharedSettings, ExportSettings, ImportSettings,
|
||||
)
|
||||
from Widgets import App
|
||||
|
||||
CategoryListWidth = 170
|
||||
RestartNote = "Theme, font and title bar changes apply the next time SaraSort starts."
|
||||
Pages = ["Flow", "Service", "Configs", "Templates", "Tools", "Settings"]
|
||||
|
||||
|
||||
def InstalledFonts() -> list[str]:
|
||||
try:
|
||||
Result = subprocess.run(["fc-list", ":", "family"], capture_output=True, text=True, timeout=5, env=ChildEnvironment())
|
||||
Families = {Line.split(",")[0].strip() for Line in Result.stdout.splitlines() if Line.strip()}
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
Families = set()
|
||||
Families.discard(BundledFontFamily)
|
||||
return [BundledFontFamily] + sorted(Families, key=str.lower)
|
||||
|
||||
|
||||
class SettingsPage(ft.Column):
|
||||
def __init__(self, TheApp: App, OnSaved: Callable[[], None]):
|
||||
super().__init__(expand=True, spacing=0)
|
||||
self.App = TheApp
|
||||
self.OnSaved = OnSaved
|
||||
self.Selected = "Shared"
|
||||
self.Detail = ft.Container(expand=True, padding=ft.Padding.only(left=16, right=16, top=8))
|
||||
self.CategoryList = ft.Column(spacing=0)
|
||||
self.Fonts: list[str] = []
|
||||
self.controls = [
|
||||
ft.Container(
|
||||
ft.Row([
|
||||
Heading("Settings"), ft.Container(expand=True),
|
||||
ChipButton("Import", self._Import, ft.Icons.FILE_DOWNLOAD_OUTLINED),
|
||||
ChipButton("Export", self._Export, ft.Icons.FILE_UPLOAD_OUTLINED),
|
||||
ChipButton("Revert", lambda e: self.Refresh(), ft.Icons.UNDO),
|
||||
ChipButton("Save", lambda e: self._Save(), ft.Icons.SAVE_OUTLINED),
|
||||
]),
|
||||
padding=ft.Padding.only(left=16, right=16, top=12, bottom=8),
|
||||
),
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
ft.Row([
|
||||
ft.Container(self.CategoryList, width=CategoryListWidth, bgcolor=Palette.Mantle, padding=4),
|
||||
self.Detail,
|
||||
], expand=True, vertical_alignment=ft.CrossAxisAlignment.STRETCH),
|
||||
ft.Container(Note(RestartNote), padding=ft.Padding.only(left=16, bottom=10, top=6)),
|
||||
]
|
||||
|
||||
def Refresh(self) -> None:
|
||||
if not self.Fonts:
|
||||
self.Fonts = InstalledFonts()
|
||||
Config = self.App.Config
|
||||
Themes = AvailableThemes()
|
||||
|
||||
def ThemePicker(Value: str) -> ft.Dropdown:
|
||||
Names = Themes if Value in Themes else [Value] + Themes
|
||||
return ft.Dropdown(value=Value, label="Theme", options=[ft.DropdownOption(key=Name, text=Name) for Name in Names])
|
||||
|
||||
def FontPicker(Value: str) -> ft.Dropdown:
|
||||
Names = self.Fonts if Value in self.Fonts else [Value] + self.Fonts
|
||||
return ft.Dropdown(value=Value, label="Font", editable=True, enable_filter=True, menu_height=320,
|
||||
options=[ft.DropdownOption(key=Name, text=Name) for Name in Names])
|
||||
|
||||
Shared = LoadSharedSettings(Config)
|
||||
self.UseShared = ft.Switch(label="SaraSort uses these shared settings", value=Config.UseSharedSettings,
|
||||
on_change=lambda e: self._DimOwn())
|
||||
self.SharedTheme = ThemePicker(Shared.Theme)
|
||||
self.SharedFont = FontPicker(Shared.FontFamily)
|
||||
self.SharedTitleBar = ft.Switch(label="Hide window title bar", value=Shared.HideWindowTitleBar)
|
||||
|
||||
self.OwnTheme = ThemePicker(Config.Theme)
|
||||
self.OwnFont = FontPicker(Config.FontFamily)
|
||||
self.OwnTitleBar = ft.Switch(label="Hide window title bar", value=Config.HideWindowTitleBar)
|
||||
self.OwnAppearance = ft.Column([
|
||||
Note("SaraSort's own appearance, used when shared settings are off"),
|
||||
self.OwnTheme, self.OwnFont, self.OwnTitleBar,
|
||||
], spacing=12)
|
||||
|
||||
self.ConfigDir = ft.TextField(label="SaraSortd config folder (GlobalConf.toml and the templates)", value=Config.SortdConfigDir)
|
||||
self.AppDir = ft.TextField(label="SaraSortd program folder (SaraSortd.py)", value=Config.SortdAppDir)
|
||||
self.Unit = ft.TextField(label="systemd user unit", value=Config.ServiceName)
|
||||
self.AutoRestart = ft.Switch(label="Restart SaraSortd after changes to GlobalConf.toml", value=Config.AutoRestart)
|
||||
self.Refresh_ = ft.TextField(label="Service page refresh (seconds)", value=str(Config.RefreshSeconds), width=260,
|
||||
keyboard_type=ft.KeyboardType.NUMBER)
|
||||
self.LogLines = ft.TextField(label="Log lines to show", value=str(Config.LogLines), width=260,
|
||||
keyboard_type=ft.KeyboardType.NUMBER)
|
||||
self.LabelLength = ft.TextField(label="Arrow label length before shortening", value=str(Config.LabelLength), width=260,
|
||||
keyboard_type=ft.KeyboardType.NUMBER)
|
||||
self.StartPage = ft.Dropdown(label="Start page", value=Config.StartPage, width=260,
|
||||
options=[ft.DropdownOption(key=Name, text=Name) for Name in Pages[:-1]])
|
||||
|
||||
self.Categories = {
|
||||
"Shared": ft.Column([
|
||||
Note("Shared by every SaraSuite program (~/.config/SaraSuite/Config.toml)."),
|
||||
self.UseShared,
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
self.SharedTheme, self.SharedFont, self.SharedTitleBar,
|
||||
], spacing=12, scroll=ft.ScrollMode.AUTO),
|
||||
"SaraSort": ft.Column([
|
||||
self.StartPage, self.LabelLength,
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
self.OwnAppearance,
|
||||
], spacing=12, scroll=ft.ScrollMode.AUTO),
|
||||
"SaraSortd": ft.Column([
|
||||
Note("Defaults are where SaraSuite installs SaraSortd."),
|
||||
self.ConfigDir, self.AppDir, self.Unit, self.AutoRestart, self.Refresh_, self.LogLines,
|
||||
], spacing=12, scroll=ft.ScrollMode.AUTO),
|
||||
}
|
||||
self._DimOwn(Update=False)
|
||||
self._Select(self.Selected, Update=False)
|
||||
self.update()
|
||||
|
||||
def _DimOwn(self, Update: bool = True) -> None:
|
||||
self.OwnAppearance.disabled = bool(self.UseShared.value)
|
||||
self.OwnAppearance.opacity = 0.5 if self.UseShared.value else 1
|
||||
if Update:
|
||||
self.OwnAppearance.update()
|
||||
|
||||
def _Select(self, Name: str, Update: bool = True) -> None:
|
||||
self.Selected = Name
|
||||
self.CategoryList.controls = [
|
||||
ft.ListTile(title=Category, selected=Category == Name, selected_tile_color=Palette.Surface1,
|
||||
selected_color=Palette.Mauve, on_click=lambda e, Category=Category: self._Select(Category))
|
||||
for Category in self.Categories
|
||||
]
|
||||
self.Detail.content = self.Categories[Name]
|
||||
if Update:
|
||||
self.update()
|
||||
|
||||
@staticmethod
|
||||
def _Int(Field: ft.TextField, Fallback: int, Minimum: int) -> int:
|
||||
try:
|
||||
return max(Minimum, int(Field.value))
|
||||
except (TypeError, ValueError):
|
||||
return Fallback
|
||||
|
||||
def _Save(self) -> None:
|
||||
Config: AppConfig = self.App.Config
|
||||
Config.UseSharedSettings = bool(self.UseShared.value)
|
||||
Config.Theme = self.OwnTheme.value or Config.Theme
|
||||
Config.FontFamily = self.OwnFont.value or Config.FontFamily
|
||||
Config.HideWindowTitleBar = bool(self.OwnTitleBar.value)
|
||||
Config.SortdConfigDir = (self.ConfigDir.value or "").strip() or Config.SortdConfigDir
|
||||
Config.SortdAppDir = (self.AppDir.value or "").strip() or Config.SortdAppDir
|
||||
Config.ServiceName = (self.Unit.value or "").strip() or Config.ServiceName
|
||||
Config.AutoRestart = bool(self.AutoRestart.value)
|
||||
Config.RefreshSeconds = self._Int(self.Refresh_, Config.RefreshSeconds, 1)
|
||||
Config.LogLines = self._Int(self.LogLines, Config.LogLines, 10)
|
||||
Config.LabelLength = self._Int(self.LabelLength, Config.LabelLength, 6)
|
||||
Config.StartPage = self.StartPage.value or Config.StartPage
|
||||
Config.Save()
|
||||
Shared = LoadSharedSettings(Config)
|
||||
SharedSettings(
|
||||
Theme=self.SharedTheme.value or Shared.Theme,
|
||||
FontFamily=self.SharedFont.value or Shared.FontFamily,
|
||||
HideWindowTitleBar=bool(self.SharedTitleBar.value),
|
||||
).Save()
|
||||
self.App.Notify("Settings saved.", "success")
|
||||
self.OnSaved()
|
||||
|
||||
async def _Export(self, e) -> None:
|
||||
Chosen = await self.App.Picker.save_file(dialog_title="Export settings", file_name="SaraSortSettings.toml",
|
||||
allowed_extensions=["toml"])
|
||||
if not Chosen:
|
||||
return
|
||||
try:
|
||||
ExportSettings(self.App.Config, Path(Chosen))
|
||||
except OSError as Error:
|
||||
self.App.Notify(f"Couldn't export settings: {Error}", "error")
|
||||
return
|
||||
self.App.Notify("Settings exported.", "success")
|
||||
|
||||
async def _Import(self, e) -> None:
|
||||
Files = await self.App.Picker.pick_files(dialog_title="Import settings", allowed_extensions=["toml"])
|
||||
if not Files:
|
||||
return
|
||||
try:
|
||||
ImportSettings(self.App.Config, Path(Files[0].path))
|
||||
except (OSError, tomlkit.exceptions.TOMLKitError) as Error:
|
||||
self.App.Notify(f"Couldn't import settings: {Error}", "error")
|
||||
return
|
||||
self.Refresh()
|
||||
self.App.Notify("Settings imported. Theme, font and title bar apply on the next start.", "success")
|
||||
self.OnSaved()
|
||||
+6
-3
@@ -7,7 +7,7 @@ from typing import Callable, Optional
|
||||
|
||||
import flet as ft
|
||||
|
||||
from Core import AppConfig, ChildEnvironment, ChipButton, Note, Palette
|
||||
from Core import AppConfig, ChildEnvironment, ChipButton, Note, NotificationStyle, Palette
|
||||
from Service import RestartIfRunning
|
||||
from Sortd import Setup, SortdError
|
||||
|
||||
@@ -20,9 +20,12 @@ class App:
|
||||
self.Navigate: Callable[..., None] = lambda Name, **Arguments: None # set by Main
|
||||
|
||||
def Notify(self, Text: str, Kind: str = "info") -> None:
|
||||
Color = {"success": Palette.Green, "error": Palette.Red, "warning": Palette.Yellow}.get(Kind, Palette.Text)
|
||||
Background, Accent = NotificationStyle(Kind)
|
||||
self.Page.show_dialog(ft.SnackBar(
|
||||
ft.Text(Text, color=Color), bgcolor=Palette.Surface0, duration=ft.Duration(seconds=4),
|
||||
content=ft.Text(Text, color=Accent),
|
||||
bgcolor=Background,
|
||||
shape=ft.RoundedRectangleBorder(radius=4, side=ft.BorderSide(1, Accent)),
|
||||
open=True,
|
||||
))
|
||||
|
||||
def LoadSetup(self) -> tuple[Optional[Setup], str]:
|
||||
|
||||
Reference in new issue
Block a user