🏡 index : ~doyle/shalom.git

author Jordan Doyle <jordan@doyle.la> 2023-11-07 18:58:59.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-11-07 18:58:59.0 +00:00:00
commit
05593d27b747151b0b5f11971fe4d990e3ce7055 [patch]
tree
0d3d9da15477b22c04e77529eb5e05f4c5997a7a
parent
d658805f73ca44fc88bf59a4da48d85530d241f1
download
05593d27b747151b0b5f11971fe4d990e3ce7055.tar.gz

Load cameras on omni view



Diff

 assets/icons/README.md               |  1 +-
 assets/icons/backward.svg            |  4 +--
 assets/icons/forward.svg             |  4 +--
 assets/icons/pause.svg               |  4 +--
 assets/icons/play.svg                |  4 +--
 assets/icons/repeat-1.svg            |  1 +-
 assets/icons/repeat.svg              |  4 +--
 assets/icons/shuffle.svg             |  4 +--
 shalom/src/hass_client.rs            | 14 ++---
 shalom/src/oracle.rs                 | 45 +++++++++++++++++-
 shalom/src/pages/omni.rs             | 95 +++++++++++++++++++++++++++++++++++--
 shalom/src/pages/room.rs             |  4 +-
 shalom/src/subscriptions.rs          |  8 +--
 shalom/src/theme.rs                  |  2 +-
 shalom/src/widgets/colour_picker.rs  |  6 +--
 shalom/src/widgets/forced_rounded.rs |  8 ++-
 shalom/src/widgets/media_player.rs   | 89 +++++++++++++++++++++++++++++------
 17 files changed, 240 insertions(+), 57 deletions(-)

diff --git a/assets/icons/README.md b/assets/icons/README.md
index ad6e55f..f40eabf 100644
--- a/assets/icons/README.md
+++ b/assets/icons/README.md
@@ -2,3 +2,4 @@

- https://heroicons.com/
- https://github.com/basmilius/weather-icons
- https://fonts.google.com/icons
diff --git a/assets/icons/backward.svg b/assets/icons/backward.svg
index a358c55..7347272 100644
--- a/assets/icons/backward.svg
+++ b/assets/icons/backward.svg
@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
    <path stroke-linecap="round" stroke-linejoin="round" d="M21 16.811c0 .864-.933 1.405-1.683.977l-7.108-4.062a1.125 1.125 0 010-1.953l7.108-4.062A1.125 1.125 0 0121 8.688v8.123zM11.25 16.811c0 .864-.933 1.405-1.683.977l-7.108-4.062a1.125 1.125 0 010-1.953L9.567 7.71a1.125 1.125 0 011.683.977v8.123z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
\ No newline at end of file
diff --git a/assets/icons/forward.svg b/assets/icons/forward.svg
index 7366fe5..48b0ea9 100644
--- a/assets/icons/forward.svg
+++ b/assets/icons/forward.svg
@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
    <path stroke-linecap="round" stroke-linejoin="round" d="M3 8.688c0-.864.933-1.405 1.683-.977l7.108 4.062a1.125 1.125 0 010 1.953l-7.108 4.062A1.125 1.125 0 013 16.81V8.688zM12.75 8.688c0-.864.933-1.405 1.683-.977l7.108 4.062a1.125 1.125 0 010 1.953l-7.108 4.062a1.125 1.125 0 01-1.683-.977V8.688z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
\ No newline at end of file
diff --git a/assets/icons/pause.svg b/assets/icons/pause.svg
index 68225b1..914f25a 100644
--- a/assets/icons/pause.svg
+++ b/assets/icons/pause.svg
@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
    <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M11,16H9V8h2V16z M15,16h-2V8h2V16z"/></g></g></svg>
\ No newline at end of file
diff --git a/assets/icons/play.svg b/assets/icons/play.svg
index cecf998..1bb457c 100644
--- a/assets/icons/play.svg
+++ b/assets/icons/play.svg
@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
    <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M9.5,16.5v-9l7,4.5L9.5,16.5z"/></g></svg>
\ No newline at end of file
diff --git a/assets/icons/repeat-1.svg b/assets/icons/repeat-1.svg
new file mode 100644
index 0000000..d90acfa
--- /dev/null
+++ b/assets/icons/repeat-1.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4zm-4-2V9h-1l-2 1v1h1.5v4H13z"/></svg>
\ No newline at end of file
diff --git a/assets/icons/repeat.svg b/assets/icons/repeat.svg
index e63b652..cd41e84 100644
--- a/assets/icons/repeat.svg
+++ b/assets/icons/repeat.svg
@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
    <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 00-3.7-3.7 48.678 48.678 0 00-7.324 0 4.006 4.006 0 00-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3l-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 003.7 3.7 48.656 48.656 0 007.324 0 4.006 4.006 0 003.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3l-3 3" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg>
\ No newline at end of file
diff --git a/assets/icons/shuffle.svg b/assets/icons/shuffle.svg
index a5d2841..5bff963 100644
--- a/assets/icons/shuffle.svg
+++ b/assets/icons/shuffle.svg
@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
    <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"/></svg>
\ No newline at end of file
diff --git a/shalom/src/hass_client.rs b/shalom/src/hass_client.rs
index 1d837f5..9f8df6d 100644
--- a/shalom/src/hass_client.rs
+++ b/shalom/src/hass_client.rs
@@ -624,19 +624,19 @@ pub mod responses {
    #[derive(Deserialize, Debug, Clone)]
    pub struct StateCameraAttributes<'a> {
        #[serde(borrow)]
        access_token: Cow<'a, str>,
        pub access_token: Cow<'a, str>,
        #[serde(borrow)]
        friendly_name: Cow<'a, str>,
        pub friendly_name: Cow<'a, str>,
        #[serde(borrow)]
        stream_source: Option<Cow<'a, str>>,
        pub stream_source: Option<Cow<'a, str>>,
        #[serde(borrow)]
        still_image_url: Option<Cow<'a, str>>,
        pub still_image_url: Option<Cow<'a, str>>,
        #[serde(borrow)]
        name: Option<Cow<'a, str>>,
        pub name: Option<Cow<'a, str>>,
        #[serde(borrow)]
        id: Option<Cow<'a, str>>,
        pub id: Option<Cow<'a, str>>,
        #[serde(borrow)]
        entity_picture: Cow<'a, str>,
        pub entity_picture: Cow<'a, str>,
    }

    #[derive(Default, Deserialize, Debug, EnumString, Copy, Clone, FromRepr)]
diff --git a/shalom/src/oracle.rs b/shalom/src/oracle.rs
index a847601..409e7d5 100644
--- a/shalom/src/oracle.rs
+++ b/shalom/src/oracle.rs
@@ -25,8 +25,8 @@ use crate::{
    hass_client::{
        responses::{
            Area, AreaRegistryList, ColorMode, DeviceRegistryList, Entity, EntityRegistryList,
            StateAttributes, StateLightAttributes, StateMediaPlayerAttributes,
            StateWeatherAttributes, StatesList, WeatherCondition,
            StateAttributes, StateCameraAttributes, StateLightAttributes,
            StateMediaPlayerAttributes, StateWeatherAttributes, StatesList, WeatherCondition,
        },
        CallServiceRequestData, CallServiceRequestLight, CallServiceRequestLightTurnOn,
        CallServiceRequestMediaPlayer, CallServiceRequestMediaPlayerMediaSeek,
@@ -45,6 +45,7 @@ pub struct Oracle {
    weather: Atomic<Weather>,
    media_players: Mutex<BTreeMap<&'static str, MediaPlayer>>,
    lights: Mutex<BTreeMap<&'static str, Light>>,
    cameras: Mutex<BTreeMap<&'static str, Camera>>,
    entity_updates: broadcast::Sender<Arc<str>>,
}

@@ -81,6 +82,7 @@ impl Oracle {

        let mut media_players = BTreeMap::new();
        let mut lights = BTreeMap::new();
        let mut cameras = BTreeMap::new();

        for state in &states.0 {
            match &state.attributes {
@@ -96,6 +98,12 @@ impl Oracle {
                        Light::from((attr.clone(), state.state.as_ref())),
                    );
                }
                StateAttributes::Camera(attr) => {
                    cameras.insert(
                        Intern::<str>::from(state.entity_id.as_ref()).as_ref(),
                        Camera::new(attr, &hass_client.base),
                    );
                }
                _ => {}
            }
        }
@@ -109,6 +117,7 @@ impl Oracle {
            media_players: Mutex::new(media_players),
            lights: Mutex::new(lights),
            entity_updates: entity_updates.clone(),
            cameras: Mutex::new(cameras),
        });

        this.clone().spawn_worker();
@@ -135,6 +144,17 @@ impl Oracle {
            .map(|_| ())
    }

    pub fn cameras(&self) -> BTreeMap<&'static str, Camera> {
        (*self.cameras.lock()).clone()
    }

    pub fn subscribe_all_cameras(&self) -> impl Stream<Item = ()> {
        BroadcastStream::new(self.entity_updates.subscribe())
            .filter_map(|v| future::ready(v.ok()))
            .filter(|v| future::ready(v.starts_with("camera.")))
            .map(|_| ())
    }

    pub fn subscribe_id(&self, id: &'static str) -> impl Stream<Item = ()> {
        BroadcastStream::new(self.entity_updates.subscribe())
            .filter_map(|v| future::ready(v.ok()))
@@ -277,6 +297,12 @@ impl Oracle {
                            Light::from((attrs.clone(), state_changed.new_state.state.as_ref())),
                        );
                    }
                    StateAttributes::Camera(attrs) => {
                        self.cameras.lock().insert(
                            Intern::<str>::from(state_changed.entity_id.as_ref()).as_ref(),
                            Camera::new(attrs, &self.client.base),
                        );
                    }
                    _ => {
                        // TODO
                    }
@@ -472,6 +498,21 @@ fn build_room(
    (area, room)
}

#[derive(Clone, Debug)]
pub struct Camera {
    pub name: Box<str>,
    pub entity_picture: Url,
}

impl Camera {
    pub fn new(value: &StateCameraAttributes, base: &Url) -> Self {
        Self {
            name: value.friendly_name.to_string().into_boxed_str(),
            entity_picture: base.join(&value.entity_picture).unwrap(),
        }
    }
}

#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum MediaPlayer {
diff --git a/shalom/src/pages/omni.rs b/shalom/src/pages/omni.rs
index 24e9308..e3fa596 100644
--- a/shalom/src/pages/omni.rs
+++ b/shalom/src/pages/omni.rs
@@ -1,18 +1,20 @@
use std::{any::TypeId, sync::Arc};
use std::{any::TypeId, collections::BTreeMap, sync::Arc};

use iced::{
    advanced::graphics::core::Element,
    font::{Stretch, Weight},
    futures::StreamExt,
    subscription,
    widget::{column, scrollable, text, Column, Row},
    widget::{column, container, image, scrollable, text, vertical_space, Column, Row},
    Font, Renderer, Subscription,
};
use itertools::Itertools;
use time::OffsetDateTime;
use url::Url;

use crate::{
    oracle::{Oracle, Weather},
    subscriptions::download_image,
    theme::Image,
    widgets::image_card,
};
@@ -21,12 +23,24 @@ use crate::{
pub struct Omni {
    oracle: Arc<Oracle>,
    weather: Weather,
    cameras: BTreeMap<&'static str, CameraImage>,
}

#[derive(Debug)]
pub enum CameraImage {
    Unresolved(Url, Option<iced::widget::image::Handle>),
    Resolved(Url, iced::widget::image::Handle),
}

impl Omni {
    pub fn new(oracle: Arc<Oracle>) -> Self {
        Self {
            weather: oracle.current_weather(),
            cameras: oracle
                .cameras()
                .into_iter()
                .map(|(k, v)| (k, CameraImage::Unresolved(v.entity_picture, None)))
                .collect(),
            oracle,
        }
    }
@@ -45,6 +59,35 @@ impl Omni {
                self.weather = self.oracle.current_weather();
                None
            }
            Message::UpdateCameras => {
                self.cameras = self
                    .oracle
                    .cameras()
                    .into_iter()
                    .map(|(k, v)| match self.cameras.remove(k) {
                        Some(CameraImage::Resolved(old_url, old_handle))
                            if old_url != v.entity_picture =>
                        {
                            (
                                k,
                                CameraImage::Unresolved(v.entity_picture, Some(old_handle)),
                            )
                        }
                        Some(CameraImage::Unresolved(old_url, old_handle))
                            if old_url != v.entity_picture =>
                        {
                            (k, CameraImage::Unresolved(v.entity_picture, old_handle))
                        }
                        Some(v) => (k, v),
                        None => (k, CameraImage::Unresolved(v.entity_picture, None)),
                    })
                    .collect();
                None
            }
            Message::CameraImageDownloaded(id, url, handle) => {
                self.cameras.insert(id, CameraImage::Resolved(url, handle));
                None
            }
        }
    }

@@ -68,6 +111,22 @@ impl Omni {
            // .width(Length::FillPortion(1))
        };

        let cameras = self
            .cameras
            .values()
            .map(|v| match v {
                CameraImage::Unresolved(_, Some(handle)) | CameraImage::Resolved(_, handle) => {
                    Element::from(image(handle.clone()).width(512.).height(288.))
                }
                CameraImage::Unresolved(..) => {
                    Element::from(container(vertical_space(0)).width(512.).height(288.))
                }
            })
            .chunks(2)
            .into_iter()
            .map(|children| children.into_iter().fold(Row::new(), Row::push))
            .fold(Column::new(), Column::push);

        let rooms = self
            .oracle
            .rooms()
@@ -82,6 +141,7 @@ impl Omni {
                greeting,
                crate::widgets::cards::weather::WeatherCard::new(self.weather),
                rooms,
                cameras,
            ]
            .spacing(20)
            .padding(40),
@@ -91,13 +151,38 @@ impl Omni {

    pub fn subscription(&self) -> Subscription<Message> {
        pub struct WeatherSubscription;
        pub struct CameraSubscription;

        subscription::run_with_id(
        let weather_subscription = subscription::run_with_id(
            TypeId::of::<WeatherSubscription>(),
            self.oracle
                .subscribe_weather()
                .map(|()| Message::UpdateWeather),
        )
        );

        let camera_subscription = subscription::run_with_id(
            TypeId::of::<CameraSubscription>(),
            self.oracle
                .subscribe_all_cameras()
                .map(|()| Message::UpdateCameras),
        );

        let camera_image_downloads =
            Subscription::batch(self.cameras.iter().filter_map(|(k, v)| {
                if let CameraImage::Unresolved(url, _) = v {
                    Some(download_image(*k, url.clone(), |id, url, handle| {
                        Message::CameraImageDownloaded(id, url, handle)
                    }))
                } else {
                    None
                }
            }));

        Subscription::batch([
            weather_subscription,
            camera_subscription,
            camera_image_downloads,
        ])
    }
}

@@ -123,4 +208,6 @@ pub enum Event {
pub enum Message {
    OpenRoom(&'static str),
    UpdateWeather,
    UpdateCameras,
    CameraImageDownloaded(&'static str, Url, iced::widget::image::Handle),
}
diff --git a/shalom/src/pages/room.rs b/shalom/src/pages/room.rs
index 8d05124..0f3f245 100644
--- a/shalom/src/pages/room.rs
+++ b/shalom/src/pages/room.rs
@@ -209,7 +209,9 @@ impl Room {
                .and_then(|(_, v)| v.entity_picture.as_ref()),
            &self.now_playing_image,
        ) {
            download_image(uri.clone(), uri.clone(), Message::NowPlayingImageLoaded)
            download_image("now-playing", uri.clone(), |_, url, handle| {
                Message::NowPlayingImageLoaded(url, handle)
            })
        } else {
            Subscription::none()
        };
diff --git a/shalom/src/subscriptions.rs b/shalom/src/subscriptions.rs
index c6867de..36aef78 100644
--- a/shalom/src/subscriptions.rs
+++ b/shalom/src/subscriptions.rs
@@ -6,10 +6,10 @@ use once_cell::sync::Lazy;
use parking_lot::Mutex;
use url::Url;

pub fn download_image<I: Hash + 'static, M: 'static>(
pub fn download_image<I: Hash + Copy + Send + 'static, M: 'static>(
    id: I,
    url: Url,
    resp: fn(Url, image::Handle) -> M,
    resp: fn(I, Url, image::Handle) -> M,
) -> Subscription<M> {
    static CACHE: Lazy<Mutex<LruCache<Url, image::Handle>>> =
        Lazy::new(|| Mutex::new(LruCache::new(NonZeroUsize::new(10).unwrap())));
@@ -18,7 +18,7 @@ pub fn download_image<I: Hash + 'static, M: 'static>(
        id,
        stream::once(async move {
            if let Some(handle) = CACHE.lock().get(&url) {
                return (resp)(url, handle.clone());
                return (resp)(id, url, handle.clone());
            }

            let bytes = reqwest::get(url.clone())
@@ -31,7 +31,7 @@ pub fn download_image<I: Hash + 'static, M: 'static>(

            CACHE.lock().push(url.clone(), handle.clone());

            (resp)(url, handle)
            (resp)(id, url, handle)
        }),
    )
}
diff --git a/shalom/src/theme.rs b/shalom/src/theme.rs
index 6004df6..76b475b 100644
--- a/shalom/src/theme.rs
+++ b/shalom/src/theme.rs
@@ -43,6 +43,7 @@ pub enum Icon {
    Play,
    Pause,
    Repeat,
    Repeat1,
    Cloud,
    ClearNight,
    Fog,
@@ -95,6 +96,7 @@ impl Icon {
            Self::ClearDay => image!("clear-day"),
            Self::Wind => image!("wind"),
            Self::Shuffle => image!("shuffle"),
            Self::Repeat1 => image!("repeat-1"),
        }
    }
}
diff --git a/shalom/src/widgets/colour_picker.rs b/shalom/src/widgets/colour_picker.rs
index a9de4b9..39e99d9 100644
--- a/shalom/src/widgets/colour_picker.rs
+++ b/shalom/src/widgets/colour_picker.rs
@@ -67,8 +67,7 @@ impl<Event: Clone> Component<Event, Renderer> for ColourPicker<Event> {
                Message::MouseUp,
            ))
            .height(192)
            .width(192)
            .into(),
            .width(192),
        );

        let hue_slider = forced_rounded(
@@ -78,8 +77,7 @@ impl<Event: Clone> Component<Event, Renderer> for ColourPicker<Event> {
                Message::MouseUp,
            ))
            .height(192)
            .width(32)
            .into(),
            .width(32),
        );

        Row::new()
diff --git a/shalom/src/widgets/forced_rounded.rs b/shalom/src/widgets/forced_rounded.rs
index dfb19c6..7e2790f 100644
--- a/shalom/src/widgets/forced_rounded.rs
+++ b/shalom/src/widgets/forced_rounded.rs
@@ -15,8 +15,12 @@ use iced::{
    Background, Color, Event, Length, Point, Rectangle, Size,
};

pub fn forced_rounded<'a, M: 'a, R>(element: iced::Element<'a, M, R>) -> ForcedRounded<'a, M, R> {
    ForcedRounded { element }
pub fn forced_rounded<'a, M: 'a, R>(
    element: impl Into<iced::Element<'a, M, R>>,
) -> ForcedRounded<'a, M, R> {
    ForcedRounded {
        element: element.into(),
    }
}

pub struct ForcedRounded<'a, M, R> {
diff --git a/shalom/src/widgets/media_player.rs b/shalom/src/widgets/media_player.rs
index ef10954..c8be440 100644
--- a/shalom/src/widgets/media_player.rs
+++ b/shalom/src/widgets/media_player.rs
@@ -5,11 +5,11 @@ use std::{

use iced::{
    advanced::graphics::core::Element,
    theme::{Svg, Text},
    theme::{Slider, Svg, Text},
    widget::{
        column as icolumn, component, container, image::Handle, row, slider, svg, text, Component,
    },
    Alignment, Length, Renderer, Theme,
    Alignment, Color, Length, Renderer, Theme,
};

use crate::{
@@ -188,8 +188,8 @@ impl<M: Clone> Component<M, Renderer> for MediaPlayer<M> {
                        .on_press(Event::ToggleShuffle),
                        mouse_area(
                            svg(Icon::Backward)
                                .height(24)
                                .width(24)
                                .height(28)
                                .width(28)
                                .style(icon_style(false))
                        )
                        .on_press(Event::PreviousTrack),
@@ -199,27 +199,31 @@ impl<M: Clone> Component<M, Renderer> for MediaPlayer<M> {
                            } else {
                                Icon::Play
                            })
                            .height(24)
                            .width(24)
                            .height(42)
                            .width(42)
                            .style(icon_style(false))
                        )
                        .on_press(Event::TogglePlaying),
                        mouse_area(
                            svg(Icon::Forward)
                                .height(24)
                                .width(24)
                                .height(28)
                                .width(28)
                                .style(icon_style(false))
                        )
                        .on_press(Event::NextTrack),
                        mouse_area(
                            svg(Icon::Repeat)
                                .height(24)
                                .width(24)
                                .style(icon_style(self.device.repeat != MediaPlayerRepeat::Off)),
                            svg(match self.device.repeat {
                                MediaPlayerRepeat::Off | MediaPlayerRepeat::All => Icon::Repeat,
                                MediaPlayerRepeat::One => Icon::Repeat1,
                            })
                            .height(28)
                            .width(28)
                            .style(icon_style(self.device.repeat != MediaPlayerRepeat::Off)),
                        )
                        .on_press(Event::ToggleRepeat),
                    ]
                    .spacing(14),
                    .spacing(14)
                    .align_items(Alignment::Center),
                    row![
                        text(format_time(position))
                            .style(Text::Color(SLATE_400))
@@ -229,7 +233,8 @@ impl<M: Clone> Component<M, Renderer> for MediaPlayer<M> {
                            position.as_secs_f64(),
                            Event::PositionChange
                        )
                        .on_release(Event::OnPositionRelease),
                        .on_release(Event::OnPositionRelease)
                        .style(Slider::Custom(Box::new(SliderStyle))),
                        text(format_time(self.device.media_duration.unwrap_or_default()))
                            .style(Text::Color(SLATE_400))
                            .size(12),
@@ -255,7 +260,8 @@ impl<M: Clone> Component<M, Renderer> for MediaPlayer<M> {
                    slider(0.0..=1.0, volume, Event::VolumeChange)
                        .width(128)
                        .step(0.01)
                        .on_release(Event::OnVolumeRelease),
                        .on_release(Event::OnVolumeRelease)
                        .style(Slider::Custom(Box::new(SliderStyle))),
                ]
                .align_items(Alignment::Center)
                .width(Length::FillPortion(4))
@@ -311,6 +317,59 @@ fn format_time(duration: Duration) -> impl Display {
    format!("{minutes:02}:{seconds:02}")
}

struct SliderStyle;

impl slider::StyleSheet for SliderStyle {
    type Style = Theme;

    fn active(&self, style: &Self::Style) -> slider::Appearance {
        let palette = style.extended_palette();

        let handle = slider::Handle {
            shape: slider::HandleShape::Rectangle {
                width: 0,
                border_radius: 0.0.into(),
            },
            color: Color::TRANSPARENT,
            border_color: Color::TRANSPARENT,
            border_width: 0.0,
        };

        slider::Appearance {
            rail: slider::Rail {
                colors: (palette.primary.base.color, palette.secondary.base.color),
                width: 4.0,
                border_radius: 4.0.into(),
            },
            handle,
        }
    }

    fn hovered(&self, style: &Self::Style) -> slider::Appearance {
        let palette = style.extended_palette();

        let handle = slider::Handle {
            shape: slider::HandleShape::Circle { radius: 6.0 },
            color: palette.background.base.color,
            border_color: palette.primary.base.color,
            border_width: 1.0,
        };

        slider::Appearance {
            rail: slider::Rail {
                colors: (palette.primary.base.color, palette.secondary.base.color),
                width: 4.0,
                border_radius: 4.0.into(),
            },
            handle,
        }
    }

    fn dragging(&self, style: &Self::Style) -> slider::Appearance {
        self.hovered(style)
    }
}

#[derive(Copy, Clone)]
pub enum Style {
    Active,