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