mirror of
https://git.srhtnin.garden/sarah/SaraFem.git
synced 2026-10-15 02:23:17 +02:00
Add SaraSortd integration: show incoming/outgoing sort rules for the browsed directory
This commit is contained in:
1 parent
31b6c554e6
commit
d588d445b4
4 files changed
+436
-9
No files matched your search
+135
-9
@@ -16,6 +16,7 @@ from typing import Callable, Optional
|
||||
|
||||
import flet as ft
|
||||
import send2trash
|
||||
import toml
|
||||
from PIL import Image
|
||||
|
||||
from Bookmarks import AddBookmark
|
||||
@@ -24,10 +25,11 @@ from Bookmarks import IconChoices as BookmarkIconChoices
|
||||
from Bookmarks import IconLabelOverrides
|
||||
from Books import ClearSaraTagCache, IsInternalSaraTagFile
|
||||
from Core import (
|
||||
AppConfig, ApplyHeightBounds, ChipButtonStyle, DragGroup, FormatBatchName, NotificationStyle, Palette,
|
||||
ResizeHandle,
|
||||
AppConfig, ApplyHeightBounds, ChipButtonStyle, DragGroup, ExportSettings, FormatBatchName, ImportSettings,
|
||||
NotificationStyle, Palette, ResizeHandle,
|
||||
)
|
||||
from Preview import HumanSize, Preview, TextExtensions
|
||||
from SaraSortdInfo import GetSortRules
|
||||
from Settings import BuildSettingsDialog
|
||||
from Tags import TagPanel
|
||||
from Thumbnails import GetThumbnail, IsThumbnailable
|
||||
@@ -299,6 +301,18 @@ class Browser(ft.Row):
|
||||
expand=True,
|
||||
)
|
||||
self.EntryList = self._MakeEntryListContainer()
|
||||
# SaraSortd integration (Features/Major): "*.png -> ~/Pictures" style
|
||||
# lines for the currently-browsed directory. Mutated in place by
|
||||
# _RefreshSortBanner on every NavigateTo, rather than rebuilt as part
|
||||
# of _BuildChrome, since a plain directory change doesn't otherwise
|
||||
# touch FileListColumn.controls.
|
||||
self.SortBanner = ft.Container(
|
||||
visible=False,
|
||||
padding=ft.Padding.symmetric(horizontal=12, vertical=6),
|
||||
bgcolor=Palette.Surface0,
|
||||
border=ft.Border.all(1, Palette.Surface1),
|
||||
content=ft.Column([], spacing=2),
|
||||
)
|
||||
# One shared popup host, opened programmatically (see _RequestMenu) so
|
||||
# a right-click resolves to exactly one menu. Its built-in triggers
|
||||
# can't be switched off: Flet 1.0.1's client reads a None trigger as
|
||||
@@ -483,6 +497,7 @@ class Browser(ft.Row):
|
||||
# bottom toolbar sat right under the last entry. Inside that fixed
|
||||
# height, the inner Container's bgcolor makes it paint, and so
|
||||
# hit-test, everywhere, including below the last entry.
|
||||
Items.append(self.SortBanner)
|
||||
Items.append(
|
||||
ft.Container(
|
||||
expand=True,
|
||||
@@ -543,6 +558,8 @@ class Browser(ft.Row):
|
||||
# unbounded/scrollable axis and an expanding child on that same axis
|
||||
# don't mix. Right-alignment happens one level up instead, via the
|
||||
# Container ActionsToolbar sits in.
|
||||
if self.Config.ToolbarHamburger:
|
||||
return self._BuildHamburgerToolbar()
|
||||
Items: list[ft.Control] = []
|
||||
if self.Config.ShowNewDirectoryButton:
|
||||
Items.append(self.NewDirectoryButton)
|
||||
@@ -554,6 +571,40 @@ class Browser(ft.Row):
|
||||
Items.append(self.OptionsButton)
|
||||
return Items
|
||||
|
||||
def _BuildHamburgerToolbar(self) -> list[ft.Control]:
|
||||
"""Same buttons, collapsed into one overflow menu (Features/Minor:
|
||||
"Hamburger menu option instead of buttons for toolbar")."""
|
||||
Items: list[ft.PopupMenuItem] = []
|
||||
if self.Config.ShowNewDirectoryButton:
|
||||
Items.append(
|
||||
ft.PopupMenuItem(
|
||||
content="New Directory",
|
||||
icon=ft.Icons.CREATE_NEW_FOLDER_OUTLINED,
|
||||
on_click=lambda e: self._ShowNewDirectoryDialog(),
|
||||
)
|
||||
)
|
||||
if self.Config.ShowPasteButton:
|
||||
Items.append(
|
||||
ft.PopupMenuItem(content="Paste", icon=ft.Icons.CONTENT_PASTE_OUTLINED, on_click=lambda e: self._Paste())
|
||||
)
|
||||
if self.Config.ShowViewToggleButton:
|
||||
Items.append(
|
||||
ft.PopupMenuItem(
|
||||
content="Grid" if self.Config.ViewMode == "list" else "List",
|
||||
icon=ft.Icons.GRID_VIEW_OUTLINED if self.Config.ViewMode == "list" else ft.Icons.VIEW_LIST_OUTLINED,
|
||||
on_click=lambda e: self._ToggleViewMode(),
|
||||
)
|
||||
)
|
||||
if self.Config.ShowOptionsButton:
|
||||
Items.append(
|
||||
ft.PopupMenuItem(
|
||||
content="Options", icon=ft.Icons.SETTINGS_OUTLINED, on_click=lambda e: self._ShowSettingsDialog()
|
||||
)
|
||||
)
|
||||
if not Items:
|
||||
return []
|
||||
return [ft.PopupMenuButton(icon=ft.Icons.MENU, items=Items)]
|
||||
|
||||
def RefreshToolbarVisibility(self) -> None:
|
||||
self.ActionsToolbar.controls = self._BuildActionsToolbar()
|
||||
self.ActionsToolbar.update()
|
||||
@@ -580,6 +631,7 @@ class Browser(ft.Row):
|
||||
self.ViewToggleButton.icon = (
|
||||
ft.Icons.VIEW_LIST_OUTLINED if self.Config.ViewMode == "grid" else ft.Icons.GRID_VIEW_OUTLINED
|
||||
)
|
||||
self.RefreshToolbarVisibility() # the hamburger menu's "Grid"/"List" label needs the same refresh
|
||||
|
||||
self.EntryList = self._MakeEntryListContainer()
|
||||
self.EntryContextMenu.content = self._BuildEntryListContent()
|
||||
@@ -748,8 +800,26 @@ class Browser(ft.Row):
|
||||
|
||||
def _Refresh(self) -> None:
|
||||
self.Breadcrumbs.controls = self._BuildBreadcrumbs(self.CurrentPath)
|
||||
self._RefreshSortBanner()
|
||||
self._RefreshEntries()
|
||||
|
||||
def _RefreshSortBanner(self) -> None:
|
||||
Lines = GetSortRules(self.CurrentPath, self.Config)
|
||||
if not Lines:
|
||||
self.SortBanner.visible = False
|
||||
return
|
||||
self.SortBanner.visible = True
|
||||
self.SortBanner.content = ft.Column(
|
||||
[
|
||||
ft.Row(
|
||||
[ft.Icon(ft.Icons.SYNC_ALT, size=14, color=Palette.Mauve), ft.Text(Line, size=self.MetaFontSize, color=Palette.Text)],
|
||||
spacing=6,
|
||||
)
|
||||
for Line in Lines
|
||||
],
|
||||
spacing=2,
|
||||
)
|
||||
|
||||
def _RefreshEntries(self) -> None:
|
||||
self.EntryList.controls = self._BuildEntries(self.CurrentPath)
|
||||
self.update()
|
||||
@@ -820,7 +890,20 @@ class Browser(ft.Row):
|
||||
return [self._BuildListRow(Entry) for Entry in Entries]
|
||||
|
||||
MinColumnWidth = 40 # floor for header-divider drags, so a column can't vanish
|
||||
ListHeaderHeight = 28 # 12px text + 2*6px vertical padding — see _BuildListHeader
|
||||
|
||||
@property
|
||||
def MetaFontSize(self) -> int:
|
||||
"""Secondary text (metadata columns, the header) scales with the
|
||||
configurable name font size but stays a couple sizes smaller, same
|
||||
visual hierarchy as the fixed 14/12 before it was configurable."""
|
||||
return max(9, self.Config.FontSize - 2)
|
||||
|
||||
@property
|
||||
def ListHeaderHeight(self) -> int:
|
||||
"""Text line height + 2*6px vertical padding — see _BuildListHeader.
|
||||
Derived from MetaFontSize (not fixed) so the header's own resize
|
||||
divider still matches its actual height at any FontSize."""
|
||||
return self.MetaFontSize + 16
|
||||
|
||||
def _MetadataCell(self, Text: str, Width: int) -> tuple[ft.Container, ft.GestureDetector]:
|
||||
"""A fixed-width metadata column (Size/Modified/...): left-click
|
||||
@@ -829,7 +912,7 @@ class Browser(ft.Row):
|
||||
things than name shouldn't be right-clickable... it should behave
|
||||
like you're right-clicking the background")."""
|
||||
Cell = ft.Container(
|
||||
content=ft.Text(Text, size=12, color=Palette.Overlay0, overflow=ft.TextOverflow.ELLIPSIS),
|
||||
content=ft.Text(Text, size=self.MetaFontSize, color=Palette.Overlay0, overflow=ft.TextOverflow.ELLIPSIS),
|
||||
width=Width,
|
||||
padding=ft.Padding.symmetric(horizontal=8, vertical=8),
|
||||
)
|
||||
@@ -849,10 +932,11 @@ class Browser(ft.Row):
|
||||
# after the last shown column, before the triple-dot (still pinned
|
||||
# right, as before), is the background gap.
|
||||
NameLabel = ft.Text(
|
||||
Entry.name, size=14, color=Palette.Text, overflow=ft.TextOverflow.ELLIPSIS, max_lines=1
|
||||
Entry.name, size=self.Config.FontSize, color=Palette.Text,
|
||||
overflow=ft.TextOverflow.ELLIPSIS, max_lines=1,
|
||||
)
|
||||
NameCell = ft.Container(
|
||||
content=ft.Row([self._LeadingFor(Entry, Size=32), NameLabel], spacing=12),
|
||||
content=ft.Row([self._LeadingFor(Entry, Size=self.Config.ListIconSize), NameLabel], spacing=12),
|
||||
padding=ft.Padding.symmetric(horizontal=12, vertical=8),
|
||||
bgcolor=Palette.Surface0 if Entry in self.SelectedPaths else None,
|
||||
width=self.Config.ListWidthName,
|
||||
@@ -943,7 +1027,7 @@ class Browser(ft.Row):
|
||||
PreviousField: Optional[str] = None
|
||||
for Label, FieldName, LeftPad in Fields:
|
||||
Cell = ft.Container(
|
||||
ft.Text(Label, size=12, color=Palette.Overlay0),
|
||||
ft.Text(Label, size=self.MetaFontSize, color=Palette.Overlay0),
|
||||
width=getattr(self.Config, f"ListWidth{FieldName}"),
|
||||
padding=ft.Padding.only(left=LeftPad, right=8),
|
||||
)
|
||||
@@ -962,7 +1046,7 @@ class Browser(ft.Row):
|
||||
def _BuildGridCell(self, Entry: Path) -> ft.Control:
|
||||
IconControl = self._LeadingFor(Entry, Size=self.Config.GridIconSize)
|
||||
NameControl = ft.Text(
|
||||
Entry.name, size=11, text_align=ft.TextAlign.CENTER,
|
||||
Entry.name, size=self.Config.FontSize, text_align=ft.TextAlign.CENTER,
|
||||
max_lines=2, overflow=ft.TextOverflow.ELLIPSIS,
|
||||
)
|
||||
CellItems = [IconControl, NameControl] if self.Config.GridNameBelowIcon else [NameControl, IconControl]
|
||||
@@ -1017,6 +1101,7 @@ class Browser(ft.Row):
|
||||
ft.PopupMenuItem(
|
||||
content="Cut", on_click=lambda e, p=Entry: self._SetClipboard(self._TargetSelection(p), "cut")
|
||||
),
|
||||
ft.PopupMenuItem(content="Copy Path", on_click=lambda e, p=Entry: self._HandleCopyPath(p)),
|
||||
ft.PopupMenuItem(content="Properties", on_click=lambda e, p=Entry: self._HandleProperties(p)),
|
||||
ft.PopupMenuItem(content="Delete", on_click=lambda e, p=Entry: self._HandleDelete(p)),
|
||||
ft.PopupMenuItem(content="Compress", on_click=lambda e, p=Entry: self._HandleCompress(p)),
|
||||
@@ -1146,6 +1231,8 @@ class Browser(ft.Row):
|
||||
self._ToggleDotfiles()
|
||||
elif Event.key == "T" and Event.ctrl:
|
||||
self._ToggleTags()
|
||||
elif Event.key == "C" and Event.ctrl and Event.shift and self.SelectedPaths:
|
||||
self._HandleCopyPath(self.SelectedPath)
|
||||
elif Event.key == "C" and Event.ctrl and self.SelectedPaths:
|
||||
self._SetClipboard(sorted(self.SelectedPaths, key=lambda p: p.name.lower()), "copy")
|
||||
elif Event.key == "X" and Event.ctrl and self.SelectedPaths:
|
||||
@@ -1165,6 +1252,17 @@ class Browser(ft.Row):
|
||||
self.PasteButton.disabled = False
|
||||
self.update()
|
||||
|
||||
def _HandleCopyPath(self, Entry: Optional[Path]) -> None:
|
||||
if Entry is None:
|
||||
return
|
||||
Targets = self._TargetSelection(Entry)
|
||||
Text = "\n".join(str(P) for P in Targets)
|
||||
self.page.run_task(self._DoCopyPath, Text, len(Targets))
|
||||
|
||||
async def _DoCopyPath(self, Text: str, Count: int) -> None:
|
||||
await ft.Clipboard().set(Text)
|
||||
self._Notify(f"Copied {Count} path(s)." if Count > 1 else "Copied path.", Kind="success")
|
||||
|
||||
def _Paste(self) -> None:
|
||||
if not self.ClipboardPaths:
|
||||
return
|
||||
@@ -1418,6 +1516,7 @@ class Browser(ft.Row):
|
||||
self.Config.ViewMode,
|
||||
*(getattr(self.Config, ConfigFlag) for ConfigFlag, _Name, _Fn in ListColumns),
|
||||
self.Config.GridIconSize, self.Config.GridNameBelowIcon,
|
||||
self.Config.ListIconSize, self.Config.FontSize,
|
||||
)
|
||||
|
||||
def _ShowSettingsDialog(self) -> None:
|
||||
@@ -1439,6 +1538,7 @@ class Browser(ft.Row):
|
||||
)
|
||||
self.EntryList = self._MakeEntryListContainer()
|
||||
self.EntryContextMenu.content = self._BuildEntryListContent()
|
||||
self._RefreshSortBanner() # a just-toggled Enabled/GlobalConfPath should take effect immediately
|
||||
self.FileListColumn.controls = self._BuildChrome()
|
||||
self.FileListColumn.update()
|
||||
self.controls = self._BuildLayout()
|
||||
@@ -1450,7 +1550,33 @@ class Browser(ft.Row):
|
||||
self.OnSidebarSettingsChanged()
|
||||
self._Notify("Options saved.", Kind="success")
|
||||
|
||||
Dialog = BuildSettingsDialog(self.Config, OnSave=OnSave, OnDismiss=self._DismissDialog)
|
||||
def OnExport(PathText: str) -> None:
|
||||
if not PathText:
|
||||
return
|
||||
try:
|
||||
ExportSettings(self.Config, Path(PathText))
|
||||
except OSError as Error:
|
||||
self._Notify(f"Couldn't export settings: {Error}")
|
||||
return
|
||||
self._Notify("Settings exported.", Kind="success")
|
||||
|
||||
def OnImport(PathText: str) -> None:
|
||||
if not PathText:
|
||||
return
|
||||
try:
|
||||
ImportSettings(self.Config, Path(PathText))
|
||||
except (OSError, toml.TomlDecodeError) as Error:
|
||||
self._Notify(f"Couldn't import settings: {Error}")
|
||||
return
|
||||
# The open dialog's widgets still show the pre-import values —
|
||||
# closing it rather than leaving it open to a stale Save that
|
||||
# would immediately overwrite what was just imported.
|
||||
self._DismissDialog()
|
||||
self._Notify("Settings imported — reopen Options to see them, restart for Theme/Font/Title bar.", Kind="success")
|
||||
|
||||
Dialog = BuildSettingsDialog(
|
||||
self.Config, OnSave=OnSave, OnDismiss=self._DismissDialog, OnExport=OnExport, OnImport=OnImport
|
||||
)
|
||||
self._ShowDialog(Dialog)
|
||||
|
||||
def _HandleProperties(self, Entry: Path) -> None:
|
||||
|
||||
Reference in new issue
Block a user