4sp-dv-latest 1.0.3 → 1.0.5
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/4simpleproblems_v5_latest.html +1185 -1593
- package/package.json +1 -1
|
@@ -6,13 +6,71 @@
|
|
|
6
6
|
<title>4SP - VERSION 5 CLIENT</title>
|
|
7
7
|
<link rel="icon" type="image/png" href="https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo.png">
|
|
8
8
|
|
|
9
|
-
<base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.0.
|
|
9
|
+
<base href="https://cdn.jsdelivr.net/npm/4sp-dv@1.0.48/logged-in/">
|
|
10
10
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
11
11
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
|
|
12
12
|
|
|
13
13
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
|
14
14
|
<style>
|
|
15
|
-
|
|
15
|
+
/* --- PRELOADER --- */
|
|
16
|
+
#app-preloader {
|
|
17
|
+
position: fixed;
|
|
18
|
+
inset: 0;
|
|
19
|
+
background-color: #000000;
|
|
20
|
+
z-index: 99999;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
align-items: flex-end;
|
|
24
|
+
justify-content: flex-end;
|
|
25
|
+
padding: 3rem;
|
|
26
|
+
transition: opacity 0.5s ease-out, visibility 0.5s;
|
|
27
|
+
}
|
|
28
|
+
#app-preloader.fade-out {
|
|
29
|
+
opacity: 0;
|
|
30
|
+
visibility: hidden;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.preloader-logo {
|
|
34
|
+
position: absolute;
|
|
35
|
+
top: 1.5rem;
|
|
36
|
+
right: 1.5rem;
|
|
37
|
+
height: 3rem;
|
|
38
|
+
width: auto;
|
|
39
|
+
opacity: 0.5; /* Initial opacity per universal style, not darkened */
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.preloader-content {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
align-items: flex-end;
|
|
46
|
+
gap: 0.5rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.preloader-title {
|
|
50
|
+
color: #fff;
|
|
51
|
+
font-size: 1.875rem; /* 3xl */
|
|
52
|
+
font-weight: 300;
|
|
53
|
+
font-style: italic;
|
|
54
|
+
font-family: 'Geist', sans-serif;
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.preloader-bar-container {
|
|
59
|
+
width: 16rem; /* 64 */
|
|
60
|
+
height: 0.25rem; /* 1 */
|
|
61
|
+
background-color: #333;
|
|
62
|
+
border-radius: 9999px;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.preloader-bar {
|
|
67
|
+
height: 100%;
|
|
68
|
+
background-color: #fff;
|
|
69
|
+
width: 0%;
|
|
70
|
+
transition: width 0.5s ease-out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
body {
|
|
16
74
|
background-color: #040404;
|
|
17
75
|
color: #c0c0c0;
|
|
18
76
|
font-family: 'Geist', sans-serif;
|
|
@@ -66,7 +124,7 @@
|
|
|
66
124
|
|
|
67
125
|
.navbar-logo { height: 40px; width: auto; transition: filter 0.3s ease; }
|
|
68
126
|
|
|
69
|
-
/* --- GLIDE / SCROLL STYLES
|
|
127
|
+
/* --- GLIDE / SCROLL STYLES --- */
|
|
70
128
|
.tab-wrapper {
|
|
71
129
|
flex-grow: 1;
|
|
72
130
|
display: flex;
|
|
@@ -75,7 +133,7 @@
|
|
|
75
133
|
min-width: 0;
|
|
76
134
|
margin: 0 1rem;
|
|
77
135
|
justify-content: center;
|
|
78
|
-
overflow: hidden;
|
|
136
|
+
overflow: hidden;
|
|
79
137
|
}
|
|
80
138
|
|
|
81
139
|
.tab-scroll-container {
|
|
@@ -86,9 +144,10 @@
|
|
|
86
144
|
scrollbar-width: none;
|
|
87
145
|
white-space: nowrap;
|
|
88
146
|
max-width: 100%;
|
|
89
|
-
scroll-behavior: smooth;
|
|
90
|
-
padding-left: 20px;
|
|
147
|
+
scroll-behavior: smooth;
|
|
148
|
+
padding-left: 20px;
|
|
91
149
|
padding-right: 20px;
|
|
150
|
+
padding-block: 10px;
|
|
92
151
|
}
|
|
93
152
|
.tab-scroll-container::-webkit-scrollbar { display: none; }
|
|
94
153
|
|
|
@@ -97,37 +156,39 @@
|
|
|
97
156
|
position: absolute;
|
|
98
157
|
top: 0;
|
|
99
158
|
height: 100%;
|
|
100
|
-
width: 60px;
|
|
159
|
+
width: 60px;
|
|
101
160
|
display: flex;
|
|
102
161
|
align-items: center;
|
|
103
162
|
justify-content: center;
|
|
104
|
-
color: #ffffff;
|
|
163
|
+
color: var(--glide-btn-color, #ffffff);
|
|
105
164
|
font-size: 1rem;
|
|
106
165
|
cursor: pointer;
|
|
107
166
|
opacity: 1;
|
|
108
|
-
transition: opacity 0.3s, color 0.3s ease;
|
|
109
|
-
z-index:
|
|
167
|
+
transition: opacity 0.3s, color 0.3s ease, background-color 0.3s ease;
|
|
168
|
+
z-index: 55;
|
|
110
169
|
pointer-events: auto;
|
|
111
170
|
background: transparent;
|
|
112
171
|
border: none;
|
|
113
172
|
}
|
|
114
173
|
|
|
115
|
-
/* Fading Gradients */
|
|
116
174
|
#glide-left {
|
|
117
175
|
left: 0;
|
|
118
|
-
background:
|
|
176
|
+
background-color: var(--navbar-bg, #000000);
|
|
177
|
+
-webkit-mask-image: linear-gradient(to right, black 30%, transparent);
|
|
178
|
+
mask-image: linear-gradient(to right, black 30%, transparent);
|
|
119
179
|
justify-content: flex-start;
|
|
120
180
|
padding-left: 8px;
|
|
121
181
|
}
|
|
122
182
|
#glide-right {
|
|
123
183
|
right: 0;
|
|
124
|
-
background:
|
|
184
|
+
background-color: var(--navbar-bg, #000000);
|
|
185
|
+
-webkit-mask-image: linear-gradient(to left, black 30%, transparent);
|
|
186
|
+
mask-image: linear-gradient(to left, black 30%, transparent);
|
|
125
187
|
justify-content: flex-end;
|
|
126
188
|
padding-right: 8px;
|
|
127
189
|
}
|
|
128
190
|
|
|
129
191
|
.scroll-glide-button.hidden { opacity: 0 !important; pointer-events: none !important; }
|
|
130
|
-
/* ----------------------------------------------------- */
|
|
131
192
|
|
|
132
193
|
.nav-tab {
|
|
133
194
|
padding: 0.5rem 1rem;
|
|
@@ -135,12 +196,15 @@
|
|
|
135
196
|
font-size: 0.875rem; font-weight: 400;
|
|
136
197
|
border-radius: 12px; text-decoration: none; display: flex; align-items: center; gap: 0.5rem;
|
|
137
198
|
border: 1px solid transparent; transition: all 0.2s; cursor: pointer;
|
|
138
|
-
flex-shrink: 0;
|
|
199
|
+
flex-shrink: 0;
|
|
200
|
+
position: relative;
|
|
139
201
|
}
|
|
140
202
|
.nav-tab:hover {
|
|
141
203
|
color: var(--tab-hover-text, #ffffff);
|
|
142
204
|
background-color: var(--tab-hover-bg, rgba(79, 70, 229, 0.05));
|
|
143
|
-
border-color: var(--tab-
|
|
205
|
+
border-color: var(--tab-active-border, #4f46e5);
|
|
206
|
+
transform: translateY(-1px);
|
|
207
|
+
z-index: 50;
|
|
144
208
|
}
|
|
145
209
|
.nav-tab.active {
|
|
146
210
|
color: var(--tab-active-text, #4f46e5);
|
|
@@ -153,234 +217,415 @@
|
|
|
153
217
|
background-color: var(--tab-active-hover-bg, rgba(79, 70, 229, 0.15));
|
|
154
218
|
}
|
|
155
219
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
background: var(--menu-bg, #000);
|
|
168
|
-
border: 1px solid var(--menu-border, #374151);
|
|
169
|
-
border-radius: 0.75rem;
|
|
170
|
-
padding: 0.5rem; display: none; flex-direction: column; gap: 0.25rem; z-index: 50;
|
|
171
|
-
}
|
|
172
|
-
.auth-menu.open { display: flex; }
|
|
173
|
-
|
|
174
|
-
.auth-menu-item {
|
|
175
|
-
display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem;
|
|
176
|
-
color: var(--menu-text, #d1d5db);
|
|
177
|
-
border-radius: 0.5rem; text-decoration: none; font-size: 0.875rem;
|
|
178
|
-
transition: background 0.15s; cursor: pointer;
|
|
179
|
-
}
|
|
180
|
-
.auth-menu-item:hover {
|
|
181
|
-
background-color: var(--menu-item-hover-bg, #374151);
|
|
182
|
-
color: var(--menu-item-hover-text, white);
|
|
183
|
-
}
|
|
184
|
-
.auth-header {
|
|
185
|
-
padding: 0.5rem;
|
|
186
|
-
border-bottom: 1px solid var(--menu-divider, #374151);
|
|
187
|
-
margin-bottom: 0.25rem;
|
|
188
|
-
}
|
|
189
|
-
.auth-username { color: var(--menu-username-text, white); font-weight: 400; font-size: 0.9rem; }
|
|
190
|
-
.auth-email { color: var(--menu-email-text, #9ca3af); font-size: 0.75rem; }
|
|
191
|
-
|
|
192
|
-
/* --- Settings Page Styles --- */
|
|
193
|
-
#settings-overlay {
|
|
194
|
-
position: fixed;
|
|
195
|
-
bottom: 0;
|
|
196
|
-
left: 0;
|
|
197
|
-
right: 0;
|
|
198
|
-
top: 64px; /* Below navbar */
|
|
199
|
-
background: #040404;
|
|
200
|
-
z-index: 50;
|
|
201
|
-
display: flex;
|
|
202
|
-
flex-direction: column;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
#settings-container {
|
|
206
|
-
display: flex;
|
|
207
|
-
flex-grow: 1;
|
|
208
|
-
padding: 20px;
|
|
209
|
-
overflow: hidden; /* Contain scroll */
|
|
210
|
-
}
|
|
220
|
+
.auth-controls-wrapper { display: flex; align-items: center; gap: 1rem; position: relative; }
|
|
221
|
+
|
|
222
|
+
.icon-btn {
|
|
223
|
+
width: 40px; height: 40px; border-radius: 50%; border: 1px solid #4b5563;
|
|
224
|
+
display: flex; align-items: center; justify-content: center; color: #d1d5db;
|
|
225
|
+
cursor: pointer; background: transparent; transition: background 0.2s; position: relative;
|
|
226
|
+
}
|
|
227
|
+
.icon-btn:hover {
|
|
228
|
+
background-color: #374151; color: white;
|
|
229
|
+
z-index: 50;
|
|
230
|
+
}
|
|
211
231
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
overflow-y: auto;
|
|
220
|
-
}
|
|
232
|
+
/* --- DISABLE ANIMATIONS FOR AUTH BTN --- */
|
|
233
|
+
#auth-btn, #auth-btn:hover, #auth-btn:active {
|
|
234
|
+
transform: none !important;
|
|
235
|
+
transition: none !important;
|
|
236
|
+
background-color: transparent !important;
|
|
237
|
+
z-index: auto !important;
|
|
238
|
+
}
|
|
221
239
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
240
|
+
/* Auth Menu & Settings */
|
|
241
|
+
.auth-menu {
|
|
242
|
+
position: absolute;
|
|
243
|
+
right: 0;
|
|
244
|
+
top: 55px;
|
|
245
|
+
width: 16rem;
|
|
246
|
+
background: var(--menu-bg, #000);
|
|
247
|
+
border: 1px solid var(--menu-border, #333);
|
|
248
|
+
border-radius: 1.5rem;
|
|
249
|
+
padding: 0.75rem;
|
|
250
|
+
display: none;
|
|
251
|
+
flex-direction: column;
|
|
252
|
+
gap: 0.5rem;
|
|
253
|
+
z-index: 50;
|
|
254
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.6);
|
|
255
|
+
transform-origin: top right;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@keyframes menu-pop-in {
|
|
259
|
+
0% { opacity: 0; transform: translateY(-10px) scale(0.95); }
|
|
260
|
+
70% { transform: translateY(2px) scale(1.01); }
|
|
261
|
+
100% { opacity: 1; transform: translateY(0) scale(1); }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.auth-menu.open {
|
|
265
|
+
display: flex;
|
|
266
|
+
animation: menu-pop-in 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.auth-menu-item {
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: center;
|
|
272
|
+
gap: 0.75rem;
|
|
273
|
+
padding: 0.75rem 1rem;
|
|
274
|
+
color: var(--menu-text, #d1d5db);
|
|
275
|
+
background: #0a0a0a;
|
|
276
|
+
border: 1px solid #333;
|
|
277
|
+
border-radius: 1rem;
|
|
278
|
+
text-decoration: none;
|
|
279
|
+
font-size: 0.9rem;
|
|
280
|
+
font-weight: 500;
|
|
281
|
+
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.auth-menu-item:hover {
|
|
286
|
+
background-color: #000;
|
|
287
|
+
border-color: #fff;
|
|
288
|
+
color: #fff;
|
|
289
|
+
transform: translateY(-2px) scale(1.02);
|
|
290
|
+
box-shadow: 0 5px 15px rgba(255,255,255,0.05);
|
|
291
|
+
}
|
|
231
292
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
border-radius: 0.75rem;
|
|
236
|
-
color: #d1d5db;
|
|
237
|
-
padding: 0.5rem 1rem;
|
|
238
|
-
font-weight: 500;
|
|
239
|
-
cursor: pointer;
|
|
240
|
-
transition: all 0.2s;
|
|
241
|
-
display: inline-flex;
|
|
242
|
-
align-items: center;
|
|
243
|
-
gap: 0.5rem;
|
|
244
|
-
}
|
|
245
|
-
.btn-toolbar-style:hover {
|
|
246
|
-
background-color: #000000;
|
|
247
|
-
border-color: #fff;
|
|
248
|
-
color: #ffffff;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
.btn-toolbar-style.btn-primary-override {
|
|
252
|
-
justify-content: center;
|
|
253
|
-
background-color: rgba(79, 70, 229, 0.1);
|
|
254
|
-
border: 1px solid #4f46e5;
|
|
255
|
-
color: #4f46e5;
|
|
256
|
-
}
|
|
257
|
-
.btn-toolbar-style.btn-primary-override:hover {
|
|
258
|
-
background-color: rgba(79, 70, 229, 0.15);
|
|
259
|
-
border-color: #6366f1;
|
|
260
|
-
color: #6366f1;
|
|
261
|
-
}
|
|
293
|
+
.auth-menu-item:active {
|
|
294
|
+
transform: translateY(0) scale(0.98);
|
|
295
|
+
}
|
|
262
296
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
297
|
+
.auth-menu-item i, .nav-tab i, .settings-tab i {
|
|
298
|
+
display: inline-flex;
|
|
299
|
+
align-items: center;
|
|
300
|
+
justify-content: center;
|
|
301
|
+
width: 1.1em;
|
|
302
|
+
height: 1.1em;
|
|
303
|
+
line-height: 1;
|
|
304
|
+
position: relative;
|
|
305
|
+
top: 1px;
|
|
306
|
+
flex-shrink: 0;
|
|
307
|
+
margin: 0;
|
|
308
|
+
}
|
|
274
309
|
|
|
275
|
-
.settings-tab {
|
|
276
|
-
width: 100%;
|
|
277
|
-
justify-content: flex-start;
|
|
278
|
-
}
|
|
279
|
-
.settings-tab.active {
|
|
280
|
-
background-color: rgba(79, 70, 229, 0.1);
|
|
281
|
-
border: 1px solid #4f46e5;
|
|
282
|
-
color: #4f46e5;
|
|
283
|
-
}
|
|
284
|
-
.settings-tab.active:hover {
|
|
285
|
-
background-color: rgba(79, 70, 229, 0.15);
|
|
286
|
-
border-color: #6366f1;
|
|
287
|
-
color: #6366f1;
|
|
288
|
-
}
|
|
289
310
|
|
|
290
|
-
.settings-box {
|
|
291
|
-
border: 1px solid #333;
|
|
292
|
-
border-radius: 1rem;
|
|
293
|
-
background-color: #000000;
|
|
294
|
-
padding: 1.5rem;
|
|
295
|
-
margin-bottom: 1.5rem;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
.input-text-style, .input-select-style {
|
|
299
|
-
width: 100%; padding: 0.75rem; border: 1px solid #252525;
|
|
300
|
-
background-color: #111111; border-radius: 0.5rem; color: #c0c0c0; transition: all 0.2s;
|
|
301
|
-
}
|
|
302
|
-
.input-text-style:focus, .input-select-style:focus {
|
|
303
|
-
border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5);
|
|
304
|
-
}
|
|
305
311
|
|
|
306
|
-
|
|
307
|
-
.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
color: #c0c0c0;
|
|
314
|
-
transition: all 0.2s;
|
|
315
|
-
font-size: 1.1rem;
|
|
316
|
-
text-align: center;
|
|
317
|
-
font-weight: 500;
|
|
318
|
-
text-transform: uppercase;
|
|
319
|
-
caret-color: transparent;
|
|
320
|
-
}
|
|
321
|
-
.input-key-style:focus {
|
|
322
|
-
border-color: #505050;
|
|
323
|
-
outline: none;
|
|
324
|
-
box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5);
|
|
325
|
-
}
|
|
326
|
-
.input-key-style::placeholder {
|
|
327
|
-
font-size: 1rem;
|
|
328
|
-
text-transform: none;
|
|
329
|
-
}
|
|
330
|
-
.general-message-area { min-height: 20px; margin-top: 1rem; font-weight: 400; }
|
|
331
|
-
.success-message { color: #4ade80; }
|
|
332
|
-
.error-message { color: #f87171; }
|
|
333
|
-
.warning-message { color: #fbbf24; }
|
|
334
|
-
|
|
335
|
-
/* --- Theme Picker Styles --- */
|
|
336
|
-
#theme-picker-container {
|
|
337
|
-
display: grid;
|
|
338
|
-
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
339
|
-
gap: 1rem;
|
|
340
|
-
width: 100%;
|
|
341
|
-
}
|
|
342
|
-
.theme-button {
|
|
343
|
-
border: 2px solid var(--tab-active-border, #4f46e5);
|
|
344
|
-
background-color: #111;
|
|
345
|
-
border-radius: 0.75rem;
|
|
346
|
-
padding: 0.75rem;
|
|
347
|
-
text-align: center;
|
|
348
|
-
cursor: pointer;
|
|
349
|
-
transition: all 0.2s ease-out;
|
|
350
|
-
opacity: 0.7;
|
|
351
|
-
position: relative;
|
|
352
|
-
}
|
|
353
|
-
.theme-button:hover {
|
|
354
|
-
opacity: 1;
|
|
355
|
-
border-color: var(--tab-active-hover-border, #6366f1);
|
|
356
|
-
transform: translateY(-2px);
|
|
357
|
-
}
|
|
358
|
-
.theme-button.active {
|
|
359
|
-
opacity: 1;
|
|
360
|
-
box-shadow: 0 0 10px rgba(79, 70, 229, 0.3);
|
|
361
|
-
}
|
|
362
|
-
.theme-button-name {
|
|
363
|
-
font-weight: 500;
|
|
364
|
-
color: #e0e0e0;
|
|
365
|
-
}
|
|
312
|
+
.auth-header {
|
|
313
|
+
padding: 0.5rem 1rem;
|
|
314
|
+
border-bottom: 1px solid var(--menu-divider, #333);
|
|
315
|
+
margin-bottom: 0.25rem;
|
|
316
|
+
}
|
|
317
|
+
.auth-username { color: var(--menu-username-text, white); font-weight: 400; font-size: 0.9rem; }
|
|
318
|
+
.auth-email { color: var(--menu-email-text, #9ca3af); font-size: 0.75rem; }
|
|
366
319
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
320
|
+
#settings-overlay {
|
|
321
|
+
position: fixed; bottom: 0; left: 0; right: 0; top: 64px;
|
|
322
|
+
background: #040404; z-index: 50; display: flex; flex-direction: column;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#settings-container { display: flex; flex-grow: 1; padding: 20px; overflow: hidden; }
|
|
326
|
+
|
|
327
|
+
#settings-sidebar {
|
|
328
|
+
width: 250px; padding-right: 20px; flex-shrink: 0;
|
|
329
|
+
display: flex; flex-direction: column; gap: 10px; overflow-y: auto;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#settings-main-view {
|
|
333
|
+
flex-grow: 1; padding: 20px; background-color: #0d0d0d;
|
|
334
|
+
border: 1px solid #1a1a1a; border-radius: 0.75rem;
|
|
335
|
+
overflow-y: auto; overflow-x: hidden;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.btn-toolbar-style {
|
|
339
|
+
background: #000000; border: 1px solid #333; border-radius: 0.75rem;
|
|
340
|
+
color: #d1d5db; padding: 0.5rem 1rem; font-weight: 500; cursor: pointer;
|
|
341
|
+
transition: all 0.2s; display: inline-flex; align-items: center; gap: 0.5rem;
|
|
342
|
+
position: relative;
|
|
343
|
+
}
|
|
344
|
+
.btn-toolbar-style:hover {
|
|
345
|
+
background-color: #000000; border-color: #fff; color: #ffffff;
|
|
346
|
+
z-index: 50;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.btn-toolbar-style.btn-primary-override {
|
|
350
|
+
justify-content: center; background-color: rgba(79, 70, 229, 0.1);
|
|
351
|
+
border: 1px solid #4f46e5; color: #4f46e5;
|
|
352
|
+
}
|
|
353
|
+
.btn-toolbar-style.btn-primary-override:hover {
|
|
354
|
+
background-color: rgba(79, 70, 229, 0.15); border-color: #6366f1; color: #6366f1;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.btn-toolbar-style.btn-primary-override-danger {
|
|
358
|
+
justify-content: center; background-color: rgba(220, 38, 38, 0.1);
|
|
359
|
+
border: 1px solid #dc2626; color: #dc2626;
|
|
360
|
+
}
|
|
361
|
+
.btn-toolbar-style.btn-primary-override-danger:hover {
|
|
362
|
+
background-color: rgba(220, 38, 38, 0.15); border-color: #ef4444; color: #ef4444;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.settings-tab { width: 100%; justify-content: flex-start; }
|
|
366
|
+
.settings-tab.active {
|
|
367
|
+
background-color: rgba(79, 70, 229, 0.1); border: 1px solid #4f46e5; color: #4f46e5;
|
|
368
|
+
}
|
|
369
|
+
.settings-tab.active:hover {
|
|
370
|
+
background-color: rgba(79, 70, 229, 0.15); border-color: #6366f1; color: #6366f1;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* --- DISABLE ANIMATIONS FOR SETTINGS TABS --- */
|
|
374
|
+
.settings-tab, .settings-tab:hover, .settings-tab:active {
|
|
375
|
+
transition: none !important;
|
|
376
|
+
transform: none !important;
|
|
377
|
+
animation: none !important;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.settings-box {
|
|
381
|
+
border: 1px solid #333; border-radius: 1.5rem;
|
|
382
|
+
background-color: #000000; padding: 1.5rem; margin-bottom: 1.5rem;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.input-text-style, .input-select-style {
|
|
386
|
+
width: 100%; padding: 0.75rem; border: 1px solid #252525;
|
|
387
|
+
background-color: #111111; border-radius: 1rem; color: #c0c0c0; transition: all 0.2s;
|
|
388
|
+
}
|
|
389
|
+
.input-text-style:focus, .input-select-style:focus {
|
|
390
|
+
border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.input-key-style {
|
|
394
|
+
width: 4rem; padding: 0.75rem; border: 1px solid #252525;
|
|
395
|
+
background-color: #111111; border-radius: 1rem; color: #c0c0c0;
|
|
396
|
+
transition: all 0.2s; font-size: 1.1rem; text-align: center;
|
|
397
|
+
font-weight: 500; text-transform: uppercase; caret-color: transparent;
|
|
398
|
+
}
|
|
399
|
+
.input-key-style:focus {
|
|
400
|
+
border-color: #505050; outline: none; box-shadow: 0 0 0 2px rgba(80, 80, 80, 0.5);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* Preference Toggles */
|
|
404
|
+
.icon-btn.off {
|
|
405
|
+
border-color: #ef4444;
|
|
406
|
+
color: #ef4444;
|
|
407
|
+
background-color: rgba(239, 68, 68, 0.05);
|
|
408
|
+
}
|
|
409
|
+
.icon-btn.off:hover {
|
|
410
|
+
background-color: rgba(239, 68, 68, 0.1);
|
|
411
|
+
border-color: #f87171;
|
|
412
|
+
}
|
|
413
|
+
.input-key-style::placeholder { font-size: 1rem; text-transform: none; }
|
|
414
|
+
|
|
415
|
+
.general-message-area { min-height: 20px; margin-top: 1rem; font-weight: 400; }
|
|
416
|
+
|
|
417
|
+
#theme-picker-container {
|
|
418
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
419
|
+
gap: 1rem; width: 100%;
|
|
420
|
+
}
|
|
421
|
+
.theme-button {
|
|
422
|
+
border: 2px solid var(--tab-active-border, #4f46e5);
|
|
423
|
+
background-color: #111; border-radius: 0.75rem; padding: 0.75rem;
|
|
424
|
+
text-align: center; cursor: pointer; transition: all 0.2s ease-out;
|
|
425
|
+
opacity: 0.7; position: relative;
|
|
426
|
+
}
|
|
427
|
+
.theme-button:hover {
|
|
428
|
+
opacity: 1; border-color: var(--tab-active-hover-border, #6366f1);
|
|
429
|
+
transform: translateY(-2px);
|
|
430
|
+
}
|
|
431
|
+
.theme-button.active { opacity: 1; box-shadow: 0 0 10px rgba(79, 70, 229, 0.3); }
|
|
432
|
+
.theme-button-name { font-weight: 500; color: #e0e0e0; }
|
|
433
|
+
|
|
434
|
+
/* Cropper */
|
|
435
|
+
#cropperModal {
|
|
436
|
+
display: none; position: fixed; z-index: 2050; left: 0; top: 0; width: 100%; height: 100%;
|
|
437
|
+
background-color: rgba(0, 0, 0, 0.85); justify-content: center; align-items: center; flex-direction: column;
|
|
438
|
+
}
|
|
439
|
+
#cropperContent {
|
|
440
|
+
background-color: #000000; padding: 0; border-radius: 1rem; border: 1px solid #333;
|
|
441
|
+
display: flex; flex-direction: column; width: 90%; max-width: 600px; overflow: hidden;
|
|
442
|
+
}
|
|
443
|
+
#cropperCanvasContainer {
|
|
444
|
+
position: relative; margin-bottom: 20px; overflow: hidden; max-height: 60vh;
|
|
445
|
+
}
|
|
446
|
+
#cropperCanvas { display: block; max-width: 100%; max-height: 60vh; border: 1px dashed rgba(255, 255, 255, 0.3); }
|
|
447
|
+
|
|
448
|
+
/* --- ANIMATION & THEME VARIABLES --- */
|
|
449
|
+
:root {
|
|
450
|
+
--bg-page: #040404; --bg-container: #000000; --border-color: #333;
|
|
451
|
+
--text-primary: #c0c0c0; --text-light-grey: #d1d5db; --accent-color: #6366f1;
|
|
452
|
+
--navbar-bg: #000000; --navbar-border: #1f2937;
|
|
453
|
+
--tab-text: #9ca3af; --tab-hover-text: #ffffff;
|
|
454
|
+
--tab-active-text: #4f46e5; --tab-active-bg: rgba(79, 70, 229, 0.1);
|
|
455
|
+
--tab-active-border: #4f46e5;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
@keyframes shake {
|
|
459
|
+
0% { transform: translateX(0); }
|
|
460
|
+
25% { transform: translateX(-5px) rotate(-5deg); }
|
|
461
|
+
50% { transform: translateX(5px) rotate(5deg); }
|
|
462
|
+
75% { transform: translateX(-5px) rotate(-5deg); }
|
|
463
|
+
100% { transform: translateX(0); }
|
|
464
|
+
}
|
|
465
|
+
.shake-anim { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
|
|
466
|
+
|
|
467
|
+
/* Animation Control */
|
|
468
|
+
body.no-animations *,
|
|
469
|
+
body.no-animations *:hover,
|
|
470
|
+
body.no-animations *:active,
|
|
471
|
+
body.no-animations *:focus {
|
|
472
|
+
transition: none !important;
|
|
473
|
+
animation: none !important;
|
|
474
|
+
transform: none !important;
|
|
475
|
+
box-shadow: none !important;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
body.no-animations .auth-menu-item .fa-gear,
|
|
479
|
+
body.no-animations .nav-tab .fa-gear {
|
|
480
|
+
transition: none !important;
|
|
481
|
+
transform: none !important;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/* Bubbly Spring Transition */
|
|
485
|
+
body:not(.no-animations) .btn-toolbar-style,
|
|
486
|
+
body:not(.no-animations) .btn-lock-screen,
|
|
487
|
+
body:not(.no-animations) .icon-btn,
|
|
488
|
+
body:not(.no-animations) .nav-tab,
|
|
489
|
+
body:not(.no-animations) .btn-card-action,
|
|
490
|
+
body:not(.no-animations) .input-text-style,
|
|
491
|
+
body:not(.no-animations) .input-lock-screen,
|
|
492
|
+
body:not(.no-animations) .input-key-style {
|
|
493
|
+
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
body:not(.no-animations) .btn-toolbar-style:active,
|
|
497
|
+
body:not(.no-animations) .btn-lock-screen:active,
|
|
498
|
+
body:not(.no-animations) .icon-btn:active,
|
|
499
|
+
body:not(.no-animations) .nav-tab:active,
|
|
500
|
+
body:not(.no-animations) .btn-card-action:active {
|
|
501
|
+
transform: scale(0.98); transition: transform 0.05s ease-out !important;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/* Refined Buttons - Reduced Scale for less clipping/pop */
|
|
505
|
+
.btn-toolbar-style {
|
|
506
|
+
background: #0a0a0a; border: 1px solid #333; border-radius: 14px;
|
|
507
|
+
color: #d1d5db; padding: 0.75rem 1.25rem; font-weight: 500; cursor: pointer;
|
|
508
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
|
|
509
|
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.6); position: relative;
|
|
510
|
+
}
|
|
511
|
+
.btn-toolbar-style:hover {
|
|
512
|
+
transform: scale(1.02) translateY(-1px);
|
|
513
|
+
box-shadow: 0 6px 20px rgba(255, 255, 255, 0.1);
|
|
514
|
+
border-color: #fff; color: #ffffff;
|
|
515
|
+
z-index: 50;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.btn-lock-screen {
|
|
519
|
+
padding: 0.75rem 1.25rem; font-size: 0.875rem; font-weight: 500; border-radius: 14px;
|
|
520
|
+
color: rgb(99 102 241); background-color: rgba(99, 102, 241, 0.1);
|
|
521
|
+
border: 1px solid rgb(99 102 241); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.6);
|
|
522
|
+
position: relative;
|
|
523
|
+
}
|
|
524
|
+
.btn-lock-screen:hover {
|
|
525
|
+
transform: scale(1.02) translateY(-1px);
|
|
526
|
+
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4);
|
|
527
|
+
background-color: rgba(99, 102, 241, 0.2);
|
|
528
|
+
z-index: 50;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.icon-btn {
|
|
532
|
+
width: 40px; height: 40px; border-radius: 14px;
|
|
533
|
+
border: 1px solid #4b5563; display: flex; align-items: center; justify-content: center; color: #d1d5db;
|
|
534
|
+
cursor: pointer; background: transparent; position: relative;
|
|
535
|
+
}
|
|
536
|
+
.icon-btn:hover {
|
|
537
|
+
background-color: #374151; color: white; transform: scale(1.05);
|
|
538
|
+
z-index: 50;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.btn-card-action {
|
|
542
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
543
|
+
width: 2rem; height: 2rem; border-radius: 14px;
|
|
544
|
+
background-color: transparent; border: 1px solid transparent;
|
|
545
|
+
color: #9ca3af; cursor: pointer; position: relative;
|
|
546
|
+
}
|
|
547
|
+
.btn-card-action:hover {
|
|
548
|
+
background-color: rgba(79, 70, 229, 0.1); color: #4f46e5; border-color: #4f46e5;
|
|
549
|
+
transform: scale(1.05); z-index: 50;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.input-text-style:focus, .input-select-style:focus { transform: scale(1.01); }
|
|
553
|
+
.input-lock-screen:focus { transform: scale(1.01); }
|
|
554
|
+
.input-key-style:focus { transform: scale(1.05); }
|
|
555
|
+
|
|
556
|
+
.nav-tab {
|
|
557
|
+
padding: 0.5rem 1rem; color: var(--tab-text);
|
|
558
|
+
font-size: 0.875rem; font-weight: 400; border-radius: 12px;
|
|
559
|
+
text-decoration: none; display: flex; align-items: center; gap: 0.5rem;
|
|
560
|
+
border: 1px solid transparent; cursor: pointer; background: transparent;
|
|
561
|
+
position: relative;
|
|
562
|
+
}
|
|
563
|
+
.nav-tab:hover {
|
|
564
|
+
color: var(--tab-hover-text); background-color: rgba(79, 70, 229, 0.05);
|
|
565
|
+
border-color: var(--tab-active-border, #4f46e5); transform: scale(1.02);
|
|
566
|
+
z-index: 50;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.eagler-dropdown {
|
|
570
|
+
position: relative; background-color: rgba(20, 20, 20, 0.98);
|
|
571
|
+
border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 14px;
|
|
572
|
+
padding: 0.5rem; min-width: 200px; display: inline-block;
|
|
573
|
+
}
|
|
574
|
+
.eagler-dropdown-link {
|
|
575
|
+
display: block; padding: 0.6rem 0.8rem; color: #d1d5db; text-decoration: none;
|
|
576
|
+
border-radius: 14px; font-size: 0.9rem;
|
|
577
|
+
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
578
|
+
}
|
|
579
|
+
.eagler-dropdown-link:hover {
|
|
580
|
+
background-color: rgba(255, 255, 255, 0.1); color: white; transform: translateX(4px);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/* Notifications */
|
|
584
|
+
#notification-container {
|
|
585
|
+
position: fixed;
|
|
586
|
+
bottom: 2rem;
|
|
587
|
+
right: 2rem;
|
|
588
|
+
display: flex;
|
|
589
|
+
flex-direction: column;
|
|
590
|
+
gap: 0.75rem;
|
|
591
|
+
z-index: 9999;
|
|
592
|
+
pointer-events: none;
|
|
593
|
+
}
|
|
594
|
+
.notification-toast {
|
|
595
|
+
background-color: #0a0a0a; border: 1px solid #333; border-radius: 14px;
|
|
596
|
+
padding: 0.75rem 1.25rem; color: #fff; box-shadow: 0 4px 15px rgba(0,0,0,0.5);
|
|
597
|
+
display: flex; align-items: center; gap: 0.75rem; font-size: 0.9rem;
|
|
598
|
+
min-width: 200px; transform: translateX(120%);
|
|
599
|
+
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s ease, background-color 0.2s;
|
|
600
|
+
opacity: 0; pointer-events: auto; cursor: default;
|
|
601
|
+
}
|
|
602
|
+
.notification-toast.show { transform: translateX(0); opacity: 1; }
|
|
603
|
+
.notification-toast.show:hover {
|
|
604
|
+
transform: scale(1.02) translateX(-5px); background-color: #151515;
|
|
605
|
+
border-color: #555; box-shadow: 0 8px 25px rgba(0,0,0,0.7);
|
|
606
|
+
}
|
|
380
607
|
|
|
608
|
+
/* UNIVERSAL LOADER CSS */
|
|
609
|
+
#universal-loader {
|
|
610
|
+
pointer-events: none; /* Allows clicks to pass through when hidden/opacity 0 */
|
|
611
|
+
}
|
|
612
|
+
#universal-loader.active {
|
|
613
|
+
pointer-events: auto;
|
|
614
|
+
}
|
|
381
615
|
</style>
|
|
382
616
|
</head>
|
|
383
617
|
<body>
|
|
618
|
+
<!-- PRELOADER -->
|
|
619
|
+
<div id="app-preloader">
|
|
620
|
+
<img src="https://cdn.jsdelivr.net/npm/4sp-asset-library@latest/logo.png" class="preloader-logo" alt="Logo" onerror="this.style.display='none'">
|
|
621
|
+
<div class="preloader-content">
|
|
622
|
+
<h2 class="preloader-title">4simpleproblems Version 5 Client</h2>
|
|
623
|
+
<div class="preloader-bar-container">
|
|
624
|
+
<div id="preloader-bar" class="preloader-bar"></div>
|
|
625
|
+
</div>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
|
|
384
629
|
<div id="lock-screen" class="w-full h-full flex flex-col items-center justify-center p-4">
|
|
385
630
|
<div class="max-w-5xl w-full bg-[#040404] flex flex-col md:flex-row border border-[#252525] rounded-3xl overflow-hidden shadow-2xl">
|
|
386
631
|
<div class="flex-1 p-8 flex flex-col justify-center items-center border-b md:border-b-0 md:border-r border-[#252525]">
|
|
@@ -430,7 +675,7 @@
|
|
|
430
675
|
<div class="auth-username" id="display-username">Client User</div>
|
|
431
676
|
<div class="auth-email" id="display-email">local@client</div>
|
|
432
677
|
</div>
|
|
433
|
-
<a href="#" class="auth-menu-item" onclick="loadPage('settings.html'); return false;">
|
|
678
|
+
<a href="#" id="auth-settings-btn" class="auth-menu-item" onclick="loadPage('settings.html'); return false;">
|
|
434
679
|
<i class="fa-solid fa-gear w-4"></i> Settings
|
|
435
680
|
</a>
|
|
436
681
|
<div class="auth-menu-item text-red-400 hover:text-red-300" id="logout-btn">
|
|
@@ -467,13 +712,29 @@
|
|
|
467
712
|
<h3 class="text-3xl font-bold text-white mb-6">General Settings</h3>
|
|
468
713
|
|
|
469
714
|
<div class="settings-box">
|
|
470
|
-
<h3 class="text-xl font-bold text-white mb-2">Account
|
|
715
|
+
<h3 class="text-xl font-bold text-white mb-2">Account Details & Preferences</h3>
|
|
716
|
+
|
|
471
717
|
<label class="block text-gray-400 text-sm mb-2 font-light">New Username</label>
|
|
472
|
-
<div class="flex gap-2">
|
|
718
|
+
<div class="flex gap-2 mb-6">
|
|
473
719
|
<input type="text" id="settings-username-input" class="input-text-style" placeholder="Enter username">
|
|
474
720
|
<button id="save-username-btn" class="btn-toolbar-style btn-primary-override">Save</button>
|
|
475
721
|
</div>
|
|
476
722
|
<p id="username-msg" class="general-message-area text-sm mt-2"></p>
|
|
723
|
+
|
|
724
|
+
<div class="flex gap-8 items-center pt-4 border-t border-[#1a1a1a]">
|
|
725
|
+
<div class="flex flex-col items-center gap-2">
|
|
726
|
+
<button id="toggle-animations-btn" class="icon-btn" title="Toggle Navbar Animations">
|
|
727
|
+
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
|
728
|
+
</button>
|
|
729
|
+
<span class="text-xs text-gray-500">Animations</span>
|
|
730
|
+
</div>
|
|
731
|
+
<div class="flex flex-col items-center gap-2">
|
|
732
|
+
<button id="toggle-sounds-btn" class="icon-btn" title="Toggle UI Sounds">
|
|
733
|
+
<i class="fa-solid fa-volume-high"></i>
|
|
734
|
+
</button>
|
|
735
|
+
<span class="text-xs text-gray-500">Sounds</span>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
477
738
|
</div>
|
|
478
739
|
|
|
479
740
|
<h3 class="text-xl font-bold text-white mb-2 mt-6">Organization</h3>
|
|
@@ -506,7 +767,7 @@
|
|
|
506
767
|
<div class="settings-box mb-8 p-4">
|
|
507
768
|
<div class="flex items-start gap-6">
|
|
508
769
|
<div class="flex flex-col items-center gap-2">
|
|
509
|
-
<div id="pfp-preview" class="w-24 h-24 rounded-
|
|
770
|
+
<div id="pfp-preview" class="w-24 h-24 rounded-[32px] bg-gray-700 overflow-hidden border-2 border-[#333] flex items-center justify-center text-3xl font-bold text-white relative">
|
|
510
771
|
</div>
|
|
511
772
|
<p class="text-xs text-gray-500">Preview</p>
|
|
512
773
|
</div>
|
|
@@ -518,15 +779,29 @@
|
|
|
518
779
|
</div>
|
|
519
780
|
|
|
520
781
|
<div id="pfp-letter-options" class="block">
|
|
521
|
-
<
|
|
522
|
-
|
|
523
|
-
|
|
782
|
+
<label class="block text-gray-400 text-xs mb-1 font-light">Custom Text (Max 3)</label>
|
|
783
|
+
<div class="flex gap-4 items-center mb-4">
|
|
784
|
+
<input type="text" id="pfp-custom-text" maxlength="3" class="input-text-style w-24 text-center uppercase" placeholder="A">
|
|
785
|
+
<p class="text-xs text-gray-500">Leave empty to use username initial.</p>
|
|
786
|
+
</div>
|
|
787
|
+
|
|
788
|
+
<label class="block text-gray-400 text-xs mb-2 font-light">Background Color</label>
|
|
789
|
+
<div class="flex flex-wrap gap-2 mb-6" id="pfp-color-grid">
|
|
790
|
+
<!-- Colors generated by JS -->
|
|
791
|
+
</div>
|
|
792
|
+
|
|
793
|
+
<button id="save-letter-pfp-btn" class="btn-toolbar-style btn-primary-override w-full justify-center">Set Letter Avatar</button>
|
|
524
794
|
</div>
|
|
525
795
|
|
|
526
796
|
<div id="pfp-upload-options" class="hidden">
|
|
527
|
-
<input type="file" id="pfp-upload-input" accept="image/*" class="
|
|
528
|
-
|
|
529
|
-
<button id="
|
|
797
|
+
<input type="file" id="pfp-upload-input" accept="image/*" class="hidden"/>
|
|
798
|
+
|
|
799
|
+
<button id="trigger-upload-btn" class="btn-toolbar-style mb-4 w-full justify-center">
|
|
800
|
+
<i class="fa-solid fa-cloud-arrow-up mr-2"></i> Choose Image...
|
|
801
|
+
</button>
|
|
802
|
+
|
|
803
|
+
<p class="text-xs text-gray-500 mb-4 text-center">Max size 2MB. Square images work best.</p>
|
|
804
|
+
<button id="save-upload-pfp-btn" class="btn-toolbar-style btn-primary-override w-full justify-center">Upload & Set</button>
|
|
530
805
|
</div>
|
|
531
806
|
</div>
|
|
532
807
|
</div>
|
|
@@ -552,1242 +827,88 @@
|
|
|
552
827
|
|
|
553
828
|
<div class="flex items-center gap-4 px-2 mb-2">
|
|
554
829
|
<label class="block text-gray-400 text-sm font-light" style="width: 4rem; text-align: center;">Key</label>
|
|
555
|
-
<label class="block text-gray-400 text-sm font-light flex-grow">Redirect URL</label>
|
|
556
|
-
</div>
|
|
557
|
-
|
|
558
|
-
<div class="flex items-center gap-4 mb-3">
|
|
559
|
-
<input type="text" id="panicKey1" data-key-id="1" class="input-key-style panic-key-input" placeholder="-" maxlength="1">
|
|
560
|
-
<input type="url" id="panicUrl1" class="input-text-style" placeholder="e.g., https://google.com">
|
|
561
|
-
</div>
|
|
562
|
-
|
|
563
|
-
<div class="flex items-center gap-4 mb-3">
|
|
564
|
-
<input type="text" id="panicKey2" data-key-id="2" class="input-key-style panic-key-input" placeholder="-" maxlength="1">
|
|
565
|
-
<input type="url" id="panicUrl2" class="input-text-style" placeholder="e.g., https://youtube.com/feed/subscriptions">
|
|
566
|
-
</div>
|
|
567
|
-
|
|
568
|
-
<div class="flex items-center gap-4 mb-3">
|
|
569
|
-
<input type="text" id="panicKey3" data-key-id="3" class="input-key-style panic-key-input" placeholder="-" maxlength="1">
|
|
570
|
-
<input type="url" id="panicUrl3" class="input-text-style" placeholder="e.g., https://wikipedia.org">
|
|
571
|
-
</div>
|
|
572
|
-
|
|
573
|
-
<div class="flex justify-between items-center pt-4 border-t border-[#252525]">
|
|
574
|
-
<p id="panicKeyMessage" class="general-message-area text-sm"></p>
|
|
575
|
-
<button id="applyPanicKeyBtn" class="btn-toolbar-style btn-primary-override w-36" style="padding: 0.5rem 0.75rem;">
|
|
576
|
-
<i class="fa-solid fa-check mr-1"></i> Apply Keys
|
|
577
|
-
</button>
|
|
578
|
-
</div>
|
|
579
|
-
</div>
|
|
580
|
-
</div>
|
|
581
|
-
|
|
582
|
-
<div id="tab-about" class="settings-section hidden">
|
|
583
|
-
<h3 class="text-3xl font-bold text-white mb-6">About 4SP</h3>
|
|
584
|
-
<div class="settings-box p-6 flex flex-col items-center text-center">
|
|
585
|
-
<img src="https://cdn.jsdelivr.net/npm/4sp-asset-library@latest/logo.png" class="h-24 mb-4" alt="Logo">
|
|
586
|
-
<h1 class="text-3xl font-bold text-white mb-2">(4SP) 4simpleproblems</h1>
|
|
587
|
-
<p class="text-gray-400 mb-6 max-w-lg font-light">From a soundboard, to a full downloadable client of a platform.</p>
|
|
588
|
-
|
|
589
|
-
<div class="inline-block bg-[#0a0a0a] border border-[#333] px-4 py-2 rounded-[14px] mb-8">
|
|
590
|
-
<span class="text-gray-500 text-sm">Version:</span>
|
|
591
|
-
<span class="text-indigo-400 font-mono font-bold ml-2">5.0.0 (DV)</span>
|
|
592
|
-
</div>
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
<div class="flex gap-4">
|
|
596
|
-
<a href="https://github.com/4simpleproblems-v5" class="text-gray-400 hover:text-white transition"><i class="fa-brands fa-github fa-xl"></i></a>
|
|
597
|
-
<a href="https://youtube.com/4simpleproblems" class="text-gray-400 hover:text-white transition"><i class="fa-brands fa-youtube fa-xl"></i></a>
|
|
598
|
-
<a href="https://x.com/@4simpleproblems" class="text-gray-400 hover:text-white transition"><i class="fa-brands fa-discord fa-xl"></i></a>
|
|
599
|
-
</div>
|
|
600
|
-
</div>
|
|
601
|
-
</div>
|
|
602
|
-
</div>
|
|
603
|
-
</div>
|
|
604
|
-
</div>
|
|
605
|
-
|
|
606
|
-
<div id="cropperModal">
|
|
607
|
-
<div id="cropperContent">
|
|
608
|
-
<div class="flex justify-between items-center p-6 border-b border-[#333] bg-black">
|
|
609
|
-
<h3 class="text-2xl font-bold text-white">Adjust Profile Picture</h3>
|
|
610
|
-
<button id="cancelCropBtn" class="btn-toolbar-style w-10 h-10 flex items-center justify-center p-0 rounded-xl">
|
|
611
|
-
<i class="fa-solid fa-xmark fa-xl"></i>
|
|
612
|
-
</button>
|
|
613
|
-
</div>
|
|
614
|
-
<div class="flex flex-col items-center p-6 bg-[#0a0a0a]">
|
|
615
|
-
<p class="text-sm text-gray-400 mb-4">Drag to move. Scroll to resize.</p>
|
|
616
|
-
<div id="cropperCanvasContainer" class="relative mb-6 border border-[#333] rounded-lg overflow-hidden w-full flex justify-center bg-black">
|
|
617
|
-
<canvas id="cropperCanvas"></canvas>
|
|
618
|
-
</div>
|
|
619
|
-
<div class="flex justify-end w-full">
|
|
620
|
-
<button id="submitCropBtn" class="btn-toolbar-style btn-primary-override px-6 py-2 rounded-xl">
|
|
621
|
-
<i class="fa-solid fa-check mr-2"></i> Submit
|
|
622
|
-
</button>
|
|
623
|
-
</div>
|
|
624
|
-
</div>
|
|
625
|
-
</div>
|
|
626
|
-
</div>
|
|
627
|
-
|
|
628
|
-
<template id="games-page-template">
|
|
629
|
-
<!DOCTYPE html>
|
|
630
|
-
<html lang="en">
|
|
631
|
-
<head>
|
|
632
|
-
<title>4SP - GAMES</title>
|
|
633
|
-
<meta charset="UTF-8" />
|
|
634
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
|
635
|
-
<link rel="icon" type="image/x-icon" href="img/favicon.ico" id="faviconLink" />
|
|
636
|
-
|
|
637
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
638
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
639
|
-
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
|
|
640
|
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
|
641
|
-
|
|
642
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
643
|
-
<script src="https://dv-service-lfs.4simpleproblems.workers.dev/url-changer.js"></script>
|
|
644
|
-
<script src="https://cdn.jsdelivr.net/npm/4sp-dv@latest/analytics.js"></script>
|
|
645
|
-
|
|
646
|
-
<script async src="https://www.googletagmanager.com/gtag/js?id=G-1D4F692C1Q"></script>
|
|
647
|
-
<script>
|
|
648
|
-
window.dataLayer = window.dataLayer || [];
|
|
649
|
-
function gtag(){dataLayer.push(arguments);}
|
|
650
|
-
gtag('js', new Date());
|
|
651
|
-
|
|
652
|
-
gtag('config', 'G-1D4F692C1Q');
|
|
653
|
-
</script>
|
|
654
|
-
<script>
|
|
655
|
-
tailwind.config = {
|
|
656
|
-
darkMode: 'class',
|
|
657
|
-
theme: {
|
|
658
|
-
extend: {
|
|
659
|
-
colors: {
|
|
660
|
-
'deep-black': '#040404',
|
|
661
|
-
'card-dark': '#111111',
|
|
662
|
-
'accent-indigo': '#4f46e5',
|
|
663
|
-
'brand-border': '#252525',
|
|
664
|
-
'strongdog-orange': '#FFA500',
|
|
665
|
-
'gn-math': '#FB2651',
|
|
666
|
-
'gameboy': '#9A2257',
|
|
667
|
-
'other-grey': '#AAAAAA',
|
|
668
|
-
},
|
|
669
|
-
fontFamily: {
|
|
670
|
-
sans: ['Geist', 'sans-serif'],
|
|
671
|
-
},
|
|
672
|
-
borderRadius: {
|
|
673
|
-
'md': '0.375rem',
|
|
674
|
-
'lg': '0.5rem',
|
|
675
|
-
'xl': '0.75rem',
|
|
676
|
-
'2xl': '1.25rem',
|
|
677
|
-
'3xl': '1.5rem',
|
|
678
|
-
},
|
|
679
|
-
aspectRatio: {
|
|
680
|
-
'3/2': '3 / 2',
|
|
681
|
-
},
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
</script>
|
|
686
|
-
|
|
687
|
-
<style>
|
|
688
|
-
/* Base styles */
|
|
689
|
-
:root {
|
|
690
|
-
--menu-bg: #000000;
|
|
691
|
-
--menu-border: #333;
|
|
692
|
-
--menu-text: #d1d5db;
|
|
693
|
-
--tab-hover-text: #ffffff;
|
|
694
|
-
--tab-active-bg: rgba(79, 70, 229, 0.1);
|
|
695
|
-
--tab-active-text: #4f46e5;
|
|
696
|
-
--tab-active-border: #4f46e5;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
body {
|
|
700
|
-
font-family: 'Geist', 'sans-serif';
|
|
701
|
-
font-weight: 300;
|
|
702
|
-
background-color: #040404;
|
|
703
|
-
padding-bottom: 100px;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
h1, h2, h3, .font-bold, .font-semibold, strong {
|
|
707
|
-
font-weight: 400 !important;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
/* --- Toolbar Button Style (Global) --- */
|
|
711
|
-
.btn-toolbar-style {
|
|
712
|
-
background: var(--menu-bg);
|
|
713
|
-
border: 1px solid var(--menu-border);
|
|
714
|
-
border-radius: 0.75rem;
|
|
715
|
-
color: var(--menu-text);
|
|
716
|
-
padding: 0.5rem 1rem;
|
|
717
|
-
font-weight: 500;
|
|
718
|
-
cursor: pointer;
|
|
719
|
-
transition: all 0.2s;
|
|
720
|
-
display: inline-flex;
|
|
721
|
-
align-items: center;
|
|
722
|
-
justify-content: center;
|
|
723
|
-
gap: 0.5rem;
|
|
724
|
-
text-decoration: none;
|
|
725
|
-
font-size: 1rem;
|
|
726
|
-
}
|
|
727
|
-
.btn-toolbar-style:hover {
|
|
728
|
-
background-color: var(--menu-bg);
|
|
729
|
-
border-color: #fff;
|
|
730
|
-
color: var(--tab-hover-text);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
/* --- Card Action Buttons --- */
|
|
734
|
-
.btn-card-action {
|
|
735
|
-
display: inline-flex;
|
|
736
|
-
align-items: center;
|
|
737
|
-
justify-content: center;
|
|
738
|
-
width: 2.25rem;
|
|
739
|
-
height: 2.25rem;
|
|
740
|
-
border-radius: 0.5rem;
|
|
741
|
-
background-color: transparent;
|
|
742
|
-
border: 1px solid transparent;
|
|
743
|
-
color: #9ca3af;
|
|
744
|
-
cursor: pointer;
|
|
745
|
-
transition: all 0.2s;
|
|
746
|
-
font-size: 1rem;
|
|
747
|
-
}
|
|
748
|
-
.btn-card-action:hover {
|
|
749
|
-
background-color: var(--tab-active-bg);
|
|
750
|
-
color: var(--tab-active-text);
|
|
751
|
-
border-color: var(--tab-active-border);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/* Favorite Button Logic */
|
|
755
|
-
.btn-card-action.fav-action .fa-solid-star { display: none; }
|
|
756
|
-
.btn-card-action.fav-action .fa-regular-star { display: inline-block; }
|
|
757
|
-
|
|
758
|
-
.btn-card-action.fav-action.favorited .fa-solid-star { display: inline-block; color: #facc15; }
|
|
759
|
-
.btn-card-action.fav-action.favorited .fa-regular-star { display: none; }
|
|
760
|
-
|
|
761
|
-
.btn-card-action.fav-action:not(.favorited):hover .fa-regular-star { color: var(--tab-active-text); }
|
|
762
|
-
|
|
763
|
-
/* Card Hover Effects */
|
|
764
|
-
.zone-item {
|
|
765
|
-
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
|
766
|
-
will-change: transform;
|
|
767
|
-
}
|
|
768
|
-
.zone-item:hover {
|
|
769
|
-
transform: translateY(-4px);
|
|
770
|
-
box-shadow: 0 8px 25px rgba(79, 70, 229, 0.2);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
.other-zone-item {
|
|
774
|
-
min-height: 250px;
|
|
775
|
-
transition: box-shadow 0.2s ease-in-out;
|
|
776
|
-
position: relative;
|
|
777
|
-
z-index: 1;
|
|
778
|
-
}
|
|
779
|
-
.other-zone-item:hover {
|
|
780
|
-
box-shadow: 0 8px 25px rgba(170, 170, 170, 0.2);
|
|
781
|
-
z-index: 10;
|
|
782
|
-
}
|
|
783
|
-
.other-zone-item .image-overlay {
|
|
784
|
-
background: linear-gradient(90deg, rgba(7,7,7,0.85) 0%, rgba(7,7,7,0) 40%, rgba(7,7,7,0) 60%, rgba(7,7,7,0.85) 100%);
|
|
785
|
-
border-radius: 1.25rem;
|
|
786
|
-
}
|
|
787
|
-
.other-zone-item:hover .image-overlay {
|
|
788
|
-
background: linear-gradient(90deg, rgba(7,7,7,0.7) 0%, rgba(7,7,7,0) 50%, rgba(7,7,7,0) 50%, rgba(7,7,7,0.7) 100%);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
/* --- Animations --- */
|
|
792
|
-
@keyframes fadeOut {
|
|
793
|
-
from { opacity: 1; transform: scale(1); }
|
|
794
|
-
to { opacity: 0; transform: scale(0.95); }
|
|
795
|
-
}
|
|
796
|
-
.fade-out {
|
|
797
|
-
animation: fadeOut 0.3s ease-out forwards;
|
|
798
|
-
pointer-events: none;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/* Search Bar & Results */
|
|
802
|
-
.search-results-container {
|
|
803
|
-
max-height: 300px;
|
|
804
|
-
overflow-y: auto;
|
|
805
|
-
scrollbar-width: thin;
|
|
806
|
-
scrollbar-color: #333 transparent;
|
|
807
|
-
}
|
|
808
|
-
.search-item {
|
|
809
|
-
display: flex;
|
|
810
|
-
cursor: pointer;
|
|
811
|
-
transition: background-color 0.2s;
|
|
812
|
-
color: #e5e7eb;
|
|
813
|
-
text-decoration: none;
|
|
814
|
-
padding: 10px 15px;
|
|
815
|
-
}
|
|
816
|
-
.search-item:hover { background-color: rgba(255, 255, 255, 0.05); }
|
|
817
|
-
|
|
818
|
-
@media (max-width: 768px) {
|
|
819
|
-
#bottom-fixed-bar {
|
|
820
|
-
left: 10px !important; right: 10px !important;
|
|
821
|
-
bottom: 10px !important; width: auto !important;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
/* Polyfill for aspect-ratio */
|
|
826
|
-
.aspect-w-3 { position: relative; padding-bottom: 66.666667%; }
|
|
827
|
-
.aspect-h-2 { }
|
|
828
|
-
.aspect-w-3 > * { position: absolute; height: 100%; width: 100%; top: 0; right: 0; bottom: 0; left: 0; }
|
|
829
|
-
|
|
830
|
-
/* --- Game Viewer Modal --- */
|
|
831
|
-
#zoneViewer { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.9); z-index: 5000; flex-direction: column; }
|
|
832
|
-
#zoneViewer .zone-header {
|
|
833
|
-
background-color: #111111;
|
|
834
|
-
padding: 12px 20px;
|
|
835
|
-
display: flex;
|
|
836
|
-
justify-content: space-between;
|
|
837
|
-
align-items: center;
|
|
838
|
-
border-bottom: 1px solid #252525;
|
|
839
|
-
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
840
|
-
flex-shrink: 0;
|
|
841
|
-
}
|
|
842
|
-
#zoneViewer .zone-header .zone-title h2 {
|
|
843
|
-
margin: 0;
|
|
844
|
-
font-size: 1.3em;
|
|
845
|
-
color: #ffffff;
|
|
846
|
-
font-family: 'Geist', sans-serif;
|
|
847
|
-
}
|
|
848
|
-
#zoneViewer .zone-header .zone-controls { display: flex; align-items: center; }
|
|
849
|
-
#zoneViewer .zone-header .zone-controls button,
|
|
850
|
-
#zoneViewer .zone-header .zone-controls a {
|
|
851
|
-
margin-left: 8px;
|
|
852
|
-
width: 40px;
|
|
853
|
-
height: 40px;
|
|
854
|
-
background-color: rgba(255, 255, 255, 0.1);
|
|
855
|
-
backdrop-filter: blur(5px);
|
|
856
|
-
-webkit-backdrop-filter: blur(5px);
|
|
857
|
-
color: white;
|
|
858
|
-
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
859
|
-
border-radius: 0.5rem;
|
|
860
|
-
cursor: pointer;
|
|
861
|
-
font-size: 1em;
|
|
862
|
-
display: flex;
|
|
863
|
-
align-items: center;
|
|
864
|
-
justify-content: center;
|
|
865
|
-
transition: background-color 0.2s ease-in-out;
|
|
866
|
-
text-decoration: none;
|
|
867
|
-
}
|
|
868
|
-
#zoneViewer .zone-header .zone-controls button:hover,
|
|
869
|
-
#zoneViewer .zone-header .zone-controls a:hover { background-color: rgba(255, 255, 255, 0.2); }
|
|
870
|
-
#zoneViewer iframe { flex-grow: 1; border: none; background-color: #000; }
|
|
871
|
-
.hidden { display: none !important; }
|
|
872
|
-
|
|
873
|
-
/* --- Dropdown (Modified to popup ABOVE) --- */
|
|
874
|
-
.eagler-dropdown {
|
|
875
|
-
display: none;
|
|
876
|
-
position: absolute;
|
|
877
|
-
bottom: 120%; /* Pops up above the button */
|
|
878
|
-
right: 0;
|
|
879
|
-
margin-bottom: 5px;
|
|
880
|
-
background-color: rgba(20, 20, 20, 0.98);
|
|
881
|
-
backdrop-filter: blur(12px);
|
|
882
|
-
-webkit-backdrop-filter: blur(12px);
|
|
883
|
-
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
884
|
-
border-radius: 0.75rem;
|
|
885
|
-
padding: 0.5rem;
|
|
886
|
-
z-index: 1000;
|
|
887
|
-
min-width: 200px;
|
|
888
|
-
box-shadow: 0 10px 25px rgba(0,0,0,0.6);
|
|
889
|
-
transform-origin: bottom right;
|
|
890
|
-
}
|
|
891
|
-
.eagler-dropdown.show { display: block; animation: fadeInUp 0.15s ease-out; }
|
|
892
|
-
|
|
893
|
-
@keyframes fadeInUp {
|
|
894
|
-
from { opacity: 0; transform: translateY(10px) scale(0.95); }
|
|
895
|
-
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
.eagler-dropdown-link {
|
|
899
|
-
display: block;
|
|
900
|
-
padding: 0.6rem 0.8rem;
|
|
901
|
-
color: #d1d5db;
|
|
902
|
-
text-decoration: none;
|
|
903
|
-
border-radius: 0.5rem;
|
|
904
|
-
transition: all 0.2s;
|
|
905
|
-
font-weight: 400;
|
|
906
|
-
white-space: nowrap;
|
|
907
|
-
font-size: 0.9rem;
|
|
908
|
-
}
|
|
909
|
-
.eagler-dropdown-link:hover { background-color: rgba(255, 255, 255, 0.1); color: white; }
|
|
910
|
-
|
|
911
|
-
#creditsModal { display: none; }
|
|
912
|
-
|
|
913
|
-
/* Modal Scrollbar */
|
|
914
|
-
.custom-scrollbar::-webkit-scrollbar { width: 8px; }
|
|
915
|
-
.custom-scrollbar::-webkit-scrollbar-track { background: rgba(0,0,0,0.3); border-radius: 4px; }
|
|
916
|
-
.custom-scrollbar::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
|
|
917
|
-
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #555; }
|
|
918
|
-
</style>
|
|
919
|
-
</head>
|
|
920
|
-
|
|
921
|
-
<body class="bg-deep-black text-white min-h-screen">
|
|
922
|
-
|
|
923
|
-
<main class="mx-auto my-5 w-full max-w-screen-2xl px-4 relative">
|
|
924
|
-
<button id="creditsBtn" class="absolute top-0 right-4 z-20 btn-toolbar-style">
|
|
925
|
-
<i class="fa-solid fa-users mr-2"></i>Credits
|
|
926
|
-
</button>
|
|
927
|
-
|
|
928
|
-
<div class="mx-auto my-8 p-4 sm:p-6 w-full max-w-3xl relative">
|
|
929
|
-
<p class="bg-card-dark text-white p-5 rounded-xl shadow-lg border border-brand-border text-sm sm:text-base text-center">
|
|
930
|
-
<strong>Welcome to 4SP Games!</strong><br>
|
|
931
|
-
This is a collection of games curated for the 4SP community. Use the search bar below to find your favorite.
|
|
932
|
-
</p>
|
|
933
|
-
</div>
|
|
934
|
-
|
|
935
|
-
<div id="favoritesSection">
|
|
936
|
-
<h2 id="favoritesHeader" class="text-center text-3xl font-bold mt-12 mb-6 text-white" style="display: none;">
|
|
937
|
-
<strong>Favorites</strong>
|
|
938
|
-
</h2>
|
|
939
|
-
<div class="games-grid grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-7 gap-4" id="favoritesGameList" style="display: none;"></div>
|
|
940
|
-
</div>
|
|
941
|
-
|
|
942
|
-
<div id="category-viewer" class="w-full mt-12">
|
|
943
|
-
<div id="category-slider-container" class="relative flex items-center justify-center max-w-sm mx-auto h-12">
|
|
944
|
-
<button id="prev-category" class="absolute left-0 top-1/2 -translate-y-1/2 text-2xl text-gray-500 hover:text-white transition-all p-2 z-10 disabled:opacity-30 disabled:text-gray-700 disabled:hover:text-gray-700">
|
|
945
|
-
<i class="fas fa-chevron-left"></i>
|
|
946
|
-
</button>
|
|
947
|
-
<div class="overflow-hidden w-64 text-center">
|
|
948
|
-
<div id="category-slider" class="flex transition-transform duration-500 ease-in-out">
|
|
949
|
-
</div>
|
|
950
|
-
</div>
|
|
951
|
-
<button id="next-category" class="absolute right-0 top-1/2 -translate-y-1/2 text-2xl text-gray-500 hover:text-white transition-all p-2 z-10 disabled:opacity-30 disabled:text-gray-700 disabled:hover:text-gray-700">
|
|
952
|
-
<i class="fas fa-chevron-right"></i>
|
|
953
|
-
</button>
|
|
954
|
-
</div>
|
|
955
|
-
|
|
956
|
-
<div id="games-grid-container" class="games-grid grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-7 gap-4 mt-6 transition-opacity duration-300 ease-in-out">
|
|
957
|
-
</div>
|
|
958
|
-
</div>
|
|
959
|
-
</main>
|
|
960
|
-
|
|
961
|
-
<div id="bottom-fixed-bar" class="fixed bottom-4 right-4 bg-black/80 backdrop-blur-xl rounded-3xl border border-brand-border shadow-2xl shadow-black/50 w-full max-w-md z-50 transition-all duration-300 flex flex-col">
|
|
962
|
-
<div id="searchResults" class="search-results-container w-full" style="display: none;">
|
|
963
|
-
</div>
|
|
964
|
-
|
|
965
|
-
<div id="searchDivider" class="w-full h-[1px] bg-white/10" style="display: none;"></div>
|
|
966
|
-
|
|
967
|
-
<div class="flex items-center w-full p-1">
|
|
968
|
-
<div class="pl-4 pr-2 text-gray-400">
|
|
969
|
-
<i class="fa-solid fa-magnifying-glass"></i>
|
|
970
|
-
</div>
|
|
971
|
-
<input type="text" id="searchInput" placeholder="Search for games…" autocomplete="off" class="w-full py-3 pr-4 bg-transparent text-white placeholder-gray-500 focus:outline-none focus:ring-0 text-base" />
|
|
972
|
-
</div>
|
|
973
|
-
</div>
|
|
974
|
-
|
|
975
|
-
<div id="zoneViewer" aria-modal="true" role="dialog">
|
|
976
|
-
<div class="zone-header">
|
|
977
|
-
<div class="zone-title">
|
|
978
|
-
<h2 id="zoneNameEl">Game Title</h2>
|
|
979
|
-
</div>
|
|
980
|
-
<div class="zone-controls">
|
|
981
|
-
<button id="fullscreenBtnZone" title="Fullscreen"><i class="fas fa-expand"></i></button>
|
|
982
|
-
<a id="downloadBtnZone" title="Download" class="hidden" href="#"><i class="fas fa-download"></i></a>
|
|
983
|
-
<button id="closeBtnZone" title="Close"><i class="fas fa-times"></i></button>
|
|
984
|
-
</div>
|
|
985
|
-
</div>
|
|
986
|
-
<iframe id="zoneFrame" title="Game Content" allowfullscreen sandbox="allow-scripts allow-same-origin allow-forms allow-pointer-lock"></iframe>
|
|
987
|
-
</div>
|
|
988
|
-
|
|
989
|
-
<div id="creditsModal" class="fixed top-0 left-0 w-full h-full bg-black/70 backdrop-blur-lg z-[6000] items-center justify-center p-4" style="display: none;">
|
|
990
|
-
<div class="bg-card-dark rounded-xl border border-brand-border shadow-2xl w-full max-w-lg overflow-y-auto max-h-[90vh]">
|
|
991
|
-
<div class="flex justify-between items-center p-4 border-b border-brand-border sticky top-0 bg-card-dark z-10">
|
|
992
|
-
<h3 class="text-xl font-bold">Project Credits</h3>
|
|
993
|
-
<button id="closeCreditsBtn" class="text-white hover:text-gray-400 p-2"><i class="fas fa-times"></i></button>
|
|
994
|
-
</div>
|
|
995
|
-
<div id="creditsContent" class="p-6 space-y-6">
|
|
996
|
-
</div>
|
|
997
|
-
</div>
|
|
998
|
-
</div>
|
|
999
|
-
|
|
1000
|
-
<div id="instruction-overlay" class="fixed inset-0 bg-black/80 z-[8000] transition-opacity duration-300 opacity-0 flex items-center justify-center p-4" style="display: none;">
|
|
1001
|
-
<div class="bg-card-dark text-white rounded-3xl border border-gray-700 shadow-2xl backdrop-blur-md w-full max-w-2xl max-h-[90vh] flex flex-col">
|
|
1002
|
-
<div class="p-6 sm:p-8 overflow-y-auto custom-scrollbar">
|
|
1003
|
-
<h3 class="text-2xl font-bold mb-4 text-center">Welcome to 4SP Games</h3>
|
|
1004
|
-
<p class="text-sm text-gray-400 mb-6 text-center">Please read the following information before playing.</p>
|
|
1005
|
-
|
|
1006
|
-
<ul class="space-y-4 text-gray-300 text-sm sm:text-base leading-relaxed">
|
|
1007
|
-
<li class="flex items-start">
|
|
1008
|
-
<i class="fas fa-compass mt-1 mr-3 text-accent-indigo"></i>
|
|
1009
|
-
<span><strong>Navigation:</strong> Use the arrows next to the category title (currently "<strong>StrongdogXP</strong>") to switch between different game collections like <strong>GN-Math</strong> and <strong>Others</strong>.</span>
|
|
1010
|
-
</li>
|
|
1011
|
-
<li class="flex items-start">
|
|
1012
|
-
<i class="fas fa-file-signature mt-1 mr-3 text-accent-indigo"></i>
|
|
1013
|
-
<span><strong>Rebranding:</strong> "4SP Game Hub" is now shortened to "<strong>4SP Games</strong>".</span>
|
|
1014
|
-
</li>
|
|
1015
|
-
<li class="flex items-start">
|
|
1016
|
-
<i class="fas fa-bug mt-1 mr-3 text-strongdog-orange"></i>
|
|
1017
|
-
<span><strong>Stability Disclaimer:</strong> Some games may be unstable, glitchy, or fail to load. This is often due to browser restrictions or the age of the game files.</span>
|
|
1018
|
-
</li>
|
|
1019
|
-
<li class="flex items-start">
|
|
1020
|
-
<i class="fas fa-user-gear mt-1 mr-3 text-gn-math"></i>
|
|
1021
|
-
<span><strong>Management:</strong> I do not personally manage or host the files for these game collections. I cannot directly fix broken game files or save data issues.</span>
|
|
1022
|
-
</li>
|
|
1023
|
-
<li class="flex items-start">
|
|
1024
|
-
<i class="fas fa-server mt-1 mr-3 text-gn-math"></i>
|
|
1025
|
-
<span><strong>GN-Math & CDN:</strong> The <strong>GN-Math</strong> category is particularly hard to modify because it uses a <em>CDN (Content Delivery Network)</em>. This means the game files are hosted on remote servers around the world to load faster, rather than being stored here. I cannot edit those files.</span>
|
|
1026
|
-
</li>
|
|
1027
|
-
</ul>
|
|
1028
|
-
</div>
|
|
1029
|
-
|
|
1030
|
-
<div class="p-6 border-t border-gray-800 bg-black/20 rounded-b-3xl flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
1031
|
-
<span class="text-xs text-gray-500">Type "I understand" to continue.</span>
|
|
1032
|
-
<input type="text" id="instruction-input" placeholder="Type 'I understand'" class="w-full sm:w-auto px-6 py-3 border border-transparent text-base font-normal rounded-xl text-white bg-purple-500/10 ring-1 ring-purple-500/50 focus:ring-purple-500/80 focus:bg-purple-500/20 focus:outline-none placeholder-gray-400 transition-all duration-200">
|
|
1033
|
-
</div>
|
|
1034
|
-
</div>
|
|
1035
|
-
</div>
|
|
1036
|
-
|
|
1037
|
-
<script>
|
|
1038
|
-
// --- Configuration & Globals ---
|
|
1039
|
-
|
|
1040
|
-
const GN_ZONES_URL = "https://cdn.jsdelivr.net/gh/gn-math/assets@main/zones.json";
|
|
1041
|
-
const GN_COVER_URL_BASE = "https://cdn.jsdelivr.net/gh/gn-math/covers@main";
|
|
1042
|
-
|
|
1043
|
-
// FIXED: Use raw.githack.com for HTML to prevent "showing code" issue
|
|
1044
|
-
const GN_HTML_URL_BASE = "https://raw.githack.com/gn-math/html/main";
|
|
1045
|
-
|
|
1046
|
-
const WORKER_ROOT = "https://dv-service-lfs.4simpleproblems.workers.dev/";
|
|
1047
|
-
const GAMES_BASE_URL = "https://dv-service-lfs.4simpleproblems.workers.dev/GAMES/";
|
|
1048
|
-
const CARDS_DATA_URL = "https://dv-service-lfs.4simpleproblems.workers.dev/GAMES/cards-data.js";
|
|
1049
|
-
|
|
1050
|
-
const FAVORITES_KEY = 'gameHubFavorites_v3';
|
|
1051
|
-
|
|
1052
|
-
let allGames = [];
|
|
1053
|
-
let searchableGames = [];
|
|
1054
|
-
let categories = [];
|
|
1055
|
-
let currentCategoryIndex = 0;
|
|
1056
|
-
const categoryColors = {
|
|
1057
|
-
'StrongdogXP': 'text-strongdog-orange',
|
|
1058
|
-
'GN-Math': 'text-gn-math',
|
|
1059
|
-
'Gameboy Games': 'text-gameboy',
|
|
1060
|
-
'Others': 'text-other-grey',
|
|
1061
|
-
};
|
|
1062
|
-
|
|
1063
|
-
const CREDITS_DATA = {
|
|
1064
|
-
'StrongdogXP': { credit: "Core game collection provided by Josh P.", githubUrl: "https://github.com/jman1593/" },
|
|
1065
|
-
'GN-Math': { credit: "Primary game source and infrastructure provided by the GN-Math Community.", githubUrl: "https://github.com/gn-math/" },
|
|
1066
|
-
'Gameboy Games': { credit: "Emulator and ROM files from various public sources." },
|
|
1067
|
-
'Others': { credit: "Various community developers and open-source projects (e.g., DOOM, Eaglercraft, Carnage3D, JS-DOS)." }
|
|
1068
|
-
};
|
|
1069
|
-
|
|
1070
|
-
const favoritesHeader = document.getElementById("favoritesHeader");
|
|
1071
|
-
const favoritesGameList = document.getElementById("favoritesGameList");
|
|
1072
|
-
const searchInput = document.getElementById("searchInput");
|
|
1073
|
-
const searchResults = document.getElementById("searchResults");
|
|
1074
|
-
const searchDivider = document.getElementById("searchDivider");
|
|
1075
|
-
const categorySlider = document.getElementById('category-slider');
|
|
1076
|
-
const prevCategoryBtn = document.getElementById('prev-category');
|
|
1077
|
-
const nextCategoryBtn = document.getElementById('next-category');
|
|
1078
|
-
const gamesGridContainer = document.getElementById('games-grid-container');
|
|
1079
|
-
|
|
1080
|
-
const creditsModal = document.getElementById('creditsModal');
|
|
1081
|
-
const creditsContent = document.getElementById('creditsContent');
|
|
1082
|
-
const creditsBtn = document.getElementById('creditsBtn');
|
|
1083
|
-
const closeCreditsBtn = document.getElementById('closeCreditsBtn');
|
|
1084
|
-
|
|
1085
|
-
const INSTRUCTION_KEY = '4sp-games-instruction-extended-seen';
|
|
1086
|
-
const instructionOverlay = document.getElementById('instruction-overlay');
|
|
1087
|
-
const instructionInput = document.getElementById('instruction-input');
|
|
1088
|
-
|
|
1089
|
-
// --- Intersection Observer for Image Virtualization ---
|
|
1090
|
-
const imageObserver = new IntersectionObserver((entries, observer) => {
|
|
1091
|
-
entries.forEach(entry => {
|
|
1092
|
-
const img = entry.target;
|
|
1093
|
-
if (entry.isIntersecting) {
|
|
1094
|
-
if (img.dataset.src) {
|
|
1095
|
-
img.src = img.dataset.src;
|
|
1096
|
-
}
|
|
1097
|
-
} else {
|
|
1098
|
-
if(img.src && !img.src.includes('placehold.co') && !img.dataset.src) {
|
|
1099
|
-
img.dataset.src = img.src;
|
|
1100
|
-
}
|
|
1101
|
-
if(img.dataset.src) {
|
|
1102
|
-
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
});
|
|
1106
|
-
}, {
|
|
1107
|
-
rootMargin: "300px 0px",
|
|
1108
|
-
threshold: 0.01
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
function observeImages(container) {
|
|
1112
|
-
const images = container.querySelectorAll('img[data-src]');
|
|
1113
|
-
images.forEach(img => imageObserver.observe(img));
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// --- URL Management ---
|
|
1117
|
-
function updateURL(category, gameId = null) {
|
|
1118
|
-
let hash = `#${encodeURIComponent(category.replace(/\s+/g, '-'))}`;
|
|
1119
|
-
if (gameId) hash += `?id=${gameId}`;
|
|
1120
|
-
try {
|
|
1121
|
-
history.replaceState(null, null, hash);
|
|
1122
|
-
} catch(e) {
|
|
1123
|
-
console.warn('History API not supported in this environment');
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
function parseURL() {
|
|
1128
|
-
const hash = window.location.hash.substring(1);
|
|
1129
|
-
if (!hash) return { category: null, gameId: null };
|
|
1130
|
-
const parts = hash.split('?');
|
|
1131
|
-
let rawCategory = parts[0];
|
|
1132
|
-
let category = decodeURIComponent(rawCategory).replace(/-/g, ' ');
|
|
1133
|
-
let gameId = null;
|
|
1134
|
-
if (parts[1]) {
|
|
1135
|
-
const params = new URLSearchParams(parts[1]);
|
|
1136
|
-
gameId = params.get('id');
|
|
1137
|
-
}
|
|
1138
|
-
return { category: rawCategory, gameId };
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
function setupInstructionOverlay() {
|
|
1142
|
-
if (localStorage.getItem(INSTRUCTION_KEY) !== 'seen') {
|
|
1143
|
-
instructionOverlay.style.display = 'flex';
|
|
1144
|
-
document.body.style.overflow = 'hidden';
|
|
1145
|
-
setTimeout(() => {
|
|
1146
|
-
instructionOverlay.classList.remove('opacity-0');
|
|
1147
|
-
instructionOverlay.classList.add('opacity-100');
|
|
1148
|
-
}, 10);
|
|
1149
|
-
instructionInput.addEventListener('input', (e) => {
|
|
1150
|
-
if (e.target.value.trim().toLowerCase() === 'i understand') {
|
|
1151
|
-
instructionOverlay.classList.remove('opacity-100');
|
|
1152
|
-
instructionOverlay.classList.add('opacity-0');
|
|
1153
|
-
setTimeout(() => {
|
|
1154
|
-
instructionOverlay.style.display = 'none';
|
|
1155
|
-
document.body.style.overflow = '';
|
|
1156
|
-
localStorage.setItem(INSTRUCTION_KEY, 'seen');
|
|
1157
|
-
}, 300);
|
|
1158
|
-
}
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// --- Favorites Management (Live Updates) ---
|
|
1164
|
-
const getFavorites = () => JSON.parse(localStorage.getItem(FAVORITES_KEY)) || [];
|
|
1165
|
-
const saveFavorites = (favs) => localStorage.setItem(FAVORITES_KEY, JSON.stringify(favs));
|
|
1166
|
-
|
|
1167
|
-
function toggleFavorite(gameId) {
|
|
1168
|
-
const strGameId = String(gameId);
|
|
1169
|
-
let favorites = getFavorites().map(String);
|
|
1170
|
-
const isFavorited = favorites.includes(strGameId);
|
|
1171
|
-
|
|
1172
|
-
if (isFavorited) {
|
|
1173
|
-
favorites = favorites.filter(id => id !== strGameId);
|
|
1174
|
-
saveFavorites(favorites);
|
|
1175
|
-
updateAllFavoriteButtons();
|
|
1176
|
-
const favCard = favoritesGameList.querySelector(`.zone-item[data-game-id='${strGameId}'], .other-zone-item[data-game-id='${strGameId}']`);
|
|
1177
|
-
if (favCard) {
|
|
1178
|
-
favCard.classList.add('fade-out');
|
|
1179
|
-
setTimeout(() => {
|
|
1180
|
-
favCard.remove();
|
|
1181
|
-
if (favoritesGameList.children.length === 0) {
|
|
1182
|
-
favoritesHeader.style.display = 'none';
|
|
1183
|
-
favoritesGameList.style.display = 'none';
|
|
1184
|
-
}
|
|
1185
|
-
}, 300);
|
|
1186
|
-
}
|
|
1187
|
-
} else {
|
|
1188
|
-
favorites.push(strGameId);
|
|
1189
|
-
saveFavorites(favorites);
|
|
1190
|
-
updateAllFavoriteButtons();
|
|
1191
|
-
let gameData = allGames.find(g => String(g.id) === strGameId);
|
|
1192
|
-
if (!gameData) {
|
|
1193
|
-
allGames.some(g => {
|
|
1194
|
-
if (g.versions) {
|
|
1195
|
-
const ver = g.versions.find(v => String(v.favoriteId) === strGameId);
|
|
1196
|
-
if (ver) {
|
|
1197
|
-
gameData = { ...g, id: ver.favoriteId, name: `${g.name} - ${ver.name}`, url: ver.url, versions: undefined };
|
|
1198
|
-
return true;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
return false;
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
1204
|
-
if (gameData) {
|
|
1205
|
-
favoritesHeader.style.display = "block";
|
|
1206
|
-
favoritesGameList.style.display = "grid";
|
|
1207
|
-
favoritesGameList.classList.add('grid', 'grid-cols-2', 'sm:grid-cols-3', 'md:grid-cols-5', 'lg:grid-cols-7', 'gap-4');
|
|
1208
|
-
const newCard = createGameCard(gameData, true);
|
|
1209
|
-
favoritesGameList.appendChild(newCard);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
function updateAllFavoriteButtons() {
|
|
1215
|
-
const favorites = getFavorites().map(String);
|
|
1216
|
-
document.querySelectorAll('.btn-card-action.fav-action').forEach(btn => {
|
|
1217
|
-
const card = btn.closest('.zone-item, .other-zone-item, .search-item');
|
|
1218
|
-
if (!card) return;
|
|
1219
|
-
const cardGameId = String(card.dataset.gameId);
|
|
1220
|
-
if (btn.classList.contains('version-favorite-btn')) {
|
|
1221
|
-
const isAnyVersionFavorited = favorites.some(favId => favId.startsWith(cardGameId + '_'));
|
|
1222
|
-
btn.classList.toggle('favorited', isAnyVersionFavorited);
|
|
1223
|
-
} else {
|
|
1224
|
-
const isFavorited = favorites.includes(cardGameId);
|
|
1225
|
-
btn.classList.toggle('favorited', isFavorited);
|
|
1226
|
-
btn.title = isFavorited ? 'Remove from Favorites' : 'Add to Favorites';
|
|
1227
|
-
}
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
// --- URL Helpers (Strongdog) ---
|
|
1232
|
-
// Modified to support injected URLs using WORKER_ROOT + directory logic
|
|
1233
|
-
function getAdjustedUrls(imgSrc, page) {
|
|
1234
|
-
if (page && page > 1) {
|
|
1235
|
-
// ../strongdog2/img/... -> exists outside GAMES dir
|
|
1236
|
-
return { adjustedImgSrc: `${WORKER_ROOT}strongdog${page}/img/${imgSrc}` };
|
|
1237
|
-
}
|
|
1238
|
-
// ./img -> assumed inside GAMES dir or wherever relative
|
|
1239
|
-
return { adjustedImgSrc: `${GAMES_BASE_URL}img/${imgSrc}` };
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
function sd_getBaseURLForPage(page) {
|
|
1243
|
-
// Updated to point to WORKER_ROOT for numbered pages, GAMES_BASE_URL for default
|
|
1244
|
-
if (page > 1) return `${WORKER_ROOT}strongdog${page}/`;
|
|
1245
|
-
return `${WORKER_ROOT}STRONGDOG/`; // Assumes default strongdog is at root/STRONGDOG? Or inside GAMES?
|
|
1246
|
-
// Re-reading logic: "stuff like ../DOOM/ will stay in GAMES directory but strongdog# directories exit"
|
|
1247
|
-
// Let's assume default STRONGDOG is adjacent to strongdog2 etc. at root level based on prior naming conventions
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
async function sd_getEmbedPath(adjustedHref, originalHref, page) {
|
|
1251
|
-
let cleanHref = adjustedHref.replace(/index\.html$/, "").replace(/base\.html$/, "").replace(/\.html$/, "");
|
|
1252
|
-
if (!cleanHref.endsWith("/")) cleanHref += "/";
|
|
1253
|
-
const pathsToTry = [cleanHref + "game/index.html", cleanHref + "game/base.html", cleanHref + "gamereal/index.html", cleanHref + "gamereal/base.html", cleanHref + "index.html", cleanHref + "base.html", ];
|
|
1254
|
-
try {
|
|
1255
|
-
const response = await fetch(adjustedHref);
|
|
1256
|
-
if (response.ok) {
|
|
1257
|
-
const text = await response.text();
|
|
1258
|
-
const match = text.match(/embedGame\((['"])(.*?)\1,\s*(['"])(.*?)\3\)/);
|
|
1259
|
-
if (match) {
|
|
1260
|
-
const resolvedPath = new URL(match[2], adjustedHref).href;
|
|
1261
|
-
if (await sd_fileExists(resolvedPath)) return resolvedPath;
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
} catch (error) {}
|
|
1265
|
-
for (const path of pathsToTry) {
|
|
1266
|
-
if (await sd_fileExists(path)) return path;
|
|
1267
|
-
}
|
|
1268
|
-
return adjustedHref;
|
|
1269
|
-
}
|
|
1270
|
-
async function sd_fileExists(url) {
|
|
1271
|
-
try { const response = await fetch(url, { method: "HEAD" }); return response.ok; } catch { return false; }
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
// --- URL Resolution Helper for "Others" ---
|
|
1275
|
-
function resolveGameUrl(relativePath) {
|
|
1276
|
-
// Logic:
|
|
1277
|
-
// If path starts with ../ -> Go to WORKER_ROOT + path (stripping ../)
|
|
1278
|
-
// If path starts with ../GAMES/ -> It will effectively be WORKER_ROOT/GAMES/ (which matches GAMES_BASE_URL)
|
|
1279
|
-
// This allows us to access root folders like ../GTA-JSDOS/ while keeping ../GAMES/sm64 intact.
|
|
1280
|
-
|
|
1281
|
-
if (relativePath.startsWith('../')) {
|
|
1282
|
-
const pathPart = relativePath.substring(3); // Remove '../'
|
|
1283
|
-
return `${WORKER_ROOT}${pathPart}`;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// If it doesn't start with ../, assume it's relative to GAMES base
|
|
1287
|
-
return `${GAMES_BASE_URL}${relativePath.replace(/^\.\//, '')}`;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
// --- Game Data (Others) ---
|
|
1291
|
-
// We keep the original relative paths here so the resolver logic above can handle them dynamically
|
|
1292
|
-
const othersGames = [
|
|
1293
|
-
{ id: "other-eaglercraft", name: "Eaglercraft", imgSrc: "./images/eaglercraft.png", description: "The classic survival and building game with authentic block-based physics and multiplayer support, accessible directly in the browser.", category: "Others", versions: [ { name: "Release 1.12.2", url: "../EAGLERCRAFT/eaglercraft-1.12.2.html", favoriteId: "other-eaglercraft_1.12.2" }, { name: "Release 1.12.2 WASM", url: "../EAGLERCRAFT/eaglercraft-1.12.2-wasm.html", favoriteId: "other-eaglercraft_wasm", wasm: true } ] },
|
|
1294
|
-
{ id: "other-gta", name: "Grand Theft Auto", imgSrc: "./images/gta.png", description: "The original top-down classic. Cause chaos, complete missions, or just go for a drive in three iconic cities.", category: "Others", versions: [ { name: "Carnage3D", url: "../CARNAGE3D/web/carnage3D.html", favoriteId: "other-gta_carnage3d" }, { name: "Grand Theft Auto (JS-DOS)", url: "../GTA-JSDOS/index.html", favoriteId: "other-gta_jsdos" } ] },
|
|
1295
|
-
{ id: "other-simcity", name: "Sim City", imgSrc: "./images/simcity2000.png", description: "The original city-building classics. Design, build, and manage the city of your dreams in either the original Sim City or the detailed Sim City 2000.", category: "Others", versions: [ { name: "Sim City", url: "../SIM-CITY/index.html", favoriteId: "other-simcity_1" }, { name: "Sim City 2000", url: "../SIM-CITY-2/index.html", favoriteId: "other-simcity_2000" } ] },
|
|
1296
|
-
{ id: "other-doom", name: "DOOM", imgSrc: "./images/doom.png", description: "The legendary first-person shooter that pioneered the genre, focusing on blasting through hordes of demons in a timeless, adrenaline-fueled classic.", category: "Others", url: "../DOOM/index.html" },
|
|
1297
|
-
{ id: "other-sm64", name: "Super Mario 64", imgSrc: "./images/sm64.png", description: "The iconic 3D platformer that revolutionized gaming. Run, jump, and triple-jump your way through vast worlds to collect stars and save the princess.", category: "Others", url: "../GAMES/sm64/index.html" }
|
|
1298
|
-
];
|
|
1299
|
-
|
|
1300
|
-
// --- Card Creation ---
|
|
1301
|
-
|
|
1302
|
-
function createOthersGameCard(game) {
|
|
1303
|
-
const favorites = getFavorites().map(String);
|
|
1304
|
-
const isAnyFavorite = game.versions ? favorites.some(favId => game.versions.some(v => String(v.favoriteId) === favId)) : favorites.includes(String(game.id));
|
|
1305
|
-
const card = document.createElement("div");
|
|
1306
|
-
card.className = 'other-zone-item bg-card-dark rounded-2xl border border-brand-border col-span-full shadow-lg';
|
|
1307
|
-
card.dataset.gameId = game.id;
|
|
1308
|
-
|
|
1309
|
-
// Resolve Image URL
|
|
1310
|
-
const resolvedImgSrc = resolveGameUrl(game.imgSrc);
|
|
1311
|
-
|
|
1312
|
-
let gameButtonsHtml = '';
|
|
1313
|
-
let favoriteButtonHtml = '';
|
|
1314
|
-
|
|
1315
|
-
if (game.versions && game.versions.length > 0) {
|
|
1316
|
-
const availableVersions = game.versions;
|
|
1317
|
-
if (availableVersions.length === 1) {
|
|
1318
|
-
const vUrl = resolveGameUrl(availableVersions[0].url);
|
|
1319
|
-
gameButtonsHtml = `<button class="btn-card-action play-action" data-url="${vUrl}" data-version-name="${availableVersions[0].name}" title="Play Game"><i class="fa-solid fa-play transition-colors"></i></button>`;
|
|
1320
|
-
} else {
|
|
1321
|
-
const dropdownLinks = availableVersions.map(v => {
|
|
1322
|
-
const vUrl = resolveGameUrl(v.url);
|
|
1323
|
-
return `<a href="#" class="eagler-dropdown-link" data-url="${vUrl}" data-version-name="${v.name}">${v.name}</a>`
|
|
1324
|
-
}).join('');
|
|
1325
|
-
gameButtonsHtml = `<div class="relative"><button class="btn-card-action version-btn" title="Select Version"><i class="fa-solid fa-chevron-up transition-colors"></i></button><div class="eagler-dropdown">${dropdownLinks}</div></div>`;
|
|
1326
|
-
}
|
|
1327
|
-
favoriteButtonHtml = `<div class="relative"><button class="btn-card-action fav-action version-favorite-btn ${isAnyFavorite ? 'favorited' : ''}" title="Favorite a version"><i class="fa-solid fa-star fa-solid-star transition-colors"></i><i class="fa-regular fa-star fa-regular-star transition-colors"></i></button><div class="eagler-dropdown">${game.versions.map(v => `<a href="#" class="eagler-dropdown-link favorite-link" data-favorite-id="${v.favoriteId}">${v.name}</a>`).join('')}</div></div>`;
|
|
1328
|
-
} else {
|
|
1329
|
-
const gUrl = resolveGameUrl(game.url);
|
|
1330
|
-
gameButtonsHtml = `<button class="btn-card-action play-action" data-url="${gUrl}" title="Play Game"><i class="fa-solid fa-play transition-colors"></i></button>`;
|
|
1331
|
-
favoriteButtonHtml = `<button class="btn-card-action fav-action ${isAnyFavorite ? 'favorited' : ''}" title="${isAnyFavorite ? 'Remove from Favorites' : 'Add to Favorites'}"><i class="fa-solid fa-star fa-solid-star transition-colors"></i><i class="fa-regular fa-star fa-regular-star transition-colors"></i></button>`;
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
card.innerHTML = `
|
|
1335
|
-
<div class="relative w-full h-full cursor-pointer group" style="min-height: 250px;">
|
|
1336
|
-
<img data-src="${resolvedImgSrc}" alt="${game.name}" loading="lazy" decoding="async" class="w-full h-full object-cover absolute inset-0 rounded-2xl">
|
|
1337
|
-
<div class="image-overlay absolute inset-0 transition-colors duration-200"></div>
|
|
1338
|
-
<div class="absolute inset-0 p-4 sm:p-6 flex flex-col justify-between rounded-2xl">
|
|
1339
|
-
<div class="flex items-start justify-between w-full">
|
|
1340
|
-
<h3 class="text-4xl font-bold text-white truncate drop-shadow-lg" style="max-width: 80%;" title="${game.name}">${game.name}</h3>
|
|
1341
|
-
<div class="flex items-center space-x-3 bg-black/80 backdrop-blur-md rounded-xl px-2 py-1">
|
|
1342
|
-
${gameButtonsHtml}
|
|
1343
|
-
${favoriteButtonHtml}
|
|
1344
|
-
</div>
|
|
1345
|
-
</div>
|
|
1346
|
-
<p class="text-lg text-white font-medium drop-shadow-lg" style="max-width: 50%;">${game.description}</p>
|
|
1347
|
-
</div>
|
|
1348
|
-
</div>
|
|
1349
|
-
`;
|
|
1350
|
-
|
|
1351
|
-
if (!game.versions) {
|
|
1352
|
-
card.querySelector('.group').addEventListener('click', () => {
|
|
1353
|
-
const gUrl = resolveGameUrl(game.url);
|
|
1354
|
-
openZone({...game, url: gUrl});
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
card.querySelectorAll('.play-action').forEach(btn => btn.addEventListener('click', (e) => { e.stopPropagation(); openZone({ ...game, name: btn.dataset.versionName ? `${game.name} - ${btn.dataset.versionName}` : game.name, url: btn.dataset.url }); }));
|
|
1358
|
-
|
|
1359
|
-
const versionBtn = card.querySelector('.version-btn');
|
|
1360
|
-
if (versionBtn) { versionBtn.addEventListener('click', (e) => { e.stopPropagation(); document.querySelectorAll('.eagler-dropdown.show').forEach(d => d.classList.remove('show')); versionBtn.nextElementSibling.classList.toggle('show'); }); }
|
|
1361
|
-
|
|
1362
|
-
card.querySelectorAll('.eagler-dropdown-link:not(.favorite-link)').forEach(link => link.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openZone({ ...game, name: link.dataset.versionName ? `${game.name} - ${link.dataset.versionName}` : game.name, url: link.dataset.url }); link.closest('.eagler-dropdown').classList.remove('show'); }));
|
|
1363
|
-
|
|
1364
|
-
const favBtn = card.querySelector('.btn-card-action.fav-action');
|
|
1365
|
-
if (favBtn.classList.contains('version-favorite-btn')) {
|
|
1366
|
-
favBtn.addEventListener('click', (e) => { e.stopPropagation(); document.querySelectorAll('.eagler-dropdown.show').forEach(d => d.classList.remove('show')); favBtn.nextElementSibling.classList.toggle('show'); });
|
|
1367
|
-
card.querySelectorAll('.favorite-link').forEach(link => link.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleFavorite(link.dataset.favoriteId); link.closest('.eagler-dropdown').classList.remove('show'); }));
|
|
1368
|
-
} else {
|
|
1369
|
-
favBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleFavorite(game.id); });
|
|
1370
|
-
}
|
|
1371
|
-
imageObserver.observe(card.querySelector('img'));
|
|
1372
|
-
return card;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
function createGameCard(game, forceSmallCard = false) {
|
|
1376
|
-
if (game.category === 'Others' && !forceSmallCard) return createOthersGameCard(game);
|
|
1377
|
-
const isStrongdog = game.category === 'StrongdogXP';
|
|
1378
|
-
|
|
1379
|
-
// Resolve Image Src
|
|
1380
|
-
let imgSrc = game.imgSrc;
|
|
1381
|
-
if (isStrongdog) {
|
|
1382
|
-
imgSrc = getAdjustedUrls(game.imgSrc, game.page).adjustedImgSrc;
|
|
1383
|
-
} else if (game.category === 'Others' || game.category === 'Gameboy Games') {
|
|
1384
|
-
imgSrc = resolveGameUrl(game.imgSrc);
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
const isFavorite = getFavorites().map(String).includes(String(game.id));
|
|
1388
|
-
|
|
1389
|
-
const card = document.createElement("div");
|
|
1390
|
-
card.className = 'zone-item bg-card-dark rounded-2xl border border-brand-border overflow-hidden';
|
|
1391
|
-
card.dataset.gameId = game.id;
|
|
1392
|
-
card.innerHTML = `
|
|
1393
|
-
<div class="relative w-full cursor-pointer group">
|
|
1394
|
-
<div class="aspect-w-3 aspect-h-2"><img data-src="${imgSrc}" alt="${game.name}" class="w-full h-full object-cover"></div>
|
|
1395
|
-
<h3 class="absolute top-2 right-2 z-10 max-w-[80%] bg-black/60 backdrop-blur-md rounded-xl px-3 py-1.5 text-white truncate text-sm font-semibold shadow-lg" title="${game.name}">${game.name}</h3>
|
|
1396
|
-
<div class="absolute bottom-2 right-2 bg-black/80 backdrop-blur-md rounded-xl px-2 py-1 flex items-center space-x-2 shadow-lg">
|
|
1397
|
-
<button class="btn-card-action play-action" title="Play Game"><i class="fa-solid fa-play transition-colors"></i></button>
|
|
1398
|
-
<button class="btn-card-action fav-action ${isFavorite ? 'favorited' : ''}" title="${isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}"><i class="fa-solid fa-star fa-solid-star transition-colors"></i><i class="fa-regular fa-star fa-regular-star transition-colors"></i></button>
|
|
1399
|
-
</div>
|
|
1400
|
-
</div>
|
|
1401
|
-
`;
|
|
1402
|
-
|
|
1403
|
-
const handlePlay = async (e) => {
|
|
1404
|
-
e.stopPropagation();
|
|
1405
|
-
if (isStrongdog) {
|
|
1406
|
-
const baseURL = sd_getBaseURLForPage(game.page);
|
|
1407
|
-
let adjustedHref = baseURL.endsWith("/") ? baseURL + game.href.replace(/^\.\//, "") : baseURL + "/" + game.href.replace(/^\.\//, "");
|
|
1408
|
-
const embedPath = await sd_getEmbedPath(adjustedHref, game.href, game.page);
|
|
1409
|
-
openZone({ name: game.name, url: embedPath, category: game.category, id: game.id });
|
|
1410
|
-
} else {
|
|
1411
|
-
// For GN-Math, url is absolute (handled in init). For Others/Gameboy, we might need to resolve
|
|
1412
|
-
let gUrl = game.url;
|
|
1413
|
-
if (game.category === 'Others' || game.category === 'Gameboy Games') {
|
|
1414
|
-
gUrl = resolveGameUrl(game.url);
|
|
1415
|
-
}
|
|
1416
|
-
openZone({ ...game, url: gUrl });
|
|
1417
|
-
}
|
|
1418
|
-
};
|
|
1419
|
-
|
|
1420
|
-
card.querySelector('.group').addEventListener('click', handlePlay);
|
|
1421
|
-
card.querySelector('.play-action').addEventListener('click', handlePlay);
|
|
1422
|
-
card.querySelector('.fav-action').addEventListener('click', (e) => { e.stopPropagation(); toggleFavorite(game.id); });
|
|
1423
|
-
imageObserver.observe(card.querySelector('img'));
|
|
1424
|
-
return card;
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
// --- Game Viewer Logic ---
|
|
1428
|
-
function openZone(game) {
|
|
1429
|
-
if (!game || !game.url) return;
|
|
1430
|
-
updateURL(categories[currentCategoryIndex], game.id || null);
|
|
1431
|
-
|
|
1432
|
-
const zoneViewer = document.getElementById('zoneViewer');
|
|
1433
|
-
const zoneFrame = document.getElementById('zoneFrame');
|
|
1434
|
-
const zoneNameEl = document.getElementById('zoneNameEl');
|
|
1435
|
-
const downloadBtn = document.getElementById('downloadBtnZone');
|
|
1436
|
-
|
|
1437
|
-
// Setup Download Button (Specific to Eaglercraft)
|
|
1438
|
-
if (game.baseGameId === 'other-eaglercraft' || game.id === 'other-eaglercraft') {
|
|
1439
|
-
downloadBtn.href = game.url;
|
|
1440
|
-
downloadBtn.download = game.name.replace(/ /g, '_') + '.html';
|
|
1441
|
-
downloadBtn.removeAttribute('target');
|
|
1442
|
-
downloadBtn.classList.remove('hidden');
|
|
1443
|
-
} else {
|
|
1444
|
-
downloadBtn.classList.add('hidden');
|
|
1445
|
-
downloadBtn.href = '#';
|
|
1446
|
-
downloadBtn.removeAttribute('download');
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
zoneNameEl.textContent = game.name;
|
|
1450
|
-
|
|
1451
|
-
// RESET FRAME BEFORE LOAD
|
|
1452
|
-
zoneFrame.src = 'about:blank';
|
|
1453
|
-
|
|
1454
|
-
// SECURITY: Set sandbox permissions
|
|
1455
|
-
const sandboxRules = 'allow-scripts allow-same-origin allow-forms allow-pointer-lock';
|
|
1456
|
-
zoneFrame.setAttribute('sandbox', sandboxRules);
|
|
1457
|
-
zoneFrame.setAttribute('allow', 'fullscreen; pointer-lock; autoplay; clipboard-write');
|
|
1458
|
-
|
|
1459
|
-
// --- REVISED LOGIC FOR LOADING GAMES ---
|
|
1460
|
-
// With raw.githack.com, we can treat GN-Math games as standard URL games again
|
|
1461
|
-
// because Githack serves correct Content-Type: text/html headers.
|
|
1462
|
-
const isStandardURLGame = game.category === 'StrongdogXP' || game.category === 'Others' || game.category === 'GN-Math';
|
|
1463
|
-
|
|
1464
|
-
if (isStandardURLGame) {
|
|
1465
|
-
// Load URL directly for standard games
|
|
1466
|
-
zoneFrame.src = game.url;
|
|
1467
|
-
zoneViewer.style.display = "flex";
|
|
1468
|
-
} else {
|
|
1469
|
-
// Fallback for other potential categories (same logic as before: fetch & write)
|
|
1470
|
-
fetch(`${game.url}?t=${Date.now()}`)
|
|
1471
|
-
.then(response => {
|
|
1472
|
-
if (!response.ok) throw new Error(`HTTP error ${response.status}`);
|
|
1473
|
-
return response.text();
|
|
1474
|
-
})
|
|
1475
|
-
.then(html => {
|
|
1476
|
-
const doc = zoneFrame.contentWindow.document;
|
|
1477
|
-
doc.open();
|
|
1478
|
-
doc.write(html);
|
|
1479
|
-
doc.close();
|
|
1480
|
-
zoneViewer.style.display = "flex";
|
|
1481
|
-
})
|
|
1482
|
-
.catch(error => {
|
|
1483
|
-
console.error(`Failed to load game "${game.name}": ${error.message}`);
|
|
1484
|
-
zoneFrame.src = game.url;
|
|
1485
|
-
zoneViewer.style.display = "flex";
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
function closeZoneViewer() {
|
|
1491
|
-
updateURL(categories[currentCategoryIndex]);
|
|
1492
|
-
const downloadBtn = document.getElementById('downloadBtnZone');
|
|
1493
|
-
downloadBtn.classList.add('hidden');
|
|
1494
|
-
downloadBtn.href = '#';
|
|
1495
|
-
downloadBtn.removeAttribute('download');
|
|
1496
|
-
downloadBtn.removeAttribute('target');
|
|
1497
|
-
const zoneViewer = document.getElementById('zoneViewer');
|
|
1498
|
-
const zoneFrame = document.getElementById('zoneFrame');
|
|
1499
|
-
zoneViewer.style.display = "none";
|
|
1500
|
-
if (zoneFrame) {
|
|
1501
|
-
zoneFrame.src = 'about:blank';
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
// --- Render Lists ---
|
|
1506
|
-
function renderGames(gamesToRender, targetElement, forceSmallCard = false) {
|
|
1507
|
-
targetElement.innerHTML = '';
|
|
1508
|
-
targetElement.className = (categories[currentCategoryIndex] === 'Others' && !forceSmallCard) ? 'grid grid-cols-1 gap-6 mt-6' : 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-7 gap-4 mt-6';
|
|
1509
|
-
gamesToRender.forEach(game => targetElement.appendChild(createGameCard(game, forceSmallCard)));
|
|
1510
|
-
observeImages(targetElement);
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
function renderFavorites() {
|
|
1514
|
-
const favoriteIds = getFavorites().map(String);
|
|
1515
|
-
if (favoriteIds.length > 0) {
|
|
1516
|
-
favoritesHeader.style.display = "block";
|
|
1517
|
-
favoritesGameList.style.display = "grid";
|
|
1518
|
-
const favoriteGames = [];
|
|
1519
|
-
allGames.forEach(game => {
|
|
1520
|
-
if (favoriteIds.includes(String(game.id)) && !game.versions) { favoriteGames.push(game); }
|
|
1521
|
-
else if (game.versions) {
|
|
1522
|
-
game.versions.forEach(v => {
|
|
1523
|
-
if (favoriteIds.includes(String(v.favoriteId))) {
|
|
1524
|
-
// Need to resolve URL here for favorites too!
|
|
1525
|
-
let vUrl = v.url;
|
|
1526
|
-
if (game.category === 'Others' || game.category === 'Gameboy Games') {
|
|
1527
|
-
vUrl = resolveGameUrl(v.url);
|
|
1528
|
-
} else if (game.category === 'StrongdogXP') {
|
|
1529
|
-
// Strongdog favorites usually just rely on the ID to regenerate logic, but for safety in this object:
|
|
1530
|
-
// Logic remains handled inside createGameCard via ID lookup generally, but let's leave as is
|
|
1531
|
-
// because strongdog logic inside createGameCard regenerates paths based on ID/Page props anyway.
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
favoriteGames.push({ ...game, id: v.favoriteId, name: `${game.name} - ${v.name}`, url: vUrl, versions: undefined });
|
|
1535
|
-
}
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
});
|
|
1539
|
-
renderGames(favoriteGames.sort((a, b) => a.name.localeCompare(b.name)), favoritesGameList, true);
|
|
1540
|
-
} else {
|
|
1541
|
-
favoritesHeader.style.display = "none";
|
|
1542
|
-
favoritesGameList.style.display = "none";
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
// --- Category Nav ---
|
|
1547
|
-
function updateArrowVisibility() {
|
|
1548
|
-
prevCategoryBtn.disabled = currentCategoryIndex === 0;
|
|
1549
|
-
nextCategoryBtn.disabled = currentCategoryIndex === categories.length - 1;
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
function switchCategory(newIndex) {
|
|
1553
|
-
currentCategoryIndex = newIndex;
|
|
1554
|
-
categorySlider.style.transform = `translateX(-${currentCategoryIndex * (100 / categories.length)}%)`;
|
|
1555
|
-
renderGamesForCurrentCategory();
|
|
1556
|
-
updateArrowVisibility();
|
|
1557
|
-
updateURL(categories[currentCategoryIndex]);
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
async function renderGamesForCurrentCategory() {
|
|
1561
|
-
const categoryName = categories[currentCategoryIndex];
|
|
1562
|
-
gamesGridContainer.classList.add('opacity-0');
|
|
1563
|
-
await new Promise(r => setTimeout(r, 150));
|
|
1564
|
-
renderGames(allGames.filter(g => g.category === categoryName), gamesGridContainer, false);
|
|
1565
|
-
gamesGridContainer.classList.remove('opacity-0');
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
// --- Search ---
|
|
1569
|
-
function debounce(func, delay) {
|
|
1570
|
-
let timeout;
|
|
1571
|
-
return function(...args) {
|
|
1572
|
-
clearTimeout(timeout);
|
|
1573
|
-
timeout = setTimeout(() => func.apply(this, args), delay);
|
|
1574
|
-
};
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
const handleSearch = debounce(() => {
|
|
1578
|
-
const text = searchInput.value.toLowerCase().trim();
|
|
1579
|
-
const filteredDropdown = text ? searchableGames.filter(g => g.name.toLowerCase().includes(text)).slice(0, 5) : [];
|
|
1580
|
-
|
|
1581
|
-
searchResults.innerHTML = "";
|
|
1582
|
-
|
|
1583
|
-
if (filteredDropdown.length === 0 || !text) {
|
|
1584
|
-
searchResults.style.display = "none";
|
|
1585
|
-
searchDivider.style.display = "none";
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
const favorites = getFavorites().map(String);
|
|
1590
|
-
filteredDropdown.forEach(game => {
|
|
1591
|
-
const isStrongdog = game.category === 'StrongdogXP';
|
|
1592
|
-
let imgSrc;
|
|
1593
|
-
let finalGameData = { ...game }; // clone
|
|
1594
|
-
|
|
1595
|
-
if (isStrongdog) {
|
|
1596
|
-
const { adjustedImgSrc } = getAdjustedUrls(game.imgSrc, game.page);
|
|
1597
|
-
imgSrc = adjustedImgSrc;
|
|
1598
|
-
} else if (game.category === 'Others' || game.category === 'Gameboy Games') {
|
|
1599
|
-
imgSrc = resolveGameUrl(game.imgSrc);
|
|
1600
|
-
finalGameData.url = resolveGameUrl(game.url);
|
|
1601
|
-
} else {
|
|
1602
|
-
imgSrc = game.imgSrc;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
const linkEl = document.createElement("a");
|
|
1606
|
-
linkEl.href = '#';
|
|
1607
|
-
linkEl.className = 'search-item flex justify-between items-center';
|
|
1608
|
-
linkEl.dataset.gameId = game.id;
|
|
1609
|
-
let isFavorite = favorites.includes(String(game.id));
|
|
1610
|
-
|
|
1611
|
-
linkEl.innerHTML = `
|
|
1612
|
-
<div class="flex items-center truncate min-w-0">
|
|
1613
|
-
<img src="${imgSrc}" alt="${game.name}" class="w-10 h-10 rounded-lg object-cover flex-shrink-0">
|
|
1614
|
-
<span class="ml-3 font-medium truncate text-gray-200" title="${game.name}">${game.name}</span>
|
|
1615
|
-
</div>
|
|
1616
|
-
<div class="flex items-center flex-shrink-0">
|
|
1617
|
-
<div class="bg-black/80 backdrop-blur-md rounded-xl px-2 py-1 flex items-center space-x-2">
|
|
1618
|
-
<button class="btn-card-action play-action" title="Play Game">
|
|
1619
|
-
<i class="fa-solid fa-play transition-colors"></i>
|
|
1620
|
-
</button>
|
|
1621
|
-
<button class="btn-card-action fav-action ${isFavorite ? 'favorited' : ''}" title="${isFavorite ? 'Remove from Favorites' : 'Add to Favorites'}">
|
|
1622
|
-
<i class="fa-solid fa-star fa-solid-star transition-colors"></i>
|
|
1623
|
-
<i class="fa-regular fa-star fa-regular-star transition-colors"></i>
|
|
1624
|
-
</button>
|
|
1625
|
-
</div>
|
|
1626
|
-
</div>
|
|
1627
|
-
`;
|
|
1628
|
-
|
|
1629
|
-
const favBtn = linkEl.querySelector('.fav-action');
|
|
1630
|
-
favBtn.addEventListener('click', (e) => {
|
|
1631
|
-
e.preventDefault(); e.stopPropagation();
|
|
1632
|
-
toggleFavorite(game.id);
|
|
1633
|
-
});
|
|
1634
|
-
|
|
1635
|
-
linkEl.addEventListener('click', async (e) => {
|
|
1636
|
-
e.preventDefault();
|
|
1637
|
-
if (isStrongdog) {
|
|
1638
|
-
const baseURL = sd_getBaseURLForPage(game.page);
|
|
1639
|
-
let adjustedHref = baseURL.endsWith("/") ? baseURL + game.href.replace(/^\.\//, "") : baseURL + "/" + game.href.replace(/^\.\//, "");
|
|
1640
|
-
const embedPath = await sd_getEmbedPath(adjustedHref, game.href, game.page);
|
|
1641
|
-
const strongdogGameData = { name: game.name, url: embedPath, category: game.category, id: game.id };
|
|
1642
|
-
openZone(strongdogGameData);
|
|
1643
|
-
} else {
|
|
1644
|
-
openZone(finalGameData);
|
|
1645
|
-
}
|
|
1646
|
-
searchResults.style.display = "none";
|
|
1647
|
-
searchDivider.style.display = "none";
|
|
1648
|
-
});
|
|
1649
|
-
searchResults.appendChild(linkEl);
|
|
1650
|
-
});
|
|
1651
|
-
searchResults.style.display = "block";
|
|
1652
|
-
searchDivider.style.display = "block";
|
|
1653
|
-
}, 250);
|
|
1654
|
-
|
|
1655
|
-
// --- FETCHING DATA MANUALLY (NO MODULES) ---
|
|
1656
|
-
// This function fetches the JS file as text, transforms it to set a global variable, and evals it.
|
|
1657
|
-
// This bypasses module loading restrictions on file:// protocols.
|
|
1658
|
-
async function fetchGameData() {
|
|
1659
|
-
try {
|
|
1660
|
-
// Fetch the file
|
|
1661
|
-
const response = await fetch(CARDS_DATA_URL);
|
|
1662
|
-
if (!response.ok) throw new Error('Failed to fetch game data');
|
|
1663
|
-
let scriptText = await response.text();
|
|
1664
|
-
|
|
1665
|
-
// Transform 'export default' to 'window.loadedGameData ='
|
|
1666
|
-
// This assumes the file structure is simple "export default [...]"
|
|
1667
|
-
if (scriptText.includes('export default')) {
|
|
1668
|
-
scriptText = scriptText.replace('export default', 'window.loadedGameData =');
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
// Execute the script
|
|
1672
|
-
// We use new Function instead of eval for slight isolation, though still unsafe for untrusted code
|
|
1673
|
-
// (But we trust this source).
|
|
1674
|
-
// However, new Function creates a local scope. We need global assignment.
|
|
1675
|
-
// Standard eval or appending a script tag is better.
|
|
1676
|
-
const scriptEl = document.createElement('script');
|
|
1677
|
-
scriptEl.textContent = scriptText;
|
|
1678
|
-
document.body.appendChild(scriptEl);
|
|
1679
|
-
|
|
1680
|
-
// Wait a tick for execution
|
|
1681
|
-
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1682
|
-
|
|
1683
|
-
if (window.loadedGameData) {
|
|
1684
|
-
return window.loadedGameData;
|
|
1685
|
-
} else {
|
|
1686
|
-
console.warn("Script loaded but window.loadedGameData is undefined");
|
|
1687
|
-
return [];
|
|
1688
|
-
}
|
|
1689
|
-
} catch (e) {
|
|
1690
|
-
console.error("Error manual loading games:", e);
|
|
1691
|
-
return [];
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
// --- Init ---
|
|
1696
|
-
async function initializeApp() {
|
|
1697
|
-
// Load Game Data Manually
|
|
1698
|
-
let gamesData = [];
|
|
1699
|
-
try {
|
|
1700
|
-
gamesData = await fetchGameData();
|
|
1701
|
-
} catch (e) {
|
|
1702
|
-
console.error("Could not load external games data", e);
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
const strongdogGames = gamesData.map(g => ({ ...g, category: 'StrongdogXP' }));
|
|
1706
|
-
|
|
1707
|
-
try {
|
|
1708
|
-
const res = await fetch(GN_ZONES_URL);
|
|
1709
|
-
const gnMathRaw = await res.json();
|
|
1710
|
-
const gnMathGames = gnMathRaw.map(g => ({ id: `gn-${g.id}`, name: g.name, imgSrc: g.cover.replace("{COVER_URL}", GN_COVER_URL_BASE), category: 'GN-Math', url: g.url.replace("{HTML_URL}", GN_HTML_URL_BASE) }));
|
|
1711
|
-
allGames = [...strongdogGames, ...gnMathGames, ...othersGames];
|
|
1712
|
-
} catch (error) {
|
|
1713
|
-
console.warn("Failed to load GN-Math, falling back to others.", error);
|
|
1714
|
-
allGames = [...strongdogGames, ...othersGames];
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
allGames = allGames.filter(g => !['bitlife', 'soundboard'].some(term => g.name.toLowerCase().includes(term)));
|
|
1718
|
-
|
|
1719
|
-
searchableGames = [];
|
|
1720
|
-
allGames.forEach(game => {
|
|
1721
|
-
if (game.versions && game.versions.length > 0) {
|
|
1722
|
-
game.versions.forEach(version => {
|
|
1723
|
-
searchableGames.push({
|
|
1724
|
-
...game,
|
|
1725
|
-
id: version.favoriteId,
|
|
1726
|
-
name: `${game.name} - ${version.name}`,
|
|
1727
|
-
url: version.url,
|
|
1728
|
-
wasm: version.wasm,
|
|
1729
|
-
versions: undefined,
|
|
1730
|
-
baseGameId: game.id
|
|
1731
|
-
});
|
|
1732
|
-
});
|
|
1733
|
-
} else {
|
|
1734
|
-
searchableGames.push(game);
|
|
1735
|
-
}
|
|
1736
|
-
});
|
|
1737
|
-
|
|
1738
|
-
categories = ['StrongdogXP', 'GN-Math', 'Gameboy Games', 'Others'].filter(cat => allGames.some(g => g.category === cat));
|
|
1739
|
-
|
|
1740
|
-
const { category: urlCat } = parseURL();
|
|
1741
|
-
if (urlCat) currentCategoryIndex = Math.max(0, categories.findIndex(c => c.replace(/\s+/g, '-').toLowerCase() === urlCat.toLowerCase()));
|
|
1742
|
-
|
|
1743
|
-
categorySlider.style.width = `${categories.length * 100}%`;
|
|
1744
|
-
categorySlider.innerHTML = categories.map(cat => `<div class="w-64 flex-shrink-0"><h2 class="text-3xl font-bold ${categoryColors[cat] || 'text-white'}"><strong>${cat}</strong></h2></div>`).join('');
|
|
830
|
+
<label class="block text-gray-400 text-sm font-light flex-grow">Redirect URL</label>
|
|
831
|
+
</div>
|
|
1745
832
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
833
|
+
<div class="flex items-center gap-4 mb-3">
|
|
834
|
+
<input type="text" id="panicKey1" data-key-id="1" class="input-key-style panic-key-input" placeholder="-" maxlength="1">
|
|
835
|
+
<input type="url" id="panicUrl1" class="input-text-style" placeholder="e.g., https://google.com">
|
|
836
|
+
</div>
|
|
837
|
+
|
|
838
|
+
<div class="flex items-center gap-4 mb-3">
|
|
839
|
+
<input type="text" id="panicKey2" data-key-id="2" class="input-key-style panic-key-input" placeholder="-" maxlength="1">
|
|
840
|
+
<input type="url" id="panicUrl2" class="input-text-style" placeholder="e.g., https://youtube.com/feed/subscriptions">
|
|
841
|
+
</div>
|
|
842
|
+
|
|
843
|
+
<div class="flex items-center gap-4 mb-3">
|
|
844
|
+
<input type="text" id="panicKey3" data-key-id="3" class="input-key-style panic-key-input" placeholder="-" maxlength="1">
|
|
845
|
+
<input type="url" id="panicUrl3" class="input-text-style" placeholder="e.g., https://wikipedia.org">
|
|
846
|
+
</div>
|
|
847
|
+
|
|
848
|
+
<div class="flex justify-between items-center pt-4 border-t border-[#252525]">
|
|
849
|
+
<p id="panicKeyMessage" class="general-message-area text-sm"></p>
|
|
850
|
+
<button id="applyPanicKeyBtn" class="btn-toolbar-style btn-primary-override w-36" style="padding: 0.5rem 0.75rem;">
|
|
851
|
+
<i class="fa-solid fa-check mr-1"></i> Apply Keys
|
|
852
|
+
</button>
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
</div>
|
|
1765
856
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
} else if (zoneFrame && zoneFrame.msRequestFullscreen) {
|
|
1778
|
-
zoneFrame.msRequestFullscreen();
|
|
1779
|
-
}
|
|
1780
|
-
});
|
|
857
|
+
<div id="tab-about" class="settings-section hidden">
|
|
858
|
+
<h3 class="text-3xl font-bold text-white mb-6">About 4SP</h3>
|
|
859
|
+
<div class="settings-box p-6 flex flex-col items-center text-center">
|
|
860
|
+
<img src="https://cdn.jsdelivr.net/npm/4sp-asset-library@latest/logo.png" class="h-24 mb-4" alt="Logo">
|
|
861
|
+
<h1 class="text-3xl font-bold text-white mb-2">(4SP) 4simpleproblems</h1>
|
|
862
|
+
<p class="text-gray-400 mb-6 max-w-lg font-light">From a soundboard, to a full downloadable client of a platform.</p>
|
|
863
|
+
|
|
864
|
+
<div class="inline-block bg-[#0a0a0a] border border-[#333] px-4 py-2 rounded-[14px] mb-8">
|
|
865
|
+
<span class="text-gray-500 text-sm">Version:</span>
|
|
866
|
+
<span class="text-indigo-400 font-mono font-bold ml-2">5.0.0 (DV)</span>
|
|
867
|
+
</div>
|
|
1781
868
|
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
869
|
+
|
|
870
|
+
<div class="flex gap-4">
|
|
871
|
+
<a href="https://github.com/4simpleproblems-v5" class="text-gray-400 hover:text-white transition"><i class="fa-brands fa-github fa-xl"></i></a>
|
|
872
|
+
<a href="https://youtube.com/4simpleproblems" class="text-gray-400 hover:text-white transition"><i class="fa-brands fa-youtube fa-xl"></i></a>
|
|
873
|
+
<a href="https://x.com/@4simpleproblems" class="text-gray-400 hover:text-white transition"><i class="fa-brands fa-discord fa-xl"></i></a>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
</div>
|
|
878
|
+
</div>
|
|
879
|
+
</div>
|
|
880
|
+
|
|
881
|
+
<div id="cropperModal">
|
|
882
|
+
<div id="cropperContent">
|
|
883
|
+
<div class="flex justify-between items-center p-6 border-b border-[#333] bg-black">
|
|
884
|
+
<h3 class="text-2xl font-bold text-white">Adjust Profile Picture</h3>
|
|
885
|
+
<button id="cancelCropBtn" class="btn-toolbar-style w-10 h-10 flex items-center justify-center p-0 rounded-xl">
|
|
886
|
+
<i class="fa-solid fa-xmark fa-xl"></i>
|
|
887
|
+
</button>
|
|
888
|
+
</div>
|
|
889
|
+
<div class="flex flex-col items-center p-6 bg-[#0a0a0a]">
|
|
890
|
+
<p class="text-sm text-gray-400 mb-4">Drag to move. Scroll to resize.</p>
|
|
891
|
+
<div id="cropperCanvasContainer" class="relative mb-6 border border-[#333] rounded-lg overflow-hidden w-full flex justify-center bg-black">
|
|
892
|
+
<canvas id="cropperCanvas"></canvas>
|
|
893
|
+
</div>
|
|
894
|
+
<div class="flex justify-end w-full">
|
|
895
|
+
<button id="submitCropBtn" class="btn-toolbar-style btn-primary-override px-6 py-2 rounded-xl">
|
|
896
|
+
<i class="fa-solid fa-check mr-2"></i> Submit
|
|
897
|
+
</button>
|
|
898
|
+
</div>
|
|
899
|
+
</div>
|
|
900
|
+
</div>
|
|
901
|
+
</div>
|
|
1785
902
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
903
|
+
<div id="universal-loader" class="fixed inset-0 bg-[#000000] z-[9999] opacity-0 flex flex-col items-end justify-end p-12 transition-opacity duration-200 hidden">
|
|
904
|
+
<img src="https://cdn.jsdelivr.net/npm/4sp-asset-library@latest/logo.png" class="absolute top-6 right-6 h-12 w-auto opacity-50" alt="Logo">
|
|
905
|
+
<div class="flex flex-col items-end gap-2">
|
|
906
|
+
<h2 id="loader-title" class="text-white text-3xl font-light italic font-[Geist]">Loading...</h2>
|
|
907
|
+
<div class="w-64 h-1 bg-[#333] rounded-full overflow-hidden">
|
|
908
|
+
<div id="loader-bar" class="h-full bg-white w-0 transition-all duration-1000 ease-out"></div>
|
|
909
|
+
</div>
|
|
910
|
+
</div>
|
|
911
|
+
</div>
|
|
1791
912
|
|
|
1792
913
|
<script type="module">
|
|
1793
914
|
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
|
|
@@ -1828,7 +949,16 @@
|
|
|
1828
949
|
const displayUsername = document.getElementById('display-username');
|
|
1829
950
|
const displayEmail = document.getElementById('display-email');
|
|
1830
951
|
|
|
1831
|
-
const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.0.
|
|
952
|
+
const BASE_URL = 'https://cdn.jsdelivr.net/npm/4sp-dv@1.0.48/logged-in/';
|
|
953
|
+
|
|
954
|
+
// Preload Logos
|
|
955
|
+
const preloadImgs = [
|
|
956
|
+
'https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo-dark.png',
|
|
957
|
+
'https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo-christmas.png',
|
|
958
|
+
'https://cdn.jsdelivr.net/npm/4sp-asset-library@latest/logo.png'
|
|
959
|
+
];
|
|
960
|
+
preloadImgs.forEach(src => new Image().src = src);
|
|
961
|
+
|
|
1832
962
|
const STORAGE_KEY = 'local_access_code';
|
|
1833
963
|
const USER_DATA_KEY = 'local_user_data';
|
|
1834
964
|
const LAST_PAGE_KEY = 'local_last_page';
|
|
@@ -1854,7 +984,9 @@
|
|
|
1854
984
|
function initAdminKeybinds(db, auth, user) {
|
|
1855
985
|
console.log("[Admin Keybinds] Initializing for", user.uid);
|
|
1856
986
|
let explicitEnabled = true;
|
|
987
|
+
let thirdPartyEnabled = true; // Track third party state
|
|
1857
988
|
let lastEnablePressTime = 0;
|
|
989
|
+
let lastThirdPartyEnablePressTime = 0; // Separate timer for third party
|
|
1858
990
|
let configUnsubscribe = null;
|
|
1859
991
|
|
|
1860
992
|
// Visual Feedback Helper
|
|
@@ -1926,9 +1058,14 @@
|
|
|
1926
1058
|
explicitEnabled = data.explicitEnabled;
|
|
1927
1059
|
console.log(`[Admin Keybinds] Explicit Enabled: ${explicitEnabled}`);
|
|
1928
1060
|
}
|
|
1061
|
+
if (data.thirdPartyEnabled !== undefined) {
|
|
1062
|
+
thirdPartyEnabled = data.thirdPartyEnabled;
|
|
1063
|
+
console.log(`[Admin Keybinds] Third Party Enabled: ${thirdPartyEnabled}`);
|
|
1064
|
+
}
|
|
1929
1065
|
} else {
|
|
1930
1066
|
console.log("[Admin Keybinds] Config doc missing. Defaulting to TRUE.");
|
|
1931
1067
|
explicitEnabled = true;
|
|
1068
|
+
thirdPartyEnabled = true;
|
|
1932
1069
|
}
|
|
1933
1070
|
}, (error) => console.error("Config Listen Error:", error));
|
|
1934
1071
|
}
|
|
@@ -1936,45 +1073,87 @@
|
|
|
1936
1073
|
// Init Listener
|
|
1937
1074
|
subscribeToConfig();
|
|
1938
1075
|
|
|
1939
|
-
//
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
if (
|
|
1943
|
-
|
|
1944
|
-
console.log("[Admin Keybinds]
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1076
|
+
// EXPOSE GLOBAL TRIGGERS
|
|
1077
|
+
window.triggerAdminKeybind = async () => {
|
|
1078
|
+
console.log("[Admin Keybinds] Triggered via global function.");
|
|
1079
|
+
if (explicitEnabled) {
|
|
1080
|
+
// Logic for disabling (Immediate)
|
|
1081
|
+
console.log("[Admin Keybinds] Disabling explicit sounds...");
|
|
1082
|
+
try {
|
|
1083
|
+
await setDoc(doc(db, 'config', 'soundboard'), { explicitEnabled: false }, { merge: true });
|
|
1084
|
+
showAdminToast("Explicit Sounds: DISABLED", "red");
|
|
1085
|
+
lastEnablePressTime = 0;
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
console.error("[Admin Keybinds] Failed to disable:", err);
|
|
1088
|
+
showAdminToast("Error: Check Console", "red");
|
|
1089
|
+
}
|
|
1090
|
+
} else {
|
|
1091
|
+
// Logic for enabling (Double Press Safety)
|
|
1092
|
+
const now = Date.now();
|
|
1093
|
+
if (now - lastEnablePressTime < 1500) {
|
|
1094
|
+
console.log("[Admin Keybinds] Enabling explicit sounds...");
|
|
1949
1095
|
try {
|
|
1950
|
-
await setDoc(doc(db, 'config', 'soundboard'), { explicitEnabled:
|
|
1951
|
-
showAdminToast("Explicit Sounds:
|
|
1096
|
+
await setDoc(doc(db, 'config', 'soundboard'), { explicitEnabled: true }, { merge: true });
|
|
1097
|
+
showAdminToast("Explicit Sounds: ENABLED", "green");
|
|
1952
1098
|
lastEnablePressTime = 0;
|
|
1953
1099
|
} catch (err) {
|
|
1954
|
-
console.error("[Admin Keybinds] Failed to
|
|
1100
|
+
console.error("[Admin Keybinds] Failed to enable:", err);
|
|
1955
1101
|
showAdminToast("Error: Check Console", "red");
|
|
1956
1102
|
}
|
|
1957
1103
|
} else {
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1104
|
+
console.log("[Admin Keybinds] Waiting for confirm...");
|
|
1105
|
+
showAdminToast("Press Ctrl+Shift+E again to ENABLE", "blue");
|
|
1106
|
+
lastEnablePressTime = now;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
window.triggerThirdPartyKeybind = async () => {
|
|
1112
|
+
console.log("[Admin Keybinds] Triggered Third Party toggle.");
|
|
1113
|
+
if (thirdPartyEnabled) {
|
|
1114
|
+
// Disable (Immediate)
|
|
1115
|
+
try {
|
|
1116
|
+
await setDoc(doc(db, 'config', 'soundboard'), { thirdPartyEnabled: false }, { merge: true });
|
|
1117
|
+
showAdminToast("Third Party Sounds: HIDDEN", "red");
|
|
1118
|
+
lastThirdPartyEnablePressTime = 0;
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
console.error("[Admin Keybinds] Failed to disable:", err);
|
|
1121
|
+
showAdminToast("Error: Check Console", "red");
|
|
1122
|
+
}
|
|
1123
|
+
} else {
|
|
1124
|
+
// Enable (Double Press Safety)
|
|
1125
|
+
const now = Date.now();
|
|
1126
|
+
if (now - lastThirdPartyEnablePressTime < 1500) {
|
|
1127
|
+
try {
|
|
1128
|
+
await setDoc(doc(db, 'config', 'soundboard'), { thirdPartyEnabled: true }, { merge: true });
|
|
1129
|
+
showAdminToast("Third Party Sounds: SHOWN", "green");
|
|
1130
|
+
lastThirdPartyEnablePressTime = 0;
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
console.error("[Admin Keybinds] Failed to enable:", err);
|
|
1133
|
+
showAdminToast("Error: Check Console", "red");
|
|
1974
1134
|
}
|
|
1135
|
+
} else {
|
|
1136
|
+
showAdminToast("Press Ctrl+Shift+F again to SHOW", "blue");
|
|
1137
|
+
lastThirdPartyEnablePressTime = now;
|
|
1975
1138
|
}
|
|
1976
1139
|
}
|
|
1977
|
-
}
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
// Key Listener (Parent Window)
|
|
1143
|
+
window.addEventListener('keydown', async (e) => {
|
|
1144
|
+
// Trigger is Ctrl + Shift + E
|
|
1145
|
+
if (e.ctrlKey && e.shiftKey && (e.key.toLowerCase() === 'e' || e.code === 'KeyE')) {
|
|
1146
|
+
e.preventDefault();
|
|
1147
|
+
window.triggerAdminKeybind();
|
|
1148
|
+
}
|
|
1149
|
+
// Trigger is Ctrl + Shift + F
|
|
1150
|
+
if (e.ctrlKey && e.shiftKey && (e.key.toLowerCase() === 'f' || e.code === 'KeyF')) {
|
|
1151
|
+
e.preventDefault();
|
|
1152
|
+
e.stopImmediatePropagation();
|
|
1153
|
+
e.stopPropagation();
|
|
1154
|
+
window.triggerThirdPartyKeybind();
|
|
1155
|
+
}
|
|
1156
|
+
}, { capture: true });
|
|
1978
1157
|
}
|
|
1979
1158
|
|
|
1980
1159
|
// --- ANALYTICS LOGIC (Integrated) ---
|
|
@@ -2061,10 +1240,11 @@
|
|
|
2061
1240
|
|
|
2062
1241
|
let currentPath = activePageUrl.replace(BASE_URL, '');
|
|
2063
1242
|
|
|
2064
|
-
Object.
|
|
1243
|
+
Object.entries(PAGE_DATA).forEach(([key, page]) => {
|
|
2065
1244
|
const isAuth = page.url === currentPath;
|
|
2066
1245
|
const tab = document.createElement('a');
|
|
2067
1246
|
tab.className = `nav-tab ${isAuth ? 'active' : ''}`;
|
|
1247
|
+
if (key === 'settings') tab.id = 'nav-settings-tab';
|
|
2068
1248
|
tab.innerHTML = `<i class="${page.icon}"></i> ${page.name}`;
|
|
2069
1249
|
tab.onclick = () => loadPage(page.url);
|
|
2070
1250
|
tabsContainerEl.appendChild(tab);
|
|
@@ -2114,7 +1294,26 @@
|
|
|
2114
1294
|
}
|
|
2115
1295
|
|
|
2116
1296
|
// Initial check
|
|
2117
|
-
setTimeout(
|
|
1297
|
+
setTimeout(() => {
|
|
1298
|
+
updateScrollGilders();
|
|
1299
|
+
|
|
1300
|
+
// Ensure Active Tab Visibility (Scroll if under gradient)
|
|
1301
|
+
const activeTab = tabsContainerEl.querySelector('.nav-tab.active');
|
|
1302
|
+
if (activeTab) {
|
|
1303
|
+
const containerRect = tabsContainerEl.getBoundingClientRect();
|
|
1304
|
+
const tabRect = activeTab.getBoundingClientRect();
|
|
1305
|
+
const gradientWidth = 70; // Gradient is ~60px + padding
|
|
1306
|
+
|
|
1307
|
+
// Check Left Edge
|
|
1308
|
+
if (tabRect.left < containerRect.left + gradientWidth) {
|
|
1309
|
+
tabsContainerEl.scrollBy({ left: tabRect.left - containerRect.left - gradientWidth - 20, behavior: 'smooth' });
|
|
1310
|
+
}
|
|
1311
|
+
// Check Right Edge
|
|
1312
|
+
else if (tabRect.right > containerRect.right - gradientWidth) {
|
|
1313
|
+
tabsContainerEl.scrollBy({ left: tabRect.right - containerRect.right + gradientWidth + 20, behavior: 'smooth' });
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}, 50);
|
|
2118
1317
|
}
|
|
2119
1318
|
|
|
2120
1319
|
// Auth Menu
|
|
@@ -2160,6 +1359,8 @@
|
|
|
2160
1359
|
pfpType: data.pfpType,
|
|
2161
1360
|
customPfp: data.customPfp,
|
|
2162
1361
|
pfpLetterBg: data.pfpLetterBg,
|
|
1362
|
+
letterAvatarText: data.letterAvatarText,
|
|
1363
|
+
letterAvatarTextColor: data.letterAvatarTextColor, // New Field
|
|
2163
1364
|
navbarTheme: data.navbarTheme // Sync theme if updated remotely
|
|
2164
1365
|
};
|
|
2165
1366
|
|
|
@@ -2225,12 +1426,16 @@
|
|
|
2225
1426
|
let pfpHtml = '';
|
|
2226
1427
|
|
|
2227
1428
|
if (pfpType === 'custom' && userData.customPfp) {
|
|
2228
|
-
pfpHtml = `<img src="${userData.customPfp}" class="w-full h-full object-cover rounded-
|
|
1429
|
+
pfpHtml = `<img src="${userData.customPfp}" class="w-full h-full object-cover rounded-[14px]" alt="Profile">`;
|
|
2229
1430
|
} else {
|
|
2230
1431
|
// Letter Avatar
|
|
2231
|
-
const
|
|
1432
|
+
const initial = (userData.username || 'U').charAt(0).toUpperCase();
|
|
1433
|
+
const letter = userData.letterAvatarText || initial; // Use custom text or fallback
|
|
2232
1434
|
const bgColor = userData.pfpLetterBg || '#3B82F6'; // Default blue
|
|
2233
|
-
|
|
1435
|
+
// Adjust font size if length > 1
|
|
1436
|
+
const fontSizeClass = letter.length > 2 ? 'text-xs' : (letter.length > 1 ? 'text-sm' : 'text-lg');
|
|
1437
|
+
|
|
1438
|
+
pfpHtml = `<div class="w-full h-full rounded-[14px] flex items-center justify-center text-white font-bold ${fontSizeClass}" style="background-color: ${bgColor};">${letter}</div>`;
|
|
2234
1439
|
}
|
|
2235
1440
|
|
|
2236
1441
|
authBtn.innerHTML = pfpHtml;
|
|
@@ -2247,9 +1452,19 @@
|
|
|
2247
1452
|
unlockBtn.classList.add('hidden');
|
|
2248
1453
|
codeInput.classList.add('hidden');
|
|
2249
1454
|
|
|
1455
|
+
// Timeout Race to prevent infinite "Verifying..."
|
|
1456
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
1457
|
+
setTimeout(() => reject(new Error("Verification timed out.")), 10000)
|
|
1458
|
+
);
|
|
1459
|
+
|
|
2250
1460
|
try {
|
|
2251
1461
|
const docRef = doc(db, "access_codes", savedCode);
|
|
2252
|
-
|
|
1462
|
+
|
|
1463
|
+
// Race the Firestore connection
|
|
1464
|
+
const docSnap = await Promise.race([
|
|
1465
|
+
getDoc(docRef),
|
|
1466
|
+
timeoutPromise
|
|
1467
|
+
]);
|
|
2253
1468
|
|
|
2254
1469
|
if (docSnap.exists() && docSnap.data().active) {
|
|
2255
1470
|
const codeData = docSnap.data();
|
|
@@ -2265,9 +1480,12 @@
|
|
|
2265
1480
|
currentUser = userData;
|
|
2266
1481
|
}
|
|
2267
1482
|
|
|
2268
|
-
// Always fetch fresh to re-verify admin status
|
|
1483
|
+
// Always fetch fresh to re-verify admin status (also raced)
|
|
2269
1484
|
if (codeData.ownerUid) {
|
|
2270
|
-
await
|
|
1485
|
+
await Promise.race([
|
|
1486
|
+
fetchUserData(codeData.ownerUid),
|
|
1487
|
+
timeoutPromise
|
|
1488
|
+
]);
|
|
2271
1489
|
}
|
|
2272
1490
|
|
|
2273
1491
|
launchApp();
|
|
@@ -2278,7 +1496,7 @@
|
|
|
2278
1496
|
}
|
|
2279
1497
|
} catch(e) {
|
|
2280
1498
|
console.error("Auto-login error:", e);
|
|
2281
|
-
resetLockScreen("Connection
|
|
1499
|
+
resetLockScreen("Connection issue. Please login again.");
|
|
2282
1500
|
}
|
|
2283
1501
|
}
|
|
2284
1502
|
}
|
|
@@ -2400,6 +1618,36 @@
|
|
|
2400
1618
|
const settingsOverlay = document.getElementById('settings-overlay');
|
|
2401
1619
|
if (settingsOverlay) settingsOverlay.classList.add('hidden');
|
|
2402
1620
|
|
|
1621
|
+
// --- UNIVERSAL LOADER LOGIC ---
|
|
1622
|
+
// Normalize page name to find title
|
|
1623
|
+
let cleanPageName = pageName;
|
|
1624
|
+
if (cleanPageName.startsWith('./')) cleanPageName = cleanPageName.substring(2);
|
|
1625
|
+
if (cleanPageName.startsWith(BASE_URL)) cleanPageName = cleanPageName.substring(BASE_URL.length);
|
|
1626
|
+
// remove query params for lookup
|
|
1627
|
+
let lookupName = cleanPageName.split('?')[0].replace('.html', '');
|
|
1628
|
+
|
|
1629
|
+
const loader = document.getElementById('universal-loader');
|
|
1630
|
+
const loaderTitle = document.getElementById('loader-title');
|
|
1631
|
+
const loaderBar = document.getElementById('loader-bar');
|
|
1632
|
+
|
|
1633
|
+
if (loader && loaderTitle && loaderBar) {
|
|
1634
|
+
// Determine Title
|
|
1635
|
+
let pageTitle = "Loading...";
|
|
1636
|
+
const pageEntry = Object.values(PAGE_DATA).find(p => p.url === cleanPageName || p.url.includes(lookupName));
|
|
1637
|
+
if (pageEntry) pageTitle = pageEntry.name;
|
|
1638
|
+
|
|
1639
|
+
loaderTitle.textContent = pageTitle;
|
|
1640
|
+
|
|
1641
|
+
// Show Loader
|
|
1642
|
+
loader.classList.remove('hidden');
|
|
1643
|
+
// Trigger reflow/anim
|
|
1644
|
+
requestAnimationFrame(() => {
|
|
1645
|
+
loader.classList.remove('opacity-0');
|
|
1646
|
+
loader.classList.add('active');
|
|
1647
|
+
loaderBar.style.width = "70%"; // Start animation
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
|
|
2403
1651
|
// Normalize path
|
|
2404
1652
|
if (pageName.startsWith('./')) pageName = pageName.substring(2);
|
|
2405
1653
|
if (pageName.startsWith('../')) pageName = pageName.substring(3);
|
|
@@ -2418,67 +1666,7 @@
|
|
|
2418
1666
|
localStorage.setItem(LAST_PAGE_KEY, pageName);
|
|
2419
1667
|
renderNavbar(pageName);
|
|
2420
1668
|
|
|
2421
|
-
//
|
|
2422
|
-
if (pageName === 'games.html') {
|
|
2423
|
-
const template = document.getElementById('games-page-template');
|
|
2424
|
-
if (template) {
|
|
2425
|
-
// Create new iframe
|
|
2426
|
-
const oldFrame = document.getElementById('app-frame');
|
|
2427
|
-
const newFrame = document.createElement('iframe');
|
|
2428
|
-
newFrame.id = 'app-frame';
|
|
2429
|
-
|
|
2430
|
-
if (oldFrame) {
|
|
2431
|
-
newFrame.style.cssText = oldFrame.style.cssText;
|
|
2432
|
-
if (oldFrame.parentNode) oldFrame.parentNode.replaceChild(newFrame, oldFrame);
|
|
2433
|
-
} else {
|
|
2434
|
-
newFrame.style.width = '100%';
|
|
2435
|
-
newFrame.style.height = '100%';
|
|
2436
|
-
newFrame.style.border = 'none';
|
|
2437
|
-
newFrame.style.display = 'block';
|
|
2438
|
-
newFrame.style.flexGrow = '1';
|
|
2439
|
-
document.body.appendChild(newFrame);
|
|
2440
|
-
}
|
|
2441
|
-
appFrame = newFrame;
|
|
2442
|
-
|
|
2443
|
-
const doc = newFrame.contentWindow.document;
|
|
2444
|
-
doc.open();
|
|
2445
|
-
|
|
2446
|
-
// We still need the base tag for certain resources, but imports inside template use absolute
|
|
2447
|
-
const baseTag = '<base href="' + BASE_URL + '">';
|
|
2448
|
-
|
|
2449
|
-
// Client bridge script
|
|
2450
|
-
const patchScript = `
|
|
2451
|
-
<script>
|
|
2452
|
-
window._LOCAL_MODE = true;
|
|
2453
|
-
window._CURRENT_PAGE_NAME = "${pageName}";
|
|
2454
|
-
|
|
2455
|
-
window.currentUser = {
|
|
2456
|
-
uid: '${currentUser ? currentUser.uid : "client-user"}',
|
|
2457
|
-
displayName: '${currentUser ? currentUser.username : "Client User"}',
|
|
2458
|
-
email: '${currentUser ? currentUser.email : "local@client"}',
|
|
2459
|
-
photoURL: null,
|
|
2460
|
-
providerData: []
|
|
2461
|
-
};
|
|
2462
|
-
|
|
2463
|
-
window.openSettings = function() { window.parent.openSettings(); };
|
|
2464
|
-
|
|
2465
|
-
// Forward keydown to parent for panic key
|
|
2466
|
-
window.addEventListener('keydown', function(e) {
|
|
2467
|
-
if(window.parent.handlePanic) window.parent.handlePanic(e);
|
|
2468
|
-
});
|
|
2469
|
-
<\/script>
|
|
2470
|
-
`;
|
|
2471
|
-
|
|
2472
|
-
// Inject template content
|
|
2473
|
-
let templateContent = template.innerHTML;
|
|
2474
|
-
let modifiedHtml = templateContent.replace('<head>', '<head>' + baseTag + patchScript);
|
|
2475
|
-
doc.write(modifiedHtml);
|
|
2476
|
-
doc.close();
|
|
2477
|
-
return;
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
// Normal fetch flow for other pages
|
|
1669
|
+
// Normal fetch flow for ALL pages
|
|
2482
1670
|
const url = BASE_URL + pageName;
|
|
2483
1671
|
|
|
2484
1672
|
fetch(url)
|
|
@@ -2503,6 +1691,21 @@
|
|
|
2503
1691
|
}
|
|
2504
1692
|
|
|
2505
1693
|
appFrame = newFrame;
|
|
1694
|
+
|
|
1695
|
+
// Loader completion logic
|
|
1696
|
+
newFrame.onload = () => {
|
|
1697
|
+
if (loaderBar) loaderBar.style.width = "100%";
|
|
1698
|
+
setTimeout(() => {
|
|
1699
|
+
if (loader) {
|
|
1700
|
+
loader.classList.add('opacity-0');
|
|
1701
|
+
loader.classList.remove('active');
|
|
1702
|
+
setTimeout(() => {
|
|
1703
|
+
loader.classList.add('hidden');
|
|
1704
|
+
if(loaderBar) loaderBar.style.width = "0%";
|
|
1705
|
+
}, 200);
|
|
1706
|
+
}
|
|
1707
|
+
}, 200);
|
|
1708
|
+
};
|
|
2506
1709
|
|
|
2507
1710
|
const doc = newFrame.contentWindow.document;
|
|
2508
1711
|
doc.open();
|
|
@@ -2527,9 +1730,48 @@
|
|
|
2527
1730
|
window.parent.openSettings();
|
|
2528
1731
|
};
|
|
2529
1732
|
|
|
2530
|
-
|
|
1733
|
+
function notifyParentTyping() {
|
|
1734
|
+
if (window.parent && window.parent.playTypeSound) {
|
|
1735
|
+
window.parent.playTypeSound();
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
2531
1739
|
window.addEventListener('keydown', function(e) {
|
|
1740
|
+
// Forward Panic
|
|
2532
1741
|
if(window.parent.handlePanic) window.parent.handlePanic(e);
|
|
1742
|
+
|
|
1743
|
+
// Forward Admin Keybind (Ctrl + Shift + E) - Explicit Check
|
|
1744
|
+
if (e.ctrlKey && e.shiftKey && (e.key.toLowerCase() === 'e' || e.code === 'KeyE')) {
|
|
1745
|
+
e.preventDefault();
|
|
1746
|
+
if (window.parent.triggerAdminKeybind) window.parent.triggerAdminKeybind();
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// Forward Admin Keybind (Ctrl + Shift + F) - Third Party Check
|
|
1750
|
+
if (e.ctrlKey && e.shiftKey && (e.key.toLowerCase() === 'f' || e.code === 'KeyF')) {
|
|
1751
|
+
e.preventDefault();
|
|
1752
|
+
e.stopImmediatePropagation();
|
|
1753
|
+
e.stopPropagation();
|
|
1754
|
+
if (window.parent.triggerThirdPartyKeybind) window.parent.triggerThirdPartyKeybind();
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// Typing Sound (Standard Inputs)
|
|
1758
|
+
const target = e.target;
|
|
1759
|
+
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA';
|
|
1760
|
+
const isContentEditable = target.isContentEditable;
|
|
1761
|
+
|
|
1762
|
+
// Ignore non-character keys to avoid spamming sound on navigation
|
|
1763
|
+
const validKey = e.key.length === 1 || e.key === 'Backspace' || e.key === 'Enter';
|
|
1764
|
+
|
|
1765
|
+
if ((isInput || isContentEditable) && validKey && !e.repeat) {
|
|
1766
|
+
notifyParentTyping();
|
|
1767
|
+
}
|
|
1768
|
+
}, { capture: true });
|
|
1769
|
+
|
|
1770
|
+
// Fallback 'input' listener for weird edge cases
|
|
1771
|
+
window.addEventListener('input', function(e) {
|
|
1772
|
+
// Simple fallback if keydown missed it (e.g. virtual keyboard)
|
|
1773
|
+
// We rely on parent debouncing
|
|
1774
|
+
notifyParentTyping();
|
|
2533
1775
|
});
|
|
2534
1776
|
|
|
2535
1777
|
document.addEventListener('click', e => {
|
|
@@ -2557,6 +1799,8 @@
|
|
|
2557
1799
|
.catch(err => {
|
|
2558
1800
|
console.error(err);
|
|
2559
1801
|
alert("Failed to load page: " + pageName);
|
|
1802
|
+
// Hide loader on error
|
|
1803
|
+
if(loader) loader.classList.add('hidden');
|
|
2560
1804
|
});
|
|
2561
1805
|
};
|
|
2562
1806
|
|
|
@@ -2587,6 +1831,9 @@
|
|
|
2587
1831
|
if (sec.id === `tab-${tabId}`) sec.classList.remove('hidden');
|
|
2588
1832
|
else sec.classList.add('hidden');
|
|
2589
1833
|
});
|
|
1834
|
+
|
|
1835
|
+
// Save State
|
|
1836
|
+
localStorage.setItem('last_settings_tab', tabId);
|
|
2590
1837
|
});
|
|
2591
1838
|
});
|
|
2592
1839
|
|
|
@@ -2656,13 +1903,30 @@
|
|
|
2656
1903
|
|
|
2657
1904
|
// --- 2. Personalization ---
|
|
2658
1905
|
// Letter Avatar Setup
|
|
2659
|
-
|
|
1906
|
+
// Removed last color (Grey) to make 10 colors (5x2 grid)
|
|
1907
|
+
const letterColors = [
|
|
1908
|
+
'EF4444', // Red
|
|
1909
|
+
'F97316', 'FDBA74', // Orange
|
|
1910
|
+
'EAB308', 'FDE047', // Yellow
|
|
1911
|
+
'22C55E', '86EFAC', // Green
|
|
1912
|
+
'06B6D4', '67E8F9', // Cyan
|
|
1913
|
+
'3B82F6', '93C5FD', // Blue
|
|
1914
|
+
'6366F1', 'A5B4FC', // Indigo
|
|
1915
|
+
'A855F7', 'D8B4FE', // Purple
|
|
1916
|
+
'EC4899', 'F9A8D4', // Pink
|
|
1917
|
+
'6B7280', '000000' // Grey & Black
|
|
1918
|
+
];
|
|
2660
1919
|
const pfpModeBtns = document.querySelectorAll('.pfp-mode-btn');
|
|
2661
1920
|
const pfpLetterOptions = document.getElementById('pfp-letter-options');
|
|
2662
1921
|
const pfpUploadOptions = document.getElementById('pfp-upload-options');
|
|
2663
1922
|
const pfpPreview = document.getElementById('pfp-preview');
|
|
2664
1923
|
const saveLetterPfpBtn = document.getElementById('save-letter-pfp-btn');
|
|
2665
1924
|
|
|
1925
|
+
// New Inputs
|
|
1926
|
+
const pfpCustomText = document.getElementById('pfp-custom-text');
|
|
1927
|
+
const pfpCustomColor = document.getElementById('pfp-custom-color');
|
|
1928
|
+
const triggerUploadBtn = document.getElementById('trigger-upload-btn');
|
|
1929
|
+
|
|
2666
1930
|
let pfpState = { mode: 'letter', letterColor: '#3B82F6' };
|
|
2667
1931
|
|
|
2668
1932
|
pfpModeBtns.forEach(btn => {
|
|
@@ -2680,17 +1944,24 @@
|
|
|
2680
1944
|
} else {
|
|
2681
1945
|
pfpLetterOptions.classList.add('hidden');
|
|
2682
1946
|
pfpUploadOptions.classList.remove('hidden');
|
|
1947
|
+
updatePfpPreview(); // Ensure preview switches if needed
|
|
2683
1948
|
}
|
|
2684
1949
|
});
|
|
2685
1950
|
});
|
|
2686
1951
|
|
|
2687
1952
|
// Render Color Grid
|
|
2688
|
-
const gridContainer =
|
|
2689
|
-
gridContainer.innerHTML = '';
|
|
1953
|
+
const gridContainer = document.getElementById('pfp-color-grid');
|
|
1954
|
+
gridContainer.innerHTML = '';
|
|
1955
|
+
|
|
2690
1956
|
letterColors.forEach(color => {
|
|
2691
1957
|
const div = document.createElement('div');
|
|
2692
|
-
div.className = 'w-
|
|
1958
|
+
div.className = 'w-10 h-10 rounded-lg cursor-pointer hover:scale-110 transition border-2 border-transparent';
|
|
2693
1959
|
div.style.backgroundColor = '#' + color;
|
|
1960
|
+
|
|
1961
|
+
if (color === '000000') {
|
|
1962
|
+
div.style.borderColor = '#ffffff';
|
|
1963
|
+
}
|
|
1964
|
+
|
|
2694
1965
|
div.onclick = () => {
|
|
2695
1966
|
pfpState.letterColor = '#' + color;
|
|
2696
1967
|
updatePfpPreview();
|
|
@@ -2698,25 +1969,69 @@
|
|
|
2698
1969
|
gridContainer.appendChild(div);
|
|
2699
1970
|
});
|
|
2700
1971
|
|
|
1972
|
+
// Listeners for new inputs
|
|
1973
|
+
pfpCustomText.addEventListener('input', () => updatePfpPreview());
|
|
1974
|
+
|
|
1975
|
+
// Removed old pfpCustomColor logic
|
|
1976
|
+
|
|
1977
|
+
if (triggerUploadBtn) {
|
|
1978
|
+
triggerUploadBtn.addEventListener('click', () => {
|
|
1979
|
+
document.getElementById('pfp-upload-input').click();
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1982
|
+
|
|
2701
1983
|
function updatePfpPreview() {
|
|
2702
1984
|
if (pfpState.mode === 'letter') {
|
|
2703
|
-
const
|
|
1985
|
+
const usernameChar = currentUser ? (currentUser.username || "U").charAt(0).toUpperCase() : "U";
|
|
1986
|
+
// Prefer input value, fallback to saved text, fallback to username char
|
|
1987
|
+
let displayText = pfpCustomText.value.trim().toUpperCase();
|
|
1988
|
+
|
|
1989
|
+
if (!displayText) displayText = usernameChar;
|
|
1990
|
+
|
|
2704
1991
|
pfpPreview.style.backgroundImage = 'none';
|
|
2705
1992
|
pfpPreview.style.backgroundColor = pfpState.letterColor;
|
|
2706
|
-
pfpPreview.
|
|
1993
|
+
pfpPreview.style.color = '#FFFFFF';
|
|
1994
|
+
pfpPreview.innerText = displayText;
|
|
1995
|
+
// Dynamic font size
|
|
1996
|
+
if (displayText.length > 2) pfpPreview.style.fontSize = '1.5rem';
|
|
1997
|
+
else if (displayText.length > 1) pfpPreview.style.fontSize = '2rem';
|
|
1998
|
+
else pfpPreview.style.fontSize = '3rem';
|
|
1999
|
+
|
|
2000
|
+
} else if (pfpState.mode === 'upload') {
|
|
2001
|
+
// ... (Existing logic)
|
|
2002
|
+
if (currentUser && currentUser.customPfp && currentUser.pfpType === 'custom') {
|
|
2003
|
+
pfpPreview.style.backgroundImage = `url('${currentUser.customPfp}')`;
|
|
2004
|
+
pfpPreview.style.backgroundColor = "transparent";
|
|
2005
|
+
pfpPreview.style.backgroundSize = "cover";
|
|
2006
|
+
pfpPreview.innerText = "";
|
|
2007
|
+
} else {
|
|
2008
|
+
const initial = currentUser ? (currentUser.username || "U").charAt(0).toUpperCase() : "U";
|
|
2009
|
+
pfpPreview.style.backgroundImage = 'none';
|
|
2010
|
+
pfpPreview.style.backgroundColor = '#333';
|
|
2011
|
+
pfpPreview.style.color = '#FFFFFF';
|
|
2012
|
+
pfpPreview.innerText = initial;
|
|
2013
|
+
pfpPreview.style.fontSize = '3rem';
|
|
2014
|
+
}
|
|
2707
2015
|
}
|
|
2708
2016
|
}
|
|
2709
2017
|
|
|
2710
2018
|
saveLetterPfpBtn.addEventListener('click', async () => {
|
|
2711
2019
|
if (!currentUser) return;
|
|
2020
|
+
|
|
2021
|
+
const textVal = pfpCustomText.value.trim().toUpperCase();
|
|
2022
|
+
if (/[<>/\\\\]/.test(textVal)) {
|
|
2023
|
+
alert("Invalid characters in avatar text.");
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2712
2027
|
saveLetterPfpBtn.innerText = "Saving...";
|
|
2713
2028
|
try {
|
|
2714
2029
|
await updateDoc(doc(db, "users", currentUser.uid), {
|
|
2715
2030
|
pfpType: 'letter',
|
|
2716
2031
|
pfpLetterBg: pfpState.letterColor,
|
|
2717
|
-
letterAvatarColor: pfpState.letterColor
|
|
2032
|
+
letterAvatarColor: pfpState.letterColor,
|
|
2033
|
+
letterAvatarText: textVal
|
|
2718
2034
|
});
|
|
2719
|
-
// Local update handled by snapshot
|
|
2720
2035
|
saveLetterPfpBtn.innerText = "Saved!";
|
|
2721
2036
|
setTimeout(() => saveLetterPfpBtn.innerText = "Set Letter Avatar", 2000);
|
|
2722
2037
|
} catch(e) {
|
|
@@ -2991,15 +2306,23 @@
|
|
|
2991
2306
|
|
|
2992
2307
|
// --- Logo Path Modification ---
|
|
2993
2308
|
let logoSrc;
|
|
2994
|
-
|
|
2309
|
+
const isLightTheme = lightThemeNames.includes(theme.name);
|
|
2310
|
+
|
|
2311
|
+
if (isLightTheme) {
|
|
2995
2312
|
logoSrc = 'https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo-dark.png';
|
|
2313
|
+
root.style.setProperty('--menu-username-text', '#000000');
|
|
2314
|
+
root.style.setProperty('--menu-email-text', '#444444');
|
|
2996
2315
|
} else if (theme.name === 'Christmas') {
|
|
2997
2316
|
logoSrc = 'https://cdn.jsdelivr.net/npm/4sp-dv@latest/images/logo-christmas.png';
|
|
2317
|
+
root.style.setProperty('--menu-username-text', 'white');
|
|
2318
|
+
root.style.setProperty('--menu-email-text', '#9ca3af');
|
|
2998
2319
|
} else {
|
|
2999
2320
|
logoSrc = 'https://cdn.jsdelivr.net/npm/4sp-asset-library@latest/logo.png';
|
|
2321
|
+
root.style.setProperty('--menu-username-text', 'white');
|
|
2322
|
+
root.style.setProperty('--menu-email-text', '#9ca3af');
|
|
3000
2323
|
}
|
|
3001
2324
|
|
|
3002
|
-
// Apply CSS Variables
|
|
2325
|
+
// Apply CSS Variables from theme object (overrides defaults if present)
|
|
3003
2326
|
for (const [key, value] of Object.entries(theme)) {
|
|
3004
2327
|
if (key !== 'logo-src' && key !== 'name') {
|
|
3005
2328
|
root.style.setProperty(`--${key}`, value);
|
|
@@ -3009,29 +2332,44 @@
|
|
|
3009
2332
|
// Apply Logo & Tinting
|
|
3010
2333
|
const logoImg = document.getElementById('navbar-logo');
|
|
3011
2334
|
if (logoImg) {
|
|
3012
|
-
// Update Source (Use absolute path comparison or specific logic)
|
|
3013
|
-
// We just set it. Browser handles cache.
|
|
3014
|
-
// Note: The original navigation.js checked src endsWith to avoid reload flicker, but simple assignment is okay.
|
|
3015
2335
|
if (!logoImg.src.includes(logoSrc)) {
|
|
3016
2336
|
logoImg.src = logoSrc;
|
|
3017
2337
|
}
|
|
3018
2338
|
|
|
3019
|
-
// Apply Tinting
|
|
3020
2339
|
const noFilterThemes = ['Dark', 'Light', 'Christmas'];
|
|
3021
|
-
|
|
2340
|
+
const isNoFilter = noFilterThemes.includes(theme.name);
|
|
2341
|
+
|
|
2342
|
+
// Check if mode is changing (Tinted <-> Standard)
|
|
2343
|
+
// We check existing transform state
|
|
2344
|
+
const wasNoFilter = logoImg.style.transform === '' || logoImg.style.transform === 'none';
|
|
2345
|
+
const modeChanged = isNoFilter !== wasNoFilter;
|
|
2346
|
+
|
|
2347
|
+
if (modeChanged) {
|
|
2348
|
+
logoImg.style.transition = 'none';
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
if (isNoFilter) {
|
|
3022
2352
|
logoImg.style.filter = '';
|
|
3023
2353
|
logoImg.style.transform = '';
|
|
3024
2354
|
} else {
|
|
3025
2355
|
const tintColor = theme['tab-active-text'] || '#ffffff';
|
|
3026
|
-
// The original navigation.js used this trick for SVG coloring via filter
|
|
3027
2356
|
logoImg.style.filter = `drop-shadow(100px 0 0 ${tintColor})`;
|
|
3028
2357
|
logoImg.style.transform = 'translateX(-100px)';
|
|
3029
2358
|
}
|
|
2359
|
+
|
|
2360
|
+
if (modeChanged) {
|
|
2361
|
+
// Force Reflow
|
|
2362
|
+
void logoImg.offsetWidth;
|
|
2363
|
+
logoImg.style.transition = ''; // Restore
|
|
2364
|
+
}
|
|
3030
2365
|
}
|
|
3031
2366
|
|
|
3032
2367
|
// Save to local storage for persistence on reload
|
|
3033
2368
|
localStorage.setItem('user-navbar-theme', JSON.stringify(theme));
|
|
3034
2369
|
|
|
2370
|
+
// Apply Glide Button Color Sync
|
|
2371
|
+
root.style.setProperty('--glide-btn-color', 'var(--tab-text)');
|
|
2372
|
+
|
|
3035
2373
|
// Also update user doc if logged in
|
|
3036
2374
|
if (currentUser) {
|
|
3037
2375
|
updateDoc(doc(db, "users", currentUser.uid), { navbarTheme: theme }).catch(console.error);
|
|
@@ -3258,16 +2596,270 @@
|
|
|
3258
2596
|
function loadSettingsData() {
|
|
3259
2597
|
if (currentUser) {
|
|
3260
2598
|
settingsUsernameInput.value = currentUser.username || "";
|
|
2599
|
+
|
|
2600
|
+
// PFP State Init
|
|
3261
2601
|
pfpState.letterColor = currentUser.pfpLetterBg || '#3B82F6';
|
|
2602
|
+
pfpState.mode = currentUser.pfpType === 'custom' ? 'upload' : 'letter';
|
|
2603
|
+
|
|
2604
|
+
// Populate Inputs
|
|
2605
|
+
pfpCustomText.value = currentUser.letterAvatarText || "";
|
|
2606
|
+
|
|
2607
|
+
// Update UI Buttons to reflect current mode
|
|
2608
|
+
pfpModeBtns.forEach(btn => {
|
|
2609
|
+
if (btn.dataset.mode === pfpState.mode) {
|
|
2610
|
+
btn.classList.add('active', 'bg-[#222]');
|
|
2611
|
+
if (pfpState.mode === 'letter') {
|
|
2612
|
+
pfpLetterOptions.classList.remove('hidden');
|
|
2613
|
+
pfpUploadOptions.classList.add('hidden');
|
|
2614
|
+
} else {
|
|
2615
|
+
pfpLetterOptions.classList.add('hidden');
|
|
2616
|
+
pfpUploadOptions.classList.remove('hidden');
|
|
2617
|
+
}
|
|
2618
|
+
} else {
|
|
2619
|
+
btn.classList.remove('active', 'bg-[#222]');
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
|
|
3262
2623
|
updatePfpPreview();
|
|
3263
2624
|
}
|
|
2625
|
+
|
|
2626
|
+
// Restore Tab
|
|
2627
|
+
const lastTab = localStorage.getItem('last_settings_tab');
|
|
2628
|
+
if (lastTab) {
|
|
2629
|
+
const tabBtn = document.querySelector(`.settings-tab[data-tab="${lastTab}"]`);
|
|
2630
|
+
if(tabBtn) tabBtn.click();
|
|
2631
|
+
}
|
|
2632
|
+
|
|
3264
2633
|
loadThemes();
|
|
3265
|
-
loadPanicKeys();
|
|
2634
|
+
loadPanicKeys();
|
|
2635
|
+
|
|
2636
|
+
// --- App Preference Toggles Init ---
|
|
2637
|
+
const animBtn = document.getElementById('toggle-animations-btn');
|
|
2638
|
+
const soundBtn = document.getElementById('toggle-sounds-btn');
|
|
2639
|
+
|
|
2640
|
+
const updateToggleUI = () => {
|
|
2641
|
+
const isEnabled = window.areAnimationsEnabled;
|
|
2642
|
+
animBtn.classList.toggle('off', !isEnabled);
|
|
2643
|
+
animBtn.innerHTML = isEnabled ? '<i class="fa-solid fa-wand-magic-sparkles"></i>' : '<i class="fa-solid fa-wand-magic"></i>';
|
|
2644
|
+
|
|
2645
|
+
// Apply to body
|
|
2646
|
+
if (isEnabled) document.body.classList.remove('no-animations');
|
|
2647
|
+
else document.body.classList.add('no-animations');
|
|
2648
|
+
|
|
2649
|
+
soundBtn.classList.toggle('off', window.isMuted);
|
|
2650
|
+
soundBtn.innerHTML = window.isMuted ? '<i class="fa-solid fa-volume-xmark"></i>' : '<i class="fa-solid fa-volume-high"></i>';
|
|
2651
|
+
};
|
|
2652
|
+
|
|
2653
|
+
if (animBtn) {
|
|
2654
|
+
animBtn.onclick = () => {
|
|
2655
|
+
window.areAnimationsEnabled = !window.areAnimationsEnabled;
|
|
2656
|
+
localStorage.setItem('app_animations_enabled', window.areAnimationsEnabled);
|
|
2657
|
+
updateToggleUI();
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
if (soundBtn) {
|
|
2662
|
+
soundBtn.onclick = () => {
|
|
2663
|
+
window.isMuted = !window.isMuted;
|
|
2664
|
+
localStorage.setItem('app_is_muted', window.isMuted);
|
|
2665
|
+
updateToggleUI();
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
updateToggleUI();
|
|
3266
2670
|
}
|
|
3267
2671
|
|
|
3268
2672
|
checkAutoLogin();
|
|
3269
2673
|
loadPanicKeys(); // Initial load for global listener
|
|
3270
2674
|
|
|
3271
2675
|
</script>
|
|
2676
|
+
|
|
2677
|
+
<div id="notification-container"></div>
|
|
2678
|
+
<script>
|
|
2679
|
+
// --- Sound & Notification Logic ---
|
|
2680
|
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
2681
|
+
|
|
2682
|
+
// Sync with global state
|
|
2683
|
+
Object.defineProperty(window, 'isMuted', {
|
|
2684
|
+
get: () => localStorage.getItem('app_is_muted') === 'true',
|
|
2685
|
+
set: (v) => localStorage.setItem('app_is_muted', v)
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
Object.defineProperty(window, 'areAnimationsEnabled', {
|
|
2689
|
+
get: () => localStorage.getItem('app_animations_enabled') !== 'false',
|
|
2690
|
+
set: (v) => localStorage.setItem('app_animations_enabled', v)
|
|
2691
|
+
});
|
|
2692
|
+
|
|
2693
|
+
// Initial Class Set
|
|
2694
|
+
if (!window.areAnimationsEnabled) document.body.classList.add('no-animations');
|
|
2695
|
+
|
|
2696
|
+
// EXPOSE PlayTypeSound GLOBALLY
|
|
2697
|
+
// Debounce to prevent rapid-fire stuttering if called too fast
|
|
2698
|
+
let typeSoundTimeout = null;
|
|
2699
|
+
window.playTypeSound = function() {
|
|
2700
|
+
if (window.isMuted) return;
|
|
2701
|
+
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
2702
|
+
|
|
2703
|
+
// Small debounce
|
|
2704
|
+
if (typeSoundTimeout) return;
|
|
2705
|
+
typeSoundTimeout = setTimeout(() => { typeSoundTimeout = null; }, 50);
|
|
2706
|
+
|
|
2707
|
+
const bufferSize = audioCtx.sampleRate * 0.02;
|
|
2708
|
+
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
|
2709
|
+
const data = buffer.getChannelData(0);
|
|
2710
|
+
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
|
|
2711
|
+
|
|
2712
|
+
const noise = audioCtx.createBufferSource();
|
|
2713
|
+
noise.buffer = buffer;
|
|
2714
|
+
const noiseFilter = audioCtx.createBiquadFilter();
|
|
2715
|
+
noiseFilter.type = 'bandpass';
|
|
2716
|
+
noiseFilter.frequency.value = 3000 + (Math.random() * 1000);
|
|
2717
|
+
noiseFilter.Q.value = 1;
|
|
2718
|
+
const noiseGain = audioCtx.createGain();
|
|
2719
|
+
noiseGain.gain.setValueAtTime(0.04, audioCtx.currentTime);
|
|
2720
|
+
noiseGain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.01);
|
|
2721
|
+
noise.connect(noiseFilter);
|
|
2722
|
+
noiseFilter.connect(noiseGain);
|
|
2723
|
+
noiseGain.connect(audioCtx.destination);
|
|
2724
|
+
|
|
2725
|
+
const osc = audioCtx.createOscillator();
|
|
2726
|
+
const gain = audioCtx.createGain();
|
|
2727
|
+
osc.type = 'sine';
|
|
2728
|
+
osc.frequency.setValueAtTime(150 + (Math.random() * 50), audioCtx.currentTime);
|
|
2729
|
+
gain.gain.setValueAtTime(0.03, audioCtx.currentTime);
|
|
2730
|
+
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.02);
|
|
2731
|
+
osc.connect(gain);
|
|
2732
|
+
gain.connect(audioCtx.destination);
|
|
2733
|
+
|
|
2734
|
+
noise.start();
|
|
2735
|
+
osc.start();
|
|
2736
|
+
noise.stop(audioCtx.currentTime + 0.02);
|
|
2737
|
+
osc.stop(audioCtx.currentTime + 0.02);
|
|
2738
|
+
};
|
|
2739
|
+
|
|
2740
|
+
function playClickSound() {
|
|
2741
|
+
if (window.isMuted) return;
|
|
2742
|
+
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
2743
|
+
|
|
2744
|
+
const osc = audioCtx.createOscillator();
|
|
2745
|
+
const gainNode = audioCtx.createGain();
|
|
2746
|
+
osc.connect(gainNode);
|
|
2747
|
+
gainNode.connect(audioCtx.destination);
|
|
2748
|
+
osc.type = 'sine';
|
|
2749
|
+
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
|
|
2750
|
+
gainNode.gain.setValueAtTime(0.08, audioCtx.currentTime);
|
|
2751
|
+
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.015);
|
|
2752
|
+
osc.start();
|
|
2753
|
+
osc.stop(audioCtx.currentTime + 0.015);
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// --- App Preferences State ---
|
|
2757
|
+
window.isMuted = localStorage.getItem('app_is_muted') === 'true';
|
|
2758
|
+
window.areAnimationsEnabled = localStorage.getItem('app_animations_enabled') !== 'false'; // Default true
|
|
2759
|
+
|
|
2760
|
+
window.playMechClick = function(isReverse = false) {
|
|
2761
|
+
if (window.isMuted || !window.areAnimationsEnabled) return;
|
|
2762
|
+
if (audioCtx.state === 'suspended') audioCtx.resume();
|
|
2763
|
+
const now = audioCtx.currentTime;
|
|
2764
|
+
|
|
2765
|
+
const noiseGainVal = isReverse ? 0.015 : 0.02;
|
|
2766
|
+
const pitchFreq = isReverse ? 100 : 150;
|
|
2767
|
+
|
|
2768
|
+
// High-frequency noise burst
|
|
2769
|
+
const bufferSize = audioCtx.sampleRate * 0.01;
|
|
2770
|
+
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
|
|
2771
|
+
const data = buffer.getChannelData(0);
|
|
2772
|
+
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
|
|
2773
|
+
|
|
2774
|
+
const noise = audioCtx.createBufferSource();
|
|
2775
|
+
noise.buffer = buffer;
|
|
2776
|
+
const filter = audioCtx.createBiquadFilter();
|
|
2777
|
+
filter.type = 'highpass';
|
|
2778
|
+
filter.frequency.value = isReverse ? 3000 : 5000;
|
|
2779
|
+
const gain = audioCtx.createGain();
|
|
2780
|
+
gain.gain.setValueAtTime(noiseGainVal, now);
|
|
2781
|
+
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.01);
|
|
2782
|
+
|
|
2783
|
+
noise.connect(filter);
|
|
2784
|
+
filter.connect(gain);
|
|
2785
|
+
gain.connect(audioCtx.destination);
|
|
2786
|
+
noise.start();
|
|
2787
|
+
noise.stop(now + 0.01);
|
|
2788
|
+
|
|
2789
|
+
// Low-frequency "thud"
|
|
2790
|
+
const osc = audioCtx.createOscillator();
|
|
2791
|
+
const oscGain = audioCtx.createGain();
|
|
2792
|
+
osc.type = 'triangle';
|
|
2793
|
+
osc.frequency.setValueAtTime(pitchFreq, now);
|
|
2794
|
+
oscGain.gain.setValueAtTime(0.01, now);
|
|
2795
|
+
oscGain.gain.exponentialRampToValueAtTime(0.001, now + 0.02);
|
|
2796
|
+
osc.connect(oscGain);
|
|
2797
|
+
oscGain.connect(audioCtx.destination);
|
|
2798
|
+
osc.start();
|
|
2799
|
+
osc.stop(now + 0.02);
|
|
2800
|
+
};
|
|
2801
|
+
|
|
2802
|
+
|
|
2803
|
+
|
|
2804
|
+
const notificationContainer = document.getElementById('notification-container');
|
|
2805
|
+
function showNotification(message, iconClass = 'fa-solid fa-info-circle', type = 'info') {
|
|
2806
|
+
if (!notificationContainer) return;
|
|
2807
|
+
while (notificationContainer.children.length >= 3) {
|
|
2808
|
+
notificationContainer.removeChild(notificationContainer.firstChild);
|
|
2809
|
+
}
|
|
2810
|
+
const toast = document.createElement('div');
|
|
2811
|
+
toast.className = 'notification-toast';
|
|
2812
|
+
toast.innerHTML = `<i class="${iconClass} notification-icon ${type}"></i><span>${message}</span>`;
|
|
2813
|
+
notificationContainer.appendChild(toast);
|
|
2814
|
+
requestAnimationFrame(() => {
|
|
2815
|
+
toast.classList.add('show');
|
|
2816
|
+
playClickSound();
|
|
2817
|
+
});
|
|
2818
|
+
setTimeout(() => {
|
|
2819
|
+
toast.classList.remove('show');
|
|
2820
|
+
setTimeout(() => { if (toast.parentElement) toast.remove(); }, 300);
|
|
2821
|
+
}, 3000);
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// Event Listeners
|
|
2825
|
+
document.addEventListener('click', (e) => {
|
|
2826
|
+
const target = e.target.closest('button, .nav-tab, .zone-item, .btn-card-action, a, .icon-btn');
|
|
2827
|
+
if (target) playClickSound();
|
|
2828
|
+
});
|
|
2829
|
+
|
|
2830
|
+
document.addEventListener('keydown', (e) => {
|
|
2831
|
+
const target = e.target.closest('input, textarea') || e.target.isContentEditable;
|
|
2832
|
+
if (target && !e.repeat) window.playTypeSound();
|
|
2833
|
+
});
|
|
2834
|
+
|
|
2835
|
+
// Copy/Paste Feedback
|
|
2836
|
+
document.addEventListener('copy', () => {
|
|
2837
|
+
if (window.getSelection().toString().length > 0) showNotification('Copied to clipboard', 'fa-solid fa-copy', 'success');
|
|
2838
|
+
});
|
|
2839
|
+
document.addEventListener('paste', (e) => {
|
|
2840
|
+
if (e.target.closest('input, textarea')) showNotification('Pasted from clipboard', 'fa-solid fa-paste', 'info');
|
|
2841
|
+
});
|
|
2842
|
+
|
|
2843
|
+
// --- PRELOADER REMOVAL ---
|
|
2844
|
+
window.addEventListener('load', () => {
|
|
2845
|
+
const preloader = document.getElementById('app-preloader');
|
|
2846
|
+
const bar = document.getElementById('preloader-bar');
|
|
2847
|
+
|
|
2848
|
+
if (preloader && bar) {
|
|
2849
|
+
// Animate bar to 100%
|
|
2850
|
+
setTimeout(() => {
|
|
2851
|
+
bar.style.width = '100%';
|
|
2852
|
+
|
|
2853
|
+
// Then fade out
|
|
2854
|
+
setTimeout(() => {
|
|
2855
|
+
preloader.classList.add('fade-out');
|
|
2856
|
+
setTimeout(() => {
|
|
2857
|
+
preloader.remove();
|
|
2858
|
+
}, 500);
|
|
2859
|
+
}, 600); // Wait for bar animation
|
|
2860
|
+
}, 100);
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
</script>
|
|
3272
2864
|
</body>
|
|
3273
2865
|
</html>
|