haraberget commited on
Commit
beb9575
·
verified ·
1 Parent(s): 4a40c83

Update injection.js

Browse files
Files changed (1) hide show
  1. injection.js +241 -339
injection.js CHANGED
@@ -1,388 +1,290 @@
1
 
2
-
3
  (() => {
4
- // H3 Render Monitor v4 — Kaggle
5
- if (window.hitRenderMonitorTimer) clearInterval(window.hitRenderMonitorTimer);
6
- document.getElementById("hit-render-monitor")?.remove();
7
-
8
- const POSITION_KEY = "hitRenderMonitorPosition";
9
-
10
- const state = {
11
- prompt: "",
12
- progress: 0,
13
- step: 0,
14
- totalSteps: 0,
15
- checkpoints: new Set(),
16
- completedUrl: "",
17
- minimized: false
18
- };
19
-
20
- const panel = document.createElement("div");
21
- panel.id = "hit-render-monitor";
22
-
23
- Object.assign(panel.style, {
24
- position: "fixed",
25
- right: "20px",
26
- bottom: "20px",
27
- width: "440px",
28
- maxHeight: "85vh",
29
- overflow: "hidden",
30
- zIndex: "999999",
31
- background: "#17191f",
32
- color: "#eeeeee",
33
- border: "1px solid #555",
34
- borderRadius: "10px",
35
- padding: "0",
36
- font: "13px/1.45 monospace",
37
- boxShadow: "0 5px 30px #0009"
38
  });
39
 
40
- panel.innerHTML = `
41
- <div id="hit-header" style="
42
- display:flex;justify-content:space-between;align-items:center;
43
- padding:12px 14px;background:#22252d;
44
- border-radius:10px 10px 0 0;cursor:grab;
45
- user-select:none;touch-action:none">
46
- <strong style="font-size:15px">
47
- H3 Monitor <span id="hit-title-progress" style="color:#20b875">· Waiting</span>
48
- </strong>
49
- <div style="display:flex;gap:6px">
50
- <button id="hit-minimize" title="Minimize"
51
- style="background:#444;color:white;border:0;padding:3px 10px;cursor:pointer;font-size:16px">−</button>
52
- <button id="hit-close" title="Close"
53
- style="background:#744;color:white;border:0;padding:3px 9px;cursor:pointer;font-size:15px">×</button>
 
 
 
 
 
 
 
 
 
 
54
  </div>
55
  </div>
56
 
57
- <div id="hit-content" style="padding:14px;max-height:calc(85vh - 45px);overflow-y:auto">
58
-
59
- <strong>RENDER PROGRESS</strong>
60
- <div style="height:12px;background:#333;border-radius:8px;margin:6px 0;overflow:hidden">
61
- <div id="hit-bar" style="height:100%;width:0%;background:#20b875;transition:width .3s"></div>
62
- </div>
63
- <div id="hit-progress" style="font-size:18px">Waiting for H3...</div>
64
- <div id="hit-step" style="color:#aaa"></div>
65
-
66
- <hr style="border-color:#444;margin:12px 0">
67
-
68
- <strong>KAGGLE SESSION</strong>
69
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:8px">
70
- <div class="hit-stat"><small>ACCELERATOR</small><strong id="hit-accelerator">--</strong></div>
71
- <div class="hit-stat"><small>QUOTA</small><strong id="hit-quota">--</strong></div>
72
- <div class="hit-stat"><small>PERSISTENCE</small><strong id="hit-persistence">--</strong></div>
73
- <div class="hit-stat"><small>INTERNET</small><strong id="hit-internet">--</strong></div>
74
- <div class="hit-stat" style="grid-column:span 2"><small>ENVIRONMENT</small><strong id="hit-environment">--</strong></div>
75
  </div>
76
 
77
- <hr style="border-color:#444;margin:12px 0">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- <strong>RESOURCE USAGE</strong>
80
- <div style="color:#aaa;font-size:11px;margin-top:6px">
81
- Live hardware readings not yet connected.
 
82
  </div>
83
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:8px">
84
- <div class="hit-stat"><small>CPU</small><strong>--</strong></div>
85
- <div class="hit-stat"><small>RAM</small><strong>--</strong></div>
86
- <div class="hit-stat"><small>DISK</small><strong>--</strong></div>
87
- <div class="hit-stat"><small>GPU / VRAM</small><strong>--</strong></div>
 
 
 
 
 
 
 
 
 
 
 
 
88
  </div>
 
 
89
 
90
- <hr style="border-color:#444;margin:12px 0">
91
 
92
- <strong>PROMPT INSPECTOR</strong>
93
- <div id="hit-prompt" style="background:#242730;padding:8px;margin-top:6px;max-height:125px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;color:#ddd">Waiting for prompt...</div>
 
94
 
95
- <hr style="border-color:#444;margin:12px 0">
 
 
96
 
97
- <div style="display:flex;justify-content:space-between;align-items:center">
98
- <strong>CALLBACK SIMULATOR</strong>
99
- <label style="font-size:11px">
100
- <input id="hit-report" type="checkbox" checked> Report to 1hit.no
101
- </label>
102
- </div>
103
- <div style="color:#e6b85c;font-size:11px;margin:4px 0 8px">
104
- Simulation only — no network requests
105
- </div>
106
- <div id="hit-json" style="background:#090b0e;padding:9px;max-height:230px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font-size:11px;color:#9fe5b2">Waiting for events...</div>
107
 
108
- <hr style="border-color:#444;margin:12px 0">
109
 
110
- <strong>OUTPUT URL</strong>
111
- <div id="hit-url" style="color:#8fcaff;overflow-wrap:anywhere;margin-top:5px">Waiting for video...</div>
112
- <div id="hit-status" style="color:#aaa;font-size:11px;margin-top:10px">Polling every 15 seconds</div>
113
- </div>
114
- `;
115
 
116
- document.body.appendChild(panel);
117
-
118
- // Style the statistic cards.
119
- panel.querySelectorAll(".hit-stat").forEach(card => {
120
- Object.assign(card.style, {
121
- background: "#242730",
122
- padding: "8px",
123
- borderRadius: "6px",
124
- minWidth: "0",
125
- overflowWrap: "anywhere"
126
- });
127
- const small = card.querySelector("small");
128
- const strong = card.querySelector("strong");
129
- if (small) Object.assign(small.style, {
130
- display: "block", color: "#aaa", fontSize: "10px", marginBottom: "3px"
131
- });
132
- if (strong) Object.assign(strong.style, {fontSize: "12px"});
133
- });
134
 
135
- const el = id => panel.querySelector("#" + id);
136
 
137
- // Restore previous popup position.
138
- try {
139
- const saved = JSON.parse(sessionStorage.getItem(POSITION_KEY) || "null");
140
- if (saved && Number.isFinite(saved.left) && Number.isFinite(saved.top)) {
141
- panel.style.left = saved.left + "px";
142
- panel.style.top = saved.top + "px";
143
- panel.style.right = "auto";
144
- panel.style.bottom = "auto";
145
- }
146
- } catch (e) {}
147
 
148
- function savePosition() {
149
- const rect = panel.getBoundingClientRect();
150
  try {
151
- sessionStorage.setItem(POSITION_KEY, JSON.stringify({
152
- left: Math.round(rect.left),
153
- top: Math.round(rect.top)
154
- }));
155
- } catch (e) {}
156
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- // Drag popup.
159
- const header = el("hit-header");
160
- let dragging = false;
161
- let dragOffsetX = 0;
162
- let dragOffsetY = 0;
163
-
164
- header.addEventListener("pointerdown", event => {
165
- if (event.target.closest("button")) return;
166
- dragging = true;
167
- const rect = panel.getBoundingClientRect();
168
- dragOffsetX = event.clientX - rect.left;
169
- dragOffsetY = event.clientY - rect.top;
170
- panel.style.right = "auto";
171
- panel.style.bottom = "auto";
172
- header.style.cursor = "grabbing";
173
- header.setPointerCapture(event.pointerId);
174
- event.preventDefault();
175
- });
176
 
177
- header.addEventListener("pointermove", event => {
178
- if (!dragging) return;
179
- const left = Math.max(0, Math.min(
180
- window.innerWidth - panel.offsetWidth,
181
- event.clientX - dragOffsetX
182
- ));
183
- const top = Math.max(0, Math.min(
184
- window.innerHeight - panel.offsetHeight,
185
- event.clientY - dragOffsetY
186
- ));
187
- panel.style.left = left + "px";
188
- panel.style.top = top + "px";
189
- });
190
 
191
- function stopDragging() {
192
- if (!dragging) return;
193
- dragging = false;
194
- header.style.cursor = "grab";
195
- savePosition();
 
 
 
196
  }
197
 
198
- header.addEventListener("pointerup", stopDragging);
199
- header.addEventListener("pointercancel", stopDragging);
200
- header.addEventListener("lostpointercapture", stopDragging);
201
-
202
- // Minimize and restore.
203
- el("hit-minimize").onclick = () => {
204
- state.minimized = !state.minimized;
205
- el("hit-content").style.display = state.minimized ? "none" : "block";
206
- el("hit-minimize").textContent = state.minimized ? "+" : "−";
207
- el("hit-minimize").title = state.minimized ? "Restore" : "Minimize";
208
- panel.style.width = state.minimized ? "260px" : "440px";
209
- savePosition();
210
- };
211
 
212
- // Read Kaggle session options from the actual page DOM.
213
- function updateKaggleOptions() {
214
- const getValue = label => {
215
- const field = [...document.querySelectorAll('[role="combobox"]')]
216
- .find(node => {
217
- const text = (
218
- node.getAttribute("aria-label") ||
219
- node.getAttribute("aria-labelledby") ||
220
- ""
221
- ).toLowerCase();
222
- return text.includes(label.toLowerCase());
223
- });
224
- return field?.innerText?.trim() || "";
225
- };
226
 
227
- const accelerator = getValue("Accelerator");
228
- const persistence = getValue("Persistence");
229
- const environment = getValue("Environment");
230
-
231
- const internetInput = document.querySelector(
232
- '#internet-toggle-switch-label input[type="checkbox"]'
233
- );
234
-
235
- const quota = [...document.querySelectorAll("p")]
236
- .find(node => node.innerText.includes("Quota:"))
237
- ?.innerText.match(/Quota:\s*(.*?)(?:\s*-\s*Link|$)/)?.[1] || "";
238
-
239
- el("hit-accelerator").textContent = accelerator || "Not detected";
240
- el("hit-quota").textContent = quota || "Not detected";
241
- el("hit-persistence").textContent = persistence || "Not detected";
242
- el("hit-environment").textContent = environment || "Not detected";
243
- el("hit-internet").textContent = internetInput
244
- ? (internetInput.checked ? "On" : "Off")
245
- : "Not detected";
246
- }
247
 
248
- function addEvent(data) {
249
- if (!el("hit-report").checked) return;
250
- const log = el("hit-json");
251
- log.textContent =
252
- (log.textContent === "Waiting for events..." ? "" : log.textContent + "\n\n") +
253
- JSON.stringify(data, null, 2);
254
- log.scrollTop = log.scrollHeight;
255
- }
256
 
257
- function getPrompt() {
258
- const fields = [...document.querySelectorAll("textarea, [contenteditable='true']")];
259
-
260
- const candidates = fields.map(node => ({
261
- text: (node.value || node.innerText || node.textContent || "").trim(),
262
- hint: [
263
- node.getAttribute("aria-label"),
264
- node.getAttribute("placeholder"),
265
- node.parentElement?.innerText
266
- ].filter(Boolean).join(" ").toLowerCase()
267
- })).filter(x => x.text.length > 10);
268
-
269
- const labelled = candidates
270
- .filter(x => /prompt|description/.test(x.hint))
271
- .sort((a, b) => b.text.length - a.text.length);
272
-
273
- if (labelled.length) return labelled[0].text;
274
- candidates.sort((a, b) => b.text.length - a.text.length);
275
- return candidates[0]?.text || "";
276
- }
277
 
278
- function getVideoUrl() {
279
- const candidates = [
280
- ...document.querySelectorAll("video"),
281
- ...document.querySelectorAll("video source"),
282
- ...document.querySelectorAll("a[href]")
283
- ];
284
-
285
- for (const node of candidates) {
286
- const url = node.currentSrc || node.src || node.href || "";
287
- if (url && (/\.mp4(?:[?#]|$)/i.test(url) || url.startsWith("blob:"))) {
288
- return url;
289
- }
290
  }
291
- return "";
292
  }
293
 
294
- function baseEvent(event) {
295
- return {
296
- session_id: "kaggle-" + location.host,
297
- event,
298
- render_url: location.href,
299
- prompt: state.prompt,
300
- progress: state.progress,
301
- step: state.step,
302
- total_steps: state.totalSteps,
303
- timestamp: new Date().toISOString()
304
- };
305
- }
306
 
307
- function poll() {
308
- updateKaggleOptions();
 
309
 
310
- const prompt = getPrompt();
311
- if (prompt && prompt !== state.prompt) {
312
- state.prompt = prompt;
313
- state.checkpoints.clear();
314
- state.completedUrl = "";
315
- el("hit-prompt").textContent = prompt;
316
- addEvent({...baseEvent("prompt"), message: "New prompt detected"});
317
- }
318
 
319
- const messages = [...document.querySelectorAll('div[type="message"]')];
320
- const matching = messages
321
- .map(node => node.innerText || "")
322
- .filter(text => text.includes("H3 denoising:"));
323
 
324
- if (matching.length) {
325
- const match = matching[matching.length - 1].match(
326
- /H3 denoising:\s*(\d+)%.*?\|\s*(\d+)\/(\d+)/
327
- );
328
 
329
- if (match) {
330
- const pct = Number(match[1]);
331
- const step = Number(match[2]);
332
- const total = Number(match[3]);
333
-
334
- if (pct < state.progress && pct <= 25) {
335
- state.checkpoints.clear();
336
- state.completedUrl = "";
337
- }
338
-
339
- state.progress = pct;
340
- state.step = step;
341
- state.totalSteps = total;
342
-
343
- el("hit-bar").style.width = pct + "%";
344
- el("hit-progress").textContent = pct + "%";
345
- el("hit-step").textContent = `Step ${step} / ${total}`;
346
- el("hit-title-progress").textContent = "· " + pct + "%";
347
-
348
- for (const threshold of [25, 50, 75, 100]) {
349
- if (pct >= threshold && !state.checkpoints.has(threshold)) {
350
- state.checkpoints.add(threshold);
351
- addEvent({...baseEvent("progress"), checkpoint: threshold});
352
- }
353
- }
354
- }
355
- }
356
 
357
- const videoUrl = getVideoUrl();
358
- if (videoUrl) {
359
- el("hit-url").textContent = videoUrl;
360
-
361
- if (state.progress >= 100 && videoUrl !== state.completedUrl) {
362
- state.completedUrl = videoUrl;
363
- addEvent({
364
- ...baseEvent("completed"),
365
- video: {
366
- url: videoUrl,
367
- filename: videoUrl.split("/").pop().split("?")[0]
368
- }
369
- });
370
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  }
 
372
 
373
- el("hit-status").textContent =
374
- "Last scan: " + new Date().toLocaleTimeString() +
375
- " · Polling every 15 seconds";
376
- }
 
377
 
378
- el("hit-close").onclick = () => {
379
- clearInterval(window.hitRenderMonitorTimer);
380
- window.hitRenderMonitorTimer = null;
381
- panel.remove();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  };
383
 
384
- poll();
385
- window.hitRenderMonitorTimer = setInterval(poll, 15000);
 
 
 
386
 
387
- console.log("H3 Render Monitor v4 started.");
388
  })();
 
1
 
 
2
  (() => {
3
+ // Clean up an earlier copy
4
+ window.oneHitBridge?.close?.();
5
+ document.getElementById("onehit-mp4-popup")?.remove();
6
+
7
+ // Remember processed URLs during this browser page session
8
+ const sent = window.__oneHitSent || (window.__oneHitSent = {
9
+ add: new Set(),
10
+ sub: new Set()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  });
12
 
13
+ let currentUrl = "";
14
+ let autoSend = false;
15
+ let busy = false;
16
+ let maximized = false;
17
+
18
+ const popup = document.createElement("div");
19
+ popup.id = "onehit-mp4-popup";
20
+ popup.style.cssText = `
21
+ position:fixed;right:20px;bottom:20px;width:390px;
22
+ z-index:2147483647;background:#17191f;color:white;
23
+ border:1px solid #555;border-radius:10px;
24
+ box-shadow:0 5px 25px #0009;font:13px Arial,sans-serif;
25
+ overflow:hidden;
26
+ `;
27
+
28
+ popup.innerHTML = `
29
+ <div id="oh-head" style="
30
+ display:flex;align-items:center;justify-content:space-between;
31
+ padding:12px;background:#292d38;cursor:move;user-select:none">
32
+ <strong>🎬 1hit MP4 Bridge</strong>
33
+ <div style="display:flex;gap:5px">
34
+ <button id="oh-min">—</button>
35
+ <button id="oh-max">□</button>
36
+ <button id="oh-close">✕</button>
37
  </div>
38
  </div>
39
 
40
+ <div id="oh-content" style="padding:14px">
41
+ <div id="oh-status" style="color:#aaa;margin-bottom:8px">
42
+ Looking for MP4 videos...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  </div>
44
 
45
+ <textarea id="oh-url" readonly placeholder="Waiting for MP4..."
46
+ style="box-sizing:border-box;width:100%;height:65px;
47
+ background:#252832;color:white;border:1px solid #555;
48
+ border-radius:5px;padding:8px;resize:vertical"></textarea>
49
+
50
+ <div style="display:flex;align-items:center;gap:10px;margin-top:12px">
51
+ <label style="flex:1">
52
+ Endpoint
53
+ <select id="oh-endpoint" style="
54
+ display:block;width:100%;margin-top:4px;padding:8px;
55
+ background:#252832;color:white;border:1px solid #555;border-radius:5px">
56
+ <option value="add">add.php</option>
57
+ <option value="sub">sub.php</option>
58
+ </select>
59
+ </label>
60
 
61
+ <label style="display:flex;align-items:center;gap:6px;padding-top:17px;cursor:pointer">
62
+ <input id="oh-auto" type="checkbox">
63
+ AUTO SEND
64
+ </label>
65
  </div>
66
+
67
+ <button id="oh-send" style="
68
+ width:100%;margin-top:12px;padding:10px;
69
+ background:#2863a0;color:white;border:0;border-radius:5px;cursor:pointer">
70
+ Send current MP4
71
+ </button>
72
+
73
+ <div id="oh-count" style="color:#999;margin-top:8px;font-size:11px"></div>
74
+
75
+ <div id="oh-footer" style="
76
+ display:flex;justify-content:space-between;align-items:center;
77
+ flex-wrap:wrap;gap:8px;border-top:1px solid #444;
78
+ margin-top:12px;padding-top:10px;font-size:12px">
79
+ <a href="https://1hit.no/kaggle" target="_blank" rel="noopener"
80
+ style="color:#70b8ff;text-decoration:none">↗ 1hit.no/kaggle</a>
81
+ <a href="https://chatgpt.com" target="_blank" rel="noopener"
82
+ style="color:#aaa;text-decoration:none">Built with ChatGPT ↗</a>
83
  </div>
84
+ </div>
85
+ `;
86
 
87
+ document.body.appendChild(popup);
88
 
89
+ const $ = id => popup.querySelector("#oh-" + id);
90
+ const status = $("status");
91
+ const urlBox = $("url");
92
 
93
+ function updateUI() {
94
+ const endpoint = $("endpoint").value;
95
+ const done = currentUrl && sent[endpoint].has(currentUrl);
96
 
97
+ $("send").disabled = !currentUrl || done || busy;
98
+ $("send").textContent = done
99
+ ? "Already sent ✓"
100
+ : endpoint === "add" ? "Send to add.php" : "Send to sub.php";
 
 
 
 
 
 
101
 
102
+ $("send").style.opacity = $("send").disabled ? ".5" : "1";
103
 
104
+ $("count").textContent =
105
+ `Remembered: ${sent.add.size} add.php · ${sent.sub.size} sub.php`;
106
+ }
 
 
107
 
108
+ async function sendCurrent(automatic = false) {
109
+ const url = currentUrl;
110
+ const endpoint = $("endpoint").value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
+ if (!url || busy || sent[endpoint].has(url)) return;
113
 
114
+ busy = true;
115
+ updateUI();
116
+
117
+ status.textContent = `Sending to ${endpoint}.php...`;
118
+ status.style.color = "#f0c674";
 
 
 
 
 
119
 
 
 
120
  try {
121
+ let request;
122
+
123
+ if (endpoint === "add") {
124
+ request = fetch(
125
+ "https://1hit.no/kaggle/add.php/?url=" + encodeURIComponent(url),
126
+ { method: "GET", mode: "no-cors" }
127
+ );
128
+ } else {
129
+ request = fetch("https://1hit.no/kaggle/sub.php", {
130
+ method: "POST",
131
+ mode: "no-cors",
132
+ headers: {
133
+ "Content-Type": "text/plain;charset=UTF-8"
134
+ },
135
+ body: JSON.stringify({
136
+ url,
137
+ client_id: "Wan2GP-Kaggle"
138
+ })
139
+ });
140
+ }
141
 
142
+ await request;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
+ // The no-cors response cannot confirm server-side completion.
145
+ sent[endpoint].add(url);
146
+
147
+ status.textContent = automatic
148
+ ? `AUTO: request sent to ${endpoint}.php ✓`
149
+ : `Request sent to ${endpoint}.php ✓`;
 
 
 
 
 
 
 
150
 
151
+ status.style.color = "#70d99a";
152
+ } catch (error) {
153
+ status.textContent = `Request failed: ${error.message}`;
154
+ status.style.color = "#ff7777";
155
+ } finally {
156
+ busy = false;
157
+ updateUI();
158
+ }
159
  }
160
 
161
+ function findVideos() {
162
+ const urls = [...document.querySelectorAll("video")]
163
+ .map(v => v.currentSrc || v.src || v.querySelector("source")?.src || "")
164
+ .filter(url =>
165
+ /^https:\/\//i.test(url) &&
166
+ /\.mp4(?:[?#]|$)/i.test(url)
167
+ );
 
 
 
 
 
 
168
 
169
+ const best =
170
+ urls.find(url => url.includes("/gradio_api/file=")) ||
171
+ urls.at(-1);
 
 
 
 
 
 
 
 
 
 
 
172
 
173
+ if (best && best !== currentUrl) {
174
+ currentUrl = best;
175
+ urlBox.value = best;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
+ status.textContent = "New MP4 detected ✓";
178
+ status.style.color = "#70d99a";
 
 
 
 
 
 
179
 
180
+ updateUI();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
+ if (autoSend) sendCurrent(true);
 
 
 
 
 
 
 
 
 
 
 
183
  }
 
184
  }
185
 
186
+ // AUTO SEND toggle
187
+ $("auto").onchange = () => {
188
+ autoSend = $("auto").checked;
 
 
 
 
 
 
 
 
 
189
 
190
+ status.textContent = autoSend
191
+ ? "AUTO SEND enabled — scanning every 10 seconds."
192
+ : "AUTO SEND disabled — manual mode.";
193
 
194
+ status.style.color = autoSend ? "#70d99a" : "#aaa";
195
+ };
 
 
 
 
 
 
196
 
197
+ $("endpoint").onchange = updateUI;
198
+ $("send").onclick = () => sendCurrent(false);
 
 
199
 
200
+ // Minimize / restore
201
+ $("min").onclick = () => {
202
+ const content = $("content");
203
+ const hidden = content.style.display === "none";
204
 
205
+ content.style.display = hidden ? "block" : "none";
206
+ $("min").textContent = hidden ? "—" : "+";
207
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
+ // Maximize / restore
210
+ $("max").onclick = () => {
211
+ maximized = !maximized;
212
+
213
+ if (maximized) {
214
+ popup.dataset.oldStyle = JSON.stringify({
215
+ width: popup.style.width,
216
+ height: popup.style.height,
217
+ top: popup.style.top,
218
+ left: popup.style.left,
219
+ right: popup.style.right,
220
+ bottom: popup.style.bottom,
221
+ borderRadius: popup.style.borderRadius
222
+ });
223
+
224
+ popup.style.width = "min(700px, calc(100vw - 30px))";
225
+ popup.style.height = "min(85vh, 750px)";
226
+ popup.style.top = "50%";
227
+ popup.style.left = "50%";
228
+ popup.style.right = "auto";
229
+ popup.style.bottom = "auto";
230
+ popup.style.transform = "translate(-50%, -50%)";
231
+
232
+ $("content").style.overflow = "auto";
233
+ $("content").style.maxHeight = "calc(85vh - 50px)";
234
+ $("max").textContent = "❐";
235
+ } else {
236
+ const old = JSON.parse(popup.dataset.oldStyle || "{}");
237
+ Object.assign(popup.style, old);
238
+ popup.style.transform = "";
239
+
240
+ $("content").style.maxHeight = "";
241
+ $("max").textContent = "□";
242
  }
243
+ };
244
 
245
+ // Drag using the header
246
+ let drag = null;
247
+
248
+ $("head").addEventListener("pointerdown", e => {
249
+ if (e.target.closest("button") || maximized) return;
250
 
251
+ const rect = popup.getBoundingClientRect();
252
+
253
+ drag = {
254
+ x: e.clientX - rect.left,
255
+ y: e.clientY - rect.top
256
+ };
257
+
258
+ $("head").setPointerCapture(e.pointerId);
259
+ });
260
+
261
+ $("head").addEventListener("pointermove", e => {
262
+ if (!drag) return;
263
+
264
+ popup.style.left = `${e.clientX - drag.x}px`;
265
+ popup.style.top = `${e.clientY - drag.y}px`;
266
+ popup.style.right = "auto";
267
+ popup.style.bottom = "auto";
268
+ });
269
+
270
+ $("head").addEventListener("pointerup", () => drag = null);
271
+ $("head").addEventListener("pointercancel", () => drag = null);
272
+
273
+ // Scan immediately, then every 10 seconds
274
+ findVideos();
275
+ const timer = setInterval(findVideos, 10000);
276
+
277
+ $("close").onclick = () => {
278
+ clearInterval(timer);
279
+ popup.remove();
280
+ delete window.oneHitBridge;
281
  };
282
 
283
+ window.oneHitBridge = {
284
+ scan: findVideos,
285
+ close: () => $("close").click(),
286
+ sent
287
+ };
288
 
289
+ updateUI();
290
  })();