Add live Markdown editing, colours and files in the tree

This commit is contained in:
sarah committed 2026-10-09 09:03:23 +02:00
1 parent 0eecfba224
commit f851ecd820
15 files changed
+1025 -58

No files matched your search

+77 -32
View File
@@ -1,35 +1,68 @@
"""The sidebar's directory tree, built like SaraFem's Filetree: one node per
root, children loaded lazily on expand (every expand on a server is a
request). Clicking a directory opens it in the browser."""
"""The sidebar's tree, built like SaraFem's Filetree: one node per root,
children loaded lazily on expand (every expand on a server is a request).
Clicking a directory opens it in the browser; clicking a file opens it in
the viewer. Files can be hidden from the tree in Settings > General."""
from typing import Callable, Optional
from typing import Callable, Optional, Union
import flet as ft
from App import App
from Core import Palette
from Core import FileIcons, Palette
from Sources import JoinPath, SourceError
Location = tuple[str, str] # (Root, Path)
def _TreeLabel(Name: str, OnTap: Callable[[], None]) -> tuple[ft.Text, ft.Container]:
Label = ft.Text(Name, size=13, color=Palette.Text, overflow=ft.TextOverflow.ELLIPSIS, max_lines=1)
Holder = ft.Container(content=ft.GestureDetector(content=Label, on_tap=lambda e: OnTap()), padding=4, border_radius=4)
return Label, Holder
def _SetHighlight(Label: ft.Text, Holder: ft.Container, IsSelected: bool) -> None:
Holder.bgcolor = Palette.Surface1 if IsSelected else None
Label.color = Palette.Mauve if IsSelected else Palette.Text
Holder.update()
class _FileLeaf:
"""A file in the tree: a plain row (no expand arrow) indented like a
child tile, so names line up with the directories beside it."""
def __init__(self, Root: str, RelativePath: str, FileType: Optional[str],
OnOpenFile: Callable[[str, str], None], Depth: int):
Name = RelativePath.rsplit("/", 1)[-1]
self.NameLabel, self.NameContainer = _TreeLabel(Name, lambda: OnOpenFile(Root, RelativePath))
self.Tile = ft.Container(
content=ft.Row(
[ft.Icon(FileIcons.get(FileType or "", ft.Icons.INSERT_DRIVE_FILE_OUTLINED), size=16, color=Palette.Overlay0),
ft.Container(self.NameContainer, expand=True)],
spacing=12,
),
# Icon and name line up with a directory tile's at the same depth.
padding=ft.Padding.only(left=8 + Depth * 14, right=8, top=2, bottom=2),
)
def SetSelected(self, IsSelected: bool) -> None:
_SetHighlight(self.NameLabel, self.NameContainer, IsSelected)
class _FiletreeNode:
def __init__(self, TheApp: App, Root: str, RelativePath: str, OnNavigate: Callable[[str, str], None], Depth: int):
def __init__(self, TheApp: App, Root: str, RelativePath: str, OnNavigate: Callable[[str, str], None],
OnOpenFile: Callable[[str, str], None], Depth: int):
self.TheApp = TheApp
self.Root = Root
self.RelativePath = RelativePath
self.OnNavigate = OnNavigate
self.OnOpenFile = OnOpenFile
self.Depth = Depth
self.ChildrenLoaded = False
self.ChildNodes: dict[str, "_FiletreeNode"] = {}
self.FileLeaves: dict[str, _FileLeaf] = {}
Name = RelativePath.rsplit("/", 1)[-1] if RelativePath else Root
self.NameLabel = ft.Text(Name, size=13, color=Palette.Text, overflow=ft.TextOverflow.ELLIPSIS, max_lines=1)
self.NameContainer = ft.Container(
content=ft.GestureDetector(content=self.NameLabel, on_tap=lambda e: self.OnNavigate(self.Root, self.RelativePath)),
padding=4,
border_radius=4,
)
self.NameLabel, self.NameContainer = _TreeLabel(Name, lambda: self.OnNavigate(self.Root, self.RelativePath))
self.Tile = ft.ExpansionTile(
title=self.NameContainer,
leading=ft.Icon(ft.Icons.STORAGE_OUTLINED if not RelativePath else ft.Icons.FOLDER_OUTLINED,
@@ -50,30 +83,35 @@ class _FiletreeNode:
def LoadChildren(self, Update: bool = True) -> None:
try:
Directories = [Item.Name for Item in self.TheApp.Source.List(self.Root, self.RelativePath) if Item.IsDirectory]
Items = self.TheApp.Source.List(self.Root, self.RelativePath)
except SourceError:
Directories = [] # locked, gone or unreachable: just no children
Items = [] # locked, gone or unreachable: just no children
self.ChildNodes = {}
for Name in Directories:
ChildPath = JoinPath(self.RelativePath, Name)
self.ChildNodes[Name] = _FiletreeNode(self.TheApp, self.Root, ChildPath, self.OnNavigate, self.Depth + 1)
self.Tile.controls = [Node.Tile for Node in self.ChildNodes.values()]
self.FileLeaves = {}
for Item in Items:
ChildPath = JoinPath(self.RelativePath, Item.Name)
if Item.IsDirectory:
self.ChildNodes[Item.Name] = _FiletreeNode(self.TheApp, self.Root, ChildPath, self.OnNavigate,
self.OnOpenFile, self.Depth + 1)
elif self.TheApp.Config.ShowFilesInTree:
self.FileLeaves[Item.Name] = _FileLeaf(self.Root, ChildPath, Item.FileType, self.OnOpenFile, self.Depth + 1)
# Directories first, then files, like the file list.
self.Tile.controls = [Node.Tile for Node in self.ChildNodes.values()] + [Leaf.Tile for Leaf in self.FileLeaves.values()]
self.ChildrenLoaded = True
if Update:
self.Tile.update()
def SetSelected(self, IsSelected: bool) -> None:
self.NameContainer.bgcolor = Palette.Surface1 if IsSelected else None
self.NameLabel.color = Palette.Mauve if IsSelected else Palette.Text
self.NameContainer.update()
_SetHighlight(self.NameLabel, self.NameContainer, IsSelected)
class Filetree(ft.Container):
def __init__(self, TheApp: App, OnNavigate: Callable[[str, str], None]):
def __init__(self, TheApp: App, OnNavigate: Callable[[str, str], None], OnOpenFile: Callable[[str, str], None]):
self.TheApp = TheApp
self.OnNavigate = OnNavigate
self.OnOpenFile = OnOpenFile
self.RootNodes: dict[str, _FiletreeNode] = {}
self.SelectedNode: Optional[_FiletreeNode] = None
self.SelectedNode: Optional[Union[_FiletreeNode, _FileLeaf]] = None
self.TreeList = ft.ListView(expand=True)
self.SourceLabel = ft.Text("", size=12, color=Palette.Overlay0, overflow=ft.TextOverflow.ELLIPSIS, max_lines=1)
super().__init__(
@@ -87,7 +125,7 @@ class Filetree(ft.Container):
def Reload(self) -> None:
"""Rebuilds the tree from the current source (after connecting,
logging in or out, or a change that moved directories around)."""
logging in or out, or a change that moved things around)."""
Source = self.TheApp.Source
self.SourceLabel.value = "Not connected" if self.TheApp.ConnectError else Source.Describe()
self.SelectedNode = None
@@ -98,7 +136,7 @@ class Filetree(ft.Container):
except SourceError:
Roots = []
Expanded = {Root: Node.Tile.expanded for Root, Node in self.RootNodes.items()}
self.RootNodes = {Root: _FiletreeNode(self.TheApp, Root, "", self.OnNavigate, 0) for Root in Roots}
self.RootNodes = {Root: _FiletreeNode(self.TheApp, Root, "", self.OnNavigate, self.OnOpenFile, 0) for Root in Roots}
for Root, Node in self.RootNodes.items():
# One root (always the case locally) starts open; otherwise keep what was open.
if Expanded.get(Root, len(Roots) == 1):
@@ -108,21 +146,28 @@ class Filetree(ft.Container):
self.update()
def RevealPath(self, Root: Optional[str], RelativePath: str) -> None:
"""Expand and highlight the tree down to a directory, loading as needed."""
"""Expand and highlight the tree down to a directory or file, loading
as needed."""
if self.SelectedNode:
self.SelectedNode.SetSelected(False)
self.SelectedNode = None
Node = self.RootNodes.get(Root or "")
if Node is None:
return
for Part in [Part for Part in RelativePath.split("/") if Part]:
Target: Union[_FiletreeNode, _FileLeaf] = Node
Parts = [Part for Part in RelativePath.split("/") if Part]
for Index, Part in enumerate(Parts):
if not Node.ChildrenLoaded:
Node.LoadChildren()
NextNode = Node.ChildNodes.get(Part)
if NextNode is None:
break # hidden or not a directory: highlight the closest parent
Leaf = Node.FileLeaves.get(Part) if Index == len(Parts) - 1 else None
if NextNode is None and Leaf is None:
break # hidden: highlight the closest parent
Node.Tile.expanded = True
Node.Tile.update()
Node = NextNode
Node.SetSelected(True)
self.SelectedNode = Node
if Leaf is not None:
Target = Leaf
break
Node = Target = NextNode
Target.SetSelected(True)
self.SelectedNode = Target