tunecamp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.local +2 -0
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/LICENSE +22 -0
- package/README.md +554 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +172 -0
- package/dist/cli.js.map +1 -0
- package/dist/generator/embedGenerator.d.ts +38 -0
- package/dist/generator/embedGenerator.d.ts.map +1 -0
- package/dist/generator/embedGenerator.js +92 -0
- package/dist/generator/embedGenerator.js.map +1 -0
- package/dist/generator/feedGenerator.d.ts +50 -0
- package/dist/generator/feedGenerator.d.ts.map +1 -0
- package/dist/generator/feedGenerator.js +167 -0
- package/dist/generator/feedGenerator.js.map +1 -0
- package/dist/generator/podcastFeedGenerator.d.ts +54 -0
- package/dist/generator/podcastFeedGenerator.d.ts.map +1 -0
- package/dist/generator/podcastFeedGenerator.js +173 -0
- package/dist/generator/podcastFeedGenerator.js.map +1 -0
- package/dist/generator/proceduralCoverGenerator.d.ts +51 -0
- package/dist/generator/proceduralCoverGenerator.d.ts.map +1 -0
- package/dist/generator/proceduralCoverGenerator.js +228 -0
- package/dist/generator/proceduralCoverGenerator.js.map +1 -0
- package/dist/generator/siteGenerator.d.ts +55 -0
- package/dist/generator/siteGenerator.d.ts.map +1 -0
- package/dist/generator/siteGenerator.js +539 -0
- package/dist/generator/siteGenerator.js.map +1 -0
- package/dist/generator/templateEngine.d.ts +13 -0
- package/dist/generator/templateEngine.d.ts.map +1 -0
- package/dist/generator/templateEngine.js +146 -0
- package/dist/generator/templateEngine.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/catalogParser.d.ts +13 -0
- package/dist/parser/catalogParser.d.ts.map +1 -0
- package/dist/parser/catalogParser.js +120 -0
- package/dist/parser/catalogParser.js.map +1 -0
- package/dist/tools/generate-codes.d.ts +14 -0
- package/dist/tools/generate-codes.d.ts.map +1 -0
- package/dist/tools/generate-codes.js +274 -0
- package/dist/tools/generate-codes.js.map +1 -0
- package/dist/tools/generate-sea-pair.d.ts +14 -0
- package/dist/tools/generate-sea-pair.d.ts.map +1 -0
- package/dist/tools/generate-sea-pair.js +111 -0
- package/dist/tools/generate-sea-pair.js.map +1 -0
- package/dist/types/index.d.ts +117 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/audioUtils.d.ts +9 -0
- package/dist/utils/audioUtils.d.ts.map +1 -0
- package/dist/utils/audioUtils.js +67 -0
- package/dist/utils/audioUtils.js.map +1 -0
- package/dist/utils/configUtils.d.ts +11 -0
- package/dist/utils/configUtils.d.ts.map +1 -0
- package/dist/utils/configUtils.js +50 -0
- package/dist/utils/configUtils.js.map +1 -0
- package/dist/utils/fileUtils.d.ts +14 -0
- package/dist/utils/fileUtils.d.ts.map +1 -0
- package/dist/utils/fileUtils.js +73 -0
- package/dist/utils/fileUtils.js.map +1 -0
- package/examples/artist-free/README.md +36 -0
- package/examples/artist-paycurtain/README.md +49 -0
- package/examples/label/README.md +33 -0
- package/gundb-keypair.json +8 -0
- package/logo.svg +30 -0
- package/package-lock.json +1176 -0
- package/package.json +42 -0
- package/public/assets/community-registry.js +291 -0
- package/public/assets/download-stats.js +263 -0
- package/public/assets/player.js +219 -0
- package/public/assets/style.css +1170 -0
- package/public/assets/theme-widget.js +353 -0
- package/public/assets/unlock-codes.js +225 -0
- package/public/atom.xml +22 -0
- package/public/catalog.m3u +3 -0
- package/public/feed.xml +22 -0
- package/public/image.png +0 -0
- package/public/index.html +249 -0
- package/public/logo.svg +30 -0
- package/public/releases/chirichetto/Homologo - Chirichetto.wav +0 -0
- package/public/releases/chirichetto/cover.png +0 -0
- package/public/releases/chirichetto/embed-code.txt +16 -0
- package/public/releases/chirichetto/embed-compact.txt +8 -0
- package/public/releases/chirichetto/embed.html +39 -0
- package/public/releases/chirichetto/index.html +389 -0
- package/public/releases/chirichetto/playlist.m3u +3 -0
- package/templates/dark/assets/community-registry.js +291 -0
- package/templates/dark/assets/download-stats.js +263 -0
- package/templates/dark/assets/player.js +219 -0
- package/templates/dark/assets/style.css +740 -0
- package/templates/dark/index.hbs +73 -0
- package/templates/dark/layout.hbs +84 -0
- package/templates/dark/release.hbs +212 -0
- package/templates/default/assets/community-registry.js +291 -0
- package/templates/default/assets/download-stats.js +263 -0
- package/templates/default/assets/player.js +219 -0
- package/templates/default/assets/style.css +1170 -0
- package/templates/default/assets/theme-widget.js +353 -0
- package/templates/default/assets/unlock-codes.js +225 -0
- package/templates/default/index.hbs +188 -0
- package/templates/default/layout.hbs +117 -0
- package/templates/default/release.hbs +553 -0
- package/templates/minimal/assets/community-registry.js +291 -0
- package/templates/minimal/assets/download-stats.js +263 -0
- package/templates/minimal/assets/player.js +219 -0
- package/templates/minimal/assets/style.css +796 -0
- package/templates/minimal/index.hbs +73 -0
- package/templates/minimal/layout.hbs +84 -0
- package/templates/minimal/release.hbs +212 -0
- package/templates/retro/assets/community-registry.js +291 -0
- package/templates/retro/assets/download-stats.js +263 -0
- package/templates/retro/assets/player.js +219 -0
- package/templates/retro/assets/style.css +872 -0
- package/templates/retro/index.hbs +73 -0
- package/templates/retro/layout.hbs +84 -0
- package/templates/retro/release.hbs +212 -0
- package/templates/translucent/assets/community-registry.js +291 -0
- package/templates/translucent/assets/download-stats.js +263 -0
- package/templates/translucent/assets/player.js +219 -0
- package/templates/translucent/assets/style.css +1352 -0
- package/templates/translucent/index.hbs +73 -0
- package/templates/translucent/layout.hbs +84 -0
- package/templates/translucent/release.hbs +212 -0
- package/website/community.html +492 -0
- package/website/index.html +195 -0
- package/website/styles.css +396 -0
- package/website/tunecamp.svg +30 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
<div class="container">
|
|
2
|
+
<nav class="breadcrumb">
|
|
3
|
+
<a href="{{path backUrl}}">← Back to catalog</a>
|
|
4
|
+
</nav>
|
|
5
|
+
|
|
6
|
+
<!-- Share & Embed Section -->
|
|
7
|
+
<section class="release-share-section">
|
|
8
|
+
<div class="share-actions">
|
|
9
|
+
<h3>Share & Embed</h3>
|
|
10
|
+
<div class="share-buttons">
|
|
11
|
+
<a href="{{path backUrl}}" class="share-btn" title="View Catalog">
|
|
12
|
+
<i class="fas fa-list"></i> Catalog
|
|
13
|
+
</a>
|
|
14
|
+
<a href="{{assetPath 'feed.xml'}}" class="share-btn" title="RSS Feed" target="_blank">
|
|
15
|
+
<i class="fas fa-rss"></i> RSS Feed
|
|
16
|
+
</a>
|
|
17
|
+
<a href="{{assetPath 'atom.xml'}}" class="share-btn" title="Atom Feed" target="_blank">
|
|
18
|
+
<i class="fas fa-rss-square"></i> Atom Feed
|
|
19
|
+
</a>
|
|
20
|
+
<button class="share-btn" onclick="copyShareLink()" title="Copy Share Link">
|
|
21
|
+
<i class="fas fa-link"></i> Copy Link
|
|
22
|
+
</button>
|
|
23
|
+
<button class="share-btn" onclick="showEmbedCode()" title="Show Embed Code">
|
|
24
|
+
<i class="fas fa-code"></i> Embed Code
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
|
|
30
|
+
<!-- Embed Code Modal -->
|
|
31
|
+
<div id="embedModal" class="embed-modal" style="display: none;">
|
|
32
|
+
<div class="embed-modal-content">
|
|
33
|
+
<div class="embed-modal-header">
|
|
34
|
+
<h3>Embed Code</h3>
|
|
35
|
+
<button class="embed-modal-close" onclick="closeEmbedModal()">×</button>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="embed-modal-body">
|
|
38
|
+
<div class="embed-tabs">
|
|
39
|
+
<button class="embed-tab active" onclick="switchEmbedTab('full')">Full Embed</button>
|
|
40
|
+
<button class="embed-tab" onclick="switchEmbedTab('compact')">Compact</button>
|
|
41
|
+
</div>
|
|
42
|
+
<div id="embedCodeContent" class="embed-code-content">
|
|
43
|
+
<textarea id="embedCodeText" readonly class="embed-code-text"></textarea>
|
|
44
|
+
<button class="btn btn-primary" onclick="copyEmbedCode()">
|
|
45
|
+
<i class="fas fa-copy"></i> Copy Code
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<article class="release-detail">
|
|
53
|
+
<header class="release-header">
|
|
54
|
+
{{#if release.coverUrl}}
|
|
55
|
+
<div class="release-cover-large">
|
|
56
|
+
<img src="{{path release.coverUrl}}" alt="{{release.config.title}}">
|
|
57
|
+
</div>
|
|
58
|
+
{{/if}}
|
|
59
|
+
|
|
60
|
+
<div class="release-metadata">
|
|
61
|
+
<h1>{{release.config.title}}</h1>
|
|
62
|
+
|
|
63
|
+
{{#if artist}}
|
|
64
|
+
<p class="release-artist">by {{artist.name}}</p>
|
|
65
|
+
{{/if}}
|
|
66
|
+
|
|
67
|
+
<p class="release-date">Released {{formatDate release.config.date}}</p>
|
|
68
|
+
|
|
69
|
+
<p class="release-downloads" id="releaseDownloads" style="display: none;">
|
|
70
|
+
<i class="fas fa-download"></i> <span id="downloadCount">0</span> downloads
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
{{#if release.config.genres}}
|
|
74
|
+
<div class="release-genres">
|
|
75
|
+
{{#each release.config.genres}}
|
|
76
|
+
<span class="genre-tag">{{this}}</span>
|
|
77
|
+
{{/each}}
|
|
78
|
+
</div>
|
|
79
|
+
{{/if}}
|
|
80
|
+
|
|
81
|
+
{{#if release.config.description}}
|
|
82
|
+
<p class="release-description">{{release.config.description}}</p>
|
|
83
|
+
{{/if}}
|
|
84
|
+
|
|
85
|
+
{{#if release.config.credits}}
|
|
86
|
+
<div class="release-credits">
|
|
87
|
+
<h3>Credits</h3>
|
|
88
|
+
<ul>
|
|
89
|
+
{{#each release.config.credits}}
|
|
90
|
+
<li><strong>{{role}}:</strong> {{name}}</li>
|
|
91
|
+
{{/each}}
|
|
92
|
+
</ul>
|
|
93
|
+
</div>
|
|
94
|
+
{{/if}}
|
|
95
|
+
|
|
96
|
+
{{#if release.config.license}}
|
|
97
|
+
<div class="release-license">
|
|
98
|
+
<h3>License</h3>
|
|
99
|
+
<p class="license-info">
|
|
100
|
+
{{#if (eq release.config.license "copyright")}}
|
|
101
|
+
<i class="fas fa-copyright"></i> All rights reserved. This work is protected by copyright.
|
|
102
|
+
{{else if (eq release.config.license "cc-by")}}
|
|
103
|
+
<i class="fab fa-creative-commons"></i> Licensed under <a
|
|
104
|
+
href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC BY 4.0</a> - Attribution
|
|
105
|
+
{{else if (eq release.config.license "cc-by-sa")}}
|
|
106
|
+
<i class="fab fa-creative-commons"></i> Licensed under <a
|
|
107
|
+
href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">CC BY-SA 4.0</a> -
|
|
108
|
+
Attribution-ShareAlike
|
|
109
|
+
{{else if (eq release.config.license "cc-by-nc")}}
|
|
110
|
+
<i class="fab fa-creative-commons"></i> Licensed under <a
|
|
111
|
+
href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank">CC BY-NC 4.0</a> -
|
|
112
|
+
Attribution-NonCommercial
|
|
113
|
+
{{else if (eq release.config.license "cc-by-nc-sa")}}
|
|
114
|
+
<i class="fab fa-creative-commons"></i> Licensed under <a
|
|
115
|
+
href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">CC BY-NC-SA 4.0</a> -
|
|
116
|
+
Attribution-NonCommercial-ShareAlike
|
|
117
|
+
{{else if (eq release.config.license "cc-by-nc-nd")}}
|
|
118
|
+
<i class="fab fa-creative-commons"></i> Licensed under <a
|
|
119
|
+
href="https://creativecommons.org/licenses/by-nc-nd/4.0/" target="_blank">CC BY-NC-ND 4.0</a> -
|
|
120
|
+
Attribution-NonCommercial-NoDerivatives
|
|
121
|
+
{{else if (eq release.config.license "cc-by-nd")}}
|
|
122
|
+
<i class="fab fa-creative-commons"></i> Licensed under <a
|
|
123
|
+
href="https://creativecommons.org/licenses/by-nd/4.0/" target="_blank">CC BY-ND 4.0</a> -
|
|
124
|
+
Attribution-NoDerivatives
|
|
125
|
+
{{else if (eq release.config.license "public-domain")}}
|
|
126
|
+
<i class="fas fa-globe"></i> Public Domain - This work is in the public domain.
|
|
127
|
+
{{/if}}
|
|
128
|
+
</p>
|
|
129
|
+
</div>
|
|
130
|
+
{{/if}}
|
|
131
|
+
|
|
132
|
+
{{#if release.config.download}}
|
|
133
|
+
<div class="release-download-actions">
|
|
134
|
+
{{#if (eq release.config.download "free")}}
|
|
135
|
+
<button class="btn btn-primary" onclick="downloadAll()">
|
|
136
|
+
<i class="fas fa-download"></i> Download All (Free)
|
|
137
|
+
</button>
|
|
138
|
+
{{else if (eq release.config.download "paycurtain")}}
|
|
139
|
+
<div class="paycurtain">
|
|
140
|
+
<div class="support-message">
|
|
141
|
+
<p><strong>Support the artist!</strong></p>
|
|
142
|
+
<p>Suggested donation: ${{release.config.price}}</p>
|
|
143
|
+
<p class="honor-system">This is an honor system - you can download for free, but your support helps the
|
|
144
|
+
artist continue creating music.</p>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="payment-buttons">
|
|
147
|
+
{{#if release.config.paypalLink}}
|
|
148
|
+
<a href="{{release.config.paypalLink}}" target="_blank" class="btn btn-primary">
|
|
149
|
+
<i class="fab fa-paypal"></i> Support via PayPal
|
|
150
|
+
</a>
|
|
151
|
+
{{/if}}
|
|
152
|
+
{{#if release.config.stripeLink}}
|
|
153
|
+
<a href="{{release.config.stripeLink}}" target="_blank" class="btn btn-secondary">
|
|
154
|
+
<i class="fab fa-stripe"></i> Support via Stripe
|
|
155
|
+
</a>
|
|
156
|
+
{{/if}}
|
|
157
|
+
<button class="btn btn-outline" onclick="downloadAll()">
|
|
158
|
+
<i class="fas fa-download"></i> Download Free
|
|
159
|
+
</button>
|
|
160
|
+
{{#unless release.config.paypalLink}}
|
|
161
|
+
{{#unless release.config.stripeLink}}
|
|
162
|
+
<p class="payment-info">No payment links configured - download is free</p>
|
|
163
|
+
{{/unless}}
|
|
164
|
+
{{/unless}}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
{{else if (eq release.config.download "codes")}}
|
|
168
|
+
<!-- Unlock Codes Section -->
|
|
169
|
+
<div class="unlock-codes-section" id="unlockCodesSection">
|
|
170
|
+
<div class="unlock-header">
|
|
171
|
+
<i class="fas fa-lock"></i>
|
|
172
|
+
<h3>Download with Unlock Code</h3>
|
|
173
|
+
<p>Enter your unlock code to access downloads</p>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div class="unlock-form" id="unlockForm">
|
|
177
|
+
<div class="code-input-group">
|
|
178
|
+
<input type="text" id="unlockCodeInput" placeholder="XXXX-XXXX-XXXX" maxlength="14" autocomplete="off"
|
|
179
|
+
spellcheck="false">
|
|
180
|
+
<button class="btn btn-primary" id="validateCodeBtn">
|
|
181
|
+
<i class="fas fa-key"></i> Unlock
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
<p class="unlock-error" id="unlockError" style="display: none;"></p>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div class="unlock-success" id="unlockSuccess" style="display: none;">
|
|
188
|
+
<div class="success-icon">
|
|
189
|
+
<i class="fas fa-check-circle"></i>
|
|
190
|
+
</div>
|
|
191
|
+
<h4>Code Validated!</h4>
|
|
192
|
+
<p id="unlockMessage">You can now download this release.</p>
|
|
193
|
+
<button class="btn btn-primary btn-lg" onclick="downloadAll()">
|
|
194
|
+
<i class="fas fa-download"></i> Download All Tracks
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="unlock-info">
|
|
199
|
+
<p><small><i class="fas fa-info-circle"></i> Don't have a code? Contact the artist or purchase
|
|
200
|
+
one.</small></p>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<!-- GunDB Unlock Codes Script -->
|
|
205
|
+
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
|
206
|
+
<script src="{{assetPath 'assets/unlock-codes.js'}}"></script>
|
|
207
|
+
<script>
|
|
208
|
+
(function () {
|
|
209
|
+
const releaseSlug = '{{release.slug}}';
|
|
210
|
+
const namespace = '{{#if release.config.unlockCodes.namespace}}{{release.config.unlockCodes.namespace}}{{else}}tunecamp{{/if}}';
|
|
211
|
+
|
|
212
|
+
// Initialize unlock codes
|
|
213
|
+
let unlockCodes;
|
|
214
|
+
|
|
215
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
216
|
+
// Check if already unlocked
|
|
217
|
+
const unlockedReleases = JSON.parse(localStorage.getItem('tunecamp_unlocked') || '{}');
|
|
218
|
+
if (unlockedReleases[releaseSlug]) {
|
|
219
|
+
showSuccess('Previously unlocked!');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Initialize GunDB unlock codes
|
|
224
|
+
unlockCodes = new TunecampUnlockCodes({ namespace });
|
|
225
|
+
|
|
226
|
+
// Bind form events
|
|
227
|
+
const input = document.getElementById('unlockCodeInput');
|
|
228
|
+
const btn = document.getElementById('validateCodeBtn');
|
|
229
|
+
|
|
230
|
+
// Format code as user types
|
|
231
|
+
input.addEventListener('input', function (e) {
|
|
232
|
+
let value = e.target.value.replace(/[^A-Za-z0-9]/g, '').toUpperCase();
|
|
233
|
+
if (value.length > 4) value = value.slice(0, 4) + '-' + value.slice(4);
|
|
234
|
+
if (value.length > 9) value = value.slice(0, 9) + '-' + value.slice(9);
|
|
235
|
+
if (value.length > 14) value = value.slice(0, 14);
|
|
236
|
+
e.target.value = value;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Validate on enter
|
|
240
|
+
input.addEventListener('keypress', function (e) {
|
|
241
|
+
if (e.key === 'Enter') validateCode();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Validate on button click
|
|
245
|
+
btn.addEventListener('click', validateCode);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
async function validateCode() {
|
|
249
|
+
const input = document.getElementById('unlockCodeInput');
|
|
250
|
+
const error = document.getElementById('unlockError');
|
|
251
|
+
const btn = document.getElementById('validateCodeBtn');
|
|
252
|
+
const code = input.value.trim();
|
|
253
|
+
|
|
254
|
+
if (code.length < 14) {
|
|
255
|
+
showError('Please enter a complete code (format: XXXX-XXXX-XXXX)');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
btn.disabled = true;
|
|
260
|
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Validating...';
|
|
261
|
+
error.style.display = 'none';
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const result = await unlockCodes.validateCode(releaseSlug, code);
|
|
265
|
+
|
|
266
|
+
if (result.valid) {
|
|
267
|
+
// Redeem the code
|
|
268
|
+
const redeemResult = await unlockCodes.redeemCode(releaseSlug, code);
|
|
269
|
+
|
|
270
|
+
if (redeemResult.success) {
|
|
271
|
+
// Store in localStorage
|
|
272
|
+
const unlockedReleases = JSON.parse(localStorage.getItem('tunecamp_unlocked') || '{}');
|
|
273
|
+
unlockedReleases[releaseSlug] = { code, unlockedAt: Date.now() };
|
|
274
|
+
localStorage.setItem('tunecamp_unlocked', JSON.stringify(unlockedReleases));
|
|
275
|
+
|
|
276
|
+
const msg = redeemResult.downloadsRemaining > 0
|
|
277
|
+
? `Downloads remaining on this code: ${redeemResult.downloadsRemaining}`
|
|
278
|
+
: 'This was the last use of this code.';
|
|
279
|
+
showSuccess(msg);
|
|
280
|
+
} else {
|
|
281
|
+
showError(redeemResult.error || 'Failed to redeem code');
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
showError(result.error || 'Invalid code');
|
|
285
|
+
}
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.error('Validation error:', err);
|
|
288
|
+
showError('Network error. Please try again.');
|
|
289
|
+
} finally {
|
|
290
|
+
btn.disabled = false;
|
|
291
|
+
btn.innerHTML = '<i class="fas fa-key"></i> Unlock';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function showError(message) {
|
|
296
|
+
const error = document.getElementById('unlockError');
|
|
297
|
+
error.textContent = message;
|
|
298
|
+
error.style.display = 'block';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function showSuccess(message) {
|
|
302
|
+
document.getElementById('unlockForm').style.display = 'none';
|
|
303
|
+
document.getElementById('unlockMessage').textContent = message;
|
|
304
|
+
document.getElementById('unlockSuccess').style.display = 'block';
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
</script>
|
|
308
|
+
{{/if}}
|
|
309
|
+
</div>
|
|
310
|
+
{{/if}}
|
|
311
|
+
</div>
|
|
312
|
+
</header>
|
|
313
|
+
|
|
314
|
+
<section class="tracklist">
|
|
315
|
+
<h2>Tracks</h2>
|
|
316
|
+
|
|
317
|
+
<div class="audio-player" id="audioPlayer">
|
|
318
|
+
<div class="player-track-info">
|
|
319
|
+
<div class="player-cover">
|
|
320
|
+
{{#if release.coverUrl}}
|
|
321
|
+
<img src="{{release.coverUrl}}" alt="{{release.config.title}}" id="playerCover">
|
|
322
|
+
{{else}}
|
|
323
|
+
<i class="fas fa-music"></i>
|
|
324
|
+
{{/if}}
|
|
325
|
+
</div>
|
|
326
|
+
<div class="player-details">
|
|
327
|
+
<div class="player-title" id="playerTitle">Select a track</div>
|
|
328
|
+
<div class="player-artist" id="playerArtist">{{#if artist}}{{artist.name}}{{/if}}</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<div class="player-controls">
|
|
333
|
+
<button class="player-btn" id="prevBtn"><i class="fas fa-step-backward"></i></button>
|
|
334
|
+
<button class="player-btn player-btn-play" id="playBtn">
|
|
335
|
+
<i class="fas fa-play"></i>
|
|
336
|
+
</button>
|
|
337
|
+
<button class="player-btn" id="nextBtn"><i class="fas fa-step-forward"></i></button>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<div class="player-progress">
|
|
341
|
+
<span class="player-time" id="currentTime">0:00</span>
|
|
342
|
+
<input type="range" class="progress-bar" id="progressBar" min="0" max="100" value="0">
|
|
343
|
+
<span class="player-time" id="duration">0:00</span>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<div class="player-volume">
|
|
347
|
+
<i class="fas fa-volume-up"></i>
|
|
348
|
+
<input type="range" class="volume-bar" id="volumeBar" min="0" max="100" value="80">
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<ol class="track-list">
|
|
353
|
+
{{#each release.tracks}}
|
|
354
|
+
<li class="track-item" data-src="{{releasePath url}}" data-index="{{@index}}">
|
|
355
|
+
<div class="track-number">{{@index}}</div>
|
|
356
|
+
<div class="track-info">
|
|
357
|
+
<div class="track-title">{{title}}</div>
|
|
358
|
+
<div class="track-meta">
|
|
359
|
+
{{formatAudioFormat filename}}
|
|
360
|
+
{{#if duration}}
|
|
361
|
+
· {{formatDuration duration}}
|
|
362
|
+
{{/if}}
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
<div class="track-actions">
|
|
366
|
+
<button class="track-play-btn" onclick="playTrack({{@index}})">
|
|
367
|
+
<i class="fas fa-play"></i>
|
|
368
|
+
</button>
|
|
369
|
+
{{#if ../release.config.download}}
|
|
370
|
+
{{#if (eq ../release.config.download "free")}}
|
|
371
|
+
<a href="{{releasePath url}}" download class="track-download-btn">
|
|
372
|
+
<i class="fas fa-download"></i>
|
|
373
|
+
</a>
|
|
374
|
+
{{/if}}
|
|
375
|
+
{{/if}}
|
|
376
|
+
</div>
|
|
377
|
+
</li>
|
|
378
|
+
{{/each}}
|
|
379
|
+
</ol>
|
|
380
|
+
</section>
|
|
381
|
+
|
|
382
|
+
{{#if artist.donationLinks}}
|
|
383
|
+
<section class="donation-section">
|
|
384
|
+
<h2>Support the Artist</h2>
|
|
385
|
+
<p>If you enjoyed this music, consider supporting the artist:</p>
|
|
386
|
+
<div class="donation-links">
|
|
387
|
+
{{#each artist.donationLinks}}
|
|
388
|
+
<a href="{{url}}" target="_blank" class="donation-link">
|
|
389
|
+
<i class="fas fa-heart"></i>
|
|
390
|
+
<span>{{platform}}</span>
|
|
391
|
+
{{#if description}}
|
|
392
|
+
<small>{{description}}</small>
|
|
393
|
+
{{/if}}
|
|
394
|
+
</a>
|
|
395
|
+
{{/each}}
|
|
396
|
+
</div>
|
|
397
|
+
</section>
|
|
398
|
+
{{/if}}
|
|
399
|
+
</article>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<script>
|
|
403
|
+
// Share & Embed functionality
|
|
404
|
+
function copyShareLink() {
|
|
405
|
+
const url = window.location.href;
|
|
406
|
+
navigator.clipboard.writeText(url).then(() => {
|
|
407
|
+
alert('Link copied to clipboard!');
|
|
408
|
+
}).catch(() => {
|
|
409
|
+
// Fallback for older browsers
|
|
410
|
+
const textarea = document.createElement('textarea');
|
|
411
|
+
textarea.value = url;
|
|
412
|
+
document.body.appendChild(textarea);
|
|
413
|
+
textarea.select();
|
|
414
|
+
document.execCommand('copy');
|
|
415
|
+
document.body.removeChild(textarea);
|
|
416
|
+
alert('Link copied to clipboard!');
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function showEmbedCode() {
|
|
421
|
+
const modal = document.getElementById('embedModal');
|
|
422
|
+
const embedCodeText = document.getElementById('embedCodeText');
|
|
423
|
+
|
|
424
|
+
// Load full embed code by default
|
|
425
|
+
fetch('{{path embedCodePath}}')
|
|
426
|
+
.then(response => response.text())
|
|
427
|
+
.then(text => {
|
|
428
|
+
embedCodeText.value = text.trim();
|
|
429
|
+
modal.style.display = 'flex';
|
|
430
|
+
})
|
|
431
|
+
.catch(err => {
|
|
432
|
+
console.error('Error loading embed code:', err);
|
|
433
|
+
alert('Error loading embed code');
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function switchEmbedTab(type) {
|
|
438
|
+
const tabs = document.querySelectorAll('.embed-tab');
|
|
439
|
+
tabs.forEach(tab => tab.classList.remove('active'));
|
|
440
|
+
event.target.classList.add('active');
|
|
441
|
+
|
|
442
|
+
const embedCodeText = document.getElementById('embedCodeText');
|
|
443
|
+
const file = type === 'full' ? '{{embedCodePath}}' : '{{embedCompactPath}}';
|
|
444
|
+
|
|
445
|
+
fetch('{{path ""}}' + file)
|
|
446
|
+
.then(response => response.text())
|
|
447
|
+
.then(text => {
|
|
448
|
+
embedCodeText.value = text.trim();
|
|
449
|
+
})
|
|
450
|
+
.catch(err => {
|
|
451
|
+
console.error('Error loading embed code:', err);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function copyEmbedCode() {
|
|
456
|
+
const embedCodeText = document.getElementById('embedCodeText');
|
|
457
|
+
embedCodeText.select();
|
|
458
|
+
document.execCommand('copy');
|
|
459
|
+
alert('Embed code copied to clipboard!');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function closeEmbedModal() {
|
|
463
|
+
document.getElementById('embedModal').style.display = 'none';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Close modal when clicking outside
|
|
467
|
+
window.onclick = function(event) {
|
|
468
|
+
const modal = document.getElementById('embedModal');
|
|
469
|
+
if (event.target === modal) {
|
|
470
|
+
modal.style.display = 'none';
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
</script>
|
|
474
|
+
|
|
475
|
+
<script>
|
|
476
|
+
// Initialize player with tracks
|
|
477
|
+
window.tracks = [
|
|
478
|
+
{{#each release.tracks}}
|
|
479
|
+
{
|
|
480
|
+
url: '{{releasePath url}}',
|
|
481
|
+
title: '{{title}}',
|
|
482
|
+
artist: '{{#if artist}}{{artist}}{{else}}{{../artist.name}}{{/if}}',
|
|
483
|
+
duration: {{#if duration}}{{duration}}{{else}}0{{/if}}
|
|
484
|
+
}{{#unless @last}},{{/unless}}
|
|
485
|
+
{{/each}}
|
|
486
|
+
];
|
|
487
|
+
</script>
|
|
488
|
+
|
|
489
|
+
<!-- Download Stats (GunDB) -->
|
|
490
|
+
<script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
|
491
|
+
<script src="{{assetPath 'assets/download-stats.js'}}"></script>
|
|
492
|
+
<script>
|
|
493
|
+
(function() {
|
|
494
|
+
const releaseSlug = '{{release.slug}}';
|
|
495
|
+
let downloadStats;
|
|
496
|
+
|
|
497
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
498
|
+
// Initialize download stats
|
|
499
|
+
downloadStats = new TunecampDownloadStats();
|
|
500
|
+
window.downloadStats = downloadStats;
|
|
501
|
+
|
|
502
|
+
// Load and display current download count
|
|
503
|
+
loadDownloadCount();
|
|
504
|
+
|
|
505
|
+
// Subscribe to real-time updates
|
|
506
|
+
downloadStats.subscribeToDownloadCount(releaseSlug, updateDownloadDisplay);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
async function loadDownloadCount() {
|
|
510
|
+
try {
|
|
511
|
+
const count = await downloadStats.getDownloadCount(releaseSlug);
|
|
512
|
+
updateDownloadDisplay(count);
|
|
513
|
+
} catch (e) {
|
|
514
|
+
console.warn('Could not load download count:', e);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function updateDownloadDisplay(count) {
|
|
519
|
+
const el = document.getElementById('releaseDownloads');
|
|
520
|
+
const countEl = document.getElementById('downloadCount');
|
|
521
|
+
if (el && countEl) {
|
|
522
|
+
countEl.textContent = downloadStats.formatCount(count);
|
|
523
|
+
el.style.display = 'block';
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Register hook for downloadAll tracking
|
|
528
|
+
window.onDownloadAll = async function() {
|
|
529
|
+
if (downloadStats) {
|
|
530
|
+
try {
|
|
531
|
+
await downloadStats.incrementDownloadCount(releaseSlug);
|
|
532
|
+
} catch (e) {
|
|
533
|
+
console.warn('Could not track download:', e);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// Track individual track downloads
|
|
539
|
+
document.querySelectorAll('.track-download-btn').forEach((btn, index) => {
|
|
540
|
+
btn.addEventListener('click', async function(e) {
|
|
541
|
+
if (downloadStats && window.tracks && window.tracks[index]) {
|
|
542
|
+
try {
|
|
543
|
+
const trackId = window.tracks[index].title.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
|
|
544
|
+
await downloadStats.incrementTrackDownloadCount(releaseSlug, trackId);
|
|
545
|
+
await downloadStats.incrementDownloadCount(releaseSlug);
|
|
546
|
+
} catch (e) {
|
|
547
|
+
console.warn('Could not track track download:', e);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
})();
|
|
553
|
+
</script>
|