mirror of
https://git.srhtnin.garden/sarah/SaraFem.git
synced 2026-10-15 02:23:17 +02:00
Add Refresh button/Ctrl+R; add custom folder icons (per-directory override + global fullness-variant system)
This commit is contained in:
1 parent
1c2acf3f16
commit
c542134f62
7 files changed
+581
-48
No files matched your search
+47
-9
@@ -46,6 +46,11 @@ class Device:
|
||||
MountPoint: Optional[Path]
|
||||
DrivePath: str = ""
|
||||
CanPowerOff: bool = False
|
||||
# True only for an internal drive mounted at a system path ("/", "/home",
|
||||
# ...) shown because of Config.ShowSystemDrive — _BuildTiles never shows
|
||||
# an eject button for one of these, no matter what CanPowerOff says.
|
||||
# Unmounting your own running root filesystem would crash the system.
|
||||
IsSystemMount: bool = False
|
||||
|
||||
|
||||
def _HumanSize(Bytes: int) -> str:
|
||||
@@ -95,7 +100,7 @@ class UDisksClient:
|
||||
raise OSError(Reply.body[0] if Reply.body else Reply.error_name)
|
||||
return Reply.body
|
||||
|
||||
async def ListDevices(self, ShowInternal: bool) -> list[Device]:
|
||||
async def ListDevices(self, ShowInternal: bool, ShowSystemDrive: bool = False) -> list[Device]:
|
||||
Objects = (await self._Call(UDisksPath, "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"))[0]
|
||||
Devices = []
|
||||
for ObjectPath, Interfaces in Objects.items():
|
||||
@@ -110,11 +115,21 @@ class UDisksClient:
|
||||
for Raw in _Value(Interfaces[FilesystemInterface], "MountPoints", [])
|
||||
]
|
||||
MountPoint = MountPoints[0] if MountPoints else None
|
||||
IsSystemMount = False
|
||||
if _Value(Block, "HintSystem", False):
|
||||
# Internal disks: only unmounted ones or ones mounted like
|
||||
# removable media, never "/", "/home", "/boot" and friends.
|
||||
if not ShowInternal or (MountPoint is not None and not _UserMount(MountPoint)):
|
||||
# Internal disks: unmounted ones or ones mounted like
|
||||
# removable media always count as "internal". One mounted at
|
||||
# a system path only counts when ShowSystemDrive is also on,
|
||||
# and even then only the root "/" itself — udisks2 reports
|
||||
# /boot, /boot/efi, etc. as their own separate block devices,
|
||||
# and showing every one of those as its own non-ejectable
|
||||
# entry was more than "the main drive" (singular) implied.
|
||||
if not ShowInternal:
|
||||
continue
|
||||
if MountPoint is not None and not _UserMount(MountPoint):
|
||||
if not ShowSystemDrive or MountPoint != Path("/"):
|
||||
continue
|
||||
IsSystemMount = True
|
||||
Drive = Objects.get(DrivePath, {}).get(DriveInterface, {})
|
||||
Label = _Value(Block, "IdLabel", "") or _Value(Block, "HintName", "")
|
||||
if not Label:
|
||||
@@ -126,6 +141,7 @@ class UDisksClient:
|
||||
MountPoint=MountPoint,
|
||||
DrivePath=DrivePath,
|
||||
CanPowerOff=bool(_Value(Drive, "CanPowerOff", False)),
|
||||
IsSystemMount=IsSystemMount,
|
||||
))
|
||||
return sorted(Devices, key=lambda D: D.Label.lower())
|
||||
|
||||
@@ -156,9 +172,19 @@ class UDisksClient:
|
||||
|
||||
|
||||
class DevicesPanel(ft.Container):
|
||||
def __init__(self, OnNavigate: Callable[[Path], None], Config: AppConfig):
|
||||
def __init__(
|
||||
self,
|
||||
OnNavigate: Callable[[Path], None],
|
||||
Config: AppConfig,
|
||||
OnEmptyChanged: Optional[Callable[[bool], None]] = None,
|
||||
):
|
||||
self.OnNavigate = OnNavigate
|
||||
self.Config = Config
|
||||
# Features/Minor: "Option to hide devices section if there are none,
|
||||
# but show if there are" — fired from _Show only on an empty/non-
|
||||
# empty transition (not every device-list change), so Sidebar isn't
|
||||
# re-toggling visibility on every plug/unplug of an already-nonempty list.
|
||||
self.OnEmptyChanged = OnEmptyChanged
|
||||
self.Client: Optional[UDisksClient] = None
|
||||
self.CurrentDevices: list[Device] = []
|
||||
self.CurrentPath: Optional[Path] = None
|
||||
@@ -212,7 +238,7 @@ class DevicesPanel(ft.Container):
|
||||
|
||||
async def _Refresh(self, IsStartup: bool = False) -> None:
|
||||
try:
|
||||
Devices = await self.Client.ListDevices(self.Config.ShowInternalDrives)
|
||||
Devices = await self.Client.ListDevices(self.Config.ShowInternalDrives, self.Config.ShowSystemDrive)
|
||||
except Exception:
|
||||
return
|
||||
Known = {D.Key for D in self.CurrentDevices}
|
||||
@@ -224,6 +250,7 @@ class DevicesPanel(ft.Container):
|
||||
await self._MountDevice(NewDevice, Open=False)
|
||||
|
||||
def _Show(self, Devices: list[Device], Force: bool = False) -> None:
|
||||
WasEmpty = not self.CurrentDevices
|
||||
if Devices == self.CurrentDevices and not Force:
|
||||
return
|
||||
self.CurrentDevices = Devices
|
||||
@@ -231,6 +258,14 @@ class DevicesPanel(ft.Container):
|
||||
if self.CurrentPath is not None:
|
||||
self._ApplySelection()
|
||||
self.update()
|
||||
IsEmpty = not Devices
|
||||
# Force (startup) always fires too, not just real transitions: an
|
||||
# empty list right from the start is still "WasEmpty == IsEmpty",
|
||||
# which looks like a no-op transition but is actually the one time
|
||||
# Sidebar's listener most needs the initial state explicitly, since
|
||||
# DevicesBox otherwise defaults to visible with nothing to show.
|
||||
if self.OnEmptyChanged is not None and (IsEmpty != WasEmpty or Force):
|
||||
self.OnEmptyChanged(IsEmpty)
|
||||
|
||||
def _BuildTiles(self) -> list[ft.Control]:
|
||||
self.DeviceTiles = {}
|
||||
@@ -250,7 +285,10 @@ class DevicesPanel(ft.Container):
|
||||
icon=ft.Icons.EJECT,
|
||||
icon_color=Palette.Overlay0,
|
||||
tooltip="Eject",
|
||||
visible=self.Client is not None and (Mounted or Current.CanPowerOff),
|
||||
# Never offer to eject the system/boot drive — unmounting
|
||||
# your own running root filesystem would crash it.
|
||||
visible=self.Client is not None and not Current.IsSystemMount
|
||||
and (Mounted or Current.CanPowerOff),
|
||||
on_click=lambda e, D=Current: self.page.run_task(self._EjectDevice, D),
|
||||
),
|
||||
tooltip=str(Current.MountPoint) if Mounted else "Not mounted: click to mount",
|
||||
@@ -284,8 +322,8 @@ class DevicesPanel(ft.Container):
|
||||
self.OnNavigate(MountPoint)
|
||||
|
||||
async def _EjectDevice(self, Target: Device) -> None:
|
||||
if self.Client is None or Target.Key in self.Busy:
|
||||
return
|
||||
if self.Client is None or Target.Key in self.Busy or Target.IsSystemMount:
|
||||
return # never unmount the system drive, regardless of how this got called
|
||||
self.Busy.add(Target.Key)
|
||||
try:
|
||||
if Target.MountPoint is not None:
|
||||
|
||||
Reference in new issue
Block a user