use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum SyncState {
#[default]
Synced,
Syncing,
Queued,
Conflict,
Error,
}
impl SyncState {
pub fn needs_attention(&self) -> bool {
matches!(self, SyncState::Conflict | SyncState::Error)
}
pub fn is_pending(&self) -> bool {
matches!(self, SyncState::Syncing | SyncState::Queued)
}
pub fn icon_name(&self) -> &'static str {
match self {
SyncState::Synced => "check-circle",
SyncState::Syncing => "refresh-cw",
SyncState::Queued => "clock",
SyncState::Conflict => "alert-triangle",
SyncState::Error => "x-circle",
}
}
pub fn color_class(&self) -> &'static str {
match self {
SyncState::Synced => "text-green-500",
SyncState::Syncing => "text-blue-500",
SyncState::Queued => "text-orange-500",
SyncState::Conflict => "text-yellow-500",
SyncState::Error => "text-red-500",
}
}
}
impl fmt::Display for SyncState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SyncState::Synced => write!(f, "Synced"),
SyncState::Syncing => write!(f, "Syncing"),
SyncState::Queued => write!(f, "Waiting to sync"),
SyncState::Conflict => write!(f, "Has conflicts"),
SyncState::Error => write!(f, "Sync failed"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SyncMetadata {
pub state: SyncState,
pub last_synced: Option<u64>,
pub pending_changes: u32,
pub conflict_count: u32,
pub error_message: Option<String>,
}
impl SyncMetadata {
pub fn synced() -> Self {
Self {
state: SyncState::Synced,
last_synced: Some(current_timestamp_millis()),
..Default::default()
}
}
pub fn syncing() -> Self {
Self {
state: SyncState::Syncing,
..Default::default()
}
}
pub fn queued(pending_changes: u32) -> Self {
Self {
state: SyncState::Queued,
pending_changes,
..Default::default()
}
}
pub fn conflict(conflict_count: u32) -> Self {
Self {
state: SyncState::Conflict,
conflict_count,
..Default::default()
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
state: SyncState::Error,
error_message: Some(message.into()),
..Default::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SyncProgress {
pub total: u32,
pub completed: u32,
pub current_item: Option<String>,
pub bytes_transferred: u64,
pub bytes_total: u64,
}
impl SyncProgress {
pub fn percentage(&self) -> u8 {
if self.total == 0 {
return 100;
}
let pct = (self.completed as f64 / self.total as f64) * 100.0;
pct.min(100.0) as u8
}
pub fn bytes_percentage(&self) -> u8 {
if self.bytes_total == 0 {
return 100;
}
let pct = (self.bytes_transferred as f64 / self.bytes_total as f64) * 100.0;
pct.min(100.0) as u8
}
pub fn is_complete(&self) -> bool {
self.completed >= self.total
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SyncSummary {
pub synced_count: u32,
pub syncing_count: u32,
pub queued_count: u32,
pub conflict_count: u32,
pub error_count: u32,
}
impl SyncSummary {
pub fn overall_state(&self) -> SyncState {
if self.error_count > 0 {
SyncState::Error
} else if self.conflict_count > 0 {
SyncState::Conflict
} else if self.syncing_count > 0 {
SyncState::Syncing
} else if self.queued_count > 0 {
SyncState::Queued
} else {
SyncState::Synced
}
}
pub fn total(&self) -> u32 {
self.synced_count
+ self.syncing_count
+ self.queued_count
+ self.conflict_count
+ self.error_count
}
}
fn current_timestamp_millis() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sync_state_display() {
assert_eq!(SyncState::Synced.to_string(), "Synced");
assert_eq!(SyncState::Syncing.to_string(), "Syncing");
assert_eq!(SyncState::Queued.to_string(), "Waiting to sync");
assert_eq!(SyncState::Conflict.to_string(), "Has conflicts");
assert_eq!(SyncState::Error.to_string(), "Sync failed");
}
#[test]
fn test_sync_state_needs_attention() {
assert!(!SyncState::Synced.needs_attention());
assert!(!SyncState::Syncing.needs_attention());
assert!(!SyncState::Queued.needs_attention());
assert!(SyncState::Conflict.needs_attention());
assert!(SyncState::Error.needs_attention());
}
#[test]
fn test_sync_state_is_pending() {
assert!(!SyncState::Synced.is_pending());
assert!(SyncState::Syncing.is_pending());
assert!(SyncState::Queued.is_pending());
assert!(!SyncState::Conflict.is_pending());
assert!(!SyncState::Error.is_pending());
}
#[test]
fn test_sync_metadata_constructors() {
let synced = SyncMetadata::synced();
assert_eq!(synced.state, SyncState::Synced);
assert!(synced.last_synced.is_some());
let queued = SyncMetadata::queued(5);
assert_eq!(queued.state, SyncState::Queued);
assert_eq!(queued.pending_changes, 5);
let conflict = SyncMetadata::conflict(3);
assert_eq!(conflict.state, SyncState::Conflict);
assert_eq!(conflict.conflict_count, 3);
let error = SyncMetadata::error("Connection failed");
assert_eq!(error.state, SyncState::Error);
assert_eq!(error.error_message, Some("Connection failed".to_string()));
}
#[test]
fn test_sync_progress_percentage() {
let progress = SyncProgress {
total: 10,
completed: 5,
..Default::default()
};
assert_eq!(progress.percentage(), 50);
let complete = SyncProgress {
total: 10,
completed: 10,
..Default::default()
};
assert_eq!(complete.percentage(), 100);
assert!(complete.is_complete());
let empty = SyncProgress::default();
assert_eq!(empty.percentage(), 100);
}
#[test]
fn test_sync_summary_overall_state() {
let all_synced = SyncSummary {
synced_count: 10,
..Default::default()
};
assert_eq!(all_synced.overall_state(), SyncState::Synced);
let has_errors = SyncSummary {
synced_count: 8,
error_count: 2,
..Default::default()
};
assert_eq!(has_errors.overall_state(), SyncState::Error);
let has_conflicts = SyncSummary {
synced_count: 8,
conflict_count: 2,
..Default::default()
};
assert_eq!(has_conflicts.overall_state(), SyncState::Conflict);
let some_syncing = SyncSummary {
synced_count: 8,
syncing_count: 2,
..Default::default()
};
assert_eq!(some_syncing.overall_state(), SyncState::Syncing);
}
}