Implement Lucide Vue icon system and UI improvements
- Replace emoji icons with professional SVG icons from Lucide Vue - Add collapsible MusicControls with compact top-right collapse button - Improve icon system with dynamic sizing and proper prop handling - Disable SSR to prevent hydration issues with audio APIs - Update IconButton to support both emoji strings and SVG components - Optimize bottom positioning for expanded vs collapsed states - Document new icon system in DESIGN_SYSTEM.md
This commit is contained in:
231
pages/index.vue
231
pages/index.vue
@@ -1,124 +1,43 @@
|
||||
<template>
|
||||
<div class="music-player">
|
||||
<!-- Aurora background -->
|
||||
<div class="aurora-bg">
|
||||
<div class="aurora-orb"></div>
|
||||
<div class="aurora-orb"></div>
|
||||
<div class="aurora-orb"></div>
|
||||
<div class="aurora-orb"></div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Aurora Background -->
|
||||
<AuroraBackground
|
||||
:is-playing="isPlaying"
|
||||
:current-track="currentTrack"
|
||||
/>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="glass-strong" style="padding: 20px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h1 style="font-size: 2rem; font-weight: 700; background: linear-gradient(45deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
|
||||
🎵 RepoDructor
|
||||
</h1>
|
||||
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<!-- Theme toggle -->
|
||||
<button @click="toggleTheme" class="btn-icon" title="Toggle theme">
|
||||
<span v-if="isDark">☀️</span>
|
||||
<span v-else>🌙</span>
|
||||
</button>
|
||||
|
||||
<!-- Shuffle -->
|
||||
<button @click="toggleShuffle" class="btn-icon" :class="{ 'active': isShuffled }" title="Shuffle">
|
||||
🔀
|
||||
</button>
|
||||
|
||||
<!-- Repeat modes -->
|
||||
<button @click="cycleRepeat" class="btn-icon" title="Repeat mode">
|
||||
<span v-if="repeatMode === 'none'">🔁</span>
|
||||
<span v-else-if="repeatMode === 'all'">🔂</span>
|
||||
<span v-else>🔂</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Container -->
|
||||
<MainContainer
|
||||
:current-track="currentTrack"
|
||||
:is-playing="isPlaying"
|
||||
@shuffle-changed="handleShuffleChanged"
|
||||
@repeat-changed="handleRepeatChanged"
|
||||
>
|
||||
<!-- Track List -->
|
||||
<TrackList
|
||||
:tracks="tracks"
|
||||
:display-tracks="displayTracks"
|
||||
:current-track="currentTrack"
|
||||
:is-playing="isPlaying"
|
||||
:loading="loading"
|
||||
:loading-track="loadingTrack"
|
||||
@track-selected="handleTrackSelected"
|
||||
/>
|
||||
</MainContainer>
|
||||
|
||||
<!-- Now Playing -->
|
||||
<div v-if="currentTrack" class="glass" style="padding: 20px; margin-bottom: 20px; text-align: center;">
|
||||
<h2 style="font-size: 1.5rem; margin-bottom: 10px;">Now Playing</h2>
|
||||
<p style="font-size: 1.2rem; font-weight: 600; color: var(--accent-primary);">
|
||||
{{ currentTrack.name }}
|
||||
</p>
|
||||
<p style="color: var(--text-secondary); margin-top: 5px;">
|
||||
Duration: {{ formatTime(currentTrack.duration || 0) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Track List -->
|
||||
<div class="track-list">
|
||||
<h3 style="margin-bottom: 15px; font-size: 1.3rem;">Your Music Library</h3>
|
||||
|
||||
<div v-if="loading" style="text-align: center; padding: 40px;">
|
||||
<p>Loading your music...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tracks.length === 0" style="text-align: center; padding: 40px;">
|
||||
<p>No music files found. Add some music files to the /music folder!</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="(track, index) in displayTracks"
|
||||
:key="track.name"
|
||||
@click="playTrack(track, index)"
|
||||
class="track-item"
|
||||
:class="{ active: currentTrack?.name === track.name }"
|
||||
style="display: flex; justify-content: space-between; align-items: center;"
|
||||
>
|
||||
<div>
|
||||
<p style="font-weight: 500;">{{ track.name }}</p>
|
||||
<p style="font-size: 0.9rem; color: var(--text-secondary);">
|
||||
{{ formatTime(track.duration || 0) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<span v-if="currentTrack?.name === track.name && !isPlaying">⏸️</span>
|
||||
<span v-else-if="currentTrack?.name === track.name && isPlaying">▶️</span>
|
||||
<span v-else>🎵</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player Controls -->
|
||||
<div class="player-controls" v-if="currentTrack">
|
||||
<!-- Previous -->
|
||||
<button @click="previousTrack" class="btn-icon" title="Previous">
|
||||
⏮️
|
||||
</button>
|
||||
|
||||
<!-- Play/Pause -->
|
||||
<button @click="togglePlay" class="btn-icon" style="width: 60px; height: 60px; font-size: 1.5rem;" title="Play/Pause">
|
||||
<span v-if="isPlaying">⏸️</span>
|
||||
<span v-else>▶️</span>
|
||||
</button>
|
||||
|
||||
<!-- Next -->
|
||||
<button @click="nextTrack" class="btn-icon" title="Next">
|
||||
⏭️
|
||||
</button>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="progress-bar" @click="seekTo" ref="progressBar">
|
||||
<div class="progress-fill" :style="{ width: progressPercent + '%' }"></div>
|
||||
</div>
|
||||
|
||||
<!-- Time -->
|
||||
<div style="min-width: 100px; text-align: center; font-size: 0.9rem;">
|
||||
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
|
||||
</div>
|
||||
|
||||
<!-- Volume -->
|
||||
<div class="volume-control">
|
||||
<span>🔊</span>
|
||||
<div class="volume-slider" @click="setVolume" ref="volumeSlider">
|
||||
<div class="progress-fill" :style="{ width: volume * 100 + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Music Controls -->
|
||||
<MusicControls
|
||||
:current-track="currentTrack"
|
||||
:is-playing="isPlaying"
|
||||
:current-time="currentTime"
|
||||
:duration="duration"
|
||||
:volume="volume"
|
||||
@previous="previousTrack"
|
||||
@next="nextTrack"
|
||||
@toggle-play="togglePlay"
|
||||
@seek="seekTo"
|
||||
@volume-change="setVolume"
|
||||
/>
|
||||
|
||||
<!-- Audio Element -->
|
||||
<audio
|
||||
@@ -133,9 +52,20 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// Disable SSR for this page since it uses browser APIs and audio elements
|
||||
definePageMeta({
|
||||
ssr: false
|
||||
})
|
||||
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
|
||||
// Import components
|
||||
import AuroraBackground from '~/components/AuroraBackground.client.vue'
|
||||
import MainContainer from '~/components/MainContainer.client.vue'
|
||||
import TrackList from '~/components/TrackList.client.vue'
|
||||
import MusicControls from '~/components/MusicControls.client.vue'
|
||||
|
||||
// Reactive state
|
||||
const tracks = ref([])
|
||||
const currentTrack = ref(null)
|
||||
@@ -145,8 +75,9 @@ const currentTime = ref(0)
|
||||
const duration = ref(0)
|
||||
const volume = ref(0.7)
|
||||
const loading = ref(true)
|
||||
const loadingTrack = ref(null)
|
||||
|
||||
// Theme
|
||||
// Theme (handled by ThemeToggle component now)
|
||||
const isDark = useLocalStorage('theme-dark', false)
|
||||
|
||||
// Playback modes
|
||||
@@ -156,14 +87,8 @@ const shuffledIndices = ref([])
|
||||
|
||||
// Refs
|
||||
const audioPlayer = ref(null)
|
||||
const progressBar = ref(null)
|
||||
const volumeSlider = ref(null)
|
||||
|
||||
// Computed
|
||||
const progressPercent = computed(() => {
|
||||
if (duration.value === 0) return 0
|
||||
return (currentTime.value / duration.value) * 100
|
||||
})
|
||||
// Computed - removed progressPercent as it's not used in this component
|
||||
|
||||
const displayTracks = computed(() => {
|
||||
if (isShuffled.value && shuffledIndices.value.length > 0) {
|
||||
@@ -197,6 +122,7 @@ const generateShuffledIndices = () => {
|
||||
}
|
||||
|
||||
const playTrack = async (track, index) => {
|
||||
loadingTrack.value = track.name
|
||||
currentTrack.value = track
|
||||
currentTrackIndex.value = isShuffled.value
|
||||
? shuffledIndices.value.indexOf(tracks.value.indexOf(track))
|
||||
@@ -224,6 +150,7 @@ const playTrack = async (track, index) => {
|
||||
|
||||
// Auto-play when ready
|
||||
audioPlayer.value.addEventListener('canplay', () => {
|
||||
loadingTrack.value = null
|
||||
audioPlayer.value.play()
|
||||
}, { once: true })
|
||||
} catch (error) {
|
||||
@@ -235,6 +162,7 @@ const playTrack = async (track, index) => {
|
||||
|
||||
// Auto-play when ready (fallback)
|
||||
audioPlayer.value.addEventListener('canplay', () => {
|
||||
loadingTrack.value = null
|
||||
audioPlayer.value.play()
|
||||
}, { once: true })
|
||||
}
|
||||
@@ -301,46 +229,30 @@ const previousTrack = () => {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const toggleShuffle = () => {
|
||||
isShuffled.value = !isShuffled.value
|
||||
if (isShuffled.value) {
|
||||
const handleShuffleChanged = (shuffled) => {
|
||||
isShuffled.value = shuffled
|
||||
if (shuffled) {
|
||||
generateShuffledIndices()
|
||||
}
|
||||
}
|
||||
|
||||
const cycleRepeat = () => {
|
||||
const modes = ['none', 'all', 'one']
|
||||
const currentIndex = modes.indexOf(repeatMode.value)
|
||||
repeatMode.value = modes[(currentIndex + 1) % modes.length]
|
||||
const handleRepeatChanged = (mode) => {
|
||||
repeatMode.value = mode
|
||||
}
|
||||
|
||||
const toggleTheme = () => {
|
||||
isDark.value = !isDark.value
|
||||
const handleTrackSelected = ({ track, index }) => {
|
||||
playTrack(track, index)
|
||||
}
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
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')}`
|
||||
}
|
||||
// Removed formatTime function as it's not used in this component - it's defined in child components where needed
|
||||
|
||||
const seekTo = (event) => {
|
||||
if (!audioPlayer.value || !progressBar.value) return
|
||||
|
||||
const rect = progressBar.value.getBoundingClientRect()
|
||||
const percent = (event.clientX - rect.left) / rect.width
|
||||
const newTime = percent * duration.value
|
||||
|
||||
const seekTo = (newTime) => {
|
||||
if (!audioPlayer.value) return
|
||||
audioPlayer.value.currentTime = newTime
|
||||
}
|
||||
|
||||
const setVolume = (event) => {
|
||||
if (!volumeSlider.value) return
|
||||
|
||||
const rect = volumeSlider.value.getBoundingClientRect()
|
||||
const percent = (event.clientX - rect.left) / rect.width
|
||||
volume.value = Math.max(0, Math.min(1, percent))
|
||||
const setVolume = (newVolume) => {
|
||||
volume.value = Math.max(0, Math.min(1, newVolume))
|
||||
|
||||
if (audioPlayer.value) {
|
||||
audioPlayer.value.volume = volume.value
|
||||
@@ -454,13 +366,6 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.active {
|
||||
background: linear-gradient(45deg, var(--accent-primary), var(--accent-secondary)) !important;
|
||||
color: white !important;
|
||||
}
|
||||
/* Page-specific styles */
|
||||
</style>
|
||||
|
||||
.btn-icon.active {
|
||||
background: var(--accent-primary) !important;
|
||||
color: white !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user