Implements progress bars in gui app.

This commit is contained in:
2026-08-23 23:08:01 -07:00
parent 6411fecb4f
commit a4838bf8d9
4 changed files with 1207 additions and 271 deletions

View File

@@ -1,11 +1,13 @@
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use tauri::Emitter;
use nextcloud_client::{
initiate_login_flow as api_initiate_login_flow,
poll_login_flow as api_poll_login_flow,
ClientConfig, CredentialStore, NextcloudClient, StoredAccount, UploadOptions,
ClientConfig, CredentialStore, NextcloudClient, ProgressEvent, StoredAccount, UploadOptions,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -32,6 +34,13 @@ pub struct GuiUploadResult {
pub direct_download_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GuiUploadProgressPayload {
pub file_path: String,
pub bytes_transferred: u64,
pub total_bytes: Option<u64>,
}
#[tauri::command]
pub fn list_accounts() -> Result<Vec<StoredAccount>, String> {
CredentialStore::list_accounts().map_err(|e| e.to_string())
@@ -184,6 +193,7 @@ pub fn get_file_info(file_path: String) -> Result<FileInfo, String> {
#[tauri::command]
pub async fn upload_file(
app: tauri::AppHandle,
file_path: String,
remote_dir: String,
create_share: bool,
@@ -222,8 +232,23 @@ pub async fn upload_file(
overwrite: true,
};
let app_handle = app.clone();
let fp = file_path.clone();
let progress_cb: nextcloud_client::ProgressCallback = Arc::new(move |event| {
if let ProgressEvent::Progress { bytes_transferred, total_bytes } = event {
let _ = app_handle.emit(
"upload-progress",
GuiUploadProgressPayload {
file_path: fp.clone(),
bytes_transferred,
total_bytes,
},
);
}
});
let res = client
.upload_and_share(&local_path, &options, None)
.upload_and_share(&local_path, &options, Some(progress_cb))
.await
.map_err(|e| e.to_string())?;
@@ -239,7 +264,7 @@ pub async fn upload_file(
#[cfg(test)]
mod tests {
use super::{FileInfo, GuiUploadResult, LoginFlowInitPayload};
use super::{FileInfo, GuiUploadProgressPayload, GuiUploadResult, LoginFlowInitPayload};
#[test]
fn test_login_flow_payload_serialization() {
@@ -267,6 +292,19 @@ mod tests {
assert_eq!(file_info, deserialized);
}
#[test]
fn test_upload_progress_payload_serialization() {
let payload = GuiUploadProgressPayload {
file_path: "/tmp/sample.iso".to_string(),
bytes_transferred: 5242880,
total_bytes: Some(10485760),
};
let json = serde_json::to_string(&payload).unwrap();
let deserialized: GuiUploadProgressPayload = serde_json::from_str(&json).unwrap();
assert_eq!(payload, deserialized);
}
#[test]
fn test_gui_upload_result_serialization() {
let res = GuiUploadResult {

View File

@@ -102,6 +102,47 @@ body {
cursor: pointer;
}
/* Navigation Tabs */
.nav-tabs {
display: flex;
gap: 8px;
margin-top: 14px;
}
.tab-button {
background: var(--surface-color);
color: var(--text-muted);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 8px 16px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.15s ease;
}
.tab-button:hover {
color: var(--text-main);
background: var(--surface-card);
}
.tab-button.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.tab-badge {
background: rgba(0, 0, 0, 0.25);
font-size: 11px;
font-weight: 700;
padding: 2px 6px;
border-radius: 10px;
}
/* Content Area */
.main-content {
flex: 1;
@@ -169,6 +210,71 @@ body {
margin-top: 4px;
}
/* Overall Progress */
.overall-progress-container {
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 12px;
margin-bottom: 14px;
}
.overall-progress-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
margin-bottom: 8px;
}
.overall-progress-title {
font-weight: 600;
color: var(--accent);
}
.overall-progress-stats {
font-weight: 700;
color: var(--text-main);
}
.progress-bar-track {
background: var(--surface-card);
border-radius: 4px;
height: 8px;
overflow: hidden;
}
.progress-bar-fill {
background: linear-gradient(90deg, var(--primary), var(--accent));
height: 100%;
border-radius: 4px;
transition: width 0.2s ease;
}
.progress-bar-animated {
background-image: linear-gradient(
45deg,
rgba(255, 255, 255, 0.15) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.15) 75%,
transparent 75%,
transparent
);
background-size: 24px 24px;
animation: move-stripes 1s linear infinite;
}
@keyframes move-stripes {
0% {
background-position: 0 0;
}
100% {
background-position: 24px 0;
}
}
/* Queue List */
.queue-title-meta {
display: flex;
@@ -204,7 +310,7 @@ body {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 280px;
max-height: 320px;
overflow-y: auto;
padding-right: 4px;
}
@@ -216,7 +322,7 @@ body {
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 8px 12px;
padding: 10px 12px;
transition: background-color 0.15s, border-color 0.15s;
}
@@ -265,10 +371,74 @@ body {
text-overflow: ellipsis;
}
/* Item Progress Bars */
.item-progress-wrap {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
}
.item-progress-track {
flex: 1;
height: 6px;
background: var(--surface-card);
border-radius: 3px;
overflow: hidden;
}
.item-progress-fill {
background: var(--accent);
height: 100%;
border-radius: 3px;
transition: width 0.15s ease;
}
.item-progress-animated {
background: linear-gradient(90deg, var(--primary), var(--accent));
animation: pulse-glow 1.5s ease-in-out infinite alternate;
}
@keyframes pulse-glow {
0% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
.item-progress-text {
font-size: 10px;
font-weight: 600;
color: var(--text-muted);
white-space: nowrap;
}
.queue-item-error {
font-size: 11px;
color: var(--danger);
margin-top: 2px;
margin-top: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.btn-retry-inline {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid var(--danger);
border-radius: 4px;
padding: 2px 6px;
font-size: 10px;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
}
.btn-retry-inline:hover {
background: var(--danger);
color: white;
}
.queue-item-size {
@@ -354,6 +524,154 @@ body {
border-color: var(--danger);
}
/* Account Cards */
.account-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.account-card {
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 12px 14px;
transition: border-color 0.15s;
}
.account-card.active {
border-color: var(--primary);
box-shadow: 0 0 0 1px var(--primary);
}
.account-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.account-card-title {
display: flex;
align-items: center;
gap: 8px;
}
.account-name {
font-size: 15px;
font-weight: 700;
color: var(--text-main);
}
.badge {
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
}
.badge-default {
background: rgba(16, 185, 129, 0.2);
color: var(--success);
border: 1px solid var(--success);
}
.badge-active {
background: rgba(2, 132, 199, 0.2);
color: var(--accent);
border: 1px solid var(--accent);
}
.account-card-actions {
display: flex;
gap: 6px;
}
.account-card-meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 6px;
font-size: 12px;
color: var(--text-muted);
}
/* Auth Mode Toggle */
.auth-mode-toggle {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.btn-mode {
flex: 1;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 8px 12px;
color: var(--text-muted);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.btn-mode:hover:not(:disabled) {
color: var(--text-main);
border-color: var(--accent);
}
.btn-mode.active {
background: var(--surface-card);
border-color: var(--primary);
color: var(--accent);
}
.auth-form-content {
display: flex;
flex-direction: column;
}
/* Polling State */
.polling-box {
background: var(--bg-color);
border: 1px dashed var(--accent);
border-radius: var(--radius);
padding: 16px;
margin-top: 10px;
text-align: center;
}
.polling-header {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-weight: 600;
font-size: 14px;
color: var(--accent);
}
.polling-text {
font-size: 12px;
color: var(--text-muted);
margin-top: 6px;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid var(--accent);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Form Controls */
.form-group {
margin-bottom: 12px;

File diff suppressed because it is too large Load Diff