Client: links and images open, line breaks like the web, directory notes setting

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
sarahandClaude Opus 5.5 committed 2026-10-13 18:22:51 +02:00
1 parent 55ee8e2f23
commit 0513d94c6a
8 files changed
+134 -23

No files matched your search

+43 -3
View File
@@ -12,7 +12,7 @@ import flet as ft
from App import App from App import App
from Core import ChipButtonStyle, FileIcons, HumanDuration, HumanSize, OpenExternally, Palette from Core import ChipButtonStyle, FileIcons, HumanDuration, HumanSize, OpenExternally, Palette
from Sources import BaseName, Entry, JoinPath, MoveDestination, ParentPath, SourceError from Sources import BaseName, ClassifyFile, Entry, JoinPath, MoveDestination, ParentPath, SourceError
from Graph import GraphView from Graph import GraphView
from Viewer import FileView from Viewer import FileView
@@ -438,24 +438,64 @@ class Browser(ft.Column):
return return
Root, RelativePath = self._EntryPath(Name) Root, RelativePath = self._EntryPath(Name)
if Item.IsDirectory: if Item.IsDirectory:
self.NavigateTo(Root, RelativePath) self.OpenDirectory(Root, RelativePath, Item.Note)
return return
if InApp is None: if InApp is None:
InApp = self.Config.OpenFilesInApp InApp = self.Config.OpenFilesInApp
if not InApp: if not InApp:
self._OpenExternally(Root, RelativePath) self._OpenExternally(Root, RelativePath)
return return
self._ShowFile(Root, RelativePath, Item)
def _ShowFile(self, Root: str, RelativePath: str, Item: Entry) -> None:
"""The file view, for a file in the current directory (it may be one
the list hides, like a directory's .sv_ note)."""
self.OpenView = FileView( self.OpenView = FileView(
self.TheApp, Root, RelativePath, Item, self.TheApp, Root, RelativePath, Item,
OnClose=self._CloseFile, OnClose=self._CloseFile,
OnOpenExternally=lambda: self._OpenExternally(Root, RelativePath), OnOpenExternally=lambda: self._OpenExternally(Root, RelativePath),
OnDownload=(lambda: self._Download(Name)) if self.TheApp.Server else None, OnDownload=(lambda: self._Download(Item.Name)) if self.TheApp.Server else None,
OnFocusChange=self._SetShortcutsPaused, OnFocusChange=self._SetShortcutsPaused,
OnOpenLink=self.OpenLink,
) )
self.FileArea.content = self.OpenView self.FileArea.content = self.OpenView
self.FileArea.update() self.FileArea.update()
self.OnPathChanged(Root, RelativePath, False) # highlights the file in the tree self.OnPathChanged(Root, RelativePath, False) # highlights the file in the tree
def OpenDirectory(self, Root: str, RelativePath: str, Note: Optional[dict] = None) -> None:
"""A directory clicked (in the list or the tree): go there, and with
Config.NavigateIntoDirectories open its .sv_ note too."""
if self.OpenView is not None and self.OpenView.IsDirty:
self.OpenView.RequestClose(Then=lambda: self.OpenDirectory(Root, RelativePath, Note))
return
self.NavigateTo(Root, RelativePath)
if self.Config.NavigateIntoDirectories and Note and RelativePath:
self._ShowFile(Root, JoinPath(RelativePath, Note["Name"]),
Entry(Note["Name"], False, Note.get("FileType") or ClassifyFile(Note["Name"])))
def OpenLink(self, Root: str, RelativePath: str) -> None:
"""A link in a note (or an image in one) clicked: open what it points at."""
if self.OpenView is not None and self.OpenView.IsDirty:
self.OpenView.RequestClose(Then=lambda: self.OpenLink(Root, RelativePath))
return
Directory, Name = ParentPath(RelativePath), BaseName(RelativePath)
if not Name:
self.NavigateTo(Root, "")
return
try:
Listed = {Item.Name: Item for Item in self.TheApp.Source.List(Root, Directory)}
except SourceError as Error:
self.TheApp.Notify(f"Couldn't open {RelativePath}: {Error}", "error")
return
Item = Listed.get(Name)
if Item is not None and Item.IsDirectory:
self.OpenDirectory(Root, RelativePath, Item.Note)
return
if (Root, Directory) != (self.Root, self.Path) or self.OpenView is not None:
self.NavigateTo(Root, Directory)
# Not listed may still be there (a hidden .sv_ note); the view says if not.
self._ShowFile(Root, RelativePath, Item or Entry(Name, False, ClassifyFile(Name)))
def _CloseFile(self) -> None: def _CloseFile(self) -> None:
self.OpenView = None self.OpenView = None
self.ShortcutsPaused = False self.ShortcutsPaused = False
+18
View File
@@ -193,6 +193,9 @@ class AppConfig:
# Double-click on a file: True opens it in SaraView's own viewer, False # 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. # hands it to the system's default program. The menu always offers the other.
OpenFilesInApp: bool = True OpenFilesInApp: bool = True
# Clicking a directory (in the tree or the list) also opens its .sv_ note,
# like the web view's "Open directory file on click".
NavigateIntoDirectories: bool = False
# Draw ```mermaid diagrams in local folders (LocalDiagrams.py); off removes # Draw ```mermaid diagrams in local folders (LocalDiagrams.py); off removes
# what it downloaded. On a server, the server draws them either way. # what it downloaded. On a server, the server draws them either way.
LocalDiagrams: bool = True LocalDiagrams: bool = True
@@ -438,6 +441,21 @@ def CodeStyleSheet() -> ft.MarkdownStyleSheet:
return ft.MarkdownStyleSheet(code_text_style=ft.TextStyle(font_family=ActiveFontFamily)) return ft.MarkdownStyleSheet(code_text_style=ft.TextStyle(font_family=ActiveFontFamily))
def HardLineBreaks(Text: str) -> str:
"""Every line break shows, like the web view's Markdown (breaks: true):
Flet's joins a paragraph's lines into one. Code blocks stay as they are."""
Lines = Text.split("\n")
InFence = False
for Index, Line in enumerate(Lines[:-1]):
if Line.lstrip().startswith(("```", "~~~")):
InFence = not InFence
continue
if InFence or not Line.strip() or not Lines[Index + 1].strip() or Line.endswith((" ", "\\")):
continue
Lines[Index] = Line + " "
return "\n".join(Lines)
def MarkdownSheet(FontSize: int) -> ft.MarkdownStyleSheet: def MarkdownSheet(FontSize: int) -> ft.MarkdownStyleSheet:
"""Rendered Markdown's colours, the same roles as the web view's """Rendered Markdown's colours, the same roles as the web view's
.MarkdownBody (Frontend/src/Styles/Theme.css): rainbow headings, coloured .MarkdownBody (Frontend/src/Styles/Theme.css): rainbow headings, coloured
+5 -4
View File
@@ -55,8 +55,8 @@ class _FileLeaf:
class _FiletreeNode: class _FiletreeNode:
def __init__(self, TheApp: App, Root: str, RelativePath: str, OnNavigate: Callable[[str, str], None], def __init__(self, TheApp: App, Root: str, RelativePath: str, OnNavigate: Callable[..., None],
OnOpenFile: Callable[[str, str], None], Depth: int): OnOpenFile: Callable[[str, str], None], Depth: int, Note: Optional[dict] = None):
self.TheApp = TheApp self.TheApp = TheApp
self.Root = Root self.Root = Root
self.RelativePath = RelativePath self.RelativePath = RelativePath
@@ -68,7 +68,8 @@ class _FiletreeNode:
self.FileLeaves: dict[str, _FileLeaf] = {} self.FileLeaves: dict[str, _FileLeaf] = {}
Name = RelativePath.rsplit("/", 1)[-1] if RelativePath else Root Name = RelativePath.rsplit("/", 1)[-1] if RelativePath else Root
self.NameLabel, self.NameContainer = _TreeLabel(Name, lambda: self.OnNavigate(self.Root, self.RelativePath)) # OnNavigate(Root, Path, Note): the browser's OpenDirectory, which may open the .sv_ note too.
self.NameLabel, self.NameContainer = _TreeLabel(Name, lambda: self.OnNavigate(self.Root, self.RelativePath, Note))
self.Tile = ft.ExpansionTile( self.Tile = ft.ExpansionTile(
title=self.NameContainer, title=self.NameContainer,
leading=ft.Icon(ft.Icons.STORAGE_OUTLINED if not RelativePath else ft.Icons.FOLDER_OUTLINED, leading=ft.Icon(ft.Icons.STORAGE_OUTLINED if not RelativePath else ft.Icons.FOLDER_OUTLINED,
@@ -98,7 +99,7 @@ class _FiletreeNode:
ChildPath = JoinPath(self.RelativePath, Item.Name) ChildPath = JoinPath(self.RelativePath, Item.Name)
if Item.IsDirectory: if Item.IsDirectory:
self.ChildNodes[Item.Name] = _FiletreeNode(self.TheApp, self.Root, ChildPath, self.OnNavigate, self.ChildNodes[Item.Name] = _FiletreeNode(self.TheApp, self.Root, ChildPath, self.OnNavigate,
self.OnOpenFile, self.Depth + 1) self.OnOpenFile, self.Depth + 1, Item.Note)
elif self.TheApp.Config.ShowFilesInTree: elif self.TheApp.Config.ShowFilesInTree:
self.FileLeaves[Item.Name] = _FileLeaf(self.Root, ChildPath, Item.FileType, self.OnOpenFile, self.Depth + 1) self.FileLeaves[Item.Name] = _FileLeaf(self.Root, ChildPath, Item.FileType, self.OnOpenFile, self.Depth + 1)
# Directories first, then files, like the file list. # Directories first, then files, like the file list.
+7 -4
View File
@@ -20,7 +20,7 @@ from typing import Callable, Optional
import flet as ft import flet as ft
import Core import Core
from Core import CodeStyleSheet, MarkdownCodeTheme, MarkdownSheet, Palette from Core import CodeStyleSheet, HardLineBreaks, MarkdownCodeTheme, MarkdownSheet, Palette
from Properties import ParseProperties, PropertiesView, ToggleLine, WithCheckboxes from Properties import ParseProperties, PropertiesView, ToggleLine, WithCheckboxes
from Variables import Substitute from Variables import Substitute
@@ -117,7 +117,8 @@ class LiveMarkdown(ft.Column):
def __init__(self, Document: str, FontSize: int, OnChange: Callable[[], None], def __init__(self, Document: str, FontSize: int, OnChange: Callable[[], None],
OnFocusChange: Callable[[bool], None], OnTapLink: Callable, OnFocusChange: Callable[[bool], None], OnTapLink: Callable,
MakeDiagram: Optional[Callable[[str, int], Optional[ft.Control]]] = None, MakeDiagram: Optional[Callable[[str, int], Optional[ft.Control]]] = None,
GetSpans: Optional[Callable[[str], list[dict]]] = None): GetSpans: Optional[Callable[[str], list[dict]]] = None,
MakeImage: Optional[Callable[[str, str], Optional[ft.Control]]] = None):
super().__init__(scroll=ft.ScrollMode.AUTO, expand=True, spacing=0, super().__init__(scroll=ft.ScrollMode.AUTO, expand=True, spacing=0,
horizontal_alignment=ft.CrossAxisAlignment.STRETCH) horizontal_alignment=ft.CrossAxisAlignment.STRETCH)
self.Document = Document self.Document = Document
@@ -127,6 +128,7 @@ class LiveMarkdown(ft.Column):
self.OnTapLink = OnTapLink self.OnTapLink = OnTapLink
self.MakeDiagram = MakeDiagram self.MakeDiagram = MakeDiagram
self.GetSpans = GetSpans self.GetSpans = GetSpans
self.MakeImage = MakeImage
self.SpansFor: tuple[str, list[dict]] = ("", []) # (document, its spans) self.SpansFor: tuple[str, list[dict]] = ("", []) # (document, its spans)
self.Blocks: list[Block] = [] self.Blocks: list[Block] = []
self.BlockControls: list[ft.Control] = [] self.BlockControls: list[ft.Control] = []
@@ -237,7 +239,7 @@ class LiveMarkdown(ft.Column):
def _Markdown(self, Text: str) -> ft.Control: def _Markdown(self, Text: str) -> ft.Control:
return ft.Markdown( return ft.Markdown(
value=Text, value=HardLineBreaks(Text),
extension_set=ft.MarkdownExtensionSet.GITHUB_FLAVORED, extension_set=ft.MarkdownExtensionSet.GITHUB_FLAVORED,
md_style_sheet=MarkdownSheet(self.FontSize), md_style_sheet=MarkdownSheet(self.FontSize),
code_theme=MarkdownCodeTheme(), code_theme=MarkdownCodeTheme(),
@@ -263,7 +265,8 @@ class LiveMarkdown(ft.Column):
elif Kind == "Properties" and (Found := ParseProperties(Text)) is not None: elif Kind == "Properties" and (Found := ParseProperties(Text)) is not None:
Rendered.content = PropertiesView(Found, self.FontSize, self._Markdown, Toggle) Rendered.content = PropertiesView(Found, self.FontSize, self._Markdown, Toggle)
elif Kind == "Text": elif Kind == "Text":
Parts = WithCheckboxes(Shown if Shown is not None else Text, 0, self._Markdown, Toggle, self.FontSize, LineMap) Parts = WithCheckboxes(Shown if Shown is not None else Text, 0, self._Markdown, Toggle, self.FontSize, LineMap,
self.MakeImage)
Rendered.content = Parts[0] if len(Parts) == 1 else ft.Column(Parts, spacing=0, tight=True) Rendered.content = Parts[0] if len(Parts) == 1 else ft.Column(Parts, spacing=0, tight=True)
else: else:
Rendered.content = self._Markdown(Shown if Shown is not None and Kind == "Heading" else Text) Rendered.content = self._Markdown(Shown if Shown is not None and Kind == "Heading" else Text)
+1 -1
View File
@@ -92,7 +92,7 @@ def Main(Page: ft.Page) -> None:
TheBrowser = Browser(TheApp, OnPathChanged, TheBrowser = Browser(TheApp, OnPathChanged,
lambda Category=None: ShowSettings(Category or ("Connection" if TheApp.ConnectError else "General")), lambda Category=None: ShowSettings(Category or ("Connection" if TheApp.ConnectError else "General")),
ToggleSidebar) ToggleSidebar)
Tree = Filetree(TheApp, TheBrowser.NavigateTo, TheBrowser.OpenFile) Tree = Filetree(TheApp, TheBrowser.OpenDirectory, TheBrowser.OpenFile)
Tree.SetEntryButtons(TheBrowser.EntryButtons, Config.NewEntryButtonsAtTop, Update=False) Tree.SetEntryButtons(TheBrowser.EntryButtons, Config.NewEntryButtonsAtTop, Update=False)
Divider = ResizeHandle(OnDrag, OnDragEnd) Divider = ResizeHandle(OnDrag, OnDragEnd)
Tree.visible = Divider.visible = Config.ShowSidebar Tree.visible = Divider.visible = Config.ShowSidebar
+13 -3
View File
@@ -22,6 +22,7 @@ from Core import Palette
OpenOrClose = re.compile(r"^---\s*$") OpenOrClose = re.compile(r"^---\s*$")
PropertyLine = re.compile(r"^([^:\s][^:]*?)\s*:\s*(.*)$") PropertyLine = re.compile(r"^([^:\s][^:]*?)\s*:\s*(.*)$")
TaskLine = re.compile(r"^(\s*(?:[-*+]|\d+[.)])\s+)\[( |x|X)\]") TaskLine = re.compile(r"^(\s*(?:[-*+]|\d+[.)])\s+)\[( |x|X)\]")
ImageLine = re.compile(r"^\s*!\[([^\]]*)\]\(<?([^)>]+?)>?\)\s*$")
BooleanValue = re.compile(r"^(true|false)$", re.IGNORECASE) BooleanValue = re.compile(r"^(true|false)$", re.IGNORECASE)
Quoted = re.compile(r"""^(["']).*\1$""") Quoted = re.compile(r"""^(["']).*\1$""")
IntegerValue = re.compile(r"^[+-]?\d+$") IntegerValue = re.compile(r"^[+-]?\d+$")
@@ -169,9 +170,10 @@ def PropertiesView(Found: Parsed, FontSize: int, MakeMarkdown: Callable[[str], f
def WithCheckboxes(Text: str, FirstLine: int, MakeMarkdown: Callable[[str], ft.Control], def WithCheckboxes(Text: str, FirstLine: int, MakeMarkdown: Callable[[str], ft.Control],
Toggle: OnToggle, FontSize: int, LineMap: Optional[list[Optional[int]]] = None) -> list[ft.Control]: Toggle: OnToggle, FontSize: int, LineMap: Optional[list[Optional[int]]] = None,
"""Markdown with its task lines as real checkboxes: the lines between MakeImage: Optional[Callable[[str, str], Optional[ft.Control]]] = None) -> list[ft.Control]:
them render as usual. FirstLine is where Text starts in the note; LineMap """Markdown with its task lines as real checkboxes, and lines that are
only an image as the image (MakeImage): the lines between render as usual. FirstLine is where Text starts in the note; LineMap
(Variables.Substitute) says which of its lines each shown line is, None (Variables.Substitute) says which of its lines each shown line is, None
for one from a variable's result (its checkbox can't be ticked).""" for one from a variable's result (its checkbox can't be ticked)."""
Controls: list[ft.Control] = [] Controls: list[ft.Control] = []
@@ -186,6 +188,14 @@ def WithCheckboxes(Text: str, FirstLine: int, MakeMarkdown: Callable[[str], ft.C
for Offset, Line in enumerate(Text.split("\n")): for Offset, Line in enumerate(Text.split("\n")):
if re.match(r"^ {0,3}(`{3,}|~{3,})", Line): if re.match(r"^ {0,3}(`{3,}|~{3,})", Line):
InFence = not InFence InFence = not InFence
# A line that's only an image: shown and clickable (MakeImage), as
# Flet's Markdown has no click for images.
Picture = ImageLine.match(Line) if MakeImage is not None and not InFence else None
Shown = MakeImage(Picture.group(2), Picture.group(1)) if Picture else None
if Shown is not None:
Flush()
Controls.append(Shown)
continue
Task = None if InFence else TaskLine.match(Line) Task = None if InFence else TaskLine.match(Line)
if Task is None: if Task is None:
Pending.append(Line) Pending.append(Line)
+5 -1
View File
@@ -103,6 +103,8 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
ConfirmDeleteSwitch = ft.Switch(label="Ask before deleting", value=Config.ConfirmDelete) ConfirmDeleteSwitch = ft.Switch(label="Ask before deleting", value=Config.ConfirmDelete)
SidebarSwitch = ft.Switch(label="Show the directory tree (Ctrl+B)", value=Config.ShowSidebar) 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) TreeFilesSwitch = ft.Switch(label="Show files in the tree (off: directories only)", value=Config.ShowFilesInTree)
DirectoryNotesSwitch = ft.Switch(label="Clicking a directory also opens its .sv_ note",
value=Config.NavigateIntoDirectories)
ButtonsAtTopSwitch = ft.Switch(label="New File, New Directory and Upload above the tree (off: below)", ButtonsAtTopSwitch = ft.Switch(label="New File, New Directory and Upload above the tree (off: below)",
value=Config.NewEntryButtonsAtTop) value=Config.NewEntryButtonsAtTop)
MarkdownEditingPicker = ft.RadioGroup( MarkdownEditingPicker = ft.RadioGroup(
@@ -228,7 +230,8 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
ft.Row([ChipButton("Export", OnExport), ChipButton("Import", OnImport)]), ft.Row([ChipButton("Export", OnExport), ChipButton("Import", OnImport)]),
], spacing=12, scroll=ft.ScrollMode.AUTO), ], spacing=12, scroll=ft.ScrollMode.AUTO),
"General": ft.Column([ "General": ft.Column([
OpenInAppSwitch, ConfirmDeleteSwitch, SidebarSwitch, TreeFilesSwitch, ButtonsAtTopSwitch, FontSizeField, MenuButton, OpenInAppSwitch, ConfirmDeleteSwitch, SidebarSwitch, TreeFilesSwitch, DirectoryNotesSwitch, ButtonsAtTopSwitch,
FontSizeField, MenuButton,
ft.Divider(height=1, color=Palette.Surface1), ft.Divider(height=1, color=Palette.Surface1),
ft.Text("Markdown editing"), ft.Text("Markdown editing"),
MarkdownEditingPicker, MarkdownEditingPicker,
@@ -304,6 +307,7 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[bool], None], StartCatego
Config.ShowSidebar = SidebarSwitch.value Config.ShowSidebar = SidebarSwitch.value
Config.ShowFilesInTree = TreeFilesSwitch.value Config.ShowFilesInTree = TreeFilesSwitch.value
Config.NewEntryButtonsAtTop = ButtonsAtTopSwitch.value Config.NewEntryButtonsAtTop = ButtonsAtTopSwitch.value
Config.NavigateIntoDirectories = DirectoryNotesSwitch.value
Config.MarkdownEditing = MarkdownEditingPicker.value or Config.MarkdownEditing Config.MarkdownEditing = MarkdownEditingPicker.value or Config.MarkdownEditing
RemoveDiagramTools = Config.LocalDiagrams and not LocalDiagramsSwitch.value RemoveDiagramTools = Config.LocalDiagrams and not LocalDiagramsSwitch.value
Config.LocalDiagrams = LocalDiagramsSwitch.value Config.LocalDiagrams = LocalDiagramsSwitch.value
+42 -7
View File
@@ -18,17 +18,18 @@ EditorView/ImageView idea, with SaraView's View/Edit/Save toolbar).
- SaraTag tags (from the server's Books) show as chips under the toolbar.""" - SaraTag tags (from the server's Books) show as chips under the toolbar."""
import re import re
from urllib.parse import unquote
from typing import Callable, Optional from typing import Callable, Optional
import flet as ft import flet as ft
from App import App from App import App
from Core import ChipButton, CodeStyleSheet, HumanSize, MarkdownCodeTheme, MarkdownSheet, Palette from Core import ChipButton, CodeStyleSheet, HardLineBreaks, HumanSize, MarkdownCodeTheme, MarkdownSheet, Palette
from Diagrams import MaybeDiagram, WithDiagrams from Diagrams import MaybeDiagram, WithDiagrams
from Properties import ParseProperties, PropertiesView, ToggleLine, WithCheckboxes from Properties import ParseProperties, PropertiesView, ToggleLine, WithCheckboxes
from Variables import FindSpans, Substitute from Variables import FindSpans, Substitute
from LiveMarkdown import LiveMarkdown from LiveMarkdown import LiveMarkdown
from Sources import Entry, SourceError, TextFileTypes from Sources import Entry, NormalizeRelativePath, ParentPath, SourceError, TextFileTypes
LanguageByExtension = { LanguageByExtension = {
".json": "json", ".toml": "toml", ".yaml": "yaml", ".yml": "yaml", ".cfg": "ini", ".ini": "ini", ".json": "json", ".toml": "toml", ".yaml": "yaml", ".yml": "yaml", ".cfg": "ini", ".ini": "ini",
@@ -58,6 +59,7 @@ class FileView(ft.Container):
OnOpenExternally: Callable[[], None], OnOpenExternally: Callable[[], None],
OnDownload: Optional[Callable[[], None]], OnDownload: Optional[Callable[[], None]],
OnFocusChange: Callable[[bool], None], OnFocusChange: Callable[[bool], None],
OnOpenLink: Optional[Callable[[str, str], None]] = None,
): ):
self.TheApp = TheApp self.TheApp = TheApp
self.Root = Root self.Root = Root
@@ -66,6 +68,8 @@ class FileView(ft.Container):
self.FileType = Item.FileType or "unknown" self.FileType = Item.FileType or "unknown"
self.OnClose = OnClose self.OnClose = OnClose
self.OnFocusChange = OnFocusChange self.OnFocusChange = OnFocusChange
self.OnOpenLink = OnOpenLink # (Root, RelativePath): a link to another note, or an image, clicked
self.Pictures: dict[str, Optional[bytes]] = {} # images by path, read once
self.IsText = self.FileType in TextFileTypes self.IsText = self.FileType in TextFileTypes
self.IsLive = (self.FileType == "markdown" and TheApp.Config.MarkdownEditing == "Live" self.IsLive = (self.FileType == "markdown" and TheApp.Config.MarkdownEditing == "Live"
and TheApp.Source.CanWrite) and TheApp.Source.CanWrite)
@@ -130,7 +134,8 @@ class FileView(ft.Container):
self.Live = LiveMarkdown(self.Content, self.TheApp.Config.FontSize, self._OnTextChanged, self.Live = LiveMarkdown(self.Content, self.TheApp.Config.FontSize, self._OnTextChanged,
self.OnFocusChange, self._OnTapLink, self.OnFocusChange, self._OnTapLink,
MakeDiagram=lambda Text, Size: MaybeDiagram(self.TheApp, Text, Size), MakeDiagram=lambda Text, Size: MaybeDiagram(self.TheApp, Text, Size),
GetSpans=lambda Document: FindSpans(self.TheApp, self.Root, self.RelativePath, Document)) GetSpans=lambda Document: FindSpans(self.TheApp, self.Root, self.RelativePath, Document),
MakeImage=self._Image)
return self.Live return self.Live
return self._RenderedView() return self._RenderedView()
if self.FileType == "image": if self.FileType == "image":
@@ -152,7 +157,7 @@ class FileView(ft.Container):
def MakeMarkdown(Part: str) -> ft.Control: def MakeMarkdown(Part: str) -> ft.Control:
return ft.Markdown( return ft.Markdown(
value=Part, value=HardLineBreaks(Part),
selectable=True, selectable=True,
extension_set=ft.MarkdownExtensionSet.GITHUB_FLAVORED, extension_set=ft.MarkdownExtensionSet.GITHUB_FLAVORED,
md_style_sheet=MarkdownSheet(FontSize), md_style_sheet=MarkdownSheet(FontSize),
@@ -164,7 +169,7 @@ class FileView(ft.Container):
def MakeWithCheckboxes(Part: str, Start: int) -> ft.Control: def MakeWithCheckboxes(Part: str, Start: int) -> ft.Control:
FirstLine = Text[:BodyStart + Start].count("\n") FirstLine = Text[:BodyStart + Start].count("\n")
Shown, LineMap = Substitute(Part, BodyStart + Start, Spans) # variables as their results Shown, LineMap = Substitute(Part, BodyStart + Start, Spans) # variables as their results
Parts = WithCheckboxes(Shown, FirstLine, MakeMarkdown, Toggle, FontSize, LineMap) Parts = WithCheckboxes(Shown, FirstLine, MakeMarkdown, Toggle, FontSize, LineMap, self._Image)
return Parts[0] if len(Parts) == 1 else ft.Column(Parts, spacing=0, tight=True) return Parts[0] if len(Parts) == 1 else ft.Column(Parts, spacing=0, tight=True)
Views = WithDiagrams(self.TheApp, Text[BodyStart:], FontSize, MakeWithCheckboxes) Views = WithDiagrams(self.TheApp, Text[BodyStart:], FontSize, MakeWithCheckboxes)
@@ -220,9 +225,39 @@ class FileView(ft.Container):
return ft.Row(Chips, wrap=True, spacing=4, run_spacing=4) return ft.Row(Chips, wrap=True, spacing=4, run_spacing=4)
def _OnTapLink(self, e) -> None: def _OnTapLink(self, e) -> None:
Target = str(e.data or "") self.OpenTarget(str(e.data or ""))
if Target.startswith(("http://", "https://", "mailto:")):
def OpenTarget(self, Target: str) -> None:
"""A clicked link or image: web addresses in the browser, a note (or
directory, or image) relative to this one in SaraView."""
if not Target or Target.startswith("#"):
return
if re.match(r"^[a-z][a-z0-9+.-]*:", Target, re.IGNORECASE):
self.TheApp.Page.run_task(ft.UrlLauncher().launch_url, Target) self.TheApp.Page.run_task(ft.UrlLauncher().launch_url, Target)
elif self.OnOpenLink is not None:
Path = NormalizeRelativePath(ParentPath(self.RelativePath), unquote(Target.split("#", 1)[0]))
self.OnOpenLink(self.Root, Path)
def _Image(self, Src: str, Alt: str) -> Optional[ft.Control]:
"""A line that's only an image (Properties.WithCheckboxes): the image,
opened when clicked. None if it can't be read: then it shows as written."""
if re.match(r"^https?://", Src):
Picture: ft.Image = ft.Image(src=Src, fit=ft.BoxFit.CONTAIN, tooltip=Alt or None)
else:
Path = NormalizeRelativePath(ParentPath(self.RelativePath), unquote(Src))
if Path not in self.Pictures:
try:
self.Pictures[Path] = self.TheApp.Source.ReadBytes(self.Root, Path)
except SourceError:
self.Pictures[Path] = None
if self.Pictures[Path] is None:
return None
Picture = ft.Image(src=self.Pictures[Path], fit=ft.BoxFit.CONTAIN, tooltip=Alt or None)
return ft.Container(
ft.GestureDetector(content=Picture, on_tap=lambda e: self.OpenTarget(Src), mouse_cursor=ft.MouseCursor.CLICK),
alignment=ft.Alignment.CENTER_LEFT,
padding=ft.Padding.symmetric(vertical=4),
)
def _ToggleAndSave(self, Line: int) -> None: def _ToggleAndSave(self, Line: int) -> None:
"""A checkbox ticked in View: flip its line and save, like the web view.""" """A checkbox ticked in View: flip its line and save, like the web view."""