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,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Theme Widget
|
|
3
|
+
* Allows users to customize theme colors in real-time
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// Theme Widget Class
|
|
10
|
+
class ThemeWidget {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.isOpen = false;
|
|
13
|
+
this.settings = this.loadSettings();
|
|
14
|
+
this.createWidget();
|
|
15
|
+
this.applySettings();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Default theme settings
|
|
19
|
+
getDefaults() {
|
|
20
|
+
return {
|
|
21
|
+
primaryColor: '#4d4747',
|
|
22
|
+
secondaryColor: '#8b5cf6',
|
|
23
|
+
bgColor: '#000000',
|
|
24
|
+
surfaceColor: '#000000',
|
|
25
|
+
textColor: '#f1f5f9',
|
|
26
|
+
textMuted: '#94a3b8',
|
|
27
|
+
borderColor: '#334155',
|
|
28
|
+
successColor: '#10b981',
|
|
29
|
+
warningColor: '#f59e0b',
|
|
30
|
+
mode: 'dark'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Load settings from localStorage
|
|
35
|
+
loadSettings() {
|
|
36
|
+
try {
|
|
37
|
+
const saved = localStorage.getItem('tunecamp-theme-settings');
|
|
38
|
+
if (saved) {
|
|
39
|
+
return { ...this.getDefaults(), ...JSON.parse(saved) };
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.warn('Could not load theme settings:', e);
|
|
43
|
+
}
|
|
44
|
+
return this.getDefaults();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Save settings to localStorage
|
|
48
|
+
saveSettings() {
|
|
49
|
+
try {
|
|
50
|
+
localStorage.setItem('tunecamp-theme-settings', JSON.stringify(this.settings));
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.warn('Could not save theme settings:', e);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Apply current settings to CSS variables
|
|
57
|
+
applySettings() {
|
|
58
|
+
const root = document.documentElement;
|
|
59
|
+
|
|
60
|
+
if (this.settings.mode === 'light') {
|
|
61
|
+
// Light mode - use lighter variants
|
|
62
|
+
root.style.setProperty('--primary-color', this.settings.primaryColor);
|
|
63
|
+
root.style.setProperty('--secondary-color', this.settings.secondaryColor);
|
|
64
|
+
root.style.setProperty('--bg-color', '#f8fafc');
|
|
65
|
+
root.style.setProperty('--surface-color', '#ffffff');
|
|
66
|
+
root.style.setProperty('--text-color', '#1e293b');
|
|
67
|
+
root.style.setProperty('--text-muted', '#64748b');
|
|
68
|
+
root.style.setProperty('--border-color', '#e2e8f0');
|
|
69
|
+
root.setAttribute('data-theme', 'light');
|
|
70
|
+
} else {
|
|
71
|
+
// Dark mode
|
|
72
|
+
root.style.setProperty('--primary-color', this.settings.primaryColor);
|
|
73
|
+
root.style.setProperty('--secondary-color', this.settings.secondaryColor);
|
|
74
|
+
root.style.setProperty('--bg-color', this.settings.bgColor);
|
|
75
|
+
root.style.setProperty('--surface-color', this.settings.surfaceColor);
|
|
76
|
+
root.style.setProperty('--text-color', this.settings.textColor);
|
|
77
|
+
root.style.setProperty('--border-color', this.settings.borderColor);
|
|
78
|
+
root.setAttribute('data-theme', 'dark');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Create the widget UI
|
|
83
|
+
createWidget() {
|
|
84
|
+
// Widget toggle button
|
|
85
|
+
const toggle = document.createElement('button');
|
|
86
|
+
toggle.className = 'theme-widget-toggle';
|
|
87
|
+
toggle.innerHTML = '🎨';
|
|
88
|
+
toggle.title = 'Customize Theme';
|
|
89
|
+
toggle.addEventListener('click', () => this.togglePanel());
|
|
90
|
+
|
|
91
|
+
// Widget panel
|
|
92
|
+
const panel = document.createElement('div');
|
|
93
|
+
panel.className = 'theme-widget-panel';
|
|
94
|
+
panel.innerHTML = `
|
|
95
|
+
<div class="theme-widget-header">
|
|
96
|
+
<span>Theme Settings</span>
|
|
97
|
+
<button class="theme-widget-close">×</button>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="theme-widget-body">
|
|
100
|
+
<div class="theme-widget-row">
|
|
101
|
+
<label>Mode</label>
|
|
102
|
+
<select id="tw-mode">
|
|
103
|
+
<option value="dark">Dark</option>
|
|
104
|
+
<option value="light">Light</option>
|
|
105
|
+
</select>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="theme-widget-row">
|
|
108
|
+
<label>Primary Color</label>
|
|
109
|
+
<input type="color" id="tw-primary" value="${this.settings.primaryColor}">
|
|
110
|
+
</div>
|
|
111
|
+
<div class="theme-widget-row">
|
|
112
|
+
<label>Secondary Color</label>
|
|
113
|
+
<input type="color" id="tw-secondary" value="${this.settings.secondaryColor}">
|
|
114
|
+
</div>
|
|
115
|
+
<div class="theme-widget-row dark-only">
|
|
116
|
+
<label>Background</label>
|
|
117
|
+
<input type="color" id="tw-bg" value="${this.settings.bgColor}">
|
|
118
|
+
</div>
|
|
119
|
+
<div class="theme-widget-row dark-only">
|
|
120
|
+
<label>Surface</label>
|
|
121
|
+
<input type="color" id="tw-surface" value="${this.settings.surfaceColor}">
|
|
122
|
+
</div>
|
|
123
|
+
<div class="theme-widget-actions">
|
|
124
|
+
<button id="tw-reset">Reset</button>
|
|
125
|
+
<button id="tw-export">Export CSS</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
// Add styles
|
|
131
|
+
const styles = document.createElement('style');
|
|
132
|
+
styles.textContent = `
|
|
133
|
+
.theme-widget-toggle {
|
|
134
|
+
position: fixed;
|
|
135
|
+
bottom: 20px;
|
|
136
|
+
right: 20px;
|
|
137
|
+
width: 50px;
|
|
138
|
+
height: 50px;
|
|
139
|
+
border-radius: 50%;
|
|
140
|
+
background: var(--primary-color);
|
|
141
|
+
border: none;
|
|
142
|
+
font-size: 24px;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
|
145
|
+
z-index: 9999;
|
|
146
|
+
transition: transform 0.3s, box-shadow 0.3s;
|
|
147
|
+
}
|
|
148
|
+
.theme-widget-toggle:hover {
|
|
149
|
+
transform: scale(1.1);
|
|
150
|
+
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
|
|
151
|
+
}
|
|
152
|
+
.theme-widget-panel {
|
|
153
|
+
position: fixed;
|
|
154
|
+
bottom: 80px;
|
|
155
|
+
right: 20px;
|
|
156
|
+
width: 280px;
|
|
157
|
+
background: var(--surface-color);
|
|
158
|
+
border: 1px solid var(--border-color);
|
|
159
|
+
border-radius: 12px;
|
|
160
|
+
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
|
161
|
+
z-index: 9998;
|
|
162
|
+
display: none;
|
|
163
|
+
font-family: system-ui, sans-serif;
|
|
164
|
+
}
|
|
165
|
+
.theme-widget-panel.open {
|
|
166
|
+
display: block;
|
|
167
|
+
animation: slideUp 0.2s ease;
|
|
168
|
+
}
|
|
169
|
+
@keyframes slideUp {
|
|
170
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
171
|
+
to { opacity: 1; transform: translateY(0); }
|
|
172
|
+
}
|
|
173
|
+
.theme-widget-header {
|
|
174
|
+
display: flex;
|
|
175
|
+
justify-content: space-between;
|
|
176
|
+
align-items: center;
|
|
177
|
+
padding: 1rem;
|
|
178
|
+
border-bottom: 1px solid var(--border-color);
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
color: var(--text-color);
|
|
181
|
+
}
|
|
182
|
+
.theme-widget-close {
|
|
183
|
+
background: none;
|
|
184
|
+
border: none;
|
|
185
|
+
font-size: 20px;
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
color: var(--text-muted);
|
|
188
|
+
}
|
|
189
|
+
.theme-widget-body {
|
|
190
|
+
padding: 1rem;
|
|
191
|
+
}
|
|
192
|
+
.theme-widget-row {
|
|
193
|
+
display: flex;
|
|
194
|
+
justify-content: space-between;
|
|
195
|
+
align-items: center;
|
|
196
|
+
margin-bottom: 0.75rem;
|
|
197
|
+
}
|
|
198
|
+
.theme-widget-row label {
|
|
199
|
+
color: var(--text-color);
|
|
200
|
+
font-size: 0.9rem;
|
|
201
|
+
}
|
|
202
|
+
.theme-widget-row input[type="color"] {
|
|
203
|
+
width: 40px;
|
|
204
|
+
height: 30px;
|
|
205
|
+
border: none;
|
|
206
|
+
border-radius: 4px;
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
}
|
|
209
|
+
.theme-widget-row select {
|
|
210
|
+
padding: 0.5rem;
|
|
211
|
+
border-radius: 4px;
|
|
212
|
+
border: 1px solid var(--border-color);
|
|
213
|
+
background: var(--bg-color);
|
|
214
|
+
color: var(--text-color);
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
}
|
|
217
|
+
.theme-widget-actions {
|
|
218
|
+
display: flex;
|
|
219
|
+
gap: 0.5rem;
|
|
220
|
+
margin-top: 1rem;
|
|
221
|
+
padding-top: 1rem;
|
|
222
|
+
border-top: 1px solid var(--border-color);
|
|
223
|
+
}
|
|
224
|
+
.theme-widget-actions button {
|
|
225
|
+
flex: 1;
|
|
226
|
+
padding: 0.5rem;
|
|
227
|
+
border: none;
|
|
228
|
+
border-radius: 6px;
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
font-size: 0.85rem;
|
|
231
|
+
}
|
|
232
|
+
#tw-reset {
|
|
233
|
+
background: var(--border-color);
|
|
234
|
+
color: var(--text-color);
|
|
235
|
+
}
|
|
236
|
+
#tw-export {
|
|
237
|
+
background: var(--primary-color);
|
|
238
|
+
color: white;
|
|
239
|
+
}
|
|
240
|
+
.dark-only {
|
|
241
|
+
display: flex;
|
|
242
|
+
}
|
|
243
|
+
[data-theme="light"] .dark-only {
|
|
244
|
+
display: none;
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
|
|
248
|
+
document.head.appendChild(styles);
|
|
249
|
+
document.body.appendChild(toggle);
|
|
250
|
+
document.body.appendChild(panel);
|
|
251
|
+
|
|
252
|
+
this.toggle = toggle;
|
|
253
|
+
this.panel = panel;
|
|
254
|
+
this.bindEvents();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Bind event handlers
|
|
258
|
+
bindEvents() {
|
|
259
|
+
// Close button
|
|
260
|
+
this.panel.querySelector('.theme-widget-close').addEventListener('click', () => {
|
|
261
|
+
this.togglePanel();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Mode select
|
|
265
|
+
const modeSelect = this.panel.querySelector('#tw-mode');
|
|
266
|
+
modeSelect.value = this.settings.mode;
|
|
267
|
+
modeSelect.addEventListener('change', (e) => {
|
|
268
|
+
this.settings.mode = e.target.value;
|
|
269
|
+
this.applySettings();
|
|
270
|
+
this.saveSettings();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Color inputs
|
|
274
|
+
this.bindColorInput('tw-primary', 'primaryColor');
|
|
275
|
+
this.bindColorInput('tw-secondary', 'secondaryColor');
|
|
276
|
+
this.bindColorInput('tw-bg', 'bgColor');
|
|
277
|
+
this.bindColorInput('tw-surface', 'surfaceColor');
|
|
278
|
+
|
|
279
|
+
// Reset button
|
|
280
|
+
this.panel.querySelector('#tw-reset').addEventListener('click', () => {
|
|
281
|
+
this.settings = this.getDefaults();
|
|
282
|
+
this.applySettings();
|
|
283
|
+
this.saveSettings();
|
|
284
|
+
this.updateInputs();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Export button
|
|
288
|
+
this.panel.querySelector('#tw-export').addEventListener('click', () => {
|
|
289
|
+
this.exportCSS();
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Bind a color input
|
|
294
|
+
bindColorInput(inputId, settingKey) {
|
|
295
|
+
const input = this.panel.querySelector(`#${inputId}`);
|
|
296
|
+
if (input) {
|
|
297
|
+
input.value = this.settings[settingKey];
|
|
298
|
+
input.addEventListener('input', (e) => {
|
|
299
|
+
this.settings[settingKey] = e.target.value;
|
|
300
|
+
this.applySettings();
|
|
301
|
+
this.saveSettings();
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Update all input values
|
|
307
|
+
updateInputs() {
|
|
308
|
+
const inputs = {
|
|
309
|
+
'tw-mode': 'mode',
|
|
310
|
+
'tw-primary': 'primaryColor',
|
|
311
|
+
'tw-secondary': 'secondaryColor',
|
|
312
|
+
'tw-bg': 'bgColor',
|
|
313
|
+
'tw-surface': 'surfaceColor'
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
for (const [id, key] of Object.entries(inputs)) {
|
|
317
|
+
const el = this.panel.querySelector(`#${id}`);
|
|
318
|
+
if (el) el.value = this.settings[key];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Toggle panel visibility
|
|
323
|
+
togglePanel() {
|
|
324
|
+
this.isOpen = !this.isOpen;
|
|
325
|
+
this.panel.classList.toggle('open', this.isOpen);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Export current theme as CSS
|
|
329
|
+
exportCSS() {
|
|
330
|
+
const css = `:root {
|
|
331
|
+
--primary-color: ${this.settings.primaryColor};
|
|
332
|
+
--secondary-color: ${this.settings.secondaryColor};
|
|
333
|
+
--bg-color: ${this.settings.bgColor};
|
|
334
|
+
--surface-color: ${this.settings.surfaceColor};
|
|
335
|
+
}`;
|
|
336
|
+
|
|
337
|
+
// Copy to clipboard
|
|
338
|
+
navigator.clipboard.writeText(css).then(() => {
|
|
339
|
+
alert('CSS copied to clipboard!');
|
|
340
|
+
}).catch(() => {
|
|
341
|
+
// Fallback: show in prompt
|
|
342
|
+
prompt('Copy this CSS:', css);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Initialize widget when DOM is ready
|
|
348
|
+
if (document.readyState === 'loading') {
|
|
349
|
+
document.addEventListener('DOMContentLoaded', () => new ThemeWidget());
|
|
350
|
+
} else {
|
|
351
|
+
new ThemeWidget();
|
|
352
|
+
}
|
|
353
|
+
})();
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tunecamp Unlock Codes - GunDB Client
|
|
3
|
+
* Decentralized unlock code validation using GunDB public peers
|
|
4
|
+
*
|
|
5
|
+
* Usage for beginners:
|
|
6
|
+
* 1. Include GunDB: <script src="https://cdn.jsdelivr.net/npm/gun/gun.js"></script>
|
|
7
|
+
* 2. Include this script
|
|
8
|
+
* 3. Initialize: const unlockCodes = new TunecampUnlockCodes();
|
|
9
|
+
* 4. Validate: unlockCodes.validateCode(releaseSlug, userCode).then(valid => {...})
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
(function() {
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
// Default public GunDB peers
|
|
16
|
+
const DEFAULT_PEERS = [
|
|
17
|
+
'https://gun-manhattan.herokuapp.com/gun',
|
|
18
|
+
'https://gun-us.herokuapp.com/gun',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* TunecampUnlockCodes class
|
|
23
|
+
* Handles code validation and redemption via GunDB
|
|
24
|
+
*/
|
|
25
|
+
class TunecampUnlockCodes {
|
|
26
|
+
/**
|
|
27
|
+
* Initialize the unlock codes system
|
|
28
|
+
* @param {Object} options - Configuration options
|
|
29
|
+
* @param {Array} options.peers - GunDB peer URLs (default: public peers)
|
|
30
|
+
* @param {string} options.namespace - GunDB namespace (default: 'tunecamp')
|
|
31
|
+
*/
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
this.peers = options.peers || DEFAULT_PEERS;
|
|
34
|
+
this.namespace = options.namespace || 'tunecamp';
|
|
35
|
+
this.gun = null;
|
|
36
|
+
this.initialized = false;
|
|
37
|
+
|
|
38
|
+
// Initialize GunDB when script loads
|
|
39
|
+
this.init();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Initialize GunDB connection
|
|
44
|
+
*/
|
|
45
|
+
async init() {
|
|
46
|
+
// Check if Gun is available
|
|
47
|
+
if (typeof Gun === 'undefined') {
|
|
48
|
+
console.warn('GunDB not loaded. Please include gun.js before unlock-codes.js');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Initialize Gun with peers
|
|
53
|
+
this.gun = Gun({
|
|
54
|
+
peers: this.peers,
|
|
55
|
+
localStorage: true, // Use localStorage for offline caching
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.initialized = true;
|
|
59
|
+
console.log('🔐 Tunecamp Unlock Codes initialized');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Wait for Gun to be ready
|
|
64
|
+
*/
|
|
65
|
+
async waitForInit() {
|
|
66
|
+
if (this.initialized) return;
|
|
67
|
+
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const check = () => {
|
|
70
|
+
if (this.initialized) {
|
|
71
|
+
resolve();
|
|
72
|
+
} else {
|
|
73
|
+
setTimeout(check, 100);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
check();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Hash a code for secure storage
|
|
82
|
+
* @param {string} code - The unlock code
|
|
83
|
+
* @returns {string} Hashed code
|
|
84
|
+
*/
|
|
85
|
+
async hashCode(code) {
|
|
86
|
+
const encoder = new TextEncoder();
|
|
87
|
+
const data = encoder.encode(code.toLowerCase().trim());
|
|
88
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
89
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
90
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate an unlock code
|
|
95
|
+
* @param {string} releaseSlug - The release identifier
|
|
96
|
+
* @param {string} code - The unlock code entered by user
|
|
97
|
+
* @returns {Promise<Object>} Validation result
|
|
98
|
+
*/
|
|
99
|
+
async validateCode(releaseSlug, code) {
|
|
100
|
+
await this.waitForInit();
|
|
101
|
+
|
|
102
|
+
if (!this.gun) {
|
|
103
|
+
return { valid: false, error: 'GunDB not initialized' };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const codeHash = await this.hashCode(code);
|
|
107
|
+
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
this.gun
|
|
110
|
+
.get(this.namespace)
|
|
111
|
+
.get('releases')
|
|
112
|
+
.get(releaseSlug)
|
|
113
|
+
.get('codes')
|
|
114
|
+
.get(codeHash)
|
|
115
|
+
.once((data) => {
|
|
116
|
+
if (!data) {
|
|
117
|
+
resolve({ valid: false, error: 'Invalid code' });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (data.used) {
|
|
122
|
+
resolve({ valid: false, error: 'Code already used' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
resolve({
|
|
127
|
+
valid: true,
|
|
128
|
+
data: {
|
|
129
|
+
maxDownloads: data.maxDownloads || 1,
|
|
130
|
+
currentDownloads: data.downloads || 0,
|
|
131
|
+
expiresAt: data.expiresAt,
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Redeem a code (mark as used)
|
|
140
|
+
* @param {string} releaseSlug - The release identifier
|
|
141
|
+
* @param {string} code - The unlock code
|
|
142
|
+
* @returns {Promise<Object>} Redemption result
|
|
143
|
+
*/
|
|
144
|
+
async redeemCode(releaseSlug, code) {
|
|
145
|
+
await this.waitForInit();
|
|
146
|
+
|
|
147
|
+
if (!this.gun) {
|
|
148
|
+
return { success: false, error: 'GunDB not initialized' };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const codeHash = await this.hashCode(code);
|
|
152
|
+
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
const codeRef = this.gun
|
|
155
|
+
.get(this.namespace)
|
|
156
|
+
.get('releases')
|
|
157
|
+
.get(releaseSlug)
|
|
158
|
+
.get('codes')
|
|
159
|
+
.get(codeHash);
|
|
160
|
+
|
|
161
|
+
codeRef.once((data) => {
|
|
162
|
+
if (!data) {
|
|
163
|
+
resolve({ success: false, error: 'Invalid code' });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (data.used) {
|
|
168
|
+
resolve({ success: false, error: 'Code already used' });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check max downloads
|
|
173
|
+
const downloads = (data.downloads || 0) + 1;
|
|
174
|
+
const maxDownloads = data.maxDownloads || 1;
|
|
175
|
+
const used = downloads >= maxDownloads;
|
|
176
|
+
|
|
177
|
+
// Update the code
|
|
178
|
+
codeRef.put({
|
|
179
|
+
...data,
|
|
180
|
+
downloads,
|
|
181
|
+
used,
|
|
182
|
+
lastUsedAt: Date.now(),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
resolve({
|
|
186
|
+
success: true,
|
|
187
|
+
downloadsRemaining: maxDownloads - downloads
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get release info (for artists to check their codes)
|
|
195
|
+
* @param {string} releaseSlug - The release identifier
|
|
196
|
+
*/
|
|
197
|
+
async getReleaseInfo(releaseSlug) {
|
|
198
|
+
await this.waitForInit();
|
|
199
|
+
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
const codes = [];
|
|
202
|
+
|
|
203
|
+
this.gun
|
|
204
|
+
.get(this.namespace)
|
|
205
|
+
.get('releases')
|
|
206
|
+
.get(releaseSlug)
|
|
207
|
+
.get('codes')
|
|
208
|
+
.map()
|
|
209
|
+
.once((data, key) => {
|
|
210
|
+
if (data) {
|
|
211
|
+
codes.push({ hash: key, ...data });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Give it some time to collect all codes
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
resolve(codes);
|
|
218
|
+
}, 1000);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Expose globally
|
|
224
|
+
window.TunecampUnlockCodes = TunecampUnlockCodes;
|
|
225
|
+
})();
|