mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-13 18:28:14 +02:00
Client: your server settings, profile, account and admin in Settings
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e2cb25e2e1
commit
c7a7596e99
3 files changed
+401
-2
No files matched your search
@@ -0,0 +1,312 @@
|
|||||||
|
"""Your SaraView account and settings on the server, in Settings while you're
|
||||||
|
logged in to one: the web view's Settings panel (Frontend/src/Components/
|
||||||
|
SettingsPanel.jsx), section for section, through the same /api endpoints.
|
||||||
|
|
||||||
|
- Web view: your settings on the server (saved with Settings' Save). Most
|
||||||
|
are how the web view looks; hidden files and .sv_ notes also apply here.
|
||||||
|
- Profile (colour, avatar), Account (username, password, log out) and, for
|
||||||
|
admins, Users, Groups and Locked directories: these take effect at once,
|
||||||
|
like the web view's."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import flet as ft
|
||||||
|
|
||||||
|
from Core import ChipButton, Palette
|
||||||
|
from Diagrams import ThemePalette
|
||||||
|
from Sources import ServerSource, SourceError
|
||||||
|
|
||||||
|
|
||||||
|
def _Note(Text: str) -> ft.Text:
|
||||||
|
return ft.Text(Text, size=12, color=Palette.Overlay0)
|
||||||
|
|
||||||
|
|
||||||
|
def _Heading(Text: str) -> ft.Text:
|
||||||
|
return ft.Text(Text, color=Palette.Mauve)
|
||||||
|
|
||||||
|
|
||||||
|
def _Divider() -> ft.Divider:
|
||||||
|
return ft.Divider(height=1, color=Palette.Surface1)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerSections:
|
||||||
|
"""Builds the sections (Categories) and saves the Web view ones (Save)."""
|
||||||
|
|
||||||
|
def __init__(self, TheApp, OnLoggedOut: Callable[[], None]):
|
||||||
|
self.TheApp = TheApp
|
||||||
|
self.Server: ServerSource = TheApp.Server
|
||||||
|
self.OnLoggedOut = OnLoggedOut
|
||||||
|
self.Values = self.Server.UserSettings() # raises SourceError: then there are no sections
|
||||||
|
self.ThemeNames = self.Server.ThemeNames()
|
||||||
|
self.IsAdmin = bool((self.Server.User or {}).get("IsAdmin"))
|
||||||
|
self.Switches: dict[str, ft.Switch] = {}
|
||||||
|
self.Categories: dict[str, ft.Control] = {
|
||||||
|
"Web view": self._WebView(),
|
||||||
|
"Profile": self._Profile(),
|
||||||
|
"Account": self._Account(),
|
||||||
|
}
|
||||||
|
if self.IsAdmin:
|
||||||
|
self.UsersList = ft.Column(spacing=6)
|
||||||
|
self.GroupsList = ft.Column(spacing=10)
|
||||||
|
self.LocksList = ft.Column(spacing=6)
|
||||||
|
self.Categories["Users"] = self._Users()
|
||||||
|
self.Categories["Groups"] = self._Groups()
|
||||||
|
self.Categories["Locked directories"] = self._Locks()
|
||||||
|
self._RefreshAdmin(Update=False)
|
||||||
|
|
||||||
|
def _Try(self, Action: Callable[[], None], Failure: str) -> bool:
|
||||||
|
try:
|
||||||
|
Action()
|
||||||
|
return True
|
||||||
|
except SourceError as Error:
|
||||||
|
self.TheApp.Notify(f"{Failure}: {Error}", "error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# --- Web view (saved with Settings' Save) -------------------------------
|
||||||
|
|
||||||
|
def _Switch(self, Key: str, Label: str) -> ft.Switch:
|
||||||
|
self.Switches[Key] = ft.Switch(label=Label, value=bool(self.Values.get(Key)))
|
||||||
|
return self.Switches[Key]
|
||||||
|
|
||||||
|
def _WebView(self) -> ft.Control:
|
||||||
|
V = self.Values
|
||||||
|
self.MarkdownEditing = ft.Dropdown(label="Markdown editing", value=V.get("MarkdownEditing", "Live"), options=[
|
||||||
|
ft.DropdownOption(key="Live", text="Live: edit the Markdown where you click in the rendered file"),
|
||||||
|
ft.DropdownOption(key="Separate", text="Separate: View and Edit are two modes")])
|
||||||
|
self.ThemePicker = ft.Dropdown(label="Colour theme", value=V.get("ThemeName"),
|
||||||
|
options=[ft.DropdownOption(key=Name, text=Name) for Name in self.ThemeNames])
|
||||||
|
self.NewThemeName = ft.TextField(label="New theme name (a copy of the selected one)", dense=True, expand=True)
|
||||||
|
self.FontFamily = ft.TextField(label="Font family", value=V.get("FontFamily", ""))
|
||||||
|
self.FontSize = ft.TextField(label="Font size (px)", value=str(V.get("FontSize", 14)),
|
||||||
|
keyboard_type=ft.KeyboardType.NUMBER)
|
||||||
|
self.PanButton = ft.Dropdown(label="Graph: pan with mouse button", value=V.get("GraphPanButton", "Right"), options=[
|
||||||
|
ft.DropdownOption(key="Right", text="Right (left-click opens a dot)"),
|
||||||
|
ft.DropdownOption(key="Left", text="Left (right-click opens a dot)")])
|
||||||
|
return ft.Column([
|
||||||
|
_Note("Your settings on the server, the same as the web view's. Most change the web view; "
|
||||||
|
"hidden directories and .sv_ notes also apply here while connected."),
|
||||||
|
_Heading("Files"),
|
||||||
|
self._Switch("ShowHiddenFolders", "Show hidden directories"),
|
||||||
|
self._Switch("ShowSvFiles", "Show directory note files (.sv_*)"),
|
||||||
|
self._Switch("ShowBreadcrumbs", "Show breadcrumbs"),
|
||||||
|
self._Switch("UseHyphenBullets", "Hyphen file icons (-)"),
|
||||||
|
self._Switch("UseAsciiFolderIcons", "ASCII directory icons (>/v)"),
|
||||||
|
self._Switch("NavigateIntoDirectories", "Open directory file on click"),
|
||||||
|
self._Switch("NewEntryButtonsAtTop", "New File/Directory buttons at top of sidebar"),
|
||||||
|
self._Switch("ConfirmBeforeDelete", "Confirm before delete"),
|
||||||
|
_Divider(), _Heading("Editor"),
|
||||||
|
self.MarkdownEditing,
|
||||||
|
self._Switch("StickyToolbar", "Sticky edit/view/save toolbar"),
|
||||||
|
self._Switch("ShowGitBlame", "Show blame gutter (who last changed each line)"),
|
||||||
|
self._Switch("ShowBlameAvatars", "Show avatars instead of names in blame"),
|
||||||
|
self._Switch("VisibleInBlame", "Show my edits in others' blame view"),
|
||||||
|
_Divider(), _Heading("Appearance"),
|
||||||
|
self.ThemePicker,
|
||||||
|
ft.Row([self.NewThemeName, ChipButton("Create", lambda e: self._CreateTheme())]),
|
||||||
|
ft.Row([ChipButton("Delete selected theme", lambda e: self._DeleteTheme(), Accent=Palette.Red)]),
|
||||||
|
self.FontFamily, self.FontSize,
|
||||||
|
_Divider(), _Heading("Graph"),
|
||||||
|
self._Switch("GraphShowDirectories", "Show directories (off: only files and the links between them)"),
|
||||||
|
self._Switch("GraphColourByDepth", "Colour directories by depth (off: all purple)"),
|
||||||
|
self._Switch("GraphShrinkByDepth", "Smaller directory dots the deeper they are"),
|
||||||
|
self.PanButton,
|
||||||
|
], spacing=12, scroll=ft.ScrollMode.AUTO)
|
||||||
|
|
||||||
|
def _RefreshThemes(self, Selected: str) -> None:
|
||||||
|
self.ThemeNames = self.Server.ThemeNames()
|
||||||
|
self.ThemePicker.options = [ft.DropdownOption(key=Name, text=Name) for Name in self.ThemeNames]
|
||||||
|
self.ThemePicker.value = Selected if Selected in self.ThemeNames else self.ThemeNames[0]
|
||||||
|
self.ThemePicker.update()
|
||||||
|
|
||||||
|
def _CreateTheme(self) -> None:
|
||||||
|
Name = (self.NewThemeName.value or "").strip()
|
||||||
|
Base = self.ThemePicker.value
|
||||||
|
if not Name or not Base:
|
||||||
|
return
|
||||||
|
if self._Try(lambda: self.Server.SaveTheme(Name, self.Server.Theme(Base)), "Couldn't create the theme"):
|
||||||
|
self.NewThemeName.value = ""
|
||||||
|
self.NewThemeName.update()
|
||||||
|
self._RefreshThemes(Name)
|
||||||
|
|
||||||
|
def _DeleteTheme(self) -> None:
|
||||||
|
Name = self.ThemePicker.value
|
||||||
|
if not Name:
|
||||||
|
return
|
||||||
|
if len(self.ThemeNames) <= 1:
|
||||||
|
self.TheApp.Notify("Can't delete the only remaining theme.", "error")
|
||||||
|
return
|
||||||
|
if self._Try(lambda: self.Server.DeleteTheme(Name), "Couldn't delete the theme"):
|
||||||
|
self._RefreshThemes("")
|
||||||
|
|
||||||
|
def Save(self) -> bool:
|
||||||
|
"""Saves the Web view section; False (after saying why) if it couldn't."""
|
||||||
|
Values = dict(self.Values)
|
||||||
|
for Key, Switch in self.Switches.items():
|
||||||
|
Values[Key] = bool(Switch.value)
|
||||||
|
Values["MarkdownEditing"] = self.MarkdownEditing.value or Values.get("MarkdownEditing", "Live")
|
||||||
|
Values["ThemeName"] = self.ThemePicker.value or Values.get("ThemeName")
|
||||||
|
Values["FontFamily"] = (self.FontFamily.value or "").strip() or Values.get("FontFamily")
|
||||||
|
try:
|
||||||
|
Values["FontSize"] = max(8, min(40, int(self.FontSize.value)))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
Values["GraphPanButton"] = self.PanButton.value or Values.get("GraphPanButton", "Right")
|
||||||
|
return self._Try(lambda: self.Server.SaveUserSettings(Values), "Couldn't save your server settings")
|
||||||
|
|
||||||
|
# --- Profile and Account (at once) ---------------------------------------
|
||||||
|
|
||||||
|
def _Profile(self) -> ft.Control:
|
||||||
|
Colours = ThemePalette()
|
||||||
|
Current = (self.Server.User or {}).get("Colour", "Purple")
|
||||||
|
self.Swatches = ft.Row(wrap=True, spacing=8, run_spacing=8)
|
||||||
|
|
||||||
|
def Pick(Colour: str) -> None:
|
||||||
|
if self._Try(lambda: self.Server.SetColour(Colour), "Couldn't change your colour"):
|
||||||
|
Draw(Colour)
|
||||||
|
self.Swatches.update()
|
||||||
|
|
||||||
|
def Draw(Selected: str) -> None:
|
||||||
|
self.Swatches.controls = [
|
||||||
|
ft.Container(width=28, height=28, border_radius=14, bgcolor=Colours.get(Name, Palette.Mauve),
|
||||||
|
border=ft.Border.all(3, Palette.Text) if Name == Selected else None,
|
||||||
|
tooltip=Name, on_click=lambda e, N=Name: Pick(N))
|
||||||
|
for Name in self.Server.ColourOptions()
|
||||||
|
]
|
||||||
|
|
||||||
|
Draw(Current)
|
||||||
|
|
||||||
|
async def PickAvatar(e) -> None:
|
||||||
|
Picked = await self.TheApp.Picker.pick_files(dialog_title="Avatar", allow_multiple=False,
|
||||||
|
file_type=ft.FilePickerFileType.IMAGE)
|
||||||
|
if Picked and Picked[0].path:
|
||||||
|
if self._Try(lambda: self.Server.UploadAvatar(Path(Picked[0].path)), "Couldn't upload the avatar"):
|
||||||
|
self.TheApp.Notify("Avatar updated.", "success")
|
||||||
|
|
||||||
|
return ft.Column([
|
||||||
|
ft.Text("Your colour"), _Note("Shown on your edits in blame, and on your badge."), self.Swatches,
|
||||||
|
_Divider(), ft.Text("Avatar"), ChipButton("Choose an image…", PickAvatar, Icon=ft.Icons.IMAGE_OUTLINED),
|
||||||
|
], spacing=12, scroll=ft.ScrollMode.AUTO)
|
||||||
|
|
||||||
|
def _Account(self) -> ft.Control:
|
||||||
|
User = self.Server.User or {}
|
||||||
|
UsernameField = ft.TextField(label="Username", value=User.get("Username", ""))
|
||||||
|
CurrentPassword = ft.TextField(label="Current password", password=True, can_reveal_password=True)
|
||||||
|
NewPassword = ft.TextField(label="New password", password=True, can_reveal_password=True)
|
||||||
|
|
||||||
|
def ChangeName(e) -> None:
|
||||||
|
Name = (UsernameField.value or "").strip()
|
||||||
|
if Name and Name != (self.Server.User or {}).get("Username") and \
|
||||||
|
self._Try(lambda: self.Server.ChangeUsername(Name), "Couldn't change your username"):
|
||||||
|
self.TheApp.Notify(f"You're now {Name}.", "success")
|
||||||
|
|
||||||
|
def ChangePassword(e) -> None:
|
||||||
|
if self._Try(lambda: self.Server.ChangePassword(CurrentPassword.value or "", NewPassword.value or ""),
|
||||||
|
"Couldn't change your password"):
|
||||||
|
CurrentPassword.value = NewPassword.value = ""
|
||||||
|
CurrentPassword.update()
|
||||||
|
NewPassword.update()
|
||||||
|
self.TheApp.Notify("Password changed.", "success")
|
||||||
|
|
||||||
|
return ft.Column([
|
||||||
|
_Note(f"Logged in to {self.Server.Url} as {User.get('Username', '?')}"
|
||||||
|
+ (" (admin)." if self.IsAdmin else ".")),
|
||||||
|
ft.Row([ft.Container(UsernameField, expand=True), ChipButton("Change", ChangeName)]),
|
||||||
|
_Divider(),
|
||||||
|
CurrentPassword, NewPassword, ft.Row([ChipButton("Change password", ChangePassword)]),
|
||||||
|
_Divider(),
|
||||||
|
ft.Row([ChipButton("Log out", lambda e: self.OnLoggedOut(), Icon=ft.Icons.LOGOUT, Accent=Palette.Red)]),
|
||||||
|
], spacing=12, scroll=ft.ScrollMode.AUTO)
|
||||||
|
|
||||||
|
# --- Admin (at once) ------------------------------------------------------
|
||||||
|
|
||||||
|
def _RefreshAdmin(self, Update: bool = True) -> None:
|
||||||
|
try:
|
||||||
|
Users, Groups, Locks = self.Server.Users(), self.Server.Groups(), self.Server.Locks()
|
||||||
|
except SourceError as Error:
|
||||||
|
self.TheApp.Notify(f"Couldn't load the admin lists: {Error}", "error")
|
||||||
|
return
|
||||||
|
Colours = ThemePalette()
|
||||||
|
self.UsersList.controls = [
|
||||||
|
ft.Row([ft.Icon(ft.Icons.CIRCLE, size=12, color=Colours.get(U.get("Colour", "Purple"), Palette.Mauve)),
|
||||||
|
ft.Text(U["Username"]), ft.Text("admin", color=Palette.Overlay0, size=12) if U.get("IsAdmin") else ft.Container()],
|
||||||
|
spacing=8)
|
||||||
|
for U in Users
|
||||||
|
]
|
||||||
|
Names = [U["Username"] for U in Users]
|
||||||
|
self.GroupsList.controls = [self._GroupRow(G, Names) for G in Groups] or [_Note("No groups yet.")]
|
||||||
|
self.LocksList.controls = [
|
||||||
|
ft.Row([ft.Text(f"{L.get('Root')}/{L.get('Path')}", expand=True),
|
||||||
|
ft.Text(", ".join([*(L.get("Users") or []), *(f"@{G}" for G in L.get("Groups") or [])]),
|
||||||
|
color=Palette.Overlay0, size=12),
|
||||||
|
ChipButton("Unlock", lambda e, L=L: self._Admin(lambda: self.Server.RemoveLock(L["Root"], L["Path"]),
|
||||||
|
"Couldn't unlock"))])
|
||||||
|
for L in Locks
|
||||||
|
] or [_Note("Nothing is locked.")]
|
||||||
|
if Update:
|
||||||
|
for Listing in (self.UsersList, self.GroupsList, self.LocksList):
|
||||||
|
try:
|
||||||
|
Listing.update()
|
||||||
|
except RuntimeError:
|
||||||
|
pass # its section isn't open: it shows the new list when it is
|
||||||
|
|
||||||
|
def _Admin(self, Action: Callable[[], None], Failure: str) -> None:
|
||||||
|
if self._Try(Action, Failure):
|
||||||
|
self._RefreshAdmin()
|
||||||
|
|
||||||
|
def _GroupRow(self, Group: dict, Usernames: list[str]) -> ft.Control:
|
||||||
|
Members = Group.get("Members") or []
|
||||||
|
|
||||||
|
def Toggle(Name: str) -> None:
|
||||||
|
New = [M for M in Members if M != Name] if Name in Members else [*Members, Name]
|
||||||
|
self._Admin(lambda: self.Server.SetGroupMembers(Group["Name"], New), "Couldn't change the group")
|
||||||
|
|
||||||
|
Chips = [ft.Container(ft.Text(Name, size=12, color=Palette.Mauve if Name in Members else Palette.Overlay0),
|
||||||
|
bgcolor=Palette.Surface0 if Name in Members else None,
|
||||||
|
border=ft.Border.all(1, Palette.Surface1), border_radius=4,
|
||||||
|
padding=ft.Padding.symmetric(horizontal=6, vertical=2),
|
||||||
|
tooltip="Click to add or remove", on_click=lambda e, N=Name: Toggle(N))
|
||||||
|
for Name in Usernames]
|
||||||
|
return ft.Column([
|
||||||
|
ft.Row([ft.Text(Group["Name"], expand=True),
|
||||||
|
ChipButton("Delete", lambda e: self.TheApp.Confirm(
|
||||||
|
"Delete group", f'Delete group "{Group["Name"]}"? This can\'t be undone.',
|
||||||
|
lambda: self._Admin(lambda: self.Server.DeleteGroup(Group["Name"]), "Couldn't delete the group")),
|
||||||
|
Accent=Palette.Red)]),
|
||||||
|
ft.Row(Chips, wrap=True, spacing=4, run_spacing=4),
|
||||||
|
], spacing=4)
|
||||||
|
|
||||||
|
def _Users(self) -> ft.Control:
|
||||||
|
Name = ft.TextField(label="Username", dense=True)
|
||||||
|
Password = ft.TextField(label="Password", password=True, can_reveal_password=True, dense=True)
|
||||||
|
IsAdmin = ft.Switch(label="Admin", value=False)
|
||||||
|
Colour = ft.Dropdown(label="Colour", value="Purple", dense=True,
|
||||||
|
options=[ft.DropdownOption(key=C, text=C) for C in self.Server.ColourOptions()])
|
||||||
|
|
||||||
|
def Create(e) -> None:
|
||||||
|
if (Name.value or "").strip():
|
||||||
|
self._Admin(lambda: self.Server.CreateUser(Name.value.strip(), Password.value or "", bool(IsAdmin.value),
|
||||||
|
Colour.value or "Purple"), "Couldn't create the user")
|
||||||
|
Name.value = Password.value = ""
|
||||||
|
Name.update()
|
||||||
|
Password.update()
|
||||||
|
|
||||||
|
return ft.Column([self.UsersList, _Divider(), ft.Text("New user"), Name, Password, ft.Row([IsAdmin, Colour]),
|
||||||
|
ft.Row([ChipButton("Create user", Create)])], spacing=12, scroll=ft.ScrollMode.AUTO)
|
||||||
|
|
||||||
|
def _Groups(self) -> ft.Control:
|
||||||
|
Name = ft.TextField(label="New group name", dense=True, expand=True)
|
||||||
|
|
||||||
|
def Create(e) -> None:
|
||||||
|
if (Name.value or "").strip():
|
||||||
|
self._Admin(lambda: self.Server.CreateGroup(Name.value.strip()), "Couldn't create the group")
|
||||||
|
Name.value = ""
|
||||||
|
Name.update()
|
||||||
|
|
||||||
|
return ft.Column([_Note("Click a name to add it to the group or take it out."), self.GroupsList,
|
||||||
|
_Divider(), ft.Row([Name, ChipButton("Create group", Create)])],
|
||||||
|
spacing=12, scroll=ft.ScrollMode.AUTO)
|
||||||
|
|
||||||
|
def _Locks(self) -> ft.Control:
|
||||||
|
return ft.Column([_Note("Directories only some users or groups can see."), self.LocksList],
|
||||||
|
spacing=12, scroll=ft.ScrollMode.AUTO)
|
||||||
+26
-2
@@ -15,6 +15,8 @@ from Core import (
|
|||||||
AppConfig, AssetsDir, AvailableThemes, ChipButton, DataHome, ExportSettings, Home, HumanSize,
|
AppConfig, AssetsDir, AvailableThemes, ChipButton, DataHome, ExportSettings, Home, HumanSize,
|
||||||
ImportSettings, InstalledFontFamilies, LoadSharedSettings, Palette, SharedSettings,
|
ImportSettings, InstalledFontFamilies, LoadSharedSettings, Palette, SharedSettings,
|
||||||
)
|
)
|
||||||
|
from ServerSettings import ServerSections
|
||||||
|
from Sources import SourceError
|
||||||
|
|
||||||
DialogWidth = 700
|
DialogWidth = 700
|
||||||
DialogHeight = 520
|
DialogHeight = 520
|
||||||
@@ -252,6 +254,23 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
|
|||||||
], spacing=12, scroll=ft.ScrollMode.AUTO),
|
], spacing=12, scroll=ft.ScrollMode.AUTO),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Logged in to a server: your account and server settings too, like the
|
||||||
|
# web view's Settings (ServerSettings.py).
|
||||||
|
Server = TheApp.Server
|
||||||
|
Extra = None
|
||||||
|
if Server is not None and Server.User is not None and not TheApp.ConnectError:
|
||||||
|
def LogOut() -> None:
|
||||||
|
TheApp.Close()
|
||||||
|
TheApp.Logout()
|
||||||
|
|
||||||
|
try:
|
||||||
|
Extra = ServerSections(TheApp, LogOut)
|
||||||
|
Categories.update(Extra.Categories)
|
||||||
|
except SourceError as Error:
|
||||||
|
TheApp.Notify(f"Couldn't load your server settings: {Error}", "error")
|
||||||
|
if StartCategory not in Categories:
|
||||||
|
StartCategory = "General"
|
||||||
|
|
||||||
DetailArea = ft.Container(Categories[StartCategory], expand=True, padding=ft.Padding.only(left=12))
|
DetailArea = ft.Container(Categories[StartCategory], expand=True, padding=ft.Padding.only(left=12))
|
||||||
CategoryTiles: dict[str, ft.ListTile] = {}
|
CategoryTiles: dict[str, ft.ListTile] = {}
|
||||||
|
|
||||||
@@ -264,12 +283,16 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
|
|||||||
|
|
||||||
for Name in Categories:
|
for Name in Categories:
|
||||||
CategoryTiles[Name] = ft.ListTile(
|
CategoryTiles[Name] = ft.ListTile(
|
||||||
title=Name, selected=Name == StartCategory, selected_tile_color=Palette.Surface1,
|
title=Name, selected=Name == StartCategory, dense=True, # room for the server sections too selected_tile_color=Palette.Surface1,
|
||||||
selected_color=Palette.Mauve, on_click=lambda e, N=Name: SelectCategory(N),
|
selected_color=Palette.Mauve, on_click=lambda e, N=Name: SelectCategory(N),
|
||||||
)
|
)
|
||||||
CategoryList = ft.Column(list(CategoryTiles.values()), spacing=0, scroll=ft.ScrollMode.AUTO)
|
CategoryList = ft.Column(list(CategoryTiles.values()), spacing=0, scroll=ft.ScrollMode.AUTO)
|
||||||
|
|
||||||
def Save(e) -> None:
|
def Save(e) -> None:
|
||||||
|
ServerFilesBefore = Extra and (Extra.Values.get("ShowHiddenFolders"), Extra.Values.get("ShowSvFiles"))
|
||||||
|
if Extra is not None and not Extra.Save():
|
||||||
|
return # stays open, so nothing's lost
|
||||||
|
ServerFilesAfter = Extra and (Extra.Switches["ShowHiddenFolders"].value, Extra.Switches["ShowSvFiles"].value)
|
||||||
SourceBefore = (Config.Mode, Config.LocalRoot, Config.ServerUrl, Config.ShowHiddenFolders,
|
SourceBefore = (Config.Mode, Config.LocalRoot, Config.ServerUrl, Config.ShowHiddenFolders,
|
||||||
Config.ShowSvFiles, Config.HideInternalFiles)
|
Config.ShowSvFiles, Config.HideInternalFiles)
|
||||||
Config.UseSharedSettings = UseSharedSwitch.value
|
Config.UseSharedSettings = UseSharedSwitch.value
|
||||||
@@ -305,7 +328,8 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
|
|||||||
SourceAfter = (Config.Mode, Config.LocalRoot, Config.ServerUrl, Config.ShowHiddenFolders,
|
SourceAfter = (Config.Mode, Config.LocalRoot, Config.ServerUrl, Config.ShowHiddenFolders,
|
||||||
Config.ShowSvFiles, Config.HideInternalFiles)
|
Config.ShowSvFiles, Config.HideInternalFiles)
|
||||||
TheApp.Close()
|
TheApp.Close()
|
||||||
OnSaved(SourceAfter != SourceBefore)
|
# A server's hidden-file settings change what it lists: reload like a new source.
|
||||||
|
OnSaved(SourceAfter != SourceBefore or ServerFilesAfter != ServerFilesBefore)
|
||||||
TheApp.Notify("Settings saved.", "success")
|
TheApp.Notify("Settings saved.", "success")
|
||||||
if RemoveDiagramTools:
|
if RemoveDiagramTools:
|
||||||
def RemoveTools() -> None:
|
def RemoveTools() -> None:
|
||||||
|
|||||||
@@ -480,6 +480,69 @@ class ServerSource(Source):
|
|||||||
# A big root takes the server a while to read through.
|
# A big root takes the server a while to read through.
|
||||||
return self._Json("GET", "/graph", params=self._At(Root, RelativePath), timeout=120)
|
return self._Json("GET", "/graph", params=self._At(Root, RelativePath), timeout=120)
|
||||||
|
|
||||||
|
# --- Your account and settings on the server (ServerSettings.py) ------
|
||||||
|
# The same /api endpoints as the web view's Settings panel.
|
||||||
|
|
||||||
|
def UserSettings(self) -> dict:
|
||||||
|
return self._Json("GET", "/settings")
|
||||||
|
|
||||||
|
def SaveUserSettings(self, Settings: dict) -> dict:
|
||||||
|
return self._Json("PUT", "/settings", json=Settings)
|
||||||
|
|
||||||
|
def ThemeNames(self) -> list[str]:
|
||||||
|
return self._Json("GET", "/themes")
|
||||||
|
|
||||||
|
def Theme(self, Name: str) -> dict:
|
||||||
|
return self._Json("GET", "/theme", params={"Name": Name})
|
||||||
|
|
||||||
|
def SaveTheme(self, Name: str, Palette: dict) -> None:
|
||||||
|
self._Json("PUT", "/theme", params={"Name": Name}, json={"Palette": Palette})
|
||||||
|
|
||||||
|
def DeleteTheme(self, Name: str) -> None:
|
||||||
|
self._Json("DELETE", "/theme", params={"Name": Name})
|
||||||
|
|
||||||
|
def ColourOptions(self) -> list[str]:
|
||||||
|
return self._Json("GET", "/profile/colours")
|
||||||
|
|
||||||
|
def SetColour(self, Colour: str) -> dict:
|
||||||
|
self.User = {**(self.User or {}), **self._Json("PUT", "/profile/colour", json={"Colour": Colour})}
|
||||||
|
return self.User
|
||||||
|
|
||||||
|
def UploadAvatar(self, LocalFile: Path) -> None:
|
||||||
|
with LocalFile.open("rb") as Handle:
|
||||||
|
self._Json("POST", "/profile/avatar", files=[("Avatar", (LocalFile.name, Handle))])
|
||||||
|
|
||||||
|
def ChangeUsername(self, NewUsername: str) -> dict:
|
||||||
|
self.User = {**(self.User or {}), **self._Json("PUT", "/profile/username", json={"NewUsername": NewUsername})}
|
||||||
|
return self.User
|
||||||
|
|
||||||
|
def ChangePassword(self, CurrentPassword: str, NewPassword: str) -> None:
|
||||||
|
self._Json("PUT", "/profile/password", json={"CurrentPassword": CurrentPassword, "NewPassword": NewPassword})
|
||||||
|
|
||||||
|
def Users(self) -> list[dict]:
|
||||||
|
return self._Json("GET", "/users")
|
||||||
|
|
||||||
|
def CreateUser(self, Username: str, Password: str, IsAdmin: bool, Colour: str) -> None:
|
||||||
|
self._Json("POST", "/users", json={"Username": Username, "Password": Password, "IsAdmin": IsAdmin, "Colour": Colour})
|
||||||
|
|
||||||
|
def Groups(self) -> list[dict]:
|
||||||
|
return self._Json("GET", "/groups")
|
||||||
|
|
||||||
|
def CreateGroup(self, Name: str) -> None:
|
||||||
|
self._Json("POST", "/groups", json={"Name": Name})
|
||||||
|
|
||||||
|
def DeleteGroup(self, Name: str) -> None:
|
||||||
|
self._Json("DELETE", "/groups", params={"Name": Name})
|
||||||
|
|
||||||
|
def SetGroupMembers(self, Name: str, Members: list[str]) -> None:
|
||||||
|
self._Json("PUT", "/groups/members", params={"Name": Name}, json={"Members": Members})
|
||||||
|
|
||||||
|
def Locks(self) -> list[dict]:
|
||||||
|
return self._Json("GET", "/locks")
|
||||||
|
|
||||||
|
def RemoveLock(self, Root: str, RelativePath: str) -> None:
|
||||||
|
self._Json("DELETE", "/lock", params=self._At(Root, RelativePath))
|
||||||
|
|
||||||
def Diagram(self, Source: str, ThemePalette: dict[str, str], FontSize: int) -> bytes:
|
def Diagram(self, Source: str, ThemePalette: dict[str, str], FontSize: int) -> bytes:
|
||||||
"""A ```mermaid block as a PNG, drawn by the server (Diagrams.py)."""
|
"""A ```mermaid block as a PNG, drawn by the server (Diagrams.py)."""
|
||||||
Body = {"Source": Source, "Palette": ThemePalette, "FontSize": FontSize}
|
Body = {"Source": Source, "Palette": ThemePalette, "FontSize": FontSize}
|
||||||
|
|||||||
Reference in new issue
Block a user