988 lines
35 KiB
HTML
988 lines
35 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>dapp.fm - Decentralized Music Player</title>
|
||
|
|
<style>
|
||
|
|
* {
|
||
|
|
box-sizing: border-box;
|
||
|
|
margin: 0;
|
||
|
|
padding: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
body {
|
||
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||
|
|
background: linear-gradient(135deg, #0f0f1a 0%, #1a0a2e 50%, #0f1a2e 100%);
|
||
|
|
min-height: 100vh;
|
||
|
|
padding: 2rem;
|
||
|
|
color: #e0e0e0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.container {
|
||
|
|
max-width: 900px;
|
||
|
|
margin: 0 auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.logo {
|
||
|
|
text-align: center;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.logo h1 {
|
||
|
|
font-size: 3rem;
|
||
|
|
font-weight: 800;
|
||
|
|
background: linear-gradient(135deg, #ff006e 0%, #8338ec 50%, #3a86ff 100%);
|
||
|
|
-webkit-background-clip: text;
|
||
|
|
-webkit-text-fill-color: transparent;
|
||
|
|
background-clip: text;
|
||
|
|
letter-spacing: -2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.logo .tagline {
|
||
|
|
color: #888;
|
||
|
|
font-size: 1rem;
|
||
|
|
margin-top: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.hero-text {
|
||
|
|
text-align: center;
|
||
|
|
margin: 2rem 0;
|
||
|
|
padding: 1.5rem;
|
||
|
|
background: rgba(255,255,255,0.03);
|
||
|
|
border-radius: 16px;
|
||
|
|
border: 1px solid rgba(255,255,255,0.05);
|
||
|
|
}
|
||
|
|
|
||
|
|
.hero-text p {
|
||
|
|
color: #aaa;
|
||
|
|
font-size: 0.95rem;
|
||
|
|
line-height: 1.6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.hero-text strong {
|
||
|
|
color: #ff006e;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card {
|
||
|
|
background: rgba(255,255,255,0.05);
|
||
|
|
border-radius: 20px;
|
||
|
|
padding: 2rem;
|
||
|
|
margin-bottom: 1.5rem;
|
||
|
|
border: 1px solid rgba(255,255,255,0.08);
|
||
|
|
backdrop-filter: blur(20px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.card h2 {
|
||
|
|
font-size: 1.2rem;
|
||
|
|
margin-bottom: 1.5rem;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.75rem;
|
||
|
|
color: #fff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card h2 .icon {
|
||
|
|
font-size: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.input-group {
|
||
|
|
margin-bottom: 1.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
label {
|
||
|
|
display: block;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
color: #888;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
|
||
|
|
textarea, input[type="password"], input[type="text"], input[type="url"] {
|
||
|
|
width: 100%;
|
||
|
|
padding: 1rem 1.25rem;
|
||
|
|
border: 2px solid rgba(255,255,255,0.1);
|
||
|
|
border-radius: 12px;
|
||
|
|
background: rgba(0,0,0,0.4);
|
||
|
|
color: #fff;
|
||
|
|
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
transition: all 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
textarea:focus, input:focus {
|
||
|
|
outline: none;
|
||
|
|
border-color: #8338ec;
|
||
|
|
box-shadow: 0 0 0 4px rgba(131, 56, 236, 0.2);
|
||
|
|
}
|
||
|
|
|
||
|
|
textarea.encrypted {
|
||
|
|
min-height: 100px;
|
||
|
|
font-size: 0.75rem;
|
||
|
|
word-break: break-all;
|
||
|
|
resize: vertical;
|
||
|
|
}
|
||
|
|
|
||
|
|
.unlock-row {
|
||
|
|
display: flex;
|
||
|
|
gap: 1rem;
|
||
|
|
align-items: flex-end;
|
||
|
|
}
|
||
|
|
|
||
|
|
.unlock-row .input-group {
|
||
|
|
flex: 1;
|
||
|
|
margin-bottom: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
button {
|
||
|
|
padding: 1rem 2.5rem;
|
||
|
|
border: none;
|
||
|
|
border-radius: 12px;
|
||
|
|
font-weight: 700;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.3s;
|
||
|
|
font-size: 1rem;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 1px;
|
||
|
|
}
|
||
|
|
|
||
|
|
button.primary {
|
||
|
|
background: linear-gradient(135deg, #ff006e 0%, #8338ec 100%);
|
||
|
|
color: #fff;
|
||
|
|
box-shadow: 0 4px 20px rgba(255, 0, 110, 0.3);
|
||
|
|
}
|
||
|
|
|
||
|
|
button.primary:hover {
|
||
|
|
transform: translateY(-3px);
|
||
|
|
box-shadow: 0 8px 30px rgba(255, 0, 110, 0.4);
|
||
|
|
}
|
||
|
|
|
||
|
|
button.primary:disabled {
|
||
|
|
opacity: 0.4;
|
||
|
|
cursor: not-allowed;
|
||
|
|
transform: none;
|
||
|
|
box-shadow: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
button.secondary {
|
||
|
|
background: rgba(255,255,255,0.1);
|
||
|
|
color: #fff;
|
||
|
|
border: 1px solid rgba(255,255,255,0.2);
|
||
|
|
}
|
||
|
|
|
||
|
|
button.secondary:hover {
|
||
|
|
background: rgba(255,255,255,0.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-indicator {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
padding: 0.75rem;
|
||
|
|
margin-bottom: 1.5rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
background: rgba(0,0,0,0.2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-indicator .dot {
|
||
|
|
width: 10px;
|
||
|
|
height: 10px;
|
||
|
|
border-radius: 50%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-indicator.loading .dot {
|
||
|
|
background: #ffc107;
|
||
|
|
animation: pulse 1s infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-indicator.ready .dot {
|
||
|
|
background: #00ff94;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-indicator.error .dot {
|
||
|
|
background: #ff5252;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes pulse {
|
||
|
|
0%, 100% { opacity: 1; }
|
||
|
|
50% { opacity: 0.3; }
|
||
|
|
}
|
||
|
|
|
||
|
|
.error-banner {
|
||
|
|
background: rgba(255, 82, 82, 0.15);
|
||
|
|
border: 1px solid rgba(255, 82, 82, 0.4);
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 1rem 1.25rem;
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
display: none;
|
||
|
|
color: #ff6b6b;
|
||
|
|
}
|
||
|
|
|
||
|
|
.error-banner.visible {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Media Player Styles */
|
||
|
|
.player-container {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.player-container.visible {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-info {
|
||
|
|
text-align: center;
|
||
|
|
margin-bottom: 2rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-artwork {
|
||
|
|
width: 200px;
|
||
|
|
height: 200px;
|
||
|
|
margin: 0 auto 1.5rem;
|
||
|
|
border-radius: 16px;
|
||
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #2d1b4e 100%);
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
font-size: 5rem;
|
||
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-artwork img, .track-artwork video {
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
object-fit: cover;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-title {
|
||
|
|
font-size: 1.5rem;
|
||
|
|
font-weight: 700;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-artist {
|
||
|
|
color: #888;
|
||
|
|
font-size: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-player-wrapper {
|
||
|
|
margin-top: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.audio-player {
|
||
|
|
width: 100%;
|
||
|
|
background: rgba(0,0,0,0.3);
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
audio, video {
|
||
|
|
width: 100%;
|
||
|
|
border-radius: 8px;
|
||
|
|
outline: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
video {
|
||
|
|
max-height: 500px;
|
||
|
|
background: #000;
|
||
|
|
}
|
||
|
|
|
||
|
|
.video-player-wrapper {
|
||
|
|
border-radius: 16px;
|
||
|
|
overflow: hidden;
|
||
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||
|
|
}
|
||
|
|
|
||
|
|
.license-info {
|
||
|
|
margin-top: 2rem;
|
||
|
|
padding: 1.5rem;
|
||
|
|
background: rgba(131, 56, 236, 0.1);
|
||
|
|
border: 1px solid rgba(131, 56, 236, 0.3);
|
||
|
|
border-radius: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.license-info h4 {
|
||
|
|
font-size: 0.9rem;
|
||
|
|
margin-bottom: 0.75rem;
|
||
|
|
color: #8338ec;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.license-info p {
|
||
|
|
font-size: 0.85rem;
|
||
|
|
color: #aaa;
|
||
|
|
line-height: 1.6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.license-info .license-token {
|
||
|
|
font-family: 'Monaco', 'Menlo', monospace;
|
||
|
|
font-size: 0.75rem;
|
||
|
|
background: rgba(0,0,0,0.3);
|
||
|
|
padding: 0.75rem;
|
||
|
|
border-radius: 8px;
|
||
|
|
margin-top: 0.75rem;
|
||
|
|
word-break: break-all;
|
||
|
|
color: #00ff94;
|
||
|
|
}
|
||
|
|
|
||
|
|
.download-section {
|
||
|
|
margin-top: 1.5rem;
|
||
|
|
padding-top: 1.5rem;
|
||
|
|
border-top: 1px solid rgba(255,255,255,0.1);
|
||
|
|
display: flex;
|
||
|
|
justify-content: center;
|
||
|
|
gap: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.download-section button {
|
||
|
|
padding: 0.75rem 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-list-section {
|
||
|
|
margin-top: 1.5rem;
|
||
|
|
padding-top: 1.5rem;
|
||
|
|
border-top: 1px solid rgba(255,255,255,0.1);
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-list-section h3 {
|
||
|
|
font-size: 0.9rem;
|
||
|
|
color: #888;
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-list {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 1rem;
|
||
|
|
padding: 0.75rem 1rem;
|
||
|
|
background: rgba(0,0,0,0.2);
|
||
|
|
border-radius: 8px;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-item:hover {
|
||
|
|
background: rgba(131, 56, 236, 0.2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-item.active {
|
||
|
|
background: rgba(255, 0, 110, 0.2);
|
||
|
|
border: 1px solid rgba(255, 0, 110, 0.4);
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-number {
|
||
|
|
font-weight: 700;
|
||
|
|
color: #8338ec;
|
||
|
|
min-width: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-name {
|
||
|
|
font-weight: 500;
|
||
|
|
font-size: 0.95rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-type {
|
||
|
|
font-size: 0.75rem;
|
||
|
|
color: #888;
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
.track-time {
|
||
|
|
font-family: 'Monaco', 'Menlo', monospace;
|
||
|
|
font-size: 0.8rem;
|
||
|
|
color: #00ff94;
|
||
|
|
}
|
||
|
|
|
||
|
|
.file-input-wrapper {
|
||
|
|
position: relative;
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.file-input-wrapper input[type="file"] {
|
||
|
|
position: absolute;
|
||
|
|
opacity: 0;
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.file-input-label {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
gap: 0.75rem;
|
||
|
|
padding: 2rem;
|
||
|
|
border: 2px dashed rgba(255,255,255,0.2);
|
||
|
|
border-radius: 12px;
|
||
|
|
background: rgba(0,0,0,0.2);
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.file-input-label:hover {
|
||
|
|
border-color: #8338ec;
|
||
|
|
background: rgba(131, 56, 236, 0.1);
|
||
|
|
}
|
||
|
|
|
||
|
|
.file-input-label .icon {
|
||
|
|
font-size: 2rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.or-divider {
|
||
|
|
text-align: center;
|
||
|
|
color: #666;
|
||
|
|
margin: 1rem 0;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.native-badge {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.25rem;
|
||
|
|
background: linear-gradient(135deg, #00ff94 0%, #00d4aa 100%);
|
||
|
|
color: #000;
|
||
|
|
font-size: 0.65rem;
|
||
|
|
font-weight: 700;
|
||
|
|
padding: 0.2rem 0.5rem;
|
||
|
|
border-radius: 4px;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.5px;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="container">
|
||
|
|
<div class="logo">
|
||
|
|
<h1>dapp.fm</h1>
|
||
|
|
<p class="tagline">Decentralized Music Distribution <span class="native-badge">Native App</span></p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="hero-text">
|
||
|
|
<p>
|
||
|
|
<strong>No middlemen. No platforms. No 70% cuts.</strong><br>
|
||
|
|
Artists encrypt their music with ChaCha20-Poly1305. Fans unlock with a license token.
|
||
|
|
Content lives on any CDN, IPFS, or artist's own server. The password IS the license.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="status" class="status-indicator ready">
|
||
|
|
<span class="dot"></span>
|
||
|
|
<span>Native decryption ready (memory speed)</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<h2><span class="icon">🔐</span> Unlock Licensed Content</h2>
|
||
|
|
|
||
|
|
<div class="file-input-wrapper">
|
||
|
|
<input type="file" id="file-input" accept=".smsg,.enc,.borg">
|
||
|
|
<label class="file-input-label">
|
||
|
|
<span class="icon">📁</span>
|
||
|
|
<span>Drop encrypted file here or click to browse</span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="or-divider">- or paste encrypted content -</div>
|
||
|
|
|
||
|
|
<div class="input-group">
|
||
|
|
<label for="encrypted-content">Encrypted Content (base64):</label>
|
||
|
|
<textarea id="encrypted-content" class="encrypted" placeholder="Paste the encrypted content from the artist..."></textarea>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="demo-banner" style="background: rgba(255, 0, 110, 0.1); border: 1px solid rgba(255, 0, 110, 0.3); border-radius: 12px; padding: 1rem; margin-bottom: 1rem;">
|
||
|
|
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem;">
|
||
|
|
<div>
|
||
|
|
<strong style="color: #ff006e;">Try the Demo!</strong>
|
||
|
|
<span style="color: #888; font-size: 0.85rem; margin-left: 0.5rem;">Bundled sample video</span>
|
||
|
|
</div>
|
||
|
|
<button id="load-demo-btn" class="secondary" style="padding: 0.6rem 1.2rem; font-size: 0.85rem;">Load Demo Track</button>
|
||
|
|
</div>
|
||
|
|
<div style="font-size: 0.8rem; color: #666; margin-top: 0.5rem;">
|
||
|
|
Password: <code style="background: rgba(0,0,0,0.3); padding: 0.2rem 0.5rem; border-radius: 4px; color: #00ff94;">dapp-fm-2024</code>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="error-banner" class="error-banner"></div>
|
||
|
|
|
||
|
|
<!-- Manifest preview (shown without decryption) -->
|
||
|
|
<div id="manifest-preview" style="display: none; background: rgba(131, 56, 236, 0.1); border: 1px solid rgba(131, 56, 236, 0.3); border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem;"></div>
|
||
|
|
|
||
|
|
<div class="unlock-row">
|
||
|
|
<div class="input-group">
|
||
|
|
<label for="license-token">License Token (Password):</label>
|
||
|
|
<input type="password" id="license-token" placeholder="Enter your license token from the artist">
|
||
|
|
</div>
|
||
|
|
<button id="unlock-btn" class="primary">Unlock</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Player appears after unlock -->
|
||
|
|
<div id="player-container" class="card player-container">
|
||
|
|
<h2><span class="icon">🎵</span> Now Playing</h2>
|
||
|
|
|
||
|
|
<div class="track-info">
|
||
|
|
<div class="track-artwork" id="track-artwork">🎶</div>
|
||
|
|
<div class="track-title" id="track-title">Track Title</div>
|
||
|
|
<div class="track-artist" id="track-artist">Artist Name</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="media-player-wrapper" id="media-player-wrapper">
|
||
|
|
<!-- Audio/Video player inserted here -->
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="track-list-section" class="track-list-section" style="display: none;">
|
||
|
|
<h3><span>💿</span> Track List</h3>
|
||
|
|
<div id="track-list" class="track-list">
|
||
|
|
<!-- Tracks populated by JS -->
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="license-info">
|
||
|
|
<h4>🔓 Licensed Content</h4>
|
||
|
|
<p id="license-description">This content was unlocked with your personal license token.
|
||
|
|
Decryption powered by native Go - no servers, memory speed.</p>
|
||
|
|
<div class="license-token" id="license-display"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="download-section">
|
||
|
|
<button class="secondary" id="download-btn">Download Original</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
// Wails runtime - provides window.go bindings
|
||
|
|
let currentMediaBlob = null;
|
||
|
|
let currentMediaName = null;
|
||
|
|
let currentMediaMime = null;
|
||
|
|
let currentManifest = null;
|
||
|
|
|
||
|
|
// Check if Wails runtime is available
|
||
|
|
function isWailsReady() {
|
||
|
|
return typeof window.go !== 'undefined' &&
|
||
|
|
typeof window.go.player !== 'undefined' &&
|
||
|
|
typeof window.go.player.Player !== 'undefined';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Wait for Wails runtime
|
||
|
|
function waitForWails() {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
if (isWailsReady()) {
|
||
|
|
resolve();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// Poll for Wails runtime
|
||
|
|
const interval = setInterval(() => {
|
||
|
|
if (isWailsReady()) {
|
||
|
|
clearInterval(interval);
|
||
|
|
resolve();
|
||
|
|
}
|
||
|
|
}, 50);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function showError(msg) {
|
||
|
|
const errorBanner = document.getElementById('error-banner');
|
||
|
|
errorBanner.textContent = msg;
|
||
|
|
errorBanner.classList.add('visible');
|
||
|
|
}
|
||
|
|
|
||
|
|
function hideError() {
|
||
|
|
document.getElementById('error-banner').classList.remove('visible');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle file input
|
||
|
|
document.getElementById('file-input').addEventListener('change', async (e) => {
|
||
|
|
const file = e.target.files[0];
|
||
|
|
if (!file) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const content = await file.arrayBuffer();
|
||
|
|
const base64 = btoa(String.fromCharCode(...new Uint8Array(content)));
|
||
|
|
document.getElementById('encrypted-content').value = base64;
|
||
|
|
await showManifestPreview(base64);
|
||
|
|
} catch (err) {
|
||
|
|
showError('Failed to read file: ' + err.message);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Listen for content paste/input
|
||
|
|
let previewDebounce = null;
|
||
|
|
document.getElementById('encrypted-content').addEventListener('input', async (e) => {
|
||
|
|
const content = e.target.value.trim();
|
||
|
|
clearTimeout(previewDebounce);
|
||
|
|
previewDebounce = setTimeout(async () => {
|
||
|
|
if (content && content.length > 100) {
|
||
|
|
await showManifestPreview(content);
|
||
|
|
}
|
||
|
|
}, 500);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Show manifest preview using Go bindings (NO WASM!)
|
||
|
|
async function showManifestPreview(encryptedB64) {
|
||
|
|
await waitForWails();
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Direct Go call at memory speed!
|
||
|
|
const manifest = await window.go.player.Player.GetManifest(encryptedB64);
|
||
|
|
currentManifest = manifest;
|
||
|
|
|
||
|
|
const previewSection = document.getElementById('manifest-preview');
|
||
|
|
while (previewSection.firstChild) {
|
||
|
|
previewSection.removeChild(previewSection.firstChild);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (manifest && manifest.title) {
|
||
|
|
previewSection.style.display = 'block';
|
||
|
|
|
||
|
|
// Header with icon
|
||
|
|
const headerDiv = document.createElement('div');
|
||
|
|
headerDiv.style.cssText = 'display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;';
|
||
|
|
|
||
|
|
const icon = document.createElement('span');
|
||
|
|
icon.style.fontSize = '2.5rem';
|
||
|
|
icon.textContent = manifest.release_type === 'djset' ? '🎧' :
|
||
|
|
manifest.release_type === 'live' ? '🎤' : '💿';
|
||
|
|
|
||
|
|
const titleDiv = document.createElement('div');
|
||
|
|
const titleEl = document.createElement('div');
|
||
|
|
titleEl.style.cssText = 'font-size: 1.2rem; font-weight: 700; color: #fff;';
|
||
|
|
titleEl.textContent = manifest.title || 'Untitled';
|
||
|
|
|
||
|
|
const artistEl = document.createElement('div');
|
||
|
|
artistEl.style.cssText = 'font-size: 0.9rem; color: #888;';
|
||
|
|
artistEl.textContent = manifest.artist || 'Unknown Artist';
|
||
|
|
|
||
|
|
titleDiv.appendChild(titleEl);
|
||
|
|
titleDiv.appendChild(artistEl);
|
||
|
|
headerDiv.appendChild(icon);
|
||
|
|
headerDiv.appendChild(titleDiv);
|
||
|
|
previewSection.appendChild(headerDiv);
|
||
|
|
|
||
|
|
// Track list
|
||
|
|
if (manifest.tracks && manifest.tracks.length > 0) {
|
||
|
|
const trackHeader = document.createElement('div');
|
||
|
|
trackHeader.style.cssText = 'font-size: 0.85rem; color: #8338ec; margin-bottom: 0.5rem;';
|
||
|
|
trackHeader.textContent = '💿 ' + manifest.tracks.length + ' track(s)';
|
||
|
|
previewSection.appendChild(trackHeader);
|
||
|
|
|
||
|
|
const trackList = document.createElement('div');
|
||
|
|
trackList.style.maxHeight = '150px';
|
||
|
|
trackList.style.overflowY = 'auto';
|
||
|
|
|
||
|
|
manifest.tracks.forEach((track, i) => {
|
||
|
|
const trackEl = document.createElement('div');
|
||
|
|
trackEl.style.cssText = 'display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 6px; margin-bottom: 0.25rem; font-size: 0.85rem;';
|
||
|
|
|
||
|
|
const numEl = document.createElement('span');
|
||
|
|
numEl.style.cssText = 'color: #8338ec; font-weight: 600; min-width: 20px;';
|
||
|
|
numEl.textContent = (track.track_num || (i + 1)) + '.';
|
||
|
|
|
||
|
|
const nameEl = document.createElement('span');
|
||
|
|
nameEl.style.cssText = 'flex: 1; color: #ccc;';
|
||
|
|
nameEl.textContent = track.title || 'Track ' + (i + 1);
|
||
|
|
|
||
|
|
const timeEl = document.createElement('span');
|
||
|
|
timeEl.style.cssText = 'color: #00ff94; font-family: monospace; font-size: 0.8rem;';
|
||
|
|
timeEl.textContent = formatTime(track.start || 0);
|
||
|
|
|
||
|
|
trackEl.appendChild(numEl);
|
||
|
|
trackEl.appendChild(nameEl);
|
||
|
|
trackEl.appendChild(timeEl);
|
||
|
|
trackList.appendChild(trackEl);
|
||
|
|
});
|
||
|
|
|
||
|
|
previewSection.appendChild(trackList);
|
||
|
|
}
|
||
|
|
|
||
|
|
// License status
|
||
|
|
if (manifest.is_expired !== undefined) {
|
||
|
|
const licenseDiv = document.createElement('div');
|
||
|
|
licenseDiv.style.cssText = 'margin-top: 1rem; padding: 0.75rem; border-radius: 8px;';
|
||
|
|
|
||
|
|
if (manifest.is_expired) {
|
||
|
|
licenseDiv.style.background = 'rgba(255, 82, 82, 0.2)';
|
||
|
|
licenseDiv.style.border = '1px solid rgba(255, 82, 82, 0.4)';
|
||
|
|
const label = document.createElement('div');
|
||
|
|
label.style.cssText = 'color: #ff5252; font-weight: 600;';
|
||
|
|
label.textContent = 'LICENSE EXPIRED';
|
||
|
|
licenseDiv.appendChild(label);
|
||
|
|
} else if (manifest.time_remaining) {
|
||
|
|
licenseDiv.style.background = 'rgba(0, 255, 148, 0.1)';
|
||
|
|
licenseDiv.style.border = '1px solid rgba(0, 255, 148, 0.3)';
|
||
|
|
const label = document.createElement('span');
|
||
|
|
label.style.cssText = 'color: #00ff94; font-weight: 600; font-size: 0.8rem;';
|
||
|
|
label.textContent = (manifest.license_type || 'LICENSE').toUpperCase();
|
||
|
|
const time = document.createElement('span');
|
||
|
|
time.style.cssText = 'color: #888; font-size: 0.8rem; margin-left: 0.5rem;';
|
||
|
|
time.textContent = manifest.time_remaining + ' remaining';
|
||
|
|
licenseDiv.appendChild(label);
|
||
|
|
licenseDiv.appendChild(time);
|
||
|
|
} else {
|
||
|
|
licenseDiv.style.background = 'rgba(0, 255, 148, 0.1)';
|
||
|
|
licenseDiv.style.border = '1px solid rgba(0, 255, 148, 0.3)';
|
||
|
|
const label = document.createElement('span');
|
||
|
|
label.style.cssText = 'color: #00ff94; font-weight: 600; font-size: 0.8rem;';
|
||
|
|
label.textContent = 'PERPETUAL LICENSE';
|
||
|
|
licenseDiv.appendChild(label);
|
||
|
|
}
|
||
|
|
previewSection.appendChild(licenseDiv);
|
||
|
|
}
|
||
|
|
|
||
|
|
const hint = document.createElement('div');
|
||
|
|
hint.style.cssText = 'margin-top: 1rem; font-size: 0.85rem; color: #888; text-align: center;';
|
||
|
|
hint.textContent = manifest.is_expired ?
|
||
|
|
'License expired. Contact artist for renewal.' :
|
||
|
|
'Enter license token to unlock and play';
|
||
|
|
previewSection.appendChild(hint);
|
||
|
|
|
||
|
|
} else {
|
||
|
|
previewSection.style.display = 'none';
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.log('Could not read manifest:', err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Unlock content using Go bindings (memory speed!)
|
||
|
|
async function unlockContent() {
|
||
|
|
hideError();
|
||
|
|
await waitForWails();
|
||
|
|
|
||
|
|
const encryptedB64 = document.getElementById('encrypted-content').value.trim();
|
||
|
|
const password = document.getElementById('license-token').value;
|
||
|
|
|
||
|
|
if (!encryptedB64) {
|
||
|
|
showError('Please provide encrypted content');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!password) {
|
||
|
|
showError('Please enter your license token');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Check license validity (memory speed)
|
||
|
|
const isValid = await window.go.player.Player.IsLicenseValid(encryptedB64);
|
||
|
|
if (!isValid) {
|
||
|
|
showError('License has expired. Contact the artist for renewal.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decrypt using Go bindings (memory speed - no HTTP/TCP!)
|
||
|
|
const result = await window.go.player.Player.Decrypt(encryptedB64, password);
|
||
|
|
displayMedia(result, password);
|
||
|
|
|
||
|
|
} catch (err) {
|
||
|
|
showError('Unlock failed: ' + err.message);
|
||
|
|
console.error(err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Display decrypted media
|
||
|
|
function displayMedia(result, password) {
|
||
|
|
const playerContainer = document.getElementById('player-container');
|
||
|
|
const mediaWrapper = document.getElementById('media-player-wrapper');
|
||
|
|
const artworkEl = document.getElementById('track-artwork');
|
||
|
|
|
||
|
|
// Set track info
|
||
|
|
const title = (currentManifest && currentManifest.title) || result.subject || 'Untitled';
|
||
|
|
const artist = (currentManifest && currentManifest.artist) || result.from || 'Unknown Artist';
|
||
|
|
document.getElementById('track-title').textContent = title;
|
||
|
|
document.getElementById('track-artist').textContent = artist;
|
||
|
|
|
||
|
|
// Show masked license token
|
||
|
|
const masked = password.substring(0, 4) + '••••••••' + password.substring(password.length - 4);
|
||
|
|
document.getElementById('license-display').textContent = masked;
|
||
|
|
|
||
|
|
// Clear previous media
|
||
|
|
while (mediaWrapper.firstChild) mediaWrapper.removeChild(mediaWrapper.firstChild);
|
||
|
|
while (artworkEl.firstChild) artworkEl.removeChild(artworkEl.firstChild);
|
||
|
|
artworkEl.textContent = '🎶';
|
||
|
|
|
||
|
|
// Process attachments
|
||
|
|
if (result.attachments && result.attachments.length > 0) {
|
||
|
|
result.attachments.forEach((att) => {
|
||
|
|
const mime = att.mime_type || 'application/octet-stream';
|
||
|
|
|
||
|
|
// URL from Go - served through Wails asset handler
|
||
|
|
const url = att.url || att.file_path || att.stream_url || att.data_url;
|
||
|
|
|
||
|
|
// Store info for download
|
||
|
|
currentMediaName = att.name;
|
||
|
|
currentMediaMime = mime;
|
||
|
|
|
||
|
|
if (mime.startsWith('video/')) {
|
||
|
|
const wrapper = document.createElement('div');
|
||
|
|
wrapper.className = 'video-player-wrapper';
|
||
|
|
const video = document.createElement('video');
|
||
|
|
video.controls = true;
|
||
|
|
video.src = url;
|
||
|
|
video.style.width = '100%';
|
||
|
|
wrapper.appendChild(video);
|
||
|
|
mediaWrapper.appendChild(wrapper);
|
||
|
|
artworkEl.textContent = '🎬';
|
||
|
|
|
||
|
|
} else if (mime.startsWith('audio/')) {
|
||
|
|
const wrapper = document.createElement('div');
|
||
|
|
wrapper.className = 'audio-player';
|
||
|
|
const audio = document.createElement('audio');
|
||
|
|
audio.controls = true;
|
||
|
|
audio.src = url;
|
||
|
|
audio.style.width = '100%';
|
||
|
|
wrapper.appendChild(audio);
|
||
|
|
mediaWrapper.appendChild(wrapper);
|
||
|
|
artworkEl.textContent = '🎵';
|
||
|
|
|
||
|
|
} else if (mime.startsWith('image/')) {
|
||
|
|
const img = document.createElement('img');
|
||
|
|
img.src = url;
|
||
|
|
artworkEl.textContent = '';
|
||
|
|
artworkEl.appendChild(img);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build track list from manifest
|
||
|
|
const trackListSection = document.getElementById('track-list-section');
|
||
|
|
const trackListEl = document.getElementById('track-list');
|
||
|
|
while (trackListEl.firstChild) trackListEl.removeChild(trackListEl.firstChild);
|
||
|
|
|
||
|
|
if (currentManifest && currentManifest.tracks && currentManifest.tracks.length > 0) {
|
||
|
|
trackListSection.style.display = 'block';
|
||
|
|
|
||
|
|
currentManifest.tracks.forEach((track, index) => {
|
||
|
|
const item = document.createElement('div');
|
||
|
|
item.className = 'track-item';
|
||
|
|
item.addEventListener('click', () => {
|
||
|
|
const media = document.querySelector('audio, video');
|
||
|
|
if (media) {
|
||
|
|
media.currentTime = track.start || 0;
|
||
|
|
media.play();
|
||
|
|
document.querySelectorAll('.track-item').forEach(t => t.classList.remove('active'));
|
||
|
|
item.classList.add('active');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const num = document.createElement('span');
|
||
|
|
num.className = 'track-number';
|
||
|
|
num.textContent = track.track_num || (index + 1);
|
||
|
|
|
||
|
|
const info = document.createElement('div');
|
||
|
|
info.style.flex = '1';
|
||
|
|
const name = document.createElement('div');
|
||
|
|
name.className = 'track-name';
|
||
|
|
name.textContent = track.title || 'Track ' + (index + 1);
|
||
|
|
info.appendChild(name);
|
||
|
|
|
||
|
|
const time = document.createElement('span');
|
||
|
|
time.className = 'track-time';
|
||
|
|
time.textContent = formatTime(track.start || 0);
|
||
|
|
|
||
|
|
item.appendChild(num);
|
||
|
|
item.appendChild(info);
|
||
|
|
item.appendChild(time);
|
||
|
|
trackListEl.appendChild(item);
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
trackListSection.style.display = 'none';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update license description
|
||
|
|
if (currentManifest && currentManifest.time_remaining) {
|
||
|
|
document.getElementById('license-description').textContent =
|
||
|
|
(currentManifest.license_type || 'Rental').toUpperCase() + ' license - ' +
|
||
|
|
currentManifest.time_remaining + ' remaining. Native Go decryption at memory speed.';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Hide preview, show player
|
||
|
|
document.getElementById('manifest-preview').style.display = 'none';
|
||
|
|
playerContainer.classList.add('visible');
|
||
|
|
playerContainer.scrollIntoView({ behavior: 'smooth' });
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatTime(seconds) {
|
||
|
|
const mins = Math.floor(seconds / 60);
|
||
|
|
const secs = Math.floor(seconds % 60);
|
||
|
|
return mins + ':' + secs.toString().padStart(2, '0');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Download handler
|
||
|
|
document.getElementById('download-btn').addEventListener('click', () => {
|
||
|
|
if (!currentMediaBlob) {
|
||
|
|
alert('No media to download');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const url = URL.createObjectURL(currentMediaBlob);
|
||
|
|
const a = document.createElement('a');
|
||
|
|
a.href = url;
|
||
|
|
a.download = currentMediaName || 'media';
|
||
|
|
document.body.appendChild(a);
|
||
|
|
a.click();
|
||
|
|
document.body.removeChild(a);
|
||
|
|
URL.revokeObjectURL(url);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Load bundled demo - DIRECT GO CALL, no HTTP!
|
||
|
|
async function loadDemo() {
|
||
|
|
const btn = document.getElementById('load-demo-btn');
|
||
|
|
const originalText = btn.textContent;
|
||
|
|
btn.textContent = 'Loading...';
|
||
|
|
btn.disabled = true;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await waitForWails();
|
||
|
|
|
||
|
|
// Get manifest first (direct Go call)
|
||
|
|
const manifest = await window.go.main.App.GetDemoManifest();
|
||
|
|
currentManifest = manifest;
|
||
|
|
|
||
|
|
// Decrypt demo directly in Go - NO fetch, NO base64 encoding!
|
||
|
|
// Go reads embedded bytes -> decrypts -> returns result
|
||
|
|
const result = await window.go.main.App.LoadDemo();
|
||
|
|
|
||
|
|
// Display the decrypted media
|
||
|
|
displayMedia(result, 'dapp-fm-2024');
|
||
|
|
|
||
|
|
btn.textContent = 'Loaded!';
|
||
|
|
setTimeout(() => {
|
||
|
|
btn.textContent = originalText;
|
||
|
|
btn.disabled = false;
|
||
|
|
}, 2000);
|
||
|
|
} catch (err) {
|
||
|
|
showError('Failed to load demo: ' + err.message);
|
||
|
|
btn.textContent = originalText;
|
||
|
|
btn.disabled = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Event listeners
|
||
|
|
document.getElementById('unlock-btn').addEventListener('click', unlockContent);
|
||
|
|
document.getElementById('license-token').addEventListener('keypress', (e) => {
|
||
|
|
if (e.key === 'Enter') unlockContent();
|
||
|
|
});
|
||
|
|
document.getElementById('load-demo-btn').addEventListener('click', loadDemo);
|
||
|
|
|
||
|
|
// Ready check
|
||
|
|
waitForWails().then(() => {
|
||
|
|
console.log('Wails bindings ready - memory speed decryption enabled');
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|