$ cat /posts/tauri-20-project-music-player-media-playback-app.md
[tags]Tauri 2.0

Tauri 2.0 Project Music Player Media Playback App

drwxr-xr-x2026-02-045 min0 views
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.

bashproject-structure
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 config

Step 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.

bashsetup.sh
# 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-fs

Step 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.

tomlsrc-tauri/Cargo.toml
[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"] }
rustsrc-tauri/src/commands.rs
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()),
    }
}
rustsrc-tauri/src/lib.rs
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");
}
rustsrc-tauri/src/main.rs
// 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.

typescriptsrc/types/index.ts
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.

typescriptsrc/hooks/useAudioPlayer.ts
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.

typescriptsrc/hooks/usePlaylist.ts
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.

typescriptsrc/hooks/useColorExtractor.ts
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.

typescriptsrc/components/Controls.tsx
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.

typescriptsrc/components/ProgressBar.tsx
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.

typescriptsrc/components/VolumeControl.tsx
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.

typescriptsrc/components/NowPlaying.tsx
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.

typescriptsrc/components/Playlist.tsx
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.

typescriptsrc/components/Equalizer.tsx
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.

typescriptsrc/App.tsx
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.

csssrc/App.css
@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.

typescriptsrc/utils/utils.ts
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.

jsonsrc-tauri/tauri.conf.json
{
  "$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"
    ]
  }
}
jsonsrc-tauri/capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "opener:default"
  ]
}
jsonpackage.json
{
  "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"
  }
}
typescriptsrc/main.tsx
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.

bashdevelopment.sh
# 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 colors

Step 18: Building for Production

Build optimized executables for distribution. The music player creates small, native applications with embedded web technologies.

bashbuild.sh
# 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: WebKit

Complete Features Breakdown

FeatureDescriptionImplementation
Audio PlaybackPlay, pause, resume with HTML5 AudiouseAudioPlayer hook
Playlist ManagementAdd, remove, reorder songs with persistenceusePlaylist hook + localStorage
Progress BarVisual playback position with seek capabilityProgressBar component
Volume ControlAdjustable volume slider with mute toggleVolumeControl component
Shuffle ModeRandomize playback orderFisher-Yates algorithm
Repeat ModesNone, repeat all, repeat onePlaylist state management
Drag and DropReorder playlist by dragging songsHTML5 Drag API
Color ExtractionDynamic background from album artCanvas API + color quantization
Equalizer10-band EQ with Web Audio filtersBiquadFilterNode array
EQ PresetsRock, Pop, Jazz, Classical presetsPredefined frequency values
Keyboard ShortcutsSpace (play/pause), arrows (seek/volume)Event listeners
Now PlayingCurrent track info with artworkNowPlaying component
File SelectionNative dialog for adding songsTauri dialog plugin
Glassmorphism UIModern frosted glass aestheticCSS backdrop-filter

Keyboard Shortcuts Reference

ShortcutActionContext
SpacePlay/Pause toggleGlobal
→ (Right Arrow)Seek forward 5 secondsDuring playback
← (Left Arrow)Seek backward 5 secondsDuring playback
↑ (Up Arrow)Increase volume 10%Global
↓ (Down Arrow)Decrease volume 10%Global
NNext trackGlobal
PPrevious trackGlobal
SToggle shuffleGlobal
RCycle repeat modesGlobal
MMute/UnmuteGlobal
OOpen file dialogGlobal
Audio Performance: This music player uses native HTML5 Audio with Web Audio API for equalizer effects. Unlike Electron-based players, Tauri provides native performance with minimal memory footprint (typically <60MB RAM) while supporting all common audio formats!

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
Audio Format Support: The music player supports MP3, WAV, OGG, and AAC formats out of the box. Format availability depends on the underlying browser engine (Chromium on Windows/Linux, WebKit on macOS). For FLAC support, consider adding a JavaScript decoder library!

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

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!

$ cd ./tutorial-series/
$ progress51/51 (100%)

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.