modde_ui/views/
downloads.rs1use std::path::PathBuf;
2
3use crate::views::selectable_text::text;
4use iced::widget::{button, column, container, progress_bar, row, scrollable};
5use iced::{Alignment, Element, Length, color};
6
7use crate::action_button::{ButtonAction, DescribedButtonExt};
8use crate::app::Message;
9
10#[derive(Debug, Clone)]
12pub enum DownloadState {
13 Queued,
14 Active {
15 bytes_downloaded: u64,
16 total_bytes: Option<u64>,
17 },
18 Paused {
19 bytes_downloaded: u64,
20 total_bytes: Option<u64>,
21 },
22 Complete {
23 path: PathBuf,
24 },
25 Failed {
26 error: String,
27 },
28}
29
30#[derive(Debug, Clone)]
32pub struct DownloadTask {
33 pub id: usize,
34 pub name: String,
35 pub state: DownloadState,
36}
37
38#[must_use]
40pub fn view(tasks: &[DownloadTask]) -> Element<'static, Message> {
41 let title_bar = row![
42 text("Downloads").size(20),
43 iced::widget::space::horizontal(),
44 ]
45 .align_y(Alignment::Center);
46
47 if tasks.is_empty() {
48 let empty = container(text("No downloads").size(14))
49 .padding(20)
50 .width(Length::Fill)
51 .center_x(Length::Fill);
52
53 return column![title_bar, iced::widget::rule::horizontal(1), empty]
54 .spacing(8)
55 .padding(16)
56 .width(Length::Fill)
57 .height(Length::Fill)
58 .into();
59 }
60
61 let items: Vec<Element<Message>> = tasks
62 .iter()
63 .map(|task| {
64 let name = text(task.name.clone()).size(14);
65
66 let (status_text, status_color) = match &task.state {
67 DownloadState::Queued => ("Queued", color!(0xAAAAFF)),
68 DownloadState::Active { .. } => ("Downloading", color!(0x88CC88)),
69 DownloadState::Paused { .. } => ("Paused", color!(0xFFAA44)),
70 DownloadState::Complete { .. } => ("Complete", color!(0x44CC44)),
71 DownloadState::Failed { .. } => ("Failed", color!(0xFF4444)),
72 };
73
74 let status = text(status_text).size(12).color(status_color);
75
76 let progress_widget: Element<Message> = match &task.state {
78 DownloadState::Active {
79 bytes_downloaded,
80 total_bytes,
81 }
82 | DownloadState::Paused {
83 bytes_downloaded,
84 total_bytes,
85 } => {
86 let ratio = total_bytes.map_or(0.0, |t| {
87 if t > 0 {
88 *bytes_downloaded as f32 / t as f32
89 } else {
90 0.0
91 }
92 });
93 let pct = format!("{:.0}%", ratio * 100.0);
94 let speed_info = if let DownloadState::Active {
95 bytes_downloaded, ..
96 } = &task.state
97 {
98 format!(" ({} downloaded)", format_bytes(*bytes_downloaded))
99 } else {
100 String::new()
101 };
102 column![
103 progress_bar(0.0..=1.0, ratio),
104 text(format!("{pct}{speed_info}")).size(11),
105 ]
106 .spacing(2)
107 .into()
108 }
109 _ => text("").into(),
110 };
111
112 let actions: Element<Message> = match &task.state {
114 DownloadState::Active { .. } => row![
115 button(text("Pause").size(11))
116 .style(button::secondary)
117 .padding([3, 8])
118 .on_action(ButtonAction::PauseDownload(task.id)),
119 button(text("Cancel").size(11))
120 .style(button::danger)
121 .padding([3, 8])
122 .on_action(ButtonAction::CancelDownload(task.id)),
123 ]
124 .spacing(4)
125 .into(),
126 DownloadState::Paused { .. } => row![
127 button(text("Resume").size(11))
128 .style(button::success)
129 .padding([3, 8])
130 .on_action(ButtonAction::ResumeDownload(task.id)),
131 button(text("Cancel").size(11))
132 .style(button::danger)
133 .padding([3, 8])
134 .on_action(ButtonAction::CancelDownload(task.id)),
135 ]
136 .spacing(4)
137 .into(),
138 DownloadState::Queued => button(text("Cancel").size(11))
139 .style(button::danger)
140 .padding([3, 8])
141 .on_action(ButtonAction::CancelDownload(task.id)),
142 DownloadState::Failed { error } => column![
143 text(format!("Error: {error}"))
144 .size(11)
145 .color(color!(0xFF4444)),
146 button(text("Retry").size(11))
147 .style(button::secondary)
148 .padding([3, 8])
149 .on_action(ButtonAction::ResumeDownload(task.id)),
150 ]
151 .spacing(2)
152 .into(),
153 DownloadState::Complete { .. } => text("").into(),
154 };
155
156 container(column![row![name, status].spacing(8), progress_widget, actions,].spacing(4))
157 .padding(8)
158 .width(Length::Fill)
159 .style(container::rounded_box)
160 .into()
161 })
162 .collect();
163
164 let list = scrollable(column(items).spacing(8)).height(Length::Fill);
165
166 column![title_bar, iced::widget::rule::horizontal(1), list]
167 .spacing(8)
168 .padding(16)
169 .width(Length::Fill)
170 .height(Length::Fill)
171 .into()
172}
173
174fn format_bytes(bytes: u64) -> String {
175 if bytes < 1024 {
176 format!("{bytes} B")
177 } else if bytes < 1024 * 1024 {
178 format!("{:.1} KB", bytes as f64 / 1024.0)
179 } else if bytes < 1024 * 1024 * 1024 {
180 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
181 } else {
182 format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
183 }
184}