Settings dialog, per-program install defaults, Options after install; Update only when needed

This commit is contained in:
sarah committed 2026-10-07 21:47:15 +02:00
1 parent 7018d57dbc
commit 08c9071b48
8 files changed
+360 -30

No files matched your search

+29 -6
View File
@@ -3,14 +3,15 @@
SaraSuite list
SaraSuite install <Name> [--set Id=Value ...] [--option Id=on|off ...]
SaraSuite update <Name>
SaraSuite options <Name> [--option Id=on|off ...] (no --option: list them)
SaraSuite remove <Name> [--configs] [--keep-runtime]
Install uses each question's default and each option's default unless set.
Install starts from your defaults (Settings, or the manifest's) unless set.
"""
import argparse
from Installer import Apply, GetStatus, Remove
from Installer import Apply, DefaultAnswers, DefaultOptions, FetchCommit, GetStatus, Remove
from Manifests import LoadPrograms
@@ -24,6 +25,10 @@ def _Pairs(Items: list[str]) -> dict[str, str]:
return Pairs
def _Switches(Items: list[str]) -> dict[str, bool]:
return {Key: Value.lower() in ("on", "yes", "true", "1") for Key, Value in _Pairs(Items).items()}
def Run(Arguments: list[str]) -> int:
Parser = argparse.ArgumentParser(prog="SaraSuite")
Commands = Parser.add_subparsers(dest="Command", required=True)
@@ -33,6 +38,9 @@ def Run(Arguments: list[str]) -> int:
Install.add_argument("--set", action="append", default=[], dest="Answers")
Install.add_argument("--option", action="append", default=[], dest="Options")
Commands.add_parser("update").add_argument("Name")
OptionsCommand = Commands.add_parser("options")
OptionsCommand.add_argument("Name")
OptionsCommand.add_argument("--option", action="append", default=[], dest="Options")
RemoveCommand = Commands.add_parser("remove")
RemoveCommand.add_argument("Name")
RemoveCommand.add_argument("--configs", action="store_true")
@@ -51,20 +59,35 @@ def Run(Arguments: list[str]) -> int:
if Program is None:
print(f"Unknown program {Args.Name!r}. Known: {', '.join(P.Name for P in Programs.values())}")
return 1
Installed = GetStatus(Program).Installed
Current = GetStatus(Program)
Installed = Current.Installed
if Args.Command == "install":
if Installed:
print(f"{Program.Name} is already installed; use update.")
return 1
Options = {Option.Id: Option.Default for Option in Program.Options}
Options.update({Key: Value.lower() in ("on", "yes", "true", "1") for Key, Value in _Pairs(Args.Options).items()})
Apply(Program, print, Options, _Pairs(Args.Answers))
Options = DefaultOptions(Program)
Options.update(_Switches(Args.Options))
Answers = DefaultAnswers(Program)
Answers.update(_Pairs(Args.Answers))
Apply(Program, print, Options, Answers)
elif Args.Command == "update":
if not Installed:
print(f"{Program.Name} isn't installed.")
return 1
Apply(Program, print)
elif Args.Command == "options":
if not Installed:
print(f"{Program.Name} isn't installed.")
return 1
if not Args.Options:
for Option in Program.Options:
State = "on" if Current.Receipt.Options.get(Option.Id) else "off"
print(f"{Option.Id:20} {State:4} {Option.Label}")
return 0
Options = dict(Current.Receipt.Options)
Options.update(_Switches(Args.Options))
Apply(Program, print, Options, Source=FetchCommit(Program, Current.Receipt.Commit, print))
elif Args.Command == "remove":
Remove(Program, Args.configs, not Args.keep_runtime, print)
return 0
+6 -1
View File
@@ -7,7 +7,7 @@ picked for one Sara program looks the same in all of them.
import os
import sys
from dataclasses import asdict, dataclass
from dataclasses import asdict, dataclass, field
from pathlib import Path
import flet as ft
@@ -106,6 +106,11 @@ class AppConfig:
# Where the Sara repos live (Gitea on SpineS); manifests
# use it as {GitHost}.
GitHost: str = "https://git.srhtnin.garden/sarah"
# What the install dialog starts with, per program, overriding the
# manifest's defaults: {Program: {OptionId: ticked}} and
# {Program: {QuestionId: answer}}. Set in Settings.
OptionDefaults: dict[str, dict[str, bool]] = field(default_factory=dict)
AnswerDefaults: dict[str, dict[str, str]] = field(default_factory=dict)
def Save(self) -> None:
ConfigDir.mkdir(parents=True, exist_ok=True)
+27 -3
View File
@@ -6,7 +6,7 @@ from typing import Callable
import flet as ft
from Core import ChipButtonStyle, Home, Palette
from Installer import ConfigPlan, RemovalItem, RemovalPlan, Status
from Installer import ConfigPlan, DefaultAnswers, DefaultOptions, RemovalItem, RemovalPlan, Status
from Manifests import Program
_MaxListedItems = 12
@@ -57,8 +57,12 @@ def _Dialog(Page: ft.Page, Title: str, Body: list[ft.Control], ConfirmText: str,
def ShowInstallDialog(Page: ft.Page, Program: Program, OnConfirm: Callable[[dict[str, bool], dict[str, str]], None]) -> None:
Fields = {Question.Id: ft.TextField(label=Question.Prompt, value=Question.Default) for Question in Program.Questions}
Boxes = {Option.Id: ft.Checkbox(label=Option.Label, value=Option.Default) for Option in Program.Options}
# Start from the user's defaults (Settings → this program's tab), which
# fall back to the manifest's.
Answers = DefaultAnswers(Program)
Ticked = DefaultOptions(Program)
Fields = {Question.Id: ft.TextField(label=Question.Prompt, value=Answers[Question.Id]) for Question in Program.Questions}
Boxes = {Option.Id: ft.Checkbox(label=Option.Label, value=Ticked[Option.Id]) for Option in Program.Options}
Body: list[ft.Control] = [ft.Text(Program.Description, size=13, color=Palette.Overlay0)]
Body += list(Fields.values())
@@ -75,6 +79,26 @@ def ShowInstallDialog(Page: ft.Page, Program: Program, OnConfirm: Callable[[dict
))
def ShowOptionsDialog(Page: ft.Page, Program: Program, Current: Status, OnConfirm: Callable[[dict[str, bool]], None]) -> None:
"""Change an installed program's options. Turning one off takes out what
it placed (and puts back any file it had replaced); turning one on places it."""
Recorded = Current.Receipt.Options if Current.Receipt else {}
Ticked = DefaultOptions(Program)
Boxes = {
Option.Id: ft.Checkbox(label=Option.Label, value=bool(Recorded.get(Option.Id, Ticked[Option.Id])))
for Option in Program.Options
}
Body: list[ft.Control] = list(Boxes.values())
Body.append(ft.Text(
"Only what these options control changes; the program stays at the version it's on.",
size=12, color=Palette.Overlay0,
))
Page.show_dialog(_Dialog(
Page, f"{Program.Name} options", Body, "Apply",
lambda: OnConfirm({Id: bool(Box.value) for Id, Box in Boxes.items()}),
))
def ShowUpdateDialog(
Page: ft.Page, Program: Program, Current: Status, NewVersion: str, NewCommit: str,
Plans: list[ConfigPlan], OnConfirm: Callable[[], None],
+39 -10
View File
@@ -19,7 +19,7 @@ from pathlib import Path, PurePosixPath
from typing import Callable, Optional
from ConfigMerge import ApplySet, MergeConfig, MergeResult
from Core import BackupsDir, ChildEnvironment, DownloadsDir, PristineDir
from Core import BackupsDir, ChildEnvironment, DownloadsDir, LoadConfig, PristineDir
from Manifests import Leftover, Program, Render
from Receipts import IniRecord, LoadReceipt, PlacedFile, Receipt
from Sources import ClearDownloads, Download, LatestCommit, SourceError
@@ -112,6 +112,23 @@ def Fetch(Program: Program, Log: LogFn) -> Fetched:
return Fetched(Commit, Download(Program.Name, Program.GitUrl, Commit, Log))
def FetchCommit(Program: Program, Commit: str, Log: LogFn) -> Fetched:
"""A specific commit, e.g. the installed one when only options change."""
return Fetched(Commit, Download(Program.Name, Program.GitUrl, Commit, Log))
def DefaultOptions(Program: Program) -> dict[str, bool]:
"""The manifest's option defaults, overridden by the user's own
(Settings → the program's tab)."""
Overrides = LoadConfig().OptionDefaults.get(Program.Name, {})
return {Option.Id: bool(Overrides.get(Option.Id, Option.Default)) for Option in Program.Options}
def DefaultAnswers(Program: Program) -> dict[str, str]:
Overrides = LoadConfig().AnswerDefaults.get(Program.Name, {})
return {Question.Id: str(Overrides.get(Question.Id, Question.Default)) for Question in Program.Questions}
def SourceFiles(Program: Program, SourceDir: Path) -> list[PurePosixPath]:
"""Relative paths of the files to copy, per the manifest's Include/Exclude."""
Found = []
@@ -448,10 +465,18 @@ def Apply(
) -> Receipt:
"""Install (no receipt yet) or update (receipt exists) a program to the
latest commit, or to Source if it was already fetched (the Update dialog
fetches first to preview the config merges).
Options/Answers are only used on install; updates keep the recorded ones."""
fetches first to preview the config merges; changing options passes the
installed commit, so nothing else changes).
Answers are only used on install. Options are used on install, and on an
installed program they replace the recorded ones (e.g. turning the file
picker off); otherwise the recorded options are kept."""
OldReceipt = LoadReceipt(Program.Name)
Action = "Updating" if OldReceipt else "Installing"
if OldReceipt is None:
Action = "Installing"
elif Options is not None and Source is not None and Source.Commit == OldReceipt.Commit:
Action = "Changing options for"
else:
Action = "Updating"
Log(f"== {Action} {Program.Name}")
Source = Source or Fetch(Program, Log)
@@ -463,14 +488,17 @@ def Apply(
ManifestHash=ManifestHash(Program),
InstalledAt=OldReceipt.InstalledAt if OldReceipt else _Now(),
UpdatedAt=_Now(),
Options=dict(OldReceipt.Options) if OldReceipt else dict(Options or {}),
Options=dict(OldReceipt.Options) if OldReceipt else {},
Answers=dict(OldReceipt.Answers) if OldReceipt else dict(Answers or {}),
)
# Options added to the manifest since this program was installed.
for Option in Program.Options:
NewReceipt.Options.setdefault(Option.Id, Option.Default)
NewReceipt.Options.update(Options or {})
# Anything not given or recorded yet (e.g. an option added to the manifest
# since this program was installed) starts at the user's/manifest default.
for OptionId, Default in DefaultOptions(Program).items():
NewReceipt.Options.setdefault(OptionId, Default)
for QuestionId, Default in DefaultAnswers(Program).items():
NewReceipt.Answers.setdefault(QuestionId, Default)
for Question in Program.Questions:
NewReceipt.Answers.setdefault(Question.Id, Question.Default)
if Question.Kind == "Path":
NewReceipt.Answers[Question.Id] = str(Path(NewReceipt.Answers[Question.Id]).expanduser().absolute())
Context = Program.Context(NewReceipt.Answers)
@@ -506,7 +534,8 @@ def Apply(
NewReceipt.Save()
Label = f"{Program.Name} {NewReceipt.Version} ({NewReceipt.Commit[:7]})".replace(" ", " ")
Log(f"== {Label} {'updated' if OldReceipt else 'installed'}")
Done = {"Installing": "installed", "Updating": "updated", "Changing options for": "options changed"}[Action]
Log(f"== {Label} {Done}")
return NewReceipt
+27 -3
View File
@@ -13,10 +13,11 @@ from Core import (
AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, ChipButtonStyle, DataDir,
DataHome, EffectiveSettings, LoadConfig, Palette,
)
from Dialogs import ShowInstallDialog, ShowRemoveDialog, ShowUpdateDialog
from Installer import Apply, Fetch, GetStatus, PlanConfigs, PlanRemoval, Remove, SourceVersion
from Dialogs import ShowInstallDialog, ShowOptionsDialog, ShowRemoveDialog, ShowUpdateDialog
from Installer import Apply, Fetch, FetchCommit, GetStatus, PlanConfigs, PlanRemoval, Remove, SourceVersion
from Manifests import LoadPrograms
from ProgramCard import ProgramCard
from Settings import ShowSettingsDialog
_MaxLogLines = 2000
@@ -154,8 +155,23 @@ def Main(Page: ft.Page) -> None:
),
)
def OnOptions(Card: ProgramCard) -> None:
# Reapplies the installed commit with the new options, so turning
# e.g. the file picker on or off doesn't also update the program.
Program = Card.Program
Current = Card.CurrentStatus
if Current is None or Current.Receipt is None:
return
Commit = Current.Receipt.Commit
ShowOptionsDialog(
Page, Program, Current,
lambda Options: RunOperation(
Card, lambda: Apply(Program, Log, Options=Options, Source=FetchCommit(Program, Commit, Log)),
),
)
for Program in Programs:
Cards.append(ProgramCard(Program, OnInstall, OnUpdate, OnRemove))
Cards.append(ProgramCard(Program, OnInstall, OnUpdate, OnRemove, OnOptions))
def ToggleMenuEntry(e) -> None:
_SetMenuEntry(not _MenuEntry.exists())
@@ -175,6 +191,13 @@ def Main(Page: ft.Page) -> None:
tooltip="Check for updates",
on_click=lambda e: RunOperation(None, RefreshAll),
)
SettingsButton = ft.IconButton(
icon=ft.Icons.SETTINGS_OUTLINED,
icon_color=Palette.Mauve,
tooltip="Settings",
# The git host may have changed, so re-check every program afterwards.
on_click=lambda e: ShowSettingsDialog(Page, Config, Programs, lambda: RunOperation(None, RefreshAll)),
)
Page.add(
ft.Column(
@@ -187,6 +210,7 @@ def Main(Page: ft.Page) -> None:
ft.Container(expand=True),
MenuButton,
RefreshButton,
SettingsButton,
]),
padding=ft.Padding.symmetric(horizontal=16, vertical=8),
bgcolor=Palette.Mantle,
+13 -3
View File
@@ -16,6 +16,7 @@ class ProgramCard(ft.Container):
OnInstall: Callable[["ProgramCard"], None],
OnUpdate: Callable[["ProgramCard"], None],
OnRemove: Callable[["ProgramCard"], None],
OnOptions: Callable[["ProgramCard"], None],
) -> None:
self.Program = Program
self.CurrentStatus: Optional[Status] = None
@@ -37,6 +38,9 @@ class ProgramCard(ft.Container):
self.UpdateButton = ft.OutlinedButton(
content="Update", style=ChipButtonStyle(), visible=False, on_click=lambda e: OnUpdate(self),
)
self.OptionsButton = ft.OutlinedButton(
content="Options", style=ChipButtonStyle(), visible=False, on_click=lambda e: OnOptions(self),
)
self.RemoveButton = ft.OutlinedButton(
content="Remove", style=ChipButtonStyle(Palette.Red), visible=False, on_click=lambda e: OnRemove(self),
)
@@ -54,6 +58,7 @@ class ProgramCard(ft.Container):
self.Spinner,
self.InstallButton,
self.UpdateButton,
self.OptionsButton,
self.RemoveButton,
],
),
@@ -102,9 +107,14 @@ class ProgramCard(ft.Container):
self.InstallButton.visible = not Current.Installed
self.InstallButton.disabled = Busy or not Current.LatestCommit
self.UpdateButton.visible = Current.Installed
self.UpdateButton.disabled = Busy or not Current.UpdateAvailable or not Current.LatestCommit
# Only there when there's something to update (or repair).
self.UpdateButton.visible = Current.UpdateAvailable
self.UpdateButton.disabled = Busy or not Current.LatestCommit
self.UpdateButton.content = "Repair" if Current.Broken else "Update"
# Change options (e.g. the file picker) after installing. Needs the
# git host, since it reapplies the installed commit.
self.OptionsButton.visible = Current.Installed and bool(self.Program.Options)
self.OptionsButton.disabled = Busy or not Current.LatestCommit
# Not installed but configs/runtime files were kept: still removable.
self.RemoveButton.visible = Current.Installed or bool(Leftovers.Items)
self.RemoveButton.content = "Remove" if Current.Installed else "Clean up"
@@ -114,6 +124,6 @@ class ProgramCard(ft.Container):
def SetBusy(self, Busy: bool, Working: bool = False) -> None:
"""Busy greys out the buttons (only one operation runs at a time);
Working also shows this card's spinner."""
for Button in (self.InstallButton, self.UpdateButton, self.RemoveButton):
for Button in (self.InstallButton, self.UpdateButton, self.OptionsButton, self.RemoveButton):
Button.disabled = Busy
self.Spinner.visible = Working
+20 -4
View File
@@ -42,10 +42,24 @@ HideWindowTitleBar = false
Each program's own `~/.config/SaraSuite/<Program>/Config.toml` has the same three keys plus
`UseSharedSettings` (default `true`). With it on, the program reads the shared file; with it
off, it uses its own values instead. SaraFem edits both in its Settings (the SaraSuite tab
for the shared file, the General tab for its own); for SaraSuite itself, edit its
`Config.toml` directly. Whichever program starts first creates the shared file from its own
values.
off, it uses its own values instead. Both SaraSuite (the gear button, Shared and SaraSuite
tabs) and SaraFem (its Settings, SaraSuite and General tabs) can edit them. Whichever program
starts first creates the shared file from its own values.
## SaraSuite's own settings
The gear button also holds the git host and, in one tab per program, what that program's
install dialog starts with: which option boxes are ticked and the default answers (e.g.
SaraSortd's root folder). They're stored in `~/.config/SaraSuite/SaraSuite/Config.toml` as
`[OptionDefaults.<Program>]` and `[AnswerDefaults.<Program>]`, and override the manifest's
defaults.
## Changing options after installing
Installed programs with options (e.g. SaraFem's file picker) get an **Options** button. Turning
an option off takes out what it placed and puts back any file it had replaced; turning it on
places it. It reapplies the installed commit, so the program itself isn't updated. The
**Update** button only shows up when there's an update (or a broken install to repair).
## Running and building
@@ -60,6 +74,8 @@ values.
SaraSuite-x86_64.AppImage list
SaraSuite-x86_64.AppImage install SaraSortd --set RootDir=~/Sorting --option Autostart=on
SaraSuite-x86_64.AppImage update SaraSortd
SaraSuite-x86_64.AppImage options SaraFem # list them
SaraSuite-x86_64.AppImage options SaraFem --option FilePicker=off
SaraSuite-x86_64.AppImage remove SaraSortd [--configs] [--keep-runtime]
```
+199
View File
@@ -0,0 +1,199 @@
"""Settings dialog: the shared SaraSuite settings, SaraSuite's own, and the
install defaults for each program. Laid out like SaraFem's Settings (a
category list on the left, the chosen category on the right)."""
import subprocess
from typing import Callable
import flet as ft
from Core import (
AppConfig, BundledFontFamily, ChildEnvironment, ChipButtonStyle, LoadSharedSettings, Palette,
SharedSettings, ThemesDir,
)
from Installer import DefaultAnswers, DefaultOptions
from Manifests import Program
DialogWidth = 700
DialogHeight = 480
CategoryListWidth = 150
RestartNote = "Theme, font and title bar changes apply the next time a program starts."
def _Note(Text: str) -> ft.Text:
return ft.Text(Text, color=Palette.Overlay0, size=12)
def _Themes() -> list[str]:
Names = {Path.stem for Path in ThemesDir.glob("*.toml")} if ThemesDir.is_dir() else set()
Names.add("CatppuccinMocha") # built in, even before SaraFem seeds the folder
return sorted(Names)
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 ShowSettingsDialog(Page: ft.Page, Config: AppConfig, Programs: list[Program], OnSaved: Callable[[], None]) -> None:
Themes = _Themes()
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],
)
# Shared: ~/.config/SaraSuite/Config.toml, read by every SaraSuite program.
Shared = LoadSharedSettings(Config)
UseSharedSwitch = ft.Switch(label="SaraSuite 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)
# SaraSuite'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("SaraSuite'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
GitHostField = ft.TextField(
value=Config.GitHost, label="Git host", hint_text="https://git.srhtnin.garden/sarah",
)
Categories: dict[str, ft.Control] = {
"Shared": ft.Column(
[
_Note("Shared by every SaraSuite program (~/.config/SaraSuite/Config.toml)."),
UseSharedSwitch,
ft.Divider(height=1, color=Palette.Surface1),
SharedTheme,
SharedFont,
SharedTitleBar,
],
spacing=12,
),
"SaraSuite": ft.Column(
[
GitHostField,
_Note("Where programs are installed and updated from; each program's repo is <Git host>/<Name>."),
ft.Divider(height=1, color=Palette.Surface1),
OwnAppearance,
],
spacing=12,
scroll=ft.ScrollMode.AUTO,
),
}
# One tab per program: what its install dialog starts with.
DefaultBoxes: dict[str, dict[str, ft.Checkbox]] = {}
DefaultFields: dict[str, dict[str, ft.TextField]] = {}
for Current in Programs:
if not Current.Options and not Current.Questions:
continue
Answers = DefaultAnswers(Current)
Ticked = DefaultOptions(Current)
DefaultFields[Current.Name] = {
Question.Id: ft.TextField(label=Question.Prompt, value=Answers[Question.Id]) for Question in Current.Questions
}
DefaultBoxes[Current.Name] = {
Option.Id: ft.Checkbox(label=Option.Label, value=Ticked[Option.Id]) for Option in Current.Options
}
Categories[Current.Name] = ft.Column(
[_Note(f"What installing {Current.Name} starts with.")]
+ list(DefaultFields[Current.Name].values())
+ list(DefaultBoxes[Current.Name].values()),
spacing=12,
scroll=ft.ScrollMode.AUTO,
)
DetailArea = ft.Container(Categories["Shared"], 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 == "Shared",
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)
def Save(e) -> None:
Config.UseSharedSettings = UseSharedSwitch.value
Config.Theme = OwnTheme.value
Config.FontFamily = OwnFont.value or Config.FontFamily
Config.HideWindowTitleBar = OwnTitleBar.value
Config.GitHost = (GitHostField.value or "").strip().rstrip("/") or Config.GitHost
for Name, Boxes in DefaultBoxes.items():
Config.OptionDefaults[Name] = {Id: bool(Box.value) for Id, Box in Boxes.items()}
for Name, Fields in DefaultFields.items():
if Fields:
Config.AnswerDefaults[Name] = {Id: (Field.value or "").strip() for Id, Field in Fields.items()}
Config.Save()
SharedSettings(
Theme=SharedTheme.value,
FontFamily=SharedFont.value or Shared.FontFamily,
HideWindowTitleBar=SharedTitleBar.value,
).Save()
Page.pop_dialog()
OnSaved()
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,
),
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),
],
))