mirror of
https://git.srhtnin.garden/sarah/SaraFem.git
synced 2026-10-15 02:23:17 +02:00
Initial commit
This commit is contained in:
commit
4c8d272719
31 files changed
+3870
No files matched your search
+232
@@ -0,0 +1,232 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user