Tauri 2.0 Project Music Player Media Playback App

Building a music player application with Tauri 2.0 demonstrates audio playback, playlist management, and modern UI design creating professional media player—complete project combining HTML5 Audio API integration with seamless playback, playlist management with drag-and-drop reordering, dynamic color extraction from album artwork, equalizer controls with audio filters, volume management with mute functionality, progress bar with seek capability, keyboard shortcuts for media control, and responsive design with glassmorphism effects delivering comprehensive music player. This music player uses native HTML5 Audio providing cross-platform audio support, React hooks managing player state and playlist logic, Canvas API extracting dominant colors from artwork, Web Audio API implementing equalizer filters, Tauri dialog API for file selection, and localStorage persisting user preferences maintaining seamless experience. This complete guide covers project setup and audio dependencies, building Rust backend with file system commands, implementing audio player hook with playback controls, creating playlist management with local storage persistence, adding color extraction for dynamic theming, building equalizer component with Web Audio filters, designing modern UI with glassmorphism and animations, implementing keyboard shortcuts for productivity, and deployment packaging maintaining production-ready music player through proper implementation. Before proceeding, understand file system access, dialog API, and basic app creation.
Project Architecture Overview
Understanding the music player architecture helps organize audio management and UI components. The app uses HTML5 Audio for playback with custom hooks managing state and Web Audio API for equalizer effects.
music-player/
├── src/ # Frontend React application
│ ├── components/ # UI Components
│ │ ├── Controls.tsx # Play/pause, next/prev controls
│ │ ├── ProgressBar.tsx # Seek bar with time display
│ │ ├── VolumeControl.tsx # Volume slider and mute
│ │ ├── Playlist.tsx # Song list with drag-drop
│ │ ├── NowPlaying.tsx # Current song display
│ │ └── Equalizer.tsx # EQ controls with presets
│ ├── hooks/ # Custom React hooks
│ │ ├── useAudioPlayer.ts # Audio playback logic
│ │ ├── usePlaylist.ts # Playlist management
│ │ └── useColorExtractor.ts # Album art color extraction
│ ├── types/ # TypeScript interfaces
│ │ └── index.ts # Song and playlist types
│ ├── utils/ # Utility functions
│ │ └── utils.ts # Helper functions
│ ├── App.tsx # Main application
│ ├── App.css # Styling with animations
│ └── main.tsx # React entry point
├── src-tauri/ # Rust backend
│ ├── src/
│ │ ├── commands.rs # File system commands
│ │ ├── main.rs # Tauri entry point
│ │ └── lib.rs # Library file
│ ├── capabilities/ # Tauri permissions
│ │ └── default.json # File and dialog permissions
│ ├── Cargo.toml # Rust dependencies
│ └── tauri.conf.json # Tauri configuration
├── package.json # Node dependencies
├── tsconfig.json # TypeScript config
└── vite.config.ts # Vite bundler configStep 1: Project Setup and Dependencies
Create a new Tauri project with React and install necessary dependencies for audio playback and UI components. This establishes the foundation for the music player.
# Create new Tauri project
npm create tauri-app@latest
# During setup:
# Project name: music-player
# Package manager: npm
# UI template: React
# TypeScript: Yes
cd music-player
# Install frontend dependencies
npm install lucide-react
# Lucide-react: Icon library for controls
# No additional audio libraries needed - using HTML5 Audio and Web Audio API
# Install Tauri plugins
npm install @tauri-apps/plugin-dialog @tauri-apps/plugin-fsStep 2: Rust Backend Configuration
Configure Rust dependencies and implement file system commands for opening audio files. The backend handles file selection dialogs and file path operations.
[package]
name = "tauri-appsystem-monitor"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
[lib]
name = "tauri_appsystem_monitor_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sysinfo = "0.38"
tokio = { version = "1", features = ["full"] }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_System_Threading", "Win32_System_SystemInformation"] }
use lofty::file::AudioFile;
use lofty::prelude::*;
use serde::{Deserialize, Serialize};
use std::path::Path;
use tauri_plugin_dialog::DialogExt;
#[derive(Debug, Serialize, Deserialize)]
pub struct AudioMetadata {
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
pub duration: Option<f64>,
pub cover_art: Option<String>,
}
#[tauri::command]
pub async fn get_audio_metadata(path: String) -> Result<AudioMetadata, String> {
let file_path = Path::new(&path);
// Read the audio file using lofty
let tagged_file = lofty::read_from_path(file_path)
.map_err(|e| format!("Failed to read audio file: {}", e))?;
// Get duration from properties
let duration = tagged_file.properties().duration().as_secs_f64();
// Try to get tags
let tag = tagged_file.primary_tag().or_else(|| tagged_file.first_tag());
let (title, artist, album, cover_art) = if let Some(tag) = tag {
let title = tag.title().map(|s| s.to_string());
let artist = tag.artist().map(|s| s.to_string());
let album = tag.album().map(|s| s.to_string());
// Extract cover art if available
let cover_art = tag.pictures().first().map(|pic| {
let base64_data = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
pic.data(),
);
let mime_type = match pic.mime_type() {
Some(lofty::picture::MimeType::Jpeg) => "image/jpeg",
Some(lofty::picture::MimeType::Png) => "image/png",
Some(lofty::picture::MimeType::Gif) => "image/gif",
Some(lofty::picture::MimeType::Bmp) => "image/bmp",
_ => "image/jpeg",
};
format!("data:{};base64,{}", mime_type, base64_data)
});
(title, artist, album, cover_art)
} else {
(None, None, None, None)
};
Ok(AudioMetadata {
title,
artist,
album,
duration: Some(duration),
cover_art,
})
}
#[tauri::command]
pub async fn open_audio_files(app: tauri::AppHandle) -> Result<Vec<String>, String> {
let files = app
.dialog()
.file()
.add_filter("Audio Files", &["mp3", "wav", "ogg", "flac", "m4a", "aac", "wma"])
.set_title("Select Audio Files")
.blocking_pick_files();
match files {
Some(file_paths) => {
let paths: Vec<String> = file_paths
.iter()
.filter_map(|f| f.as_path().map(|p| p.to_string_lossy().to_string()))
.collect();
Ok(paths)
}
None => Err("No files selected".to_string()),
}
}
mod monitor;
use monitor::SystemMonitor;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let monitor = SystemMonitor::new();
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.manage(monitor)
.invoke_handler(tauri::generate_handler![
monitor::get_system_info,
monitor::get_cpu_per_core,
monitor::get_disk_info,
monitor::get_network_info,
monitor::get_processes,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
tauri_appsystem_monitor_lib::run()
}
Step 3: TypeScript Type Definitions
Define TypeScript interfaces for song data, playlist state, and player controls. Type safety ensures reliable audio management.
export interface SystemInfo {
cpu_usage: number;
cpu_count: number;
memory_total: number;
memory_used: number;
memory_available: number;
swap_total: number;
swap_used: number;
uptime: number;
}
export interface DiskInfo {
name: string;
mount_point: string;
total_space: number;
available_space: number;
usage_percent: number;
}
export interface NetworkInfo {
bytes_received: number;
bytes_sent: number;
packets_received: number;
packets_sent: number;
}
export interface ProcessInfo {
pid: number;
name: string;
cpu_usage: number;
memory: number;
status: string;
}
Step 4: Audio Player Custom Hook
Create a custom React hook managing audio playback with HTML5 Audio API. This hook handles play, pause, seek, volume, and track navigation.
import { useState, useRef, useEffect, useCallback } from 'react';
import { convertFileSrc } from '@tauri-apps/api/core';
import type { Track, PlayerState } from '../types';
export function useAudioPlayer(onTrackEnd?: () => void) {
const audioRef = useRef<HTMLAudioElement>(new Audio());
const [playerState, setPlayerState] = useState<PlayerState>({
currentTrack: null,
isPlaying: false,
currentTime: 0,
duration: 0,
volume: 0.8,
isMuted: false,
isShuffle: false,
repeatMode: 'off',
});
// Initialize audio element
useEffect(() => {
const audio = audioRef.current;
// Set initial volume
audio.volume = playerState.volume;
// Event handlers
const handleTimeUpdate = () => {
setPlayerState((prev) => ({
...prev,
currentTime: audio.currentTime,
}));
};
const handleDurationChange = () => {
setPlayerState((prev) => ({
...prev,
duration: audio.duration || 0,
}));
};
const handleEnded = () => {
setPlayerState((prev) => ({
...prev,
isPlaying: false,
}));
onTrackEnd?.();
};
const handlePlay = () => {
setPlayerState((prev) => ({ ...prev, isPlaying: true }));
};
const handlePause = () => {
setPlayerState((prev) => ({ ...prev, isPlaying: false }));
};
const handleError = (e: Event) => {
console.error('Audio playback error:', e);
setPlayerState((prev) => ({ ...prev, isPlaying: false }));
};
const handleLoadedMetadata = () => {
setPlayerState((prev) => ({
...prev,
duration: audio.duration || 0,
}));
};
audio.addEventListener('timeupdate', handleTimeUpdate);
audio.addEventListener('durationchange', handleDurationChange);
audio.addEventListener('ended', handleEnded);
audio.addEventListener('play', handlePlay);
audio.addEventListener('pause', handlePause);
audio.addEventListener('error', handleError);
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
return () => {
audio.removeEventListener('timeupdate', handleTimeUpdate);
audio.removeEventListener('durationchange', handleDurationChange);
audio.removeEventListener('ended', handleEnded);
audio.removeEventListener('play', handlePlay);
audio.removeEventListener('pause', handlePause);
audio.removeEventListener('error', handleError);
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
};
}, [onTrackEnd]);
// Load track
const loadTrack = useCallback((track: Track, autoPlay: boolean = true) => {
const audio = audioRef.current;
// Convert file path to asset protocol URL for Tauri using the official API
const assetUrl = convertFileSrc(track.path);
console.log('Loading audio from:', assetUrl);
audio.src = assetUrl;
audio.load();
setPlayerState((prev) => ({
...prev,
currentTrack: track,
currentTime: 0,
duration: 0,
}));
if (autoPlay) {
audio.play().catch((error) => {
console.error('Playback failed:', error);
});
}
}, []);
// Play/Pause
const togglePlay = useCallback(() => {
const audio = audioRef.current;
if (playerState.isPlaying) {
audio.pause();
} else {
audio.play().catch((error) => {
console.error('Playback failed:', error);
});
}
}, [playerState.isPlaying]);
// Play
const play = useCallback(() => {
const audio = audioRef.current;
audio.play().catch((error) => {
console.error('Playback failed:', error);
});
}, []);
// Pause
const pause = useCallback(() => {
audioRef.current.pause();
}, []);
// Seek
const seek = useCallback((time: number) => {
const audio = audioRef.current;
if (audio.duration) {
audio.currentTime = Math.min(Math.max(0, time), audio.duration);
}
}, []);
// Volume
const setVolume = useCallback((volume: number) => {
const audio = audioRef.current;
const clampedVolume = Math.min(Math.max(0, volume), 1);
audio.volume = clampedVolume;
setPlayerState((prev) => ({ ...prev, volume: clampedVolume, isMuted: false }));
}, []);
// Mute toggle
const toggleMute = useCallback(() => {
const audio = audioRef.current;
setPlayerState((prev) => {
const newMuted = !prev.isMuted;
audio.muted = newMuted;
return { ...prev, isMuted: newMuted };
});
}, []);
// Toggle shuffle
const toggleShuffle = useCallback(() => {
setPlayerState((prev) => ({
...prev,
isShuffle: !prev.isShuffle,
}));
}, []);
// Cycle repeat mode
const cycleRepeatMode = useCallback(() => {
setPlayerState((prev) => {
const modes: Array<'off' | 'one' | 'all'> = ['off', 'one', 'all'];
const currentIndex = modes.indexOf(prev.repeatMode);
const nextMode = modes[(currentIndex + 1) % modes.length];
return { ...prev, repeatMode: nextMode };
});
}, []);
// Get audio element for visualizer
const getAudioElement = useCallback(() => {
return audioRef.current;
}, []);
return {
playerState,
loadTrack,
togglePlay,
play,
pause,
seek,
setVolume,
toggleMute,
toggleShuffle,
cycleRepeatMode,
getAudioElement,
};
}
Step 5: Playlist Management Hook
Implement playlist management with localStorage persistence, shuffle, repeat modes, and drag-and-drop reordering. Users can manage their music library efficiently.
import { useState, useEffect, useCallback } from 'react';
import { invoke } from '@tauri-apps/api/core';
import type { Track, AudioMetadata } from '../types';
const STORAGE_KEYS = {
PLAYLIST: 'music-player-playlist',
FAVORITES: 'music-player-favorites',
RECENTLY_PLAYED: 'music-player-recently-played',
};
const MAX_RECENTLY_PLAYED = 50;
export function usePlaylist() {
const [tracks, setTracks] = useState<Track[]>([]);
const [currentIndex, setCurrentIndex] = useState<number>(-1);
const [shuffledIndices, setShuffledIndices] = useState<number[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [favorites, setFavorites] = useState<Set<string>>(new Set());
const [recentlyPlayed, setRecentlyPlayed] = useState<Track[]>([]);
// Load saved data on mount
useEffect(() => {
const savedPlaylist = localStorage.getItem(STORAGE_KEYS.PLAYLIST);
const savedFavorites = localStorage.getItem(STORAGE_KEYS.FAVORITES);
const savedRecentlyPlayed = localStorage.getItem(STORAGE_KEYS.RECENTLY_PLAYED);
if (savedPlaylist) {
try {
const parsed = JSON.parse(savedPlaylist);
setTracks(parsed.tracks || []);
setCurrentIndex(parsed.currentIndex ?? -1);
} catch (e) {
console.error('Failed to parse saved playlist:', e);
}
}
if (savedFavorites) {
try {
setFavorites(new Set(JSON.parse(savedFavorites)));
} catch (e) {
console.error('Failed to parse favorites:', e);
}
}
if (savedRecentlyPlayed) {
try {
setRecentlyPlayed(JSON.parse(savedRecentlyPlayed));
} catch (e) {
console.error('Failed to parse recently played:', e);
}
}
}, []);
// Save playlist whenever it changes
useEffect(() => {
if (tracks.length > 0 || currentIndex !== -1) {
localStorage.setItem(STORAGE_KEYS.PLAYLIST, JSON.stringify({
tracks,
currentIndex,
}));
}
}, [tracks, currentIndex]);
// Save favorites whenever they change
useEffect(() => {
localStorage.setItem(STORAGE_KEYS.FAVORITES, JSON.stringify([...favorites]));
}, [favorites]);
// Save recently played whenever it changes
useEffect(() => {
localStorage.setItem(STORAGE_KEYS.RECENTLY_PLAYED, JSON.stringify(recentlyPlayed));
}, [recentlyPlayed]);
// Add to recently played
const addToRecentlyPlayed = useCallback((track: Track) => {
setRecentlyPlayed((prev) => {
// Remove if already exists
const filtered = prev.filter((t) => t.id !== track.id);
// Add to front
const updated = [{ ...track, playedAt: Date.now() }, ...filtered];
// Limit to max
return updated.slice(0, MAX_RECENTLY_PLAYED);
});
}, []);
// Generate UUID
const generateId = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};
// Get filename from path
const getFilename = (path: string) => {
const parts = path.split(/[/\\]/);
const filename = parts[parts.length - 1];
return filename.replace(/\.[^/.]+$/, '');
};
// Add tracks
const addTracks = useCallback(async () => {
setIsLoading(true);
try {
const files = await invoke<string[]>('open_audio_files');
if (!files || files.length === 0) {
setIsLoading(false);
return;
}
const newTracks: Track[] = await Promise.all(
files.map(async (path) => {
try {
const metadata = await invoke<AudioMetadata>('get_audio_metadata', { path });
return {
id: generateId(),
path,
title: metadata.title || getFilename(path),
artist: metadata.artist || 'Unknown Artist',
album: metadata.album || 'Unknown Album',
duration: metadata.duration || 0,
coverArt: metadata.cover_art || undefined,
};
} catch (error) {
console.error('Failed to get metadata for:', path, error);
return {
id: generateId(),
path,
title: getFilename(path),
artist: 'Unknown Artist',
album: 'Unknown Album',
duration: 0,
};
}
})
);
setTracks((prev) => [...prev, ...newTracks]);
setCurrentIndex((prev) => (prev === -1 ? 0 : prev));
} catch (error) {
console.error('Failed to add tracks:', error);
} finally {
setIsLoading(false);
}
}, []);
// Remove track
const removeTrack = useCallback((trackId: string) => {
setTracks((prev) => {
const index = prev.findIndex((t) => t.id === trackId);
const newTracks = prev.filter((t) => t.id !== trackId);
if (index !== -1 && index <= currentIndex) {
setCurrentIndex((prevIndex) => Math.max(0, prevIndex - 1));
}
return newTracks;
});
}, [currentIndex]);
// Clear playlist
const clearPlaylist = useCallback(() => {
setTracks([]);
setCurrentIndex(-1);
setShuffledIndices([]);
localStorage.removeItem(STORAGE_KEYS.PLAYLIST);
}, []);
// Toggle favorite
const toggleFavorite = useCallback((trackId: string) => {
setFavorites((prev) => {
const newFavorites = new Set(prev);
if (newFavorites.has(trackId)) {
newFavorites.delete(trackId);
} else {
newFavorites.add(trackId);
}
return newFavorites;
});
}, []);
// Check if track is favorite
const isFavorite = useCallback((trackId: string) => {
return favorites.has(trackId);
}, [favorites]);
// Get favorite tracks
const getFavoriteTracks = useCallback(() => {
return tracks.filter((t) => favorites.has(t.id));
}, [tracks, favorites]);
// Generate shuffle indices
const generateShuffleOrder = useCallback((trackCount: number, startIndex: number) => {
const indices = Array.from({ length: trackCount }, (_, i) => i);
for (let i = indices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[indices[i], indices[j]] = [indices[j], indices[i]];
}
const currentPos = indices.indexOf(startIndex);
if (currentPos > 0) {
indices.splice(currentPos, 1);
indices.unshift(startIndex);
}
setShuffledIndices(indices);
}, []);
// Get next track index
const getNextIndex = useCallback(
(isShuffle: boolean, repeatMode: 'off' | 'one' | 'all') => {
if (tracks.length === 0) return -1;
if (repeatMode === 'one') {
return currentIndex;
}
if (isShuffle && shuffledIndices.length > 0) {
const currentShuffledPos = shuffledIndices.indexOf(currentIndex);
const nextPos = currentShuffledPos + 1;
if (nextPos >= shuffledIndices.length) {
if (repeatMode === 'all') {
generateShuffleOrder(tracks.length, 0);
return shuffledIndices[0];
}
return -1;
}
return shuffledIndices[nextPos];
}
const nextIndex = currentIndex + 1;
if (nextIndex >= tracks.length) {
return repeatMode === 'all' ? 0 : -1;
}
return nextIndex;
},
[currentIndex, shuffledIndices, tracks.length, generateShuffleOrder]
);
// Get previous track index
const getPreviousIndex = useCallback(
(isShuffle: boolean) => {
if (tracks.length === 0) return -1;
if (isShuffle && shuffledIndices.length > 0) {
const currentShuffledPos = shuffledIndices.indexOf(currentIndex);
const prevPos = currentShuffledPos - 1;
if (prevPos < 0) {
return shuffledIndices[shuffledIndices.length - 1];
}
return shuffledIndices[prevPos];
}
return currentIndex === 0 ? tracks.length - 1 : currentIndex - 1;
},
[currentIndex, shuffledIndices, tracks.length]
);
// Next track
const next = useCallback(
(isShuffle: boolean, repeatMode: 'off' | 'one' | 'all') => {
const nextIndex = getNextIndex(isShuffle, repeatMode);
if (nextIndex !== -1) {
setCurrentIndex(nextIndex);
return tracks[nextIndex];
}
return null;
},
[tracks, getNextIndex]
);
// Previous track
const previous = useCallback(
(isShuffle: boolean) => {
const prevIndex = getPreviousIndex(isShuffle);
if (prevIndex !== -1) {
setCurrentIndex(prevIndex);
return tracks[prevIndex];
}
return null;
},
[tracks, getPreviousIndex]
);
// Play specific track
const playTrack = useCallback(
(index: number) => {
if (index >= 0 && index < tracks.length) {
setCurrentIndex(index);
addToRecentlyPlayed(tracks[index]);
return tracks[index];
}
return null;
},
[tracks, addToRecentlyPlayed]
);
// Initialize shuffle when enabled
const initializeShuffle = useCallback(() => {
if (tracks.length > 0) {
generateShuffleOrder(tracks.length, currentIndex >= 0 ? currentIndex : 0);
}
}, [tracks.length, currentIndex, generateShuffleOrder]);
return {
tracks,
currentIndex,
currentTrack: currentIndex >= 0 && currentIndex < tracks.length ? tracks[currentIndex] : null,
isLoading,
favorites,
recentlyPlayed,
addTracks,
removeTrack,
clearPlaylist,
next,
previous,
playTrack,
initializeShuffle,
toggleFavorite,
isFavorite,
getFavoriteTracks,
addToRecentlyPlayed,
};
}
Step 6: Dynamic Color Extraction
Extract dominant colors from album artwork using Canvas API for dynamic UI theming. The background adapts to currently playing track creating immersive experience.
import { useState, useEffect, useRef } from 'react';
interface ColorResult {
dominant: string;
palette: string[];
isDark: boolean;
}
const DEFAULT_COLORS: ColorResult = {
dominant: 'rgba(250, 45, 72, 0.3)',
palette: [],
isDark: true,
};
export function useColorExtractor(imageUrl: string | undefined): ColorResult {
const [colors, setColors] = useState<ColorResult>(DEFAULT_COLORS);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (!imageUrl) {
setColors(DEFAULT_COLORS);
return;
}
// Create canvas only once
if (!canvasRef.current) {
canvasRef.current = document.createElement('canvas');
}
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (!ctx) {
console.error('Failed to get canvas context');
return;
}
const img = new Image();
// For base64 data URLs, we don't need crossOrigin
if (!imageUrl.startsWith('data:')) {
img.crossOrigin = 'anonymous';
}
img.onload = () => {
try {
// Use small size for performance
const size = 100;
canvas.width = size;
canvas.height = size;
ctx.drawImage(img, 0, 0, size, size);
let imageData: ImageData;
try {
imageData = ctx.getImageData(0, 0, size, size);
} catch (securityError) {
console.warn('Canvas security error, using fallback color');
// Generate a color from the URL hash as fallback
const hash = imageUrl.split('').reduce((a, b) => {
a = ((a << 5) - a) + b.charCodeAt(0);
return a & a;
}, 0);
const hue = Math.abs(hash) % 360;
setColors({
dominant: `hsla(${hue}, 70%, 50%, 0.3)`,
palette: [],
isDark: true,
});
return;
}
const data = imageData.data;
const colorCounts: Map<string, number> = new Map();
// Sample pixels and count colors
for (let i = 0; i < data.length; i += 20) { // Sample every 5th pixel
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
if (a < 128) continue; // Skip transparent pixels
// Skip very dark or very light colors
const brightness = (r + g + b) / 3;
if (brightness < 30 || brightness > 220) continue;
// Calculate saturation to prefer colorful pixels
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const saturation = max === 0 ? 0 : (max - min) / max;
// Skip low saturation (grayish) colors
if (saturation < 0.15) continue;
// Quantize to reduce color variations
const qr = Math.round(r / 24) * 24;
const qg = Math.round(g / 24) * 24;
const qb = Math.round(b / 24) * 24;
const key = `${qr},${qg},${qb}`;
const weight = saturation * 2; // Weight by saturation
colorCounts.set(key, (colorCounts.get(key) || 0) + weight);
}
// Find most frequent color
let maxCount = 0;
let dominantColor = { r: 250, g: 45, b: 72 };
colorCounts.forEach((count, key) => {
if (count > maxCount) {
maxCount = count;
const [r, g, b] = key.split(',').map(Number);
dominantColor = { r, g, b };
}
});
// Calculate if color is dark
const luminance = (0.299 * dominantColor.r + 0.587 * dominantColor.g + 0.114 * dominantColor.b) / 255;
const isDark = luminance < 0.5;
// Boost saturation and adjust lightness for more vibrant gradient
const hsl = rgbToHsl(dominantColor.r, dominantColor.g, dominantColor.b);
hsl.s = Math.min(1, hsl.s * 1.3); // Increase saturation more
hsl.l = Math.max(0.25, Math.min(0.55, hsl.l)); // Clamp lightness
const boosted = hslToRgb(hsl.h, hsl.s, hsl.l);
console.log('Extracted dominant color:', boosted);
setColors({
dominant: `rgba(${boosted.r}, ${boosted.g}, ${boosted.b}, 0.5)`,
palette: Array.from(colorCounts.keys()).slice(0, 5).map(key => {
const [r, g, b] = key.split(',').map(Number);
return `rgb(${r}, ${g}, ${b})`;
}),
isDark,
});
} catch (error) {
console.error('Color extraction failed:', error);
setColors(DEFAULT_COLORS);
}
};
img.onerror = (error) => {
console.error('Image load error:', error);
setColors(DEFAULT_COLORS);
};
img.src = imageUrl;
}, [imageUrl]);
return colors;
}
// Helper functions for color conversion
function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
break;
case g:
h = ((b - r) / d + 2) / 6;
break;
case b:
h = ((r - g) / d + 4) / 6;
break;
}
}
return { h, s, l };
}
function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
};
}
Step 7: Playback Controls Component
Build the playback controls with play/pause, previous/next track, shuffle, and repeat buttons. Intuitive controls provide familiar music player experience.
import React from 'react';
import {
Play,
Pause,
SkipForward,
SkipBack,
Shuffle,
Repeat,
Repeat1,
} from 'lucide-react';
import { cn } from '../lib/utils';
import type { PlayerState } from '../types';
interface ControlsProps {
playerState: PlayerState;
onPlay: () => void;
onNext: () => void;
onPrevious: () => void;
onShuffle: () => void;
onRepeat: () => void;
disabled?: boolean;
}
const Controls: React.FC<ControlsProps> = ({
playerState,
onPlay,
onNext,
onPrevious,
onShuffle,
onRepeat,
disabled = false,
}) => {
const getRepeatIcon = () => {
if (playerState.repeatMode === 'one') {
return <Repeat1 className="h-5 w-5" />;
}
return <Repeat className="h-5 w-5" />;
};
return (
<div className="flex items-center justify-center gap-4">
{/* Shuffle */}
<button
className={cn(
'control-btn',
playerState.isShuffle && 'active text-emerald-400'
)}
onClick={onShuffle}
disabled={disabled}
title="Shuffle"
>
<Shuffle className="h-5 w-5" />
</button>
{/* Previous */}
<button
className="control-btn"
onClick={onPrevious}
disabled={disabled}
title="Previous"
>
<SkipBack className="h-6 w-6" />
</button>
{/* Play/Pause */}
<button
className={cn(
'play-btn',
playerState.isPlaying && 'playing'
)}
onClick={onPlay}
disabled={disabled}
title={playerState.isPlaying ? 'Pause' : 'Play'}
>
{playerState.isPlaying ? (
<Pause className="h-7 w-7" />
) : (
<Play className="h-7 w-7 ml-1" />
)}
</button>
{/* Next */}
<button
className="control-btn"
onClick={onNext}
disabled={disabled}
title="Next"
>
<SkipForward className="h-6 w-6" />
</button>
{/* Repeat */}
<button
className={cn(
'control-btn',
playerState.repeatMode !== 'off' && 'active text-emerald-400'
)}
onClick={onRepeat}
disabled={disabled}
title={`Repeat: ${playerState.repeatMode}`}
>
{getRepeatIcon()}
</button>
</div>
);
};
export default Controls;
Step 8: Progress Bar with Seek
Create an interactive progress bar showing playback position with seek capability. Users can click or drag to jump to any position in the track.
import React, { useRef, useCallback } from 'react';
interface ProgressBarProps {
currentTime: number;
duration: number;
onSeek: (time: number) => void;
disabled?: boolean;
}
const ProgressBar: React.FC<ProgressBarProps> = ({
currentTime,
duration,
onSeek,
disabled = false,
}) => {
const progressRef = useRef<HTMLDivElement>(null);
const formatTime = (seconds: number) => {
if (!seconds || isNaN(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const handleSeek = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!progressRef.current || !duration || disabled) return;
const rect = progressRef.current.getBoundingClientRect();
const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
const newTime = percent * duration;
onSeek(newTime);
},
[duration, onSeek, disabled]
);
const handleDrag = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (e.buttons !== 1) return; // Only left mouse button
handleSeek(e);
},
[handleSeek]
);
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
return (
<div className="progress-container">
<span className="time-label">{formatTime(currentTime)}</span>
<div
ref={progressRef}
className="progress-bar"
onClick={handleSeek}
onMouseMove={handleDrag}
role="slider"
aria-valuemin={0}
aria-valuemax={duration}
aria-valuenow={currentTime}
tabIndex={0}
>
<div className="progress-track">
<div
className="progress-fill"
style={{ width: `${progress}%` }}
>
<div className="progress-handle" />
</div>
</div>
</div>
<span className="time-label">{formatTime(duration)}</span>
</div>
);
};
export default ProgressBar;
Step 9: Volume Control Component
Implement volume slider with mute functionality and visual feedback. Volume settings persist across sessions using localStorage.
import React, { useCallback, useRef } from 'react';
import { Volume2, Volume1, VolumeX } from 'lucide-react';
import { cn } from '../lib/utils';
interface VolumeControlProps {
volume: number;
isMuted: boolean;
onVolumeChange: (volume: number) => void;
onMuteToggle: () => void;
}
const VolumeControl: React.FC<VolumeControlProps> = ({
volume,
isMuted,
onVolumeChange,
onMuteToggle,
}) => {
const sliderRef = useRef<HTMLDivElement>(null);
const getVolumeIcon = () => {
if (isMuted || volume === 0) {
return <VolumeX className="h-5 w-5" />;
}
if (volume < 0.5) {
return <Volume1 className="h-5 w-5" />;
}
return <Volume2 className="h-5 w-5" />;
};
const handleVolumeChange = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!sliderRef.current) return;
const rect = sliderRef.current.getBoundingClientRect();
const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
onVolumeChange(percent);
},
[onVolumeChange]
);
const handleDrag = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (e.buttons !== 1) return;
handleVolumeChange(e);
},
[handleVolumeChange]
);
const displayVolume = isMuted ? 0 : volume;
return (
<div className="volume-control">
<button
className={cn('control-btn', isMuted && 'text-slate-500')}
onClick={onMuteToggle}
title={isMuted ? 'Unmute' : 'Mute'}
>
{getVolumeIcon()}
</button>
<div
ref={sliderRef}
className="volume-slider"
onClick={handleVolumeChange}
onMouseMove={handleDrag}
role="slider"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(displayVolume * 100)}
tabIndex={0}
>
<div className="volume-track">
<div
className="volume-fill"
style={{ width: `${displayVolume * 100}%` }}
>
<div className="volume-handle" />
</div>
</div>
</div>
</div>
);
};
export default VolumeControl;
Step 10: Now Playing Display
Display currently playing track with album artwork, song title, and artist information. The component shows rich metadata for better user experience.
import React from 'react';
import { Music, Disc3 } from 'lucide-react';
import type { Track } from '../types';
import Equalizer from './Equalizer';
interface NowPlayingProps {
track: Track | null;
isPlaying: boolean;
}
const NowPlaying: React.FC<NowPlayingProps> = ({ track, isPlaying }) => {
if (!track) {
return (
<div className="now-playing-empty">
<div className="album-art-placeholder">
<Music className="h-16 w-16 text-slate-600" />
</div>
<div className="now-playing-info">
<h2 className="text-xl font-bold text-slate-400">No Track Selected</h2>
<p className="text-slate-500">Add some music to get started</p>
</div>
</div>
);
}
return (
<div className="now-playing">
{/* Album Art */}
<div className="album-art-container">
{track.coverArt ? (
<img
src={track.coverArt}
alt={track.album}
className="album-art"
/>
) : (
<div className="album-art-placeholder">
<Disc3 className={`h-20 w-20 text-slate-500 ${isPlaying ? 'animate-spin-slow' : ''}`} />
</div>
)}
{/* Equalizer overlay when playing */}
{isPlaying && (
<div className="album-art-equalizer">
<Equalizer isPlaying={isPlaying} barCount={5} />
</div>
)}
</div>
{/* Track Info */}
<div className="now-playing-info">
<h2 className="now-playing-title">{track.title}</h2>
<p className="now-playing-artist">{track.artist}</p>
<p className="now-playing-album">{track.album}</p>
</div>
</div>
);
};
export default NowPlaying;
Step 11: Playlist Component
Build the playlist view with drag-and-drop reordering, song deletion, and track selection. Users can visualize and manage their queue.
import React from 'react';
import { Music, X, Heart } from 'lucide-react';
import { cn } from '../lib/utils';
import type { Track } from '../types';
interface PlaylistProps {
tracks: Track[];
currentIndex: number;
onPlayTrack: (index: number) => void;
onRemoveTrack: (trackId: string) => void;
onToggleFavorite?: (trackId: string) => void;
isFavorite?: (trackId: string) => boolean;
showFavorites?: boolean;
}
const Playlist: React.FC<PlaylistProps> = ({
tracks,
currentIndex,
onPlayTrack,
onRemoveTrack,
onToggleFavorite,
isFavorite,
showFavorites = true,
}) => {
const formatDuration = (seconds: number) => {
if (!seconds || isNaN(seconds)) return '--:--';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
if (tracks.length === 0) {
return (
<div className="playlist-empty">
<Music className="h-12 w-12 text-white opacity-20 mb-3" />
<p className="text-white/50 text-sm">No tracks in playlist</p>
<p className="text-white/30 text-xs mt-1">Click "Add Music" to get started</p>
</div>
);
}
return (
<div className="playlist custom-scrollbar">
{tracks.map((track, index) => (
<div
key={track.id}
className={cn(
'playlist-item',
index === currentIndex && 'active'
)}
onClick={() => onPlayTrack(index)}
>
{/* Track Number */}
<div className="playlist-item-index">
{index === currentIndex ? (
<div className="playlist-item-playing">
<div className="eq-bar" />
<div className="eq-bar" />
<div className="eq-bar" />
</div>
) : (
index + 1
)}
</div>
{/* Track thumbnail */}
<div className="playlist-item-thumb">
{track.coverArt ? (
<img
src={track.coverArt}
alt={track.album}
/>
) : (
<Music className="h-5 w-5 text-white opacity-30" />
)}
</div>
{/* Track info */}
<div className="playlist-item-info">
<span className="playlist-item-title">{track.title}</span>
<span className="playlist-item-artist">{track.artist}</span>
</div>
{/* Album */}
<div className="playlist-item-album">{track.album}</div>
{/* Duration */}
<div className="playlist-item-duration">
{formatDuration(track.duration)}
</div>
{/* Actions */}
<div className="playlist-item-actions">
{showFavorites && onToggleFavorite && isFavorite && (
<button
className={cn(
'playlist-item-favorite',
isFavorite(track.id) && 'active'
)}
onClick={(e) => {
e.stopPropagation();
onToggleFavorite(track.id);
}}
title={isFavorite(track.id) ? 'Remove from favorites' : 'Add to favorites'}
>
<Heart
className="h-4 w-4"
fill={isFavorite(track.id) ? 'currentColor' : 'none'}
/>
</button>
)}
<button
className="playlist-item-remove"
onClick={(e) => {
e.stopPropagation();
onRemoveTrack(track.id);
}}
title="Remove from playlist"
>
<X className="h-4 w-4" />
</button>
</div>
</div>
))}
</div>
);
};
export default Playlist;
Step 12: Equalizer Component
Implement an equalizer with Web Audio API filters. Users can adjust frequency bands and apply presets for customized audio experience.
import React, { useEffect, useState, useRef } from 'react';
import { cn } from '../lib/utils';
interface EqualizerProps {
isPlaying: boolean;
barCount?: number;
className?: string;
}
const Equalizer: React.FC<EqualizerProps> = ({
isPlaying,
barCount = 8,
className,
}) => {
const [heights, setHeights] = useState<number[]>(Array(barCount).fill(20));
const animationRef = useRef<number>();
const lastUpdateRef = useRef<number>(0);
useEffect(() => {
if (!isPlaying) {
// Reset to minimal heights when not playing
setHeights(Array(barCount).fill(15));
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
return;
}
const animate = (timestamp: number) => {
// Throttle updates to ~30fps for performance
if (timestamp - lastUpdateRef.current < 33) {
animationRef.current = requestAnimationFrame(animate);
return;
}
lastUpdateRef.current = timestamp;
setHeights((prev) =>
prev.map((_, i) => {
// Create more dynamic, music-like movement
const baseHeight = 20;
const maxVariation = 70;
// Use multiple sine waves for more organic movement
const wave1 = Math.sin(timestamp / 200 + i * 0.8) * 30;
const wave2 = Math.sin(timestamp / 300 + i * 1.2) * 20;
const wave3 = Math.sin(timestamp / 150 + i * 0.5) * 15;
const random = (Math.random() - 0.5) * 20;
const height = baseHeight + wave1 + wave2 + wave3 + random;
return Math.max(15, Math.min(baseHeight + maxVariation, height));
})
);
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isPlaying, barCount]);
return (
<div className={cn('equalizer', className)}>
{heights.map((height, index) => (
<div
key={index}
className="equalizer-bar"
style={{
height: `${height}%`,
animationDelay: `${index * 0.05}s`,
}}
/>
))}
</div>
);
};
export default Equalizer;
Step 13: Main Application Component
Assemble all components into the main App with keyboard shortcuts, file selection, and responsive layout. This orchestrates the complete music player interface.
import CPUMonitor from './components/CPUMonitor';
import MemoryMonitor from './components/MemoryMonitor';
import DiskMonitor from './components/DiskMonitor';
import ProcessList from './components/ProcessList';
import { DataProvider } from './contexts/DataContext';
import './App.css';
import { LayoutDashboard, Zap } from 'lucide-react';
import codersHandbookLogo from './assets/coders-handbook-logo.png';
function App() {
return (
<DataProvider>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-100 text-slate-800 font-sans selection:bg-blue-100">
{/* Header */}
<header className="fixed top-0 inset-x-0 z-50 glass-card border-b border-slate-200/60 h-16">
<div className="container mx-auto px-6 h-full flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-blue-500/25">
<LayoutDashboard className="h-5 w-5 text-white" />
</div>
<div>
<h1 className="text-lg font-bold tracking-tight text-slate-900">Coders Handbook System Monitor</h1>
<p className="text-xs text-slate-500 font-medium">Real-time Performance Dashboard</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-50 border border-emerald-200 rounded-full">
<span className="relative flex h-2.5 w-2.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
</span>
<span className="text-xs font-semibold text-emerald-700">Live</span>
</div>
<div className="hidden sm:flex items-center gap-1.5 px-3 py-1.5 bg-slate-100 rounded-full border border-slate-200">
<Zap className="h-3.5 w-3.5 text-amber-500" />
<span className="text-xs font-medium text-slate-600">Tauri v2.0</span>
</div>
</div>
</div>
</header>
{/* Main Content */}
<main className="container mx-auto px-6 pt-24 pb-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* CPU Monitor Card */}
<div className="glass-card rounded-2xl border border-slate-200/60 p-6 shadow-soft-lg hover:shadow-xl transition-shadow duration-300">
<CPUMonitor />
</div>
{/* Memory Monitor Card */}
<div className="glass-card rounded-2xl border border-slate-200/60 p-6 shadow-soft-lg hover:shadow-xl transition-shadow duration-300">
<MemoryMonitor />
</div>
{/* Disk Monitor Card */}
<div className="glass-card rounded-2xl border border-slate-200/60 p-6 shadow-soft-lg hover:shadow-xl transition-shadow duration-300">
<DiskMonitor />
</div>
{/* Process List - Full Width */}
<div className="col-span-1 md:col-span-2 lg:col-span-3 glass-card rounded-2xl border border-slate-200/60 p-6 shadow-soft-lg">
<ProcessList />
</div>
</div>
{/* Footer */}
<footer className="mt-8 text-center">
<p className="text-xs text-slate-400">
Built with <span className="text-red-400">♥</span> using Tauri + React
</p>
</footer>
</main>
{/* Floating Branding */}
<a
href="https://codershandbook.com/page/tauri-2-0-tutorial-series-complete-guide"
target="_blank"
rel="noopener noreferrer"
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 group"
>
<div className="flex items-center gap-3 px-5 py-2.5 bg-white/90 backdrop-blur-md border border-slate-200 rounded-full shadow-lg shadow-slate-900/10 hover:shadow-xl hover:bg-white hover:border-slate-300 transition-all duration-300 hover:scale-105">
<img
src={codersHandbookLogo}
alt="Coders Handbook"
className="h-7 w-7 rounded-lg shadow-sm"
/>
<div className="flex flex-col">
<span className="text-sm font-bold text-slate-800 group-hover:text-blue-600 transition-colors">
{"{Coders Handbook}"}
</span>
<span className="text-[10px] text-slate-500 group-hover:text-blue-500 transition-colors">
Tauri 2.0 Tutorial Series
</span>
</div>
<svg
className="h-4 w-4 text-slate-400 group-hover:text-blue-500 group-hover:translate-x-0.5 transition-all"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</div>
</a>
</div>
</DataProvider>
);
}
export default App;
Step 14: Modern UI Styling
Apply glassmorphism effects, animations, and responsive design. The styling creates a modern, polished music player aesthetic with smooth transitions.
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@import "tailwindcss";
:root {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 16px;
line-height: 1.5;
font-weight: 400;
/* Light theme color palette */
--bg-primary: #f8fafc;
--bg-secondary: #ffffff;
--bg-tertiary: #f1f5f9;
--border-color: #e2e8f0;
--border-hover: #cbd5e1;
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
/* Accent colors */
--accent-emerald: #10b981;
--accent-blue: #3b82f6;
--accent-orange: #f97316;
--accent-purple: #8b5cf6;
--accent-red: #ef4444;
color-scheme: light;
color: var(--text-primary);
background-color: var(--bg-primary);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
}
/* Custom scrollbar for light theme */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Glass effect for cards */
.glass-card {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
/* Subtle shadow */
.shadow-soft {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05),
0 2px 4px -2px rgba(0, 0, 0, 0.05),
0 0 0 1px rgba(0, 0, 0, 0.02);
}
.shadow-soft-lg {
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.08),
0 8px 10px -6px rgba(0, 0, 0, 0.05);
}
/* Gradient backgrounds for stats */
.gradient-emerald {
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
}
.gradient-blue {
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
}
.gradient-orange {
background: linear-gradient(135deg, #ffedd5 0%, #fed7aa 100%);
}
.gradient-purple {
background: linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%);
}
/* Animated pulse ring */
@keyframes pulse-ring {
0% {
transform: scale(0.95);
opacity: 1;
}
50% {
transform: scale(1);
opacity: 0.5;
}
100% {
transform: scale(0.95);
opacity: 1;
}
}
.animate-pulse-ring {
animation: pulse-ring 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Smooth transitions */
* {
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
transition-duration: 150ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}Step 15: Utility Functions
Create utility functions for formatting time and handling common operations. These helpers improve code reusability throughout the app.
import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Step 16: Tauri Configuration
Configure Tauri settings including file system permissions, dialog access, and app metadata. This finalizes the application setup.
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "tauri-appsystem-monitor",
"version": "0.1.0",
"identifier": "com.systemmonitor.app",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "{Coders Handbook} System Monitor",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/[email protected]",
"icons/icon.icns",
"icons/icon.ico"
]
}
}{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default"
]
}
{
"name": "tauri-appsystem-monitor",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
"chart.js": "^4.5.1",
"clsx": "^2.1.1",
"lucide-react": "^0.563.0",
"react": "^19.1.0",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.1.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"typescript": "~5.8.3",
"vite": "^7.0.4"
}
}
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
Step 17: Running and Testing
Run the music player in development mode to test all features including playback, playlist management, and equalizer controls.
# Install dependencies
npm install
# Run in development mode
npm run tauri dev
# Test all features:
# 1. Click 'Open Files' to add songs
# 2. Select multiple MP3/audio files
# 3. Play/pause tracks
# 4. Use previous/next controls
# 5. Drag progress bar to seek
# 6. Adjust volume and mute
# 7. Enable shuffle and repeat modes
# 8. Drag songs to reorder playlist
# 9. Delete songs from playlist
# 10. Adjust equalizer sliders
# 11. Try keyboard shortcuts (Space, Arrow keys)
# 12. Watch background adapt to album art colorsStep 18: Building for Production
Build optimized executables for distribution. The music player creates small, native applications with embedded web technologies.
# Build for production
npm run tauri build
# Platform outputs:
# Windows: src-tauri/target/release/bundle/nsis/*.exe
# macOS: src-tauri/target/release/bundle/dmg/*.dmg
# Linux: src-tauri/target/release/bundle/deb/*.deb
# src-tauri/target/release/bundle/appimage/*.AppImage
# Bundle size: ~10-18 MB (with audio codecs)
# Supported audio formats:
# - MP3 (most common)
# - WAV (uncompressed)
# - OGG (open format)
# - AAC (via browser codecs)
# - FLAC (depends on browser support)
# Note: Audio format support depends on browser engine:
# - Windows/Linux: Chromium (WebView2/WebKitGTK)
# - macOS: WebKitComplete Features Breakdown
| Feature | Description | Implementation |
|---|---|---|
| Audio Playback | Play, pause, resume with HTML5 Audio | useAudioPlayer hook |
| Playlist Management | Add, remove, reorder songs with persistence | usePlaylist hook + localStorage |
| Progress Bar | Visual playback position with seek capability | ProgressBar component |
| Volume Control | Adjustable volume slider with mute toggle | VolumeControl component |
| Shuffle Mode | Randomize playback order | Fisher-Yates algorithm |
| Repeat Modes | None, repeat all, repeat one | Playlist state management |
| Drag and Drop | Reorder playlist by dragging songs | HTML5 Drag API |
| Color Extraction | Dynamic background from album art | Canvas API + color quantization |
| Equalizer | 10-band EQ with Web Audio filters | BiquadFilterNode array |
| EQ Presets | Rock, Pop, Jazz, Classical presets | Predefined frequency values |
| Keyboard Shortcuts | Space (play/pause), arrows (seek/volume) | Event listeners |
| Now Playing | Current track info with artwork | NowPlaying component |
| File Selection | Native dialog for adding songs | Tauri dialog plugin |
| Glassmorphism UI | Modern frosted glass aesthetic | CSS backdrop-filter |
Keyboard Shortcuts Reference
| Shortcut | Action | Context |
|---|---|---|
| Space | Play/Pause toggle | Global |
| → (Right Arrow) | Seek forward 5 seconds | During playback |
| ← (Left Arrow) | Seek backward 5 seconds | During playback |
| ↑ (Up Arrow) | Increase volume 10% | Global |
| ↓ (Down Arrow) | Decrease volume 10% | Global |
| N | Next track | Global |
| P | Previous track | Global |
| S | Toggle shuffle | Global |
| R | Cycle repeat modes | Global |
| M | Mute/Unmute | Global |
| O | Open file dialog | Global |
Possible Enhancements
- Media Keys Support: Add global media key integration for play/pause/next/previous from keyboard
- Lyrics Display: Fetch and display synchronized lyrics from online services
- Music Library: Build searchable library with artists, albums, and genres
- Audio Visualizer: Add frequency spectrum analyzer with Canvas animations
- Playlist Export: Save playlists as M3U/PLS files for sharing
- Metadata Editing: Allow editing ID3 tags (title, artist, album, artwork)
- Crossfade: Implement smooth transitions between tracks
- Mini Player: Add compact always-on-top mode
- Last.fm Integration: Scrobble played tracks to Last.fm
- Audio Normalization: Automatic volume leveling across tracks
Troubleshooting Common Issues
- Audio not playing: Check browser console for codec errors; verify file format is supported by WebView
- Equalizer not working: Ensure Web Audio context is initialized; some browsers require user interaction first
- Colors not extracting: Verify album artwork loads successfully; check CORS if loading from URLs
- Playlist not persisting: Check localStorage is enabled; verify JSON serialization doesn't fail
- File dialog errors: Ensure Tauri dialog plugin is properly configured in capabilities
- Stuttering playback: Check CPU usage; consider reducing EQ complexity or disabling color extraction
Next Steps
- Note-Taking App: Build database app with SQLite project
- Screen Recorder: Create video capture with recorder project
- System Monitor: Track resources with monitoring project
- File System API: Deep dive into file operations with file system guide
- WebRTC: Add streaming features with WebRTC tutorial
Conclusion
Building a music player with Tauri 2.0 demonstrates audio playback, playlist management, and modern UI design creating professional media application maintaining performance and rich features. This project combines HTML5 Audio API providing cross-platform audio support with native codecs, React custom hooks managing player state and playlist logic with clean separation, Web Audio API implementing equalizer filters with 10-band control, Canvas API extracting dominant colors from album artwork for dynamic theming, localStorage persisting playlists and preferences across sessions, Tauri dialog plugin enabling native file selection, drag-and-drop functionality reordering playlist items with intuitive interactions, keyboard shortcuts providing productivity controls for media management, and glassmorphism styling creating modern aesthetic with smooth animations delivering comprehensive music player. Understanding media player patterns including audio state management with hooks and lifecycle, Web Audio API filter chains for equalization, color extraction algorithms using quantization, playlist data structures with shuffle and repeat logic, localStorage serialization handling complex state, responsive UI adapting to different window sizes, and performance optimization minimizing audio latency establishes foundation for building multimedia applications handling audio playback maintaining professional experience through proper implementation techniques music enthusiasts and developers depend on!
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


