music-player / templates /index.html
prasanth.thangavel
Major revamp aka v2
5c81fbf
<!DOCTYPE html>
<html>
<head>
<title>Music Player</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--bg-primary: #0f0f0f;
--bg-secondary: #1a1a1a;
--bg-tertiary: #2a2a2a;
--bg-hover: #3a3a3a;
--accent-primary: #ff6b6b;
--accent-secondary: #4ecdc4;
--text-primary: #ffffff;
--text-secondary: #b3b3b3;
--text-muted: #666666;
--border-color: #333333;
--success: #4caf50;
--warning: #ff9800;
--error: #f44336;
--gradient-primary: linear-gradient(135deg, #ff6b6b 0%, #4ecdc4 100%);
--gradient-secondary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
--border-radius: 12px;
--border-radius-sm: 8px;
--border-radius-lg: 16px;
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
overflow-x: hidden;
}
/* Background Animation */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at 20% 50%, rgba(255, 107, 107, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(78, 205, 196, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 80%, rgba(102, 126, 234, 0.1) 0%, transparent 50%);
z-index: -1;
animation: backgroundShift 20s ease-in-out infinite;
}
@keyframes backgroundShift {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.sw-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
}
.sw-player {
background: var(--bg-secondary);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-lg);
padding: 32px;
margin-bottom: 24px;
backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
}
.sw-header {
font-size: 2.5rem;
font-weight: 700;
text-align: center;
margin-bottom: 32px;
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
position: relative;
}
.sw-header::after {
content: '';
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background: var(--gradient-primary);
border-radius: 2px;
}
.sw-now-playing {
text-align: center;
margin-bottom: 24px;
padding: 16px;
background: var(--bg-tertiary);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
}
.sw-now-playing-label {
font-size: 0.85rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.sw-now-playing-song {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sw-custom-player {
margin: 24px 0;
padding: 20px;
background: var(--bg-tertiary);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
}
.sw-progress-container {
margin-bottom: 20px;
}
.sw-time-display {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 8px;
}
.sw-progress-bar {
position: relative;
height: 6px;
background: var(--border-color);
border-radius: 3px;
cursor: pointer;
overflow: hidden;
}
.sw-progress-fill {
height: 100%;
background: var(--gradient-primary);
width: 0%;
transition: width 0.1s ease;
border-radius: 3px;
}
.sw-progress-handle {
position: absolute;
top: 50%;
left: 0%;
width: 16px;
height: 16px;
background: var(--accent-primary);
border-radius: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
box-shadow: var(--shadow-sm);
}
.sw-progress-bar:hover .sw-progress-handle {
opacity: 1;
}
.sw-audio-controls {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
.sw-volume-control {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.sw-volume-control i {
color: var(--text-secondary);
font-size: 1.1rem;
min-width: 20px;
}
.sw-volume-slider {
flex: 1;
max-width: 120px;
}
.sw-slider {
width: 100%;
height: 4px;
border-radius: 2px;
background: var(--border-color);
outline: none;
-webkit-appearance: none;
appearance: none;
}
.sw-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent-primary);
cursor: pointer;
box-shadow: var(--shadow-sm);
}
.sw-slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent-primary);
cursor: pointer;
border: none;
box-shadow: var(--shadow-sm);
}
.sw-slider::-webkit-slider-track {
background: var(--gradient-primary);
height: 4px;
border-radius: 2px;
}
.sw-slider::-moz-range-track {
background: var(--gradient-primary);
height: 4px;
border-radius: 2px;
border: none;
}
#swVolumeValue {
font-size: 0.85rem;
color: var(--text-secondary);
min-width: 40px;
}
.sw-speed-control {
display: flex;
align-items: center;
gap: 8px;
}
.sw-speed-control i {
color: var(--text-secondary);
font-size: 1.1rem;
}
.sw-speed-dropdown {
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 6px 12px;
font-size: 0.85rem;
cursor: pointer;
outline: none;
}
.sw-speed-dropdown:hover {
border-color: var(--accent-primary);
}
.sw-speed-dropdown:focus {
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.2);
}
/* Upload Section Styles */
.sw-upload-section {
margin: 24px 0;
padding: 20px;
background: var(--bg-tertiary);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
}
.sw-upload-header {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.sw-upload-area {
border: 2px dashed var(--border-color);
border-radius: var(--border-radius);
padding: 40px 20px;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
background: var(--bg-primary);
}
.sw-upload-area:hover {
border-color: var(--accent-primary);
background: var(--bg-secondary);
}
.sw-upload-area.drag-over {
border-color: var(--accent-primary);
background: var(--bg-secondary);
transform: scale(1.02);
}
.sw-upload-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.sw-upload-icon {
font-size: 3rem;
color: var(--text-muted);
margin-bottom: 8px;
}
.sw-upload-text {
font-size: 1.1rem;
color: var(--text-primary);
margin: 0;
}
.sw-upload-subtext {
font-size: 0.9rem;
color: var(--text-muted);
margin: 0;
}
.sw-upload-btn {
margin: 8px 0;
}
.sw-upload-info {
font-size: 0.8rem;
color: var(--text-muted);
margin: 8px 0 0 0;
}
.sw-upload-progress {
margin-top: 16px;
}
.sw-progress-item {
background: var(--bg-primary);
border-radius: var(--border-radius-sm);
padding: 12px;
border: 1px solid var(--border-color);
}
.sw-progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 0.9rem;
}
.sw-progress-bar-upload {
height: 6px;
background: var(--border-color);
border-radius: 3px;
overflow: hidden;
}
.sw-progress-fill-upload {
height: 100%;
background: var(--gradient-primary);
width: 0%;
transition: width 0.3s ease;
border-radius: 3px;
}
.sw-upload-success {
color: var(--success);
font-weight: 500;
}
.sw-upload-error {
color: var(--error);
font-weight: 500;
}
/* Advanced Playlist Management Styles */
.sw-modal-large {
max-width: 700px;
max-height: 90vh;
}
.sw-playlist-editor {
margin-bottom: 24px;
padding: 16px;
background: var(--bg-primary);
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
}
.sw-label {
display: block;
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 8px;
font-weight: 500;
}
.sw-name-editor {
display: flex;
gap: 12px;
align-items: center;
}
.sw-name-editor .sw-input {
flex: 1;
margin-bottom: 0;
}
.sw-btn-small {
padding: 8px 16px;
font-size: 0.85rem;
min-width: auto;
}
.sw-manage-section {
margin-bottom: 24px;
}
.sw-manage-section h3 {
font-size: 1.1rem;
color: var(--text-primary);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.sw-song-count {
font-size: 0.9rem;
color: var(--text-muted);
font-weight: normal;
}
.sw-available-songs, .sw-current-songs {
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--bg-primary);
}
.sw-song-item {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
transition: background 0.2s ease;
cursor: pointer;
}
.sw-song-item:last-child {
border-bottom: none;
}
.sw-song-item:hover {
background: var(--bg-hover);
}
.sw-song-item.dragging {
opacity: 0.5;
background: var(--bg-hover);
}
.sw-song-item.drag-over {
border-top: 2px solid var(--accent-primary);
}
.sw-song-info {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
}
.sw-song-title {
font-size: 0.9rem;
color: var(--text-primary);
}
.sw-song-actions {
display: flex;
gap: 8px;
}
.sw-action-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
font-size: 0.9rem;
}
.sw-action-btn:hover {
color: var(--accent-primary);
background: var(--bg-hover);
}
.sw-action-btn.remove {
color: var(--error);
}
.sw-action-btn.add {
color: var(--success);
}
.sw-drag-handle {
cursor: grab;
color: var(--text-muted);
}
.sw-drag-handle:active {
cursor: grabbing;
}
.sw-playlist-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.sw-controls {
display: flex;
justify-content: center;
gap: 16px;
margin: 24px 0;
flex-wrap: wrap;
}
.sw-btn {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 12px 20px;
font-size: 0.9rem;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
min-width: 100px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.sw-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: var(--gradient-primary);
transition: left 0.3s ease;
z-index: 0;
}
.sw-btn span {
position: relative;
z-index: 1;
}
.sw-btn:hover::before {
left: 0;
}
.sw-btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--accent-primary);
}
.sw-btn.active {
background: var(--gradient-primary);
border-color: var(--accent-primary);
color: white;
box-shadow: var(--shadow-md);
}
.sw-btn.active::before {
left: 0;
}
.sw-btn:active {
transform: translateY(0);
}
.sw-dropdown {
width: 100%;
padding: 16px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 1rem;
font-family: inherit;
margin-bottom: 24px;
background: var(--bg-tertiary);
color: var(--text-primary);
cursor: pointer;
transition: all 0.3s ease;
}
.sw-dropdown:hover {
border-color: var(--accent-primary);
box-shadow: var(--shadow-sm);
}
.sw-dropdown:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(255, 107, 107, 0.2);
}
/* Search and Filter Styles */
.sw-search-section {
margin-bottom: 24px;
}
.sw-search-container {
position: relative;
margin-bottom: 16px;
}
.sw-search-input {
width: 100%;
padding: 16px 50px 16px 16px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 1rem;
font-family: inherit;
background: var(--bg-tertiary);
color: var(--text-primary);
transition: all 0.3s ease;
}
.sw-search-input:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(255, 107, 107, 0.2);
}
.sw-search-input::placeholder {
color: var(--text-muted);
}
.sw-search-clear {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.sw-search-clear:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.sw-filter-options {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.sw-filter-btn {
padding: 8px 16px;
border: 1px solid var(--border-color);
border-radius: 20px;
background: var(--bg-tertiary);
color: var(--text-muted);
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 6px;
}
.sw-filter-btn:hover {
border-color: var(--accent-primary);
color: var(--text-primary);
}
.sw-filter-btn.active {
background: var(--accent-primary);
border-color: var(--accent-primary);
color: white;
}
/* Playlist View Styles */
.sw-playlist-view {
margin-top: 24px;
}
.sw-playlist-view-header {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.sw-playlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.sw-playlist-card {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
}
.sw-playlist-card:hover {
border-color: var(--accent-primary);
box-shadow: var(--shadow-sm);
transform: translateY(-2px);
}
.sw-playlist-card-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.sw-playlist-card-icon {
width: 40px;
height: 40px;
background: var(--accent-primary);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.2rem;
}
.sw-playlist-card-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
}
.sw-playlist-card-info {
color: var(--text-muted);
font-size: 0.9rem;
margin-bottom: 16px;
}
.sw-playlist-card-actions {
display: flex;
gap: 8px;
}
.sw-playlist-card-btn {
padding: 8px 16px;
border: 1px solid var(--border-color);
border-radius: 20px;
background: var(--bg-secondary);
color: var(--text-primary);
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 6px;
}
.sw-playlist-card-btn:hover {
border-color: var(--accent-primary);
background: var(--accent-primary);
color: white;
}
/* Queue Management Styles */
.sw-queue-section {
margin-top: 32px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
overflow: hidden;
}
.sw-queue-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.sw-queue-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
}
.sw-queue-count {
color: var(--text-muted);
font-weight: 400;
font-size: 0.9rem;
}
.sw-queue-controls {
display: flex;
gap: 8px;
}
.sw-queue-btn {
padding: 8px 16px;
border: 1px solid var(--border-color);
border-radius: 20px;
background: var(--bg-tertiary);
color: var(--text-primary);
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 6px;
}
.sw-queue-btn:hover {
border-color: var(--accent-primary);
background: var(--accent-primary);
color: white;
}
.sw-queue-content {
max-height: 400px;
overflow-y: auto;
}
.sw-queue-list {
padding: 0;
}
.sw-queue-item {
display: flex;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.sw-queue-item:hover {
background: var(--bg-secondary);
}
.sw-queue-item.current {
background: rgba(255, 107, 107, 0.1);
border-left: 4px solid var(--accent-primary);
}
.sw-queue-item.playing {
background: rgba(255, 107, 107, 0.15);
}
.sw-queue-item-number {
width: 30px;
text-align: center;
color: var(--text-muted);
font-size: 0.9rem;
margin-right: 12px;
}
.sw-queue-item.current .sw-queue-item-number {
color: var(--accent-primary);
font-weight: 600;
}
.sw-queue-item-info {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
}
.sw-queue-item-icon {
width: 16px;
text-align: center;
color: var(--text-muted);
}
.sw-queue-item.current .sw-queue-item-icon {
color: var(--accent-primary);
}
.sw-queue-item-name {
flex: 1;
color: var(--text-primary);
font-size: 0.95rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sw-queue-item.current .sw-queue-item-name {
font-weight: 600;
}
.sw-queue-item-actions {
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.3s ease;
}
.sw-queue-item:hover .sw-queue-item-actions {
opacity: 1;
}
.sw-queue-item-btn {
width: 32px;
height: 32px;
border: 1px solid var(--border-color);
border-radius: 50%;
background: var(--bg-tertiary);
color: var(--text-muted);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
}
.sw-queue-item-btn:hover {
border-color: var(--accent-primary);
background: var(--accent-primary);
color: white;
}
.sw-queue-drag-handle {
cursor: grab;
color: var(--text-muted);
margin-right: 8px;
}
.sw-queue-drag-handle:active {
cursor: grabbing;
}
.sw-queue-item.dragging {
opacity: 0.5;
transform: rotate(5deg);
}
.sw-queue-empty {
padding: 40px 20px;
text-align: center;
color: var(--text-muted);
}
.sw-queue-empty i {
font-size: 2rem;
margin-bottom: 16px;
display: block;
}
.sw-playlist-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 24px;
}
.sw-tracklist {
margin-top: 24px;
}
.sw-tracklist-header {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 16px;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.sw-track {
padding: 16px 20px;
margin: 8px 0;
border-radius: var(--border-radius);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
transition: all 0.3s ease;
cursor: pointer;
font-size: 0.9rem;
position: relative;
overflow: hidden;
}
.sw-track::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 4px;
height: 100%;
background: var(--gradient-primary);
transform: scaleY(0);
transition: transform 0.3s ease;
}
.sw-track:hover {
background: var(--bg-hover);
transform: translateX(8px);
box-shadow: var(--shadow-sm);
}
.sw-track:hover::before {
transform: scaleY(1);
}
.sw-track.active {
background: var(--bg-hover);
border-color: var(--accent-primary);
color: var(--accent-primary);
box-shadow: var(--shadow-sm);
}
.sw-track.active::before {
transform: scaleY(1);
}
.sw-modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
padding: 20px;
z-index: 1000;
backdrop-filter: blur(5px);
}
.sw-modal-content {
background: var(--bg-secondary);
width: 100%;
max-width: 500px;
border-radius: var(--border-radius-lg);
padding: 32px;
border: 1px solid var(--border-color);
box-shadow: var(--shadow-lg);
max-height: 80vh;
overflow-y: auto;
}
.sw-modal h2 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 24px;
color: var(--text-primary);
}
.sw-input {
width: 100%;
padding: 16px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 1rem;
font-family: inherit;
margin-bottom: 24px;
background: var(--bg-tertiary);
color: var(--text-primary);
transition: all 0.3s ease;
}
.sw-input:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(255, 107, 107, 0.2);
}
.sw-input::placeholder {
color: var(--text-muted);
}
.sw-checkbox-list {
max-height: 300px;
overflow-y: auto;
margin-bottom: 24px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--bg-tertiary);
}
.sw-checkbox-item {
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 12px;
transition: background 0.3s ease;
}
.sw-checkbox-item:last-child {
border-bottom: none;
}
.sw-checkbox-item:hover {
background: var(--bg-hover);
}
.sw-checkbox-item input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--accent-primary);
cursor: pointer;
}
.sw-checkbox-item label {
cursor: pointer;
font-size: 0.9rem;
color: var(--text-secondary);
flex: 1;
}
.sw-modal-actions {
display: flex;
gap: 16px;
justify-content: flex-end;
}
/* Touch-friendly improvements */
@media (hover: none) and (pointer: coarse) {
.sw-btn {
min-height: 48px;
padding: 16px 20px;
font-size: 1rem;
}
.sw-track {
min-height: 56px;
padding: 18px 20px;
font-size: 1rem;
}
.sw-dropdown {
min-height: 48px;
padding: 16px;
font-size: 1rem;
}
.sw-input {
min-height: 48px;
padding: 16px;
font-size: 1rem;
}
}
/* Responsive Design */
@media (max-width: 768px) {
.sw-container {
padding: 12px;
}
.sw-player {
padding: 20px;
margin-bottom: 16px;
}
.sw-header {
font-size: 2rem;
margin-bottom: 24px;
}
.sw-now-playing {
padding: 12px;
margin-bottom: 20px;
}
.sw-now-playing-song {
font-size: 1rem;
}
.sw-custom-player {
margin: 20px 0;
padding: 16px;
}
.sw-audio-controls {
flex-direction: column;
gap: 16px;
}
.sw-volume-control {
justify-content: center;
}
.sw-speed-control {
justify-content: center;
}
.sw-upload-section {
margin: 20px 0;
padding: 16px;
}
.sw-upload-area {
padding: 30px 15px;
}
.sw-upload-icon {
font-size: 2.5rem;
}
.sw-upload-text {
font-size: 1rem;
}
.sw-controls {
gap: 8px;
margin: 20px 0;
}
.sw-btn {
min-width: 70px;
padding: 12px 16px;
font-size: 0.85rem;
min-height: 44px;
}
.sw-btn span {
display: none;
}
.sw-btn i {
font-size: 1.1rem;
}
.sw-search-section {
margin-bottom: 20px;
}
.sw-search-input {
padding: 14px 45px 14px 14px;
font-size: 0.9rem;
}
.sw-filter-options {
gap: 6px;
}
.sw-filter-btn {
padding: 6px 12px;
font-size: 0.8rem;
min-height: 36px;
}
.sw-filter-btn span {
display: none;
}
.sw-playlist-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.sw-playlist-card {
padding: 16px;
}
.sw-playlist-card-actions {
flex-wrap: wrap;
gap: 6px;
}
.sw-playlist-card-btn {
padding: 6px 12px;
font-size: 0.8rem;
}
.sw-queue-section {
margin-top: 24px;
}
.sw-queue-header {
padding: 12px 16px;
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.sw-queue-title {
justify-content: center;
}
.sw-queue-controls {
justify-content: center;
}
.sw-queue-btn {
padding: 8px 12px;
font-size: 0.8rem;
}
.sw-queue-btn span {
display: none;
}
.sw-queue-item {
padding: 10px 16px;
}
.sw-queue-item-number {
width: 25px;
margin-right: 8px;
}
.sw-queue-drag-handle {
margin-right: 6px;
}
.sw-playlist-controls {
grid-template-columns: 1fr;
gap: 12px;
}
.sw-playlist-controls .sw-btn span {
display: inline;
}
.sw-dropdown {
padding: 14px;
font-size: 0.9rem;
margin-bottom: 20px;
}
.sw-tracklist {
margin-top: 20px;
}
.sw-tracklist-header {
font-size: 1.1rem;
margin-bottom: 12px;
}
.sw-track {
padding: 16px;
margin: 6px 0;
font-size: 0.9rem;
}
.sw-modal-content {
padding: 20px;
margin: 12px;
max-height: 85vh;
}
.sw-modal h2 {
font-size: 1.3rem;
margin-bottom: 20px;
}
.sw-input {
padding: 14px;
margin-bottom: 20px;
font-size: 0.9rem;
}
.sw-checkbox-list {
margin-bottom: 20px;
}
.sw-checkbox-item {
padding: 14px 16px;
}
.sw-modal-actions {
gap: 12px;
}
.sw-modal-actions .sw-btn {
flex: 1;
justify-content: center;
}
.sw-modal-actions .sw-btn span {
display: inline;
}
}
@media (max-width: 480px) {
.sw-container {
padding: 8px;
}
.sw-player {
padding: 16px;
}
.sw-header {
font-size: 1.8rem;
margin-bottom: 20px;
}
.sw-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin: 16px 0;
}
.sw-btn {
min-width: auto;
padding: 10px 12px;
font-size: 0.8rem;
min-height: 40px;
}
.sw-btn i {
font-size: 1rem;
}
.sw-playlist-controls {
gap: 8px;
}
.sw-track {
padding: 14px 12px;
font-size: 0.85rem;
}
.sw-modal-content {
padding: 16px;
margin: 8px;
}
.sw-modal h2 {
font-size: 1.2rem;
margin-bottom: 16px;
}
.sw-modal-actions {
flex-direction: column;
gap: 8px;
}
.sw-modal-actions .sw-btn {
width: 100%;
}
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* Loading Animation */
.sw-loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--border-color);
border-radius: 50%;
border-top-color: var(--accent-primary);
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="sw-container">
<div class="sw-player">
<h1 class="sw-header">
<i class="fas fa-music"></i> Music Player
</h1>
<div class="sw-now-playing">
<div class="sw-now-playing-label">Now Playing</div>
<div class="sw-now-playing-song" id="swCurrentSong">No song selected</div>
</div>
<!-- Custom Audio Player -->
<div class="sw-custom-player">
<audio id="swAudio" preload="none" autoplay="false">
Your browser does not support audio playback.
</audio>
<!-- Progress Bar -->
<div class="sw-progress-container">
<div class="sw-time-display">
<span id="swCurrentTime">0:00</span>
<span id="swDuration">0:00</span>
</div>
<div class="sw-progress-bar" id="swProgressBar">
<div class="sw-progress-fill" id="swProgressFill"></div>
<div class="sw-progress-handle" id="swProgressHandle"></div>
</div>
</div>
<!-- Volume and Speed Controls -->
<div class="sw-audio-controls">
<div class="sw-volume-control">
<i class="fas fa-volume-up"></i>
<div class="sw-volume-slider">
<input type="range" id="swVolumeSlider" min="0" max="100" value="70" class="sw-slider">
</div>
<span id="swVolumeValue">70%</span>
</div>
<div class="sw-speed-control">
<i class="fas fa-tachometer-alt"></i>
<select id="swSpeedSelect" class="sw-speed-dropdown">
<option value="0.5">0.5x</option>
<option value="0.75">0.75x</option>
<option value="1" selected>1x</option>
<option value="1.25">1.25x</option>
<option value="1.5">1.5x</option>
<option value="2">2x</option>
</select>
</div>
</div>
</div>
<div class="sw-controls">
<button class="sw-btn" onclick="swPrevious()">
<i class="fas fa-step-backward"></i>
<span>Previous</span>
</button>
<button class="sw-btn" onclick="swNext()">
<i class="fas fa-step-forward"></i>
<span>Next</span>
</button>
<button class="sw-btn" id="swLoopBtn" onclick="swToggleLoop()">
<i class="fas fa-redo"></i>
<span>Loop</span>
</button>
<button class="sw-btn" id="swShuffleBtn" onclick="swToggleShuffle()">
<i class="fas fa-random"></i>
<span>Shuffle</span>
</button>
</div>
<select class="sw-dropdown" id="swPlaylistSelect" onchange="swChangePlaylist()">
<option value="all">🎵 All Songs</option>
</select>
<!-- Search and Filter Section -->
<div class="sw-search-section">
<div class="sw-search-container">
<input type="text" class="sw-search-input" id="swSearchInput" placeholder="Search songs or playlists..." oninput="swHandleSearch()">
<button class="sw-search-clear" id="swSearchClear" onclick="swClearSearch()" style="display: none;">
<i class="fas fa-times"></i>
</button>
</div>
<div class="sw-filter-options">
<button class="sw-filter-btn active" data-filter="all" onclick="swSetFilter('all')">
<i class="fas fa-list"></i> All
</button>
<button class="sw-filter-btn" data-filter="songs" onclick="swSetFilter('songs')">
<i class="fas fa-music"></i> Songs Only
</button>
<button class="sw-filter-btn" data-filter="playlists" onclick="swSetFilter('playlists')">
<i class="fas fa-folder"></i> My Playlists
</button>
</div>
</div>
<div class="sw-playlist-controls">
<button class="sw-btn" onclick="swShowPlaylistModal()">
<i class="fas fa-plus"></i>
<span>Create Playlist</span>
</button>
<button class="sw-btn" onclick="swShowManageModal()" id="swManagePlaylistBtn" style="display: none;">
<i class="fas fa-edit"></i>
<span>Manage Playlist</span>
</button>
<button class="sw-btn" onclick="swDeletePlaylist()">
<i class="fas fa-trash"></i>
<span>Delete Playlist</span>
</button>
</div>
<!-- File Upload Section -->
<div class="sw-upload-section">
<div class="sw-upload-header">
<i class="fas fa-cloud-upload-alt"></i>
Add New Songs
</div>
<div class="sw-upload-area" id="swUploadArea">
<div class="sw-upload-content">
<i class="fas fa-cloud-upload-alt sw-upload-icon"></i>
<p class="sw-upload-text">Drag & drop audio files here</p>
<p class="sw-upload-subtext">or</p>
<button class="sw-btn sw-upload-btn" onclick="document.getElementById('swFileInput').click()">
<i class="fas fa-folder-open"></i>
<span>Browse Files</span>
</button>
<input type="file" id="swFileInput" accept=".mp3,.wav,.m4a,.flac,.ogg,.aac" multiple style="display: none;">
<p class="sw-upload-info">Supported formats: MP3, WAV, M4A, FLAC, OGG, AAC (Max 50MB each)</p>
</div>
</div>
<div class="sw-upload-progress" id="swUploadProgress" style="display: none;">
<div class="sw-progress-item">
<div class="sw-progress-info">
<span id="swUploadFileName">Uploading...</span>
<span id="swUploadPercent">0%</span>
</div>
<div class="sw-progress-bar-upload">
<div class="sw-progress-fill-upload" id="swUploadProgressFill"></div>
</div>
</div>
</div>
</div>
<div class="sw-tracklist">
<div class="sw-tracklist-header">
<i class="fas fa-list"></i>
Track List
</div>
<!-- Rendered by the JavaScript swUpdateTracks() -->
</div>
<!-- Playlist View (hidden by default) -->
<div class="sw-playlist-view" id="swPlaylistView" style="display: none;">
<div class="sw-playlist-view-header">
<i class="fas fa-folder"></i>
<span>My Playlists</span>
</div>
<div class="sw-playlist-grid" id="swPlaylistGrid">
<!-- Playlist cards will be populated here -->
</div>
</div>
<!-- Queue View -->
<div class="sw-queue-section">
<div class="sw-queue-header">
<div class="sw-queue-title">
<i class="fas fa-list-ol"></i>
<span>Play Queue</span>
<span class="sw-queue-count" id="swQueueCount">(0)</span>
</div>
<div class="sw-queue-controls">
<button class="sw-queue-btn" id="swToggleQueueBtn" onclick="swToggleQueue()">
<i class="fas fa-eye"></i>
<span>Show Queue</span>
</button>
<button class="sw-queue-btn" onclick="swClearQueue()">
<i class="fas fa-trash"></i>
<span>Clear</span>
</button>
</div>
</div>
<div class="sw-queue-content" id="swQueueContent" style="display: none;">
<div class="sw-queue-list" id="swQueueList">
<!-- Queue items will be populated here -->
</div>
</div>
</div>
</div>
</div>
<div class="sw-modal" id="swPlaylistModal">
<div class="sw-modal-content">
<h2><i class="fas fa-plus-circle"></i> Create New Playlist</h2>
<input type="text" class="sw-input" id="swPlaylistName" placeholder="Enter playlist name...">
<div class="sw-checkbox-list" id="swSongCheckboxes"></div>
<div class="sw-modal-actions">
<button class="sw-btn" onclick="swCloseModal()">
<i class="fas fa-times"></i>
<span>Cancel</span>
</button>
<button class="sw-btn" onclick="swCreatePlaylist()">
<i class="fas fa-save"></i>
<span>Save Playlist</span>
</button>
</div>
</div>
</div>
<!-- Advanced Playlist Management Modal -->
<div class="sw-modal" id="swPlaylistManageModal">
<div class="sw-modal-content sw-modal-large">
<h2><i class="fas fa-edit"></i> Manage Playlist: <span id="swManagePlaylistName"></span></h2>
<!-- Playlist Name Editor -->
<div class="sw-playlist-editor">
<label class="sw-label">Playlist Name:</label>
<div class="sw-name-editor">
<input type="text" class="sw-input" id="swEditPlaylistName" placeholder="Enter new playlist name...">
<button class="sw-btn sw-btn-small" onclick="swRenamePlaylist()">
<i class="fas fa-check"></i>
<span>Rename</span>
</button>
</div>
</div>
<!-- Add Songs Section -->
<div class="sw-manage-section">
<h3><i class="fas fa-plus"></i> Add Songs</h3>
<div class="sw-available-songs" id="swAvailableSongs">
<!-- Available songs will be populated here -->
</div>
</div>
<!-- Current Playlist Songs -->
<div class="sw-manage-section">
<h3><i class="fas fa-list"></i> Current Songs <span class="sw-song-count" id="swCurrentSongCount">(0)</span></h3>
<div class="sw-current-songs" id="swCurrentSongs">
<!-- Current playlist songs will be populated here -->
</div>
</div>
<div class="sw-modal-actions">
<button class="sw-btn" onclick="swCloseManageModal()">
<i class="fas fa-times"></i>
<span>Close</span>
</button>
</div>
</div>
</div>
<script>
const swAudio = document.getElementById('swAudio');
swAudio.addEventListener('ended', swNext);
const swTracks = {{ audio_files|tojson|safe }};
let swCurrentIndex = 0;
let swPlaylists = {};
let swCurrentPlaylist = 'all';
let swIsLooping = false;
let swIsShuffling = false;
let swShuffledIndices = [];
let swSearchQuery = '';
let swCurrentFilter = 'all';
let swFilteredTracks = [];
let swFilteredPlaylists = [];
let swPlaylistsCache = null;
let swPlayQueue = [];
let swQueueVisible = false;
// API functions
async function apiGetPlaylists() {
try {
const response = await fetch('/api/playlists');
const playlists = await response.json();
return playlists;
} catch (error) {
console.error('Error fetching playlists:', error);
return [];
}
}
async function apiCreatePlaylist(name, songs) {
try {
const response = await fetch('/api/playlists', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, songs })
});
return await response.json();
} catch (error) {
console.error('Error creating playlist:', error);
throw error;
}
}
async function apiDeletePlaylist(playlistId) {
try {
const response = await fetch(`/api/playlists/${playlistId}`, {
method: 'DELETE'
});
return await response.json();
} catch (error) {
console.error('Error deleting playlist:', error);
throw error;
}
}
async function apiGetPlaylistSongs(playlistId) {
try {
const response = await fetch(`/api/playlists/${playlistId}/songs`);
const songs = await response.json();
return songs.map(song => song.song_name);
} catch (error) {
console.error('Error fetching playlist songs:', error);
return [];
}
}
async function apiGetPreference(key) {
try {
const response = await fetch(`/api/preferences/${key}`);
const data = await response.json();
return data.value;
} catch (error) {
console.error('Error fetching preference:', error);
return null;
}
}
async function apiSetPreference(key, value) {
try {
const response = await fetch(`/api/preferences/${key}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ value })
});
return await response.json();
} catch (error) {
console.error('Error setting preference:', error);
throw error;
}
}
async function swPlay(file) {
try {
// Initialize queue if empty
if (swPlayQueue.length === 0) {
await swInitializeQueue();
}
// Find the index in the queue
const queueIdx = swPlayQueue.indexOf(file);
if (queueIdx >= 0) {
// File is in queue, use queue index
swCurrentIndex = queueIdx;
} else {
// File not in queue, try to find in current tracks and add to queue
const currentPlaylistTracks = await swGetCurrentTracks();
const playlistIdx = currentPlaylistTracks.indexOf(file);
if (playlistIdx < 0) {
console.error("File not found in current playlist:", file);
return;
}
// Re-initialize queue with current playlist and find the index
swPlayQueue = [...currentPlaylistTracks];
swCurrentIndex = playlistIdx;
}
swAudio.src = `/audio/${file}`;
await swAudio.play();
document.getElementById('swCurrentSong').textContent = file;
await swUpdateTracks();
swUpdateQueueDisplay();
} catch (error) {
console.error('Error playing file:', error);
}
}
async function swLoadPlaylists() {
const select = document.getElementById('swPlaylistSelect');
select.innerHTML = '<option value="all">🎵 All Songs</option>';
try {
swPlaylistsCache = await apiGetPlaylists(); // Refresh cache
swPlaylistsCache.forEach(playlist => {
select.innerHTML += `<option value="${playlist.id}">🎶 ${playlist.name}</option>`;
});
} catch (error) {
console.error('Error loading playlists:', error);
}
}
function swShowPlaylistModal() {
const modal = document.getElementById('swPlaylistModal');
const checkboxes = document.getElementById('swSongCheckboxes');
checkboxes.innerHTML = swTracks.map(song =>
`<div class="sw-checkbox-item">
<input type="checkbox" id="${song}" value="${song}">
<label for="${song}">${song}</label>
</div>`
).join('');
modal.style.display = 'flex';
}
function swCloseModal() {
document.getElementById('swPlaylistModal').style.display = 'none';
}
async function swCreatePlaylist() {
const name = document.getElementById('swPlaylistName').value;
if (!name) return alert('Please enter playlist name');
const songs = Array.from(document.querySelectorAll('#swSongCheckboxes input:checked'))
.map(cb => cb.value);
if (!songs.length) return alert('Select at least one song');
try {
await apiCreatePlaylist(name, songs);
await swLoadPlaylists();
swCloseModal();
document.getElementById('swPlaylistName').value = '';
} catch (error) {
alert('Error creating playlist: ' + error.message);
}
}
async function swDeletePlaylist() {
if (swCurrentPlaylist === 'all') return;
try {
await apiDeletePlaylist(swCurrentPlaylist);
swCurrentPlaylist = 'all';
await swLoadPlaylists();
await swChangePlaylist();
} catch (error) {
alert('Error deleting playlist: ' + error.message);
}
}
async function swGetCurrentTracks() {
if (swCurrentPlaylist === 'all') {
return swTracks;
} else {
return await apiGetPlaylistSongs(swCurrentPlaylist);
}
}
async function swUpdateTracks() {
try {
// If search is active, use search-filtered results
if (swSearchQuery || swCurrentFilter !== 'all') {
await swUpdateTracksWithFilter();
return;
}
const tracks = await swGetCurrentTracks();
const trackList = document.querySelector('.sw-tracklist');
const header = trackList.querySelector('.sw-tracklist-header');
const tracksHtml = tracks.map((file, i) => {
const escapedFile = file.replace(/'/g, "\\'").replace(/"/g, '\\"');
return `<div class="sw-track ${i === swCurrentIndex ? 'active' : ''}" onclick="swPlay('${escapedFile}')">
<i class="fas fa-music" style="margin-right: 8px; color: var(--text-muted);"></i>
${file}
</div>`;
}).join('');
trackList.innerHTML = header.outerHTML + tracksHtml;
} catch (error) {
console.error('Error updating tracks:', error);
}
}
// Custom Audio Player Controls
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
function initializeCustomPlayer() {
const audio = swAudio;
const progressBar = document.getElementById('swProgressBar');
const progressFill = document.getElementById('swProgressFill');
const progressHandle = document.getElementById('swProgressHandle');
const currentTimeDisplay = document.getElementById('swCurrentTime');
const durationDisplay = document.getElementById('swDuration');
const volumeSlider = document.getElementById('swVolumeSlider');
const volumeValue = document.getElementById('swVolumeValue');
const speedSelect = document.getElementById('swSpeedSelect');
// Set initial volume
audio.volume = 0.7;
// Update progress bar
audio.addEventListener('timeupdate', () => {
if (audio.duration) {
const progress = (audio.currentTime / audio.duration) * 100;
progressFill.style.width = progress + '%';
progressHandle.style.left = progress + '%';
currentTimeDisplay.textContent = formatTime(audio.currentTime);
}
});
// Update duration when metadata loads
audio.addEventListener('loadedmetadata', () => {
durationDisplay.textContent = formatTime(audio.duration);
});
// Seek functionality
let isDragging = false;
function seek(e) {
if (audio.duration) {
const rect = progressBar.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const percentage = clickX / rect.width;
const newTime = percentage * audio.duration;
audio.currentTime = Math.max(0, Math.min(newTime, audio.duration));
}
}
progressBar.addEventListener('click', seek);
progressHandle.addEventListener('mousedown', () => {
isDragging = true;
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
seek(e);
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
// Volume control
volumeSlider.addEventListener('input', (e) => {
const volume = e.target.value / 100;
audio.volume = volume;
volumeValue.textContent = e.target.value + '%';
});
// Speed control
speedSelect.addEventListener('change', (e) => {
audio.playbackRate = parseFloat(e.target.value);
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
switch(e.key) {
case ' ':
e.preventDefault();
if (audio.paused) {
audio.play();
} else {
audio.pause();
}
break;
case 'ArrowLeft':
e.preventDefault();
audio.currentTime = Math.max(0, audio.currentTime - 10);
break;
case 'ArrowRight':
e.preventDefault();
audio.currentTime = Math.min(audio.duration, audio.currentTime + 10);
break;
case 'ArrowUp':
e.preventDefault();
const newVolumeUp = Math.min(1, audio.volume + 0.1);
audio.volume = newVolumeUp;
volumeSlider.value = newVolumeUp * 100;
volumeValue.textContent = Math.round(newVolumeUp * 100) + '%';
break;
case 'ArrowDown':
e.preventDefault();
const newVolumeDown = Math.max(0, audio.volume - 0.1);
audio.volume = newVolumeDown;
volumeSlider.value = newVolumeDown * 100;
volumeValue.textContent = Math.round(newVolumeDown * 100) + '%';
break;
}
});
}
// Initialize the application
async function initializeApp() {
try {
await swLoadPlaylists();
await swUpdateTracks();
// Load preferences from database
const isLooping = await apiGetPreference('is_looping');
const isShuffling = await apiGetPreference('is_shuffling');
swIsLooping = isLooping === 'true';
swIsShuffling = isShuffling === 'true';
updateControlButtons();
initializeCustomPlayer();
initializeFileUpload();
if (swTracks.length > 0) {
await swPlay(swTracks[0]);
} else {
document.getElementById('swCurrentSong').textContent = 'No song selected';
}
const audioElements = document.getElementsByTagName('audio');
for (let audio of audioElements) {
audio.autoplay = false;
audio.pause();
audio.currentTime = 0;
}
} catch (error) {
console.error('Error initializing app:', error);
}
}
document.addEventListener('DOMContentLoaded', initializeApp);
async function swToggleLoop() {
swIsLooping = !swIsLooping;
try {
await apiSetPreference('is_looping', swIsLooping.toString());
updateControlButtons();
} catch (error) {
console.error('Error saving loop preference:', error);
}
}
async function swToggleShuffle() {
swIsShuffling = !swIsShuffling;
try {
await apiSetPreference('is_shuffling', swIsShuffling.toString());
if (swIsShuffling) {
generateShuffledIndices();
}
updateControlButtons();
} catch (error) {
console.error('Error saving shuffle preference:', error);
}
}
function updateControlButtons() {
document.getElementById('swLoopBtn').classList.toggle('active', swIsLooping);
document.getElementById('swShuffleBtn').classList.toggle('active', swIsShuffling);
}
async function generateShuffledIndices() {
try {
// Initialize queue if empty
if (swPlayQueue.length === 0) {
await swInitializeQueue();
}
swShuffledIndices = Array.from({length: swPlayQueue.length}, (_, i) => i);
// Fisher-Yates shuffle
for (let i = swShuffledIndices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[swShuffledIndices[i], swShuffledIndices[j]] = [swShuffledIndices[j], swShuffledIndices[i]];
}
} catch (error) {
console.error('Error generating shuffled indices:', error);
}
}
// Touch gesture support for mobile
let touchStartX = 0;
let touchStartY = 0;
let touchEndX = 0;
let touchEndY = 0;
function handleTouchStart(e) {
touchStartX = e.changedTouches[0].screenX;
touchStartY = e.changedTouches[0].screenY;
}
function handleTouchEnd(e) {
touchEndX = e.changedTouches[0].screenX;
touchEndY = e.changedTouches[0].screenY;
handleSwipe();
}
function handleSwipe() {
const deltaX = touchEndX - touchStartX;
const deltaY = touchEndY - touchStartY;
const minSwipeDistance = 50;
// Only process horizontal swipes (ignore vertical scrolling)
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > minSwipeDistance) {
if (deltaX > 0) {
// Swipe right - previous track
swPrevious();
} else {
// Swipe left - next track
swNext();
}
}
}
// File Upload Functionality
function initializeFileUpload() {
const uploadArea = document.getElementById('swUploadArea');
const fileInput = document.getElementById('swFileInput');
const uploadProgress = document.getElementById('swUploadProgress');
const uploadFileName = document.getElementById('swUploadFileName');
const uploadPercent = document.getElementById('swUploadPercent');
const uploadProgressFill = document.getElementById('swUploadProgressFill');
// Drag and drop events
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('drag-over');
const files = e.dataTransfer.files;
handleFileUpload(files);
});
// File input change event
fileInput.addEventListener('change', (e) => {
const files = e.target.files;
handleFileUpload(files);
});
function handleFileUpload(files) {
if (files.length === 0) return;
// Process files one by one
uploadFiles(Array.from(files), 0);
}
async function uploadFiles(files, index) {
if (index >= files.length) {
// All files uploaded, refresh the track list
await refreshTrackList();
uploadProgress.style.display = 'none';
return;
}
const file = files[index];
const formData = new FormData();
formData.append('file', file);
// Show progress
uploadProgress.style.display = 'block';
uploadFileName.textContent = `Uploading: ${file.name}`;
uploadPercent.textContent = '0%';
uploadProgressFill.style.width = '0%';
try {
const xhr = new XMLHttpRequest();
// Upload progress
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
uploadPercent.textContent = Math.round(percentComplete) + '%';
uploadProgressFill.style.width = percentComplete + '%';
}
});
// Upload complete
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
uploadFileName.innerHTML = `<span class="sw-upload-success">✓ ${file.name} uploaded successfully</span>`;
setTimeout(() => {
// Upload next file
uploadFiles(files, index + 1);
}, 1000);
} else {
const response = JSON.parse(xhr.responseText);
uploadFileName.innerHTML = `<span class="sw-upload-error">✗ ${file.name}: ${response.error}</span>`;
setTimeout(() => {
uploadFiles(files, index + 1);
}, 2000);
}
});
xhr.addEventListener('error', () => {
uploadFileName.innerHTML = `<span class="sw-upload-error">✗ ${file.name}: Upload failed</span>`;
setTimeout(() => {
uploadFiles(files, index + 1);
}, 2000);
});
xhr.open('POST', '/api/upload');
xhr.send(formData);
} catch (error) {
uploadFileName.innerHTML = `<span class="sw-upload-error">✗ ${file.name}: ${error.message}</span>`;
setTimeout(() => {
uploadFiles(files, index + 1);
}, 2000);
}
}
async function refreshTrackList() {
try {
const response = await fetch('/api/refresh-songs');
const data = await response.json();
// Update the global tracks array
swTracks.length = 0;
swTracks.push(...data.songs);
// Refresh the UI
await swUpdateTracks();
} catch (error) {
console.error('Error refreshing track list:', error);
}
}
}
// Advanced Playlist Management
let currentManagePlaylistId = null;
async function swShowManageModal() {
if (swCurrentPlaylist === 'all') return;
currentManagePlaylistId = swCurrentPlaylist;
const modal = document.getElementById('swPlaylistManageModal');
// Get playlist info
const playlists = await apiGetPlaylists();
const playlist = playlists.find(p => p.id == currentManagePlaylistId);
if (!playlist) return;
document.getElementById('swManagePlaylistName').textContent = playlist.name;
document.getElementById('swEditPlaylistName').value = playlist.name;
await loadAvailableSongs();
await loadCurrentPlaylistSongs();
modal.style.display = 'flex';
}
function swCloseManageModal() {
document.getElementById('swPlaylistManageModal').style.display = 'none';
}
async function loadAvailableSongs() {
const availableSongsContainer = document.getElementById('swAvailableSongs');
const currentSongs = await apiGetPlaylistSongs(currentManagePlaylistId);
const allSongs = swTracks.filter(song => !currentSongs.includes(song));
availableSongsContainer.innerHTML = allSongs.map(song => `
<div class="sw-song-item">
<div class="sw-song-info">
<i class="fas fa-music"></i>
<span class="sw-song-title">${song}</span>
</div>
<div class="sw-song-actions">
<button class="sw-action-btn add" onclick="swAddSongToPlaylist('${song.replace(/'/g, "\\'")}')">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
`).join('');
}
async function loadCurrentPlaylistSongs() {
const currentSongsContainer = document.getElementById('swCurrentSongs');
const currentSongs = await apiGetPlaylistSongs(currentManagePlaylistId);
document.getElementById('swCurrentSongCount').textContent = `(${currentSongs.length})`;
currentSongsContainer.innerHTML = currentSongs.map((song, index) => `
<div class="sw-song-item" draggable="true" data-song="${song}" data-index="${index}">
<div class="sw-song-info">
<i class="fas fa-grip-vertical sw-drag-handle"></i>
<i class="fas fa-music"></i>
<span class="sw-song-title">${song}</span>
</div>
<div class="sw-song-actions">
<button class="sw-action-btn remove" onclick="swRemoveSongFromPlaylist('${song.replace(/'/g, "\\'")}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).join('');
// Add drag and drop functionality
initializeDragAndDrop();
}
async function swAddSongToPlaylist(songName) {
try {
const response = await fetch(`/api/playlists/${currentManagePlaylistId}/add-song`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ song_name: songName })
});
if (response.ok) {
await loadAvailableSongs();
await loadCurrentPlaylistSongs();
// Refresh queue if currently viewing this playlist
if (swCurrentPlaylist == currentManagePlaylistId) {
await swInitializeQueue();
}
} else {
const error = await response.json();
alert('Error: ' + error.error);
}
} catch (error) {
console.error('Error adding song to playlist:', error);
alert('Error adding song to playlist');
}
}
async function swRemoveSongFromPlaylist(songName) {
try {
const response = await fetch(`/api/playlists/${currentManagePlaylistId}/remove-song`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ song_name: songName })
});
if (response.ok) {
await loadAvailableSongs();
await loadCurrentPlaylistSongs();
// Refresh main track list if currently viewing this playlist
if (swCurrentPlaylist == currentManagePlaylistId) {
await swUpdateTracks();
await swInitializeQueue();
}
} else {
const error = await response.json();
alert('Error: ' + error.error);
}
} catch (error) {
console.error('Error removing song from playlist:', error);
alert('Error removing song from playlist');
}
}
async function swRenamePlaylist() {
const newName = document.getElementById('swEditPlaylistName').value.trim();
if (!newName) return;
try {
const response = await fetch(`/api/playlists/${currentManagePlaylistId}/rename`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: newName })
});
if (response.ok) {
document.getElementById('swManagePlaylistName').textContent = newName;
await swLoadPlaylists();
} else {
const error = await response.json();
alert('Error: ' + error.error);
}
} catch (error) {
console.error('Error renaming playlist:', error);
alert('Error renaming playlist');
}
}
function initializeDragAndDrop() {
const songItems = document.querySelectorAll('#swCurrentSongs .sw-song-item');
songItems.forEach(item => {
item.addEventListener('dragstart', handleDragStart);
item.addEventListener('dragover', handleDragOver);
item.addEventListener('drop', handleDrop);
item.addEventListener('dragend', handleDragEnd);
});
}
let draggedElement = null;
function handleDragStart(e) {
draggedElement = e.target;
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.target.outerHTML);
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const afterElement = getDragAfterElement(e.currentTarget.parentNode, e.clientY);
if (afterElement == null) {
e.currentTarget.parentNode.appendChild(draggedElement);
} else {
e.currentTarget.parentNode.insertBefore(draggedElement, afterElement);
}
}
function handleDrop(e) {
e.preventDefault();
reorderPlaylistSongs();
}
function handleDragEnd(e) {
e.target.classList.remove('dragging');
draggedElement = null;
}
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.sw-song-item:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
async function reorderPlaylistSongs() {
const songItems = document.querySelectorAll('#swCurrentSongs .sw-song-item');
const songOrder = Array.from(songItems).map(item => item.dataset.song);
try {
const response = await fetch(`/api/playlists/${currentManagePlaylistId}/reorder`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ song_order: songOrder })
});
if (response.ok) {
// Refresh main track list if currently viewing this playlist
if (swCurrentPlaylist == currentManagePlaylistId) {
await swUpdateTracks();
await swInitializeQueue();
}
} else {
const error = await response.json();
console.error('Error reordering playlist:', error);
}
} catch (error) {
console.error('Error reordering playlist:', error);
}
}
// Update playlist dropdown change to show/hide manage button
async function swChangePlaylist() {
swCurrentPlaylist = document.getElementById('swPlaylistSelect').value;
swCurrentIndex = 0;
// Show/hide manage button
const manageBtn = document.getElementById('swManagePlaylistBtn');
if (swCurrentPlaylist === 'all') {
manageBtn.style.display = 'none';
} else {
manageBtn.style.display = 'block';
}
// Reinitialize queue with new playlist
await swInitializeQueue();
await swUpdateTracks();
const currentTracks = await swGetCurrentTracks();
if (currentTracks.length > 0) {
await swPlay(currentTracks[0]);
}
}
// Search and Filter Functions
function swHandleSearch() {
const searchInput = document.getElementById('swSearchInput');
const clearBtn = document.getElementById('swSearchClear');
swSearchQuery = searchInput.value.toLowerCase().trim();
// Show/hide clear button
clearBtn.style.display = swSearchQuery ? 'flex' : 'none';
// Apply search and filter
swApplySearchAndFilter();
}
function swClearSearch() {
const searchInput = document.getElementById('swSearchInput');
const clearBtn = document.getElementById('swSearchClear');
searchInput.value = '';
swSearchQuery = '';
clearBtn.style.display = 'none';
// Apply search and filter
swApplySearchAndFilter();
}
function swSetFilter(filter) {
swCurrentFilter = filter;
// Update filter button states
document.querySelectorAll('.sw-filter-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.filter === filter) {
btn.classList.add('active');
}
});
// Apply search and filter
swApplySearchAndFilter();
}
async function swApplySearchAndFilter() {
const trackList = document.querySelector('.sw-tracklist');
const playlistSelect = document.getElementById('swPlaylistSelect');
// Get all tracks and playlists (cache to avoid repeated API calls)
const allTracks = swTracks;
if (!swPlaylistsCache) {
swPlaylistsCache = await apiGetPlaylists();
}
const allPlaylists = swPlaylistsCache;
// Filter tracks based on search query
swFilteredTracks = swSearchQuery ?
allTracks.filter(track => track.toLowerCase().includes(swSearchQuery)) :
allTracks;
// Filter playlists based on search query
swFilteredPlaylists = swSearchQuery ?
allPlaylists.filter(playlist => playlist.name.toLowerCase().includes(swSearchQuery)) :
allPlaylists;
// Update playlist dropdown based on filter
if (swCurrentFilter === 'all' || swCurrentFilter === 'playlists') {
playlistSelect.innerHTML = '<option value="all">🎵 All Songs</option>';
swFilteredPlaylists.forEach(playlist => {
playlistSelect.innerHTML += `<option value="${playlist.id}">🎶 ${playlist.name}</option>`;
});
} else {
playlistSelect.innerHTML = '<option value="all">🎵 All Songs</option>';
allPlaylists.forEach(playlist => {
playlistSelect.innerHTML += `<option value="${playlist.id}">🎶 ${playlist.name}</option>`;
});
}
// Update track list based on filter
if (swCurrentFilter === 'all' || swCurrentFilter === 'songs') {
trackList.style.display = 'block'; // Ensure track list is visible
await swUpdateTracks();
swHidePlaylistView();
} else if (swCurrentFilter === 'playlists') {
// Show playlist view when showing only playlists
trackList.style.display = 'none';
swShowPlaylistView();
}
}
async function swUpdateTracksWithFilter() {
const trackList = document.querySelector('.sw-tracklist');
const header = trackList.querySelector('.sw-tracklist-header');
let currentTracks;
if (swCurrentPlaylist === 'all') {
currentTracks = swFilteredTracks;
} else {
const playlistSongs = await apiGetPlaylistSongs(swCurrentPlaylist);
// Filter playlist songs based on search query
currentTracks = swSearchQuery ?
playlistSongs.filter(track => track.toLowerCase().includes(swSearchQuery)) :
playlistSongs;
}
const tracksHtml = currentTracks.map((file, i) => {
const escapedFile = file.replace(/'/g, "\\'").replace(/"/g, '\\"');
return `<div class="sw-track ${i === swCurrentIndex ? 'active' : ''}" onclick="swPlay('${escapedFile}')">
<i class="fas fa-music" style="margin-right: 8px; color: var(--text-muted);"></i>
${file}
</div>`;
}).join('');
trackList.innerHTML = header.outerHTML + tracksHtml;
}
// Playlist View Functions
function swShowPlaylistView() {
const playlistView = document.getElementById('swPlaylistView');
playlistView.style.display = 'block';
swPopulatePlaylistGrid();
}
function swHidePlaylistView() {
const playlistView = document.getElementById('swPlaylistView');
playlistView.style.display = 'none';
}
async function swPopulatePlaylistGrid() {
const playlistGrid = document.getElementById('swPlaylistGrid');
if (!swPlaylistsCache) {
swPlaylistsCache = await apiGetPlaylists();
}
const playlists = swSearchQuery ?
swPlaylistsCache.filter(playlist => playlist.name.toLowerCase().includes(swSearchQuery)) :
swPlaylistsCache;
if (playlists.length === 0) {
playlistGrid.innerHTML = `
<div class="sw-empty-state">
<i class="fas fa-folder-open" style="font-size: 3rem; color: var(--text-muted); margin-bottom: 16px;"></i>
<p style="color: var(--text-muted);">No playlists found</p>
<button class="sw-btn" onclick="swShowPlaylistModal()">
<i class="fas fa-plus"></i>
<span>Create Your First Playlist</span>
</button>
</div>
`;
return;
}
playlistGrid.innerHTML = playlists.map(playlist => `
<div class="sw-playlist-card" onclick="swSelectPlaylistFromCard(${playlist.id})">
<div class="sw-playlist-card-header">
<div class="sw-playlist-card-icon">
<i class="fas fa-music"></i>
</div>
<div class="sw-playlist-card-title">${playlist.name}</div>
</div>
<div class="sw-playlist-card-info">
Created: ${new Date(playlist.created_at).toLocaleDateString()}
</div>
<div class="sw-playlist-card-actions">
<button class="sw-playlist-card-btn" onclick="event.stopPropagation(); swSelectPlaylistFromCard(${playlist.id})">
<i class="fas fa-play"></i>
Play
</button>
<button class="sw-playlist-card-btn" onclick="event.stopPropagation(); swManagePlaylistFromCard(${playlist.id})">
<i class="fas fa-edit"></i>
Edit
</button>
<button class="sw-playlist-card-btn" onclick="event.stopPropagation(); swDeletePlaylistFromCard(${playlist.id})">
<i class="fas fa-trash"></i>
Delete
</button>
</div>
</div>
`).join('');
}
async function swSelectPlaylistFromCard(playlistId) {
const playlistSelect = document.getElementById('swPlaylistSelect');
playlistSelect.value = playlistId;
swCurrentPlaylist = playlistId;
swCurrentIndex = 0;
if (swIsShuffling) {
generateShuffledIndices();
}
// Switch back to "All" filter to show the tracks
swSetFilter('all');
const currentTracks = await swGetCurrentTracks();
if (currentTracks.length > 0) {
swPlay(currentTracks[0]);
}
}
function swManagePlaylistFromCard(playlistId) {
const playlistSelect = document.getElementById('swPlaylistSelect');
playlistSelect.value = playlistId;
swCurrentPlaylist = playlistId;
swShowManageModal();
}
async function swDeletePlaylistFromCard(playlistId) {
if (confirm('Are you sure you want to delete this playlist?')) {
try {
await apiDeletePlaylist(playlistId);
swPlaylistsCache = null; // Clear cache
await swLoadPlaylists();
swPopulatePlaylistGrid();
if (swCurrentPlaylist == playlistId) {
swCurrentPlaylist = 'all';
await swChangePlaylist();
}
} catch (error) {
alert('Error deleting playlist: ' + error.message);
}
}
}
// Queue Management Functions
function swToggleQueue() {
const queueContent = document.getElementById('swQueueContent');
const toggleBtn = document.getElementById('swToggleQueueBtn');
swQueueVisible = !swQueueVisible;
if (swQueueVisible) {
queueContent.style.display = 'block';
toggleBtn.innerHTML = '<i class="fas fa-eye-slash"></i><span>Hide Queue</span>';
swUpdateQueue();
} else {
queueContent.style.display = 'none';
toggleBtn.innerHTML = '<i class="fas fa-eye"></i><span>Show Queue</span>';
}
}
async function swInitializeQueue() {
const currentTracks = await swGetCurrentTracks();
swPlayQueue = [...currentTracks];
// Regenerate shuffle indices if shuffling is enabled
if (swIsShuffling) {
await generateShuffledIndices();
}
swUpdateQueueDisplay();
}
function swUpdateQueue() {
swUpdateQueueDisplay();
}
function swUpdateQueueDisplay() {
const queueList = document.getElementById('swQueueList');
const queueCount = document.getElementById('swQueueCount');
queueCount.textContent = `(${swPlayQueue.length})`;
if (swPlayQueue.length === 0) {
queueList.innerHTML = `
<div class="sw-queue-empty">
<i class="fas fa-list-ol"></i>
<p>Queue is empty</p>
<p style="font-size: 0.9rem; margin-top: 8px;">Songs will appear here when you start playing</p>
</div>
`;
return;
}
queueList.innerHTML = swPlayQueue.map((track, index) => `
<div class="sw-queue-item ${index === swCurrentIndex ? 'current' : ''}"
data-index="${index}"
draggable="true"
onclick="swPlayFromQueue(${index})">
<div class="sw-queue-drag-handle">
<i class="fas fa-grip-vertical"></i>
</div>
<div class="sw-queue-item-number">${index + 1}</div>
<div class="sw-queue-item-info">
<div class="sw-queue-item-icon">
<i class="fas fa-${index === swCurrentIndex ? 'play' : 'music'}"></i>
</div>
<div class="sw-queue-item-name">${track}</div>
</div>
<div class="sw-queue-item-actions">
<button class="sw-queue-item-btn" onclick="event.stopPropagation(); swRemoveFromQueue(${index})" title="Remove from queue">
<i class="fas fa-times"></i>
</button>
</div>
</div>
`).join('');
// Add drag and drop functionality
swInitializeQueueDragAndDrop();
}
async function swPlayFromQueue(index) {
swCurrentIndex = index;
await swPlay(swPlayQueue[index]);
swUpdateQueueDisplay();
}
function swRemoveFromQueue(index) {
swPlayQueue.splice(index, 1);
// Adjust current index if necessary
if (index < swCurrentIndex) {
swCurrentIndex--;
} else if (index === swCurrentIndex && swCurrentIndex >= swPlayQueue.length) {
swCurrentIndex = Math.max(0, swPlayQueue.length - 1);
}
swUpdateQueueDisplay();
}
function swClearQueue() {
if (confirm('Are you sure you want to clear the entire queue?')) {
swPlayQueue = [];
swCurrentIndex = 0;
swUpdateQueueDisplay();
}
}
function swAddToQueue(track) {
if (!swPlayQueue.includes(track)) {
swPlayQueue.push(track);
swUpdateQueueDisplay();
}
}
function swInitializeQueueDragAndDrop() {
const queueItems = document.querySelectorAll('.sw-queue-item');
queueItems.forEach(item => {
item.addEventListener('dragstart', handleQueueDragStart);
item.addEventListener('dragover', handleQueueDragOver);
item.addEventListener('drop', handleQueueDrop);
item.addEventListener('dragend', handleQueueDragEnd);
});
}
let swDraggedQueueItem = null;
function handleQueueDragStart(e) {
swDraggedQueueItem = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.outerHTML);
}
function handleQueueDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
function handleQueueDrop(e) {
e.preventDefault();
if (this !== swDraggedQueueItem) {
const draggedIndex = parseInt(swDraggedQueueItem.dataset.index);
const targetIndex = parseInt(this.dataset.index);
// Reorder the queue
const draggedTrack = swPlayQueue[draggedIndex];
swPlayQueue.splice(draggedIndex, 1);
swPlayQueue.splice(targetIndex, 0, draggedTrack);
// Update current index
if (draggedIndex === swCurrentIndex) {
swCurrentIndex = targetIndex;
} else if (draggedIndex < swCurrentIndex && targetIndex >= swCurrentIndex) {
swCurrentIndex--;
} else if (draggedIndex > swCurrentIndex && targetIndex <= swCurrentIndex) {
swCurrentIndex++;
}
swUpdateQueueDisplay();
}
}
function handleQueueDragEnd(e) {
this.classList.remove('dragging');
swDraggedQueueItem = null;
}
// Update existing functions to work with queue
async function swNext() {
try {
if (swPlayQueue.length === 0) {
await swInitializeQueue();
}
if (swIsShuffling && swShuffledIndices.length > 0) {
const currentShuffleIndex = swShuffledIndices.indexOf(swCurrentIndex);
if (currentShuffleIndex < swShuffledIndices.length - 1) {
swCurrentIndex = swShuffledIndices[currentShuffleIndex + 1];
} else if (swIsLooping) {
swCurrentIndex = swShuffledIndices[0];
}
} else {
if (swCurrentIndex < swPlayQueue.length - 1) {
swCurrentIndex++;
} else if (swIsLooping) {
swCurrentIndex = 0;
}
}
if (swCurrentIndex < swPlayQueue.length) {
await swPlay(swPlayQueue[swCurrentIndex]);
swUpdateQueueDisplay();
}
} catch (error) {
console.error('Error in swNext:', error);
}
}
async function swPrevious() {
try {
if (swPlayQueue.length === 0) {
await swInitializeQueue();
}
if (swIsShuffling && swShuffledIndices.length > 0) {
const currentShuffleIndex = swShuffledIndices.indexOf(swCurrentIndex);
if (currentShuffleIndex > 0) {
swCurrentIndex = swShuffledIndices[currentShuffleIndex - 1];
} else if (swIsLooping) {
swCurrentIndex = swShuffledIndices[swShuffledIndices.length - 1];
}
} else {
if (swCurrentIndex > 0) {
swCurrentIndex--;
} else if (swIsLooping) {
swCurrentIndex = swPlayQueue.length - 1;
}
}
if (swCurrentIndex >= 0 && swCurrentIndex < swPlayQueue.length) {
await swPlay(swPlayQueue[swCurrentIndex]);
swUpdateQueueDisplay();
}
} catch (error) {
console.error('Error in swPrevious:', error);
}
}
// Add touch event listeners for swipe gestures
document.addEventListener('DOMContentLoaded', function() {
const player = document.querySelector('.sw-player');
player.addEventListener('touchstart', handleTouchStart, false);
player.addEventListener('touchend', handleTouchEnd, false);
// Initialize queue
swInitializeQueue();
});
</script>
</body>
</html>