mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-13 18:28:14 +02:00
Client: diagrams in local folders (setting)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f99b227cc3
commit
db81507b7c
4 files changed
+374
-7
No files matched your search
@@ -190,6 +190,9 @@ class AppConfig:
|
||||
# Double-click on a file: True opens it in SaraView's own viewer, False
|
||||
# hands it to the system's default program. The menu always offers the other.
|
||||
OpenFilesInApp: bool = True
|
||||
# Draw ```mermaid diagrams in local folders (LocalDiagrams.py); off removes
|
||||
# what it downloaded. On a server, the server draws them either way.
|
||||
LocalDiagrams: bool = True
|
||||
|
||||
def Save(self) -> None:
|
||||
ConfigDir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
+31
-5
@@ -1,7 +1,8 @@
|
||||
"""Mermaid diagrams (```mermaid blocks), like the web view's. Flet can't draw
|
||||
them, so the server does (POST /api/diagram, Backend/Diagrams.py) and they're
|
||||
shown as pictures in this client's theme. In a local folder (no server), or
|
||||
if the server can't draw them, they stay code blocks.
|
||||
shown as pictures in this client's theme. In a local folder, LocalDiagrams.py
|
||||
draws them on this computer instead (Settings > General, on by default). If
|
||||
neither can, they stay code blocks.
|
||||
|
||||
A DiagramBlock shows its code straight away and swaps in the picture once it
|
||||
arrives (fetched on a background thread), so opening a note never waits on
|
||||
@@ -14,6 +15,7 @@ from typing import Callable, Optional
|
||||
|
||||
import flet as ft
|
||||
|
||||
import LocalDiagrams
|
||||
from Core import CodeStyleSheet, MarkdownCodeTheme, MarkdownSheet, Palette
|
||||
from Sources import SourceError
|
||||
|
||||
@@ -51,8 +53,14 @@ def ThemePalette() -> dict[str, str]:
|
||||
}
|
||||
|
||||
|
||||
def IsLocal(TheApp) -> bool:
|
||||
return TheApp.Source.Kind != "Server"
|
||||
|
||||
|
||||
def CanDraw(TheApp) -> bool:
|
||||
return TheApp.Source.Kind == "Server" and not ServerCantDraw
|
||||
if IsLocal(TheApp):
|
||||
return TheApp.Config.LocalDiagrams
|
||||
return not ServerCantDraw
|
||||
|
||||
|
||||
class DiagramBlock(ft.Container):
|
||||
@@ -83,14 +91,21 @@ class DiagramBlock(ft.Container):
|
||||
alignment=ft.MainAxisAlignment.CENTER, scroll=ft.ScrollMode.AUTO)
|
||||
Problem = Problems.get(self.Key)
|
||||
if Problem is not None:
|
||||
return ft.Column([self._Code(), ft.Text(f"Diagram error: {Problem}", color=Palette.Red,
|
||||
size=self.FontSize - 2, selectable=True)], spacing=4)
|
||||
return self._WithNote(f"Diagram error: {Problem}", Palette.Red)
|
||||
if self.Fetching and IsLocal(self.TheApp) and not LocalDiagrams.IsSetUp():
|
||||
return self._WithNote("Setting up diagrams (first time only, can take a few minutes)…", Palette.Overlay0)
|
||||
return self._Code()
|
||||
|
||||
def _WithNote(self, Note: str, Colour: str) -> ft.Control:
|
||||
return ft.Column([self._Code(), ft.Text(Note, color=Colour, size=self.FontSize - 2, selectable=True)], spacing=4)
|
||||
|
||||
def did_mount(self) -> None:
|
||||
self.Mounted = True
|
||||
if self.Key not in Pictures and self.Key not in Problems and not self.Fetching and CanDraw(self.TheApp):
|
||||
self.Fetching = True
|
||||
if IsLocal(self.TheApp) and not LocalDiagrams.IsSetUp():
|
||||
self.content = self._Content()
|
||||
self.update()
|
||||
self.page.run_thread(self._Fetch)
|
||||
|
||||
def will_unmount(self) -> None:
|
||||
@@ -99,13 +114,24 @@ class DiagramBlock(ft.Container):
|
||||
def _Fetch(self) -> None:
|
||||
global ServerCantDraw
|
||||
try:
|
||||
if IsLocal(self.TheApp):
|
||||
Pictures[self.Key] = LocalDiagrams.Render(self.Source, ThemePalette(), self.FontSize)
|
||||
else:
|
||||
Pictures[self.Key] = self.TheApp.Source.Diagram(self.Source, ThemePalette(), self.FontSize)
|
||||
except LocalDiagrams.DiagramError as Error:
|
||||
Problems[self.Key] = str(Error)
|
||||
except SourceError as Error:
|
||||
if Error.Status == 422:
|
||||
Problems[self.Key] = str(Error)
|
||||
elif Error.Status in (404, 503):
|
||||
ServerCantDraw = True # an older server, or one without Chromium: stay code blocks
|
||||
except Exception as Error:
|
||||
# Can't draw on this computer (setup failed, or it timed out): say why.
|
||||
Problems[self.Key] = f"Can't draw diagrams here: {Error or type(Error).__name__}"
|
||||
finally:
|
||||
self._Finish()
|
||||
|
||||
def _Finish(self) -> None:
|
||||
self.Fetching = False
|
||||
if self.Mounted:
|
||||
self.content = self._Content()
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
"""Drawing Mermaid diagrams in a local folder, where there's no server to do
|
||||
it (Diagrams.py): the same way the server's Backend/Diagrams.py does, with
|
||||
Mermaid in a headless Chromium driven by Playwright.
|
||||
|
||||
Settings > General > "Draw diagrams in local folders" (Config.LocalDiagrams,
|
||||
on by default). Nothing for this ships with SaraView: the first diagram sets
|
||||
up what's needed, all of it in ToolsDir (~/.local/share/SaraView/DiagramTools):
|
||||
- Playwright, installed with pip into ToolsDir/Python;
|
||||
- Mermaid, the version the web view uses, downloaded from npm and checked;
|
||||
- a headless Chromium (ToolsDir/Browsers), only when no Chromium-based
|
||||
browser (Chrome, Chromium, Brave, Edge) is installed or it won't start.
|
||||
An installed one is used as it is, never changed.
|
||||
Turning the setting off deletes ToolsDir: all of that, and nothing else.
|
||||
|
||||
Playwright's sync API has to stay on the thread that started it, so one
|
||||
worker thread does all the drawing; Render() hands it a job and waits."""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import queue
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import threading
|
||||
from concurrent.futures import Future
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from Core import AssetsDir, ChildEnvironment, DataHome, AppName, IsWindows
|
||||
|
||||
ToolsDir = DataHome / AppName / "DiagramTools"
|
||||
PythonDir = ToolsDir / "Python"
|
||||
BrowsersDir = ToolsDir / "Browsers"
|
||||
MermaidPath = ToolsDir / "mermaid.min.js"
|
||||
PagePath = ToolsDir / "Render.html"
|
||||
|
||||
PlaywrightVersion = "1.62.0" # same as the server's (Backend/requirements.txt)
|
||||
MermaidVersion = "11.17.2"
|
||||
MermaidIntegrity = "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg=="
|
||||
FontPath = AssetsDir / "fonts" / "Fixedsys_Excelsior_Regular.ttf"
|
||||
RenderTimeout = 20 # seconds, per diagram
|
||||
SetupTimeout = 900 # seconds, for the downloads
|
||||
|
||||
BrowserNames = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser",
|
||||
"brave-browser", "brave", "microsoft-edge", "microsoft-edge-stable"]
|
||||
WindowsBrowsers = [
|
||||
r"Microsoft\Edge\Application\msedge.exe",
|
||||
r"Google\Chrome\Application\chrome.exe",
|
||||
r"BraveSoftware\Brave-Browser\Application\brave.exe",
|
||||
]
|
||||
|
||||
|
||||
class DiagramError(Exception):
|
||||
"""The diagram itself is wrong (Mermaid's message)."""
|
||||
|
||||
|
||||
class Unavailable(Exception):
|
||||
"""Diagrams can't be drawn here (setup failed, or a frozen build)."""
|
||||
|
||||
|
||||
def ThemeVariables(Palette: dict[str, str], FontSize: int) -> dict:
|
||||
"""Mermaid's colours from the theme's roles: the same mapping as
|
||||
Backend/Diagrams.py and the web view's Frontend/src/Mermaid.js."""
|
||||
Role = Palette.get
|
||||
Background = Role("Background")
|
||||
Red, Green, Blue = (int(Background[Index:Index + 2], 16) for Index in (1, 3, 5))
|
||||
return {
|
||||
"darkMode": (0.299 * Red + 0.587 * Green + 0.114 * Blue) / 255 < 0.5,
|
||||
"background": Background,
|
||||
"fontFamily": '"Fixedsys Excelsior", "Cascadia Mono", monospace',
|
||||
"fontSize": f"{FontSize}px",
|
||||
"textColor": Role("Text"), "lineColor": Role("TextMuted"),
|
||||
"primaryColor": Role("SurfaceAlt"), "primaryTextColor": Role("Text"), "primaryBorderColor": Role("Purple"),
|
||||
"secondaryColor": Role("Surface"), "secondaryTextColor": Role("Text"), "secondaryBorderColor": Role("Blue"),
|
||||
"tertiaryColor": Role("Background"), "tertiaryTextColor": Role("Text"), "tertiaryBorderColor": Role("Border"),
|
||||
"mainBkg": Role("SurfaceAlt"), "nodeBorder": Role("Purple"),
|
||||
"clusterBkg": Role("Surface"), "clusterBorder": Role("Border"),
|
||||
"edgeLabelBackground": Role("Surface"), "titleColor": Role("Text"),
|
||||
"noteBkgColor": Role("Surface"), "noteTextColor": Role("Text"), "noteBorderColor": Role("Yellow"),
|
||||
"actorBkg": Role("SurfaceAlt"), "actorBorder": Role("Purple"), "actorTextColor": Role("Text"),
|
||||
"actorLineColor": Role("TextMuted"), "signalColor": Role("Text"), "signalTextColor": Role("Text"),
|
||||
"labelBoxBkgColor": Role("SurfaceAlt"), "labelBoxBorderColor": Role("Purple"), "labelTextColor": Role("Text"),
|
||||
"loopTextColor": Role("Text"), "activationBkgColor": Role("Surface"), "activationBorderColor": Role("Blue"),
|
||||
"pie1": Role("Blue"), "pie2": Role("Purple"), "pie3": Role("Green"),
|
||||
"pie4": Role("Yellow"), "pie5": Role("Orange"), "pie6": Role("Red"),
|
||||
"pieTitleTextColor": Role("Text"), "pieSectionTextColor": Role("Black"), "pieLegendTextColor": Role("Text"),
|
||||
"pieStrokeColor": Background, "pieOuterStrokeColor": Role("Border"),
|
||||
}
|
||||
|
||||
|
||||
# The same drawing as Backend/Diagrams.py's RenderScript.
|
||||
RenderScript = """async ({ Source, Variables, Id }) => {
|
||||
await document.fonts.ready;
|
||||
mermaid.initialize({ startOnLoad: false, securityLevel: "strict", theme: "base", themeVariables: Variables });
|
||||
try {
|
||||
await mermaid.parse(Source);
|
||||
} catch (Error) {
|
||||
return { Error: String(Error.message || Error) };
|
||||
}
|
||||
const { svg } = await mermaid.render(Id, Source);
|
||||
const Out = document.getElementById("Out");
|
||||
Out.innerHTML = svg;
|
||||
const Picture = Out.querySelector("svg");
|
||||
const Box = Picture.viewBox.baseVal;
|
||||
Picture.style.maxWidth = "none";
|
||||
Picture.setAttribute("width", Box.width);
|
||||
Picture.setAttribute("height", Box.height);
|
||||
return {};
|
||||
}"""
|
||||
|
||||
|
||||
# --- Setup ----------------------------------------------------------------
|
||||
|
||||
def _Run(Command: list[str], Environment: Optional[dict] = None) -> None:
|
||||
Result = subprocess.run(Command, env=Environment or ChildEnvironment(), capture_output=True, text=True,
|
||||
timeout=SetupTimeout)
|
||||
if Result.returncode != 0:
|
||||
Detail = (Result.stderr or Result.stdout).strip().splitlines()
|
||||
raise Unavailable(Detail[-1] if Detail else f"{Command[0]} failed")
|
||||
|
||||
|
||||
def _PlaywrightEnvironment() -> dict:
|
||||
Environment = ChildEnvironment()
|
||||
Environment["PYTHONPATH"] = str(PythonDir)
|
||||
Environment["PLAYWRIGHT_BROWSERS_PATH"] = str(BrowsersDir)
|
||||
return Environment
|
||||
|
||||
|
||||
def _InstallPlaywright() -> None:
|
||||
if (PythonDir / "playwright").is_dir():
|
||||
return
|
||||
if getattr(sys, "frozen", False):
|
||||
raise Unavailable("This build of SaraView can't install what diagrams need; "
|
||||
"the SaraSuite install can.")
|
||||
_Run([sys.executable, "-m", "pip", "install", "--quiet", "--disable-pip-version-check",
|
||||
"--target", str(PythonDir), f"playwright=={PlaywrightVersion}"])
|
||||
|
||||
|
||||
def _InstallMermaid() -> None:
|
||||
if MermaidPath.is_file():
|
||||
return
|
||||
Url = f"https://registry.npmjs.org/mermaid/-/mermaid-{MermaidVersion}.tgz"
|
||||
try:
|
||||
Archive = httpx.get(Url, timeout=120, follow_redirects=True).raise_for_status().content
|
||||
except httpx.HTTPError as Error:
|
||||
raise Unavailable(f"Couldn't download Mermaid: {Error}")
|
||||
Expected = base64.b64decode(MermaidIntegrity.removeprefix("sha512-"))
|
||||
if hashlib.sha512(Archive).digest() != Expected:
|
||||
raise Unavailable("The Mermaid download didn't match its checksum.")
|
||||
with tarfile.open(fileobj=io.BytesIO(Archive), mode="r:gz") as Tar:
|
||||
Script = Tar.extractfile("package/dist/mermaid.min.js")
|
||||
if Script is None:
|
||||
raise Unavailable("The Mermaid download didn't have mermaid.min.js.")
|
||||
Partial = MermaidPath.with_suffix(".part")
|
||||
Partial.write_bytes(Script.read())
|
||||
Partial.replace(MermaidPath)
|
||||
|
||||
|
||||
def _InstalledBrowser() -> Optional[str]:
|
||||
if IsWindows:
|
||||
for Base in (os.environ.get("ProgramFiles(x86)"), os.environ.get("ProgramFiles"),
|
||||
os.environ.get("LOCALAPPDATA")):
|
||||
for Relative in WindowsBrowsers:
|
||||
if Base and (Path(Base) / Relative).is_file():
|
||||
return str(Path(Base) / Relative)
|
||||
return None
|
||||
return next((Found for Name in BrowserNames if (Found := shutil.which(Name))), None)
|
||||
|
||||
|
||||
def _InstallBrowser() -> None:
|
||||
_Run([sys.executable, "-m", "playwright", "install", "--only-shell", "chromium"], _PlaywrightEnvironment())
|
||||
|
||||
|
||||
def _WritePage() -> None:
|
||||
FontFace = f'@font-face {{ font-family: "Fixedsys Excelsior"; src: url("{FontPath.as_uri()}"); }}' if FontPath.is_file() else ""
|
||||
PagePath.write_text(
|
||||
"<!doctype html><html><head><meta charset='utf-8'>"
|
||||
f"<style>{FontFace} body {{ margin: 0; }} #Out {{ display: inline-block; padding: 8px; }}</style>"
|
||||
f"<script src='{MermaidPath.as_uri()}'></script></head><body><div id='Out'></div></body></html>",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
# --- The worker -----------------------------------------------------------
|
||||
|
||||
class _Worker:
|
||||
def __init__(self) -> None:
|
||||
self.Jobs: "queue.Queue[Optional[tuple]]" = queue.Queue()
|
||||
self.Thread: Optional[threading.Thread] = None
|
||||
self.Lock = threading.Lock()
|
||||
self.Failure: Optional[str] = None # why it can't draw, once setup failed
|
||||
self.SettingUp = False
|
||||
|
||||
def Start(self) -> None:
|
||||
with self.Lock:
|
||||
if self.Thread is None or not self.Thread.is_alive():
|
||||
self.Thread = threading.Thread(target=self._Run, name="LocalDiagrams", daemon=True)
|
||||
self.Thread.start()
|
||||
|
||||
def Stop(self) -> None:
|
||||
with self.Lock:
|
||||
Thread = self.Thread
|
||||
self.Thread = None
|
||||
if Thread is not None and Thread.is_alive():
|
||||
self.Jobs.put(None)
|
||||
Thread.join(timeout=30)
|
||||
|
||||
def _Setup(self):
|
||||
"""Everything ToolsDir needs, then a page ready to draw on."""
|
||||
self.SettingUp = True
|
||||
try:
|
||||
ToolsDir.mkdir(parents=True, exist_ok=True)
|
||||
_InstallPlaywright()
|
||||
_InstallMermaid()
|
||||
_WritePage()
|
||||
if str(PythonDir) not in sys.path:
|
||||
sys.path.insert(0, str(PythonDir))
|
||||
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = str(BrowsersDir)
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
Playwright = sync_playwright().start()
|
||||
Browser = None
|
||||
Installed = _InstalledBrowser()
|
||||
if Installed:
|
||||
try:
|
||||
Browser = Playwright.chromium.launch(executable_path=Installed)
|
||||
except Exception:
|
||||
Browser = None # e.g. a Snap Chromium that can't be driven: use our own
|
||||
if Browser is None:
|
||||
if not BrowsersDir.is_dir():
|
||||
_InstallBrowser()
|
||||
Browser = Playwright.chromium.launch()
|
||||
Page = Browser.new_context(device_scale_factor=2, viewport={"width": 1600, "height": 1200}).new_page()
|
||||
Page.route("**/*", lambda Route: Route.continue_() if Route.request.url.startswith("file:") else Route.abort())
|
||||
Page.goto(PagePath.as_uri())
|
||||
return Playwright, Browser, Page
|
||||
finally:
|
||||
self.SettingUp = False
|
||||
|
||||
def _Run(self) -> None:
|
||||
Playwright = Browser = Page = None
|
||||
Count = 0
|
||||
try:
|
||||
while True:
|
||||
Job = self.Jobs.get()
|
||||
if Job is None:
|
||||
return
|
||||
Source, Palette, FontSize, Result = Job
|
||||
try:
|
||||
if Page is None:
|
||||
Playwright, Browser, Page = self._Setup()
|
||||
Count += 1
|
||||
Outcome = Page.evaluate(RenderScript, {"Source": Source, "Variables": ThemeVariables(Palette, FontSize),
|
||||
"Id": f"Diagram{Count}"})
|
||||
if Outcome.get("Error"):
|
||||
raise DiagramError(Outcome["Error"])
|
||||
Result.set_result(Page.locator("#Out").screenshot(omit_background=True, timeout=RenderTimeout * 1000))
|
||||
except DiagramError as Error:
|
||||
Result.set_exception(Error)
|
||||
except Unavailable as Error:
|
||||
self.Failure = str(Error)
|
||||
Result.set_exception(Error)
|
||||
except Exception as Error:
|
||||
if Page is None: # setup itself broke
|
||||
self.Failure = str(Error)
|
||||
Result.set_exception(Unavailable(str(Error)))
|
||||
else:
|
||||
Result.set_exception(DiagramError(f"Couldn't draw the diagram ({Error})."))
|
||||
finally:
|
||||
for Closing in (Browser, Playwright):
|
||||
try:
|
||||
if Closing is not None:
|
||||
Closing.close() if Closing is Browser else Closing.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
_SharedWorker = _Worker()
|
||||
|
||||
|
||||
def IsSetUp() -> bool:
|
||||
"""Whether the first diagram will draw without downloading anything."""
|
||||
return (PythonDir / "playwright").is_dir() and MermaidPath.is_file()
|
||||
|
||||
|
||||
def IsSettingUp() -> bool:
|
||||
return _SharedWorker.SettingUp
|
||||
|
||||
|
||||
def Render(Source: str, Palette: dict[str, str], FontSize: int) -> bytes:
|
||||
"""A diagram as a PNG at twice the size. Blocks: call it off the UI thread.
|
||||
Raises DiagramError for a wrong diagram, Unavailable if it can't draw."""
|
||||
if _SharedWorker.Failure is not None:
|
||||
raise Unavailable(_SharedWorker.Failure)
|
||||
_SharedWorker.Start()
|
||||
Result: Future = Future()
|
||||
_SharedWorker.Jobs.put((Source, Palette, FontSize, Result))
|
||||
return Result.result(timeout=SetupTimeout + RenderTimeout)
|
||||
|
||||
|
||||
def Remove() -> None:
|
||||
"""Stops drawing and deletes ToolsDir: everything set up for local diagrams."""
|
||||
_SharedWorker.Stop()
|
||||
_SharedWorker.Failure = None
|
||||
shutil.rmtree(ToolsDir, ignore_errors=True)
|
||||
|
||||
|
||||
def ToolsSize() -> int:
|
||||
"""Bytes in ToolsDir, for the setting's note."""
|
||||
return sum(File.stat().st_size for File in ToolsDir.rglob("*") if File.is_file()) if ToolsDir.is_dir() else 0
|
||||
+25
-2
@@ -9,10 +9,11 @@ from typing import Callable
|
||||
import flet as ft
|
||||
import tomlkit
|
||||
|
||||
import LocalDiagrams
|
||||
from App import App
|
||||
from Core import (
|
||||
AppConfig, AssetsDir, AvailableThemes, ChipButton, DataHome, ExportSettings, Home, ImportSettings,
|
||||
InstalledFontFamilies, LoadSharedSettings, Palette, SharedSettings,
|
||||
AppConfig, AssetsDir, AvailableThemes, ChipButton, DataHome, ExportSettings, Home, HumanSize,
|
||||
ImportSettings, InstalledFontFamilies, LoadSharedSettings, Palette, SharedSettings,
|
||||
)
|
||||
|
||||
DialogWidth = 700
|
||||
@@ -109,6 +110,16 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
|
||||
)
|
||||
FontSizeField = ft.TextField(label="Text size", value=str(Config.FontSize), keyboard_type=ft.KeyboardType.NUMBER, width=160)
|
||||
|
||||
LocalDiagramsSwitch = ft.Switch(label="Draw diagrams in local folders", value=Config.LocalDiagrams)
|
||||
ToolsBytes = LocalDiagrams.ToolsSize()
|
||||
LocalDiagramsNote = _Note(
|
||||
"```mermaid blocks (flowcharts and the like) as pictures, drawn on this computer. The first one "
|
||||
"downloads what that needs: ~150 MB with Chrome, Chromium, Brave or Edge installed, ~430 MB without. "
|
||||
"Off: deletes those downloads"
|
||||
+ (f" ({HumanSize(ToolsBytes)} now)." if ToolsBytes else ".")
|
||||
+ " On a server, the server draws them either way."
|
||||
)
|
||||
|
||||
def ToggleMenuEntry(e) -> None:
|
||||
SetMenuEntry(not MenuEntryPath.exists())
|
||||
MenuButton.content = "Remove from app menu" if MenuEntryPath.exists() else "Add to app menu"
|
||||
@@ -217,6 +228,8 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
ft.Text("Markdown editing"),
|
||||
MarkdownEditingPicker,
|
||||
LocalDiagramsSwitch,
|
||||
LocalDiagramsNote,
|
||||
ft.Divider(height=1, color=Palette.Surface1),
|
||||
OwnAppearance,
|
||||
], spacing=12, scroll=ft.ScrollMode.AUTO),
|
||||
@@ -266,6 +279,8 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
|
||||
Config.ShowSidebar = SidebarSwitch.value
|
||||
Config.ShowFilesInTree = TreeFilesSwitch.value
|
||||
Config.MarkdownEditing = MarkdownEditingPicker.value or Config.MarkdownEditing
|
||||
RemoveDiagramTools = Config.LocalDiagrams and not LocalDiagramsSwitch.value
|
||||
Config.LocalDiagrams = LocalDiagramsSwitch.value
|
||||
Config.GraphShowDirectories = GraphDirectoriesSwitch.value
|
||||
Config.GraphColourByDepth = ColourByDepthSwitch.value
|
||||
Config.GraphShrinkByDepth = ShrinkByDepthSwitch.value
|
||||
@@ -289,6 +304,14 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
|
||||
TheApp.Close()
|
||||
OnSaved(SourceAfter != SourceBefore)
|
||||
TheApp.Notify("Settings saved.", "success")
|
||||
if RemoveDiagramTools:
|
||||
def RemoveTools() -> None:
|
||||
Freed = LocalDiagrams.ToolsSize()
|
||||
LocalDiagrams.Remove()
|
||||
if Freed:
|
||||
TheApp.Notify(f"Removed the diagram downloads ({HumanSize(Freed)}).", "success")
|
||||
|
||||
Page.run_thread(RemoveTools)
|
||||
|
||||
TheApp.DialogOpen = True
|
||||
Page.show_dialog(ft.AlertDialog(
|
||||
|
||||
Reference in new issue
Block a user