create-bridgething 0.1.2

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.
@@ -0,0 +1,98 @@
1
+ import { BridgethingClient, type ConnectionState, type PlayerState } from '@bridgething/client';
2
+ import { useEffect, useMemo, useState } from 'react';
3
+
4
+ // Production: the webapp is served by the daemon on the device, so a
5
+ // relative `ws://<location.host>/` reaches it. In dev (vite on your
6
+ // laptop) point at the device explicitly with VITE_BRIDGETHING_URL.
7
+ const wsUrl =
8
+ import.meta.env.VITE_BRIDGETHING_URL ??
9
+ (typeof window !== 'undefined' ? `ws://${window.location.host}/` : 'ws://127.0.0.1:8891/');
10
+
11
+ export default function App() {
12
+ const client = useMemo(() => new BridgethingClient({ url: wsUrl }), []);
13
+ const [conn, setConn] = useState<ConnectionState>(client.connectionState);
14
+ const [state, setState] = useState<PlayerState | null>(null);
15
+ const [artUrl, setArtUrl] = useState<string | null>(null);
16
+
17
+ useEffect(() => {
18
+ const offConn = client.on(event => {
19
+ if (event.type === 'open' || event.type === 'close' || event.type === 'connecting') {
20
+ setConn(client.connectionState);
21
+ }
22
+ });
23
+ const offSnapshot = client.player.onSnapshot(reply => setState(reply.state));
24
+ client.player.stateGet().then(r => r.ok && setState(r.response.state));
25
+ return () => {
26
+ offConn();
27
+ offSnapshot();
28
+ };
29
+ }, [client]);
30
+
31
+ const track = state?.track ?? null;
32
+ const artworkId = track?.artworkId ?? null;
33
+
34
+ useEffect(() => {
35
+ if (!artworkId) {
36
+ setArtUrl(null);
37
+ return;
38
+ }
39
+ let revoked = false;
40
+ let blobUrl: string | null = null;
41
+ (async () => {
42
+ const result = await client.asset.get({ id: artworkId, requestId: crypto.randomUUID() });
43
+ if (revoked) return;
44
+ if (result.ok) {
45
+ const bytes = new Uint8Array(result.response.bytes as unknown as number[]);
46
+ blobUrl = URL.createObjectURL(new Blob([bytes], { type: result.response.mime ?? 'image/jpeg' }));
47
+ setArtUrl(blobUrl);
48
+ }
49
+ })();
50
+ return () => {
51
+ revoked = true;
52
+ if (blobUrl) URL.revokeObjectURL(blobUrl);
53
+ };
54
+ }, [client, artworkId]);
55
+
56
+ const playing = state?.playback.state === 'playing';
57
+
58
+ return (
59
+ <div className="flex h-full w-full items-center justify-center gap-12 bg-bt-charcoal p-12 text-bt-off-white">
60
+ {artUrl ? (
61
+ <img src={artUrl} alt="" className="h-full max-h-96 w-auto rounded-2xl shadow-2xl" />
62
+ ) : (
63
+ <div className="grid h-96 w-96 place-items-center rounded-2xl bg-black/40 text-sm text-bt-soft-gray">
64
+ {conn === 'open' ? 'no track' : conn}
65
+ </div>
66
+ )}
67
+ <div className="flex flex-col gap-3">
68
+ <div className="text-xs uppercase tracking-widest text-bt-soft-gray">{conn}</div>
69
+ {track ? (
70
+ <>
71
+ <div className="text-3xl font-semibold leading-tight">{track.title ?? 'unknown'}</div>
72
+ <div className="text-xl text-bt-soft-gray">{track.artist ?? ''}</div>
73
+ <div className="text-sm text-bt-soft-gray/70">{track.album ?? ''}</div>
74
+ <div className="mt-6 flex gap-4">
75
+ <button
76
+ className="rounded-full bg-white/10 px-6 py-3 text-sm font-medium transition active:bg-white/20"
77
+ onClick={() => client.player.skipPrev()}>
78
+ ◀◀
79
+ </button>
80
+ <button
81
+ className="rounded-full bg-bt-blue px-6 py-3 text-sm font-medium text-bt-charcoal transition active:scale-95"
82
+ onClick={() => (playing ? client.player.pause() : client.player.resume())}>
83
+ {playing ? '❚❚' : '▶'}
84
+ </button>
85
+ <button
86
+ className="rounded-full bg-white/10 px-6 py-3 text-sm font-medium transition active:bg-white/20"
87
+ onClick={() => client.player.skipNext()}>
88
+ ▶▶
89
+ </button>
90
+ </div>
91
+ </>
92
+ ) : (
93
+ <div className="text-2xl text-bt-soft-gray">connect a phone to see now playing</div>
94
+ )}
95
+ </div>
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,52 @@
1
+ @import "tailwindcss";
2
+
3
+ @font-face {
4
+ font-family: "Outfit";
5
+ font-style: normal;
6
+ font-weight: 100 900;
7
+ font-display: block;
8
+ src: url("./fonts/Outfit-Variable.ttf") format("truetype-variations");
9
+ }
10
+
11
+ @font-face {
12
+ font-family: "Inter";
13
+ font-style: normal;
14
+ font-weight: 100 900;
15
+ font-display: block;
16
+ src: url("./fonts/Inter-Variable.ttf") format("truetype-variations");
17
+ }
18
+
19
+ @theme {
20
+ --color-bt-charcoal: #1b1f23;
21
+ --color-bt-off-white: #efefef;
22
+ --color-bt-blue: #00a8e8;
23
+ --color-bt-soft-gray: #a7adb5;
24
+
25
+ --font-display: "Outfit", system-ui, sans-serif;
26
+ --font-sans: "Inter", system-ui, sans-serif;
27
+ }
28
+
29
+ @layer base {
30
+ :root {
31
+ color-scheme: dark;
32
+ }
33
+ * {
34
+ -webkit-tap-highlight-color: transparent;
35
+ }
36
+ html,
37
+ body {
38
+ width: 800px;
39
+ height: 480px;
40
+ margin: 0;
41
+ overflow: hidden;
42
+ overscroll-behavior: none;
43
+ font-family: var(--font-sans);
44
+ background: var(--color-bt-charcoal);
45
+ color: var(--color-bt-off-white);
46
+ }
47
+ }
48
+
49
+ @utility bt-wordmark {
50
+ font-family: var(--font-display);
51
+ letter-spacing: -0.02em;
52
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App';
4
+ import './index.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,9 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_BRIDGETHING_URL?: string;
5
+ }
6
+
7
+ interface ImportMeta {
8
+ readonly env: ImportMetaEnv;
9
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noUnusedLocals": true,
10
+ "noUnusedParameters": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "allowImportingTsExtensions": true,
18
+ "verbatimModuleSyntax": true,
19
+ "useDefineForClassFields": true
20
+ },
21
+ "include": ["src"]
22
+ }
@@ -0,0 +1,14 @@
1
+ import tailwindcss from '@tailwindcss/vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { defineConfig } from 'vite';
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), tailwindcss()],
7
+ build: {
8
+ target: 'es2022',
9
+ sourcemap: true,
10
+ },
11
+ server: {
12
+ host: true,
13
+ },
14
+ });