mlv2boii 1.0.6 → 1.0.7
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 +186 -194
- 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,47 +49,45 @@
|
|
|
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.
|
|
54
|
-
|
|
55
|
-
/* thin, quiet scrollbars */
|
|
56
|
-
* { scrollbar-width: thin; scrollbar-color: #2a2a30 transparent; }
|
|
57
|
-
*::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
58
|
-
*::-webkit-scrollbar-track { background: transparent; }
|
|
59
|
-
*::-webkit-scrollbar-thumb { background: #26262b; border-radius: 999px; }
|
|
60
|
-
*::-webkit-scrollbar-thumb:hover { background: #3a3a42; }
|
|
66
|
+
::selection { background: rgba(232,20,47,0.4); color: #fff; }
|
|
67
|
+
input { caret-color: var(--color-red-400); }
|
|
61
68
|
|
|
62
|
-
/*
|
|
63
|
-
|
|
64
|
-
|
|
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; }
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
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)); }
|
|
68
77
|
|
|
69
|
-
/*
|
|
70
|
-
|
|
71
|
-
.skel { background: linear-gradient(100deg, #18181c 30%, #202024 45%, #18181c 60%); background-size: 200% 100%; animation: shimmer 1.6s ease-in-out infinite; }
|
|
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; }
|
|
72
80
|
|
|
73
|
-
|
|
81
|
+
.no-scrollbar { scrollbar-width: none; }
|
|
82
|
+
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
83
|
+
* { scrollbar-width: thin; scrollbar-color: #2c2c30 transparent; }
|
|
84
|
+
*::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
85
|
+
*::-webkit-scrollbar-thumb { background: #2c2c30; border-radius: 999px; }
|
|
74
86
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
50% { transform: translate3d(0, 12px, 0) scale(1.04); }
|
|
80
|
-
}
|
|
81
|
-
@keyframes heroLine {
|
|
82
|
-
0%, 100% { opacity: .35; transform: scaleX(.7); }
|
|
83
|
-
50% { opacity: 1; transform: scaleX(1); }
|
|
84
|
-
}
|
|
87
|
+
/* the single page-load moment: title and subline rise in */
|
|
88
|
+
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
|
89
|
+
.rise { animation: rise 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; animation-delay: var(--d, 0s); }
|
|
90
|
+
@media (prefers-reduced-motion: reduce) { .rise { animation: none; } }
|
|
85
91
|
|
|
86
92
|
/* Blank Overlay Lock Screen */
|
|
87
93
|
#blank-overlay {
|
|
@@ -100,107 +106,86 @@
|
|
|
100
106
|
</style>
|
|
101
107
|
</head>
|
|
102
108
|
|
|
103
|
-
<body class="
|
|
109
|
+
<body class="min-h-screen bg-ink-950 font-sans text-ink-50 antialiased">
|
|
104
110
|
|
|
105
111
|
<!-- Blank Overlay Screen -->
|
|
106
112
|
<div id="blank-overlay"></div>
|
|
107
113
|
|
|
108
|
-
<div class="grain"></div>
|
|
109
|
-
<div class="spotlight"></div>
|
|
110
|
-
|
|
111
114
|
<!-- ================= HEADER ================= -->
|
|
112
|
-
<header class="sticky top-0 z-40 border-b border-
|
|
113
|
-
<div class="mx-auto
|
|
114
|
-
<a href="#" class="flex items-center gap-2
|
|
115
|
-
<span class="
|
|
116
|
-
|
|
117
|
-
<span class="absolute inset-0 animate-ping rounded-full bg-red-500 opacity-60"></span>
|
|
118
|
-
</span>
|
|
119
|
-
<span class="font-display font-semibold tracking-tight text-[15px] leading-none">mlv2</span>
|
|
115
|
+
<header class="sticky top-0 z-40 border-b border-white/[0.08] bg-black/70 pt-[env(safe-area-inset-top)] backdrop-blur-xl backdrop-saturate-150">
|
|
116
|
+
<div class="page-x mx-auto flex h-14 max-w-5xl items-center gap-4">
|
|
117
|
+
<a href="#" class="flex shrink-0 items-center gap-2" aria-label="mlv2 home">
|
|
118
|
+
<span class="h-2.5 w-2.5 rounded-full bg-red-500"></span>
|
|
119
|
+
<span class="text-[17px] font-semibold tracking-[-0.02em]">mlv2</span>
|
|
120
120
|
</a>
|
|
121
121
|
|
|
122
|
-
<div class="
|
|
123
|
-
<
|
|
124
|
-
<
|
|
125
|
-
class="
|
|
122
|
+
<div class="ml-auto flex min-w-0 flex-1 items-center justify-end gap-4">
|
|
123
|
+
<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>
|
|
124
|
+
<div class="relative w-full max-w-[16rem]">
|
|
125
|
+
<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>
|
|
126
|
+
<input id="search" type="text" placeholder="Search games" aria-label="Search games" autocomplete="off" enterkeyhint="search" class="field pl-9" />
|
|
127
|
+
</div>
|
|
126
128
|
</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
|
-
</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
129
|
</div>
|
|
136
130
|
</header>
|
|
137
131
|
|
|
138
132
|
<!-- ================= 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>
|
|
133
|
+
<main class="page-x mx-auto max-w-5xl pb-24">
|
|
134
|
+
|
|
135
|
+
<!-- ================= TITLE ================= -->
|
|
136
|
+
<section class="pb-10 pt-14 sm:pb-14 sm:pt-24">
|
|
137
|
+
<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>
|
|
138
|
+
<p class="rise mt-4 text-[19px] text-ink-300 sm:text-[21px]" style="--d: 0.08s">we're so back</p>
|
|
154
139
|
</section>
|
|
155
140
|
|
|
141
|
+
<!-- categories -->
|
|
142
|
+
<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>
|
|
143
|
+
|
|
156
144
|
<!-- 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-[
|
|
145
|
+
<div id="setup-banner" class="mb-8 hidden rounded-xl bg-ink-900 p-4 ring-1 ring-white/10">
|
|
146
|
+
<p class="text-[15px] font-semibold">Not connected yet</p>
|
|
147
|
+
<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
148
|
</div>
|
|
161
149
|
|
|
162
150
|
<!-- skeleton loading state -->
|
|
163
|
-
<div id="skeleton-grid" class="grid grid-cols-
|
|
164
|
-
</div>
|
|
151
|
+
<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
152
|
|
|
166
153
|
<!-- empty state -->
|
|
167
154
|
<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>
|
|
155
|
+
<p id="empty-title" class="text-[19px] font-semibold">No games yet</p>
|
|
156
|
+
<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
157
|
</div>
|
|
174
158
|
|
|
175
159
|
<!-- game grid -->
|
|
176
|
-
<div id="grid" class="hidden grid grid-cols-
|
|
160
|
+
<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
161
|
</main>
|
|
178
162
|
|
|
179
163
|
<!-- ================= FOOTER ================= -->
|
|
180
|
-
<footer class="
|
|
181
|
-
<div class="mx-auto max-w-
|
|
182
|
-
<p
|
|
183
|
-
<button id="admin-trigger" class="
|
|
164
|
+
<footer class="border-t border-white/[0.08]">
|
|
165
|
+
<div class="page-x mx-auto flex max-w-5xl items-center justify-between py-4 text-[13px] text-ink-400">
|
|
166
|
+
<p>mlv2 © <span id="year"></span></p>
|
|
167
|
+
<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
168
|
</div>
|
|
185
169
|
</footer>
|
|
186
170
|
|
|
187
171
|
<!-- ================= PLAYER MODAL ================= -->
|
|
188
172
|
<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
|
-
|
|
173
|
+
<div class="absolute inset-0 bg-black" data-player-close></div>
|
|
174
|
+
<div id="player-panel" class="relative flex h-full w-full flex-col opacity-0">
|
|
175
|
+
<div class="border-b border-white/[0.08] bg-black pt-[env(safe-area-inset-top)]">
|
|
176
|
+
<div class="flex h-14 items-center justify-between gap-3 px-3 sm:px-4">
|
|
177
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
178
|
+
<img id="player-icon" class="h-8 w-8 rounded-[22.37%] object-cover" alt="" />
|
|
179
|
+
<p id="player-title" class="truncate text-[15px] font-semibold"></p>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
182
|
+
<a id="player-newtab" target="_blank" rel="noopener" title="Open in new tab" aria-label="Open in new tab" class="icon-btn">
|
|
183
|
+
<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>
|
|
184
|
+
</a>
|
|
185
|
+
<button id="player-close" title="Close" aria-label="Close" class="icon-btn">
|
|
186
|
+
<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>
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
204
189
|
</div>
|
|
205
190
|
</div>
|
|
206
191
|
<div class="relative flex-1 bg-black">
|
|
@@ -211,40 +196,40 @@
|
|
|
211
196
|
|
|
212
197
|
<!-- ================= ADMIN: PIN GATE ================= -->
|
|
213
198
|
<div id="admin-login-modal" class="fixed inset-0 z-50 hidden">
|
|
214
|
-
<div class="absolute inset-0 bg-
|
|
199
|
+
<div class="absolute inset-0 bg-black/80 backdrop-blur-xl" data-admin-login-close></div>
|
|
215
200
|
<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-[
|
|
201
|
+
<form id="admin-login-form" class="w-full max-w-[340px] rounded-[28px] bg-ink-900 p-6 opacity-0 ring-1 ring-white/10" data-fx>
|
|
202
|
+
<p class="text-[19px] font-semibold">Enter PIN</p>
|
|
203
|
+
<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
204
|
|
|
220
|
-
<div id="pin-boxes" class="mt-
|
|
205
|
+
<div id="pin-boxes" class="mt-5 grid grid-cols-6 gap-2">
|
|
221
206
|
<!-- 6 single-digit inputs, filled in by JS -->
|
|
222
207
|
</div>
|
|
223
208
|
<input id="admin-pin" type="hidden" />
|
|
224
209
|
|
|
225
|
-
<p id="admin-login-error" class="
|
|
210
|
+
<p id="admin-login-error" class="mt-3 hidden text-[14px] text-red-400" role="alert"></p>
|
|
226
211
|
|
|
227
|
-
<button type="submit" id="admin-login-submit" class="mt-
|
|
228
|
-
<button type="button" data-admin-login-close class="mt-2 w-full
|
|
212
|
+
<button type="submit" id="admin-login-submit" class="btn btn-primary mt-5 w-full">Unlock</button>
|
|
213
|
+
<button type="button" data-admin-login-close class="btn btn-ghost mt-2 w-full">Cancel</button>
|
|
229
214
|
</form>
|
|
230
215
|
</div>
|
|
231
216
|
</div>
|
|
232
217
|
|
|
233
218
|
<!-- ================= ADMIN: PANEL ================= -->
|
|
234
219
|
<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 w-full
|
|
220
|
+
<div class="absolute inset-0 bg-black/80 backdrop-blur-xl" data-admin-panel-close></div>
|
|
221
|
+
<div class="absolute inset-0 flex items-start justify-center p-0 sm:items-center sm:p-4">
|
|
222
|
+
<div id="admin-panel-card" class="relative flex h-full w-full flex-col bg-ink-900 opacity-0 sm:h-[85vh] sm:max-w-2xl sm:rounded-[32px] sm:ring-1 sm:ring-white/10" data-fx>
|
|
238
223
|
|
|
239
|
-
<div class="flex items-center justify-between
|
|
224
|
+
<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
225
|
<div>
|
|
241
|
-
<p class="
|
|
242
|
-
<p class="mt-1 text-[
|
|
226
|
+
<p class="text-[17px] font-semibold leading-none">Admin</p>
|
|
227
|
+
<p class="mt-1 text-[13px] text-ink-400">Unlocked with PIN</p>
|
|
243
228
|
</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-
|
|
229
|
+
<div class="flex items-center gap-2">
|
|
230
|
+
<button id="admin-logout" class="btn btn-ghost">Lock</button>
|
|
231
|
+
<button data-admin-panel-close aria-label="Close" class="icon-btn">
|
|
232
|
+
<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
233
|
</button>
|
|
249
234
|
</div>
|
|
250
235
|
</div>
|
|
@@ -252,36 +237,38 @@
|
|
|
252
237
|
<div class="flex-1 overflow-y-auto px-5 py-5">
|
|
253
238
|
|
|
254
239
|
<!-- add game form -->
|
|
255
|
-
<form id="add-game-form" class="rounded-xl
|
|
256
|
-
<p class="text-[
|
|
257
|
-
<div class="mt-
|
|
240
|
+
<form id="add-game-form" class="rounded-xl p-4 ring-1 ring-white/10">
|
|
241
|
+
<p class="text-[15px] font-semibold">Add game</p>
|
|
242
|
+
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
258
243
|
<div>
|
|
259
|
-
<label class="text-[
|
|
260
|
-
<input id="new-name" required class="mt-1
|
|
244
|
+
<label for="new-name" class="text-[13px] text-ink-300">Name</label>
|
|
245
|
+
<input id="new-name" required class="field mt-1.5" />
|
|
261
246
|
</div>
|
|
262
247
|
<div>
|
|
263
|
-
<label class="text-[
|
|
264
|
-
<input id="new-category" class="mt-1
|
|
248
|
+
<label for="new-category" class="text-[13px] text-ink-300">Category (optional)</label>
|
|
249
|
+
<input id="new-category" class="field mt-1.5" />
|
|
265
250
|
</div>
|
|
266
251
|
<div class="sm:col-span-2">
|
|
267
|
-
<label class="text-[
|
|
268
|
-
<input id="new-url" required type="url" placeholder="https
|
|
252
|
+
<label for="new-url" class="text-[13px] text-ink-300">Game URL</label>
|
|
253
|
+
<input id="new-url" required type="url" placeholder="https://" class="field mt-1.5" />
|
|
269
254
|
</div>
|
|
270
255
|
<div>
|
|
271
|
-
<label class="text-[
|
|
272
|
-
<input id="new-icon-file" type="file" accept="image/*" class="mt-
|
|
256
|
+
<label for="new-icon-file" class="text-[13px] text-ink-300">Icon upload</label>
|
|
257
|
+
<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
258
|
</div>
|
|
274
259
|
<div>
|
|
275
|
-
<label class="text-[
|
|
276
|
-
<input id="new-icon-url" type="url" placeholder="https
|
|
260
|
+
<label for="new-icon-url" class="text-[13px] text-ink-300">Or external icon URL</label>
|
|
261
|
+
<input id="new-icon-url" type="url" placeholder="https://" class="field mt-1.5" />
|
|
277
262
|
</div>
|
|
278
263
|
</div>
|
|
279
|
-
<
|
|
280
|
-
|
|
264
|
+
<div class="mt-5 flex items-center gap-3">
|
|
265
|
+
<button type="submit" id="add-game-submit" class="btn btn-primary">Add game</button>
|
|
266
|
+
<span id="add-game-status" class="text-[13px] text-ink-400"></span>
|
|
267
|
+
</div>
|
|
281
268
|
</form>
|
|
282
269
|
|
|
283
270
|
<!-- existing games -->
|
|
284
|
-
<p class="
|
|
271
|
+
<p class="mb-3 mt-8 text-[15px] font-semibold">Games</p>
|
|
285
272
|
<div id="admin-list" class="space-y-2"></div>
|
|
286
273
|
</div>
|
|
287
274
|
</div>
|
|
@@ -289,7 +276,7 @@
|
|
|
289
276
|
</div>
|
|
290
277
|
|
|
291
278
|
<!-- ================= TOASTS ================= -->
|
|
292
|
-
<div id="toast-root" class="fixed bottom-4 right-4 z-[60] flex flex-col gap-2
|
|
279
|
+
<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>
|
|
293
280
|
|
|
294
281
|
<!-- Konami Code Script with 1-Hour Expiration -->
|
|
295
282
|
<script>
|
|
@@ -361,7 +348,9 @@ const isConfigured = () =>
|
|
|
361
348
|
!CONFIG.projectId.includes("YOUR_") &&
|
|
362
349
|
!CONFIG.databaseId.includes("YOUR_");
|
|
363
350
|
|
|
351
|
+
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
364
352
|
function fx(targets, keyframes, options = {}) {
|
|
353
|
+
if (reduceMotion) options = { ...options, duration: 0, delay: 0 };
|
|
365
354
|
return animate(targets, keyframes, options);
|
|
366
355
|
}
|
|
367
356
|
|
|
@@ -386,8 +375,8 @@ function iconSrcFor(game) {
|
|
|
386
375
|
function monogramSrc(name) {
|
|
387
376
|
const initials = (name || "?").trim().slice(0, 2).toUpperCase();
|
|
388
377
|
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="
|
|
378
|
+
<rect width="100" height="100" fill="#161618"/>
|
|
379
|
+
<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
380
|
</svg>`;
|
|
392
381
|
return "data:image/svg+xml;utf8," + encodeURIComponent(svg);
|
|
393
382
|
}
|
|
@@ -399,20 +388,20 @@ function escapeHtml(s) {
|
|
|
399
388
|
function toast(message, tone = "default") {
|
|
400
389
|
const root = el("toast-root");
|
|
401
390
|
const node = document.createElement("div");
|
|
402
|
-
const toneClasses = tone === "error" ? "
|
|
403
|
-
node.className = `rounded-
|
|
391
|
+
const toneClasses = tone === "error" ? "text-red-300" : "text-ink-50";
|
|
392
|
+
node.className = `rounded-full ${toneClasses} bg-ink-800/90 backdrop-blur-xl px-4 py-2.5 text-[14px] ring-1 ring-white/10 opacity-0`;
|
|
404
393
|
node.textContent = message;
|
|
405
394
|
root.appendChild(node);
|
|
406
|
-
fx(node, { opacity: [0, 1], y: [8, 0] }, { duration: 0.25,
|
|
395
|
+
fx(node, { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, ease: "easeOut" });
|
|
407
396
|
setTimeout(() => {
|
|
408
|
-
fx(node, { opacity: [1, 0], y: [0, 6] }, { duration: 0.2,
|
|
397
|
+
fx(node, { opacity: [1, 0], y: [0, 6] }, { duration: 0.2, ease: "easeIn" }).finished.then(() => node.remove());
|
|
409
398
|
}, 2800);
|
|
410
399
|
}
|
|
411
400
|
|
|
412
401
|
/* -------------------------- render: skeleton -------------------------- */
|
|
413
402
|
function renderSkeleton() {
|
|
414
|
-
el("skeleton-grid").innerHTML = Array.from({ length:
|
|
415
|
-
.map(() => `<div class="
|
|
403
|
+
el("skeleton-grid").innerHTML = Array.from({ length: 12 })
|
|
404
|
+
.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
405
|
.join("");
|
|
417
406
|
}
|
|
418
407
|
renderSkeleton();
|
|
@@ -432,8 +421,8 @@ function renderCategoryBar() {
|
|
|
432
421
|
bar.innerHTML = cats
|
|
433
422
|
.map((c) => {
|
|
434
423
|
const active = c === activeCategory;
|
|
435
|
-
return `<button data-cat="${escapeHtml(c)}" class="
|
|
436
|
-
active ? "
|
|
424
|
+
return `<button data-cat="${escapeHtml(c)}" aria-pressed="${active}" class="chip ${
|
|
425
|
+
active ? "bg-red-500 text-white" : "bg-white/[0.07] text-ink-300 hover:bg-white/[0.12] hover:text-ink-50"
|
|
437
426
|
}">${escapeHtml(c)}</button>`;
|
|
438
427
|
})
|
|
439
428
|
.join("");
|
|
@@ -446,6 +435,7 @@ function renderCategoryBar() {
|
|
|
446
435
|
);
|
|
447
436
|
}
|
|
448
437
|
|
|
438
|
+
let gridShown = false;
|
|
449
439
|
function renderGrid() {
|
|
450
440
|
const grid = el("grid");
|
|
451
441
|
const games = currentFiltered();
|
|
@@ -473,13 +463,12 @@ function renderGrid() {
|
|
|
473
463
|
grid.innerHTML = games
|
|
474
464
|
.map((g) => {
|
|
475
465
|
const src = iconSrcFor(g) || monogramSrc(g.name);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
</div>
|
|
466
|
+
return `<button data-fx data-id="${g.$id}" class="tile group block w-full text-left opacity-0">
|
|
467
|
+
<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]">
|
|
468
|
+
<img src="${src}" alt="" loading="lazy" class="tile-img h-full w-full object-cover" onerror="this.onerror=null;this.src='${monogramSrc(g.name)}'" />
|
|
469
|
+
</span>
|
|
470
|
+
<span class="mt-2.5 block truncate text-center text-[13px] font-medium leading-tight text-ink-50">${escapeHtml(g.name)}</span>
|
|
471
|
+
${g.category ? `<span class="mt-0.5 block truncate text-center text-[12px] text-ink-400">${escapeHtml(g.category)}</span>` : ""}
|
|
483
472
|
</button>`;
|
|
484
473
|
})
|
|
485
474
|
.join("");
|
|
@@ -488,7 +477,10 @@ function renderGrid() {
|
|
|
488
477
|
card.addEventListener("click", () => openPlayer(games.find((g) => g.$id === card.dataset.id)))
|
|
489
478
|
);
|
|
490
479
|
|
|
491
|
-
|
|
480
|
+
// full entrance once; later filter/search changes only fade
|
|
481
|
+
fx("#grid [data-fx]", { opacity: [0, 1], y: gridShown ? 0 : [8, 0] },
|
|
482
|
+
{ duration: gridShown ? 0.18 : 0.5, delay: gridShown ? 0 : stagger(0.03), ease: "easeOut" });
|
|
483
|
+
gridShown = true;
|
|
492
484
|
}
|
|
493
485
|
|
|
494
486
|
el("count-badge") && (el("count-num").textContent = "0");
|
|
@@ -547,10 +539,10 @@ function openPlayer(game) {
|
|
|
547
539
|
const modal = el("player-modal");
|
|
548
540
|
modal.classList.remove("hidden");
|
|
549
541
|
document.documentElement.style.overflow = "hidden";
|
|
550
|
-
fx("#player-panel", { opacity: [0, 1], scale: [0.98, 1] }, { duration: 0.28,
|
|
542
|
+
fx("#player-panel", { opacity: [0, 1], scale: [0.98, 1] }, { duration: 0.28, ease: "easeOut" });
|
|
551
543
|
}
|
|
552
544
|
function closePlayer() {
|
|
553
|
-
fx("#player-panel", { opacity: [1, 0], scale: [1, 0.98] }, { duration: 0.18,
|
|
545
|
+
fx("#player-panel", { opacity: [1, 0], scale: [1, 0.98] }, { duration: 0.18, ease: "easeIn" }).finished.then(() => {
|
|
554
546
|
el("player-modal").classList.add("hidden");
|
|
555
547
|
el("player-frame").src = "";
|
|
556
548
|
document.documentElement.style.overflow = "";
|
|
@@ -569,7 +561,7 @@ document.addEventListener("keydown", (e) => {
|
|
|
569
561
|
/* -------------------------------- admin -------------------------------- */
|
|
570
562
|
function openAdminLogin() {
|
|
571
563
|
el("admin-login-modal").classList.remove("hidden");
|
|
572
|
-
fx("#admin-login-form", { opacity: [0, 1], y: [8, 0] }, { duration: 0.25,
|
|
564
|
+
fx("#admin-login-form", { opacity: [0, 1], y: [8, 0] }, { duration: 0.25, ease: "easeOut" });
|
|
573
565
|
resetPinBoxes();
|
|
574
566
|
pinInputs[0]?.focus();
|
|
575
567
|
}
|
|
@@ -579,7 +571,7 @@ function closeAdminLogin() {
|
|
|
579
571
|
}
|
|
580
572
|
function openAdminPanel() {
|
|
581
573
|
el("admin-panel").classList.remove("hidden");
|
|
582
|
-
fx("#admin-panel-card", { opacity: [0, 1], y: [12, 0] }, { duration: 0.28,
|
|
574
|
+
fx("#admin-panel-card", { opacity: [0, 1], y: [12, 0] }, { duration: 0.28, ease: "easeOut" });
|
|
583
575
|
renderAdminList();
|
|
584
576
|
}
|
|
585
577
|
function closeAdminPanel() {
|
|
@@ -601,8 +593,8 @@ const pinBoxesEl = el("pin-boxes");
|
|
|
601
593
|
pinBoxesEl.innerHTML = Array.from({ length: 6 })
|
|
602
594
|
.map(
|
|
603
595
|
(_, i) =>
|
|
604
|
-
`<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1"
|
|
605
|
-
class="h-12 w-full rounded-
|
|
596
|
+
`<input data-pin-idx="${i}" type="password" inputmode="numeric" pattern="[0-9]*" maxlength="1" aria-label="Digit ${i + 1}"
|
|
597
|
+
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
598
|
)
|
|
607
599
|
.join("");
|
|
608
600
|
const pinInputs = Array.from(pinBoxesEl.querySelectorAll("[data-pin-idx]"));
|
|
@@ -657,7 +649,7 @@ el("admin-logout").addEventListener("click", () => {
|
|
|
657
649
|
|
|
658
650
|
async function renderAdminList() {
|
|
659
651
|
const list = el("admin-list");
|
|
660
|
-
list.innerHTML = `<p class="text-[
|
|
652
|
+
list.innerHTML = `<p class="text-[14px] text-ink-400">Loading…</p>`;
|
|
661
653
|
try {
|
|
662
654
|
const res = await tablesDB.listRows({
|
|
663
655
|
databaseId: CONFIG.databaseId,
|
|
@@ -665,24 +657,24 @@ async function renderAdminList() {
|
|
|
665
657
|
queries: [Query.limit(200), Query.orderAsc("order")],
|
|
666
658
|
});
|
|
667
659
|
if (res.rows.length === 0) {
|
|
668
|
-
list.innerHTML = `<p class="text-[
|
|
660
|
+
list.innerHTML = `<p class="text-[14px] text-ink-400">No games added yet.</p>`;
|
|
669
661
|
return;
|
|
670
662
|
}
|
|
671
663
|
list.innerHTML = res.rows
|
|
672
664
|
.map(
|
|
673
665
|
(g) => `
|
|
674
|
-
<div class="flex items-center gap-3 rounded-xl
|
|
675
|
-
<img src="${iconSrcFor(g) || monogramSrc(g.name)}" class="h-
|
|
666
|
+
<div class="flex items-center gap-3 rounded-xl p-3 ring-1 ring-white/10" data-row="${g.$id}">
|
|
667
|
+
<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
668
|
<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-[
|
|
669
|
+
<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" />
|
|
670
|
+
<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
671
|
</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" />
|
|
672
|
+
<input data-field="category" data-id="${g.$id}" value="${escapeHtml(g.category || "")}" placeholder="Category" aria-label="Category" class="field hidden !w-28 sm:block" />
|
|
673
|
+
<label class="hidden shrink-0 items-center gap-2 text-[13px] text-ink-300 sm:flex">
|
|
674
|
+
<input type="checkbox" data-field="enabled" data-id="${g.$id}" ${g.enabled === false ? "" : "checked"} class="h-4 w-4 accent-red-500" /> On
|
|
683
675
|
</label>
|
|
684
|
-
<button data-delete="${g.$id}" title="Delete" class="
|
|
685
|
-
<svg class="h-
|
|
676
|
+
<button data-delete="${g.$id}" title="Delete" aria-label="Delete" class="icon-btn hover:!text-red-400">
|
|
677
|
+
<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
678
|
</button>
|
|
687
679
|
</div>`
|
|
688
680
|
)
|
|
@@ -718,7 +710,7 @@ async function renderAdminList() {
|
|
|
718
710
|
})
|
|
719
711
|
);
|
|
720
712
|
} catch (err) {
|
|
721
|
-
list.innerHTML = `<p class="text-[
|
|
713
|
+
list.innerHTML = `<p class="text-[14px] text-red-400">${escapeHtml(err?.message || "Failed to load games.")}</p>`;
|
|
722
714
|
}
|
|
723
715
|
}
|
|
724
716
|
|
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
|
-
|