Skip to main content

utils/
color.rs

1use color_thief::{ColorFormat, get_palette};
2use image::ImageReader;
3use reqwest;
4use std::io::Cursor;
5
6#[derive(Debug, Clone, PartialEq, Default)]
7pub struct Color {
8    pub r: u8,
9    pub g: u8,
10    pub b: u8,
11}
12
13impl Color {
14    pub fn new(r: u8, g: u8, b: u8) -> Self {
15        Self { r, g, b }
16    }
17}
18
19pub async fn get_palette_from_url(url: &str) -> Option<Vec<Color>> {
20    let bytes = if url.starts_with("http") {
21        reqwest::get(url).await.ok()?.bytes().await.ok()?.to_vec()
22    } else {
23        let path = if let Some(stripped) = url.strip_prefix("artwork://local") {
24            let decoded = percent_encoding::percent_decode_str(stripped).decode_utf8_lossy();
25            decoded.to_string()
26        } else {
27            url.to_string()
28        };
29        std::fs::read(path).ok()?
30    };
31
32    let img = ImageReader::new(Cursor::new(bytes))
33        .with_guessed_format()
34        .ok()?
35        .decode()
36        .ok()?;
37
38    let rgb = img.to_rgb8();
39    let pixels = rgb.as_raw();
40
41    let palette = get_palette(pixels, ColorFormat::Rgb, 10, 8).ok()?;
42
43    Some(
44        palette
45            .into_iter()
46            .map(|p| Color::new(p.r, p.g, p.b))
47            .collect(),
48    )
49}
50
51pub fn get_background_style(colors: Option<&[Color]>) -> String {
52    if let Some(colors) = colors
53        && !colors.is_empty()
54    {
55        let bg_color = &colors[0];
56        let mut bg_image_parts = Vec::new();
57        let positions = [
58            "0% 0%",
59            "100% 0%",
60            "100% 100%",
61            "0% 100%",
62            "50% 50%",
63            "25% 0%",
64            "75% 100%",
65        ];
66        for (i, c) in colors.iter().skip(1).enumerate().take(positions.len()) {
67            let pos = positions[i];
68            bg_image_parts.push(format!(
69                "radial-gradient(circle at {}, rgba({}, {}, {}, 0.8) 0%, transparent 80%)",
70                pos, c.r, c.g, c.b
71            ));
72        }
73
74        if bg_image_parts.is_empty() {
75            return format!(
76                "background-color: rgb({}, {}, {}); background-image: none;",
77                bg_color.r, bg_color.g, bg_color.b
78            );
79        } else {
80            return format!(
81                "background-color: rgb({}, {}, {}); background-image: {};",
82                bg_color.r,
83                bg_color.g,
84                bg_color.b,
85                bg_image_parts.join(", ")
86            );
87        }
88    }
89    "background-color: var(--color-black); background-image: none;".to_string()
90}