mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-13 18:28:14 +02:00
Fixed download sizes and directory highlighting
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
843ad1986e
commit
d4c018a402
8 files changed
+113
-37
No files matched your search
+3
-6
@@ -800,12 +800,9 @@ def GetDownloadEndpoint(Root: str, Path: str, CurrentUser: dict = Depends(GetCur
|
|||||||
EncodedName = quote(DownloadName)
|
EncodedName = quote(DownloadName)
|
||||||
Headers = {"Content-Disposition": f"attachment; filename*=UTF-8''{EncodedName}"}
|
Headers = {"Content-Disposition": f"attachment; filename*=UTF-8''{EncodedName}"}
|
||||||
|
|
||||||
def IterateDownload():
|
# FileResponse sends Content-Length, so browsers can show the size and
|
||||||
with EntryPath.open("rb") as SourceFile:
|
# time left (a plain stream showed them as unknown).
|
||||||
while Chunk := SourceFile.read(1024 * 1024):
|
return FileResponse(EntryPath, media_type=MimeType, headers=Headers)
|
||||||
yield Chunk
|
|
||||||
|
|
||||||
return StreamingResponse(IterateDownload(), media_type=MimeType, headers=Headers)
|
|
||||||
|
|
||||||
|
|
||||||
@Application.get("/api/book")
|
@Application.get("/api/book")
|
||||||
|
|||||||
+45
-23
@@ -11,7 +11,7 @@ from typing import Callable, Optional
|
|||||||
import flet as ft
|
import flet as ft
|
||||||
|
|
||||||
from App import App
|
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 Sources import BaseName, Entry, JoinPath, MoveDestination, ParentPath, SourceError
|
||||||
from Graph import GraphView
|
from Graph import GraphView
|
||||||
from Viewer import FileView
|
from Viewer import FileView
|
||||||
@@ -84,11 +84,12 @@ class Browser(ft.Column):
|
|||||||
spacing=8,
|
spacing=8,
|
||||||
tight=True,
|
tight=True,
|
||||||
)
|
)
|
||||||
# Shown while uploading: which file, how far (Uploads' progress).
|
# Shown while uploading or downloading: which file, how far, how long (ShowTransfer).
|
||||||
self.UploadText = ft.Text("", size=12, color=Palette.Overlay0)
|
self.TransferText = ft.Text("", size=12, color=Palette.Overlay0)
|
||||||
self.UploadBar = ft.ProgressBar(value=0, color=Palette.Mauve, bgcolor=Palette.Surface0, bar_height=4)
|
self.TransferBar = 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),
|
self.TransferRow = ft.Container(ft.Column([self.TransferText, self.TransferBar], spacing=4),
|
||||||
padding=ft.Padding.symmetric(horizontal=8, vertical=6), visible=False)
|
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)
|
self.EntryList = ft.ListView(expand=True, spacing=2)
|
||||||
# One shared popup host, opened programmatically (see _RequestMenu).
|
# One shared popup host, opened programmatically (see _RequestMenu).
|
||||||
# Its built-in triggers can't be switched off, so they're set to
|
# 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).
|
# 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.Row([ft.Container(self.Breadcrumbs, padding=8, expand=True), ft.Container(self.ActionsToolbar, padding=8)]),
|
||||||
ft.Divider(height=1, color=Palette.Surface1),
|
ft.Divider(height=1, color=Palette.Surface1),
|
||||||
self.UploadRow,
|
self.TransferRow,
|
||||||
self.FileArea,
|
self.FileArea,
|
||||||
]
|
]
|
||||||
self._ShowList(Update=False)
|
self._ShowList(Update=False)
|
||||||
@@ -610,21 +611,14 @@ class Browser(ft.Column):
|
|||||||
Total = sum(Sizes)
|
Total = sum(Sizes)
|
||||||
Verb = "Uploading" if Server else "Copying in"
|
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:
|
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 ""
|
Of = f" ({Index + 1} of {len(Paths)})" if len(Paths) > 1 else ""
|
||||||
self.UploadText.value = (f"{Verb} {Paths[Index].name}{Of}: {Percent}% "
|
self._ShowTransfer(f"{Verb} {Paths[Index].name}{Of}", sum(Sizes[:Index]) + Sizes[Index] * Fraction, Total, Started)
|
||||||
f"({HumanSize(int(Done))} of {HumanSize(Total)})")
|
|
||||||
self.UploadBar.value = Done / Total if Total else 1
|
|
||||||
self.UploadRow.update()
|
|
||||||
|
|
||||||
self.UploadRow.visible = True
|
self.TransferShown = None
|
||||||
|
self.TransferRow.visible = True
|
||||||
for Index, LocalFile in enumerate(Paths):
|
for Index, LocalFile in enumerate(Paths):
|
||||||
ShowProgress(Index, 0)
|
ShowProgress(Index, 0)
|
||||||
try:
|
try:
|
||||||
@@ -634,15 +628,15 @@ class Browser(ft.Column):
|
|||||||
Saved += 1
|
Saved += 1
|
||||||
except SourceError as Error:
|
except SourceError as Error:
|
||||||
if Error.Status == 401:
|
if Error.Status == 401:
|
||||||
self.UploadRow.visible = False
|
self.TransferRow.visible = False
|
||||||
self.UploadRow.update()
|
self.TransferRow.update()
|
||||||
self.TheApp.HandleError(Error)
|
self.TheApp.HandleError(Error)
|
||||||
return
|
return
|
||||||
Failed.append(f"{LocalFile.name}: {Error}")
|
Failed.append(f"{LocalFile.name}: {Error}")
|
||||||
except OSError as Error:
|
except OSError as Error:
|
||||||
Failed.append(f"{LocalFile.name}: {Error}")
|
Failed.append(f"{LocalFile.name}: {Error}")
|
||||||
self.UploadRow.visible = False
|
self.TransferRow.visible = False
|
||||||
self.UploadRow.update()
|
self.TransferRow.update()
|
||||||
self.Refresh()
|
self.Refresh()
|
||||||
if Failed:
|
if Failed:
|
||||||
self.TheApp.Notify("Couldn't upload " + "; ".join(Failed), "error")
|
self.TheApp.Notify("Couldn't upload " + "; ".join(Failed), "error")
|
||||||
@@ -663,14 +657,42 @@ class Browser(ft.Column):
|
|||||||
initial_directory=str(Path.home() / "Downloads"))
|
initial_directory=str(Path.home() / "Downloads"))
|
||||||
if not Chosen:
|
if not Chosen:
|
||||||
return
|
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:
|
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:
|
except SourceError as Error:
|
||||||
self.TheApp.HandleError(Error, f"Couldn't download {Name}")
|
self.TheApp.HandleError(Error, f"Couldn't download {Name}")
|
||||||
return
|
return
|
||||||
|
finally:
|
||||||
|
self.TransferRow.visible = False
|
||||||
|
self.TransferRow.update()
|
||||||
self.TheApp.Notify(f"Saved {Chosen}", "success")
|
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 ---------------------------------------------------------
|
# --- Keyboard ---------------------------------------------------------
|
||||||
|
|
||||||
def _SetShortcutsPaused(self, Paused: bool) -> None:
|
def _SetShortcutsPaused(self, Paused: bool) -> None:
|
||||||
|
|||||||
@@ -539,6 +539,17 @@ def HumanSize(NumBytes: Optional[int]) -> str:
|
|||||||
return f"{Size:.1f} TB"
|
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:
|
def DisplayPath(PathText: str) -> str:
|
||||||
"""/home/sarah/Notes -> ~/Notes, for labels (C:\\Users\\x\\Notes -> ~\\Notes)."""
|
"""/home/sarah/Notes -> ~/Notes, for labels (C:\\Users\\x\\Notes -> ~\\Notes)."""
|
||||||
HomeText = str(Home)
|
HomeText = str(Home)
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ def _SetHighlight(Label: ft.Text, Holder: ft.Container, IsSelected: bool) -> Non
|
|||||||
Holder.update()
|
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:
|
class _FileLeaf:
|
||||||
"""A file in the tree: a plain row (no expand arrow) indented like a
|
"""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."""
|
child tile, so names line up with the directories beside it."""
|
||||||
@@ -104,6 +110,9 @@ class _FiletreeNode:
|
|||||||
def SetSelected(self, IsSelected: bool) -> None:
|
def SetSelected(self, IsSelected: bool) -> None:
|
||||||
_SetHighlight(self.NameLabel, self.NameContainer, IsSelected)
|
_SetHighlight(self.NameLabel, self.NameContainer, IsSelected)
|
||||||
|
|
||||||
|
def SetOnPath(self, IsOnPath: bool) -> None:
|
||||||
|
_SetOnPath(self.NameContainer, IsOnPath)
|
||||||
|
|
||||||
|
|
||||||
class Filetree(ft.Container):
|
class Filetree(ft.Container):
|
||||||
def __init__(self, TheApp: App, OnNavigate: Callable[[str, str], None], OnOpenFile: Callable[[str, str], None]):
|
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.OnOpenFile = OnOpenFile
|
||||||
self.RootNodes: dict[str, _FiletreeNode] = {}
|
self.RootNodes: dict[str, _FiletreeNode] = {}
|
||||||
self.SelectedNode: Optional[Union[_FiletreeNode, _FileLeaf]] = None
|
self.SelectedNode: Optional[Union[_FiletreeNode, _FileLeaf]] = None
|
||||||
|
self.PathNodes: list[_FiletreeNode] = [] # the directories you're in, marked
|
||||||
self.TreeList = ft.ListView(expand=True)
|
self.TreeList = ft.ListView(expand=True)
|
||||||
self.SourceLabel = ft.Text("", size=12, color=Palette.Overlay0, overflow=ft.TextOverflow.ELLIPSIS, max_lines=1)
|
self.SourceLabel = ft.Text("", size=12, color=Palette.Overlay0, overflow=ft.TextOverflow.ELLIPSIS, max_lines=1)
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -129,6 +139,7 @@ class Filetree(ft.Container):
|
|||||||
Source = self.TheApp.Source
|
Source = self.TheApp.Source
|
||||||
self.SourceLabel.value = "Not connected" if self.TheApp.ConnectError else Source.Describe()
|
self.SourceLabel.value = "Not connected" if self.TheApp.ConnectError else Source.Describe()
|
||||||
self.SelectedNode = None
|
self.SelectedNode = None
|
||||||
|
self.PathNodes = []
|
||||||
Roots: list[str] = []
|
Roots: list[str] = []
|
||||||
if not self.TheApp.ConnectError:
|
if not self.TheApp.ConnectError:
|
||||||
try:
|
try:
|
||||||
@@ -151,6 +162,9 @@ class Filetree(ft.Container):
|
|||||||
if self.SelectedNode:
|
if self.SelectedNode:
|
||||||
self.SelectedNode.SetSelected(False)
|
self.SelectedNode.SetSelected(False)
|
||||||
self.SelectedNode = None
|
self.SelectedNode = None
|
||||||
|
for Marked in self.PathNodes:
|
||||||
|
Marked.SetOnPath(False)
|
||||||
|
self.PathNodes = []
|
||||||
Node = self.RootNodes.get(Root or "")
|
Node = self.RootNodes.get(Root or "")
|
||||||
if Node is None:
|
if Node is None:
|
||||||
return
|
return
|
||||||
@@ -169,5 +183,15 @@ class Filetree(ft.Container):
|
|||||||
Target = Leaf
|
Target = Leaf
|
||||||
break
|
break
|
||||||
Node = Target = NextNode
|
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)
|
Target.SetSelected(True)
|
||||||
self.SelectedNode = Target
|
self.SelectedNode = Target
|
||||||
+9
-2
@@ -435,8 +435,10 @@ class ServerSource(Source):
|
|||||||
Done = {**Params, "UploadId": UploadId, "TotalChunks": TotalChunks, "FileName": LocalFile.name}
|
Done = {**Params, "UploadId": UploadId, "TotalChunks": TotalChunks, "FileName": LocalFile.name}
|
||||||
return self._Json("POST", "/upload/complete", params=Done)["Saved"]
|
return self._Json("POST", "/upload/complete", params=Done)["Saved"]
|
||||||
|
|
||||||
def Download(self, Root: str, RelativePath: str, Destination: Path) -> None:
|
def Download(self, Root: str, RelativePath: str, Destination: Path,
|
||||||
"""A file as-is, a directory as a .zip (the server builds it)."""
|
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:
|
if not self.Url:
|
||||||
raise SourceError("No server address yet.")
|
raise SourceError("No server address yet.")
|
||||||
Partial = Destination.with_name(Destination.name + ".part")
|
Partial = Destination.with_name(Destination.name + ".part")
|
||||||
@@ -445,9 +447,14 @@ class ServerSource(Source):
|
|||||||
if not Response.is_success:
|
if not Response.is_success:
|
||||||
Response.read()
|
Response.read()
|
||||||
raise SourceError(f"Download failed: {Response.status_code} {Response.reason_phrase}", Response.status_code)
|
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:
|
with Partial.open("wb") as Handle:
|
||||||
for Chunk in Response.iter_bytes(1024 * 1024):
|
for Chunk in Response.iter_bytes(1024 * 1024):
|
||||||
Handle.write(Chunk)
|
Handle.write(Chunk)
|
||||||
|
Received += len(Chunk)
|
||||||
|
if OnProgress:
|
||||||
|
OnProgress(Received, Total)
|
||||||
Partial.replace(Destination)
|
Partial.replace(Destination)
|
||||||
except httpx.HTTPError as Error:
|
except httpx.HTTPError as Error:
|
||||||
Partial.unlink(missing_ok=True)
|
Partial.unlink(missing_ok=True)
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ function TreeNode({
|
|||||||
const [ErrorMessage, SetErrorMessage] = useState(null);
|
const [ErrorMessage, SetErrorMessage] = useState(null);
|
||||||
const FullPath = ParentPath ? `${ParentPath}/${Entry.Name}` : Entry.Name;
|
const FullPath = ParentPath ? `${ParentPath}/${Entry.Name}` : Entry.Name;
|
||||||
const IsSelected = SelectedFile === FullPath;
|
const IsSelected = SelectedFile === FullPath;
|
||||||
const IsCurrentDirectory = Entry.IsDirectory && CurrentDirectoryPath === FullPath;
|
|
||||||
const IsAncestorOfCurrentDirectory =
|
const IsAncestorOfCurrentDirectory =
|
||||||
Entry.IsDirectory &&
|
Entry.IsDirectory &&
|
||||||
CurrentDirectoryPath &&
|
CurrentDirectoryPath &&
|
||||||
@@ -144,7 +143,7 @@ function TreeNode({
|
|||||||
<div className="TreeNode">
|
<div className="TreeNode">
|
||||||
<div
|
<div
|
||||||
className={`TreeRow${IsSelected ? " TreeRowSelected" : ""}${
|
className={`TreeRow${IsSelected ? " TreeRowSelected" : ""}${
|
||||||
IsCurrentDirectory ? " TreeRowCurrentDirectory" : ""
|
IsAncestorOfCurrentDirectory ? " TreeRowCurrentDirectory" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={ToggleExpand}
|
onClick={ToggleExpand}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useSyncExternalStore } from "react";
|
import React, { useSyncExternalStore } from "react";
|
||||||
|
|
||||||
import { FormatSize, GetUploadState, SubscribeUploads } from "../Upload.js";
|
import { FormatDuration, FormatSize, GetUploadState, SubscribeUploads } from "../Upload.js";
|
||||||
|
|
||||||
export default function UploadToast() {
|
export default function UploadToast() {
|
||||||
const State = useSyncExternalStore(SubscribeUploads, GetUploadState);
|
const State = useSyncExternalStore(SubscribeUploads, GetUploadState);
|
||||||
@@ -8,13 +8,18 @@ export default function UploadToast() {
|
|||||||
|
|
||||||
const Files = `${State.Count} file${State.Count === 1 ? "" : "s"}`;
|
const Files = `${State.Count} file${State.Count === 1 ? "" : "s"}`;
|
||||||
const Percent = State.Total > 0 ? Math.floor((State.Done / State.Total) * 100) : 100;
|
const Percent = State.Total > 0 ? Math.floor((State.Done / State.Total) * 100) : 100;
|
||||||
|
const Elapsed = (performance.now() - State.Started) / 1000;
|
||||||
|
const Left =
|
||||||
|
Elapsed > 2 && State.Done > 0 && State.Done < State.Total
|
||||||
|
? `, about ${FormatDuration(((State.Total - State.Done) * Elapsed) / State.Done)} left`
|
||||||
|
: "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="UploadToast" role="status">
|
<div className="UploadToast" role="status">
|
||||||
<span>
|
<span>
|
||||||
{State.Finished
|
{State.Finished
|
||||||
? `Uploaded ${Files}.`
|
? `Uploaded ${Files}.`
|
||||||
: `Uploading ${Files}: ${Percent}% (${FormatSize(State.Done)} of ${FormatSize(State.Total)})`}
|
: `Uploading ${Files}: ${Percent}% (${FormatSize(State.Done)} of ${FormatSize(State.Total)}${Left})`}
|
||||||
</span>
|
</span>
|
||||||
{!State.Finished && (
|
{!State.Finished && (
|
||||||
<div className="UploadToastBar">
|
<div className="UploadToastBar">
|
||||||
|
|||||||
+13
-2
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { IsLargeUpload, UploadFileChunked, UploadFiles } from "./Api.js";
|
import { IsLargeUpload, UploadFileChunked, UploadFiles } from "./Api.js";
|
||||||
|
|
||||||
let State = null; // { Count, Done, Total (bytes), Finished }
|
let State = null; // { Count, Done, Total (bytes), Started (ms), Finished }
|
||||||
let ActiveUploads = 0;
|
let ActiveUploads = 0;
|
||||||
const Listeners = new Set();
|
const Listeners = new Set();
|
||||||
|
|
||||||
@@ -23,6 +23,16 @@ export function GetUploadState() {
|
|||||||
return State;
|
return State;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Roughly, for "about ... left": 8 s, 3 min, 1 h, 1 h 20 min. Same as the client's HumanDuration.
|
||||||
|
export function FormatDuration(Seconds) {
|
||||||
|
const Rounded = Math.max(1, Math.round(Seconds));
|
||||||
|
if (Rounded < 60) return `${Rounded} s`;
|
||||||
|
const Minutes = Math.round(Rounded / 60);
|
||||||
|
const Hours = Math.floor(Minutes / 60);
|
||||||
|
if (!Hours) return `${Minutes} min`;
|
||||||
|
return Minutes % 60 ? `${Hours} h ${Minutes % 60} min` : `${Hours} h`;
|
||||||
|
}
|
||||||
|
|
||||||
export function FormatSize(Bytes) {
|
export function FormatSize(Bytes) {
|
||||||
const Units = ["B", "KB", "MB", "GB", "TB"];
|
const Units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
let Size = Bytes;
|
let Size = Bytes;
|
||||||
@@ -53,8 +63,9 @@ export async function UploadAll(RootName, DirectoryPath, Files) {
|
|||||||
const Errors = [];
|
const Errors = [];
|
||||||
let Finished = 0; // bytes of the parts already done
|
let Finished = 0; // bytes of the parts already done
|
||||||
|
|
||||||
|
const Started = performance.now();
|
||||||
const Report = (Current) =>
|
const Report = (Current) =>
|
||||||
Publish({ Count: FileArray.length, Done: Math.min(Total, Finished + Current), Total, Finished: false });
|
Publish({ Count: FileArray.length, Done: Math.min(Total, Finished + Current), Total, Started, Finished: false });
|
||||||
|
|
||||||
ActiveUploads++;
|
ActiveUploads++;
|
||||||
Report(0);
|
Report(0);
|
||||||
|
|||||||
Reference in new issue
Block a user