diff --git a/README.md b/README.md
index ad1f1b5..caf0b28 100644
--- a/README.md
+++ b/README.md
@@ -1,72 +1,75 @@
# RustySound
-A lightweight cross-platform music streaming client for Navidrome and Subsonic-compatible servers, built with Rust and Dioxus, < 10mb
+A lightweight cross-platform music streaming client for Navidrome and Subsonic-compatible servers, built with Rust and Dioxus, < 15mb
-
-
-
-
-
-
+
+
+
+
+
+
+
-# Features
+
-- 🎵 **Multi-platform Support**: Available on Desktop (macOS, Windows, Linux), Mobile (iOS, Android), and Web
-- 🎧 **Audio Playback**: High-quality audio streaming with queue management
-- 📱 **Server Integration**: Connect to Navidrome and Subsonic-compatible music servers
-- 💾 **Local Storage**: Persistent settings and playback state across sessions
-- 🎼 **Playlist Management**: Create and manage playlists
-- 🔍 **Search & Browse**: Browse your music library by artists, albums, and tracks
-- 🎚️ **Audio Controls**: Play, pause, skip, shuffle, and repeat functionality
-- 🌙 **Modern UI**: Clean, responsive interface built with Tailwind CSS
-- 🎨 **Themes**: Multiple built-in themes with custom theme/CSS support
+## Features
-# Themes
+- **Multi-platform**: Desktop (macOS, Windows, Linux), Mobile (iOS, Android), and Web
+- **Audio streaming**: High-quality playback with queue management
+- **Server integration**: Connect to Navidrome and Subsonic-compatible servers
+- **Offline support**: Local storage, downloads, persistent settings and playback state
+- **Playlist management**: Create and organize playlists
+- **Browse and search**: Find music by artist, album, or track
+- **Full controls**: Play, pause, skip, shuffle, repeat, and more
+- **Modern UI**: Clean, responsive Tailwind CSS interface
+- **Customizable themes**: Multiple built-in themes with CSS overrides
-RustySound now supports multiple built-in themes and custom themes.
+## Supported Platforms
-- Switch between bundled themes in **Settings**
-- Apply your own custom CSS overrides
+| Platform | Installation |
+| ----------- | ---------------------------------------------------------- |
+| **iOS** | AltStore (recommended), manual sideloading, or source feed |
+| **Android** | APK from Releases |
+| **macOS** | Homebrew, DMG installer |
+| **Windows** | Scoop, standalone EXE |
+| **Linux** | Flatpak |
+| **Web** | Browser + PWA, Docker |
-# Supported Platforms
+> **Note**: Android is feature-aligned but not under active day-to-day development and may have platform-specific issues.
-## Web
+## Installation
-- **Browser**: WebAssembly-based web application
-- **Progressive Web App**: Installable PWA support
+### iOS
-## Desktop
+#### AltStore Installation (Recommended)
-- **macOS**: DMG installer and Homebrew
-- **Windows**: NSIS installer and scoop.
-- **Linux**: Flatpak
+This is the easiest way to install and keep RustySound updated on iOS.
-## Mobile
+
+
+
-- **iOS**: unsigned .ipa
-- **Android**: APK release artifact
+Or manually add `https://ad-archer.github.io/packages/source.json` as a source in:
-> Android status: Android builds are published and kept feature-aligned, but Android is not under active day-to-day development and may contain platform-specific bugs.
+- [AltStore](https://altstore.io/) or [AltServer](https://altstore.io/) for Mac/Windows
+- [LiveContainer](https://github.com/LiveContainer/LiveContainer) on iOS
-# Installation
+#### Manual Sideloading
-## Desktop
-
-### IOS
-
-1. Add `https://ad-archer.github.io/packages/source.json` as a source in [AltStore/AltServer](https://altstore.io/) or [LiveContainer](https://github.com/LiveContainer/LiveContainer)
-2. Install RustySound from that source on your device
-3. Use the latest `.ipa` from [Releases](https://github.com/AD-Archer/RustySound/releases) only if you want to sideload manually instead of using the source feed
+1. Download the latest `.ipa` file from [Releases](https://github.com/AD-Archer/RustySound/releases)
+2. Use [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or [Xcode](https://developer.apple.com/xcode/) to install
### Android
1. Download the latest `.apk` file from [Releases](https://github.com/AD-Archer/RustySound/releases)
-2. Enable installation from unknown sources on your device
+2. Enable "Installation from unknown sources" in Settings
3. Install the APK and launch RustySound
-### macOS
+### Desktop
-#### Homebrew
+#### macOS
+
+##### Homebrew (Recommended)
```bash
brew tap ad-archer/homebrew-tap
@@ -79,42 +82,39 @@ To update:
brew upgrade rustysound
```
-#### DMG
+##### DMG Installer
1. Download the latest `.dmg` file from [Releases](https://github.com/AD-Archer/RustySound/releases)
2. Open the DMG and drag RustySound to your Applications folder
-If macOS says the app is damaged or won't open:
-
-1. In Finder, right-click `RustySound.app` and choose **Open**, then confirm.
-2. Or run:
+**Troubleshooting**: If macOS says the app is damaged or won't open:
```bash
+# Option 1: Right-click in Finder and choose Open, then confirm
+# Option 2: Run this command
xattr -dr com.apple.quarantine /Applications/RustySound.app
open /Applications/RustySound.app
```
-### Windows
+#### Windows
-#### Scoop
+##### Scoop (Recommended)
```powershell
scoop bucket add ad-archer https://github.com/ad-archer/scoop
scoop install ad-archer/rustysound
```
-#### Executable
+##### Portable Executable
1. Download the latest `.exe` file from [Releases](https://github.com/AD-Archer/RustySound/releases)
2. Open the exe and install RustySound
-Note: Antivirus may flag this installer since this exe is not verified by windows.
+> **Note**: Antivirus may flag the installer since it is not verified by Windows. This is safe to ignore for builds from the official repository.
-Note: release artifacts may be unsigned/ad-hoc signed when Apple notarization secrets are not configured in CI. For public distribution without warnings, a paid Apple Developer signing + notarization flow is required.
+#### Linux
-### Linux
-
-#### Flatpak (Recommended)
+##### Flatpak (Recommended)
```bash
flatpak remote-add --if-not-exists --user adarcher-rustysound https://ad-archer.github.io/packages/rustysound.flatpakrepo
@@ -122,7 +122,7 @@ flatpak install --user adarcher-rustysound app.adarcher.rustysound//stable
flatpak run app.adarcher.rustysound
```
-If you installed an older build that tracked `master`, migrate once with:
+To update:
```bash
flatpak install --user adarcher-rustysound app.adarcher.rustysound//stable
@@ -135,7 +135,7 @@ flatpak uninstall --user app.adarcher.rustysound
flatpak remote-delete --user adarcher-rustysound
```
-#### Troubleshooting — missing GNOME runtime
+**Troubleshooting — missing GNOME runtime**
If you see an error like:
@@ -143,34 +143,32 @@ If you see an error like:
error: The application app.adarcher.rustysound/x86_64/master requires the runtime org.gnome.Platform/x86_64/49 which was not found
```
-this usually means the remote you added doesn't provide the GNOME runtime. Install the runtime from Flathub and try again:
+Install the runtime from Flathub:
```bash
# Add Flathub (if not already present)
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
-# Install the GNOME 49 runtime (user or system-wide)
+# Install the GNOME 49 runtime
flatpak install --user flathub org.gnome.Platform//49
-# Optional: locale data
-flatpak install --user flathub org.gnome.Platform.Locale//49
-# Then reinstall the app from the adarcher remote
+# Reinstall the app
flatpak install --user adarcher-rustysound app.adarcher.rustysound//stable
```
-If you prefer a system-wide install (no `--user`), omit `--user` from the commands. Also ensure the runtime architecture matches your system (x86_64 vs aarch64).
+For system-wide install, omit `--user` from the commands.
### Web
-Visit [rustysound](https://rustysound-demo.adarcher.app) to use the web version.
+Visit [rustysound](https://rustysound-demo.adarcher.app) to try the web version.
#### Docker Deployment
-You can also run RustySound as a Docker container:
+##### Docker Compose (Recommended)
1. Ensure you have Docker and Docker Compose installed
2. Clone this repository or copy [`docker-compose.yml`](https://raw.githubusercontent.com/AD-Archer/RustySound/refs/heads/main/docker-compose.yml)
-3. Run the application:
+3. Run:
```bash
docker-compose up -d
@@ -178,20 +176,20 @@ docker-compose up -d
The web interface will be available at `http://localhost:8080`.
-To stop the container:
+To stop:
```bash
docker-compose down
```
-#### Manual Docker Run
-
-If you prefer to run the container directly:
+##### Manual Docker Run
```bash
docker run -d -p 8080:80 --name rustysound ghcr.io/ad-archer/rustysound:latest
```
+## Development
+
### Prerequisites
- Rust 1.70+ ([install here](https://rustup.rs/))
@@ -212,119 +210,113 @@ cd RustySound
cargo build
```
-### Running the Application
+### Running Locally
-#### Development Server
+#### Web
```bash
-dx serve
+dx serve --platform web
```
-#### Just Shortcuts
+#### Desktop
```bash
-just # list recipes
-just serve # dx serve
-just serve-ios # iOS simulator dev (safe linker env)
-just serve-android # Android dev/debug (auto-create/start emulator)
-just bundle # macOS + iOS + unsigned IPA
-just bundle-android-release # Android release APK into dist/android
-just check # cargo check
+dx serve --platform desktop
```
-#### iOS Simulator Development
+#### Mobile
-Use the helper below instead of raw `dx serve --ios` if your shell exports Homebrew/Nix compiler flags:
+##### iOS Simulator
```bash
./scripts/serve-ios.sh
```
-You can pass normal `dx serve` options through:
+You can pass `dx serve` options:
```bash
./scripts/serve-ios.sh --device "iPhone 16 Pro"
```
-#### Specific Platforms
+##### Android Emulator
```bash
-# Web (default)
-dx serve --platform web
-
-# Desktop
-dx serve --platform desktop
-
-# Mobile (iOS Simulator)
-dx serve --platform ios
-
-# Mobile (Android Emulator)
dx serve --platform android
+# or with auto-setup (NixOS):
+just serve-android
```
-For NixOS convenience (auto create/start emulator + boot wait):
+### Quick Commands
+
+Use `just` for common tasks:
```bash
-just serve-android
-# alias:
-just serve-andoird
+just # list all recipes
+just serve # dx serve (web)
+just serve-ios # iOS simulator dev (safe linker env)
+just serve-android # Android dev/debug (auto-create/start emulator)
+just check # cargo check
+just bundle # macOS + iOS + unsigned IPA
+just bundle-android-release # Android release APK
```
-### Building for Production
+### Building for Release
-#### Desktop Bundles
+#### Desktop
```bash
dx bundle --platform desktop --release
```
-#### Mobile Builds
+#### iOS
```bash
-# iOS
dx bundle --platform ios --release
-
-# Android
-./scripts/bundle-android.sh
-# or
-just bundle-android-release
```
-`scripts/bundle-android.sh` only exports Android release `.apk` artifacts into `dist/android`.
-
-Optional signing env vars for `scripts/bundle-android.sh`:
-
-- `ANDROID_KEYSTORE_BASE64` (or `ANDROID_KEYSTORE_PATH`)
-- `ANDROID_KEYSTORE_PASSWORD`
-- `ANDROID_KEY_ALIAS`
-- `ANDROID_KEY_PASSWORD` (optional)
-
-CI/CD builds Android release APKs and publishes only `.apk` artifacts from `dist/android`.
-
-#### Apple Bundles (.app + unsigned .ipa)
+Or use the Apple bundling script:
```bash
./scripts/bundle-apple.sh
```
-- macOS `.app` output: `dist/apple/macos`
-- iOS `.app` output: `dist/apple/ios`
+Outputs:
+
+- macOS `.app`: `dist/apple/macos`
+- iOS `.app`: `dist/apple/ios`
- Unsigned iOS `.ipa`: `dist/apple/ios/*-unsigned.ipa`
-By default, the script builds for physical iOS devices (`aarch64-apple-ios`). To build for the simulator instead:
+To build for simulator:
```bash
IOS_TARGET=aarch64-apple-ios-sim ./scripts/bundle-apple.sh
```
-If your shell exports Homebrew C/C++ flags (for example `LDFLAGS`/`LIBRARY_PATH` for `libiconv`), prefer this script over raw `dx bundle --ios` so those vars are unset for iOS linking.
+If your shell exports compiler flags (e.g., `LDFLAGS`), use the script to avoid linking issues.
-You can also override icon source/name if needed:
+Customize the icon and app name:
```bash
APP_NAME="RustySound" IOS_ICON_SOURCE="/absolute/path/to/icon-1024.png" ./scripts/bundle-apple.sh
```
+#### Android
+
+```bash
+./scripts/bundle-android.sh
+# or
+just bundle-android-release
+```
+
+This exports release `.apk` to `dist/android`.
+
+Optional signing environment variables:
+
+- `ANDROID_KEYSTORE_BASE64` or `ANDROID_KEYSTORE_PATH`
+- `ANDROID_KEYSTORE_PASSWORD`
+- `ANDROID_KEY_ALIAS`
+- `ANDROID_KEY_PASSWORD` (optional)
+
## Project Structure
```
@@ -350,20 +342,6 @@ rustysound/
└── tailwind.css # Tailwind CSS styles
```
-## Configuration
-
-### Server Connection
-
-1. Launch RustySound
-2. Go to Settings
-
-### Supported Servers
-
-- **[Navidrome](https://www.navidrome.org/)**: Full feature support
-- **Subsonic**: Compatible with Subsonic API v1.16.1+
-- **Airsonic**: Compatible servers
-- **Gonic**: Compatible servers
-
## Contributing
1. Fork the repository
diff --git a/assets/tailwind.css b/assets/tailwind.css
index c1b9a46..f9476cf 100644
--- a/assets/tailwind.css
+++ b/assets/tailwind.css
@@ -563,6 +563,9 @@
.mb-12 {
margin-bottom: calc(var(--spacing) * 12);
}
+ .-ml-1 {
+ margin-left: calc(var(--spacing) * -1);
+ }
.ml-0\.5 {
margin-left: calc(var(--spacing) * 0.5);
}
@@ -872,6 +875,9 @@
.max-w-sm {
max-width: var(--container-sm);
}
+ .max-w-xl {
+ max-width: var(--container-xl);
+ }
.max-w-xs {
max-width: var(--container-xs);
}
@@ -973,6 +979,9 @@
.grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
+ .grid-cols-6 {
+ grid-template-columns: repeat(6, minmax(0, 1fr));
+ }
.grid-cols-12 {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
@@ -1616,6 +1625,9 @@
background-color: color-mix(in oklab, var(--color-emerald-500) 95%, transparent);
}
}
+ .bg-emerald-600 {
+ background-color: var(--color-emerald-600);
+ }
.bg-emerald-900\/20 {
background-color: color-mix(in srgb, oklch(37.8% 0.077 168.94) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -4059,6 +4071,11 @@
padding: calc(var(--spacing) * 8);
}
}
+ .md\:px-4 {
+ @media (width >= 48rem) {
+ padding-inline: calc(var(--spacing) * 4);
+ }
+ }
.md\:px-6 {
@media (width >= 48rem) {
padding-inline: calc(var(--spacing) * 6);
diff --git a/src/api/navidrome/playlist_mutations.rs b/src/api/navidrome/playlist_mutations.rs
index 445377c..7ff0001 100644
--- a/src/api/navidrome/playlist_mutations.rs
+++ b/src/api/navidrome/playlist_mutations.rs
@@ -1,5 +1,41 @@
// Playlist write/update operations.
impl NavidromeClient {
+ pub async fn rename_playlist(&self, playlist_id: &str, name: &str) -> Result<(), String> {
+ let trimmed = name.trim();
+ if trimmed.is_empty() {
+ return Err("Playlist name cannot be empty.".to_string());
+ }
+
+ let url = self.build_url_owned(
+ "updatePlaylist",
+ vec![
+ ("playlistId".to_string(), playlist_id.to_string()),
+ ("name".to_string(), trimmed.to_string()),
+ ],
+ );
+ let response = HTTP_CLIENT
+ .get(&url)
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+ let json: SubsonicResponse = response.json().await.map_err(|e| e.to_string())?;
+
+ if json.subsonic_response.status != "ok" {
+ return Err(json
+ .subsonic_response
+ .error
+ .map(|e| e.message)
+ .unwrap_or("Unknown error".to_string()));
+ }
+
+ let _ = cache_remove_prefix(&format!(
+ "api:getPlaylist:v1:{}:{}",
+ self.server.id, playlist_id
+ ));
+ self.invalidate_playlist_cache();
+ Ok(())
+ }
+
pub async fn create_playlist(
&self,
name: &str,
diff --git a/src/components/app.rs b/src/components/app.rs
index 311c4e0..f207b83 100644
--- a/src/components/app.rs
+++ b/src/components/app.rs
@@ -5,19 +5,20 @@ use crate::cache_service::{
};
use crate::components::views::home_layout::HomeFeedLoadProfile;
use crate::components::{
- ios_audio_log_snapshot, ios_diag_log, view_label, AddIntent, AddMenuController,
- AddToMenuOverlay, AppView, AudioController, AudioState, HomeRefreshSignal, Icon,
- IsPlayingSignal, Navigation, PlaybackPositionSignal, Player, PreviewPlaybackSignal,
+ ios_audio_log_snapshot, ios_diag_log, view_instance_key, view_label, AddIntent,
+ AddMenuController, AddToMenuOverlay, AppView, AudioController, AudioState, HomeRefreshSignal,
+ Icon, IsPlayingSignal, Navigation, PlaybackPositionSignal, Player, PreviewPlaybackSignal,
SeekRequestSignal, ShuffleEnabledSignal, Sidebar, SidebarOpenSignal, SongDetailsController,
SongDetailsOverlay, SongDetailsState, VolumeSignal,
};
use crate::db::{
initialize_database, load_playback_state, load_servers, load_settings, save_playback_state,
- save_servers, save_settings, AppSettings, PlaybackState, QueueItem,
+ save_servers, save_settings, save_temporary_queue_snapshot, AppSettings, PlaybackState,
+ QueueItem, TemporaryQueueSnapshot,
};
use crate::diagnostics::{log_perf, PerfTimer};
use crate::offline_audio::{prune_temporary_queue_prefetch_downloads, run_auto_download_pass};
-use chrono::{DateTime, NaiveDateTime};
+use chrono::{DateTime, NaiveDateTime, Utc};
#[cfg(target_arch = "wasm32")]
use dioxus::core::{Runtime, RuntimeGuard};
#[cfg(all(feature = "desktop", target_os = "macos"))]
@@ -32,6 +33,7 @@ use web_sys::window;
// Re-export RepeatMode for other components
pub use crate::db::RepeatMode;
use dioxus::prelude::*;
+use dioxus_router::use_navigator;
#[cfg(target_arch = "wasm32")]
const HISTORY_SWIPE_THRESHOLD: f64 = 100.0;
@@ -112,6 +114,31 @@ fn home_init_profile_cache_key(profile: HomeFeedLoadProfile) -> &'static str {
profile.as_storage()
}
+fn queue_snapshot_signature(
+ queue: &[Song],
+ queue_index: usize,
+ now_playing: Option<&Song>,
+) -> String {
+ let mut signature = String::new();
+ signature.push_str(&queue.len().to_string());
+ signature.push('|');
+ signature.push_str(&queue_index.to_string());
+ signature.push('|');
+ if let Some(song) = now_playing {
+ signature.push_str(song.server_id.trim());
+ signature.push(':');
+ signature.push_str(song.id.trim());
+ }
+ signature.push('|');
+ for song in queue {
+ signature.push_str(song.server_id.trim());
+ signature.push(':');
+ signature.push_str(song.id.trim());
+ signature.push(';');
+ }
+ signature
+}
+
#[cfg(all(feature = "desktop", target_os = "macos"))]
fn focus_global_search_input() {
let _ = document::eval(
@@ -881,6 +908,10 @@ async fn initialize_home_cache(
pub fn AppShell() -> Element {
let mut servers = use_signal(Vec::::new);
let current_view = use_route::();
+ let router_navigator = use_navigator();
+ let mut current_view_signal = use_signal(|| current_view.clone());
+ let pending_navigation_target = use_signal(|| None::);
+ let outlet_key = view_instance_key(¤t_view);
let now_playing = use_signal(|| None::);
let queue = use_signal(Vec::::new);
let mut queue_index = use_signal(|| 0usize);
@@ -889,6 +920,7 @@ pub fn AppShell() -> Element {
let mut app_settings = use_signal(AppSettings::default);
let mut playback_position = use_signal(|| 0.0f64);
let mut last_playback_save = use_signal(|| None::<(String, String, u64, usize, usize)>);
+ let mut last_queue_snapshot_signature = use_signal(String::new);
let mut db_initialized = use_signal(|| false);
let mut servers_loaded = use_signal(|| false);
let mut settings_loaded = use_signal(|| false);
@@ -910,7 +942,38 @@ pub fn AppShell() -> Element {
let audio_state = use_signal(AudioState::default);
let preview_playback = use_signal(|| false);
let sidebar_open = use_signal(|| false);
- let navigation = Navigation::new();
+ use_effect({
+ let current_view = current_view.clone();
+ move || {
+ if current_view_signal() != current_view {
+ current_view_signal.set(current_view.clone());
+ }
+ }
+ });
+
+ let navigation = Navigation::new(
+ router_navigator,
+ current_view_signal,
+ pending_navigation_target,
+ );
+ use_effect({
+ let navigation = navigation;
+ move || {
+ let current_view = current_view_signal();
+ let pending_target = pending_navigation_target();
+ if pending_target.is_some() && matches!(current_view, AppView::HomeView {}) {
+ eprintln!(
+ "[nav.refresh.effect] current={} pending={}",
+ current_view,
+ pending_target
+ .as_ref()
+ .map(ToString::to_string)
+ .unwrap_or_else(|| "".to_string())
+ );
+ navigation.resume_pending_navigation();
+ }
+ }
+ });
let seek_request = use_signal(|| None::<(String, f64)>);
let mut resume_bookmark_loaded = use_signal(|| false);
#[cfg(target_arch = "wasm32")]
@@ -1893,9 +1956,49 @@ pub fn AppShell() -> Element {
}
});
+ // Save temporary queue snapshots so queue can be restored after tab/app close.
+ use_effect(move || {
+ if !db_initialized() || !settings_loaded() {
+ return;
+ }
+ if preview_playback() {
+ return;
+ }
+
+ let queue_snapshot = queue();
+ if queue_snapshot.is_empty() {
+ return;
+ }
+
+ let clamped_queue_index = queue_index().min(queue_snapshot.len().saturating_sub(1));
+ let now_playing_snapshot = now_playing();
+ let signature = queue_snapshot_signature(
+ &queue_snapshot,
+ clamped_queue_index,
+ now_playing_snapshot.as_ref(),
+ );
+ if signature == last_queue_snapshot_signature() {
+ return;
+ }
+ last_queue_snapshot_signature.set(signature);
+
+ let saved_at_epoch_ms = Utc::now().timestamp_millis();
+ let snapshot = TemporaryQueueSnapshot {
+ id: format!("queue-{saved_at_epoch_ms}"),
+ saved_at_epoch_ms,
+ queue: queue_snapshot,
+ queue_index: clamped_queue_index,
+ now_playing: now_playing_snapshot,
+ playback_position: playback_position().max(0.0),
+ };
+
+ spawn(async move {
+ let _ = save_temporary_queue_snapshot(snapshot).await;
+ });
+ });
+
let view = use_route::();
let sidebar_signal = sidebar_open.clone();
- let can_go_back = navigation.can_go_back();
let song_details_open = song_details_state().is_open;
let is_startup_bootstrapping = !db_initialized() || !settings_loaded();
let is_home_initializing = home_init_in_progress() && matches!(&view, AppView::HomeView {});
@@ -1963,22 +2066,6 @@ pub fn AppShell() -> Element {
class: "w-5 h-5".to_string(),
}
}
- if can_go_back {
- button {
- class: "p-2 rounded-lg text-zinc-300 hover:text-white hover:bg-zinc-800/60 transition-colors",
- aria_label: "Go back",
- onclick: {
- let navigation = navigation.clone();
- move |_| {
- let _ = navigation.go_back();
- }
- },
- Icon {
- name: "arrow-left".to_string(),
- class: "w-5 h-5".to_string(),
- }
- }
- }
}
div { class: "flex flex-col items-center text-center",
span { class: "text-xs uppercase tracking-widest text-zinc-500",
@@ -2034,7 +2121,10 @@ pub fn AppShell() -> Element {
}
}
}
- Outlet:: {}
+ div {
+ key: "{outlet_key}",
+ Outlet:: {}
+ }
}
}
Player {}
diff --git a/src/components/app_view.rs b/src/components/app_view.rs
index 9c5718c..4e27699 100644
--- a/src/components/app_view.rs
+++ b/src/components/app_view.rs
@@ -74,3 +74,35 @@ pub fn view_label(view: &AppView) -> &'static str {
AppView::PlaylistDetailView { .. } => "Playlist",
}
}
+
+pub fn view_instance_key(view: &AppView) -> String {
+ match view {
+ AppView::HomeView {} => "home".to_string(),
+ AppView::SearchView {} => "search".to_string(),
+ AppView::SongsView {} => "songs".to_string(),
+ AppView::Albums {} => "albums".to_string(),
+ AppView::AlbumsWithGenre { genre } => format!("albums:{genre}"),
+ AppView::ArtistsView {} => "artists".to_string(),
+ AppView::PlaylistsView {} => "playlists".to_string(),
+ AppView::RadioView {} => "radio".to_string(),
+ AppView::BookmarksView {} => "bookmarks".to_string(),
+ AppView::FavoritesView {} => "favorites".to_string(),
+ AppView::DownloadsView {} => "downloads".to_string(),
+ AppView::RandomView {} => "random".to_string(),
+ AppView::SettingsView {} => "settings".to_string(),
+ AppView::StatsView {} => "stats".to_string(),
+ AppView::QueueView {} => "queue".to_string(),
+ AppView::AlbumDetailView {
+ album_id,
+ server_id,
+ } => format!("album:{server_id}:{album_id}"),
+ AppView::ArtistDetailView {
+ artist_id,
+ server_id,
+ } => format!("artist:{server_id}:{artist_id}"),
+ AppView::PlaylistDetailView {
+ playlist_id,
+ server_id,
+ } => format!("playlist:{server_id}:{playlist_id}"),
+ }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index ae47566..f5ec64b 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -41,7 +41,7 @@ pub struct ShuffleEnabledSignal(pub Signal);
pub use add_to_menu::*;
pub use app::*;
-pub use app_view::{view_label, AppView};
+pub use app_view::{view_instance_key, view_label, AppView};
pub use audio_manager::*;
pub use icons::*;
pub use navigation::Navigation;
diff --git a/src/components/navigation.rs b/src/components/navigation.rs
index 2009411..e726686 100644
--- a/src/components/navigation.rs
+++ b/src/components/navigation.rs
@@ -1,4 +1,5 @@
use dioxus::prelude::*;
+use dioxus_router::Navigator;
use crate::components::app_view::AppView;
@@ -7,8 +8,58 @@ use wasm_bindgen::JsCast;
#[cfg(target_arch = "wasm32")]
use web_sys::{window, HtmlElement};
-#[derive(Clone, Copy, Default)]
-pub struct Navigation;
+#[derive(Clone, Copy)]
+pub struct Navigation(
+ pub Navigator,
+ pub Signal,
+ pub Signal