1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7const CATEGORY_PLASMA_WIDGET: u16 = 705;
9const CATEGORY_WALLPAPER_PLUGIN: u16 = 715;
10const CATEGORY_KWIN_EFFECT: u16 = 719;
11const CATEGORY_KWIN_SCRIPT: u16 = 720;
12const CATEGORY_KWIN_SWITCHER: u16 = 721;
13const CATEGORY_GLOBAL_THEME: u16 = 722;
14const CATEGORY_PLASMA_STYLE: u16 = 709;
15const CATEGORY_AURORAE_DECORATION: u16 = 114;
16const CATEGORY_COLOR_SCHEME: u16 = 112;
17const CATEGORY_SPLASH_SCREEN: u16 = 708;
18const CATEGORY_SDDM_THEME: u16 = 101;
19const CATEGORY_ICON_THEME: u16 = 132;
20const CATEGORY_WALLPAPER: u16 = 299;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum ComponentType {
29 PlasmaWidget,
30 WallpaperPlugin,
31 KWinEffect,
32 KWinScript,
33 KWinSwitcher,
34 GlobalTheme,
35 PlasmaStyle,
36 AuroraeDecoration,
37 ColorScheme,
38 SplashScreen,
39 SddmTheme,
40 IconTheme,
41 Wallpaper,
42}
43
44impl ComponentType {
45 pub(crate) const fn category_id(self) -> u16 {
46 match self {
47 Self::PlasmaWidget => CATEGORY_PLASMA_WIDGET,
48 Self::WallpaperPlugin => CATEGORY_WALLPAPER_PLUGIN,
49 Self::KWinEffect => CATEGORY_KWIN_EFFECT,
50 Self::KWinScript => CATEGORY_KWIN_SCRIPT,
51 Self::KWinSwitcher => CATEGORY_KWIN_SWITCHER,
52 Self::GlobalTheme => CATEGORY_GLOBAL_THEME,
53 Self::PlasmaStyle => CATEGORY_PLASMA_STYLE,
54 Self::AuroraeDecoration => CATEGORY_AURORAE_DECORATION,
55 Self::ColorScheme => CATEGORY_COLOR_SCHEME,
56 Self::SplashScreen => CATEGORY_SPLASH_SCREEN,
57 Self::SddmTheme => CATEGORY_SDDM_THEME,
58 Self::IconTheme => CATEGORY_ICON_THEME,
59 Self::Wallpaper => CATEGORY_WALLPAPER,
60 }
61 }
62
63 pub(crate) const fn matches_type_id(self, type_id: u16) -> bool {
70 if self.category_id() == type_id {
71 return true;
72 }
73 matches!((self, type_id), (Self::PlasmaWidget, 700..=799))
78 }
79
80 pub(crate) const fn kpackage_type(self) -> Option<&'static str> {
81 match self {
82 Self::PlasmaWidget => Some("Plasma/Applet"),
83 Self::WallpaperPlugin => Some("Plasma/Wallpaper"),
84 Self::KWinEffect => Some("KWin/Effect"),
85 Self::KWinScript => Some("KWin/Script"),
86 Self::KWinSwitcher => Some("KWin/WindowSwitcher"),
87 Self::GlobalTheme => Some("Plasma/LookAndFeel"),
88 Self::PlasmaStyle => Some("Plasma/Theme"),
89 Self::SplashScreen => Some("Plasma/LookAndFeel"),
90 _ => None,
91 }
92 }
93
94 pub(crate) const fn has_direct_fallback(self) -> bool {
102 matches!(
103 self,
104 Self::GlobalTheme | Self::PlasmaStyle | Self::SplashScreen
105 )
106 }
107
108 pub(crate) const fn registry_only(self) -> bool {
111 matches!(self, Self::IconTheme | Self::Wallpaper | Self::ColorScheme)
112 }
113
114 pub(crate) fn shared_path_types(self) -> &'static [ComponentType] {
120 match self {
121 Self::GlobalTheme | Self::SplashScreen => &[Self::GlobalTheme, Self::SplashScreen],
122 Self::PlasmaWidget => &[Self::PlasmaWidget],
123 Self::WallpaperPlugin => &[Self::WallpaperPlugin],
124 Self::KWinEffect => &[Self::KWinEffect],
125 Self::KWinScript => &[Self::KWinScript],
126 Self::KWinSwitcher => &[Self::KWinSwitcher],
127 Self::PlasmaStyle => &[Self::PlasmaStyle],
128 Self::AuroraeDecoration => &[Self::AuroraeDecoration],
129 Self::ColorScheme => &[Self::ColorScheme],
130 Self::SddmTheme => &[Self::SddmTheme],
131 Self::IconTheme => &[Self::IconTheme],
132 Self::Wallpaper => &[Self::Wallpaper],
133 }
134 }
135
136 pub(crate) const fn user_suffix(self) -> Option<&'static str> {
140 match self {
141 Self::PlasmaWidget => Some("plasma/plasmoids"),
142 Self::WallpaperPlugin => Some("plasma/wallpapers"),
143 Self::KWinEffect => Some("kwin/effects"),
144 Self::KWinScript => Some("kwin/scripts"),
145 Self::KWinSwitcher => Some("kwin/tabbox"),
146 Self::GlobalTheme | Self::SplashScreen => Some("plasma/look-and-feel"),
147 Self::PlasmaStyle => Some("plasma/desktoptheme"),
148 Self::AuroraeDecoration => Some("aurorae/themes"),
149 Self::ColorScheme => Some("color-schemes"),
150 Self::SddmTheme => None,
151 Self::IconTheme => Some("icons"),
152 Self::Wallpaper => Some("wallpapers"),
153 }
154 }
155
156 pub fn user_path(self) -> PathBuf {
158 match self.user_suffix() {
159 Some(suffix) => crate::paths::data_home().join(suffix),
160 None => PathBuf::new(),
161 }
162 }
163
164 pub fn system_path(self) -> PathBuf {
166 PathBuf::from(match self {
167 Self::PlasmaWidget => "/usr/share/plasma/plasmoids",
168 Self::WallpaperPlugin => "/usr/share/plasma/wallpapers",
169 Self::KWinEffect => "/usr/share/kwin/effects",
170 Self::KWinScript => "/usr/share/kwin/scripts",
171 Self::KWinSwitcher => "/usr/share/kwin/tabbox",
172 Self::GlobalTheme | Self::SplashScreen => "/usr/share/plasma/look-and-feel",
173 Self::PlasmaStyle => "/usr/share/plasma/desktoptheme",
174 Self::AuroraeDecoration => "/usr/share/aurorae/themes",
175 Self::ColorScheme => "/usr/share/color-schemes",
176 Self::SddmTheme => "/usr/share/sddm/themes",
177 Self::IconTheme => "/usr/share/icons",
178 Self::Wallpaper => "/usr/share/wallpapers",
179 })
180 }
181
182 pub(crate) const fn backup_subdir(self) -> &'static str {
184 match self {
185 Self::PlasmaWidget => "plasma-plasmoids",
186 Self::WallpaperPlugin => "plasma-wallpapers",
187 Self::KWinEffect => "kwin-effects",
188 Self::KWinScript => "kwin-scripts",
189 Self::KWinSwitcher => "kwin-tabbox",
190 Self::GlobalTheme => "plasma-look-and-feel",
191 Self::PlasmaStyle => "plasma-desktoptheme",
192 Self::AuroraeDecoration => "aurorae-themes",
193 Self::ColorScheme => "color-schemes",
194 Self::SplashScreen => "plasma-splash",
195 Self::SddmTheme => "sddm-themes",
196 Self::IconTheme => "icons",
197 Self::Wallpaper => "wallpapers",
198 }
199 }
200
201 pub(crate) const fn registry_file(self) -> Option<&'static str> {
204 match self {
205 Self::PlasmaWidget => Some("plasmoids.knsregistry"),
206 Self::KWinEffect => Some("kwineffect.knsregistry"),
207 Self::KWinScript => Some("kwinscripts.knsregistry"),
208 Self::KWinSwitcher => Some("kwinswitcher.knsregistry"),
209 Self::WallpaperPlugin => Some("wallpaperplugin.knsregistry"),
210 Self::GlobalTheme => Some("lookandfeel.knsregistry"),
211 Self::PlasmaStyle => Some("plasma-themes.knsregistry"),
212 Self::AuroraeDecoration => Some("aurorae.knsregistry"),
213 Self::ColorScheme => Some("colorschemes.knsregistry"),
214 Self::SplashScreen => Some("ksplash.knsregistry"),
215 Self::SddmTheme => Some("sddmtheme.knsregistry"),
216 Self::IconTheme => Some("icons.knsregistry"),
217 Self::Wallpaper => Some("wallpaper.knsregistry"),
218 }
219 }
220
221 pub const fn all() -> &'static [ComponentType] {
224 &[
225 Self::PlasmaWidget,
226 Self::WallpaperPlugin,
227 Self::KWinEffect,
228 Self::KWinScript,
229 Self::KWinSwitcher,
230 Self::GlobalTheme,
231 Self::PlasmaStyle,
232 Self::AuroraeDecoration,
233 Self::ColorScheme,
234 Self::SplashScreen,
235 Self::SddmTheme,
236 Self::IconTheme,
237 Self::Wallpaper,
238 ]
239 }
240
241 pub const fn all_user() -> &'static [ComponentType] {
242 &[
243 Self::PlasmaWidget,
244 Self::WallpaperPlugin,
245 Self::KWinEffect,
246 Self::KWinScript,
247 Self::KWinSwitcher,
248 Self::GlobalTheme,
249 Self::PlasmaStyle,
250 Self::AuroraeDecoration,
251 Self::ColorScheme,
252 Self::SplashScreen,
253 Self::IconTheme,
254 Self::Wallpaper,
255 ]
256 }
257}
258
259impl std::fmt::Display for ComponentType {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 match self {
262 Self::PlasmaWidget => write!(f, "Plasma Widget"),
263 Self::WallpaperPlugin => write!(f, "Wallpaper Plugin"),
264 Self::KWinEffect => write!(f, "KWin Effect"),
265 Self::KWinScript => write!(f, "KWin Script"),
266 Self::KWinSwitcher => write!(f, "KWin Switcher"),
267 Self::GlobalTheme => write!(f, "Global Theme"),
268 Self::PlasmaStyle => write!(f, "Plasma Style"),
269 Self::AuroraeDecoration => write!(f, "Aurorae Decoration"),
270 Self::ColorScheme => write!(f, "Color Scheme"),
271 Self::SplashScreen => write!(f, "Splash Screen"),
272 Self::SddmTheme => write!(f, "SDDM Theme"),
273 Self::IconTheme => write!(f, "Icon Theme"),
274 Self::Wallpaper => write!(f, "Wallpaper"),
275 }
276 }
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct InstalledComponent {
284 pub name: String,
285 pub directory_name: String,
286 pub version: String,
287 pub component_type: ComponentType,
288 #[serde(with = "pathbuf_serde")]
289 pub path: PathBuf,
290 pub is_system: bool,
291 pub release_date: String,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct AvailableUpdate {
297 pub installed: InstalledComponent,
298 pub content_id: u64,
299 pub latest_version: String,
300 pub download_url: String,
301 pub store_url: String,
302 pub release_date: String,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub checksum: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub download_size: Option<u64>,
307}
308
309pub(crate) struct AvailableUpdateBuilder {
311 installed: InstalledComponent,
312 content_id: u64,
313 latest_version: String,
314 download_url: String,
315 release_date: String,
316 checksum: Option<String>,
317 download_size: Option<u64>,
318}
319
320impl AvailableUpdateBuilder {
321 pub(crate) fn checksum(mut self, checksum: Option<String>) -> Self {
322 self.checksum = checksum;
323 self
324 }
325
326 pub(crate) fn download_size(mut self, size: Option<u64>) -> Self {
327 self.download_size = size;
328 self
329 }
330
331 pub(crate) fn build(self) -> AvailableUpdate {
332 let store_url = format!("https://store.kde.org/p/{}", self.content_id);
333 AvailableUpdate {
334 installed: self.installed,
335 content_id: self.content_id,
336 latest_version: self.latest_version,
337 download_url: self.download_url,
338 store_url,
339 release_date: self.release_date,
340 checksum: self.checksum,
341 download_size: self.download_size,
342 }
343 }
344}
345
346impl AvailableUpdate {
347 pub(crate) fn builder(
348 installed: InstalledComponent,
349 content_id: u64,
350 latest_version: String,
351 download_url: String,
352 release_date: String,
353 ) -> AvailableUpdateBuilder {
354 AvailableUpdateBuilder {
355 installed,
356 content_id,
357 latest_version,
358 download_url,
359 release_date,
360 checksum: None,
361 download_size: None,
362 }
363 }
364}
365
366#[derive(Debug, Clone)]
368pub(crate) struct StoreEntry {
369 pub id: u64,
370 pub name: String,
371 pub version: String,
372 pub type_id: u16,
373 pub download_links: Vec<DownloadLink>,
374 pub changed_date: String,
375}
376
377#[derive(Debug, Clone)]
379pub(crate) struct DownloadLink {
380 pub url: String,
381 pub version: String,
382 pub checksum: Option<String>,
383 pub size_kb: Option<u64>,
384}
385
386#[derive(Debug, Clone, Default, Deserialize)]
388pub(crate) struct PackageMetadata {
389 #[serde(rename = "KPlugin")]
390 pub kplugin: Option<KPluginInfo>,
391}
392
393#[derive(Debug, Clone, Default, Deserialize, Serialize)]
395pub(crate) struct KPluginInfo {
396 #[serde(rename = "Name")]
397 pub name: Option<String>,
398 #[serde(rename = "Version")]
399 pub version: Option<String>,
400 #[serde(rename = "Description")]
401 pub description: Option<String>,
402 #[serde(rename = "Icon")]
403 pub icon: Option<String>,
404}
405
406impl PackageMetadata {
407 pub(crate) fn name(&self) -> Option<&str> {
408 self.kplugin.as_ref()?.name.as_deref()
409 }
410
411 pub(crate) fn version(&self) -> Option<&str> {
412 self.kplugin.as_ref()?.version.as_deref()
413 }
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct Diagnostic {
422 pub name: String,
423 pub reason: String,
424 #[serde(skip_serializing_if = "Option::is_none")]
425 pub installed_version: Option<String>,
426 #[serde(skip_serializing_if = "Option::is_none")]
427 pub available_version: Option<String>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub content_id: Option<u64>,
430}
431
432impl Diagnostic {
433 pub(crate) fn new(name: String, reason: String) -> Self {
434 Self {
435 name,
436 reason,
437 installed_version: None,
438 available_version: None,
439 content_id: None,
440 }
441 }
442
443 pub(crate) fn with_versions(
444 mut self,
445 installed: Option<String>,
446 available: Option<String>,
447 ) -> Self {
448 self.installed_version = installed;
449 self.available_version = available;
450 self
451 }
452
453 pub(crate) fn with_content_id(mut self, id: u64) -> Self {
454 self.content_id = Some(id);
455 self
456 }
457}
458
459#[derive(Debug, Clone, Default, Serialize, Deserialize)]
461pub(crate) struct UpdateCheckResult {
462 pub updates: Vec<AvailableUpdate>,
463 pub unresolved: Vec<Diagnostic>,
464 pub check_failures: Vec<Diagnostic>,
465}
466
467impl UpdateCheckResult {
468 pub fn add_update(&mut self, update: AvailableUpdate) {
469 self.updates.push(update);
470 }
471
472 pub fn add_unresolved(&mut self, diagnostic: Diagnostic) {
473 self.unresolved.push(diagnostic);
474 }
475
476 pub fn add_check_failure(&mut self, diagnostic: Diagnostic) {
477 self.check_failures.push(diagnostic);
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn shared_path_types_returns_both_for_global_theme() {
487 let types = ComponentType::GlobalTheme.shared_path_types();
488 assert!(types.contains(&ComponentType::GlobalTheme));
489 assert!(types.contains(&ComponentType::SplashScreen));
490 assert_eq!(types.len(), 2);
491 }
492
493 #[test]
494 fn shared_path_types_returns_both_for_splash_screen() {
495 let types = ComponentType::SplashScreen.shared_path_types();
496 assert!(types.contains(&ComponentType::GlobalTheme));
497 assert!(types.contains(&ComponentType::SplashScreen));
498 assert_eq!(types.len(), 2);
499 }
500
501 #[test]
502 fn plasma_widget_matches_extended_subcategories() {
503 assert!(ComponentType::PlasmaWidget.matches_type_id(705)); assert!(ComponentType::PlasmaWidget.matches_type_id(706)); assert!(ComponentType::PlasmaWidget.matches_type_id(714)); assert!(ComponentType::PlasmaWidget.matches_type_id(718)); assert!(ComponentType::PlasmaWidget.matches_type_id(723)); assert!(!ComponentType::PlasmaWidget.matches_type_id(100)); assert!(!ComponentType::PlasmaWidget.matches_type_id(800)); }
511
512 #[test]
513 fn shared_path_types_returns_single_for_unique_path() {
514 assert_eq!(
515 ComponentType::PlasmaWidget.shared_path_types(),
516 &[ComponentType::PlasmaWidget]
517 );
518 assert_eq!(
519 ComponentType::KWinEffect.shared_path_types(),
520 &[ComponentType::KWinEffect]
521 );
522 assert_eq!(
523 ComponentType::IconTheme.shared_path_types(),
524 &[ComponentType::IconTheme]
525 );
526 }
527}
528
529mod pathbuf_serde {
530 use std::path::{Path, PathBuf};
531
532 use serde::{self, Deserialize, Deserializer, Serializer};
533
534 pub fn serialize<S>(path: &Path, serializer: S) -> Result<S::Ok, S::Error>
535 where
536 S: Serializer,
537 {
538 serializer.serialize_str(&path.to_string_lossy())
539 }
540
541 pub fn deserialize<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
542 where
543 D: Deserializer<'de>,
544 {
545 let s = String::deserialize(deserializer)?;
546 Ok(PathBuf::from(s))
547 }
548}