mirror of
https://git.srhtnin.garden/sarah/SaraTag.git
synced 2026-10-13 18:28:16 +02:00
319 lines
14 KiB
Python
319 lines
14 KiB
Python
"""Settings dialog: the shared SaraSuite settings (with export/import),
|
|
SaraTag's own GUI settings, and SaraTagConf.toml (Explorer, Lexicons,
|
|
History). Laid out like SaraFem's, SaraSuite's and SaraSort'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, IsWindows, LoadSharedSettings, Palette, SharedSettings,
|
|
)
|
|
from Library import ConfPath, ConfValue, DataDir, DataError, SaveConfValues
|
|
from Widgets import App
|
|
|
|
DialogWidth = 720
|
|
DialogHeight = 540
|
|
CategoryListWidth = 150
|
|
RestartNote = "Theme, font and title bar changes apply the next time SaraTag starts."
|
|
StartPages = ["Browse", "Records", "Libraries", "Lexicons", "Update"]
|
|
|
|
|
|
def _Note(Text: str) -> ft.Text:
|
|
return ft.Text(Text, color=Palette.Overlay0, size=12)
|
|
|
|
|
|
def _WindowsFonts() -> set[str]:
|
|
"""Font families from the system and per-user Fonts registry keys
|
|
("Arial Bold (TrueType)" -> "Arial")."""
|
|
import re
|
|
import winreg
|
|
|
|
Families: set[str] = set()
|
|
Key = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
|
|
for Root in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER):
|
|
try:
|
|
with winreg.OpenKey(Root, Key) as Fonts:
|
|
Index = 0
|
|
while True:
|
|
try:
|
|
Name = winreg.EnumValue(Fonts, Index)[0]
|
|
except OSError:
|
|
break
|
|
Index += 1
|
|
Name = re.sub(r"\s*\(.*\)$", "", Name)
|
|
Name = re.sub(r"\s+(Bold|Italic|Light|Semibold|SemiBold|Black|Regular|Medium|Thin)\b.*$", "", Name)
|
|
if Name and "&" not in Name:
|
|
Families.add(Name.strip())
|
|
except OSError:
|
|
continue
|
|
return Families
|
|
|
|
|
|
def _Fonts() -> list[str]:
|
|
"""Installed font families (fontconfig, or the registry on Windows), with
|
|
the bundled font first."""
|
|
if IsWindows:
|
|
Families = _WindowsFonts()
|
|
Families.discard(BundledFontFamily)
|
|
return [BundledFontFamily] + sorted(Families, key=str.lower)
|
|
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, dense=True)
|
|
|
|
|
|
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="SaraTag 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 / "SaraTag-Settings.toml"), label="Settings file path", dense=True)
|
|
|
|
# SaraTag'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("SaraTag'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 "Browse",
|
|
options=[ft.DropdownOption(key=Name, text=Name) for Name in StartPages],
|
|
)
|
|
PanelWidth = _NumberField("Browse side panel width", Config.PreviewWidth)
|
|
|
|
# SaraTagConf.toml, the portable config next to SaraTag's data.
|
|
def Switch(Section: str, Key: str, Label: str, Fallback: bool = True) -> ft.Switch:
|
|
return ft.Switch(label=Label, value=bool(ConfValue(Section, Key, Fallback)), data=(Section, Key))
|
|
|
|
def Number(Section: str, Key: str, Label: str, Fallback: int) -> ft.TextField:
|
|
Field = _NumberField(Label, int(ConfValue(Section, Key, Fallback)))
|
|
Field.data = (Section, Key, Fallback)
|
|
return Field
|
|
|
|
ConfSwitches = [
|
|
Switch("Explorer", "HideUntaggedDirectories", "Hide directories with no Lexicon in or below them"),
|
|
Switch("Explorer", "HideTagFiles", "Hide a directory's own Lexicon file from its listing"),
|
|
Switch("Explorer", "ShowInvalidEntries", "Show files whose Book is broken or has invalid values"),
|
|
Switch("Explorer", "ShowImagePreviews", "Show image previews"),
|
|
Switch("Lexicon", "AutoInheritance", "New subdirectory Lexicons inherit from the one above"),
|
|
Switch("Lexicon", "RelativeSourcePaths", "Write Source paths relative to the Lexicon"),
|
|
Switch("History", "Enabled", "Keep history (.SaraTagHistory.toml)"),
|
|
Switch("History", "RememberFilter", "Remember each collection's filter"),
|
|
Switch("History", "RememberSorting", "Remember each collection's sorting"),
|
|
Switch("History", "RememberLastDirectory", "Remember the last directory"),
|
|
Switch("History", "RememberLastSelectedFile", "Remember the last selected file"),
|
|
Switch("History", "RememberRecentFiles", "Remember recently opened files"),
|
|
Switch("History", "RememberUpdaterMode", "Remember the Update page's last choices"),
|
|
Switch("History", "RememberUpdates", "Remember which Books updates changed"),
|
|
]
|
|
ConfNumbers = [
|
|
Number("History", "MaxFileHistory", "Recent files kept", 30),
|
|
Number("History", "MaxDirectoryHistory", "Collections remembered", 20),
|
|
Number("History", "MaxUpdateHistory", "Updated Books remembered", 20),
|
|
]
|
|
VideoPlayer = ft.TextField(label="Video player (empty: the default app)", value=str(ConfValue("Media", "VideoPlayer", "")), dense=True)
|
|
PatternField = ft.TextField(label="Lexicon file name pattern", value=str(ConfValue("Lexicon", "LexiconFilePattern", "{Source}_{DirectoryName}.toml")),
|
|
dense=True)
|
|
|
|
def ConfControls(Section: str) -> list[ft.Control]:
|
|
return [Control for Control in ConfSwitches + ConfNumbers if Control.data[0] == Section]
|
|
|
|
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 SaraTag's own "
|
|
"(General). Same format as SaraFem's and SaraSort'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, PanelWidth, ft.Divider(height=1, color=Palette.Surface1), OwnAppearance],
|
|
spacing=12, scroll=ft.ScrollMode.AUTO,
|
|
),
|
|
"Browse": ft.Column(
|
|
[_Note(f"Stored in SaraTagConf.toml ({DataDir})."), *ConfControls("Explorer"), VideoPlayer],
|
|
spacing=8, scroll=ft.ScrollMode.AUTO,
|
|
),
|
|
"Lexicons": ft.Column(
|
|
[_Note("Stored in SaraTagConf.toml."), *ConfControls("Lexicon"), PatternField,
|
|
_Note("{Source} is the parent Lexicon's name (or the parent directory's), {DirectoryName} the directory's.")],
|
|
spacing=8, scroll=ft.ScrollMode.AUTO,
|
|
),
|
|
"History": ft.Column(
|
|
[_Note("Stored in SaraTagConf.toml; the history itself in .SaraTagHistory.toml."), *ConfControls("History")],
|
|
spacing=8, 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.PreviewWidth = _ReadInt(PanelWidth, Config.PreviewWidth, 240)
|
|
Config.Save()
|
|
SharedSettings(
|
|
Theme=SharedTheme.value,
|
|
FontFamily=SharedFont.value or Shared.FontFamily,
|
|
HideWindowTitleBar=SharedTitleBar.value,
|
|
).Save()
|
|
Values: dict[str, dict] = {}
|
|
for Control in ConfSwitches:
|
|
Values.setdefault(Control.data[0], {})[Control.data[1]] = bool(Control.value)
|
|
for Control in ConfNumbers:
|
|
Section, Key, Fallback = Control.data
|
|
Values.setdefault(Section, {})[Key] = _ReadInt(Control, int(ConfValue(Section, Key, Fallback)), 0)
|
|
Values.setdefault("Media", {})["VideoPlayer"] = (VideoPlayer.value or "").strip()
|
|
Pattern = (PatternField.value or "").strip()
|
|
if "{DirectoryName}" in Pattern and Pattern.endswith(".toml"):
|
|
Values["Lexicon"]["LexiconFilePattern"] = Pattern
|
|
elif Pattern:
|
|
TheApp.Notify("The Lexicon pattern needs {DirectoryName} and must end in .toml; kept the old one.", "warning")
|
|
try:
|
|
SaveConfValues(Values)
|
|
except (DataError, OSError) as Error:
|
|
TheApp.Notify(f"Couldn't save {ConfPath.name}: {Error}", "error")
|
|
return
|
|
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),
|
|
],
|
|
))
|