mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-13 18:28:14 +02:00
305 lines
14 KiB
Python
305 lines
14 KiB
Python
"""Settings dialog, laid out like SaraFem's and SaraSort's: a category list on
|
|
the left (SaraSuite / General / Connection), the chosen category on the right."""
|
|
|
|
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
import flet as ft
|
|
import tomlkit
|
|
|
|
from App import App
|
|
from Core import (
|
|
AppConfig, AssetsDir, AvailableThemes, ChipButton, DataHome, ExportSettings, Home, ImportSettings,
|
|
InstalledFontFamilies, LoadSharedSettings, Palette, SharedSettings,
|
|
)
|
|
|
|
DialogWidth = 700
|
|
DialogHeight = 520
|
|
CategoryListWidth = 150
|
|
RestartNote = "Theme, font and title bar changes apply the next time SaraView starts."
|
|
|
|
# When running as an AppImage, SaraView can put itself in the app menu (like SaraSort).
|
|
AppImagePath = os.environ.get("APPIMAGE", "")
|
|
MenuEntryPath = DataHome / "applications" / "SaraView.desktop"
|
|
MenuIconPath = DataHome / "SaraView" / "SaraView.png"
|
|
|
|
|
|
def SetMenuEntry(Enabled: bool) -> None:
|
|
if not Enabled:
|
|
MenuEntryPath.unlink(missing_ok=True)
|
|
MenuIconPath.unlink(missing_ok=True)
|
|
return
|
|
MenuIconPath.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy(AssetsDir / "Icon.png", MenuIconPath)
|
|
MenuEntryPath.parent.mkdir(parents=True, exist_ok=True)
|
|
MenuEntryPath.write_text(
|
|
"[Desktop Entry]\nType=Application\nName=SaraView\n"
|
|
"Comment=View and edit files, locally or on a SaraView server\n"
|
|
f"Exec={AppImagePath}\nIcon={MenuIconPath}\nTerminal=false\n"
|
|
"Categories=Utility;\nStartupNotify=true\n"
|
|
)
|
|
|
|
|
|
def _Note(Text: str) -> ft.Text:
|
|
return ft.Text(Text, color=Palette.Overlay0, size=12)
|
|
|
|
|
|
def _ReadInt(Field: ft.TextField, Fallback: int, Minimum: int, Maximum: int) -> int:
|
|
try:
|
|
return min(Maximum, 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], StartCategory: str = "General") -> None:
|
|
"""OnSaved(Reconnect): Reconnect is True when the source itself changed
|
|
(mode, root, server or local listing options)."""
|
|
Page = TheApp.Page
|
|
Config: AppConfig = TheApp.Config
|
|
Themes = AvailableThemes()
|
|
Fonts = InstalledFontFamilies()
|
|
|
|
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 = 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="SaraView 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 / "SaraView-Settings.toml"), label="Settings file path")
|
|
|
|
# SaraView'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("SaraView'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
|
|
|
|
# General.
|
|
OpenInAppSwitch = ft.Switch(label="Double-click opens files in SaraView (off: in their default program)",
|
|
value=Config.OpenFilesInApp)
|
|
ConfirmDeleteSwitch = ft.Switch(label="Ask before deleting", value=Config.ConfirmDelete)
|
|
SidebarSwitch = ft.Switch(label="Show the directory tree (Ctrl+B)", value=Config.ShowSidebar)
|
|
TreeFilesSwitch = ft.Switch(label="Show files in the tree (off: directories only)", value=Config.ShowFilesInTree)
|
|
MarkdownEditingPicker = ft.RadioGroup(
|
|
ft.Column([
|
|
ft.Radio(value="Live", label="Live: click a part of the rendered file to edit its Markdown"),
|
|
ft.Radio(value="Separate", label="Separate: View and Edit are two modes"),
|
|
], spacing=0),
|
|
value=Config.MarkdownEditing,
|
|
)
|
|
FontSizeField = ft.TextField(label="Text size", value=str(Config.FontSize), keyboard_type=ft.KeyboardType.NUMBER, width=160)
|
|
|
|
def ToggleMenuEntry(e) -> None:
|
|
SetMenuEntry(not MenuEntryPath.exists())
|
|
MenuButton.content = "Remove from app menu" if MenuEntryPath.exists() else "Add to app menu"
|
|
MenuButton.update()
|
|
|
|
MenuButton = ChipButton("Remove from app menu" if MenuEntryPath.exists() else "Add to app menu", ToggleMenuEntry)
|
|
MenuButton.visible = bool(AppImagePath)
|
|
|
|
# Graph.
|
|
GraphDirectoriesSwitch = ft.Switch(label="Show directories (off: only files and the links between them)",
|
|
value=Config.GraphShowDirectories)
|
|
PanButtonPicker = ft.RadioGroup(
|
|
ft.Column([
|
|
ft.Radio(value="Left", label="Left"),
|
|
ft.Radio(value="Middle", label="Middle (click to grab, move, click again to let go)"),
|
|
ft.Radio(value="Right", label="Right"),
|
|
], spacing=0),
|
|
value=Config.GraphPanButton,
|
|
)
|
|
|
|
# Connection.
|
|
ModePicker = ft.RadioGroup(
|
|
ft.Row([ft.Radio(value="Local", label="Local files"), ft.Radio(value="Server", label="SaraView server")]),
|
|
value=Config.Mode,
|
|
)
|
|
LocalRootField = ft.TextField(label="Root directory", value=Config.LocalRoot, expand=True)
|
|
|
|
async def BrowseRoot(e) -> None:
|
|
Start = os.path.expanduser(LocalRootField.value or "~")
|
|
Chosen = await TheApp.Picker.get_directory_path(dialog_title="Root directory",
|
|
initial_directory=Start if os.path.isdir(Start) else None)
|
|
if Chosen:
|
|
LocalRootField.value = Chosen
|
|
LocalRootField.update()
|
|
|
|
ShowHiddenSwitch = ft.Switch(label="Show hidden directories and files", value=Config.ShowHiddenFolders)
|
|
ShowSvSwitch = ft.Switch(label="Show .sv_ directory notes", value=Config.ShowSvFiles)
|
|
HideInternalSwitch = ft.Switch(label="Hide SaraTag's own files (Books, .saratag)", value=Config.HideInternalFiles)
|
|
LocalOptions = ft.Column([
|
|
ft.Row([LocalRootField, ft.IconButton(ft.Icons.FOLDER_OPEN_OUTLINED, tooltip="Browse", on_click=BrowseRoot)]),
|
|
ShowHiddenSwitch, ShowSvSwitch, HideInternalSwitch,
|
|
], spacing=12, visible=Config.Mode == "Local")
|
|
|
|
ServerUrlField = ft.TextField(label="Server address", value=Config.ServerUrl, hint_text="http://spines:8420")
|
|
RememberSwitch = ft.Switch(label="Stay logged in between starts", value=Config.RememberLogin)
|
|
ServerOptions = ft.Column([
|
|
ServerUrlField,
|
|
_Note("Over Tailscale, like the web view. Logged out, you browse as Guest; log in from the "
|
|
"connection button in the toolbar to change files. Hidden files and the theme on the "
|
|
"server follow your SaraView user settings."),
|
|
RememberSwitch,
|
|
], spacing=12, visible=Config.Mode == "Server")
|
|
|
|
def OnModeChanged(e) -> None:
|
|
LocalOptions.visible = ModePicker.value == "Local"
|
|
ServerOptions.visible = ModePicker.value == "Server"
|
|
LocalOptions.update()
|
|
ServerOptions.update()
|
|
|
|
ModePicker.on_change = OnModeChanged
|
|
|
|
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 old values; close it rather than
|
|
# leave a stale Save that would overwrite the import.
|
|
TheApp.Close()
|
|
OnSaved(True)
|
|
TheApp.Notify("Settings imported. 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 (never your login). Same format as SaraFem's."),
|
|
SettingsFileField,
|
|
ft.Row([ChipButton("Export", OnExport), ChipButton("Import", OnImport)]),
|
|
], spacing=12, scroll=ft.ScrollMode.AUTO),
|
|
"General": ft.Column([
|
|
OpenInAppSwitch, ConfirmDeleteSwitch, SidebarSwitch, TreeFilesSwitch, FontSizeField, MenuButton,
|
|
ft.Divider(height=1, color=Palette.Surface1),
|
|
ft.Text("Markdown editing"),
|
|
MarkdownEditingPicker,
|
|
ft.Divider(height=1, color=Palette.Surface1),
|
|
OwnAppearance,
|
|
], spacing=12, scroll=ft.ScrollMode.AUTO),
|
|
"Graph": ft.Column([
|
|
_Note("The graph view (Ctrl+G): directories in the heading colours by depth, files white, "
|
|
"joined to their directory and to the files that reference them."),
|
|
GraphDirectoriesSwitch,
|
|
ft.Divider(height=1, color=Palette.Surface1),
|
|
ft.Text("Pan with mouse button"),
|
|
PanButtonPicker,
|
|
_Note("Scroll zooms. Flet can't follow a held middle button, so Middle works like "
|
|
"middle-click autoscroll."),
|
|
], spacing=12, scroll=ft.ScrollMode.AUTO),
|
|
"Connection": ft.Column([
|
|
_Note("Browse a directory on this computer, or connect to a SaraView server."),
|
|
ModePicker, LocalOptions, ServerOptions,
|
|
], spacing=12, scroll=ft.ScrollMode.AUTO),
|
|
}
|
|
|
|
DetailArea = ft.Container(Categories[StartCategory], 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 == StartCategory, 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:
|
|
SourceBefore = (Config.Mode, Config.LocalRoot, Config.ServerUrl, Config.ShowHiddenFolders,
|
|
Config.ShowSvFiles, Config.HideInternalFiles)
|
|
Config.UseSharedSettings = UseSharedSwitch.value
|
|
Config.Theme = OwnTheme.value or Config.Theme
|
|
Config.FontFamily = OwnFont.value or Config.FontFamily
|
|
Config.HideWindowTitleBar = OwnTitleBar.value
|
|
Config.OpenFilesInApp = OpenInAppSwitch.value
|
|
Config.ConfirmDelete = ConfirmDeleteSwitch.value
|
|
Config.ShowSidebar = SidebarSwitch.value
|
|
Config.ShowFilesInTree = TreeFilesSwitch.value
|
|
Config.MarkdownEditing = MarkdownEditingPicker.value or Config.MarkdownEditing
|
|
Config.GraphShowDirectories = GraphDirectoriesSwitch.value
|
|
Config.GraphPanButton = PanButtonPicker.value or Config.GraphPanButton
|
|
Config.FontSize = _ReadInt(FontSizeField, Config.FontSize, 8, 32)
|
|
Config.Mode = ModePicker.value or Config.Mode
|
|
Config.LocalRoot = (LocalRootField.value or "").strip() or "~"
|
|
Config.ServerUrl = (ServerUrlField.value or "").strip().rstrip("/")
|
|
Config.RememberLogin = RememberSwitch.value
|
|
Config.ShowHiddenFolders = ShowHiddenSwitch.value
|
|
Config.ShowSvFiles = ShowSvSwitch.value
|
|
Config.HideInternalFiles = HideInternalSwitch.value
|
|
Config.Save()
|
|
SharedSettings(
|
|
Theme=SharedTheme.value,
|
|
FontFamily=SharedFont.value or Shared.FontFamily,
|
|
HideWindowTitleBar=SharedTitleBar.value,
|
|
).Save()
|
|
SourceAfter = (Config.Mode, Config.LocalRoot, Config.ServerUrl, Config.ShowHiddenFolders,
|
|
Config.ShowSvFiles, Config.HideInternalFiles)
|
|
TheApp.Close()
|
|
OnSaved(SourceAfter != SourceBefore)
|
|
TheApp.Notify("Settings saved.", "success")
|
|
|
|
TheApp.DialogOpen = True
|
|
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=[ChipButton("Cancel", lambda e: TheApp.Close()), ChipButton("Save", Save)],
|
|
on_dismiss=lambda e: setattr(TheApp, "DialogOpen", False),
|
|
))
|