Skip to content
Open

Dev #645

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
970 changes: 423 additions & 547 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion crates/dwall-settings/src/services/theme/applier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ impl ThemeApplier {
let writer = dwall::infrastructure::filesystem::config_writer::ConfigWriter;
writer.write_to_path(&config_path, &config)?;

if config.monitor_specific_wallpapers().is_empty() {
if config
.monitor_specific_wallpapers()
.is_none_or(|w| w.is_empty())
{
return Ok(());
}

Expand Down
9 changes: 6 additions & 3 deletions crates/dwall-settings/src/services/theme/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ impl ThemeStatusProvider {

let theme_id = if monitor_id == "all" {
match monitor_themes {
dwall::config::MonitorSpecificWallpapers::All(theme_id) => Some(theme_id.clone()),
dwall::config::MonitorSpecificWallpapers::Individual(themes_map) => {
Some(dwall::config::MonitorSpecificWallpapers::All(theme_id)) => {
Some(theme_id.clone())
}
Some(dwall::config::MonitorSpecificWallpapers::Individual(themes_map)) => {
let mut iter = themes_map.values();
let first_value = iter.next();
if iter.all(|value| Some(value) == first_value) {
Expand All @@ -34,9 +36,10 @@ impl ThemeStatusProvider {
None
}
}
None => None,
}
} else {
monitor_themes.get(monitor_id).cloned()
monitor_themes.and_then(|t| t.get(monitor_id).cloned())
};

Ok(theme_id)
Expand Down
6 changes: 4 additions & 2 deletions crates/dwall/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ dirs = { workspace = true, default-features = false }
serde = { workspace = true, default-features = false }
serde_json = { workspace = true, default-features = false }
thiserror = { workspace = true, default-features = false }
getrandom = "0.4"

[target.'cfg(windows)'.dependencies]
windows = { version = "0", default-features = false, features = [
# Keep version in sync with tauri
windows = { version = "0.61", default-features = false, features = [
"std",
"Devices_Geolocation",
"Win32_System_Registry",
Expand All @@ -49,7 +51,7 @@ windows = { version = "0", default-features = false, features = [


[dev-dependencies]
mockall = "0.13"
mockall = "0"
insta = { version = "1", features = ["json", "redactions"] }

[features]
Expand Down
200 changes: 187 additions & 13 deletions crates/dwall/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,31 @@ impl MonitorSpecificWallpapers {
}
}

/// 壁纸切换模式
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "snake_case", tag = "mode")]
pub enum WallpaperMode {
/// 固定主题模式:用户为显示器配置主题,根据太阳位置切换壁纸
Fixed {
#[serde(default = "default_monitor_specific_wallpapers")]
monitor_specific_wallpapers: MonitorSpecificWallpapers,
},
/// 随机主题模式:每天随机选择一套主题,所有显示器统一
Random {
/// 随机池(None 表示使用所有可用主题)
#[serde(default)]
pool: Option<Vec<String>>,
},
}

impl Default for WallpaperMode {
fn default() -> Self {
WallpaperMode::Fixed {
monitor_specific_wallpapers: MonitorSpecificWallpapers::Individual(HashMap::new()),
}
}
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(untagged)]
pub enum Network {
Expand Down Expand Up @@ -170,9 +195,9 @@ pub struct Config {
#[serde(default = "default_customized_themes_directory")]
customized_themes_directory: PathBuf,

/// Wallpapers specific to each monitor, using monitor ID as key
#[serde(default = "default_monitor_specific_wallpapers")]
monitor_specific_wallpapers: MonitorSpecificWallpapers,
/// Wallpaper mode (Fixed or Random)
#[serde(default)]
wallpaper_mode: WallpaperMode,

/// Time interval for detecting solar altitude angle and azimuth angle
/// Measured in seconds, range: `[MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS]`
Expand Down Expand Up @@ -312,9 +337,24 @@ impl Config {
&self.position_source
}

/// Returns the monitor-specific wallpapers map
pub fn monitor_specific_wallpapers(&self) -> &MonitorSpecificWallpapers {
&self.monitor_specific_wallpapers
/// Returns the wallpaper mode
pub fn wallpaper_mode(&self) -> &WallpaperMode {
&self.wallpaper_mode
}

/// Returns the monitor-specific wallpapers map (only for Fixed mode)
pub fn monitor_specific_wallpapers(&self) -> Option<&MonitorSpecificWallpapers> {
match &self.wallpaper_mode {
WallpaperMode::Fixed {
monitor_specific_wallpapers,
} => Some(monitor_specific_wallpapers),
WallpaperMode::Random { .. } => None,
}
}

/// Set the wallpaper mode
pub fn set_wallpaper_mode(&mut self, mode: WallpaperMode) {
self.wallpaper_mode = mode;
}
}

Expand All @@ -329,7 +369,7 @@ impl Default for Config {
themes_directory: default_themes_directory(),
customized_themes_directory: default_customized_themes_directory(),
lock_screen_wallpaper_enabled: default_lock_screen_wallpaper_enabled(),
monitor_specific_wallpapers: default_monitor_specific_wallpapers(),
wallpaper_mode: Default::default(),
// On the equator, an azimuth change of 0.1 degrees takes
// approximately 12 seconds, and an altitude change of 0.1
// degrees takes about 24 seconds.
Expand Down Expand Up @@ -383,6 +423,10 @@ pub struct RawConfig {
#[serde(default = "default_monitor_specific_wallpapers")]
monitor_specific_wallpapers: MonitorSpecificWallpapers,

/// Wallpaper mode (new field for migration)
#[serde(default)]
wallpaper_mode: Option<WallpaperMode>,

/// Time interval for detecting solar altitude angle and azimuth angle
/// Measured in seconds, range: `[MIN_INTERVAL_SECONDS, MAX_INTERVAL_SECONDS]`
#[serde(
Expand All @@ -400,6 +444,11 @@ impl From<RawConfig> for Config {
.map(Network::GitHubMirrorTemplate)
});

// Migrate wallpaper_mode: if absent, wrap monitor_specific_wallpapers in Fixed mode
let wallpaper_mode = raw.wallpaper_mode.unwrap_or(WallpaperMode::Fixed {
monitor_specific_wallpapers: raw.monitor_specific_wallpapers,
});

Config {
title_bar_color_follows_windows_theme: raw.title_bar_color_follows_windows_theme,
network,
Expand All @@ -409,7 +458,7 @@ impl From<RawConfig> for Config {
lock_screen_wallpaper_enabled: raw.lock_screen_wallpaper_enabled,
themes_directory: raw.themes_directory,
customized_themes_directory: raw.customized_themes_directory,
monitor_specific_wallpapers: raw.monitor_specific_wallpapers,
wallpaper_mode,
interval: raw.interval,
}
}
Expand Down Expand Up @@ -463,7 +512,9 @@ mod tests {
lock_screen_wallpaper_enabled: false,
themes_directory: PathBuf::from("/tmp/themes"),
customized_themes_directory: PathBuf::from("/tmp/customize"),
monitor_specific_wallpapers: MonitorSpecificWallpapers::All("theme1".to_string()),
wallpaper_mode: WallpaperMode::Fixed {
monitor_specific_wallpapers: MonitorSpecificWallpapers::All("theme1".to_string()),
},
interval: 30,
};

Expand All @@ -473,10 +524,7 @@ mod tests {
assert_eq!(deserialized.interval, original.interval);
assert_eq!(deserialized.image_format, original.image_format);
assert_eq!(deserialized.network, original.network);
assert_eq!(
deserialized.monitor_specific_wallpapers,
original.monitor_specific_wallpapers
);
assert_eq!(deserialized.wallpaper_mode, original.wallpaper_mode);
}

/// Test RawConfig migration from legacy github_mirror_template
Expand All @@ -495,6 +543,7 @@ mod tests {
monitor_specific_wallpapers: MonitorSpecificWallpapers::Individual(
std::collections::HashMap::new(),
),
wallpaper_mode: None,
interval: 15,
};

Expand Down Expand Up @@ -526,6 +575,7 @@ mod tests {
monitor_specific_wallpapers: MonitorSpecificWallpapers::Individual(
std::collections::HashMap::new(),
),
wallpaper_mode: None,
interval: 15,
};

Expand All @@ -540,6 +590,37 @@ mod tests {
);
}

/// Test RawConfig migration wraps monitor_specific_wallpapers in Fixed mode
#[test]
fn raw_config_migration_wallpaper_mode() {
let mut map = std::collections::HashMap::new();
map.insert("monitor1".to_string(), "theme1".to_string());

let raw = RawConfig {
github_mirror_template: None,
network: None,
title_bar_color_follows_windows_theme: false,
image_format: ImageFormat::Jpeg,
position_source: PositionSource::default(),
auto_detect_color_scheme: true,
lock_screen_wallpaper_enabled: true,
themes_directory: PathBuf::from("/tmp/themes"),
customized_themes_directory: PathBuf::from("/tmp/customize"),
monitor_specific_wallpapers: MonitorSpecificWallpapers::Individual(map.clone()),
wallpaper_mode: None, // Old config without wallpaper_mode
interval: 15,
};

let config: Config = raw.into();
// Should be migrated to Fixed mode
assert_eq!(
config.wallpaper_mode,
WallpaperMode::Fixed {
monitor_specific_wallpapers: MonitorSpecificWallpapers::Individual(map)
}
);
}

/// Test MonitorSpecificWallpapers serialization variants
#[test]
fn monitor_wallpapers_all_variant() {
Expand Down Expand Up @@ -632,6 +713,99 @@ mod tests {
assert!(!config.auto_detect_color_scheme);
}

/// Test WallpaperMode::Fixed serialization
#[test]
fn wallpaper_mode_fixed_serialization() {
let mode = WallpaperMode::Fixed {
monitor_specific_wallpapers: MonitorSpecificWallpapers::All("theme1".to_string()),
};
let json = serde_json::to_string(&mode).unwrap();
assert!(json.contains(r#""mode":"fixed""#));
assert!(json.contains("theme1"));
}

/// Test WallpaperMode::Random serialization
#[test]
fn wallpaper_mode_random_serialization() {
let mode = WallpaperMode::Random {
pool: Some(vec!["theme1".to_string(), "theme2".to_string()]),
};
let json = serde_json::to_string(&mode).unwrap();
assert!(json.contains(r#""mode":"random""#));
assert!(json.contains("theme1"));
}

/// Test WallpaperMode::Random with null pool
#[test]
fn wallpaper_mode_random_null_pool() {
let json = r#"{"mode":"random","pool":null}"#;
let mode: WallpaperMode = serde_json::from_str(json).unwrap();
assert!(matches!(mode, WallpaperMode::Random { pool: None }));
}

/// Test WallpaperMode default is Fixed
#[test]
fn wallpaper_mode_default_is_fixed() {
let mode = WallpaperMode::default();
assert!(matches!(mode, WallpaperMode::Fixed { .. }));
}

/// Test old config without wallpaper_mode migrates to Fixed
#[test]
fn old_config_migrates_to_fixed_mode() {
// Old config format: no wallpaper_mode field
let json = r#"{"monitor_specific_wallpapers": "theme1"}"#;
let raw: RawConfig = serde_json::from_str(json).unwrap();
let config: Config = raw.into();

assert!(matches!(
config.wallpaper_mode(),
WallpaperMode::Fixed { .. }
));
}

/// Test new config with wallpaper_mode is preserved
#[test]
fn new_config_preserves_wallpaper_mode() {
let json = r#"{
"wallpaper_mode": {
"mode": "random",
"pool": ["theme1", "theme2"]
}
}"#;
let raw: RawConfig = serde_json::from_str(json).unwrap();
let config: Config = raw.into();

match config.wallpaper_mode() {
WallpaperMode::Random { pool } => {
assert_eq!(pool.as_ref().unwrap().len(), 2);
}
_ => panic!("Expected Random mode"),
}
}

/// Test monitor_specific_wallpapers() returns None for Random mode
#[test]
fn monitor_specific_wallpapers_none_for_random_mode() {
let config = Config {
wallpaper_mode: WallpaperMode::Random { pool: None },
..Config::default()
};
assert!(config.monitor_specific_wallpapers().is_none());
}

/// Test monitor_specific_wallpapers() returns Some for Fixed mode
#[test]
fn monitor_specific_wallpapers_some_for_fixed_mode() {
let config = Config {
wallpaper_mode: WallpaperMode::Fixed {
monitor_specific_wallpapers: MonitorSpecificWallpapers::All("theme1".to_string()),
},
..Config::default()
};
assert!(config.monitor_specific_wallpapers().is_some());
}

// ── Snapshot tests ────────────────────────────────────────────────────────

#[test]
Expand Down
Loading
Loading