scratch-explorer-mtp 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/about.html ADDED
@@ -0,0 +1,201 @@
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" />
6
+ <link rel="icon" href="favicon.ico" type="image/x-icon">
7
+ <title>About Scratch Explorer 🎮</title>
8
+ <style>
9
+ /* Reset and basics */
10
+ * {
11
+ box-sizing: border-box;
12
+ }
13
+ body {
14
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
15
+ background: #1e1e2f;
16
+ color: #eee;
17
+ line-height: 1.6;
18
+ margin: 0;
19
+ padding: 0 20px 40px;
20
+ min-height: 100vh;
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ }
25
+ header {
26
+ margin-top: 40px;
27
+ text-align: center;
28
+ }
29
+ header h1 {
30
+ font-size: 3rem;
31
+ margin-bottom: 0;
32
+ color: #4caf50;
33
+ letter-spacing: 2px;
34
+ text-shadow: 0 0 8px #4caf50aa;
35
+ }
36
+ header p {
37
+ font-size: 1.25rem;
38
+ margin-top: 4px;
39
+ color: #a0a0a0;
40
+ }
41
+
42
+ main {
43
+ max-width: 900px;
44
+ margin-top: 40px;
45
+ background: #29294b;
46
+ padding: 30px 40px;
47
+ border-radius: 12px;
48
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
49
+ }
50
+
51
+ h2 {
52
+ color: #76c7b7;
53
+ border-bottom: 2px solid #4caf50;
54
+ padding-bottom: 6px;
55
+ margin-bottom: 20px;
56
+ font-weight: 700;
57
+ letter-spacing: 1.2px;
58
+ }
59
+
60
+ p {
61
+ margin-bottom: 16px;
62
+ font-size: 1.1rem;
63
+ color: #d4d4d8;
64
+ }
65
+
66
+ ul {
67
+ list-style-type: disc;
68
+ margin-left: 20px;
69
+ margin-bottom: 24px;
70
+ color: #c9f0f1;
71
+ font-size: 1.1rem;
72
+ }
73
+ ul li {
74
+ margin-bottom: 10px;
75
+ }
76
+
77
+ code {
78
+ background-color: #3a3a5a;
79
+ padding: 3px 7px;
80
+ border-radius: 4px;
81
+ font-family: monospace;
82
+ font-weight: 600;
83
+ color: #a8ff60;
84
+ }
85
+
86
+ a {
87
+ color: #76c7b7;
88
+ text-decoration: none;
89
+ font-weight: 600;
90
+ }
91
+ a:hover {
92
+ text-decoration: underline;
93
+ }
94
+
95
+ .back-btn {
96
+ display: inline-block;
97
+ margin-top: 30px;
98
+ padding: 12px 24px;
99
+ background-color: #4caf50;
100
+ border: none;
101
+ border-radius: 8px;
102
+ color: white;
103
+ font-size: 1.1rem;
104
+ cursor: pointer;
105
+ box-shadow: 0 4px 10px #4caf50cc;
106
+ transition: background-color 0.3s ease;
107
+ text-align: center;
108
+ }
109
+ .back-btn:hover {
110
+ background-color: #3b8e3b;
111
+ box-shadow: 0 6px 14px #3b8e3bcc;
112
+ }
113
+
114
+ footer {
115
+ margin-top: auto;
116
+ padding: 20px 0;
117
+ color: #666;
118
+ font-size: 0.9rem;
119
+ text-align: center;
120
+ border-top: 1px solid #444;
121
+ width: 100%;
122
+ max-width: 900px;
123
+ }
124
+
125
+ @media (max-width: 600px) {
126
+ main {
127
+ padding: 20px;
128
+ }
129
+ header h1 {
130
+ font-size: 2.2rem;
131
+ }
132
+ p, ul {
133
+ font-size: 1rem;
134
+ }
135
+ }
136
+ </style>
137
+ </head>
138
+ <body>
139
+ <header>
140
+ <h1>About Scratch Explorer 🎮</h1>
141
+ <p>Your gateway to discovering, playing, and exploring Scratch projects with ease!</p>
142
+ </header>
143
+
144
+ <main>
145
+ <section>
146
+ <h2>What is Scratch Explorer?</h2>
147
+ <p>
148
+ Scratch Explorer is a custom web app designed to help you search and play Scratch projects directly from the Scratch platform in a smooth and user-friendly way. Whether you're looking for projects by username, searching by project title, or jumping straight to a project by its ID, Scratch Explorer makes it quick and fun.
149
+ </p>
150
+ </section>
151
+
152
+ <section>
153
+ <h2>Features & How To Use</h2>
154
+ <ul>
155
+ <li><strong>Search by Username:</strong> Enter a Scratch username and browse their latest projects with pagination support.</li>
156
+ <li><strong>Search by Project Title:</strong> Look for projects by typing keywords and get the most popular results.</li>
157
+ <li><strong>Load Projects:</strong> Click project thumbnails to instantly load and play projects via TurboWarp embedded player.</li>
158
+ <li><strong>Fullscreen Mode:</strong> Go full screen for an immersive gaming experience.</li>
159
+ <li><strong>Audio Management:</strong> The app manages audio context so projects with sound work smoothly.</li>
160
+ <li><strong>Custom Username Parameter:</strong> Add your Scratch username to some projects that support username-based features.</li>
161
+ </ul>
162
+ <p>
163
+ The interface includes pagination buttons ("Back" and "More"/"Next") to navigate through large lists of projects. When you load a project, you can also view detailed stats like views, loves, and favorites.
164
+ </p>
165
+ </section>
166
+
167
+ <section>
168
+ <h2>Why Use Scratch Explorer?</h2>
169
+ <p>
170
+ While Scratch's official site offers many features, Scratch Explorer provides a focused and lightweight way to search and play projects without distractions. It uses TurboWarp’s powerful embed player for faster loading and better performance.
171
+ </p>
172
+ <p>
173
+ Plus, the added pagination and title search modes help you discover hidden gems and popular projects efficiently.
174
+ </p>
175
+ </section>
176
+
177
+ <section>
178
+ <h2>Technical Notes</h2>
179
+ <ul>
180
+ <li>The app uses a CORS proxy to fetch data from Scratch's API because Scratch does not allow direct cross-origin requests.</li>
181
+ <li>AudioContext is initialized and resumed automatically to avoid browser restrictions on sound playback.</li>
182
+ <li>Project thumbnails and user info are dynamically loaded via Scratch’s public API endpoints.</li>
183
+ <li>All project embeds are powered by <a href="https://turbowarp.org" target="_blank" rel="noopener">TurboWarp</a> for enhanced performance.</li>
184
+ </ul>
185
+ </section>
186
+
187
+ <section>
188
+ <h2>Feedback & Contributions</h2>
189
+ <p>
190
+ This project is a work in progress. Your feedback is super valuable! Feel free to reach out or contribute ideas to improve Scratch Explorer.
191
+ </p>
192
+ </section>
193
+
194
+ <button class="back-btn" onclick="history.back()">← Back to Explorer</button>
195
+ </main>
196
+
197
+ <footer>
198
+ &copy; 2025 Scratch Explorer by Micah Scott Nordlund. All rights reserved.
199
+ </footer>
200
+ </body>
201
+ </html>
package/favicon.ico ADDED
Binary file
package/index.html ADDED
@@ -0,0 +1,487 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Scratch Explorer</title>
6
+ </head>
7
+ <body>
8
+ <style>
9
+ /* ==== BASE STYLES ==== */
10
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;800&display=swap');
11
+
12
+ body {
13
+ font-family: 'Poppins', 'Helvetica Neue', Arial, sans-serif;
14
+ background: linear-gradient(135deg, #f0f4ff 0%, #d9e8ff 100%);
15
+ color: #1e2a47;
16
+ margin: 0;
17
+ padding: 40px 28px;
18
+ text-align: center;
19
+ line-height: 1.65;
20
+ letter-spacing: 0.015em;
21
+ user-select: none;
22
+ -webkit-font-smoothing: antialiased;
23
+ -moz-osx-font-smoothing: grayscale;
24
+ transition: background-color 0.4s ease;
25
+ }
26
+
27
+ /* ==== HEADINGS ==== */
28
+ h1 {
29
+ font-weight: 800;
30
+ font-size: clamp(2.8rem, 6vw, 4rem);
31
+ color: #3b82f6;
32
+ margin-bottom: 2rem;
33
+ letter-spacing: 1.5px;
34
+ text-shadow:
35
+ 0 2px 6px rgba(59, 130, 246, 0.5),
36
+ 0 6px 15px rgba(59, 130, 246, 0.3);
37
+ animation: fadeInDown 0.8s ease forwards;
38
+ opacity: 0;
39
+ }
40
+
41
+ /* Animate heading on load */
42
+ @keyframes fadeInDown {
43
+ to {
44
+ opacity: 1;
45
+ transform: translateY(0);
46
+ }
47
+ from {
48
+ opacity: 0;
49
+ transform: translateY(-30px);
50
+ }
51
+ }
52
+
53
+ /* ==== INPUTS & BUTTONS ==== */
54
+ input, button {
55
+ font-size: 1.1rem;
56
+ padding: 15px 24px;
57
+ margin: 14px 12px;
58
+ border-radius: 14px;
59
+ border: 2px solid transparent;
60
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
61
+ outline: none;
62
+ font-weight: 600;
63
+ user-select: none;
64
+ box-sizing: border-box;
65
+ box-shadow:
66
+ 0 2px 6px rgba(0, 0, 0, 0.07);
67
+ background: #fff;
68
+ cursor: pointer;
69
+ }
70
+
71
+ /* Inputs specifically */
72
+ input {
73
+ width: 355px;
74
+ color: #1e2a47;
75
+ box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.1);
76
+ border: 2px solid #cbd5e1;
77
+ background: linear-gradient(145deg, #f9fbff, #e3ebfc);
78
+ font-weight: 500;
79
+ transition:
80
+ border-color 0.3s ease,
81
+ box-shadow 0.3s ease;
82
+ }
83
+
84
+ input::placeholder {
85
+ color: #94a3b8;
86
+ font-style: italic;
87
+ }
88
+
89
+ input:focus {
90
+ border-color: #3b82f6;
91
+ box-shadow:
92
+ 0 0 12px 3px rgba(59, 130, 246, 0.45),
93
+ inset 0 3px 8px rgba(0, 0, 0, 0.12);
94
+ background: #fff;
95
+ }
96
+
97
+ /* Buttons specifically */
98
+ button {
99
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
100
+ color: #fff;
101
+ border: none;
102
+ box-shadow:
103
+ 0 8px 20px rgba(59, 130, 246, 0.35);
104
+ font-weight: 700;
105
+ letter-spacing: 0.05em;
106
+ text-transform: uppercase;
107
+ position: relative;
108
+ overflow: hidden;
109
+ }
110
+
111
+ button::before {
112
+ content: '';
113
+ position: absolute;
114
+ left: -50%;
115
+ top: 0;
116
+ width: 200%;
117
+ height: 100%;
118
+ background: linear-gradient(120deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%);
119
+ transform: skewX(-20deg);
120
+ transition: all 0.5s ease;
121
+ pointer-events: none;
122
+ }
123
+
124
+ button:hover::before {
125
+ left: 100%;
126
+ transition: all 0.5s ease;
127
+ }
128
+
129
+ button:hover {
130
+ background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%);
131
+ box-shadow:
132
+ 0 12px 26px rgba(37, 99, 235, 0.55);
133
+ transform: translateY(-3px);
134
+ }
135
+
136
+ button:active {
137
+ background: #1e3a8a;
138
+ box-shadow:
139
+ 0 6px 12px rgba(30, 58, 138, 0.4);
140
+ transform: translateY(0);
141
+ transition: transform 0.1s ease;
142
+ }
143
+
144
+ /* ==== SPECIAL BUTTONS ==== */
145
+ .more-back-btn {
146
+ background: linear-gradient(135deg, #34d399 0%, #059669 100%);
147
+ box-shadow: 0 8px 20px rgba(5, 150, 105, 0.45);
148
+ color: #fff;
149
+ font-weight: 700;
150
+ }
151
+
152
+ .more-back-btn:hover {
153
+ background: linear-gradient(135deg, #059669 0%, #065f46 100%);
154
+ box-shadow: 0 12px 28px rgba(6, 95, 70, 0.55);
155
+ transform: translateY(-3px);
156
+ }
157
+
158
+ .back-btn {
159
+ background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%);
160
+ box-shadow: 0 8px 20px rgba(185, 28, 28, 0.45);
161
+ color: #fff;
162
+ font-weight: 700;
163
+ }
164
+
165
+ .back-btn:hover {
166
+ background: linear-gradient(135deg, #b91c1c 0%, #7f1d1d 100%);
167
+ box-shadow: 0 12px 28px rgba(127, 29, 29, 0.55);
168
+ transform: translateY(-3px);
169
+ }
170
+
171
+ /* ==== IFRAME PLAYER CONTAINER ==== */
172
+ .iframe-container {
173
+ max-width: 940px;
174
+ margin: 48px auto;
175
+ padding: 32px;
176
+ border-radius: 20px;
177
+ background: #fff;
178
+ box-shadow:
179
+ 0 12px 40px rgba(30, 42, 70, 0.12),
180
+ inset 0 0 12px rgba(59, 130, 246, 0.05);
181
+ min-height: 680px;
182
+ transition: box-shadow 0.3s ease;
183
+
184
+ /* Add this so container's width never shrinks below full */
185
+ width: 100%; /* make container responsive */
186
+ box-sizing: border-box; /* include padding in width */
187
+ }
188
+
189
+ .iframe-container:hover {
190
+ box-shadow:
191
+ 0 18px 48px rgba(30, 42, 70, 0.18),
192
+ inset 0 0 18px rgba(59, 130, 246, 0.1);
193
+ }
194
+
195
+ /* ==== EMBEDDED PLAYER ==== */
196
+ embed {
197
+ width: 100%; /* always fill container width */
198
+ height: 675px; /* consistent height */
199
+ border-radius: 16px;
200
+ border: none;
201
+ display: block;
202
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1);
203
+ transition: transform 0.25s ease;
204
+
205
+ /* Prevent shrinking on exit game */
206
+ min-width: 100%;
207
+ box-sizing: border-box;
208
+ }
209
+
210
+ embed:hover {
211
+ transform: scale(1.02);
212
+ }
213
+
214
+ /* ==== PROJECT INFO BOX ==== */
215
+ .project-info {
216
+ margin: 32px auto 0;
217
+ background: linear-gradient(145deg, #e6efff, #f0f8ff);
218
+ border-radius: 16px;
219
+ padding: 24px 28px;
220
+ box-shadow:
221
+ inset 0 2px 8px rgba(59, 130, 246, 0.15);
222
+ color: #34495e;
223
+ max-width: 460px;
224
+ font-weight: 600;
225
+ line-height: 1.5;
226
+ letter-spacing: 0.02em;
227
+ transition: background 0.4s ease;
228
+ }
229
+
230
+ .project-info img {
231
+ border-radius: 16px;
232
+ margin-bottom: 20px;
233
+ max-width: 100%;
234
+ box-shadow:
235
+ 0 8px 20px rgba(52, 73, 94, 0.2);
236
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
237
+ cursor: zoom-in;
238
+ }
239
+
240
+ .project-info img:hover {
241
+ transform: scale(1.08) rotate(1deg);
242
+ box-shadow:
243
+ 0 12px 28px rgba(52, 73, 94, 0.3);
244
+ }
245
+
246
+ /* ==== RESULTS CONTAINER ==== */
247
+ #results {
248
+ margin: 36px auto 0;
249
+ max-width: 960px;
250
+ filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.06));
251
+ }
252
+
253
+ /* ==== FULLSCREEN BUTTONS ==== */
254
+ #fullscreenButton, #exitButton {
255
+ margin-top: 28px;
256
+ padding: 16px 32px;
257
+ font-weight: 700;
258
+ border-radius: 16px;
259
+ border: none;
260
+ cursor: pointer;
261
+ box-shadow:
262
+ 0 8px 22px rgba(59, 130, 246, 0.35);
263
+ transition:
264
+ background-color 0.4s ease,
265
+ box-shadow 0.4s ease,
266
+ transform 0.3s ease;
267
+ user-select: none;
268
+ font-size: 1.1rem;
269
+ letter-spacing: 0.05em;
270
+ }
271
+
272
+ #fullscreenButton {
273
+ background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);
274
+ color: #fff;
275
+ position: relative;
276
+ overflow: hidden;
277
+ }
278
+
279
+ #fullscreenButton:hover {
280
+ background: linear-gradient(135deg, #5b21b6 0%, #3b0f92 100%);
281
+ box-shadow:
282
+ 0 12px 30px rgba(91, 33, 182, 0.55);
283
+ transform: translateY(-4px);
284
+ }
285
+
286
+ #exitButton {
287
+ background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%);
288
+ color: #fff;
289
+ display: none;
290
+ }
291
+
292
+ #exitButton:hover {
293
+ background: linear-gradient(135deg, #b91c1c 0%, #7f1d1d 100%);
294
+ box-shadow:
295
+ 0 12px 30px rgba(185, 28, 28, 0.55);
296
+ transform: translateY(-4px);
297
+ }
298
+
299
+ /* ==== FULLSCREEN STYLES ==== */
300
+ #playerWrapper:fullscreen,
301
+ #playerWrapper:-webkit-full-screen {
302
+ width: 100vw;
303
+ height: 100vh;
304
+ margin: 0;
305
+ padding: 0;
306
+ border-radius: 0;
307
+ background-color: #0f111a;
308
+ transition: background-color 0.5s ease;
309
+ }
310
+
311
+ #playerWrapper:fullscreen embed,
312
+ #playerWrapper:-webkit-full-screen embed {
313
+ width: 100vw;
314
+ height: 100vh;
315
+ border-radius: 0;
316
+ border: none;
317
+ }
318
+
319
+ /* ==== PROJECT CARDS ==== */
320
+ .project-card {
321
+ display: inline-block;
322
+ width: 260px;
323
+ background: #fff;
324
+ border-radius: 20px;
325
+ box-shadow:
326
+ 0 10px 26px rgba(0, 0, 0, 0.1);
327
+ margin: 18px 16px;
328
+ padding: 20px 18px;
329
+ vertical-align: top;
330
+ transition:
331
+ transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
332
+ box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1);
333
+ cursor: pointer;
334
+ user-select: none;
335
+ position: relative;
336
+ overflow: hidden;
337
+ }
338
+
339
+ .project-card:hover {
340
+ transform: translateY(-12px) scale(1.03);
341
+ box-shadow:
342
+ 0 20px 40px rgba(0, 0, 0, 0.18),
343
+ 0 8px 22px rgba(0, 0, 0, 0.15);
344
+ z-index: 10;
345
+ }
346
+
347
+ /* Glowing border on hover */
348
+ .project-card:hover::before {
349
+ content: '';
350
+ position: absolute;
351
+ top: -2px;
352
+ left: -2px;
353
+ right: -2px;
354
+ bottom: -2px;
355
+ border-radius: 20px;
356
+ pointer-events: none;
357
+ background: linear-gradient(45deg, #3b82f6, #06b6d4, #3b82f6, #06b6d4);
358
+ background-size: 400% 400%;
359
+ animation: gradientShift 8s ease infinite;
360
+ z-index: -1;
361
+ filter: blur(12px);
362
+ opacity: 0.7;
363
+ }
364
+
365
+ @keyframes gradientShift {
366
+ 0% {
367
+ background-position: 0% 50%;
368
+ }
369
+ 50% {
370
+ background-position: 100% 50%;
371
+ }
372
+ 100% {
373
+ background-position: 0% 50%;
374
+ }
375
+ }
376
+
377
+ /* ==== THUMBNAIL ==== */
378
+ .project-thumbnail {
379
+ width: 100%;
380
+ border-radius: 14px;
381
+ margin-bottom: 16px;
382
+ user-select: none;
383
+ pointer-events: auto;
384
+ cursor: pointer;
385
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
386
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
387
+ }
388
+
389
+ .project-thumbnail:hover {
390
+ transform: scale(1.07) rotate(0.5deg);
391
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.14);
392
+ }
393
+
394
+ /* ==== PROJECT TITLE BUTTON ==== */
395
+ .project-title-btn {
396
+ background: none;
397
+ border: none;
398
+ font-weight: 800;
399
+ font-size: 1.15rem;
400
+ color: #2563eb;
401
+ margin-bottom: 10px;
402
+ cursor: pointer;
403
+ padding: 0;
404
+ text-align: left;
405
+ width: 100%;
406
+ user-select: none;
407
+ transition: color 0.3s ease, text-shadow 0.3s ease;
408
+ }
409
+
410
+ .project-title-btn:hover {
411
+ color: #1d4ed8;
412
+ text-shadow: 0 0 8px rgba(37, 99, 235, 0.75);
413
+ text-decoration: underline;
414
+ }
415
+
416
+ /* ==== AUTHOR ==== */
417
+ .project-author {
418
+ font-size: 0.9rem;
419
+ color: #6b7280;
420
+ margin: 0;
421
+ user-select: none;
422
+ font-weight: 500;
423
+ letter-spacing: 0.02em;
424
+ }
425
+
426
+ /* ==== PAGINATION BUTTONS ==== */
427
+ .pagination-buttons {
428
+ display: flex;
429
+ justify-content: center;
430
+ gap: 22px;
431
+ margin-top: 32px;
432
+ flex-wrap: wrap;
433
+ }
434
+
435
+ /* ==== EXTRA: subtle page fade-in ==== */
436
+ body {
437
+ animation: pageFadeIn 0.8s ease forwards;
438
+ opacity: 0;
439
+ }
440
+
441
+ @keyframes pageFadeIn {
442
+ to { opacity: 1; }
443
+ }
444
+
445
+ #aboutBtn {
446
+ background-color: #4CAF50; /* Fresh green vibe */
447
+ color: white;
448
+ border: none;
449
+ padding: 10px 18px;
450
+ font-size: 16px;
451
+ border-radius: 6px;
452
+ cursor: pointer;
453
+ margin-top: 8px;
454
+ transition: background-color 0.3s ease;
455
+ }
456
+
457
+ #aboutBtn:hover {
458
+ background-color: #45a049; /* Darker green on hover */
459
+ }
460
+ </style>
461
+ <h1>Scratch Explorer 🎮</h1>
462
+ <button id="aboutBtn">About Scratch Explorer</button>
463
+ <div id="titleSearchDiv">
464
+ <input type="text" id="titleSearchInput" placeholder="Search projects by title">
465
+ <button onclick="searchByTitle(document.getElementById('titleSearchInput').value)">Search Titles</button>
466
+ </div>
467
+ <input type="text" id="searchInput" placeholder="Enter project ID or username">
468
+ <button onclick="search()">Search</button>
469
+ <input type="text" id="twUsernameInput" placeholder="Username to show in game (optional)">
470
+
471
+ <div id="results"></div>
472
+
473
+ <div id="usernameSearchDiv" style="display: none;">
474
+ <h2>Search Projects by this User:</h2>
475
+ <input type="text" id="userProjectSearch" placeholder="Search projects...">
476
+ </div>
477
+
478
+ <!-- Wrap the embed inside a div with id="playerWrapper" -->
479
+ <div class="iframe-container" id="playerWrapper">
480
+ <embed id="player" src="" allow="microphone; camera; fullscreen" allowfullscreen>
481
+ </div>
482
+ <button id="fullscreenButton" onclick="goFullscreen()">Fullscreen Mode</button>
483
+
484
+ <script src="main.js"></script>
485
+ <link rel="icon" href="favicon.ico" type="image/x-icon">
486
+ </body>
487
+ </html>
package/main.js ADDED
@@ -0,0 +1,284 @@
1
+ let currentOffset = 0; // Track where we are in the project list
2
+ let projects = []; // To store fetched projects
3
+ let titleOffset = 0; // Offset for title-based search
4
+ let lastTitle = ''; // Store last title searched
5
+ let audioCtx = null;
6
+ let gameActive = false; // Track if a game is currently active
7
+
8
+ async function search() {
9
+
10
+ const input = document.getElementById('searchInput').value.trim();
11
+ const resultsDiv = document.getElementById('results');
12
+ const player = document.getElementById('player');
13
+ const usernameSearchDiv = document.getElementById('usernameSearchDiv');
14
+ resultsDiv.innerHTML = '';
15
+
16
+ if (!input) return;
17
+
18
+ // If input is just a number, treat as project ID
19
+ if (/^\d+$/.test(input)) {
20
+ loadProject(input);
21
+ return;
22
+ }
23
+
24
+ // Else treat as username — use CORS proxy
25
+ const proxy = "https://withered-wind-d31c.micah-nordlund.workers.dev/?url=";
26
+ const apiUrl = `https://api.scratch.mit.edu/users/${input}/projects?limit=8&offset=${currentOffset}`;
27
+ const fullUrl = proxy + encodeURIComponent(apiUrl);
28
+
29
+ try {
30
+ const res = await fetch(fullUrl);
31
+ if (!res.ok) {
32
+ resultsDiv.innerText = '❌ User not found.';
33
+ return;
34
+ }
35
+
36
+ projects = await res.json();
37
+ if (projects.length === 0) {
38
+ resultsDiv.innerText = '❌ No projects found for this user.';
39
+ return;
40
+ }
41
+
42
+ resultsDiv.innerHTML = `<h2>Projects by ${input}</h2>`;
43
+ displayProjects(projects, true); // Hide usernames for user profile search
44
+
45
+ // Show the project search bar for this user
46
+ usernameSearchDiv.style.display = 'none';
47
+
48
+ // Create a container div for buttons
49
+ const btnContainer = document.createElement('div');
50
+ btnContainer.className = 'pagination-buttons';
51
+
52
+ // Add "Back" button if offset > 0
53
+ if (currentOffset > 0) {
54
+ const backBtn = document.createElement('button');
55
+ backBtn.innerText = 'Back';
56
+ backBtn.classList.add('back-btn');
57
+ backBtn.onclick = searchBack;
58
+ btnContainer.appendChild(backBtn);
59
+ }
60
+
61
+ // Add "More" button if projects.length === 8
62
+ if (projects.length === 8) {
63
+ const moreBtn = document.createElement('button');
64
+ moreBtn.innerText = 'More';
65
+ moreBtn.classList.add('more-back-btn');
66
+ moreBtn.onclick = searchMore;
67
+ btnContainer.appendChild(moreBtn);
68
+ }
69
+
70
+ // Append the container div to resultsDiv
71
+ resultsDiv.appendChild(btnContainer);
72
+
73
+ } catch (err) {
74
+ console.error("Fetch failed:", err);
75
+ resultsDiv.innerText = '❌ Failed to fetch projects.';
76
+ }
77
+ }
78
+
79
+ // Display fetched projects in the results
80
+ function displayProjects(projectList, hideUsernames = false) {
81
+ const resultsDiv = document.getElementById('results');
82
+ resultsDiv.innerHTML = ''; // Clear previous results
83
+
84
+ projectList.forEach(p => {
85
+ const card = document.createElement('div');
86
+ card.className = 'project-card';
87
+
88
+ // Thumbnail image — clickable to load project
89
+ const thumbnail = document.createElement('img');
90
+ thumbnail.src = `https://uploads.scratch.mit.edu/projects/thumbnails/${p.id}.png`;
91
+ thumbnail.alt = p.title;
92
+ thumbnail.className = 'project-thumbnail';
93
+ thumbnail.style.cursor = 'pointer';
94
+ thumbnail.addEventListener('click', () => {
95
+ if (!audioCtx) {
96
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
97
+ }
98
+ if (audioCtx.state === 'suspended') {
99
+ audioCtx.resume();
100
+ }
101
+ loadProject(p.id);
102
+ });
103
+ card.appendChild(thumbnail);
104
+ // Title (not clickable)
105
+ const titleBtn = document.createElement('button');
106
+ titleBtn.className = 'project-title-btn';
107
+ titleBtn.innerText = p.title;
108
+ // no onclick here anymore
109
+ card.appendChild(titleBtn);
110
+
111
+ // Author name
112
+ if (!hideUsernames && p.author && p.author.username) {
113
+ const author = document.createElement('p');
114
+ author.className = 'project-author';
115
+ author.innerHTML = `👤 <strong>${p.author.username}</strong>`;
116
+ card.appendChild(author);
117
+ }
118
+
119
+ resultsDiv.appendChild(card);
120
+ });
121
+ }
122
+
123
+ // Load a specific project into the player
124
+ async function loadProject(id) {
125
+ if (gameActive) {
126
+ // Auto-exit current game instead of alert
127
+ exitGame();
128
+ }
129
+
130
+ gameActive = true;
131
+
132
+ const player = document.getElementById('player');
133
+ const resultsDiv = document.getElementById('results');
134
+
135
+ const customTWUsername = document.getElementById('twUsernameInput').value.trim();
136
+ const usernameParam = customTWUsername ? `&username=${encodeURIComponent(customTWUsername)}` : '';
137
+ player.src = `https://turbowarp.org/${id}/embed?autoplay&allow=microphone;camera;fullscreen${usernameParam}`;
138
+
139
+ const proxy = "https://withered-wind-d31c.micah-nordlund.workers.dev/?url=";
140
+ const apiUrl = `https://api.scratch.mit.edu/projects/${id}`;
141
+ const fullUrl = proxy + encodeURIComponent(apiUrl);
142
+
143
+ try {
144
+ const res = await fetch(fullUrl);
145
+ if (!res.ok) throw new Error("Project not found");
146
+ const data = await res.json();
147
+
148
+ resultsDiv.innerHTML = `
149
+ <div class="project-info">
150
+ <img src="https://uploads.scratch.mit.edu/projects/thumbnails/${id}.png" width="200"><br>
151
+ <h3>${data.title}</h3>
152
+ <p><strong>By:</strong> <a href="https://scratch.mit.edu/users/${data.author.username}" target="_blank">${data.author.username}</a></p>
153
+ <small>Project ID: ${id}</small>
154
+ <p>👀 Views: ${data.stats.views}</p>
155
+ <p>❤️ Loves: ${data.stats.loves}</p>
156
+ <p>⭐ Favorites: ${data.stats.favorites}</p>
157
+ </div>
158
+ `;
159
+ } catch (err) {
160
+ resultsDiv.innerHTML = `<p>❌ Could not load project details.</p>`;
161
+ console.error(err);
162
+ }
163
+ }
164
+
165
+ // Exit the current game
166
+ function exitGame() {
167
+ const player = document.getElementById('player');
168
+ const playerWrapper = document.getElementById('playerWrapper');
169
+ const exitButton = document.getElementById('exitButton');
170
+ const resultsDiv = document.getElementById('results');
171
+
172
+ // Instead of removing and recreating iframe, just clear src to stop game
173
+ if (player) {
174
+ player.src = '';
175
+ }
176
+
177
+ resultsDiv.innerHTML = '';
178
+ gameActive = false;
179
+
180
+ // If audio context exists, shut it down
181
+ if (audioCtx && typeof audioCtx.close === 'function') {
182
+ audioCtx.close().catch(() => {});
183
+ audioCtx = null;
184
+ }
185
+ }
186
+
187
+ function goFullscreen() {
188
+ const wrapper = document.getElementById('playerWrapper');
189
+ if (wrapper.requestFullscreen) {
190
+ wrapper.requestFullscreen();
191
+ } else if (wrapper.webkitRequestFullscreen) {
192
+ wrapper.webkitRequestFullscreen(); // Safari
193
+ } else if (wrapper.msRequestFullscreen) {
194
+ wrapper.msRequestFullscreen(); // IE11
195
+ } else {
196
+ alert("Fullscreen not supported on this browser.");
197
+ }
198
+ }
199
+
200
+ async function searchByTitle(keyword = null) {
201
+
202
+ const resultsDiv = document.getElementById('results');
203
+ const player = document.getElementById('player');
204
+ const titleInput = document.getElementById('titleSearchInput');
205
+
206
+ if (keyword !== null) {
207
+ lastTitle = keyword.trim();
208
+ titleOffset = 0;
209
+ }
210
+
211
+ const query = keyword !== null ? keyword.trim() : lastTitle;
212
+ if (!query) return;
213
+
214
+ resultsDiv.innerHTML = '';
215
+
216
+ const proxy = "https://withered-wind-d31c.micah-nordlund.workers.dev/?url=";
217
+ const apiUrl = `https://api.scratch.mit.edu/search/projects?limit=8&offset=${titleOffset}&mode=popular&q=${encodeURIComponent(query)}`;
218
+ const fullUrl = proxy + encodeURIComponent(apiUrl);
219
+
220
+ try {
221
+ const res = await fetch(fullUrl);
222
+ if (!res.ok) throw new Error('Failed to search');
223
+
224
+ const data = await res.json();
225
+ if (!data || data.length === 0) {
226
+ resultsDiv.innerText = '❌ No projects found with that title.';
227
+ return;
228
+ }
229
+
230
+ resultsDiv.innerHTML = `<h2>Search Results for "${query}"</h2>`;
231
+ displayProjects(data);
232
+
233
+ // Create container for pagination buttons
234
+ const btnContainer = document.createElement('div');
235
+ btnContainer.className = 'pagination-buttons';
236
+
237
+ // Add "Back" button if titleOffset > 0
238
+ if (titleOffset > 0) {
239
+ const backBtn = document.createElement('button');
240
+ backBtn.innerText = 'Back';
241
+ backBtn.classList.add('back-btn');
242
+ backBtn.onclick = () => {
243
+ titleOffset -= 8;
244
+ searchByTitle();
245
+ };
246
+ btnContainer.appendChild(backBtn);
247
+ }
248
+
249
+ // Add "Next" button if data.length === 8
250
+ if (data.length === 8) {
251
+ const nextBtn = document.createElement('button');
252
+ nextBtn.innerText = 'Next';
253
+ nextBtn.classList.add('more-back-btn');
254
+ nextBtn.onclick = () => {
255
+ titleOffset += 8;
256
+ searchByTitle();
257
+ };
258
+ btnContainer.appendChild(nextBtn);
259
+ }
260
+
261
+ // Append the container div to resultsDiv
262
+ resultsDiv.appendChild(btnContainer);
263
+
264
+ } catch (err) {
265
+ console.error("Title search failed:", err);
266
+ resultsDiv.innerText = '❌ Could not search by title.';
267
+ }
268
+ }
269
+
270
+ function searchMore() {
271
+ currentOffset += 8;
272
+ search();
273
+ }
274
+
275
+ function searchBack() {
276
+ if (currentOffset >= 8) {
277
+ currentOffset -= 8;
278
+ }
279
+ search();
280
+ }
281
+
282
+ document.getElementById('aboutBtn').addEventListener('click', () => {
283
+ window.location.href = 'about.html';
284
+ });
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "scratch-explorer-mtp",
3
+ "version": "1.0.0",
4
+ "description": "Frontend project served via unpkg",
5
+ "main": "index.html",
6
+ "scripts": {
7
+ "test": "echo \"No tests\""
8
+ },
9
+ "keywords": ["frontend", "unpkg", "demo"],
10
+ "author": "Micah",
11
+ "license": "ISC"
12
+ }