Files
SaraFem/Sidebar.py
T
2026-09-24 22:03:58 +02:00

233 lines
9.1 KiB
Python

"""Left-hand panel: Bookmarks (flat, user-curated — Trash is just one of
them, and they're drag-reorderable), Devices (removable media via udisks2,
Devices.py) and a real Filetree (Filetree.py), each independently shown/
hidden via Config. Bookmarks and Filetree both accept file/directory drops
from the Browser's file list to move things into them."""
from pathlib import Path
from typing import Callable, Optional
import flet as ft
from Bookmarks import IconChoices, LoadBookmarks, SaveBookmarks
from Core import (
AppConfig,
ApplyHeightBounds,
ChipButtonStyle,
DragGroup,
MoveIntoDirectory,
NotificationStyle,
Palette,
)
from Devices import DevicesPanel
from Filetree import Filetree
class Sidebar(ft.Container):
def __init__(
self,
OnNavigate: Callable[[Path], None],
Config: AppConfig,
OnFilesMoved: Optional[Callable[[], None]] = None,
):
self.OnNavigate = OnNavigate
self.Config = Config
self.OnFilesMoved = OnFilesMoved
self.LocationTiles: dict[Path, Callable[[bool], None]] = {}
self.BookmarksListView: Optional[ft.ReorderableListView] = None
self.DevicesView: Optional[DevicesPanel] = None
self.FiletreeView: Optional[Filetree] = None
Sections: list[ft.Control] = []
SplitEvenly = Config.SplitBookmarksFiletreeEvenly and Config.ShowBookmarks and Config.ShowFiletree
if Config.ShowBookmarks:
self.BookmarksListView = ft.ReorderableListView(
controls=self._BuildBookmarkTiles(),
spacing=0,
show_default_drag_handles=False,
on_reorder=self._OnBookmarksReordered,
)
if SplitEvenly:
self.BookmarksListView.expand = 1
Sections.append(self.BookmarksListView)
else:
Sections.append(
ApplyHeightBounds(self.BookmarksListView, Config.BookmarksMinHeight, Config.BookmarksMaxHeight)
)
if Config.ShowBookmarks and (Config.ShowDevices or Config.ShowFiletree):
Sections.append(ft.Divider(height=1, color=Palette.Surface1))
if Config.ShowDevices:
self.DevicesView = DevicesPanel(OnNavigate=OnNavigate, Config=Config)
Sections.append(
ApplyHeightBounds(self.DevicesView, Config.DevicesMinHeight, Config.DevicesMaxHeight)
)
if Config.ShowDevices and Config.ShowFiletree:
Sections.append(ft.Divider(height=1, color=Palette.Surface1))
if Config.ShowFiletree:
self.FiletreeView = Filetree(
OnNavigate=OnNavigate,
Config=Config,
RootPath=Path(Config.FiletreeRoot or "/"),
OnFilesMoved=OnFilesMoved,
)
if SplitEvenly:
self.FiletreeView.expand = 1
Sections.append(self.FiletreeView)
else:
Sections.append(
ApplyHeightBounds(self.FiletreeView, Config.FiletreeMinHeight, Config.FiletreeMaxHeight)
)
BorderSide = ft.BorderSide(1, Palette.Surface1)
Border = ft.Border(left=BorderSide) if Config.SidebarSide == "right" else ft.Border(right=BorderSide)
super().__init__(
content=ft.Column(Sections, spacing=0, expand=True),
width=Config.SidebarWidth,
padding=0,
bgcolor=Palette.Mantle,
border=Border,
)
def _BuildBookmarkTiles(self) -> list[ft.Control]:
self.LocationTiles = {}
return [self._BuildBookmarkTile(B) for B in LoadBookmarks()]
def RefreshBookmarks(self) -> None:
"""Called after add/edit/remove/reorder so the sidebar updates
immediately — no restart needed for bookmark changes."""
if self.BookmarksListView is None:
return
self.BookmarksListView.controls = self._BuildBookmarkTiles()
self.BookmarksListView.update()
def _OnBookmarksReordered(self, e: ft.OnReorderEvent) -> None:
Bookmarks = LoadBookmarks()
Moved = Bookmarks.pop(e.old_index)
Bookmarks.insert(e.new_index, Moved)
SaveBookmarks(Bookmarks)
self.RefreshBookmarks()
def _BuildBookmarkTile(self, Bookmark) -> ft.Control:
# Not a ListTile: the drag-to-reorder handle needs to wrap everything
# except the triple-dot menu, which ListTile's fixed leading/title/
# trailing layout doesn't let us carve out — so this is a plain Row.
NameLabel = ft.Text(Bookmark.Name, size=14, color=Palette.Text)
RowContent = ft.Container(
content=ft.Row(
[
ft.Icon(getattr(ft.Icons, Bookmark.Icon, ft.Icons.BOOKMARK_OUTLINE), size=20, color=Palette.Overlay0),
NameLabel,
],
spacing=12,
),
padding=ft.Padding.symmetric(horizontal=16, vertical=10),
border_radius=4,
ink=True,
on_click=lambda e, p=Bookmark.Path: self.OnNavigate(p),
)
def SetSelected(IsSelected: bool) -> None:
RowContent.bgcolor = Palette.Surface1 if IsSelected else None
NameLabel.color = Palette.Mauve if IsSelected else Palette.Text
self.LocationTiles[Bookmark.Path] = SetSelected
Row = ft.Row(
[
ft.Container(ft.ReorderableDragHandle(content=RowContent), expand=True),
ft.PopupMenuButton(
icon=ft.Icons.MORE_VERT,
items=[
ft.PopupMenuItem(
content="Edit", on_click=lambda e, b=Bookmark: self._ShowEditBookmarkDialog(b)
),
],
),
],
spacing=0,
)
return ft.DragTarget(
group=DragGroup,
content=Row,
on_accept=lambda e, dest=Bookmark.Path: self._OnEntryDropped(e, dest),
)
def _OnEntryDropped(self, e: ft.DragTargetEvent, DestinationDir: Path) -> None:
SourcePath = Path(e.src.data)
try:
MoveIntoDirectory(SourcePath, DestinationDir)
except OSError as Error:
self._Notify(str(Error))
return
self.RefreshBookmarks()
if self.FiletreeView is not None:
self.FiletreeView.RevealPath(DestinationDir)
if self.OnFilesMoved:
self.OnFilesMoved()
self._Notify(f'Moved "{SourcePath.name}" to {DestinationDir.name or DestinationDir}.', Kind="success")
def _ShowEditBookmarkDialog(self, Bookmark) -> None:
NameField = ft.TextField(value=Bookmark.Name, autofocus=True, label="Name")
PathField = ft.TextField(value=str(Bookmark.Path), label="Path")
IconDropdown = ft.Dropdown(
value=Bookmark.Icon,
label="Icon",
options=[ft.DropdownOption(key=Name, text=Name.split("_")[0].title()) for Name in IconChoices],
)
def DoSave(e=None) -> None:
self.page.pop_dialog()
Bookmarks = LoadBookmarks()
for B in Bookmarks:
if B.Path == Bookmark.Path:
B.Name = NameField.value.strip() or Bookmark.Name
B.Path = Path(PathField.value.strip() or Bookmark.Path)
B.Icon = IconDropdown.value or Bookmark.Icon
SaveBookmarks(Bookmarks)
self.RefreshBookmarks()
self._Notify("Bookmark updated.", Kind="success")
def DoRemove(e=None) -> None:
self.page.pop_dialog()
SaveBookmarks([B for B in LoadBookmarks() if B.Path != Bookmark.Path])
self.RefreshBookmarks()
self._Notify("Bookmark removed.", Kind="success")
Dialog = ft.AlertDialog(
title="Edit Bookmark",
content=ft.Column([NameField, PathField, IconDropdown], tight=True, width=320),
actions=[
ft.OutlinedButton(content="Remove", style=ChipButtonStyle(), on_click=DoRemove),
ft.OutlinedButton(content="Cancel", style=ChipButtonStyle(), on_click=lambda e: self.page.pop_dialog()),
ft.OutlinedButton(content="Save", style=ChipButtonStyle(), on_click=DoSave),
],
)
self.page.show_dialog(Dialog)
def _Notify(self, Message: str, Kind: str = "error") -> None:
Background, Accent = NotificationStyle(Kind)
self.page.show_dialog(
ft.SnackBar(
content=ft.Text(Message, color=Accent),
bgcolor=Background,
shape=ft.RoundedRectangleBorder(radius=4, side=ft.BorderSide(1, Accent)),
open=True,
)
)
def UpdateSelection(self, CurrentPath: Path) -> None:
for LocationPath, SetSelected in self.LocationTiles.items():
SetSelected(LocationPath == CurrentPath)
if self.LocationTiles:
self.update()
if self.DevicesView is not None:
self.DevicesView.UpdateSelection(CurrentPath)
if self.FiletreeView is not None:
self.FiletreeView.RevealPath(CurrentPath)