From c78dcda55e299fce9d7eb37be0836a097013f51d Mon Sep 17 00:00:00 2001 From: spinline Date: Wed, 11 Feb 2026 23:54:36 +0300 Subject: [PATCH] feat: integrate ThemeToggle component and fix module visibility errors --- frontend/src/app.rs | 17 +++ frontend/src/components/hooks/mod.rs | 1 + .../src/components/hooks/use_theme_mode.rs | 108 ++++++++++++++++++ frontend/src/components/layout/sidebar.rs | 48 +------- frontend/src/components/mod.rs | 1 + frontend/src/components/ui/mod.rs | 2 + frontend/src/components/ui/svg_icon.rs | 25 ++++ frontend/src/components/ui/theme_toggle.rs | 76 ++++++++++++ 8 files changed, 233 insertions(+), 45 deletions(-) create mode 100644 frontend/src/components/hooks/use_theme_mode.rs create mode 100644 frontend/src/components/ui/svg_icon.rs create mode 100644 frontend/src/components/ui/theme_toggle.rs diff --git a/frontend/src/app.rs b/frontend/src/app.rs index 14e6908..272a09d 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -7,10 +7,27 @@ use leptos::task::spawn_local; use leptos_router::components::{Router, Routes, Route}; use leptos_router::hooks::use_navigate; use crate::components::ui::toast::Toaster; +use crate::components::hooks::use_theme_mode::ThemeMode; #[component] pub fn App() -> impl IntoView { crate::components::ui::toast::provide_toaster(); + let theme_mode = ThemeMode::init(); + + // Sync theme with document + Effect::new(move |_| { + let is_dark = theme_mode.get(); + if let Some(doc) = document().document_element() { + if is_dark { + let _ = doc.class_list().add_1("dark"); + let _ = doc.set_attribute("data-theme", "dark"); + } else { + let _ = doc.class_list().remove_1("dark"); + let _ = doc.set_attribute("data-theme", "light"); + } + } + }); + view! { diff --git a/frontend/src/components/hooks/mod.rs b/frontend/src/components/hooks/mod.rs index 6def963..8ed876e 100644 --- a/frontend/src/components/hooks/mod.rs +++ b/frontend/src/components/hooks/mod.rs @@ -1 +1,2 @@ pub mod use_random; +pub mod use_theme_mode; diff --git a/frontend/src/components/hooks/use_theme_mode.rs b/frontend/src/components/hooks/use_theme_mode.rs new file mode 100644 index 0000000..fd44f23 --- /dev/null +++ b/frontend/src/components/hooks/use_theme_mode.rs @@ -0,0 +1,108 @@ +use leptos::prelude::*; +use web_sys::Storage; + +#[derive(Debug, Clone, Copy)] +pub struct ThemeMode { + state: RwSignal, +} + +const LOCALSTORAGE_KEY: &str = "darkmode"; + +/// Hook to access the dark mode context +/// +/// Returns the ThemeMode instance from context for easy access +pub fn use_theme_mode() -> ThemeMode { + expect_context::() +} + +/* ========================================================== */ +/* ✨ FUNCTIONS ✨ */ +/* ========================================================== */ + +impl ThemeMode { + #[must_use] + /// Initializes a new ThemeMode instance. + pub fn init() -> Self { + let theme_mode = Self { state: RwSignal::new(false) }; + + provide_context(theme_mode); + + // Use Effect to handle browser-only initialization + Effect::new(move |_| { + let initial = Self::get_storage_state().unwrap_or(Self::prefers_dark_mode()); + theme_mode.state.set(initial); + }); + + theme_mode + } + + pub fn toggle(&self) { + self.state.update(|state| { + *state = !*state; + Self::set_storage_state(*state); + }); + } + + pub fn set_dark(&self) { + self.set(true); + } + + pub fn set_light(&self) { + self.set(false); + } + + /// - `dark`: Set to `true` for dark mode, and `false` for light mode. + pub fn set(&self, dark: bool) { + self.state.set(dark); + Self::set_storage_state(dark); + } + + #[must_use] + pub fn get(&self) -> bool { + self.state.get() + } + + #[must_use] + pub fn is_dark(&self) -> bool { + self.state.get() + } + + #[must_use] + pub fn is_light(&self) -> bool { + !self.state.get() + } + + /* ========================================================== */ + /* ✨ FUNCTIONS ✨ */ + /* ========================================================== */ + + /// Retrieves the local storage object, if available. + fn get_storage() -> Option { + window().local_storage().ok().flatten() + } + + /// Retrieves the dark mode state from local storage, if available. + fn get_storage_state() -> Option { + Self::get_storage() + .and_then(|storage| storage.get(LOCALSTORAGE_KEY).ok()) + .flatten() + .and_then(|entry| entry.parse::().ok()) + } + + /// Checks whether the user's system prefers dark mode based on media queries. + fn prefers_dark_mode() -> bool { + window() + .match_media("(prefers-color-scheme: dark)") + .ok() + .flatten() + .map(|media| media.matches()) + .unwrap_or_default() + } + + /// Stores the dark mode state in local storage. + fn set_storage_state(state: bool) { + if let Some(storage) = Self::get_storage() { + storage.set(LOCALSTORAGE_KEY, state.to_string().as_str()).ok(); + } + } +} \ No newline at end of file diff --git a/frontend/src/components/layout/sidebar.rs b/frontend/src/components/layout/sidebar.rs index 5a5fe4b..a4ee0dc 100644 --- a/frontend/src/components/layout/sidebar.rs +++ b/frontend/src/components/layout/sidebar.rs @@ -1,9 +1,6 @@ use leptos::prelude::*; use leptos::task::spawn_local; -use leptos_use::storage::use_local_storage; -use ::codee::string::FromToStringCodec; - #[component] pub fn Sidebar() -> impl IntoView { let store = use_context::().expect("store not provided"); @@ -67,34 +64,6 @@ pub fn Sidebar() -> impl IntoView { username().chars().next().unwrap_or('?').to_uppercase().to_string() }; - // --- THEME LOGIC START --- - let (current_theme, set_current_theme, _) = use_local_storage::("vibetorrent_theme"); - - // Initialize with default if empty - let current_theme_val = current_theme.get(); - if current_theme_val.is_empty() { - set_current_theme.set("dark".to_string()); - } - - // Automatically sync theme to document attribute - Effect::new(move |_| { - let theme = current_theme.get().to_lowercase(); - if let Some(doc) = document().document_element() { - let _ = doc.set_attribute("data-theme", &theme); - if theme == "dark" || theme == "dracula" || theme == "dim" || theme == "abyss" || theme == "sunset" || theme == "cyberpunk" || theme == "nord" || theme == "business" || theme == "night" || theme == "black" || theme == "luxury" || theme == "coffee" || theme == "forest" || theme == "halloween" || theme == "synthwave" { - let _ = doc.class_list().add_1("dark"); - } else { - let _ = doc.class_list().remove_1("dark"); - } - } - }); - - let toggle_theme = move |_| { - let new_theme = if current_theme.get() == "dark" { "light" } else { "dark" }; - set_current_theme.set(new_theme.to_string()); - }; - // --- THEME LOGIC END --- - view! {
@@ -164,20 +133,9 @@ pub fn Sidebar() -> impl IntoView {
// Theme toggle button - +
+ +
// Logout button + } +} \ No newline at end of file