Skip to content

Commit 72e6dd4

Browse files
committed
refactor: improve frontend and mobile support
1 parent 5bf0532 commit 72e6dd4

File tree

21 files changed

+1238
-1200
lines changed

21 files changed

+1238
-1200
lines changed

frontend/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8" />
5-
<meta name="viewport" content="width=device-width, initial-scale=1" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
66
<meta name="theme-color" content="#9b8d82" />
77
<link
88
rel="stylesheet"

frontend/src/app.tsx

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Component, createMemo, Show } from "solid-js";
1+
import { type Component, createMemo, onCleanup, Show } from "solid-js";
22

33
import { Layout } from "./components/layout/layout";
44
import Sidebar from "./components/layout/sidebar";
@@ -12,6 +12,56 @@ export const App: Component = () => {
1212
const [store, actions] = useStore();
1313
const { toast } = useToast();
1414

15+
// Fix iOS Safari WebSocket hanging bug using alternating RAF/setTimeout pattern
16+
// Source - https://stackoverflow.com/a/42036303
17+
// iOS has a bug where rapid WebSocket sends during pointer events can hang the connection
18+
// Solution: alternate between requestAnimationFrame and setTimeout to break the pattern
19+
let scheduledId: number | undefined;
20+
let pendingLedData: { index: number; status: number } | null = null;
21+
let useRaf = true;
22+
let frametime = 0;
23+
let lastframe = Date.now();
24+
25+
const deferredLedSend = (data: { index: number; status: number }) => {
26+
pendingLedData = data;
27+
28+
if (scheduledId) return;
29+
30+
const sendLed = () => {
31+
frametime = Date.now() - lastframe;
32+
lastframe = Date.now();
33+
34+
if (pendingLedData) {
35+
actions.send(
36+
JSON.stringify({
37+
event: "led",
38+
...pendingLedData,
39+
}),
40+
);
41+
pendingLedData = null;
42+
}
43+
44+
scheduledId = undefined;
45+
useRaf = !useRaf;
46+
};
47+
48+
if (useRaf) {
49+
scheduledId = requestAnimationFrame(sendLed);
50+
} else {
51+
scheduledId = setTimeout(sendLed, Math.max(0, frametime)) as unknown as number;
52+
}
53+
};
54+
55+
onCleanup(() => {
56+
if (scheduledId) {
57+
if (useRaf) {
58+
cancelAnimationFrame(scheduledId);
59+
} else {
60+
clearTimeout(scheduledId);
61+
}
62+
}
63+
});
64+
1565
const rotatedMatrix = createMemo(() => rotateArray(store.indexMatrix, store.rotation));
1666

1767
const wsMessage = (
@@ -120,18 +170,57 @@ export const App: Component = () => {
120170
</Show>
121171
}
122172
>
123-
<LedMatrix
124-
disabled={store.plugin !== 1}
125-
data={store.leds || []}
126-
indexData={rotatedMatrix()}
127-
brightness={store.brightness ?? 255}
128-
onSetLed={(data) => {
129-
wsMessage("led", data);
130-
}}
131-
onSetMatrix={(data) => {
132-
actions?.setLeds([...data]);
133-
}}
134-
/>
173+
<div class="flex flex-col items-center gap-6">
174+
<LedMatrix
175+
disabled={store.plugin !== 1}
176+
data={store.leds || []}
177+
indexData={rotatedMatrix()}
178+
brightness={store.brightness ?? 255}
179+
onSetLed={(data) => {
180+
deferredLedSend(data);
181+
}}
182+
onSetMatrix={(data) => {
183+
actions?.setLeds([...data]);
184+
}}
185+
/>
186+
187+
<div class="lg:hidden w-full max-w-100 sm:max-w-125">
188+
<div class="grid grid-cols-4 gap-2">
189+
<button
190+
type="button"
191+
onClick={handleLoadImage}
192+
class="flex flex-col items-center justify-center gap-1 bg-gray-700 text-white border-0 p-2 cursor-pointer font-semibold hover:opacity-80 active:-translate-y-px transition-all rounded text-xs"
193+
>
194+
<i class="fa-solid fa-file-import text-base" />
195+
<span>Import</span>
196+
</button>
197+
<button
198+
type="button"
199+
onClick={handleClear}
200+
class="flex flex-col items-center justify-center gap-1 bg-gray-700 text-white border-0 p-2 cursor-pointer font-semibold hover:opacity-80 active:-translate-y-px transition-all rounded hover:bg-red-600 text-xs"
201+
>
202+
<i class="fa-solid fa-trash text-base" />
203+
<span>Clear</span>
204+
</button>
205+
<button
206+
type="button"
207+
onClick={handlePersist}
208+
class="flex flex-col items-center justify-center gap-1 bg-gray-700 text-white border-0 p-2 cursor-pointer font-semibold hover:opacity-80 active:-translate-y-px transition-all rounded text-xs"
209+
>
210+
<i class="fa-solid fa-floppy-disk text-base" />
211+
<span>Save</span>
212+
</button>
213+
<button
214+
type="button"
215+
onClick={handleLoad}
216+
class="flex flex-col items-center justify-center gap-1 bg-gray-700 text-white border-0 p-2 cursor-pointer font-semibold hover:opacity-80 active:-translate-y-px transition-all rounded text-xs"
217+
>
218+
<i class="fa-solid fa-refresh text-base" />
219+
<span>Load</span>
220+
</button>
221+
</div>
222+
</div>
223+
</div>
135224
</Show>
136225
</div>
137226
);

frontend/src/components/button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const Button: Component<
66
return (
77
<button
88
{...props}
9-
class={`${props.widthAuto ? "w-auto" : "w-full"} bg-gray-600 text-white border-0 px-4 py-3 uppercase text-sm leading-6 tracking-wider cursor-pointer font-bold hover:opacity-80 active:-translate-y-px transition-all rounded disabled:opacity-40 hover:disabled:bg-gray-600 ${
9+
class={`${props.widthAuto ? "w-auto" : "w-full"} bg-gray-600 text-white border-0 px-3 py-2 text-sm cursor-pointer font-semibold hover:opacity-80 active:-translate-y-px transition-all rounded disabled:opacity-40 hover:disabled:bg-gray-600 ${
1010
props.class || ""
1111
}`}
1212
>

frontend/src/components/layout/layout.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@ export const Layout: Component<{
2020
fallback={
2121
<main class="h-full overflow-auto">
2222
<ScreenInfo>
23-
<h2 class="text-4xl">{store.connectionStatus}...</h2>
23+
<h2 class="text-4xl mb-4">{store.connectionStatus}...</h2>
24+
<Show when={store.connectionState() === 0}>
25+
<p class="text-white text-sm max-w-md mx-auto">
26+
Make sure your device is powered on and connected to the same network. Check
27+
browser console for connection errors.
28+
</p>
29+
</Show>
2430
</ScreenInfo>
2531
</main>
2632
}
2733
>
2834
<div class="flex-1 lg:grid lg:grid-cols-[320px_1fr] lg:gap-6 lg:p-6 relative overflow-hidden">
29-
{/* Mobile Overlay */}
3035
<Show when={isMobileMenuOpen()}>
3136
<button
3237
type="button"
@@ -40,22 +45,19 @@ export const Layout: Component<{
4045
/>
4146
</Show>
4247

43-
{/* Sidebar */}
4448
<aside
4549
class={`bg-white p-6 shadow-lg flex flex-col
4650
lg:relative lg:h-full lg:rounded-2xl
4751
fixed top-0 left-0 h-full w-full z-50
4852
transition-transform duration-300 ease-in-out
4953
${isMobileMenuOpen() ? "translate-x-0" : "-translate-x-full lg:translate-x-0"}`}
5054
onClick={(e) => {
51-
// Close menu when clicks links in sidebar
5255
const target = e.target as HTMLElement;
5356
if (target.tagName === "A" || target.closest("a")) {
5457
closeMobileMenu();
5558
}
5659
}}
5760
onKeyDown={(e) => {
58-
// Close menu when Enter is pressed on links in sidebar
5961
if (e.key === "Enter") {
6062
const target = e.target as HTMLElement;
6163
if (target.tagName === "A" || target.closest("a")) {
@@ -65,27 +67,25 @@ export const Layout: Component<{
6567
}}
6668
>
6769
{props.sidebar}
68-
{/* Mobile Close Button */}
6970
<button
7071
type="button"
7172
onClick={closeMobileMenu}
72-
class="lg:hidden mt-4 w-full bg-gray-700 text-white border-0 px-4 py-3 uppercase text-sm leading-6 tracking-wider cursor-pointer font-bold hover:opacity-80 active:-translate-y-px transition-all rounded"
73+
class="lg:hidden mt-4 w-full bg-gray-700 text-white border-0 px-3 py-2 text-sm cursor-pointer font-semibold hover:opacity-80 active:-translate-y-px transition-all rounded"
7374
>
7475
<i class="fa-solid fa-times mr-2" />
7576
Close Menu
7677
</button>
7778
</aside>
7879

79-
<main class="h-full overflow-auto lg:pb-0 pb-20" ref={props.ref}>
80+
<main class="h-full overflow-auto" ref={props.ref}>
8081
{props.content}
8182
</main>
8283
</div>
8384

84-
{/* Mobile Menu Button - Fixed at bottom */}
8585
<button
8686
type="button"
8787
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen())}
88-
class="lg:hidden fixed bottom-6 right-6 z-30 bg-gray-700 text-white border-0 p-4 rounded-full shadow-lg cursor-pointer font-bold hover:opacity-80 active:-translate-y-px transition-all"
88+
class="lg:hidden fixed bottom-6 right-6 z-30 bg-gray-700 text-white border-0 py-3 px-4 rounded-full shadow-lg cursor-pointer font-bold hover:opacity-80 active:-translate-y-px transition-all"
8989
>
9090
<i class="fa-solid fa-bars text-xl" />
9191
</button>

0 commit comments

Comments
 (0)