1use serde::{Deserialize, Serialize};
9use std::fmt;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
16pub enum SyncState {
17 #[default]
19 Synced,
20 Syncing,
22 Queued,
24 Conflict,
26 Error,
28}
29
30impl SyncState {
31 pub fn needs_attention(&self) -> bool {
33 matches!(self, SyncState::Conflict | SyncState::Error)
34 }
35
36 pub fn is_pending(&self) -> bool {
38 matches!(self, SyncState::Syncing | SyncState::Queued)
39 }
40
41 pub fn icon_name(&self) -> &'static str {
43 match self {
44 SyncState::Synced => "check-circle",
45 SyncState::Syncing => "refresh-cw",
46 SyncState::Queued => "clock",
47 SyncState::Conflict => "alert-triangle",
48 SyncState::Error => "x-circle",
49 }
50 }
51
52 pub fn color_class(&self) -> &'static str {
54 match self {
55 SyncState::Synced => "text-green-500",
56 SyncState::Syncing => "text-blue-500",
57 SyncState::Queued => "text-orange-500",
58 SyncState::Conflict => "text-yellow-500",
59 SyncState::Error => "text-red-500",
60 }
61 }
62}
63
64impl fmt::Display for SyncState {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 SyncState::Synced => write!(f, "Synced"),
68 SyncState::Syncing => write!(f, "Syncing"),
69 SyncState::Queued => write!(f, "Waiting to sync"),
70 SyncState::Conflict => write!(f, "Has conflicts"),
71 SyncState::Error => write!(f, "Sync failed"),
72 }
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
78pub struct SyncMetadata {
79 pub state: SyncState,
81 pub last_synced: Option<u64>,
83 pub pending_changes: u32,
85 pub conflict_count: u32,
87 pub error_message: Option<String>,
89}
90
91impl SyncMetadata {
92 pub fn synced() -> Self {
94 Self {
95 state: SyncState::Synced,
96 last_synced: Some(current_timestamp_millis()),
97 ..Default::default()
98 }
99 }
100
101 pub fn syncing() -> Self {
103 Self {
104 state: SyncState::Syncing,
105 ..Default::default()
106 }
107 }
108
109 pub fn queued(pending_changes: u32) -> Self {
111 Self {
112 state: SyncState::Queued,
113 pending_changes,
114 ..Default::default()
115 }
116 }
117
118 pub fn conflict(conflict_count: u32) -> Self {
120 Self {
121 state: SyncState::Conflict,
122 conflict_count,
123 ..Default::default()
124 }
125 }
126
127 pub fn error(message: impl Into<String>) -> Self {
129 Self {
130 state: SyncState::Error,
131 error_message: Some(message.into()),
132 ..Default::default()
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
139pub struct SyncProgress {
140 pub total: u32,
142 pub completed: u32,
144 pub current_item: Option<String>,
146 pub bytes_transferred: u64,
148 pub bytes_total: u64,
150}
151
152impl SyncProgress {
153 pub fn percentage(&self) -> u8 {
155 if self.total == 0 {
156 return 100;
157 }
158 let pct = (self.completed as f64 / self.total as f64) * 100.0;
159 pct.min(100.0) as u8
160 }
161
162 pub fn bytes_percentage(&self) -> u8 {
164 if self.bytes_total == 0 {
165 return 100;
166 }
167 let pct = (self.bytes_transferred as f64 / self.bytes_total as f64) * 100.0;
168 pct.min(100.0) as u8
169 }
170
171 pub fn is_complete(&self) -> bool {
173 self.completed >= self.total
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
179pub struct SyncSummary {
180 pub synced_count: u32,
182 pub syncing_count: u32,
184 pub queued_count: u32,
186 pub conflict_count: u32,
188 pub error_count: u32,
190}
191
192impl SyncSummary {
193 pub fn overall_state(&self) -> SyncState {
195 if self.error_count > 0 {
196 SyncState::Error
197 } else if self.conflict_count > 0 {
198 SyncState::Conflict
199 } else if self.syncing_count > 0 {
200 SyncState::Syncing
201 } else if self.queued_count > 0 {
202 SyncState::Queued
203 } else {
204 SyncState::Synced
205 }
206 }
207
208 pub fn total(&self) -> u32 {
210 self.synced_count
211 + self.syncing_count
212 + self.queued_count
213 + self.conflict_count
214 + self.error_count
215 }
216}
217
218fn current_timestamp_millis() -> u64 {
220 std::time::SystemTime::now()
221 .duration_since(std::time::UNIX_EPOCH)
222 .map(|d| d.as_millis() as u64)
223 .unwrap_or(0)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_sync_state_display() {
232 assert_eq!(SyncState::Synced.to_string(), "Synced");
233 assert_eq!(SyncState::Syncing.to_string(), "Syncing");
234 assert_eq!(SyncState::Queued.to_string(), "Waiting to sync");
235 assert_eq!(SyncState::Conflict.to_string(), "Has conflicts");
236 assert_eq!(SyncState::Error.to_string(), "Sync failed");
237 }
238
239 #[test]
240 fn test_sync_state_needs_attention() {
241 assert!(!SyncState::Synced.needs_attention());
242 assert!(!SyncState::Syncing.needs_attention());
243 assert!(!SyncState::Queued.needs_attention());
244 assert!(SyncState::Conflict.needs_attention());
245 assert!(SyncState::Error.needs_attention());
246 }
247
248 #[test]
249 fn test_sync_state_is_pending() {
250 assert!(!SyncState::Synced.is_pending());
251 assert!(SyncState::Syncing.is_pending());
252 assert!(SyncState::Queued.is_pending());
253 assert!(!SyncState::Conflict.is_pending());
254 assert!(!SyncState::Error.is_pending());
255 }
256
257 #[test]
258 fn test_sync_metadata_constructors() {
259 let synced = SyncMetadata::synced();
260 assert_eq!(synced.state, SyncState::Synced);
261 assert!(synced.last_synced.is_some());
262
263 let queued = SyncMetadata::queued(5);
264 assert_eq!(queued.state, SyncState::Queued);
265 assert_eq!(queued.pending_changes, 5);
266
267 let conflict = SyncMetadata::conflict(3);
268 assert_eq!(conflict.state, SyncState::Conflict);
269 assert_eq!(conflict.conflict_count, 3);
270
271 let error = SyncMetadata::error("Connection failed");
272 assert_eq!(error.state, SyncState::Error);
273 assert_eq!(error.error_message, Some("Connection failed".to_string()));
274 }
275
276 #[test]
277 fn test_sync_progress_percentage() {
278 let progress = SyncProgress {
279 total: 10,
280 completed: 5,
281 ..Default::default()
282 };
283 assert_eq!(progress.percentage(), 50);
284
285 let complete = SyncProgress {
286 total: 10,
287 completed: 10,
288 ..Default::default()
289 };
290 assert_eq!(complete.percentage(), 100);
291 assert!(complete.is_complete());
292
293 let empty = SyncProgress::default();
294 assert_eq!(empty.percentage(), 100);
295 }
296
297 #[test]
298 fn test_sync_summary_overall_state() {
299 let all_synced = SyncSummary {
300 synced_count: 10,
301 ..Default::default()
302 };
303 assert_eq!(all_synced.overall_state(), SyncState::Synced);
304
305 let has_errors = SyncSummary {
306 synced_count: 8,
307 error_count: 2,
308 ..Default::default()
309 };
310 assert_eq!(has_errors.overall_state(), SyncState::Error);
311
312 let has_conflicts = SyncSummary {
313 synced_count: 8,
314 conflict_count: 2,
315 ..Default::default()
316 };
317 assert_eq!(has_conflicts.overall_state(), SyncState::Conflict);
318
319 let some_syncing = SyncSummary {
320 synced_count: 8,
321 syncing_count: 2,
322 ..Default::default()
323 };
324 assert_eq!(some_syncing.overall_state(), SyncState::Syncing);
325 }
326}