mlv2boii 1.0.6 → 1.0.8
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/index.html +215 -251
- package/package.json +1 -1
- package/savedconfig.txt +10 -0
- package/holdw.py +0 -21
package/index.html
CHANGED
|
@@ -1,39 +1,47 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
6
|
<title>mlv2</title>
|
|
7
|
-
<meta name="description" content="mlv2
|
|
7
|
+
<meta name="description" content="mlv2: a small, fast library of games." />
|
|
8
8
|
<meta name="color-scheme" content="dark" />
|
|
9
|
-
<meta name="theme-color" content="#
|
|
10
|
-
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='
|
|
9
|
+
<meta name="theme-color" content="#000000" />
|
|
10
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23000'/%3E%3Ccircle cx='16' cy='16' r='6' fill='%23e8142f'/%3E%3C/svg%3E" />
|
|
11
11
|
|
|
12
|
-
<!-- Fonts -->
|
|
12
|
+
<!-- Fonts: SF Pro on Apple devices (system stack), Inter everywhere else -->
|
|
13
13
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
14
14
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
15
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
15
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
16
16
|
|
|
17
17
|
<!-- Tailwind CSS v4 (Play CDN, mirrored on jsDelivr) -->
|
|
18
18
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
19
19
|
<style type="text/tailwindcss">
|
|
20
|
+
/*
|
|
21
|
+
Design language
|
|
22
|
+
Mood: quiet, precise, confident. Airy. Ancestor: Vercel/Resend black, Apple HIG habits.
|
|
23
|
+
Color: void black canvas, Apple grays for text, ONE red for tint (selection, primary action, focus).
|
|
24
|
+
Type: one family. 17/19/21 for reading, 13 for labels, 44-72 for the title.
|
|
25
|
+
Shape: 10px controls, 12px groups, 28px sheets (concentric: 12 + 16 padding), pills for chips,
|
|
26
|
+
22.37% on game icons (the iOS app icon ratio).
|
|
27
|
+
Motion: 150-300ms ease-out on interaction, one entrance, all of it off under reduced-motion.
|
|
28
|
+
*/
|
|
20
29
|
@theme {
|
|
21
|
-
--font-
|
|
22
|
-
--font-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
--color-ink-
|
|
26
|
-
--color-ink-
|
|
27
|
-
--color-ink-
|
|
28
|
-
--color-ink-
|
|
29
|
-
--color-ink-
|
|
30
|
-
--color-ink-
|
|
31
|
-
--color-ink-
|
|
32
|
-
--color-ink-
|
|
33
|
-
--color-ink-
|
|
34
|
-
--color-ink-
|
|
35
|
-
--color-ink-
|
|
36
|
-
--color-ink-50: #f6f5f7;
|
|
30
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
31
|
+
--font-mono: ui-monospace, "SF Mono", Menlo, monospace;
|
|
32
|
+
|
|
33
|
+
--color-ink-950: #000000;
|
|
34
|
+
--color-ink-900: #0a0a0b;
|
|
35
|
+
--color-ink-850: #101012;
|
|
36
|
+
--color-ink-800: #161618;
|
|
37
|
+
--color-ink-700: #232326;
|
|
38
|
+
--color-ink-600: #2c2c30;
|
|
39
|
+
--color-ink-500: #48484c;
|
|
40
|
+
--color-ink-400: #76767b;
|
|
41
|
+
--color-ink-300: #a1a1a6;
|
|
42
|
+
--color-ink-200: #c7c7cc;
|
|
43
|
+
--color-ink-100: #e5e5ea;
|
|
44
|
+
--color-ink-50: #f5f5f7;
|
|
37
45
|
|
|
38
46
|
--color-red-700: #8f0e20;
|
|
39
47
|
--color-red-600: #c11228;
|
|
@@ -41,166 +49,159 @@
|
|
|
41
49
|
--color-red-400: #ff4d61;
|
|
42
50
|
--color-red-300: #ff8b96;
|
|
43
51
|
}
|
|
52
|
+
|
|
53
|
+
@layer components {
|
|
54
|
+
.btn { @apply inline-flex h-11 items-center justify-center rounded-[10px] px-4 text-[15px] font-medium transition-[background-color,filter,transform] duration-200 active:scale-[0.98] disabled:opacity-50 sm:h-9 sm:text-[14px]; }
|
|
55
|
+
.btn-primary { @apply bg-red-500 text-white hover:brightness-110; }
|
|
56
|
+
.btn-ghost { @apply bg-white/10 text-ink-50 hover:bg-white/15; }
|
|
57
|
+
.icon-btn { @apply grid h-11 w-11 shrink-0 place-items-center rounded-full bg-white/10 text-ink-200 transition-colors duration-200 hover:bg-white/15 hover:text-white sm:h-9 sm:w-9; }
|
|
58
|
+
.field { @apply h-11 w-full rounded-[10px] bg-white/[0.07] px-3 text-[16px] text-ink-50 ring-1 ring-inset ring-white/10 transition-shadow duration-200 placeholder:text-ink-400 focus:ring-2 focus:ring-red-500 sm:h-9 sm:text-[14px]; }
|
|
59
|
+
.chip { @apply h-10 shrink-0 whitespace-nowrap rounded-full px-4 text-[14px] font-medium transition-colors duration-200 sm:h-9; }
|
|
60
|
+
}
|
|
44
61
|
</style>
|
|
45
62
|
|
|
46
63
|
<style>
|
|
47
|
-
|
|
48
|
-
Base layer — kept outside the Tailwind @theme block so it works
|
|
49
|
-
regardless of Tailwind's preflight timing.
|
|
50
|
-
----------------------------------------------------------------*/
|
|
51
|
-
html, body { background: var(--color-ink-950); }
|
|
64
|
+
html, body { background: #000; }
|
|
52
65
|
body { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
|
|
53
|
-
::selection { background: rgba(232,20,47,0.
|
|
66
|
+
::selection { background: rgba(232,20,47,0.4); color: #fff; }
|
|
67
|
+
input { caret-color: var(--color-red-400); }
|
|
54
68
|
|
|
55
|
-
/*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*::-webkit-scrollbar-thumb:hover { background: #3a3a42; }
|
|
69
|
+
/* keyboard focus: one red ring everywhere */
|
|
70
|
+
:focus-visible { outline: 2px solid var(--color-red-400); outline-offset: 2px; }
|
|
71
|
+
input:focus-visible { outline: none; }
|
|
72
|
+
.tile:focus-visible { outline: none; }
|
|
73
|
+
.tile:focus-visible .tile-icon { outline: 2px solid var(--color-red-400); outline-offset: 3px; }
|
|
61
74
|
|
|
62
|
-
/*
|
|
63
|
-
.
|
|
64
|
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
|
|
75
|
+
/* safe areas (viewport-fit=cover) */
|
|
76
|
+
.page-x { padding-left: max(1.25rem, env(safe-area-inset-left)); padding-right: max(1.25rem, env(safe-area-inset-right)); }
|
|
65
77
|
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
/* game icons: 10% edge so dark art never melts into the canvas */
|
|
79
|
+
.tile-img { outline: 1px solid rgba(255,255,255,0.1); outline-offset: -1px; }
|
|
68
80
|
|
|
69
|
-
/* skeleton shimmer */
|
|
70
|
-
@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
|
|
71
|
-
.skel { background: linear-gradient(100deg, #18181c 30%, #202024 45%, #18181c 60%); background-size: 200% 100%; animation: shimmer 1.6s ease-in-out infinite; }
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
/* Liquid glass: translucent fill, blur + saturation, bright top edge, light rim.
|
|
83
|
+
Used only on floating layers (header, sheets, toasts, controls). */
|
|
84
|
+
.glass {
|
|
85
|
+
background: rgba(255,255,255,0.06);
|
|
86
|
+
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
|
87
|
+
backdrop-filter: blur(24px) saturate(180%);
|
|
88
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.22), inset 0 -1px 0 rgba(255,255,255,0.04), 0 10px 32px rgba(0,0,0,0.45);
|
|
80
89
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
.glass-sheet {
|
|
91
|
+
background: rgba(28,28,31,0.55);
|
|
92
|
+
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
|
93
|
+
backdrop-filter: blur(40px) saturate(180%);
|
|
84
94
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
background-color: #000000;
|
|
91
|
-
z-index: 99999;
|
|
92
|
-
display: flex;
|
|
93
|
-
transition: opacity 0.4s ease, visibility 0.4s ease;
|
|
95
|
+
.glass::before {
|
|
96
|
+
content: ""; position: absolute; inset: 0; border-radius: inherit; padding: 1px; pointer-events: none;
|
|
97
|
+
background: linear-gradient(135deg, rgba(255,255,255,0.4), rgba(255,255,255,0.05) 40%, rgba(255,255,255,0.05) 60%, rgba(255,255,255,0.22));
|
|
98
|
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
99
|
+
-webkit-mask-composite: xor; mask-composite: exclude;
|
|
94
100
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
/* small controls: same edge light, no blur (nothing behind them to blur) */
|
|
102
|
+
.glass-lite { background: rgba(255,255,255,0.07); box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), inset 0 0 0 1px rgba(255,255,255,0.08); }
|
|
103
|
+
.glass-lite:hover { background: rgba(255,255,255,0.12); }
|
|
104
|
+
.chip-on { background: rgba(232,20,47,0.92); box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), inset 0 0 0 1px rgba(255,255,255,0.12); }
|
|
105
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
106
|
+
.glass { background: rgba(22,22,24,0.94); } .glass-sheet { background: rgba(18,18,20,0.97); }
|
|
107
|
+
}
|
|
108
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
109
|
+
.glass, .glass-sheet { -webkit-backdrop-filter: none; backdrop-filter: none; background: #161618; }
|
|
99
110
|
}
|
|
100
|
-
</style>
|
|
101
|
-
</head>
|
|
102
111
|
|
|
103
|
-
|
|
112
|
+
.no-scrollbar { scrollbar-width: none; }
|
|
113
|
+
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
114
|
+
* { scrollbar-width: thin; scrollbar-color: #2c2c30 transparent; }
|
|
115
|
+
*::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
116
|
+
*::-webkit-scrollbar-thumb { background: #2c2c30; border-radius: 999px; }
|
|
104
117
|
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
/* the single page-load moment: title and subline rise in */
|
|
119
|
+
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
|
120
|
+
.rise { animation: rise 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; animation-delay: var(--d, 0s); }
|
|
121
|
+
@media (prefers-reduced-motion: reduce) { .rise { animation: none; } }
|
|
107
122
|
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
</style>
|
|
124
|
+
</head>
|
|
125
|
+
|
|
126
|
+
<body class="min-h-screen bg-ink-950 font-sans text-ink-50 antialiased">
|
|
110
127
|
|
|
111
128
|
<!-- ================= HEADER ================= -->
|
|
112
|
-
<header class="sticky top-0 z-40
|
|
113
|
-
<div class="mx-auto max-w-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</span>
|
|
119
|
-
<span class="font-display font-semibold tracking-tight text-[15px] leading-none">mlv2</span>
|
|
129
|
+
<header class="pointer-events-none sticky top-0 z-40 pt-[calc(env(safe-area-inset-top)+0.75rem)]">
|
|
130
|
+
<div class="page-x mx-auto max-w-5xl">
|
|
131
|
+
<div class="glass pointer-events-auto relative flex h-14 items-center gap-4 rounded-full pl-5 pr-2">
|
|
132
|
+
<a href="#" class="flex shrink-0 items-center gap-2" aria-label="mlv2 home">
|
|
133
|
+
<span class="h-2.5 w-2.5 rounded-full bg-red-500"></span>
|
|
134
|
+
<span class="text-[17px] font-semibold tracking-[-0.02em]">mlv2</span>
|
|
120
135
|
</a>
|
|
121
136
|
|
|
122
|
-
<div class="
|
|
123
|
-
<
|
|
124
|
-
<
|
|
125
|
-
class="
|
|
137
|
+
<div class="ml-auto flex min-w-0 flex-1 items-center justify-end gap-4">
|
|
138
|
+
<span id="count-badge" class="hidden shrink-0 text-[13px] tabular-nums text-ink-400 sm:block"><span id="count-num">0</span> games</span>
|
|
139
|
+
<div class="relative w-full max-w-[16rem]">
|
|
140
|
+
<svg class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-ink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
|
|
141
|
+
<input id="search" type="text" placeholder="Search games" aria-label="Search games" autocomplete="off" enterkeyhint="search" class="field rounded-full pl-9" />
|
|
142
|
+
</div>
|
|
126
143
|
</div>
|
|
127
|
-
|
|
128
|
-
<span id="count-badge" class="hidden sm:flex items-center gap-1.5 shrink-0 text-[11px] font-mono uppercase tracking-wider text-ink-400">
|
|
129
|
-
<span id="count-num">0</span> games
|
|
130
|
-
</span>
|
|
131
144
|
</div>
|
|
132
|
-
|
|
133
|
-
<div class="border-t border-ink-800/60">
|
|
134
|
-
<div id="category-bar" class="mx-auto flex max-w-5xl items-center justify-center gap-1.5 overflow-x-auto px-4 py-2.5 sm:px-6"></div>
|
|
135
145
|
</div>
|
|
136
146
|
</header>
|
|
137
147
|
|
|
138
148
|
<!-- ================= MAIN ================= -->
|
|
139
|
-
<main class="
|
|
140
|
-
|
|
141
|
-
<!-- =================
|
|
142
|
-
<section class="
|
|
143
|
-
<
|
|
144
|
-
<
|
|
145
|
-
<div class="relative">
|
|
146
|
-
<div class="mx-auto mb-5 flex w-fit items-center gap-2 rounded-full border border-red-500/25 bg-red-500/[0.07] px-3 py-1 text-[10px] font-mono uppercase tracking-[0.22em] text-red-300">
|
|
147
|
-
<span class="h-1.5 w-1.5 rounded-full bg-red-400 shadow-[0_0_12px_rgba(255,77,97,0.9)]"></span>
|
|
148
|
-
Play something
|
|
149
|
-
</div>
|
|
150
|
-
<h1 class="font-display text-4xl font-semibold tracking-[-0.05em] text-ink-50 sm:text-6xl">school sucks, <br/>games r <span class="text-red-400">better</span></h1>
|
|
151
|
-
<p class="mx-auto mt-4 max-w-md text-sm leading-6 text-ink-300 sm:text-[15px]">we're so back 🥹</p>
|
|
152
|
-
<div class="hero-line mx-auto mt-7 h-px w-24 bg-gradient-to-r from-transparent via-red-400 to-transparent"></div>
|
|
153
|
-
</div>
|
|
149
|
+
<main class="page-x mx-auto max-w-5xl pb-24">
|
|
150
|
+
|
|
151
|
+
<!-- ================= TITLE ================= -->
|
|
152
|
+
<section class="pb-10 pt-14 sm:pb-14 sm:pt-24">
|
|
153
|
+
<h1 class="rise text-balance text-[2.75rem] font-semibold leading-[1.04] tracking-[-0.04em] text-ink-50 sm:text-[4.5rem]">school sucks,<br/>games r better</h1>
|
|
154
|
+
<p class="rise mt-4 text-[19px] text-ink-300 sm:text-[21px]" style="--d: 0.08s">we're so back</p>
|
|
154
155
|
</section>
|
|
155
156
|
|
|
157
|
+
<!-- categories -->
|
|
158
|
+
<div id="category-bar" role="group" aria-label="Categories" class="no-scrollbar -mx-5 mb-8 flex items-center gap-2 overflow-x-auto px-5 sm:mx-0 sm:mb-10 sm:px-0"></div>
|
|
159
|
+
|
|
156
160
|
<!-- setup banner, shown only while CONFIG still has placeholder values -->
|
|
157
|
-
<div id="setup-banner" class="
|
|
158
|
-
<p class="text-[
|
|
159
|
-
<p class="mt-1 text-[
|
|
161
|
+
<div id="setup-banner" class="mb-8 hidden rounded-xl bg-ink-900 p-4 ring-1 ring-white/10">
|
|
162
|
+
<p class="text-[15px] font-semibold">Not connected yet</p>
|
|
163
|
+
<p class="mt-1 text-[14px] leading-relaxed text-ink-300">Open this file and fill in the <code class="font-mono text-ink-50">CONFIG</code> object near the bottom with your Appwrite endpoint, project, database, table and bucket IDs.</p>
|
|
160
164
|
</div>
|
|
161
165
|
|
|
162
166
|
<!-- skeleton loading state -->
|
|
163
|
-
<div id="skeleton-grid" class="grid grid-cols-
|
|
164
|
-
</div>
|
|
167
|
+
<div id="skeleton-grid" class="grid grid-cols-3 gap-x-4 gap-y-7 sm:grid-cols-4 sm:gap-x-5 sm:gap-y-8 md:grid-cols-5 lg:grid-cols-6"></div>
|
|
165
168
|
|
|
166
169
|
<!-- empty state -->
|
|
167
170
|
<div id="empty-state" class="hidden flex-col items-center justify-center py-24 text-center">
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
</div>
|
|
171
|
-
<p id="empty-title" class="font-display text-[15px] font-medium text-ink-100">No games yet</p>
|
|
172
|
-
<p id="empty-sub" class="mt-1.5 text-[13px] text-ink-400 max-w-xs">Games you add in Appwrite with a valid link will show up here automatically.</p>
|
|
171
|
+
<p id="empty-title" class="text-[19px] font-semibold">No games yet</p>
|
|
172
|
+
<p id="empty-sub" class="mt-2 max-w-xs text-pretty text-[15px] text-ink-400">Games you add in Appwrite with a valid link will show up here automatically.</p>
|
|
173
173
|
</div>
|
|
174
174
|
|
|
175
175
|
<!-- game grid -->
|
|
176
|
-
<div id="grid" class="hidden grid grid-cols-
|
|
176
|
+
<div id="grid" class="hidden grid grid-cols-3 gap-x-4 gap-y-7 sm:grid-cols-4 sm:gap-x-5 sm:gap-y-8 md:grid-cols-5 lg:grid-cols-6"></div>
|
|
177
177
|
</main>
|
|
178
178
|
|
|
179
179
|
<!-- ================= FOOTER ================= -->
|
|
180
|
-
<footer class="
|
|
181
|
-
<div class="mx-auto max-w-
|
|
182
|
-
<p
|
|
183
|
-
<button id="admin-trigger" class="
|
|
180
|
+
<footer class="border-t border-white/[0.08]">
|
|
181
|
+
<div class="page-x mx-auto flex max-w-5xl items-center justify-between py-4 text-[13px] text-ink-400">
|
|
182
|
+
<p>mlv2 © <span id="year"></span></p>
|
|
183
|
+
<button id="admin-trigger" class="-mr-3 rounded-lg px-3 py-2.5 transition-colors duration-200 hover:text-ink-50">Admin</button>
|
|
184
184
|
</div>
|
|
185
185
|
</footer>
|
|
186
186
|
|
|
187
187
|
<!-- ================= PLAYER MODAL ================= -->
|
|
188
188
|
<div id="player-modal" class="fixed inset-0 z-50 hidden">
|
|
189
|
-
<div class="absolute inset-0 bg-
|
|
190
|
-
<div id="player-panel" class="relative h-full w-full flex
|
|
191
|
-
<div class="
|
|
192
|
-
<div class="flex items-center gap-
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
189
|
+
<div class="absolute inset-0 bg-black" data-player-close></div>
|
|
190
|
+
<div id="player-panel" class="relative flex h-full w-full flex-col opacity-0">
|
|
191
|
+
<div class="border-b border-white/[0.08] bg-black pt-[env(safe-area-inset-top)]">
|
|
192
|
+
<div class="flex h-14 items-center justify-between gap-3 px-3 sm:px-4">
|
|
193
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
194
|
+
<img id="player-icon" class="h-8 w-8 rounded-[22.37%] object-cover" alt="" />
|
|
195
|
+
<p id="player-title" class="truncate text-[15px] font-semibold"></p>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
198
|
+
<a id="player-newtab" target="_blank" rel="noopener" title="Open in new tab" aria-label="Open in new tab" class="glass-lite icon-btn">
|
|
199
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><path d="M15 3h6v6"/><path d="M10 14 21 3"/></svg>
|
|
200
|
+
</a>
|
|
201
|
+
<button id="player-close" title="Close" aria-label="Close" class="glass-lite icon-btn">
|
|
202
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
204
205
|
</div>
|
|
205
206
|
</div>
|
|
206
207
|
<div class="relative flex-1 bg-black">
|
|
@@ -211,40 +212,40 @@
|
|
|
211
212
|
|
|
212
213
|
<!-- ================= ADMIN: PIN GATE ================= -->
|
|
213
214
|
<div id="admin-login-modal" class="fixed inset-0 z-50 hidden">
|
|
214
|
-
<div class="absolute inset-0 bg-
|
|
215
|
+
<div class="absolute inset-0 bg-black/50 backdrop-blur-md" data-admin-login-close></div>
|
|
215
216
|
<div class="absolute inset-0 flex items-center justify-center p-4">
|
|
216
|
-
<form id="admin-login-form" class="w-full max-w-[
|
|
217
|
-
<p class="
|
|
218
|
-
<p class="mt-1 text-[
|
|
217
|
+
<form id="admin-login-form" class="glass glass-sheet relative w-full max-w-[340px] rounded-[28px] p-6 opacity-0" data-fx>
|
|
218
|
+
<p class="text-[19px] font-semibold">Enter PIN</p>
|
|
219
|
+
<p class="mt-1.5 text-pretty text-[14px] text-ink-400">This unlocks the admin screen on this device. Write access is controlled by your Appwrite permissions.</p>
|
|
219
220
|
|
|
220
|
-
<div id="pin-boxes" class="mt-
|
|
221
|
+
<div id="pin-boxes" class="mt-5 grid grid-cols-6 gap-2">
|
|
221
222
|
<!-- 6 single-digit inputs, filled in by JS -->
|
|
222
223
|
</div>
|
|
223
224
|
<input id="admin-pin" type="hidden" />
|
|
224
225
|
|
|
225
|
-
<p id="admin-login-error" class="
|
|
226
|
+
<p id="admin-login-error" class="mt-3 hidden text-[14px] text-red-400" role="alert"></p>
|
|
226
227
|
|
|
227
|
-
<button type="submit" id="admin-login-submit" class="mt-
|
|
228
|
-
<button type="button" data-admin-login-close class="mt-2 w-full
|
|
228
|
+
<button type="submit" id="admin-login-submit" class="btn btn-primary mt-5 w-full">Unlock</button>
|
|
229
|
+
<button type="button" data-admin-login-close class="btn btn-ghost mt-2 w-full">Cancel</button>
|
|
229
230
|
</form>
|
|
230
231
|
</div>
|
|
231
232
|
</div>
|
|
232
233
|
|
|
233
234
|
<!-- ================= ADMIN: PANEL ================= -->
|
|
234
235
|
<div id="admin-panel" class="fixed inset-0 z-50 hidden">
|
|
235
|
-
<div class="absolute inset-0 bg-
|
|
236
|
-
<div class="absolute inset-0 flex items-start
|
|
237
|
-
<div id="admin-panel-card" class="relative
|
|
236
|
+
<div class="absolute inset-0 bg-black/50 backdrop-blur-md" data-admin-panel-close></div>
|
|
237
|
+
<div class="absolute inset-0 flex items-start justify-center p-0 sm:items-center sm:p-4">
|
|
238
|
+
<div id="admin-panel-card" class="glass glass-sheet relative flex h-full w-full flex-col opacity-0 sm:h-[85vh] sm:max-w-2xl sm:rounded-[32px]" data-fx>
|
|
238
239
|
|
|
239
|
-
<div class="flex items-center justify-between
|
|
240
|
+
<div class="flex h-14 shrink-0 items-center justify-between border-b border-white/[0.08] px-5 pt-[env(safe-area-inset-top)] box-content">
|
|
240
241
|
<div>
|
|
241
|
-
<p class="
|
|
242
|
-
<p class="mt-1 text-[
|
|
242
|
+
<p class="text-[17px] font-semibold leading-none">Admin</p>
|
|
243
|
+
<p class="mt-1 text-[13px] text-ink-400">Unlocked with PIN</p>
|
|
243
244
|
</div>
|
|
244
|
-
<div class="flex items-center gap-
|
|
245
|
-
<button id="admin-logout" class="
|
|
246
|
-
<button data-admin-panel-close class="
|
|
247
|
-
<svg class="h-
|
|
245
|
+
<div class="flex items-center gap-2">
|
|
246
|
+
<button id="admin-logout" class="btn btn-ghost">Lock</button>
|
|
247
|
+
<button data-admin-panel-close aria-label="Close" class="glass-lite icon-btn">
|
|
248
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
|
248
249
|
</button>
|
|
249
250
|
</div>
|
|
250
251
|
</div>
|
|
@@ -252,36 +253,38 @@
|
|
|
252
253
|
<div class="flex-1 overflow-y-auto px-5 py-5">
|
|
253
254
|
|
|
254
255
|
<!-- add game form -->
|
|
255
|
-
<form id="add-game-form" class="rounded-xl
|
|
256
|
-
<p class="text-[
|
|
257
|
-
<div class="mt-
|
|
256
|
+
<form id="add-game-form" class="rounded-xl p-4 ring-1 ring-white/10">
|
|
257
|
+
<p class="text-[15px] font-semibold">Add game</p>
|
|
258
|
+
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
258
259
|
<div>
|
|
259
|
-
<label class="text-[
|
|
260
|
-
<input id="new-name" required class="mt-1
|
|
260
|
+
<label for="new-name" class="text-[13px] text-ink-300">Name</label>
|
|
261
|
+
<input id="new-name" required class="field mt-1.5" />
|
|
261
262
|
</div>
|
|
262
263
|
<div>
|
|
263
|
-
<label class="text-[
|
|
264
|
-
<input id="new-category" class="mt-1
|
|
264
|
+
<label for="new-category" class="text-[13px] text-ink-300">Category (optional)</label>
|
|
265
|
+
<input id="new-category" class="field mt-1.5" />
|
|
265
266
|
</div>
|
|
266
267
|
<div class="sm:col-span-2">
|
|
267
|
-
<label class="text-[
|
|
268
|
-
<input id="new-url" required type="url" placeholder="https
|
|
268
|
+
<label for="new-url" class="text-[13px] text-ink-300">Game URL</label>
|
|
269
|
+
<input id="new-url" required type="url" placeholder="https://" class="field mt-1.5" />
|
|
269
270
|
</div>
|
|
270
271
|
<div>
|
|
271
|
-
<label class="text-[
|
|
272
|
-
<input id="new-icon-file" type="file" accept="image/*" class="mt-
|
|
272
|
+
<label for="new-icon-file" class="text-[13px] text-ink-300">Icon upload</label>
|
|
273
|
+
<input id="new-icon-file" type="file" accept="image/*" class="mt-2 w-full text-[13px] text-ink-400 file:mr-3 file:rounded-full file:border-0 file:bg-white/10 file:px-3.5 file:py-1.5 file:text-[13px] file:font-medium file:text-ink-50" />
|
|
273
274
|
</div>
|
|
274
275
|
<div>
|
|
275
|
-
<label class="text-[
|
|
276
|
-
<input id="new-icon-url" type="url" placeholder="https
|
|
276
|
+
<label for="new-icon-url" class="text-[13px] text-ink-300">Or external icon URL</label>
|
|
277
|
+
<input id="new-icon-url" type="url" placeholder="https://" class="field mt-1.5" />
|
|
277
278
|
</div>
|
|
278
279
|
</div>
|
|
279
|
-
<
|
|
280
|
-
|
|
280
|
+
<div class="mt-5 flex items-center gap-3">
|
|
281
|
+
<button type="submit" id="add-game-submit" class="btn btn-primary">Add game</button>
|
|
282
|
+
<span id="add-game-status" class="text-[13px] text-ink-400"></span>
|
|
283
|
+
</div>
|
|
281
284
|
</form>
|
|
282
285
|
|
|
283
286
|
<!-- existing games -->
|
|
284
|
-
<p class="
|
|
287
|
+
<p class="mb-3 mt-8 text-[15px] font-semibold">Games</p>
|
|
285
288
|
<div id="admin-list" class="space-y-2"></div>
|
|
286
289
|
</div>
|
|
287
290
|
</div>
|
|
@@ -289,51 +292,7 @@
|
|
|
289
292
|
</div>
|
|
290
293
|
|
|
291
294
|
<!-- ================= TOASTS ================= -->
|
|
292
|
-
<div id="toast-root" class="fixed bottom-4 right-4 z-[60] flex flex-col gap-2
|
|
293
|
-
|
|
294
|
-
<!-- Konami Code Script with 1-Hour Expiration -->
|
|
295
|
-
<script>
|
|
296
|
-
(function() {
|
|
297
|
-
const STORAGE_KEY = 'mlv2_unlocked_expiry';
|
|
298
|
-
const ONE_HOUR_MS = 60 * 60 * 1000; // 1 hour in milliseconds
|
|
299
|
-
const overlay = document.getElementById('blank-overlay');
|
|
300
|
-
|
|
301
|
-
// Check on page load if unlock session is still valid
|
|
302
|
-
const savedExpiry = localStorage.getItem(STORAGE_KEY);
|
|
303
|
-
if (savedExpiry && Date.now() < parseInt(savedExpiry, 10)) {
|
|
304
|
-
overlay.classList.add('unlocked');
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const konamiCode = [
|
|
308
|
-
'ArrowUp', 'ArrowUp',
|
|
309
|
-
'ArrowDown', 'ArrowDown',
|
|
310
|
-
'ArrowLeft', 'ArrowRight',
|
|
311
|
-
'ArrowLeft', 'ArrowRight',
|
|
312
|
-
'b', 'a', 'Enter'
|
|
313
|
-
];
|
|
314
|
-
let konamiIndex = 0;
|
|
315
|
-
|
|
316
|
-
document.addEventListener('keydown', (e) => {
|
|
317
|
-
const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
318
|
-
const targetKey = konamiCode[konamiIndex].length === 1
|
|
319
|
-
? konamiCode[konamiIndex].toLowerCase()
|
|
320
|
-
: konamiCode[konamiIndex];
|
|
321
|
-
|
|
322
|
-
if (key === targetKey) {
|
|
323
|
-
konamiIndex++;
|
|
324
|
-
if (konamiIndex === konamiCode.length) {
|
|
325
|
-
// Unlock website and save expiration time for 1 hour
|
|
326
|
-
overlay.classList.add('unlocked');
|
|
327
|
-
const expiryTime = Date.now() + ONE_HOUR_MS;
|
|
328
|
-
localStorage.setItem(STORAGE_KEY, expiryTime.toString());
|
|
329
|
-
konamiIndex = 0;
|
|
330
|
-
}
|
|
331
|
-
} else {
|
|
332
|
-
konamiIndex = 0;
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
})();
|
|
336
|
-
</script>
|
|
295
|
+
<div id="toast-root" class="fixed bottom-4 right-4 z-[60] flex flex-col items-end gap-2 pb-[env(safe-area-inset-bottom)]"></div>
|
|
337
296
|
|
|
338
297
|
<script src="https://cdn.jsdelivr.net/npm/appwrite@26.2.0"></script>
|
|
339
298
|
<script type="module">
|
|
@@ -361,7 +320,9 @@ const isConfigured = () =>
|
|
|
361
320
|
!CONFIG.projectId.includes("YOUR_") &&
|
|
362
321
|
!CONFIG.databaseId.includes("YOUR_");
|
|
363
322
|
|
|
323
|
+
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
364
324
|
function fx(targets, keyframes, options = {}) {
|
|
325
|
+
if (reduceMotion) options = { ...options, duration: 0, delay: 0 };
|
|
365
326
|
return animate(targets, keyframes, options);
|
|
366
327
|
}
|
|
367
328
|
|
|
@@ -386,8 +347,8 @@ function iconSrcFor(game) {
|
|
|
386
347
|
function monogramSrc(name) {
|
|
387
348
|
const initials = (name || "?").trim().slice(0, 2).toUpperCase();
|
|
388
349
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
389
|
-
<rect width="100" height="100" fill="#
|
|
390
|
-
<text x="50" y="58" font-family="
|
|
350
|
+
<rect width="100" height="100" fill="#161618"/>
|
|
351
|
+
<text x="50" y="58" font-family="-apple-system, Inter, sans-serif" font-size="34" font-weight="600" fill="#e8142f" text-anchor="middle">${initials}</text>
|
|
391
352
|
</svg>`;
|
|
392
353
|
return "data:image/svg+xml;utf8," + encodeURIComponent(svg);
|
|
393
354
|
}
|
|
@@ -399,20 +360,20 @@ function escapeHtml(s) {
|
|
|
399
360
|
function toast(message, tone = "default") {
|
|
400
361
|
const root = el("toast-root");
|
|
401
362
|
const node = document.createElement("div");
|
|
402
|
-
const toneClasses = tone === "error" ? "
|
|
403
|
-
node.className = `rounded-
|
|
363
|
+
const toneClasses = tone === "error" ? "text-red-300" : "text-ink-50";
|
|
364
|
+
node.className = `glass glass-sheet relative rounded-full ${toneClasses} px-4 py-2.5 text-[14px] opacity-0`;
|
|
404
365
|
node.textContent = message;
|
|
405
366
|
root.appendChild(node);
|
|
406
|
-
fx(node, { opacity: [0, 1], y: [8, 0] }, { duration: 0.25,
|
|
367
|
+
fx(node, { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, ease: "easeOut" });
|
|
407
368
|
setTimeout(() => {
|
|
408
|
-
fx(node, { opacity: [1, 0], y: [0, 6] }, { duration: 0.2,
|
|
369
|
+
fx(node, { opacity: [1, 0], y: [0, 6] }, { duration: 0.2, ease: "easeIn" }).finished.then(() => node.remove());
|
|
409
370
|
}, 2800);
|
|
410
371
|
}
|
|
411
372
|
|
|
412
373
|
/* -------------------------- render: skeleton -------------------------- */
|
|
413
374
|
function renderSkeleton() {
|
|
414
|
-
el("skeleton-grid").innerHTML = Array.from({ length:
|
|
415
|
-
.map(() => `<div class="
|
|
375
|
+
el("skeleton-grid").innerHTML = Array.from({ length: 12 })
|
|
376
|
+
.map(() => `<div class="animate-pulse motion-reduce:animate-none"><div class="aspect-square rounded-[22.37%] bg-ink-800"></div><div class="mx-auto mt-3 h-3 w-2/3 rounded-full bg-ink-800"></div></div>`)
|
|
416
377
|
.join("");
|
|
417
378
|
}
|
|
418
379
|
renderSkeleton();
|
|
@@ -432,8 +393,8 @@ function renderCategoryBar() {
|
|
|
432
393
|
bar.innerHTML = cats
|
|
433
394
|
.map((c) => {
|
|
434
395
|
const active = c === activeCategory;
|
|
435
|
-
return `<button data-cat="${escapeHtml(c)}" class="
|
|
436
|
-
active ? "
|
|
396
|
+
return `<button data-cat="${escapeHtml(c)}" aria-pressed="${active}" class="chip ${
|
|
397
|
+
active ? "chip-on text-white" : "glass-lite text-ink-300 hover:text-ink-50"
|
|
437
398
|
}">${escapeHtml(c)}</button>`;
|
|
438
399
|
})
|
|
439
400
|
.join("");
|
|
@@ -446,6 +407,7 @@ function renderCategoryBar() {
|
|
|
446
407
|
);
|
|
447
408
|
}
|
|
448
409
|
|
|
410
|
+
let gridShown = false;
|
|
449
411
|
function renderGrid() {
|
|
450
412
|
const grid = el("grid");
|
|
451
413
|
const games = currentFiltered();
|
|
@@ -473,13 +435,12 @@ function renderGrid() {
|
|
|
473
435
|
grid.innerHTML = games
|
|
474
436
|
.map((g) => {
|
|
475
437
|
const src = iconSrcFor(g) || monogramSrc(g.name);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
</div>
|
|
438
|
+
return `<button data-fx data-id="${g.$id}" class="tile group block w-full text-left opacity-0">
|
|
439
|
+
<span class="tile-icon block aspect-square overflow-hidden rounded-[22.37%] bg-ink-800 transition-transform duration-300 ease-out group-hover:scale-[1.04] group-active:scale-[0.97]">
|
|
440
|
+
<img src="${src}" alt="" loading="lazy" class="tile-img h-full w-full object-cover" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
|
|
441
|
+
</span>
|
|
442
|
+
<span class="mt-2.5 block truncate text-center text-[13px] font-medium leading-tight text-ink-50">${escapeHtml(g.name)}</span>
|
|
443
|
+
${g.category ? `<span class="mt-0.5 block truncate text-center text-[12px] text-ink-400">${escapeHtml(g.category)}</span>` : ""}
|
|
483
444
|
</button>`;
|
|
484
445
|
})
|
|
485
446
|
.join("");
|
|
@@ -488,7 +449,10 @@ function renderGrid() {
|
|
|
488
449
|
card.addEventListener("click", () => openPlayer(games.find((g) => g.$id === card.dataset.id)))
|
|
489
450
|
);
|
|
490
451
|
|
|
491
|
-
|
|
452
|
+
// full entrance once; later filter/search changes only fade
|
|
453
|
+
fx("#grid [data-fx]", { opacity: [0, 1], y: gridShown ? 0 : [8, 0] },
|
|
454
|
+
{ duration: gridShown ? 0.18 : 0.5, delay: gridShown ? 0 : stagger(0.03), ease: "easeOut" });
|
|
455
|
+
gridShown = true;
|
|
492
456
|
}
|
|
493
457
|
|
|
494
458
|
el("count-badge") && (el("count-num").textContent = "0");
|
|
@@ -547,10 +511,10 @@ function openPlayer(game) {
|
|
|
547
511
|
const modal = el("player-modal");
|
|
548
512
|
modal.classList.remove("hidden");
|
|
549
513
|
document.documentElement.style.overflow = "hidden";
|
|
550
|
-
fx("#player-panel", { opacity: [0, 1], scale: [0.98, 1] }, { duration: 0.28,
|
|
514
|
+
fx("#player-panel", { opacity: [0, 1], scale: [0.98, 1] }, { duration: 0.28, ease: "easeOut" });
|
|
551
515
|
}
|
|
552
516
|
function closePlayer() {
|
|
553
|
-
fx("#player-panel", { opacity: [1, 0], scale: [1, 0.98] }, { duration: 0.18,
|
|
517
|
+
fx("#player-panel", { opacity: [1, 0], scale: [1, 0.98] }, { duration: 0.18, ease: "easeIn" }).finished.then(() => {
|
|
554
518
|
el("player-modal").classList.add("hidden");
|
|
555
519
|
el("player-frame").src = "";
|
|
556
520
|
document.documentElement.style.overflow = "";
|
|
@@ -569,7 +533,7 @@ document.addEventListener("keydown", (e) => {
|
|
|
569
533
|
/* -------------------------------- admin -------------------------------- */
|
|
570
534
|
function openAdminLogin() {
|
|
571
535
|
el("admin-login-modal").classList.remove("hidden");
|
|
572
|
-
fx("#admin-login-form", { opacity: [0, 1], y: [8, 0] }, { duration: 0.25,
|
|
536
|
+
fx("#admin-login-form", { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, ease: "easeOut" });
|
|
573
537
|
resetPinBoxes();
|
|
574
538
|
pinInputs[0]?.focus();
|
|
575
539
|
}
|
|
@@ -579,7 +543,7 @@ function closeAdminLogin() {
|
|
|
579
543
|
}
|
|
580
544
|
function openAdminPanel() {
|
|
581
545
|
el("admin-panel").classList.remove("hidden");
|
|
582
|
-
fx("#admin-panel-card", { opacity: [0, 1], y: [12, 0] }, { duration: 0.28,
|
|
546
|
+
fx("#admin-panel-card", { opacity: [0, 1], y: [12, 0] }, { duration: 0.28, ease: "easeOut" });
|
|
583
547
|
renderAdminList();
|
|
584
548
|
}
|
|
585
549
|
function closeAdminPanel() {
|
|
@@ -601,8 +565,8 @@ const pinBoxesEl = el("pin-boxes");
|
|
|
601
565
|
pinBoxesEl.innerHTML = Array.from({ length: 6 })
|
|
602
566
|
.map(
|
|
603
567
|
(_, i) =>
|
|
604
|
-
`<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1"
|
|
605
|
-
class="h-12 w-full rounded-
|
|
568
|
+
`<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1" aria-label="Digit ${i + 1}"
|
|
569
|
+
class="h-12 w-full rounded-[10px] bg-white/[0.07] text-center text-[20px] tabular-nums outline-none ring-1 ring-inset ring-white/10 transition-shadow focus:ring-2 focus:ring-red-500" />`
|
|
606
570
|
)
|
|
607
571
|
.join("");
|
|
608
572
|
const pinInputs = Array.from(pinBoxesEl.querySelectorAll("[data-pin-idx]"));
|
|
@@ -657,7 +621,7 @@ el("admin-logout").addEventListener("click", () => {
|
|
|
657
621
|
|
|
658
622
|
async function renderAdminList() {
|
|
659
623
|
const list = el("admin-list");
|
|
660
|
-
list.innerHTML = `<p class="text-[
|
|
624
|
+
list.innerHTML = `<p class="text-[14px] text-ink-400">Loading…</p>`;
|
|
661
625
|
try {
|
|
662
626
|
const res = await tablesDB.listRows({
|
|
663
627
|
databaseId: CONFIG.databaseId,
|
|
@@ -665,24 +629,24 @@ async function renderAdminList() {
|
|
|
665
629
|
queries: [Query.limit(200), Query.orderAsc("order")],
|
|
666
630
|
});
|
|
667
631
|
if (res.rows.length === 0) {
|
|
668
|
-
list.innerHTML = `<p class="text-[
|
|
632
|
+
list.innerHTML = `<p class="text-[14px] text-ink-400">No games added yet.</p>`;
|
|
669
633
|
return;
|
|
670
634
|
}
|
|
671
635
|
list.innerHTML = res.rows
|
|
672
636
|
.map(
|
|
673
637
|
(g) => `
|
|
674
|
-
<div class="flex items-center gap-3 rounded-xl
|
|
675
|
-
<img src="${iconSrcFor(g) || monogramSrc(g.name)}" class="h-
|
|
638
|
+
<div class="flex items-center gap-3 rounded-xl p-3 ring-1 ring-white/10" data-row="${g.$id}">
|
|
639
|
+
<img src="${iconSrcFor(g) || monogramSrc(g.name)}" alt="" class="tile-img h-10 w-10 shrink-0 rounded-[22.37%] object-cover" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
|
|
676
640
|
<div class="min-w-0 flex-1">
|
|
677
|
-
<input data-field="name" data-id="${g.$id}" value="${escapeHtml(g.name)}" class="w-full bg-transparent text-[
|
|
678
|
-
<input data-field="url" data-id="${g.$id}" value="${escapeHtml(g.url)}" class="mt-0.5 w-full bg-transparent text-[
|
|
641
|
+
<input data-field="name" data-id="${g.$id}" value="${escapeHtml(g.name)}" aria-label="Name" class="w-full border-b border-transparent bg-transparent text-[15px] font-medium outline-none focus:border-red-500" />
|
|
642
|
+
<input data-field="url" data-id="${g.$id}" value="${escapeHtml(g.url)}" aria-label="URL" class="mt-0.5 w-full border-b border-transparent bg-transparent text-[13px] text-ink-400 outline-none focus:border-red-500" />
|
|
679
643
|
</div>
|
|
680
|
-
<input data-field="category" data-id="${g.$id}" value="${escapeHtml(g.category || "")}" placeholder="
|
|
681
|
-
<label class="hidden
|
|
682
|
-
<input type="checkbox" data-field="enabled" data-id="${g.$id}" ${g.enabled === false ? "" : "checked"} class="accent-red-500" />
|
|
644
|
+
<input data-field="category" data-id="${g.$id}" value="${escapeHtml(g.category || "")}" placeholder="Category" aria-label="Category" class="field hidden !w-28 sm:block" />
|
|
645
|
+
<label class="hidden shrink-0 items-center gap-2 text-[13px] text-ink-300 sm:flex">
|
|
646
|
+
<input type="checkbox" data-field="enabled" data-id="${g.$id}" ${g.enabled === false ? "" : "checked"} class="h-4 w-4 accent-red-500" /> On
|
|
683
647
|
</label>
|
|
684
|
-
<button data-delete="${g.$id}" title="Delete" class="
|
|
685
|
-
<svg class="h-
|
|
648
|
+
<button data-delete="${g.$id}" title="Delete" aria-label="Delete" class="glass-lite icon-btn hover:!text-red-400">
|
|
649
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0-1 14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2L4 6"/></svg>
|
|
686
650
|
</button>
|
|
687
651
|
</div>`
|
|
688
652
|
)
|
|
@@ -718,7 +682,7 @@ async function renderAdminList() {
|
|
|
718
682
|
})
|
|
719
683
|
);
|
|
720
684
|
} catch (err) {
|
|
721
|
-
list.innerHTML = `<p class="text-[
|
|
685
|
+
list.innerHTML = `<p class="text-[14px] text-red-400">${escapeHtml(err?.message || "Failed to load games.")}</p>`;
|
|
722
686
|
}
|
|
723
687
|
}
|
|
724
688
|
|
package/package.json
CHANGED
package/savedconfig.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const CONFIG = {
|
|
2
|
+
endpoint: "https://nyc.cloud.appwrite.io/v1",
|
|
3
|
+
projectId: "6a9b8a1b001b9c61537f",
|
|
4
|
+
databaseId: "6a9b8b23003de0ec5cf7",
|
|
5
|
+
gamesTableId: "6a9b8bb3000013a3fd80",
|
|
6
|
+
iconsBucketId: "6a9b8b36002565ac5987",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const ADMIN_PIN = "051511";
|
|
10
|
+
const ADMIN_SESSION_KEY = "mlv2_admin_unlocked";
|
package/holdw.py
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import keyboard
|
|
2
|
-
import time
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def start():
|
|
7
|
-
keyboard_pressing = True
|
|
8
|
-
while keyboard_pressing:
|
|
9
|
-
keyboard.press("W")
|
|
10
|
-
time.sleep(0.1)
|
|
11
|
-
|
|
12
|
-
if keyboard.is_pressed("C"):
|
|
13
|
-
keyboard_pressing = False
|
|
14
|
-
while not (keyboard.is_pressed("X")):
|
|
15
|
-
time.sleep(0.1)
|
|
16
|
-
keyboard_pressing = True; start()
|
|
17
|
-
|
|
18
|
-
start()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|