Initial commit - SoundWave v1.0
- Full PWA support with offline capabilities - Comprehensive search across songs, playlists, and channels - Offline playlist manager with download tracking - Pre-built frontend for zero-build deployment - Docker-based deployment with docker compose - Material-UI dark theme interface - YouTube audio download and management - Multi-user authentication support
11
frontend/public/avatars/preset_1.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background circle -->
|
||||
<circle cx="100" cy="100" r="100" fill="#6366F1"/>
|
||||
|
||||
<!-- Music note -->
|
||||
<g fill="white">
|
||||
<circle cx="90" cy="140" r="15"/>
|
||||
<rect x="102" y="90" width="8" height="65" rx="4"/>
|
||||
<path d="M 110 90 Q 130 85 135 75 Q 138 68 135 65 Q 132 62 125 65 L 110 72 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 406 B |
11
frontend/public/avatars/preset_2.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background circle -->
|
||||
<circle cx="100" cy="100" r="100" fill="#EC4899"/>
|
||||
|
||||
<!-- Headphones -->
|
||||
<g fill="white" stroke="white" stroke-width="4">
|
||||
<path d="M 60 100 Q 60 50 100 50 Q 140 50 140 100" fill="none"/>
|
||||
<rect x="50" y="95" width="20" height="35" rx="5"/>
|
||||
<rect x="130" y="95" width="20" height="35" rx="5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 442 B |
12
frontend/public/avatars/preset_3.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background circle -->
|
||||
<circle cx="100" cy="100" r="100" fill="#10B981"/>
|
||||
|
||||
<!-- Microphone -->
|
||||
<g fill="white">
|
||||
<rect x="85" y="60" width="30" height="50" rx="15"/>
|
||||
<path d="M 70 100 Q 70 130 100 130 Q 130 130 130 100" fill="none" stroke="white" stroke-width="6"/>
|
||||
<rect x="95" y="130" width="10" height="30"/>
|
||||
<rect x="75" y="155" width="50" height="8" rx="4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 495 B |
12
frontend/public/avatars/preset_4.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background circle -->
|
||||
<circle cx="100" cy="100" r="100" fill="#F59E0B"/>
|
||||
|
||||
<!-- Vinyl record -->
|
||||
<g>
|
||||
<circle cx="100" cy="100" r="60" fill="white"/>
|
||||
<circle cx="100" cy="100" r="50" fill="#1F2937"/>
|
||||
<circle cx="100" cy="100" r="15" fill="white"/>
|
||||
<circle cx="100" cy="100" r="8" fill="#1F2937"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 428 B |
15
frontend/public/avatars/preset_5.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background circle -->
|
||||
<circle cx="100" cy="100" r="100" fill="#8B5CF6"/>
|
||||
|
||||
<!-- Waveform -->
|
||||
<g fill="white">
|
||||
<rect x="50" y="90" width="8" height="20" rx="4"/>
|
||||
<rect x="65" y="70" width="8" height="60" rx="4"/>
|
||||
<rect x="80" y="50" width="8" height="100" rx="4"/>
|
||||
<rect x="95" y="80" width="8" height="40" rx="4"/>
|
||||
<rect x="110" y="60" width="8" height="80" rx="4"/>
|
||||
<rect x="125" y="75" width="8" height="50" rx="4"/>
|
||||
<rect x="140" y="85" width="8" height="30" rx="4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 615 B |
BIN
frontend/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
271
frontend/public/icon-preview.html
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SoundWave PWA Icons Preview</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 40px 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 40px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 30px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.icon-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 15px;
|
||||
transition: all 0.3s ease;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.icon-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
|
||||
border-color: #1976d2;
|
||||
}
|
||||
.icon-wrapper {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 15px;
|
||||
display: inline-block;
|
||||
}
|
||||
.icon-card img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
.icon-label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.icon-size {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 1.8em;
|
||||
color: #333;
|
||||
margin: 40px 0 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 3px solid #1976d2;
|
||||
}
|
||||
.special-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75em;
|
||||
margin-top: 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.maskable {
|
||||
background: #ff9800;
|
||||
}
|
||||
.apple {
|
||||
background: #000;
|
||||
}
|
||||
.stats {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
.stats h3 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.stat-item {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎵 SoundWave</h1>
|
||||
<p class="subtitle">PWA Icon Set - All Sizes Generated</p>
|
||||
|
||||
<h2 class="section-title">Standard Icons (Any Purpose)</h2>
|
||||
<div class="grid">
|
||||
<div class="icon-card">
|
||||
<div class="icon-wrapper">
|
||||
<img src="/img/icons/icon-72x72.png" alt="72x72" width="72" height="72">
|
||||
</div>
|
||||
<div class="icon-label">Small</div>
|
||||
<div class="icon-size">72×72 pixels</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<div class="icon-wrapper">
|
||||
<img src="/img/icons/icon-96x96.png" alt="96x96" width="96" height="96">
|
||||
</div>
|
||||
<div class="icon-label">Medium</div>
|
||||
<div class="icon-size">96×96 pixels</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<div class="icon-wrapper">
|
||||
<img src="/img/icons/icon-128x128.png" alt="128x128" width="128" height="128">
|
||||
</div>
|
||||
<div class="icon-label">Large</div>
|
||||
<div class="icon-size">128×128 pixels</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<div class="icon-wrapper">
|
||||
<img src="/img/icons/icon-144x144.png" alt="144x144" width="144" height="144">
|
||||
</div>
|
||||
<div class="icon-label">X-Large</div>
|
||||
<div class="icon-size">144×144 pixels</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<div class="icon-wrapper">
|
||||
<img src="/img/icons/icon-152x152.png" alt="152x152" width="152" height="152">
|
||||
</div>
|
||||
<div class="icon-label">iOS</div>
|
||||
<div class="icon-size">152×152 pixels</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<div class="icon-wrapper">
|
||||
<img src="/img/icons/icon-192x192.png" alt="192x192" width="192" height="192">
|
||||
</div>
|
||||
<div class="icon-label">Standard HD</div>
|
||||
<div class="icon-size">192×192 pixels</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<div class="icon-wrapper">
|
||||
<img src="/img/icons/icon-384x384.png" alt="384x384" width="192" height="192">
|
||||
</div>
|
||||
<div class="icon-label">Retina</div>
|
||||
<div class="icon-size">384×384 pixels</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<div class="icon-wrapper">
|
||||
<img src="/img/icons/icon-512x512.png" alt="512x512" width="192" height="192">
|
||||
</div>
|
||||
<div class="icon-label">Ultra HD</div>
|
||||
<div class="icon-size">512×512 pixels</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Platform-Specific Icons</h2>
|
||||
<div class="special-grid">
|
||||
<div class="icon-card">
|
||||
<div class="icon-wrapper">
|
||||
<img src="/img/icons/icon-192x192-maskable.png" alt="192x192 Maskable" width="192" height="192">
|
||||
</div>
|
||||
<div class="icon-label">Android Maskable</div>
|
||||
<div class="icon-size">192×192 pixels</div>
|
||||
<span class="badge maskable">Maskable</span>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<div class="icon-wrapper">
|
||||
<img src="/img/icons/icon-512x512-maskable.png" alt="512x512 Maskable" width="192" height="192">
|
||||
</div>
|
||||
<div class="icon-label">Android HD Maskable</div>
|
||||
<div class="icon-size">512×512 pixels</div>
|
||||
<span class="badge maskable">Maskable</span>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<div class="icon-wrapper">
|
||||
<img src="/img/icons/apple-touch-icon.png" alt="Apple Touch Icon" width="180" height="180">
|
||||
</div>
|
||||
<div class="icon-label">Apple Touch Icon</div>
|
||||
<div class="icon-size">180×180 pixels</div>
|
||||
<span class="badge apple">iOS</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<h3>📊 Icon Set Statistics</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">11</div>
|
||||
<div class="stat-label">Total Icons</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">8</div>
|
||||
<div class="stat-label">Standard Sizes</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">2</div>
|
||||
<div class="stat-label">Maskable Icons</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">1</div>
|
||||
<div class="stat-label">Apple Icon</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">~500KB</div>
|
||||
<div class="stat-label">Total Size</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">✅</div>
|
||||
<div class="stat-label">PWA Ready</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 40px; color: #666;">
|
||||
<p>✨ All icons generated from official SoundWave logo</p>
|
||||
<p style="margin-top: 10px;">🎯 Optimized for all platforms: Android, iOS, Desktop</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
23
frontend/public/img/GENERATE_ICONS.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# PWA Icon Generation Script
|
||||
# This creates placeholder icons - replace with actual app icons
|
||||
|
||||
# For production, generate proper icons using:
|
||||
# https://www.pwabuilder.com/imageGenerator
|
||||
# or
|
||||
# https://realfavicongenerator.net/
|
||||
|
||||
echo "To generate PWA icons, use one of these tools:"
|
||||
echo "1. PWA Builder Image Generator: https://www.pwabuilder.com/imageGenerator"
|
||||
echo "2. Real Favicon Generator: https://realfavicongenerator.net/"
|
||||
echo ""
|
||||
echo "Required icon sizes:"
|
||||
echo "- 72x72"
|
||||
echo "- 96x96"
|
||||
echo "- 128x128"
|
||||
echo "- 144x144"
|
||||
echo "- 152x152"
|
||||
echo "- 192x192"
|
||||
echo "- 384x384"
|
||||
echo "- 512x512"
|
||||
echo ""
|
||||
echo "Place generated icons in: frontend/public/img/"
|
||||
BIN
frontend/public/img/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/public/img/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
frontend/public/img/icons/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
frontend/public/img/icons/icon-144x144.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
frontend/public/img/icons/icon-152x152.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
frontend/public/img/icons/icon-192x192-maskable.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
frontend/public/img/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
frontend/public/img/icons/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
frontend/public/img/icons/icon-512x512-maskable.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
frontend/public/img/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
frontend/public/img/icons/icon-72x72.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
frontend/public/img/icons/icon-96x96.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
17
frontend/public/img/icons/logo-source.svg
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768">
|
||||
<rect width="768" height="768" fill="#A8D5D8"/>
|
||||
<circle cx="384" cy="330" r="135" fill="none" stroke="#0F4C75" stroke-width="12"/>
|
||||
<circle cx="384" cy="330" r="110" fill="none" stroke="#0F4C75" stroke-width="12"/>
|
||||
<path d="M 385 230 Q 405 240 385 250 Q 405 255 385 270 Q 410 275 385 290" fill="none" stroke="#00C8C8" stroke-width="8" stroke-linecap="round"/>
|
||||
<circle cx="384" cy="330" r="80" fill="none" stroke="#00C8C8" stroke-width="10"/>
|
||||
<path fill="#0F4C75" d="M 350 305 L 350 355 L 410 330 Z"/>
|
||||
<rect x="220" y="310" width="12" height="40" rx="6" fill="#00C8C8"/>
|
||||
<rect x="245" y="295" width="12" height="70" rx="6" fill="#00C8C8"/>
|
||||
<rect x="270" y="285" width="12" height="90" rx="6" fill="#0F4C75"/>
|
||||
<rect x="510" y="310" width="12" height="40" rx="6" fill="#00C8C8"/>
|
||||
<rect x="535" y="295" width="12" height="70" rx="6" fill="#00C8C8"/>
|
||||
<rect x="560" y="285" width="12" height="90" rx="6" fill="#0F4C75"/>
|
||||
<text x="384" y="550" font-family="Arial, sans-serif" font-size="80" font-weight="bold" text-anchor="middle" fill="#0F4C75">sound</text>
|
||||
<text x="384" y="550" font-family="Arial, sans-serif" font-size="80" font-weight="bold" text-anchor="middle" fill="#00C8C8" dx="160">wave</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
39
frontend/public/img/logo-app.svg
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="768" height="768">
|
||||
<!-- Background circle -->
|
||||
<circle cx="384" cy="384" r="384" fill="#A8DADC"/>
|
||||
|
||||
<!-- Sound wave lines - left -->
|
||||
<rect x="190" y="340" width="12" height="60" rx="6" fill="#4ECDC4"/>
|
||||
<rect x="210" y="355" width="12" height="30" rx="6" fill="#4ECDC4"/>
|
||||
<rect x="230" y="345" width="12" height="50" rx="6" fill="#4ECDC4"/>
|
||||
|
||||
<!-- Outer circle - cyan -->
|
||||
<circle cx="384" cy="320" r="110" fill="none" stroke="#4ECDC4" stroke-width="16"/>
|
||||
|
||||
<!-- Middle circle - dark blue -->
|
||||
<circle cx="384" cy="320" r="85" fill="none" stroke="#1D3557" stroke-width="12"/>
|
||||
|
||||
<!-- Inner circle - cyan -->
|
||||
<circle cx="384" cy="320" r="65" fill="#A8DADC" stroke="#4ECDC4" stroke-width="10"/>
|
||||
|
||||
<!-- Play button -->
|
||||
<path d="M 370 300 L 370 340 L 405 320 Z" fill="#1D3557"/>
|
||||
|
||||
<!-- Sound wave lines - right -->
|
||||
<rect x="526" y="345" width="12" height="50" rx="6" fill="#4ECDC4"/>
|
||||
<rect x="546" y="355" width="12" height="30" rx="6" fill="#4ECDC4"/>
|
||||
<rect x="566" y="340" width="12" height="60" rx="6" fill="#4ECDC4"/>
|
||||
|
||||
<!-- Text "soundwave" curved -->
|
||||
<path id="curve" d="M 180 420 Q 384 500 588 420" fill="none"/>
|
||||
<text font-family="Arial, sans-serif" font-size="80" font-weight="700" fill="#1D3557">
|
||||
<textPath href="#curve" startOffset="12%">
|
||||
sound
|
||||
</textPath>
|
||||
</text>
|
||||
<text font-family="Arial, sans-serif" font-size="80" font-weight="700" fill="#4ECDC4">
|
||||
<textPath href="#curve" startOffset="50%">
|
||||
wave
|
||||
</textPath>
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
frontend/public/img/logo-new.png
Normal file
|
|
@ -0,0 +1 @@
|
|||
# This will be replaced with the actual image file
|
||||
BIN
frontend/public/img/logo-temp.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
frontend/public/img/logo.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
39
frontend/public/img/logo.svg
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="768" height="768">
|
||||
<!-- Background circle -->
|
||||
<circle cx="384" cy="384" r="384" fill="#A8DADC"/>
|
||||
|
||||
<!-- Sound wave lines - left -->
|
||||
<rect x="190" y="340" width="12" height="60" rx="6" fill="#4ECDC4"/>
|
||||
<rect x="210" y="355" width="12" height="30" rx="6" fill="#4ECDC4"/>
|
||||
<rect x="230" y="345" width="12" height="50" rx="6" fill="#4ECDC4"/>
|
||||
|
||||
<!-- Outer circle - cyan -->
|
||||
<circle cx="384" cy="320" r="110" fill="none" stroke="#4ECDC4" stroke-width="16"/>
|
||||
|
||||
<!-- Middle circle - dark blue -->
|
||||
<circle cx="384" cy="320" r="85" fill="none" stroke="#1D3557" stroke-width="12"/>
|
||||
|
||||
<!-- Inner circle - cyan -->
|
||||
<circle cx="384" cy="320" r="65" fill="#A8DADC" stroke="#4ECDC4" stroke-width="10"/>
|
||||
|
||||
<!-- Play button -->
|
||||
<path d="M 370 300 L 370 340 L 405 320 Z" fill="#1D3557"/>
|
||||
|
||||
<!-- Sound wave lines - right -->
|
||||
<rect x="526" y="345" width="12" height="50" rx="6" fill="#4ECDC4"/>
|
||||
<rect x="546" y="355" width="12" height="30" rx="6" fill="#4ECDC4"/>
|
||||
<rect x="566" y="340" width="12" height="60" rx="6" fill="#4ECDC4"/>
|
||||
|
||||
<!-- Text "soundwave" curved -->
|
||||
<path id="curve" d="M 180 420 Q 384 500 588 420" fill="none"/>
|
||||
<text font-family="Arial, sans-serif" font-size="80" font-weight="700" fill="#1D3557">
|
||||
<textPath href="#curve" startOffset="12%">
|
||||
sound
|
||||
</textPath>
|
||||
</text>
|
||||
<text font-family="Arial, sans-serif" font-size="80" font-weight="700" fill="#4ECDC4">
|
||||
<textPath href="#curve" startOffset="50%">
|
||||
wave
|
||||
</textPath>
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
135
frontend/public/manifest.json
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
"name": "SoundWave",
|
||||
"short_name": "SoundWave",
|
||||
"description": "Music streaming platform with local file support and offline playback",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#121212",
|
||||
"theme_color": "#1976d2",
|
||||
"orientation": "portrait-primary",
|
||||
"scope": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/img/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/icon-192x192-maskable.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/img/icons/icon-512x512-maskable.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/img/screenshot-wide.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide"
|
||||
},
|
||||
{
|
||||
"src": "/img/screenshot-narrow.png",
|
||||
"sizes": "540x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
],
|
||||
"categories": ["music", "entertainment"],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Home",
|
||||
"short_name": "Home",
|
||||
"description": "Open home page",
|
||||
"url": "/",
|
||||
"icons": [{ "src": "/img/icons/icon-192x192.png", "sizes": "192x192" }]
|
||||
},
|
||||
{
|
||||
"name": "Search",
|
||||
"short_name": "Search",
|
||||
"description": "Search for music",
|
||||
"url": "/search",
|
||||
"icons": [{ "src": "/img/icons/icon-192x192.png", "sizes": "192x192" }]
|
||||
},
|
||||
{
|
||||
"name": "Library",
|
||||
"short_name": "Library",
|
||||
"description": "View your library",
|
||||
"url": "/library",
|
||||
"icons": [{ "src": "/img/icons/icon-192x192.png", "sizes": "192x192" }]
|
||||
},
|
||||
{
|
||||
"name": "Local Files",
|
||||
"short_name": "Local Files",
|
||||
"description": "View local files",
|
||||
"url": "/local-files",
|
||||
"icons": [{ "src": "/img/icons/icon-192x192.png", "sizes": "192x192" }]
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/share",
|
||||
"method": "POST",
|
||||
"enctype": "multipart/form-data",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text",
|
||||
"url": "url",
|
||||
"files": [
|
||||
{
|
||||
"name": "audio",
|
||||
"accept": ["audio/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"prefer_related_applications": false
|
||||
}
|
||||
9
frontend/public/robots.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Disallow admin and API endpoints
|
||||
Disallow: /api/
|
||||
Disallow: /admin/
|
||||
|
||||
# Sitemap
|
||||
Sitemap: https://soundwave.app/sitemap.xml
|
||||
377
frontend/public/service-worker.js
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
const CACHE_NAME = 'soundwave-v1';
|
||||
const API_CACHE_NAME = 'soundwave-api-v1';
|
||||
const AUDIO_CACHE_NAME = 'soundwave-audio-v1';
|
||||
const IMAGE_CACHE_NAME = 'soundwave-images-v1';
|
||||
|
||||
// Assets to cache on install
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/manifest.json',
|
||||
'/favicon.ico',
|
||||
];
|
||||
|
||||
// Install event - cache static assets
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[Service Worker] Installing...');
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
console.log('[Service Worker] Caching static assets');
|
||||
return cache.addAll(STATIC_ASSETS).catch((err) => {
|
||||
console.error('[Service Worker] Failed to cache static assets:', err);
|
||||
});
|
||||
}).then(() => {
|
||||
console.log('[Service Worker] Installed');
|
||||
return self.skipWaiting(); // Activate immediately
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[Service Worker] Activating...');
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (
|
||||
cacheName !== CACHE_NAME &&
|
||||
cacheName !== API_CACHE_NAME &&
|
||||
cacheName !== AUDIO_CACHE_NAME &&
|
||||
cacheName !== IMAGE_CACHE_NAME
|
||||
) {
|
||||
console.log('[Service Worker] Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
}).then(() => {
|
||||
console.log('[Service Worker] Activated');
|
||||
return self.clients.claim(); // Take control immediately
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - implement caching strategies
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip chrome extensions and non-http(s) requests
|
||||
if (!url.protocol.startsWith('http')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API requests - Network first, fallback to cache
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(networkFirstStrategy(request, API_CACHE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
// Audio files - Cache first, fallback to network
|
||||
if (
|
||||
url.pathname.includes('/audio/') ||
|
||||
url.pathname.includes('/media/local_audio/') ||
|
||||
request.destination === 'audio'
|
||||
) {
|
||||
event.respondWith(cacheFirstStrategy(request, AUDIO_CACHE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
// Images - Cache first, fallback to network
|
||||
if (
|
||||
url.pathname.includes('/img/') ||
|
||||
url.pathname.includes('/media/') ||
|
||||
url.pathname.includes('thumbnail') ||
|
||||
url.pathname.includes('cover') ||
|
||||
request.destination === 'image'
|
||||
) {
|
||||
event.respondWith(cacheFirstStrategy(request, IMAGE_CACHE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets (JS, CSS) - Stale while revalidate
|
||||
if (
|
||||
request.destination === 'script' ||
|
||||
request.destination === 'style' ||
|
||||
url.pathname.endsWith('.js') ||
|
||||
url.pathname.endsWith('.css')
|
||||
) {
|
||||
event.respondWith(staleWhileRevalidateStrategy(request, CACHE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
// HTML pages - Network first, fallback to cache
|
||||
if (request.mode === 'navigate' || request.destination === 'document') {
|
||||
event.respondWith(networkFirstStrategy(request, CACHE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
// Default - Network first
|
||||
event.respondWith(networkFirstStrategy(request, CACHE_NAME));
|
||||
});
|
||||
|
||||
// Network first strategy - try network, fallback to cache
|
||||
async function networkFirstStrategy(request, cacheName) {
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Cache successful responses
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
const cache = await caches.open(cacheName);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.log('[Service Worker] Network request failed, trying cache:', request.url);
|
||||
const cachedResponse = await caches.match(request);
|
||||
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Return offline page for navigation requests
|
||||
if (request.mode === 'navigate') {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const offlinePage = await cache.match('/index.html');
|
||||
if (offlinePage) {
|
||||
return offlinePage;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache first strategy - try cache, fallback to network
|
||||
async function cacheFirstStrategy(request, cacheName) {
|
||||
const cachedResponse = await caches.match(request);
|
||||
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Cache successful responses
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
const cache = await caches.open(cacheName);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.error('[Service Worker] Cache and network failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Stale while revalidate - return cache immediately, update in background
|
||||
async function staleWhileRevalidateStrategy(request, cacheName) {
|
||||
const cachedResponse = await caches.match(request);
|
||||
|
||||
const fetchPromise = fetch(request).then((networkResponse) => {
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
const cache = caches.open(cacheName);
|
||||
cache.then((c) => c.put(request, networkResponse.clone()));
|
||||
}
|
||||
return networkResponse;
|
||||
}).catch((error) => {
|
||||
console.log('[Service Worker] Background fetch failed:', error);
|
||||
});
|
||||
|
||||
return cachedResponse || fetchPromise;
|
||||
}
|
||||
|
||||
// Background sync for offline uploads
|
||||
self.addEventListener('sync', (event) => {
|
||||
console.log('[Service Worker] Background sync:', event.tag);
|
||||
|
||||
if (event.tag === 'sync-audio-uploads') {
|
||||
event.waitUntil(syncAudioUploads());
|
||||
}
|
||||
|
||||
if (event.tag === 'sync-favorites') {
|
||||
event.waitUntil(syncFavorites());
|
||||
}
|
||||
});
|
||||
|
||||
async function syncAudioUploads() {
|
||||
console.log('[Service Worker] Syncing audio uploads...');
|
||||
// Implementation for syncing pending uploads when back online
|
||||
// This would read from IndexedDB and upload pending files
|
||||
}
|
||||
|
||||
async function syncFavorites() {
|
||||
console.log('[Service Worker] Syncing favorites...');
|
||||
// Implementation for syncing favorite changes when back online
|
||||
}
|
||||
|
||||
// Push notifications
|
||||
self.addEventListener('push', (event) => {
|
||||
console.log('[Service Worker] Push notification received');
|
||||
|
||||
const data = event.data ? event.data.json() : {};
|
||||
const title = data.title || 'SoundWave';
|
||||
const options = {
|
||||
body: data.body || 'New content available',
|
||||
icon: '/img/icon-192x192.png',
|
||||
badge: '/img/icon-72x72.png',
|
||||
vibrate: [200, 100, 200],
|
||||
data: data.url || '/',
|
||||
actions: [
|
||||
{ action: 'open', title: 'Open' },
|
||||
{ action: 'close', title: 'Close' },
|
||||
],
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title, options)
|
||||
);
|
||||
});
|
||||
|
||||
// Notification click
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
console.log('[Service Worker] Notification clicked');
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'open' || !event.action) {
|
||||
const urlToOpen = event.notification.data || '/';
|
||||
event.waitUntil(
|
||||
clients.openWindow(urlToOpen)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Message handling for cache management
|
||||
self.addEventListener('message', (event) => {
|
||||
console.log('[Service Worker] Message received:', event.data);
|
||||
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'CLEAR_CACHE') {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => caches.delete(cacheName))
|
||||
);
|
||||
}).then(() => {
|
||||
event.ports[0].postMessage({ success: true });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'CACHE_AUDIO') {
|
||||
const { url } = event.data;
|
||||
event.waitUntil(
|
||||
caches.open(AUDIO_CACHE_NAME).then((cache) => {
|
||||
return cache.add(url);
|
||||
}).then(() => {
|
||||
event.ports[0].postMessage({ success: true });
|
||||
}).catch((error) => {
|
||||
event.ports[0].postMessage({ success: false, error: error.message });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Cache playlist for offline access with authentication
|
||||
if (event.data && event.data.type === 'CACHE_PLAYLIST') {
|
||||
const { playlistId, audioUrls } = event.data;
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
try {
|
||||
console.log('[Service Worker] Caching playlist:', playlistId, 'with', audioUrls.length, 'tracks');
|
||||
|
||||
const results = {
|
||||
metadata: false,
|
||||
audioFiles: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
// Cache playlist metadata API response (includes items)
|
||||
try {
|
||||
const apiCache = await caches.open(API_CACHE_NAME);
|
||||
const metadataUrl = `/api/playlist/${playlistId}/?include_items=true`;
|
||||
await apiCache.add(metadataUrl);
|
||||
results.metadata = true;
|
||||
console.log('[Service Worker] Cached playlist metadata');
|
||||
} catch (err) {
|
||||
console.warn('[Service Worker] Failed to cache playlist metadata:', err);
|
||||
}
|
||||
|
||||
// Cache all audio files in playlist with authentication
|
||||
const audioCache = await caches.open(AUDIO_CACHE_NAME);
|
||||
|
||||
for (const url of audioUrls) {
|
||||
try {
|
||||
// Create authenticated request
|
||||
const authRequest = new Request(url, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'audio/*',
|
||||
}
|
||||
});
|
||||
|
||||
const response = await fetch(authRequest);
|
||||
|
||||
if (response.ok) {
|
||||
// Clone and cache the response
|
||||
await audioCache.put(url, response.clone());
|
||||
results.audioFiles.push(url);
|
||||
console.log('[Service Worker] Cached audio:', url);
|
||||
} else {
|
||||
results.failed.push(url);
|
||||
console.warn('[Service Worker] Failed to cache audio (status ' + response.status + '):', url);
|
||||
}
|
||||
} catch (err) {
|
||||
results.failed.push(url);
|
||||
console.warn('[Service Worker] Failed to cache audio:', url, err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Service Worker] Playlist caching complete:', results);
|
||||
event.ports[0].postMessage({
|
||||
success: results.audioFiles.length > 0,
|
||||
metadata: results.metadata,
|
||||
cached: results.audioFiles.length,
|
||||
failed: results.failed.length,
|
||||
details: results
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Service Worker] Playlist caching error:', error);
|
||||
event.ports[0].postMessage({ success: false, error: error.message });
|
||||
}
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
// Remove cached playlist
|
||||
if (event.data && event.data.type === 'REMOVE_PLAYLIST_CACHE') {
|
||||
const { playlistId, audioUrls } = event.data;
|
||||
event.waitUntil(
|
||||
Promise.all([
|
||||
// Remove playlist metadata from cache
|
||||
caches.open(API_CACHE_NAME).then((cache) => {
|
||||
return cache.delete(`/api/playlist/${playlistId}/`);
|
||||
}),
|
||||
// Remove audio files from cache (only if not used by other playlists)
|
||||
caches.open(AUDIO_CACHE_NAME).then((cache) => {
|
||||
return Promise.all(
|
||||
audioUrls.map(url => cache.delete(url))
|
||||
);
|
||||
})
|
||||
]).then(() => {
|
||||
event.ports[0].postMessage({ success: true });
|
||||
}).catch((error) => {
|
||||
event.ports[0].postMessage({ success: false, error: error.message });
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Service Worker] Loaded');
|
||||
33
frontend/public/sitemap.xml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://soundwave.app/</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://soundwave.app/search</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://soundwave.app/library</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://soundwave.app/favorites</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://soundwave.app/local-files</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://soundwave.app/settings</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
</urlset>
|
||||