mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 02:38:11 +02:00
129 lines
5.6 KiB
Python
129 lines
5.6 KiB
Python
"""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."""
|
|
|
|
from typing import Callable, Optional
|
|
|
|
import flet as ft
|
|
|
|
from App import App
|
|
from Core import Palette
|
|
from Sources import JoinPath, SourceError
|
|
|
|
Location = tuple[str, str] # (Root, Path)
|
|
|
|
|
|
class _FiletreeNode:
|
|
def __init__(self, TheApp: App, Root: str, RelativePath: str, OnNavigate: Callable[[str, str], None], Depth: int):
|
|
self.TheApp = TheApp
|
|
self.Root = Root
|
|
self.RelativePath = RelativePath
|
|
self.OnNavigate = OnNavigate
|
|
self.Depth = Depth
|
|
self.ChildrenLoaded = False
|
|
self.ChildNodes: dict[str, "_FiletreeNode"] = {}
|
|
|
|
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.Tile = ft.ExpansionTile(
|
|
title=self.NameContainer,
|
|
leading=ft.Icon(ft.Icons.STORAGE_OUTLINED if not RelativePath else ft.Icons.FOLDER_OUTLINED,
|
|
size=16, color=Palette.Mauve if not RelativePath else Palette.Overlay0),
|
|
controls=[],
|
|
tile_padding=ft.Padding.only(left=8 + Depth * 14, right=8),
|
|
icon_color=Palette.Overlay0,
|
|
collapsed_icon_color=Palette.Overlay0,
|
|
# No shape: the default draws divider lines above and below an open tile.
|
|
shape=ft.RoundedRectangleBorder(radius=4),
|
|
collapsed_shape=ft.RoundedRectangleBorder(radius=4),
|
|
on_change=self._OnExpansionChanged,
|
|
)
|
|
|
|
def _OnExpansionChanged(self, e) -> None:
|
|
if self.Tile.expanded and not self.ChildrenLoaded:
|
|
self.LoadChildren()
|
|
|
|
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]
|
|
except SourceError:
|
|
Directories = [] # 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.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()
|
|
|
|
|
|
class Filetree(ft.Container):
|
|
def __init__(self, TheApp: App, OnNavigate: Callable[[str, str], None]):
|
|
self.TheApp = TheApp
|
|
self.OnNavigate = OnNavigate
|
|
self.RootNodes: dict[str, _FiletreeNode] = {}
|
|
self.SelectedNode: Optional[_FiletreeNode] = 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__(
|
|
content=ft.Column([
|
|
ft.Container(self.SourceLabel, padding=ft.Padding.only(left=12, right=12, top=10, bottom=4)),
|
|
self.TreeList,
|
|
], spacing=0, expand=True),
|
|
width=TheApp.Config.SidebarWidth,
|
|
bgcolor=Palette.Mantle,
|
|
)
|
|
|
|
def Reload(self) -> None:
|
|
"""Rebuilds the tree from the current source (after connecting,
|
|
logging in or out, or a change that moved directories around)."""
|
|
Source = self.TheApp.Source
|
|
self.SourceLabel.value = "Not connected" if self.TheApp.ConnectError else Source.Describe()
|
|
self.SelectedNode = None
|
|
Roots: list[str] = []
|
|
if not self.TheApp.ConnectError:
|
|
try:
|
|
Roots = Source.Roots()
|
|
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}
|
|
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):
|
|
Node.LoadChildren(Update=False)
|
|
Node.Tile.expanded = True
|
|
self.TreeList.controls = [Node.Tile for Node in self.RootNodes.values()]
|
|
self.update()
|
|
|
|
def RevealPath(self, Root: Optional[str], RelativePath: str) -> None:
|
|
"""Expand and highlight the tree down to a directory, 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]:
|
|
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
|
|
Node.Tile.expanded = True
|
|
Node.Tile.update()
|
|
Node = NextNode
|
|
Node.SetSelected(True)
|
|
self.SelectedNode = Node
|