From e90f59bc16d99bb0ca04e021abab50efc009f5bb Mon Sep 17 00:00:00 2001 From: SRHTNIN Date: Wed, 7 Oct 2026 22:36:04 +0200 Subject: [PATCH] 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 --- Core.py | 17 ++++ Main.py | 74 ++++++++++---- README.md | 8 +- Settings.py | 253 ++++++++++++++++++++++++++++++++++++++++++++++++ SettingsPage.py | 196 ------------------------------------- Widgets.py | 9 +- 6 files changed, 338 insertions(+), 219 deletions(-) create mode 100644 Settings.py delete mode 100644 SettingsPage.py diff --git a/Core.py b/Core.py index 0c71f2b..2427099 100644 --- a/Core.py +++ b/Core.py @@ -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( diff --git a/Main.py b/Main.py index 75aa261..66220b8 100644 --- a/Main.py +++ b/Main.py @@ -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)) diff --git a/README.md b/README.md index 9ffaadb..610fb8e 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/Settings.py b/Settings.py new file mode 100644 index 0000000..f6b0bf1 --- /dev/null +++ b/Settings.py @@ -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), + ], + )) diff --git a/SettingsPage.py b/SettingsPage.py deleted file mode 100644 index 58c337a..0000000 --- a/SettingsPage.py +++ /dev/null @@ -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() diff --git a/Widgets.py b/Widgets.py index 1312205..b5a93c2 100644 --- a/Widgets.py +++ b/Widgets.py @@ -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]: