feat: enhance backup and download functionalities

- Updated `BackupCenterPage` to support download progress tracking during remote backup downloads.
- Modified `ImportPage` to simplify export functionality by removing unnecessary payload handling.
- Improved `JwtWarningPage` to utilize a new clipboard utility for copying text with feedback.
- Enhanced `PublicSendPage` to show download progress for files being downloaded.
- Updated `RecoverTwoFactorPage` to include autocomplete attributes for better user experience.
- Refactored `SendsPage` to use the new clipboard utility for copying access URLs.
- Enhanced `SettingsPage` to utilize the clipboard utility for copying sensitive information.
- Improved `TotpCodesPage` to use the clipboard utility for copying TOTP codes.
- Updated `VaultPage` and related components to support download progress for attachments.
- Introduced a new `app-notify` module for consistent notification handling across the application.
- Created a `clipboard` utility for improved clipboard interactions with user feedback.
- Added progress tracking for file downloads in the API layer, enhancing user experience during downloads.
This commit is contained in:
shuaiplus
2026-03-15 23:12:45 +08:00
parent 9820c2ed44
commit 4b8cad6d00
33 changed files with 387 additions and 121 deletions
+58
View File
@@ -10,3 +10,61 @@ export function downloadBytesAsFile(bytes: Uint8Array, fileName: string, mimeTyp
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
}
export interface DownloadProgressState {
loaded: number;
total: number | null;
percent: number | null;
}
type ProgressCallback = (progress: DownloadProgressState) => void;
function parseContentLength(response: Response): number | null {
const raw = String(response.headers.get('Content-Length') || '').trim();
if (!raw) return null;
const parsed = Number(raw);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
export async function readResponseBytesWithProgress(
response: Response,
onProgress?: ProgressCallback
): Promise<Uint8Array> {
const total = parseContentLength(response);
const report = (loaded: number) => {
onProgress?.({
loaded,
total,
percent: total ? Math.max(0, Math.min(100, Math.round((loaded / total) * 100))) : null,
});
};
if (!response.body) {
const bytes = new Uint8Array(await response.arrayBuffer());
report(bytes.byteLength);
return bytes;
}
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let loaded = 0;
report(0);
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (!value) continue;
chunks.push(value);
loaded += value.byteLength;
report(loaded);
}
const bytes = new Uint8Array(loaded);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
report(loaded);
return bytes;
}