Fixed download sizes and directory highlighting

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
sarahandClaude Opus 5.5 committed 2026-10-13 14:46:47 +02:00
1 parent 843ad1986e
commit d4c018a402
8 files changed
+113 -37

No files matched your search

+45 -23
View File
@@ -11,7 +11,7 @@ from typing import Callable, Optional
import flet as ft
from App import App
from Core import ChipButtonStyle, FileIcons, HumanSize, OpenExternally, Palette
from Core import ChipButtonStyle, FileIcons, HumanDuration, HumanSize, OpenExternally, Palette
from Sources import BaseName, Entry, JoinPath, MoveDestination, ParentPath, SourceError
from Graph import GraphView
from Viewer import FileView
@@ -84,11 +84,12 @@ class Browser(ft.Column):
spacing=8,
tight=True,
)
# Shown while uploading: which file, how far (Uploads' progress).
self.UploadText = ft.Text("", size=12, color=Palette.Overlay0)
self.UploadBar = ft.ProgressBar(value=0, color=Palette.Mauve, bgcolor=Palette.Surface0, bar_height=4)
self.UploadRow = ft.Container(ft.Column([self.UploadText, self.UploadBar], spacing=4),
# Shown while uploading or downloading: which file, how far, how long (ShowTransfer).
self.TransferText = ft.Text("", size=12, color=Palette.Overlay0)
self.TransferBar = ft.ProgressBar(value=0, color=Palette.Mauve, bgcolor=Palette.Surface0, bar_height=4)
self.TransferRow = ft.Container(ft.Column([self.TransferText, self.TransferBar], spacing=4),
padding=ft.Padding.symmetric(horizontal=8, vertical=6), visible=False)
self.TransferShown: Optional[tuple] = None # (text, percent) on screen
self.EntryList = ft.ListView(expand=True, spacing=2)
# One shared popup host, opened programmatically (see _RequestMenu).
# Its built-in triggers can't be switched off, so they're set to
@@ -108,7 +109,7 @@ class Browser(ft.Column):
# and the breadcrumbs get the rest (and wrap when it's tight).
ft.Row([ft.Container(self.Breadcrumbs, padding=8, expand=True), ft.Container(self.ActionsToolbar, padding=8)]),
ft.Divider(height=1, color=Palette.Surface1),
self.UploadRow,
self.TransferRow,
self.FileArea,
]
self._ShowList(Update=False)
@@ -610,21 +611,14 @@ class Browser(ft.Column):
Total = sum(Sizes)
Verb = "Uploading" if Server else "Copying in"
Shown = [-1, -1] # (file, percent) on screen: redraw only when it changes
Started = time.monotonic()
def ShowProgress(Index: int, Fraction: float) -> None:
Done = sum(Sizes[:Index]) + Sizes[Index] * Fraction
Percent = int(Done / Total * 100) if Total else 100
if Shown == [Index, Percent]:
return
Shown[:] = [Index, Percent]
Of = f" ({Index + 1} of {len(Paths)})" if len(Paths) > 1 else ""
self.UploadText.value = (f"{Verb} {Paths[Index].name}{Of}: {Percent}% "
f"({HumanSize(int(Done))} of {HumanSize(Total)})")
self.UploadBar.value = Done / Total if Total else 1
self.UploadRow.update()
self._ShowTransfer(f"{Verb} {Paths[Index].name}{Of}", sum(Sizes[:Index]) + Sizes[Index] * Fraction, Total, Started)
self.UploadRow.visible = True
self.TransferShown = None
self.TransferRow.visible = True
for Index, LocalFile in enumerate(Paths):
ShowProgress(Index, 0)
try:
@@ -634,15 +628,15 @@ class Browser(ft.Column):
Saved += 1
except SourceError as Error:
if Error.Status == 401:
self.UploadRow.visible = False
self.UploadRow.update()
self.TransferRow.visible = False
self.TransferRow.update()
self.TheApp.HandleError(Error)
return
Failed.append(f"{LocalFile.name}: {Error}")
except OSError as Error:
Failed.append(f"{LocalFile.name}: {Error}")
self.UploadRow.visible = False
self.UploadRow.update()
self.TransferRow.visible = False
self.TransferRow.update()
self.Refresh()
if Failed:
self.TheApp.Notify("Couldn't upload " + "; ".join(Failed), "error")
@@ -663,14 +657,42 @@ class Browser(ft.Column):
initial_directory=str(Path.home() / "Downloads"))
if not Chosen:
return
self.TheApp.Notify(f"Downloading {FileName}...", "info")
Started = time.monotonic()
self.TransferShown = None
self.TransferText.value = f"Preparing {FileName}…" # a directory is zipped first
self.TransferBar.value = None # moving, until the size is known
self.TransferRow.visible = True
self.TransferRow.update()
try:
await asyncio.to_thread(Server.Download, Root, RelativePath, Path(Chosen))
await asyncio.to_thread(Server.Download, Root, RelativePath, Path(Chosen),
lambda Done, Total: self._ShowTransfer(f"Downloading {FileName}", Done, Total, Started))
except SourceError as Error:
self.TheApp.HandleError(Error, f"Couldn't download {Name}")
return
finally:
self.TransferRow.visible = False
self.TransferRow.update()
self.TheApp.Notify(f"Saved {Chosen}", "success")
def _ShowTransfer(self, What: str, Done: float, Total: int, Started: float) -> None:
"""The transfer row: "Downloading a.zip: 45% (54.0 MB of 122.1 MB,
about 12 s left)". Redraws only when the percentage changes."""
Percent = int(Done / Total * 100) if Total else None
if self.TransferShown == (What, Percent):
return
self.TransferShown = (What, Percent)
Elapsed = time.monotonic() - Started
if Percent is None:
Detail = f"{HumanSize(int(Done))}"
else:
Detail = f"{Percent}% ({HumanSize(int(Done))} of {HumanSize(Total)}"
if Elapsed > 2 and 0 < Done < Total:
Detail += f", about {HumanDuration((Total - Done) * Elapsed / Done)} left"
Detail += ")"
self.TransferText.value = f"{What}: {Detail}"
self.TransferBar.value = Done / Total if Total else None
self.TransferRow.update()
# --- Keyboard ---------------------------------------------------------
def _SetShortcutsPaused(self, Paused: bool) -> None:
+11
View File
@@ -539,6 +539,17 @@ def HumanSize(NumBytes: Optional[int]) -> str:
return f"{Size:.1f} TB"
def HumanDuration(Seconds: float) -> str:
"""Roughly, for "about ... left": 8 s, 3 min, 1 h, 1 h 20 min."""
Seconds = max(1, round(Seconds))
if Seconds < 60:
return f"{Seconds} s"
Hours, Minutes = divmod(round(Seconds / 60), 60)
if not Hours:
return f"{Minutes} min"
return f"{Hours} h {Minutes} min" if Minutes else f"{Hours} h"
def DisplayPath(PathText: str) -> str:
"""/home/sarah/Notes -> ~/Notes, for labels (C:\\Users\\x\\Notes -> ~\\Notes)."""
HomeText = str(Home)
+24
View File
@@ -26,6 +26,12 @@ def _SetHighlight(Label: ft.Text, Holder: ft.Container, IsSelected: bool) -> Non
Holder.update()
def _SetOnPath(Holder: ft.Container, IsOnPath: bool) -> None:
"""The bar beside every directory you're in (the web view's TreeRowCurrentDirectory)."""
Holder.border = ft.Border.only(left=ft.BorderSide(2, Palette.Mauve)) if IsOnPath else None
Holder.update()
class _FileLeaf:
"""A file in the tree: a plain row (no expand arrow) indented like a
child tile, so names line up with the directories beside it."""
@@ -104,6 +110,9 @@ class _FiletreeNode:
def SetSelected(self, IsSelected: bool) -> None:
_SetHighlight(self.NameLabel, self.NameContainer, IsSelected)
def SetOnPath(self, IsOnPath: bool) -> None:
_SetOnPath(self.NameContainer, IsOnPath)
class Filetree(ft.Container):
def __init__(self, TheApp: App, OnNavigate: Callable[[str, str], None], OnOpenFile: Callable[[str, str], None]):
@@ -112,6 +121,7 @@ class Filetree(ft.Container):
self.OnOpenFile = OnOpenFile
self.RootNodes: dict[str, _FiletreeNode] = {}
self.SelectedNode: Optional[Union[_FiletreeNode, _FileLeaf]] = None
self.PathNodes: list[_FiletreeNode] = [] # the directories you're in, marked
self.TreeList = ft.ListView(expand=True)
self.SourceLabel = ft.Text("", size=12, color=Palette.Overlay0, overflow=ft.TextOverflow.ELLIPSIS, max_lines=1)
super().__init__(
@@ -129,6 +139,7 @@ class Filetree(ft.Container):
Source = self.TheApp.Source
self.SourceLabel.value = "Not connected" if self.TheApp.ConnectError else Source.Describe()
self.SelectedNode = None
self.PathNodes = []
Roots: list[str] = []
if not self.TheApp.ConnectError:
try:
@@ -151,6 +162,9 @@ class Filetree(ft.Container):
if self.SelectedNode:
self.SelectedNode.SetSelected(False)
self.SelectedNode = None
for Marked in self.PathNodes:
Marked.SetOnPath(False)
self.PathNodes = []
Node = self.RootNodes.get(Root or "")
if Node is None:
return
@@ -169,5 +183,15 @@ class Filetree(ft.Container):
Target = Leaf
break
Node = Target = NextNode
# Every directory down to (and including) the one you're in, like the web view.
Walk = self.RootNodes.get(Root or "")
self.PathNodes = [Walk] if Walk else []
for Part in Parts:
Walk = Walk.ChildNodes.get(Part) if Walk else None
if Walk is None:
break
self.PathNodes.append(Walk)
for Marked in self.PathNodes:
Marked.SetOnPath(True)
Target.SetSelected(True)
self.SelectedNode = Target
+9 -2
View File
@@ -435,8 +435,10 @@ class ServerSource(Source):
Done = {**Params, "UploadId": UploadId, "TotalChunks": TotalChunks, "FileName": LocalFile.name}
return self._Json("POST", "/upload/complete", params=Done)["Saved"]
def Download(self, Root: str, RelativePath: str, Destination: Path) -> None:
"""A file as-is, a directory as a .zip (the server builds it)."""
def Download(self, Root: str, RelativePath: str, Destination: Path,
OnProgress: Optional[Callable[[int, int], None]] = None) -> None:
"""A file as-is, a directory as a .zip (the server builds it).
OnProgress(BytesReceived, TotalBytes) as it arrives (Total 0 if unknown)."""
if not self.Url:
raise SourceError("No server address yet.")
Partial = Destination.with_name(Destination.name + ".part")
@@ -445,9 +447,14 @@ class ServerSource(Source):
if not Response.is_success:
Response.read()
raise SourceError(f"Download failed: {Response.status_code} {Response.reason_phrase}", Response.status_code)
Total = int(Response.headers.get("Content-Length") or 0)
Received = 0
with Partial.open("wb") as Handle:
for Chunk in Response.iter_bytes(1024 * 1024):
Handle.write(Chunk)
Received += len(Chunk)
if OnProgress:
OnProgress(Received, Total)
Partial.replace(Destination)
except httpx.HTTPError as Error:
Partial.unlink(missing_ok=True)