frontend/app/components/WinampWindow.vue

342 lines
8.6 KiB
Vue

<template>
<div class="winamp-window main-window" @mousedown="startDrag">
<div class="title-bar">
<div class="title-bar-text">WINAMP</div>
<div class="title-bar-controls">
<button aria-label="Minimize"></button>
<button aria-label="Maximize"></button>
<button aria-label="Close"></button>
</div>
</div>
<div class="main-body">
<div class="top-panel">
<div class="visualizer"></div>
<div class="track-display">
<div class="track-title-marquee">
<span>{{ currentTrack ? `${currentTrackIndex + 1}. ${currentTrack.artist} - ${currentTrack.title}` : 'No Track Loaded' }}</span>
</div>
<div class="time-display">{{ formatTime(currentTime) }}</div>
</div>
</div>
<div class="middle-panel">
<div class="kbps">
{{ Math.round(audioRate / 1000) || '0' }}
<span>kbps</span>
</div>
<div class="khz">
{{ Math.round(audioSampleRate / 1000) || '0' }}
<span>kHz</span>
</div>
<div class="mono-stereo">{{ stereoStatus }}</div>
</div>
<div class="bottom-panel">
<div class="control-buttons">
<button class="control-btn prev" @click="$emit('prev')"></button>
<button class="control-btn play" @click="$emit('play-pause')" :class="{ 'pause-active': isPlaying }"></button>
<button class="control-btn stop" @click="$emit('stop')"></button>
<button class="control-btn next" @click="$emit('next')"></button>
</div>
<div class="volume-slider-container">
<input
type="range"
min="0"
max="1"
step="0.01"
:value="volume"
class="slider volume-slider"
@input="$emit('update:volume', $event.target.value)"
/>
</div>
<div class="progress-bar-container">
<input
type="range"
min="0"
:max="duration"
:value="currentTime"
class="slider progress-slider"
@input="$emit('seek', $event.target.value)"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, computed } from 'vue';
const props = defineProps({
currentTrack: Object,
currentTrackIndex: Number,
isPlaying: Boolean,
currentTime: Number,
duration: Number,
volume: Number,
audioRate: Number, // Example for bitrate, you'd calculate this or get from metadata
audioSampleRate: Number, // Example for sample rate
stereoStatus: String, // 'mono' or 'stereo'
});
const emit = defineEmits(['prev', 'play-pause', 'stop', 'next', 'update:volume', 'seek', 'start-drag']);
const formatTime = (time) => {
if (isNaN(time) || time === 0) return '0:00';
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60).toString().padStart(2, '0');
return `${minutes}:${seconds}`;
};
const startDrag = (event) => {
if (event.target.classList.contains('title-bar')) {
emit('start-drag', event, 'main-window');
}
};
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');
.winamp-window {
font-family: 'VT323', monospace;
background-color: #3b3b3b; /* Dark grey base */
border: 1px solid #7a7a7a;
border-right-color: #000;
border-bottom-color: #000;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
position: absolute;
color: #c0c0c0; /* Light grey text */
box-sizing: border-box;
}
.main-window {
width: 275px;
height: 116px; /* Adjusted height to match the image */
left: 50px;
top: 50px;
}
.title-bar {
background: #000;
color: #00ff00; /* Green text */
padding: 1px 3px;
display: flex;
justify-content: space-between;
align-items: center;
height: 15px;
cursor: grab;
font-size: 14px;
border-bottom: 1px solid #3b3b3b;
}
.title-bar:active { cursor: grabbing; }
.title-bar-text {
font-weight: bold;
}
.title-bar-controls button {
width: 13px;
height: 11px;
background: #c0c0c0;
border: 1px solid #fff;
border-right-color: #404040;
border-bottom-color: #404040;
margin-left: 1px;
font-size: 0; /* Hide default button text */
position: relative;
overflow: hidden;
}
.title-bar-controls button[aria-label="Close"] {
background-color: #800000; /* Red close button */
border: 1px solid #ff0000;
border-right-color: #400000;
border-bottom-color: #400000;
}
.title-bar-controls button[aria-label="Close"]::before,
.title-bar-controls button[aria-label="Close"]::after {
content: '';
position: absolute;
top: 5px; /* Center 'x' */
left: 2px;
width: 9px;
height: 1px;
background: white;
}
.title-bar-controls button[aria-label="Close"]::before {
transform: rotate(45deg);
}
.title-bar-controls button[aria-label="Close"]::after {
transform: rotate(-45deg);
}
/* Main Player Body */
.main-body {
padding: 3px;
}
.top-panel {
display: flex;
background-color: #000;
border: 1px inset #808080;
padding: 2px;
height: 25px;
margin-bottom: 2px;
}
.visualizer {
width: 40px; /* Placeholder for visualizer */
height: 100%;
background-color: #003300; /* Dark green for silent visualizer */
margin-right: 5px;
}
.track-display {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
color: #00ff00; /* Bright green text */
overflow: hidden;
}
.track-title-marquee {
overflow: hidden;
white-space: nowrap;
position: relative;
height: 14px; /* Adjust height for single line of text */
}
.track-title-marquee span {
position: absolute;
animation: marquee 10s linear infinite;
padding-left: 100%; /* Start off-screen */
}
@keyframes marquee {
0% { transform: translateX(0%); }
100% { transform: translateX(-100%); }
}
.time-display {
font-size: 16px;
text-align: right;
height: 14px;
}
.middle-panel {
display: flex;
justify-content: space-between;
background-color: #000;
border: 1px inset #808080;
padding: 2px;
color: #c0c0c0;
font-size: 12px;
height: 16px;
margin-bottom: 2px;
}
.kbps, .khz, .mono-stereo {
display: flex;
align-items: center;
}
.kbps span, .khz span {
font-size: 10px;
margin-left: 2px;
}
.bottom-panel {
display: flex;
align-items: center;
gap: 5px;
background-color: #3b3b3b;
padding: 2px;
border: 1px solid #7a7a7a;
border-top-color: #000;
border-left-color: #000;
}
.control-buttons {
display: flex;
gap: 1px;
}
.control-btn {
width: 20px;
height: 18px;
border: 1px solid #7a7a7a;
border-right-color: #000;
border-bottom-color: #000;
background-color: #3b3b3b;
cursor: pointer;
background-repeat: no-repeat;
background-position: center;
background-size: 80%;
position: relative;
overflow: hidden;
}
.control-btn:active {
border: 1px solid #000;
border-right-color: #7a7a7a;
border-bottom-color: #7a7a7a;
}
/* Data URI for button icons (basic representation, replace with actual pixel art) */
.control-btn.prev { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23c0c0c0"><path d="M11 18V6l-7 6zm3.5-6l7 6V6z"/></svg>'); }
.control-btn.play { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23c0c0c0"><path d="M8 5v14l11-7z"/></svg>'); }
.control-btn.play.pause-active { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23c0c0c0"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>'); }
.control-btn.stop { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23c0c0c0"><path d="M6 6h12v12H6z"/></svg>'); }
.control-btn.next { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23c0c0c0"><path d="M13 6v12l7-6zm-7-6v12l7-6z"/></svg>'); }
/* Sliders */
.slider {
-webkit-appearance: none;
appearance: none;
background: #000;
outline: none;
cursor: pointer;
border: 1px inset #808080; /* Sunken effect */
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 8px;
height: 16px;
background: #c0c0c0;
border: 1px solid #fff;
border-right-color: #404040;
border-bottom-color: #404040;
cursor: grab;
}
.slider::-moz-range-thumb {
width: 8px;
height: 16px;
background: #c0c0c0;
border: 1px solid #fff;
border-right-color: #404040;
border-bottom-color: #404040;
cursor: grab;
}
.volume-slider-container {
width: 60px;
}
.volume-slider {
width: 100%;
height: 8px;
}
.progress-bar-container {
flex-grow: 1;
}
.progress-slider {
width: 100%;
height: 8px;
}
</style>