Initial commit: StreamFlow IPTV platform
10
frontend/public/favicon.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#a855f7;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="32" height="32" rx="7" fill="url(#grad1)"/>
|
||||
<path d="M 12 9 L 12 23 L 23 16 Z" fill="#ffffff" opacity="0.95"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 468 B |
292
frontend/public/icons-preview.html
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>StreamFlow Icons Preview</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.subtitle {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 30px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.icon-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.icon-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.icon-card img {
|
||||
width: 100%;
|
||||
max-width: 150px;
|
||||
height: auto;
|
||||
margin-bottom: 15px;
|
||||
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
.icon-size {
|
||||
font-weight: 700;
|
||||
color: #a855f7;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.icon-type {
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.color-palette {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
margin-bottom: 40px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.color-palette h2 {
|
||||
color: #333;
|
||||
margin-bottom: 25px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.colors {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.color-swatch {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
.color-box {
|
||||
height: 100px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.color-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.color-hex {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.usage-section {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.usage-section h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.usage-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.usage-card {
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
.usage-card img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.usage-info h3 {
|
||||
color: #333;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.usage-info p {
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.gradient-demo {
|
||||
background: linear-gradient(135deg, #a855f7 0%, #3b82f6 100%);
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
margin: 20px 0;
|
||||
box-shadow: 0 8px 24px rgba(168, 85, 247, 0.4);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎬 StreamFlow Logo & Icons</h1>
|
||||
<p class="subtitle">Professional IPTV Application Branding</p>
|
||||
|
||||
<!-- Color Palette -->
|
||||
<div class="color-palette">
|
||||
<h2>🎨 Brand Colors</h2>
|
||||
<div class="colors">
|
||||
<div class="color-swatch">
|
||||
<div class="color-box" style="background: #a855f7;"></div>
|
||||
<div class="color-name">Purple Primary</div>
|
||||
<div class="color-hex">#a855f7</div>
|
||||
</div>
|
||||
<div class="color-swatch">
|
||||
<div class="color-box" style="background: #3b82f6;"></div>
|
||||
<div class="color-name">Blue Secondary</div>
|
||||
<div class="color-hex">#3b82f6</div>
|
||||
</div>
|
||||
<div class="color-swatch">
|
||||
<div class="color-box" style="background: #c084fc;"></div>
|
||||
<div class="color-name">Purple Light</div>
|
||||
<div class="color-hex">#c084fc</div>
|
||||
</div>
|
||||
<div class="color-swatch">
|
||||
<div class="color-box" style="background: #9333ea;"></div>
|
||||
<div class="color-name">Purple Dark</div>
|
||||
<div class="color-hex">#9333ea</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gradient-demo"></div>
|
||||
<p style="text-align: center; color: #666; margin-top: 10px;">
|
||||
<strong>Gradient:</strong> linear-gradient(135deg, #a855f7 0%, #3b82f6 100%)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Icon Sizes -->
|
||||
<h2 style="color: white; text-align: center; margin-bottom: 30px; font-size: 2rem;">📱 PWA Icons (Rounded)</h2>
|
||||
<div class="grid">
|
||||
<div class="icon-card">
|
||||
<img src="icons/icon-72x72.svg" alt="72x72">
|
||||
<div class="icon-size">72×72</div>
|
||||
<div class="icon-type">Mobile Small</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<img src="icons/icon-96x96.svg" alt="96x96">
|
||||
<div class="icon-size">96×96</div>
|
||||
<div class="icon-type">Mobile Medium</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<img src="icons/icon-128x128.svg" alt="128x128">
|
||||
<div class="icon-size">128×128</div>
|
||||
<div class="icon-type">Tablet</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<img src="icons/icon-144x144.svg" alt="144x144">
|
||||
<div class="icon-size">144×144</div>
|
||||
<div class="icon-type">Mobile High-Res</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<img src="icons/icon-152x152.svg" alt="152x152">
|
||||
<div class="icon-size">152×152</div>
|
||||
<div class="icon-type">iPad Touch</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<img src="icons/icon-192x192.svg" alt="192x192">
|
||||
<div class="icon-size">192×192</div>
|
||||
<div class="icon-type">Standard PWA</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<img src="icons/icon-384x384.svg" alt="384x384">
|
||||
<div class="icon-size">384×384</div>
|
||||
<div class="icon-type">High-DPI</div>
|
||||
</div>
|
||||
<div class="icon-card">
|
||||
<img src="icons/icon-512x512.svg" alt="512x512">
|
||||
<div class="icon-size">512×512</div>
|
||||
<div class="icon-type">Desktop / Splash</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Examples -->
|
||||
<div class="usage-section">
|
||||
<h2>🚀 Platform Usage</h2>
|
||||
<div class="usage-grid">
|
||||
<div class="usage-card">
|
||||
<img src="favicon.svg" alt="Favicon">
|
||||
<div class="usage-info">
|
||||
<h3>Browser Tab</h3>
|
||||
<p>favicon.svg (32×32)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usage-card">
|
||||
<img src="icons/icon-192x192.svg" alt="Android">
|
||||
<div class="usage-info">
|
||||
<h3>Android Home Screen</h3>
|
||||
<p>192×192 maskable icon</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usage-card">
|
||||
<img src="icons/icon-192x192.svg" alt="iOS">
|
||||
<div class="usage-info">
|
||||
<h3>iOS Home Screen</h3>
|
||||
<p>Apple touch icon</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usage-card">
|
||||
<img src="icons/icon-512x512.svg" alt="Desktop">
|
||||
<div class="usage-info">
|
||||
<h3>Desktop PWA</h3>
|
||||
<p>512×512 for shortcuts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usage-card">
|
||||
<img src="icons/icon-512x512.svg" alt="Linux">
|
||||
<div class="usage-info">
|
||||
<h3>Linux AppImage</h3>
|
||||
<p>512×512 app icon</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usage-card">
|
||||
<img src="icons/icon-192x192.svg" alt="APK">
|
||||
<div class="usage-info">
|
||||
<h3>Android APK</h3>
|
||||
<p>Adaptive icon resource</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 30px; padding: 20px; background: #f9fafb; border-radius: 12px; border-left: 4px solid #a855f7;">
|
||||
<h3 style="color: #333; margin-bottom: 10px;">✅ All icons feature:</h3>
|
||||
<ul style="color: #666; line-height: 1.8; margin-left: 20px;">
|
||||
<li><strong>Rounded corners</strong> for modern aesthetics (~22% radius)</li>
|
||||
<li><strong>Purple-blue gradient</strong> matching brand identity</li>
|
||||
<li><strong>SVG format</strong> for crisp rendering at any scale</li>
|
||||
<li><strong>Maskable safe zone</strong> for adaptive launcher icons</li>
|
||||
<li><strong>Flow lines</strong> suggesting streaming motion</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
12
frontend/public/icons/icon-128x128.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#a855f7;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="128" height="128" rx="28" fill="url(#grad1)"/>
|
||||
<path d="M 48 35 L 48 93 L 93 64 Z" fill="#ffffff" opacity="0.95"/>
|
||||
<path d="M 64 64 C 64 64, 54 56, 48 50 C 42 44, 38 38, 38 36 L 96 64 C 96 64, 94 68, 88 74 C 82 80, 74 88, 66 93"
|
||||
fill="none" stroke="#ffffff" stroke-width="3" opacity="0.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 686 B |
12
frontend/public/icons/icon-144x144.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#a855f7;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="144" height="144" rx="32" fill="url(#grad1)"/>
|
||||
<path d="M 54 40 L 54 104 L 104 72 Z" fill="#ffffff" opacity="0.95"/>
|
||||
<path d="M 72 72 C 72 72, 60 62, 54 56 C 48 50, 42 42, 42 40 L 108 72 C 108 72, 106 76, 99 83 C 92 90, 83 99, 74 104"
|
||||
fill="none" stroke="#ffffff" stroke-width="3.5" opacity="0.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 694 B |
12
frontend/public/icons/icon-152x152.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="152" height="152" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#a855f7;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="152" height="152" rx="34" fill="url(#grad1)"/>
|
||||
<path d="M 57 42 L 57 110 L 110 76 Z" fill="#ffffff" opacity="0.95"/>
|
||||
<path d="M 76 76 C 76 76, 64 66, 57 59 C 50 52, 44 44, 44 42 L 114 76 C 114 76, 112 80, 105 87 C 98 94, 88 104, 78 110"
|
||||
fill="none" stroke="#ffffff" stroke-width="3.5" opacity="0.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 696 B |
12
frontend/public/icons/icon-192x192.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="192" height="192" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#a855f7;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="192" height="192" rx="42" fill="url(#grad1)"/>
|
||||
<path d="M 72 53 L 72 139 L 139 96 Z" fill="#ffffff" opacity="0.95"/>
|
||||
<path d="M 96 96 C 96 96, 80 83, 72 74 C 64 65, 56 55, 56 53 L 144 96 C 144 96, 141 101, 132 110 C 123 119, 111 131, 98 139"
|
||||
fill="none" stroke="#ffffff" stroke-width="4" opacity="0.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 699 B |
12
frontend/public/icons/icon-384x384.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="384" height="384" viewBox="0 0 384 384" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#a855f7;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="384" height="384" rx="84" fill="url(#grad1)"/>
|
||||
<path d="M 144 106 L 144 278 L 278 192 Z" fill="#ffffff" opacity="0.95"/>
|
||||
<path d="M 192 192 C 192 192, 160 166, 144 148 C 128 130, 112 110, 112 106 L 288 192 C 288 192, 282 202, 264 220 C 246 238, 222 262, 196 278"
|
||||
fill="none" stroke="#ffffff" stroke-width="6" opacity="0.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 720 B |
12
frontend/public/icons/icon-512x512.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#a855f7;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="112" fill="url(#grad1)"/>
|
||||
<path d="M 192 142 L 192 370 L 370 256 Z" fill="#ffffff" opacity="0.95"/>
|
||||
<path d="M 256 256 C 256 256, 214 222, 192 198 C 170 174, 150 146, 150 142 L 384 256 C 384 256, 376 270, 352 294 C 328 318, 296 350, 262 370"
|
||||
fill="none" stroke="#ffffff" stroke-width="8" opacity="0.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 721 B |
12
frontend/public/icons/icon-72x72.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="72" height="72" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#a855f7;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="72" height="72" rx="16" fill="url(#grad1)"/>
|
||||
<path d="M 28 20 L 28 52 L 52 36 Z" fill="#ffffff" opacity="0.95"/>
|
||||
<path d="M 36 36 C 36 36, 30 32, 28 30 C 26 28, 24 26, 24 24 L 54 36 C 54 36, 52 38, 50 40 C 48 42, 44 46, 40 48"
|
||||
fill="none" stroke="#ffffff" stroke-width="2" opacity="0.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 680 B |
12
frontend/public/icons/icon-96x96.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#a855f7;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="96" height="96" rx="21" fill="url(#grad1)"/>
|
||||
<path d="M 36 26 L 36 70 L 70 48 Z" fill="#ffffff" opacity="0.95"/>
|
||||
<path d="M 48 48 C 48 48, 40 42, 36 38 C 32 34, 30 30, 30 28 L 72 48 C 72 48, 70 52, 66 56 C 62 60, 56 66, 50 70"
|
||||
fill="none" stroke="#ffffff" stroke-width="2.5" opacity="0.4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 682 B |
78
frontend/public/manifest.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"name": "StreamFlow IPTV",
|
||||
"short_name": "StreamFlow",
|
||||
"description": "Professional IPTV Streaming with Multi-Screen & Picture-in-Picture",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#121212",
|
||||
"theme_color": "#a855f7",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-72x72.svg",
|
||||
"sizes": "72x72",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-96x96.svg",
|
||||
"sizes": "96x96",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-128x128.svg",
|
||||
"sizes": "128x128",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-144x144.svg",
|
||||
"sizes": "144x144",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-152x152.svg",
|
||||
"sizes": "152x152",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-192x192.svg",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-384x384.svg",
|
||||
"sizes": "384x384",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512x512.svg",
|
||||
"sizes": "512x512",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"categories": ["entertainment", "multimedia", "video"],
|
||||
"screenshots": [],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Live TV",
|
||||
"short_name": "Live",
|
||||
"description": "Watch Live TV channels",
|
||||
"url": "/live-tv",
|
||||
"icons": [{ "src": "/icons/icon-96x96.svg", "sizes": "96x96" }]
|
||||
},
|
||||
{
|
||||
"name": "Movies",
|
||||
"short_name": "Movies",
|
||||
"description": "Browse movies",
|
||||
"url": "/movies",
|
||||
"icons": [{ "src": "/icons/icon-96x96.svg", "sizes": "96x96" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
6
frontend/public/placeholder-channel.png
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="#f5f5f5"/>
|
||||
<text x="50%" y="50%" font-family="Arial" font-size="16" fill="#999" text-anchor="middle" dy=".3em">
|
||||
Channel Logo
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 253 B |
6
frontend/public/placeholder-show.png
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="120" height="80" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="120" height="80" fill="#e0e0e0"/>
|
||||
<text x="50%" y="50%" font-family="Arial" font-size="12" fill="#666" text-anchor="middle" dy=".3em">
|
||||
Show
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 243 B |
178
frontend/public/service-worker.js
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
const CACHE_NAME = 'streamflow-v1';
|
||||
const urlsToCache = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/manifest.json'
|
||||
];
|
||||
|
||||
// Global error handler for service worker (CWE-391 protection)
|
||||
self.addEventListener('error', (event) => {
|
||||
// Log error but don't expose to user
|
||||
console.error('Service Worker error:', event.error);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections in service worker
|
||||
self.addEventListener('unhandledrejection', (event) => {
|
||||
// Log error but don't expose to user
|
||||
console.error('Service Worker unhandled rejection:', event.reason);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Install event - cache static assets
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => cache.addAll(urlsToCache))
|
||||
.then(() => self.skipWaiting())
|
||||
.catch((error) => {
|
||||
// Handle cache errors gracefully
|
||||
console.error('Cache installation error:', error);
|
||||
// Still skip waiting even if caching fails
|
||||
return self.skipWaiting();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
}).then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - serve from cache, fallback to network
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// Skip non-GET requests - let them pass through to network
|
||||
if (event.request.method !== 'GET') {
|
||||
event.respondWith(fetch(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip API requests and streaming URLs
|
||||
if (event.request.url.includes('/api/') ||
|
||||
event.request.url.includes('m3u8') ||
|
||||
event.request.url.includes('ts')) {
|
||||
event.respondWith(fetch(event.request));
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then((response) => {
|
||||
// Cache hit - return response
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return fetch(event.request).then((response) => {
|
||||
// Check if valid response
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Clone the response
|
||||
const responseToCache = response.clone();
|
||||
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => {
|
||||
cache.put(event.request, responseToCache);
|
||||
})
|
||||
.catch((error) => {
|
||||
// Silently handle cache write errors
|
||||
console.error('Cache write error:', error);
|
||||
});
|
||||
|
||||
return response;
|
||||
}).catch((error) => {
|
||||
// Network error - return generic offline response
|
||||
console.error('Fetch error:', error);
|
||||
// Return a basic error response instead of throwing
|
||||
return new Response('Offline', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable'
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
// Handle any cache read errors
|
||||
console.error('Cache read error:', error);
|
||||
// Try to fetch from network as fallback
|
||||
return fetch(event.request).catch(() => {
|
||||
return new Response('Offline', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable'
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle background sync
|
||||
self.addEventListener('sync', (event) => {
|
||||
if (event.tag === 'sync-playlists') {
|
||||
event.waitUntil(syncPlaylists());
|
||||
}
|
||||
});
|
||||
|
||||
// Handle push notifications
|
||||
self.addEventListener('push', (event) => {
|
||||
const data = event.data ? event.data.json() : {};
|
||||
const options = {
|
||||
body: data.body || 'New notification from StreamFlow',
|
||||
icon: '/icons/icon-192x192.svg',
|
||||
badge: '/icons/icon-72x72.svg',
|
||||
vibrate: [200, 100, 200],
|
||||
tag: data.tag || 'streamflow-notification',
|
||||
renotify: true,
|
||||
data: data.data || {},
|
||||
actions: data.actions || [
|
||||
{
|
||||
action: 'open',
|
||||
title: 'Open'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(data.title || 'StreamFlow', options)
|
||||
);
|
||||
});
|
||||
|
||||
// Handle notification clicks
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close();
|
||||
|
||||
const urlToOpen = event.notification.data.url || '/';
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
||||
.then((clientList) => {
|
||||
// Check if there's already a window open
|
||||
for (let i = 0; i < clientList.length; i++) {
|
||||
const client = clientList[i];
|
||||
if (client.url.includes(urlToOpen) && 'focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
// If no window is open, open a new one
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(urlToOpen);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Sync function
|
||||
async function syncPlaylists() {
|
||||
// Implementation for syncing playlists
|
||||
console.log('Syncing playlists...');
|
||||
}
|
||||
8
frontend/public/viewport-fix.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Fix for mobile viewport height with address bar
|
||||
function setVh() {
|
||||
const vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
}
|
||||
setVh();
|
||||
window.addEventListener('resize', setVh);
|
||||
window.addEventListener('orientationchange', setVh);
|
||||