Skip to content

Commit a47a9ee

Browse files
feat(downloads): enhance UI for Downloads page with improved layout and styling
1 parent 328b9fe commit a47a9ee

7 files changed

Lines changed: 477 additions & 9 deletions

File tree

src/celemod-ui/src/components/DownloadList.scss

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,3 +538,80 @@
538538
padding: 8px 10px 28px;
539539
}
540540
}
541+
542+
// Keep the full-page task list information-dense instead of card-like.
543+
.download-page {
544+
padding: 4px 12px 24px;
545+
}
546+
547+
.download-page-header {
548+
padding: 4px 4px 7px;
549+
}
550+
551+
.download-page-heading {
552+
gap: 7px;
553+
554+
.download-page-icon {
555+
width: 24px;
556+
height: 24px;
557+
border-radius: 6px;
558+
font-size: 11px;
559+
}
560+
561+
h1 {
562+
font-size: 14px;
563+
}
564+
565+
p {
566+
margin-top: 1px;
567+
font-size: 9px;
568+
}
569+
}
570+
571+
.download-page-list .download-task {
572+
padding: 8px 6px 7px;
573+
}
574+
575+
.download-task-toggle {
576+
min-height: 24px;
577+
gap: 6px;
578+
}
579+
580+
.download-task-status-icon {
581+
width: 20px;
582+
height: 20px;
583+
border-radius: 6px;
584+
}
585+
586+
.download-task-progress {
587+
height: 4px;
588+
margin: 7px 0 6px 26px;
589+
}
590+
591+
.download-task-summary {
592+
margin-left: 26px;
593+
gap: 7px;
594+
}
595+
596+
.download-page-list .download-dependency-list {
597+
margin: 7px 0 0 26px;
598+
padding-top: 6px;
599+
}
600+
601+
.download-dependency-heading {
602+
margin-bottom: 4px;
603+
}
604+
605+
.download-page-list .download-detail-row {
606+
margin-top: 3px;
607+
padding: 3px 0;
608+
}
609+
610+
.download-dependency-progress {
611+
height: 3px;
612+
margin-top: 4px;
613+
}
614+
615+
.download-detail-summary {
616+
margin-top: 3px;
617+
}

src/celemod-ui/src/components/DownloadList.tsx

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ const formatBytes = (bytes: number) => {
2222
const formatSpeed = (bytesPerSec: number) =>
2323
bytesPerSec ? `${formatBytes(bytesPerSec)}/s` : "0 B/s";
2424

25+
// Prefer byte counters over the streamed percentage: the latter can lag
26+
// behind and is also used for aggregate root progress in the store.
27+
const getTaskProgress = (task: Download.TaskInfo) => {
28+
if (task.totalBytes > 0) {
29+
return Math.max(
30+
0,
31+
Math.min(100, (task.downloadedBytes / task.totalBytes) * 100)
32+
);
33+
}
34+
return Math.max(
35+
0,
36+
Math.min(100, task.state === "finished" ? 100 : task.progress || 0)
37+
);
38+
};
39+
2540
const getMetrics = (tasks: Download.TaskInfo[]) => {
2641
const downloadedBytes = tasks.reduce(
2742
(sum, task) => sum + task.downloadedBytes,
@@ -79,6 +94,24 @@ const stateLabel = (task: Download.TaskInfo) => {
7994
return { label: _i18n.t("下载中"), icon: "download", tone: "active" };
8095
};
8196

97+
const getRootStatus = (task: Download.TaskInfo, dependencyTasks: Download.TaskInfo[]) => {
98+
if (task.canceled) return task;
99+
if (dependencyTasks.some((dependency) => dependency.canceled)) {
100+
return { ...task, canceled: true };
101+
}
102+
if (dependencyTasks.some((dependency) => dependency.state === "failed")) {
103+
return { ...task, state: "failed" as const };
104+
}
105+
// A root may finish before its dependencies; keep it active until all are done.
106+
if (
107+
task.state === "finished" &&
108+
dependencyTasks.some((dependency) => dependency.state !== "finished")
109+
) {
110+
return { ...task, state: "pending" as const };
111+
}
112+
return task;
113+
};
114+
82115
const DownloadDetailRow = ({
83116
task,
84117
children,
@@ -94,7 +127,7 @@ const DownloadDetailRow = ({
94127
const togglePauseDownload = useDownloadStore(
95128
(state) => state.togglePauseDownload
96129
);
97-
const progress = Math.max(0, Math.min(100, task.progress || 0));
130+
const progress = getTaskProgress(task);
98131
const status = stateLabel(task);
99132
const canControl = allowControl && task.state === "pending" && !task.canceled;
100133
return (
@@ -161,8 +194,10 @@ export const DownloadTask = ({
161194
const metricTasks = [task, ...dependencyTasks];
162195
const metrics = getMetrics(metricTasks);
163196
const progress = Math.max(0, Math.min(100, Number(metrics.progress) || 0));
164-
const status = stateLabel(task);
165-
const canControl = allowControl && task.state === "pending" && !task.canceled;
197+
const displayTask = getRootStatus(task, dependencyTasks);
198+
const status = stateLabel(displayTask);
199+
const canControl =
200+
allowControl && displayTask.state === "pending" && !displayTask.canceled;
166201
const action =
167202
allowRetry && task.state === "failed" && task.source
168203
? {

src/celemod-ui/src/index.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
@use "./styles/themes/material-you";
55

66
:root {
7+
--window-titlebar-height: 45px;
78
/* Runtime theme tokens. Theme providers can override these values on
89
[data-theme] without recompiling component styles. */
910
--color-canvas: #{$bg};
@@ -518,7 +519,7 @@ select option {
518519

519520
.blocking-mask {
520521
position: fixed;
521-
inset: 30px 0 0;
522+
inset: var(--window-titlebar-height) 0 0;
522523
z-index: 1200;
523524
display: none;
524525
align-items: center;

src/celemod-ui/src/stores/download.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,12 +212,14 @@ export const useDownloadStore = create<DownloadStore>((set, get) => ({
212212
attemptId: attemptId + index,
213213
}));
214214
const byName = new Map(mapped.map((task) => [normalizeName(task.name), task]));
215+
// Keep each task's own progress in the store. The aggregate is only
216+
// needed for callbacks and must not leak into the "主文件" row.
215217
const aggregated = mapped.map((task) =>
216218
task.requested
217219
? { ...task, progress: aggregateProgress(task, byName) }
218220
: task,
219221
);
220-
set((store) => ({ tasks: replaceTasks(store.tasks, aggregated) }));
222+
set((store) => ({ tasks: replaceTasks(store.tasks, mapped) }));
221223
const progress = aggregated.length
222224
? aggregated.filter((task) => task.requested).reduce((sum, task) => sum + task.progress, 0) /
223225
Math.max(1, aggregated.filter((task) => task.requested).length)
@@ -387,11 +389,14 @@ export const useDownloadStore = create<DownloadStore>((set, get) => ({
387389
? { ...task, progress: aggregateProgress(task, byName) }
388390
: task,
389391
);
390-
set((store) => ({ tasks: replaceTasks(store.tasks, aggregated) }));
392+
// Store per-file progress; use the aggregate only for the callback.
393+
set((store) => ({ tasks: replaceTasks(store.tasks, mapped) }));
391394

392395
const currentRoot =
393-
aggregated.find((task) => normalizeName(task.name) === key) ?? current;
394-
const overallProgress = currentRoot.progress;
396+
mapped.find((task) => normalizeName(task.name) === key) ?? current;
397+
const overallProgress =
398+
aggregated.find((task) => normalizeName(task.name) === key)?.progress ??
399+
currentRoot.progress;
395400
const rootTask = {
396401
...currentRoot,
397402
state,

src/celemod-ui/src/styles/themes/fluent/_pages.scss

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,172 @@ html[data-theme="fluent"] .keybindings-page {
792792
background: transparent;
793793
}
794794

795+
/* Downloads are a ListView-style route, not a collection of Fluent buttons.
796+
The generic control rules above intentionally target every button, so keep
797+
the task toggles and icon actions on their own compact geometry. */
798+
html[data-theme="fluent"] .page-Downloads {
799+
background: transparent;
800+
}
801+
802+
html[data-theme="fluent"] .page-Downloads .download-page {
803+
width: 100%;
804+
max-width: none;
805+
margin: 0;
806+
padding: 18px 28px 40px;
807+
box-sizing: border-box;
808+
color: var(--fluent-text-secondary);
809+
}
810+
811+
html[data-theme="fluent"] .page-Downloads .download-page-header {
812+
margin-bottom: 0;
813+
padding: 4px 6px 12px;
814+
border-bottom-color: var(--fluent-stroke-divider);
815+
}
816+
817+
html[data-theme="fluent"] .page-Downloads .download-page-heading {
818+
gap: 10px;
819+
}
820+
821+
html[data-theme="fluent"] .page-Downloads .download-page-icon {
822+
width: 28px;
823+
height: 28px;
824+
border: 1px solid color-mix(in srgb, var(--fluent-accent) 30%, transparent);
825+
border-radius: 4px;
826+
color: var(--fluent-accent);
827+
background: color-mix(in srgb, var(--fluent-accent) 12%, transparent);
828+
font-size: 13px;
829+
}
830+
831+
html[data-theme="fluent"] .page-Downloads .download-page-heading h1 {
832+
color: var(--fluent-text-primary);
833+
font-size: 20px;
834+
font-weight: 600;
835+
letter-spacing: -.015em;
836+
}
837+
838+
html[data-theme="fluent"] .page-Downloads .download-page-heading p {
839+
margin-top: 3px;
840+
color: var(--fluent-text-tertiary);
841+
}
842+
843+
html[data-theme="fluent"] .page-Downloads .download-task {
844+
margin: 0;
845+
padding: 12px 8px 11px;
846+
border: 0;
847+
border-bottom: 1px solid var(--fluent-stroke-divider);
848+
border-radius: 0;
849+
background: transparent;
850+
}
851+
852+
html[data-theme="fluent"] .page-Downloads .download-task:hover {
853+
background: var(--fluent-fill-subtle);
854+
}
855+
856+
html[data-theme="fluent"] .page-Downloads .download-task-toggle,
857+
html[data-theme="fluent"] .page-Downloads .download-task-action,
858+
html[data-theme="fluent"] .page-Downloads .download-dependency-actions button {
859+
min-height: 0 !important;
860+
padding: 0 !important;
861+
border: 0 !important;
862+
border-radius: 4px !important;
863+
color: var(--fluent-text-secondary) !important;
864+
background: transparent !important;
865+
box-shadow: none !important;
866+
}
867+
868+
html[data-theme="fluent"] .page-Downloads .download-task-toggle {
869+
width: 100%;
870+
height: 28px;
871+
justify-content: stretch;
872+
font-size: inherit;
873+
}
874+
875+
html[data-theme="fluent"] .page-Downloads .download-task-toggle:hover,
876+
html[data-theme="fluent"] .page-Downloads .download-task-action:hover,
877+
html[data-theme="fluent"] .page-Downloads .download-dependency-actions button:hover {
878+
color: var(--fluent-text-primary) !important;
879+
background: var(--fluent-fill-control-hover) !important;
880+
box-shadow: none !important;
881+
}
882+
883+
html[data-theme="fluent"] .page-Downloads .download-task-status-icon {
884+
width: 22px;
885+
height: 22px;
886+
border: 1px solid color-mix(in srgb, var(--fluent-accent) 28%, transparent);
887+
border-radius: 4px;
888+
color: var(--fluent-accent);
889+
background: color-mix(in srgb, var(--fluent-accent) 11%, transparent);
890+
}
891+
892+
html[data-theme="fluent"] .page-Downloads .download-task-title {
893+
color: var(--fluent-text-primary);
894+
font-size: 12px;
895+
font-weight: 600;
896+
}
897+
898+
html[data-theme="fluent"] .page-Downloads .download-task-status-text,
899+
html[data-theme="fluent"] .page-Downloads .download-task-summary,
900+
html[data-theme="fluent"] .page-Downloads .download-detail-summary,
901+
html[data-theme="fluent"] .page-Downloads .download-dependency-heading {
902+
color: var(--fluent-text-tertiary);
903+
}
904+
905+
html[data-theme="fluent"] .page-Downloads .download-task-progress,
906+
html[data-theme="fluent"] .page-Downloads .download-dependency-progress {
907+
background: var(--fluent-fill-control);
908+
}
909+
910+
html[data-theme="fluent"] .page-Downloads .download-task-progress > span,
911+
html[data-theme="fluent"] .page-Downloads .download-dependency-progress > span {
912+
background: var(--fluent-accent);
913+
}
914+
915+
html[data-theme="fluent"] .page-Downloads .download-task-failed .download-task-progress > span,
916+
html[data-theme="fluent"] .page-Downloads .download-detail-row-failed .download-dependency-progress > span {
917+
background: var(--fluent-danger);
918+
}
919+
920+
html[data-theme="fluent"] .page-Downloads .download-task-canceled .download-task-progress > span,
921+
html[data-theme="fluent"] .page-Downloads .download-task-paused .download-task-progress > span {
922+
background: var(--fluent-text-tertiary);
923+
}
924+
925+
html[data-theme="fluent"] .page-Downloads .download-task-failed .download-task-status-icon,
926+
html[data-theme="fluent"] .page-Downloads .download-detail-row-failed .download-dependency-main > .icon,
927+
html[data-theme="fluent"] .page-Downloads .download-task-error-details,
928+
html[data-theme="fluent"] .page-Downloads .download-dependency-error {
929+
color: var(--fluent-danger);
930+
}
931+
932+
html[data-theme="fluent"] .page-Downloads .download-task-canceled .download-task-status-icon,
933+
html[data-theme="fluent"] .page-Downloads .download-task-paused .download-task-status-icon {
934+
color: var(--fluent-text-tertiary);
935+
}
936+
937+
html[data-theme="fluent"] .page-Downloads .download-task-cancel:hover {
938+
color: var(--fluent-danger) !important;
939+
background: color-mix(in srgb, var(--fluent-danger) 13%, transparent) !important;
940+
}
941+
942+
html[data-theme="fluent"] .page-Downloads .download-dependency-list {
943+
margin-left: 30px;
944+
border-top-color: var(--fluent-stroke-divider);
945+
}
946+
947+
html[data-theme="fluent"] .page-Downloads .download-detail-row {
948+
border: 0;
949+
border-radius: 0;
950+
background: transparent;
951+
}
952+
953+
html[data-theme="fluent"] .page-Downloads .download-detail-row:hover {
954+
background: var(--fluent-fill-subtle);
955+
}
956+
957+
html[data-theme="fluent"] .page-Downloads .download-list-empty {
958+
color: var(--fluent-text-tertiary);
959+
}
960+
795961
html[data-theme="fluent"] .keybindings-main {
796962
width: 100%;
797963
}

src/celemod-ui/src/styles/themes/fluent/_shell.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ html[data-theme="fluent"] .app-frame::after {
2626
mix-blend-mode: soft-light;
2727
}
2828

29+
html[data-theme="fluent"] {
30+
--window-titlebar-height: 46px;
31+
}
32+
2933
html[data-theme="fluent"] .window-titlebar {
3034
flex-basis: 46px;
3135
min-height: 46px;
@@ -191,4 +195,4 @@ html[data-theme="fluent"][data-fluent-keyboard="true"] :focus-visible {
191195

192196
html[data-theme="fluent"] .sidebar-footer {
193197
margin-bottom: 8px;
194-
}
198+
}

0 commit comments

Comments
 (0)