mirror of
https://git.srhtnin.garden/sarah/SaraTune.git
synced 2026-10-13 18:18:11 +02:00
313 lines
13 KiB
Python
313 lines
13 KiB
Python
"""Settings dialog: the shared SaraSuite settings (with export/import),
|
|
SaraTune's own, and Music mode's library. Laid out like SaraFem's and
|
|
SaraSuite's Settings (a category list on the left, the chosen category on the
|
|
right)."""
|
|
|
|
import os
|
|
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, PlaylistsDir, SharedSettings,
|
|
)
|
|
from Widgets import App
|
|
|
|
DialogWidth = 700
|
|
DialogHeight = 520
|
|
CategoryListWidth = 150
|
|
RestartNote = "Theme, font and title bar changes apply the next time SaraTune starts."
|
|
Modes = ["Music", "Audio"]
|
|
|
|
|
|
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)
|
|
|
|
|
|
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[[bool], None], OnClearCache: Callable[[], None]) -> None:
|
|
"""OnSaved(FoldersChanged) runs after saving; OnClearCache empties the
|
|
library cache (Music mode rescans)."""
|
|
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="SaraTune 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 / "SaraTune-Settings.toml"), label="Settings file path")
|
|
|
|
# SaraTune'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("SaraTune'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
|
|
|
|
StartMode = ft.Dropdown(
|
|
label="Start in", value=Config.StartMode if Config.StartMode in Modes else "Music",
|
|
options=[ft.DropdownOption(key=Name, text=f"{Name} mode") for Name in Modes],
|
|
)
|
|
SeekField = _NumberField("Arrow keys seek by (seconds, Audio mode)", Config.SeekSeconds)
|
|
AdvanceSwitch = ft.Switch(label="Audio mode plays the next file in the folder when one ends",
|
|
value=Config.AudioAutoAdvance)
|
|
|
|
# Music: one folder per line, plus a Browse button that adds a line.
|
|
FoldersField = ft.TextField(label="Music folders (one per line)", value="\n".join(Config.MusicFolders),
|
|
multiline=True, min_lines=3, max_lines=6, expand=True)
|
|
|
|
async def AddFolder(e) -> None:
|
|
Chosen = await TheApp.Picker.get_directory_path(dialog_title="Add a music folder")
|
|
if Chosen:
|
|
Lines = [Line for Line in (FoldersField.value or "").splitlines() if Line.strip()]
|
|
if Chosen not in Lines:
|
|
FoldersField.value = "\n".join(Lines + [Chosen])
|
|
FoldersField.update()
|
|
|
|
ScanSwitch = ft.Switch(label="Look for new and changed files every start", value=Config.ScanOnStart)
|
|
SessionSwitch = ft.Switch(label="Remember the queue and position between runs", value=Config.RememberSession)
|
|
|
|
def ClearCache(e) -> None:
|
|
Page.pop_dialog()
|
|
OnClearCache()
|
|
|
|
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
|
|
OldFolders = list(Config.MusicFolders)
|
|
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(Config.MusicFolders != OldFolders)
|
|
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(
|
|
[
|
|
StartMode,
|
|
_Note("Opening files with SaraTune (e.g. from a file manager) always starts in Audio mode."),
|
|
SeekField,
|
|
AdvanceSwitch,
|
|
ft.Divider(height=1, color=Palette.Surface1),
|
|
OwnAppearance,
|
|
],
|
|
spacing=12,
|
|
scroll=ft.ScrollMode.AUTO,
|
|
),
|
|
"Music": ft.Column(
|
|
[
|
|
_Note("Music mode's library is every audio file below these folders."),
|
|
ft.Row([FoldersField, ft.IconButton(ft.Icons.CREATE_NEW_FOLDER_OUTLINED, tooltip="Add a folder",
|
|
on_click=AddFolder)],
|
|
vertical_alignment=ft.CrossAxisAlignment.START),
|
|
ScanSwitch,
|
|
SessionSwitch,
|
|
ft.Divider(height=1, color=Palette.Surface1),
|
|
_Note("Playlists are kept in ~/.config/SaraSuite/SaraTune/Playlists/, one TOML file each. "
|
|
"The library's tags and cover thumbnails are only a cache."),
|
|
ft.Row([
|
|
ft.OutlinedButton(content="Open playlists folder", style=ChipButtonStyle(),
|
|
on_click=lambda e: (PlaylistsDir.mkdir(parents=True, exist_ok=True),
|
|
TheApp.OpenFolder(str(PlaylistsDir)))),
|
|
ft.OutlinedButton(content="Clear cache and rescan", style=ChipButtonStyle(Palette.Yellow),
|
|
on_click=ClearCache),
|
|
], wrap=True),
|
|
],
|
|
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:
|
|
OldFolders = list(Config.MusicFolders)
|
|
Config.UseSharedSettings = UseSharedSwitch.value
|
|
Config.Theme = OwnTheme.value or Config.Theme
|
|
Config.FontFamily = OwnFont.value or Config.FontFamily
|
|
Config.HideWindowTitleBar = OwnTitleBar.value
|
|
Config.StartMode = StartMode.value or Config.StartMode
|
|
Config.SeekSeconds = _ReadInt(SeekField, Config.SeekSeconds, 1)
|
|
Config.AudioAutoAdvance = AdvanceSwitch.value
|
|
Config.MusicFolders = [os.path.expanduser(Line.strip()) for Line in (FoldersField.value or "").splitlines()
|
|
if Line.strip()]
|
|
Config.ScanOnStart = ScanSwitch.value
|
|
Config.RememberSession = SessionSwitch.value
|
|
Config.Save()
|
|
SharedSettings(
|
|
Theme=SharedTheme.value,
|
|
FontFamily=SharedFont.value or Shared.FontFamily,
|
|
HideWindowTitleBar=SharedTitleBar.value,
|
|
).Save()
|
|
Page.pop_dialog()
|
|
OnSaved(Config.MusicFolders != OldFolders)
|
|
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),
|
|
],
|
|
))
|