SaltProphet commited on
Commit
229a072
·
verified ·
1 Parent(s): 0bc0d81

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +383 -19
index.html CHANGED
@@ -1,19 +1,383 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>LittleBunnyPaws' Magic Co-Pilot</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Mochiy+Pop+One&display=swap" rel="stylesheet">
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.7.77/Tone.js"></script>
10
+ <style>
11
+ body { font-family: 'Inter', sans-serif; background-color: #fdf2f8; } /* pink-50 */
12
+ .font-brand { font-family: 'Mochiy Pop One', sans-serif; }
13
+ .hidden { display: none; }
14
+ .tip-bar-progress { transition: width 0.5s ease-in-out; }
15
+ .input-glow { transition: box-shadow 0.3s ease; }
16
+ .input-glow:focus { box-shadow: 0 0 0 3px rgba(236, 72, 153, 0.4); } /* pink-500 with opacity */
17
+ </style>
18
+ </head>
19
+ <body>
20
+
21
+ <!-- =================================================================== -->
22
+ <!-- SECTION 1: INITIAL SETUP (Ask for username) -->
23
+ <!-- =================================================================== -->
24
+ <div id="setup-view" class="flex items-center justify-center min-h-screen">
25
+ <div class="w-full max-w-md mx-auto bg-white rounded-2xl shadow-xl p-8 border-4 border-pink-200 text-center">
26
+ <h1 class="font-brand text-4xl text-pink-500 mb-4">🐰 Welcome! 🐰</h1>
27
+ <p class="text-gray-600 mb-6">Let's get your stream co-pilot ready. Please enter your Chaturbate username below.</p>
28
+ <input type="text" id="username-input" placeholder="e.g., littlebunnypaws" class="w-full p-3 text-center border-2 border-gray-300 rounded-lg mb-4 text-lg input-glow focus:outline-none focus:border-pink-400">
29
+ <button id="start-button" class="w-full bg-pink-500 hover:bg-pink-600 text-white font-bold py-3 px-6 rounded-lg shadow-md text-xl">Start Co-Pilot</button>
30
+ </div>
31
+ </div>
32
+
33
+ <!-- =================================================================== -->
34
+ <!-- SECTION 2: CONTROL PANEL VIEW (Her private controls) -->
35
+ <!-- =================================================================== -->
36
+ <div id="control-panel-view" class="hidden p-4">
37
+ <div class="w-full max-w-4xl mx-auto bg-white rounded-2xl shadow-xl p-6 border-4 border-pink-200">
38
+ <header class="text-center mb-4">
39
+ <h1 class="font-brand text-3xl md:text-4xl text-pink-500">🐰 Stream Co-Pilot 🐰</h1>
40
+ <p class="text-gray-600">You are connected to <strong id="display-username" class="text-pink-600"></strong>'s chat.</p>
41
+ </header>
42
+
43
+ <div class="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4 rounded-lg mb-6">
44
+ <h3 class="font-bold">Your OBS Overlay Link</h3>
45
+ <p class="text-sm">Copy this link and paste it into a new Browser Source in OBS. This is what your viewers will see.</p>
46
+ <input type="text" id="obs-link-display" readonly class="w-full bg-blue-50 p-2 mt-2 rounded border border-blue-300 select-all">
47
+ </div>
48
+
49
+ <div class="bg-pink-100 rounded-xl p-4 my-6 text-center">
50
+ <div id="group-timer-display" class="font-brand text-7xl md:text-8xl text-pink-600 tracking-wider">00:00</div>
51
+ </div>
52
+ <div id="group-status-message" class="text-center text-lg font-semibold text-gray-600 h-8 mb-4">Set up your stream below!</div>
53
+
54
+ <div class="grid md:grid-cols-2 gap-6">
55
+ <!-- Live Controls -->
56
+ <div class="bg-gray-50 rounded-xl p-4">
57
+ <h3 class="font-brand text-xl text-center text-gray-700 mb-4">Live Controls</h3>
58
+ <div class="flex items-center justify-center gap-4 mb-4">
59
+ <input type="number" id="group-minutes" value="5" min="1" class="w-24 p-2 text-center border-2 border-gray-300 rounded-lg" placeholder="Mins">
60
+ <button id="group-start-btn" class="w-full bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-6 rounded-lg shadow-md">Start</button>
61
+ <button id="group-stop-btn" class="w-full bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-6 rounded-lg shadow-md hidden">Reset</button>
62
+ </div>
63
+ <div class="text-center border-t-2 border-gray-200 pt-4 mt-4">
64
+ <p class="font-semibold text-gray-700 mb-3">🛠️ Manual Tip Override 🛠️</p>
65
+ <div id="dynamic-tip-buttons-container" class="flex flex-wrap justify-center gap-3"><p class="text-gray-500 text-sm">Create tip rules to add manual buttons.</p></div>
66
+ </div>
67
+ </div>
68
+
69
+ <!-- Setup -->
70
+ <div class="bg-gray-100 rounded-xl p-4">
71
+ <h3 class="font-brand text-xl text-center text-gray-700 mb-4">Stream Setup</h3>
72
+ <div class="space-y-4">
73
+ <div>
74
+ <label for="tip-goal-input" class="block font-semibold text-gray-700 mb-1">Community Tip Goal</label>
75
+ <div class="flex items-center gap-2">
76
+ <input type="number" id="tip-goal-input" value="1000" class="w-full p-2 border-gray-300 rounded-lg">
77
+ <button id="set-goal-btn" class="bg-pink-500 text-white font-bold py-2 px-4 rounded-lg">Set</button>
78
+ </div>
79
+ </div>
80
+ <div>
81
+ <label for="sound-select" class="block font-semibold text-gray-700 mb-1">Tip Sound Alert</label>
82
+ <select id="sound-select" class="w-full p-2 border-gray-300 rounded-lg">
83
+ <option value="chime">Chime</option><option value="coin">Coin</option><option value="powerup">Power Up</option><option value="boop">Boop</option><option value="none">None</option>
84
+ </select>
85
+ </div>
86
+ <div class="border-t-2 pt-4">
87
+ <label class="block font-semibold text-gray-700 mb-2 text-center">Tip-to-Time Automation Rules</label>
88
+ <div class="flex items-center justify-center gap-2">
89
+ <input type="number" id="tip-token-input" placeholder="Tokens" class="w-24 p-2 text-center border-gray-300 rounded-lg">
90
+ <input type="number" id="tip-time-input" placeholder="Seconds" class="w-24 p-2 text-center border-gray-300 rounded-lg">
91
+ <button id="add-tip-button-btn" class="bg-blue-500 text-white font-bold py-2 px-4 rounded-lg">Add Rule</button>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- =================================================================== -->
101
+ <!-- SECTION 3: ON-STREAM OVERLAY VIEW (What viewers see) -->
102
+ <!-- =================================================================== -->
103
+ <div id="overlay-view" class="hidden p-4">
104
+ <div class="w-full max-w-2xl mx-auto">
105
+ <div class="bg-black bg-opacity-50 rounded-full p-1 border-2 border-pink-300 shadow-lg">
106
+ <div class="relative w-full h-10 flex items-center justify-center">
107
+ <div id="tip-bar-progress" class="absolute top-0 left-0 h-full bg-gradient-to-r from-pink-400 to-purple-500 rounded-full tip-bar-progress" style="width: 0%;"></div>
108
+ <div class="relative z-10 font-brand text-white text-lg tracking-wider text-shadow">
109
+ <span class="font-bold">BUNNY BONUS:</span>
110
+ <span id="current-tips-display">0</span> / <span id="tip-goal-display">1000</span> TOKENS
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+
118
+ <script>
119
+ // --- APP STATE & CONFIG ---
120
+ const APP_STATE_KEY = 'bunnyCoPilotState';
121
+ const TIP_PATTERN = /"m":"(\w+) has tipped (\d+) tokens!"/; // Updated for common bot format
122
+
123
+ // --- DOM Elements ---
124
+ const views = {
125
+ setup: document.getElementById('setup-view'),
126
+ controls: document.getElementById('control-panel-view'),
127
+ overlay: document.getElementById('overlay-view'),
128
+ };
129
+ const elements = {
130
+ usernameInput: document.getElementById('username-input'),
131
+ startButton: document.getElementById('start-button'),
132
+ displayUsername: document.getElementById('display-username'),
133
+ obsLinkDisplay: document.getElementById('obs-link-display'),
134
+ timer: document.getElementById('group-timer-display'),
135
+ status: document.getElementById('group-status-message'),
136
+ minutesInput: document.getElementById('group-minutes'),
137
+ startBtn: document.getElementById('group-start-btn'),
138
+ stopBtn: document.getElementById('group-stop-btn'),
139
+ buttonsContainer: document.getElementById('dynamic-tip-buttons-container'),
140
+ tipGoalInput: document.getElementById('tip-goal-input'),
141
+ setGoalBtn: document.getElementById('set-goal-btn'),
142
+ currentTipsDisplay: document.getElementById('current-tips-display'),
143
+ tipGoalDisplay: document.getElementById('tip-goal-display'),
144
+ tipBarProgress: document.getElementById('tip-bar-progress'),
145
+ soundSelect: document.getElementById('sound-select'),
146
+ addRuleBtn: document.getElementById('add-tip-button-btn'),
147
+ tokenInput: document.getElementById('tip-token-input'),
148
+ timeInput: document.getElementById('tip-time-input'),
149
+ };
150
+
151
+ // --- SOUNDS ---
152
+ let sounds = {};
153
+ const initSounds = () => {
154
+ sounds = {
155
+ chime: new Tone.Synth({ oscillator: { type: "sine" }, envelope: { attack: 0.005, decay: 0.1, sustain: 0.3, release: 1 } }).toDestination(),
156
+ coin: new Tone.Synth({ oscillator: { type: "square" }, envelope: { attack: 0.001, decay: 0.1, sustain: 0, release: 0.1 } }).toDestination(),
157
+ powerup: new Tone.Synth({ oscillator: { type: "triangle" }, envelope: { attack: 0.01, decay: 0.2, sustain: 0.1, release: 0.2 } }).toDestination(),
158
+ boop: new Tone.MembraneSynth().toDestination()
159
+ };
160
+ };
161
+ const playSound = (soundName) => {
162
+ if (soundName === 'none' || !sounds[soundName]) return;
163
+ Tone.start().then(() => {
164
+ if (soundName === 'chime') sounds.chime.triggerAttackRelease("C5", "8n");
165
+ if (soundName === 'coin') sounds.coin.triggerAttackRelease("E6", "16n");
166
+ if (soundName === 'powerup') { const now = Tone.now(); sounds.powerup.triggerAttackRelease("C4", "16n", now); sounds.powerup.triggerAttackRelease("G4", "16n", now + 0.1); sounds.powerup.triggerAttackRelease("C5", "16n", now + 0.2); }
167
+ if (soundName === 'boop') sounds.boop.triggerAttackRelease("C2", "8n");
168
+ });
169
+ };
170
+
171
+ // --- STATE MANAGEMENT ---
172
+ let state = {
173
+ username: '',
174
+ isTimerRunning: false,
175
+ timeLeft: 0,
176
+ tipGoal: 1000,
177
+ currentTips: 0,
178
+ automationRules: [],
179
+ selectedSound: 'chime',
180
+ };
181
+ let timerInterval = null;
182
+
183
+ const saveState = () => localStorage.setItem(APP_STATE_KEY, JSON.stringify(state));
184
+ const loadState = () => Object.assign(state, JSON.parse(localStorage.getItem(APP_STATE_KEY)));
185
+
186
+ // --- UI RENDERING ---
187
+ const formatTime = (s) => `${Math.floor(s/60)}`.padStart(2,'0')+':'+`${s%60}`.padStart(2,'0');
188
+
189
+ function render() {
190
+ // Render for both controls and overlay
191
+ elements.timer.textContent = formatTime(state.timeLeft);
192
+ elements.currentTipsDisplay.textContent = state.currentTips;
193
+ elements.tipGoalDisplay.textContent = state.tipGoal;
194
+ const percentage = Math.min(100, (state.currentTips / state.tipGoal) * 100);
195
+ elements.tipBarProgress.style.width = `${percentage}%`;
196
+
197
+ // Render for controls only
198
+ elements.tipGoalInput.value = state.tipGoal;
199
+ elements.soundSelect.value = state.selectedSound;
200
+ elements.startBtn.classList.toggle('hidden', state.isTimerRunning);
201
+ elements.stopBtn.classList.toggle('hidden', !state.isTimerRunning);
202
+
203
+ elements.buttonsContainer.innerHTML = '';
204
+ if (state.automationRules.length === 0) {
205
+ elements.buttonsContainer.innerHTML = '<p class="text-gray-500 text-sm">Create automation rules.</p>';
206
+ } else {
207
+ state.automationRules.forEach(rule => {
208
+ const button = document.createElement('button');
209
+ button.className = 'bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-lg shadow-md text-sm';
210
+ button.textContent = `${rule.tokens}t (+${formatTime(rule.seconds)})`;
211
+ button.onclick = () => processTip(rule.tokens, true); // true for manual override
212
+ elements.buttonsContainer.appendChild(button);
213
+ });
214
+ }
215
+ }
216
+
217
+ // --- CORE LOGIC ---
218
+ function processTip(tokens, isManual = false) {
219
+ if (!isManual && !state.isTimerRunning) return; // Don't auto-add time if timer isn't running
220
+
221
+ const rule = state.automationRules.find(r => r.tokens === tokens);
222
+ if (!rule) return;
223
+
224
+ state.timeLeft += rule.seconds;
225
+ state.currentTips += tokens;
226
+ playSound(state.selectedSound);
227
+ saveState();
228
+ render(); // Immediately render for controls, event listener handles overlay
229
+ }
230
+
231
+ function startTimer() {
232
+ if (state.isTimerRunning) return;
233
+ const minutes = parseInt(elements.minutesInput.value, 10);
234
+ if (isNaN(minutes) || minutes <= 0) return;
235
+
236
+ state.isTimerRunning = true;
237
+ state.timeLeft = minutes * 60;
238
+ elements.status.textContent = "Timer is LIVE!";
239
+
240
+ clearInterval(timerInterval);
241
+ timerInterval = setInterval(() => {
242
+ state.timeLeft--;
243
+ if (state.timeLeft <= 0) {
244
+ state.timeLeft = 0;
245
+ state.isTimerRunning = false;
246
+ clearInterval(timerInterval);
247
+ elements.status.textContent = "Time's up! Tip to add more!";
248
+ }
249
+ saveState();
250
+ render();
251
+ }, 1000);
252
+
253
+ saveState();
254
+ render();
255
+ }
256
+
257
+ function stopTimer() {
258
+ state.isTimerRunning = false;
259
+ state.timeLeft = 0;
260
+ state.currentTips = 0; // Reset progress
261
+ clearInterval(timerInterval);
262
+ elements.status.textContent = "Timer Reset. Ready to go!";
263
+ saveState();
264
+ render();
265
+ }
266
+
267
+ // --- CHATURBATE CONNECTION ---
268
+ async function watchChat() {
269
+ console.log(`Connecting to ${state.username}'s chat...`);
270
+ try {
271
+ const api_url = `https://chaturbate.com/get_edge_host/?room=${state.username}&type=chat`;
272
+ const response = await fetch(api_url).then(res => res.json());
273
+ if (!response.host) throw new Error("Could not find chat server.");
274
+
275
+ const ws_url = `wss://${response.host}/`;
276
+ const socket = new WebSocket(ws_url);
277
+
278
+ socket.onopen = () => {
279
+ console.log("Chat connection established.");
280
+ socket.send(`{"method":"connect","data":{"user":"guest_obs-${Date.now()}","room":"${state.username}","password":""}}`);
281
+ socket.send(`{"method":"joinroom","data":{"room":"${state.username}"}}`);
282
+ elements.status.textContent = "Automation connected! Ready to go live.";
283
+ };
284
+
285
+ socket.onmessage = (event) => {
286
+ const msg = event.data;
287
+ const match = TIP_PATTERN.exec(msg);
288
+ if (match) {
289
+ const tokens = parseInt(match[2], 10);
290
+ console.log(`Tip detected: ${tokens} tokens`);
291
+ processTip(tokens);
292
+ }
293
+ };
294
+
295
+ socket.onclose = () => {
296
+ console.warn("Chat connection closed. Reconnecting in 5 seconds...");
297
+ elements.status.textContent = "Automation lost! Reconnecting...";
298
+ setTimeout(watchChat, 5000);
299
+ };
300
+ socket.onerror = (err) => {
301
+ console.error("Chat socket error:", err);
302
+ socket.close();
303
+ };
304
+
305
+ } catch (error) {
306
+ console.error("Failed to connect to chat:", error);
307
+ elements.status.textContent = "Connection failed! Retrying...";
308
+ setTimeout(watchChat, 10000);
309
+ }
310
+ }
311
+
312
+ // --- INITIALIZATION & ROUTING ---
313
+ function main() {
314
+ const urlParams = new URLSearchParams(window.location.search);
315
+ const view = urlParams.get('view');
316
+ const username = urlParams.get('room');
317
+
318
+ if (view === 'overlay' && username) {
319
+ // --- OVERLAY VIEW ---
320
+ document.body.style.backgroundColor = 'transparent';
321
+ views.setup.classList.add('hidden');
322
+ views.controls.classList.add('hidden');
323
+ views.overlay.classList.remove('hidden');
324
+
325
+ if(localStorage.getItem(APP_STATE_KEY)) loadState();
326
+ render();
327
+
328
+ window.addEventListener('storage', (event) => {
329
+ if (event.key === APP_STATE_KEY) {
330
+ loadState();
331
+ render();
332
+ }
333
+ });
334
+
335
+ } else if (username) {
336
+ // --- CONTROL PANEL VIEW ---
337
+ state.username = username;
338
+ views.setup.classList.add('hidden');
339
+ views.overlay.classList.add('hidden');
340
+ views.controls.classList.remove('hidden');
341
+
342
+ elements.displayUsername.textContent = state.username;
343
+ const overlayUrl = `${window.location.origin}${window.location.pathname}?view=overlay&room=${state.username}`;
344
+ elements.obsLinkDisplay.value = overlayUrl;
345
+
346
+ initSounds(); // Initialize audio context on user interaction
347
+ if(localStorage.getItem(APP_STATE_KEY)) loadState(); // Load previous session settings
348
+ render();
349
+ watchChat();
350
+
351
+ // Setup event listeners for controls
352
+ elements.startButton.onclick = () => initSounds();
353
+ elements.startBtn.onclick = startTimer;
354
+ elements.stopBtn.onclick = stopTimer;
355
+ elements.setGoalBtn.onclick = () => { state.tipGoal = parseInt(elements.tipGoalInput.value, 10); state.currentTips = 0; saveState(); render(); };
356
+ elements.soundSelect.onchange = (e) => { state.selectedSound = e.target.value; saveState(); };
357
+ elements.addRuleBtn.onclick = () => {
358
+ const tokens = parseInt(elements.tokenInput.value, 10);
359
+ const seconds = parseInt(elements.timeInput.value, 10);
360
+ if (!isNaN(tokens) && !isNaN(seconds) && tokens > 0 && seconds > 0) {
361
+ state.automationRules.push({ tokens, seconds });
362
+ state.automationRules.sort((a,b) => a.tokens - b.tokens);
363
+ elements.tokenInput.value = ''; elements.timeInput.value = '';
364
+ saveState(); render();
365
+ }
366
+ };
367
+
368
+ } else {
369
+ // --- SETUP VIEW ---
370
+ views.controls.classList.add('hidden');
371
+ views.overlay.classList.add('hidden');
372
+ views.setup.classList.remove('hidden');
373
+
374
+ elements.startButton.onclick = () => {
375
+ const enteredUsername = elements.usernameInput.value.trim();
376
+ if (enteredUsername) {
377
+ window.location.search = `?room=${enteredUsername}`;
378
+ }
379
+ };
380
+ }
381
+ }
382
+
383
+