@thomasfarineau/anvil 0.0.2 → 0.0.4

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.
Files changed (45) hide show
  1. package/README.md +21 -191
  2. package/dist/cli.cjs +557 -0
  3. package/package.json +26 -10
  4. package/src/client/config.schema.json +58 -6
  5. package/src/client/index.d.ts +60 -1
  6. package/src/rust/src/lib.rs +633 -21
  7. package/src/template/_gitignore +1 -0
  8. package/src/template/capabilities/default.json +7 -1
  9. package/src/template/config.json +2 -2
  10. package/src/template/frontends/react-js/src/App.jsx +183 -0
  11. package/src/template/frontends/react-js/src/index.html +12 -0
  12. package/src/template/frontends/react-js/src/main.jsx +5 -0
  13. package/src/template/frontends/react-js/vite.config.js +10 -0
  14. package/src/template/frontends/react-ts/src/App.tsx +190 -0
  15. package/src/template/frontends/react-ts/src/index.html +12 -0
  16. package/src/template/frontends/react-ts/src/main.tsx +5 -0
  17. package/src/template/frontends/react-ts/tsconfig.json +14 -0
  18. package/src/template/frontends/react-ts/vite.config.ts +10 -0
  19. package/src/template/frontends/solid-js/src/App.jsx +190 -0
  20. package/src/template/frontends/solid-js/src/index.html +12 -0
  21. package/src/template/frontends/solid-js/src/main.jsx +5 -0
  22. package/src/template/frontends/solid-js/vite.config.js +10 -0
  23. package/src/template/frontends/solid-ts/src/App.tsx +193 -0
  24. package/src/template/frontends/solid-ts/src/index.html +12 -0
  25. package/src/template/frontends/solid-ts/src/main.tsx +5 -0
  26. package/src/template/frontends/solid-ts/tsconfig.json +15 -0
  27. package/src/template/frontends/solid-ts/vite.config.ts +10 -0
  28. package/src/template/{src → frontends/vanilla-js/src}/index.html +110 -178
  29. package/src/template/frontends/vanilla-ts/src/index.html +51 -0
  30. package/src/template/frontends/vanilla-ts/src/main.ts +193 -0
  31. package/src/template/frontends/vanilla-ts/tsconfig.json +13 -0
  32. package/src/template/frontends/vanilla-ts/vite.config.ts +8 -0
  33. package/src/template/frontends/vue-js/src/App.vue +155 -0
  34. package/src/template/frontends/vue-js/src/index.html +12 -0
  35. package/src/template/frontends/vue-js/src/main.js +5 -0
  36. package/src/template/frontends/vue-js/vite.config.js +10 -0
  37. package/src/template/frontends/vue-ts/src/App.vue +158 -0
  38. package/src/template/frontends/vue-ts/src/index.html +12 -0
  39. package/src/template/frontends/vue-ts/src/main.ts +5 -0
  40. package/src/template/frontends/vue-ts/tsconfig.json +13 -0
  41. package/src/template/frontends/vue-ts/vite.config.ts +10 -0
  42. package/src/template/{src → shared}/api.js +38 -1
  43. package/src/template/shared/logo.svg +6 -0
  44. package/src/template/shared/style.css +226 -0
  45. package/src/cli.cjs +0 -352
@@ -1,3 +1,4 @@
1
1
  node_modules/
2
+ dist/
2
3
  src-anvil/target/
3
4
  src-anvil/gen/
@@ -3,5 +3,11 @@
3
3
  "identifier": "default",
4
4
  "description": "enables the default permissions",
5
5
  "windows": ["main"],
6
- "permissions": ["core:default"]
6
+ "permissions": [
7
+ "core:default",
8
+ "core:window:allow-close",
9
+ "core:window:allow-minimize",
10
+ "core:window:allow-toggle-maximize",
11
+ "core:window:allow-start-dragging"
12
+ ]
7
13
  }
@@ -1,12 +1,12 @@
1
1
  {
2
- "$schema": "node_modules/anvil/src/client/config.schema.json",
2
+ "$schema": "node_modules/@thomasfarineau/anvil/src/client/config.schema.json",
3
3
  "identifier": "",
4
4
  "target": "",
5
5
  "app_name": "My Launcher",
6
6
  "data_folder": ".my-launcher",
7
7
  "java_version": 21,
8
8
  "update_url": "",
9
- "logo": "",
9
+ "logo": "logo.svg",
10
10
  "session": "none",
11
11
  "window_decorations": true,
12
12
  "window_resizable": false,
@@ -0,0 +1,183 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { MC } from './api.js';
3
+
4
+ export default function App() {
5
+ const [config, setConfig] = useState(null);
6
+ const [status, setStatus] = useState(null);
7
+ const [view, setView] = useState('loading');
8
+ const [username, setUsername] = useState('');
9
+ const [dir, setDir] = useState('');
10
+ const [progress, setProgress] = useState({});
11
+ const [installing, setInstalling] = useState(false);
12
+ const [running, setRunning] = useState([]);
13
+ const [message, setMessage] = useState('');
14
+ const [error, setError] = useState('');
15
+
16
+ useEffect(() => {
17
+ (async () => {
18
+ const [cfg, settings, init] = await Promise.all([
19
+ MC.getConfig(),
20
+ MC.getSettings(),
21
+ MC.getInitStatus(),
22
+ ]);
23
+ document.title = cfg.app_name;
24
+ setConfig(cfg);
25
+ setStatus(init);
26
+ setUsername(settings.username ?? '');
27
+ setDir(init.launcher_dir);
28
+ setView(
29
+ init.java_ok && init.instances.every((i) => i.installed)
30
+ ? 'main'
31
+ : 'setup',
32
+ );
33
+
34
+ MC.on.setupProgress(({ step, current, total, label, error: err }) => {
35
+ setProgress((p) => ({
36
+ ...p,
37
+ [step]: {
38
+ pct: total > 0 ? Math.round((current * 100) / total) : 0,
39
+ label,
40
+ error: err,
41
+ },
42
+ }));
43
+ });
44
+ MC.on.setupDone(async () => {
45
+ const s = await MC.getInitStatus();
46
+ setStatus(s);
47
+ setInstalling(false);
48
+ if (s.java_ok && s.instances.every((i) => i.installed)) setView('main');
49
+ });
50
+ MC.on.gameStarting((id) => setRunning((r) => [...r, id]));
51
+ MC.on.gameExit(({ instance_id, code }) => {
52
+ setRunning((r) => r.filter((x) => x !== instance_id));
53
+ setMessage(code === 0 ? 'Session ended.' : `Exited with code ${code}.`);
54
+ });
55
+ })();
56
+ }, []);
57
+
58
+ async function install() {
59
+ if (!dir.trim()) {
60
+ setError('Choose an install folder.');
61
+ return;
62
+ }
63
+ setError('');
64
+ setInstalling(true);
65
+ try {
66
+ const settings = await MC.getSettings();
67
+ await MC.saveSettings({ ...settings, launcher_dir: dir.trim() });
68
+ await MC.runSetup();
69
+ } catch (e) {
70
+ setError(`Error: ${e}`);
71
+ setInstalling(false);
72
+ }
73
+ }
74
+
75
+ async function playOrStop(id) {
76
+ if (running.includes(id)) {
77
+ await MC.stop(id).catch(() => {});
78
+ return;
79
+ }
80
+ if (!username.trim()) return;
81
+ const settings = await MC.getSettings();
82
+ await MC.saveSettings({ ...settings, username: username.trim() });
83
+ try {
84
+ await MC.verify(id);
85
+ await MC.play(id);
86
+ } catch (e) {
87
+ setMessage(`Error: ${e}`);
88
+ }
89
+ }
90
+
91
+ if (view === 'loading') return null;
92
+
93
+ if (view === 'setup') {
94
+ const steps = [
95
+ { id: 'java', name: 'Java JRE', installed: status.java_ok },
96
+ ...config.instances.map((inst) => ({
97
+ id: inst.id,
98
+ name: inst.name,
99
+ installed:
100
+ status.instances.find((i) => i.id === inst.id)?.installed ?? false,
101
+ })),
102
+ ];
103
+ return (
104
+ <div id="setup-overlay">
105
+ <div className="card">
106
+ <h1>Setup — {config.app_name}</h1>
107
+ <label>Install folder</label>
108
+ <div className="dir-row">
109
+ <input
110
+ type="text"
111
+ value={dir}
112
+ onChange={(e) => setDir(e.target.value)}
113
+ />
114
+ <button onClick={async () => setDir(await MC.getDefaultDir())}>
115
+ Default
116
+ </button>
117
+ </div>
118
+ {steps.map((s) => {
119
+ const p = progress[s.id];
120
+ const pct = p ? p.pct : s.installed ? 100 : 0;
121
+ return (
122
+ <div className="step" key={s.id}>
123
+ <div
124
+ className="step-name"
125
+ style={{ color: s.installed ? '#4caf50' : '#aaa' }}>
126
+ {s.installed ? '✓' : '○'} {s.name}
127
+ </div>
128
+ <div className="bar-track">
129
+ <div
130
+ className="bar-fill"
131
+ style={{
132
+ width: `${pct}%`,
133
+ background: p?.error ? '#ef5350' : '#4caf50',
134
+ }}
135
+ />
136
+ </div>
137
+ <div className="step-label">
138
+ {p?.label ?? (s.installed ? 'Installed' : '')}
139
+ </div>
140
+ </div>
141
+ );
142
+ })}
143
+ <button
144
+ className="install-btn"
145
+ disabled={installing}
146
+ onClick={install}>
147
+ {installing ? 'Installing…' : 'Install'}
148
+ </button>
149
+ <p className="status" style={{ color: '#ef5350' }}>
150
+ {error}
151
+ </p>
152
+ </div>
153
+ </div>
154
+ );
155
+ }
156
+
157
+ return (
158
+ <div className="card">
159
+ <div className="brand">
160
+ {config.logo && <img src={`./${config.logo}`} alt="" />}
161
+ <h1>{config.app_name}</h1>
162
+ </div>
163
+ <label>Username</label>
164
+ <input
165
+ type="text"
166
+ maxLength={16}
167
+ placeholder="Steve"
168
+ value={username}
169
+ onChange={(e) => setUsername(e.target.value)}
170
+ />
171
+ {config.instances.map((inst) => (
172
+ <button
173
+ key={inst.id}
174
+ className="instance-btn"
175
+ disabled={!running.includes(inst.id) && !username.trim()}
176
+ onClick={() => playOrStop(inst.id)}>
177
+ {running.includes(inst.id) ? '■ Stop' : `▶ ${inst.name}`}
178
+ </button>
179
+ ))}
180
+ <p className="status">{message}</p>
181
+ </div>
182
+ );
183
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Launcher</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./main.jsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,5 @@
1
+ import { createRoot } from 'react-dom/client';
2
+ import App from './App.jsx';
3
+ import './style.css';
4
+
5
+ createRoot(document.getElementById('root')).render(<App />);
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ root: 'src',
6
+ plugins: [react()],
7
+ clearScreen: false,
8
+ server: { port: 5173, strictPort: true },
9
+ build: { outDir: '../dist', emptyOutDir: true, target: 'es2022' },
10
+ });
@@ -0,0 +1,190 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { MC } from './api.js';
3
+ import type { InitStatus, LauncherConfig } from './api.js';
4
+
5
+ interface StepProgress {
6
+ pct: number;
7
+ label: string;
8
+ error: boolean;
9
+ }
10
+
11
+ export default function App() {
12
+ const [config, setConfig] = useState<LauncherConfig | null>(null);
13
+ const [status, setStatus] = useState<InitStatus | null>(null);
14
+ const [view, setView] = useState<'loading' | 'setup' | 'main'>('loading');
15
+ const [username, setUsername] = useState('');
16
+ const [dir, setDir] = useState('');
17
+ const [progress, setProgress] = useState<Record<string, StepProgress>>({});
18
+ const [installing, setInstalling] = useState(false);
19
+ const [running, setRunning] = useState<string[]>([]);
20
+ const [message, setMessage] = useState('');
21
+ const [error, setError] = useState('');
22
+
23
+ useEffect(() => {
24
+ (async () => {
25
+ const [cfg, settings, init] = await Promise.all([
26
+ MC.getConfig(),
27
+ MC.getSettings(),
28
+ MC.getInitStatus(),
29
+ ]);
30
+ document.title = cfg.app_name;
31
+ setConfig(cfg);
32
+ setStatus(init);
33
+ setUsername(settings.username ?? '');
34
+ setDir(init.launcher_dir);
35
+ setView(
36
+ init.java_ok && init.instances.every((i) => i.installed)
37
+ ? 'main'
38
+ : 'setup',
39
+ );
40
+
41
+ MC.on.setupProgress(({ step, current, total, label, error: err }) => {
42
+ setProgress((p) => ({
43
+ ...p,
44
+ [step]: {
45
+ pct: total > 0 ? Math.round((current * 100) / total) : 0,
46
+ label,
47
+ error: err,
48
+ },
49
+ }));
50
+ });
51
+ MC.on.setupDone(async () => {
52
+ const s = await MC.getInitStatus();
53
+ setStatus(s);
54
+ setInstalling(false);
55
+ if (s.java_ok && s.instances.every((i) => i.installed)) setView('main');
56
+ });
57
+ MC.on.gameStarting((id) => setRunning((r) => [...r, id]));
58
+ MC.on.gameExit(({ instance_id, code }) => {
59
+ setRunning((r) => r.filter((x) => x !== instance_id));
60
+ setMessage(code === 0 ? 'Session ended.' : `Exited with code ${code}.`);
61
+ });
62
+ })();
63
+ }, []);
64
+
65
+ async function install() {
66
+ if (!dir.trim()) {
67
+ setError('Choose an install folder.');
68
+ return;
69
+ }
70
+ setError('');
71
+ setInstalling(true);
72
+ try {
73
+ const settings = await MC.getSettings();
74
+ await MC.saveSettings({ ...settings, launcher_dir: dir.trim() });
75
+ await MC.runSetup();
76
+ } catch (e) {
77
+ setError(`Error: ${e}`);
78
+ setInstalling(false);
79
+ }
80
+ }
81
+
82
+ async function playOrStop(id: string) {
83
+ if (running.includes(id)) {
84
+ await MC.stop(id).catch(() => {});
85
+ return;
86
+ }
87
+ if (!username.trim()) return;
88
+ const settings = await MC.getSettings();
89
+ await MC.saveSettings({ ...settings, username: username.trim() });
90
+ try {
91
+ await MC.verify(id);
92
+ await MC.play(id);
93
+ } catch (e) {
94
+ setMessage(`Error: ${e}`);
95
+ }
96
+ }
97
+
98
+ if (view === 'loading' || !config || !status) return null;
99
+
100
+ if (view === 'setup') {
101
+ const steps = [
102
+ { id: 'java', name: 'Java JRE', installed: status.java_ok },
103
+ ...config.instances.map((inst) => ({
104
+ id: inst.id,
105
+ name: inst.name,
106
+ installed:
107
+ status.instances.find((i) => i.id === inst.id)?.installed ?? false,
108
+ })),
109
+ ];
110
+ return (
111
+ <div id="setup-overlay">
112
+ <div className="card">
113
+ <h1>Setup — {config.app_name}</h1>
114
+ <label>Install folder</label>
115
+ <div className="dir-row">
116
+ <input
117
+ type="text"
118
+ value={dir}
119
+ onChange={(e) => setDir(e.target.value)}
120
+ />
121
+ <button onClick={async () => setDir(await MC.getDefaultDir())}>
122
+ Default
123
+ </button>
124
+ </div>
125
+ {steps.map((s) => {
126
+ const p = progress[s.id];
127
+ const pct = p ? p.pct : s.installed ? 100 : 0;
128
+ return (
129
+ <div className="step" key={s.id}>
130
+ <div
131
+ className="step-name"
132
+ style={{ color: s.installed ? '#4caf50' : '#aaa' }}>
133
+ {s.installed ? '✓' : '○'} {s.name}
134
+ </div>
135
+ <div className="bar-track">
136
+ <div
137
+ className="bar-fill"
138
+ style={{
139
+ width: `${pct}%`,
140
+ background: p?.error ? '#ef5350' : '#4caf50',
141
+ }}
142
+ />
143
+ </div>
144
+ <div className="step-label">
145
+ {p?.label ?? (s.installed ? 'Installed' : '')}
146
+ </div>
147
+ </div>
148
+ );
149
+ })}
150
+ <button
151
+ className="install-btn"
152
+ disabled={installing}
153
+ onClick={install}>
154
+ {installing ? 'Installing…' : 'Install'}
155
+ </button>
156
+ <p className="status" style={{ color: '#ef5350' }}>
157
+ {error}
158
+ </p>
159
+ </div>
160
+ </div>
161
+ );
162
+ }
163
+
164
+ return (
165
+ <div className="card">
166
+ <div className="brand">
167
+ {config.logo && <img src={`./${config.logo}`} alt="" />}
168
+ <h1>{config.app_name}</h1>
169
+ </div>
170
+ <label>Username</label>
171
+ <input
172
+ type="text"
173
+ maxLength={16}
174
+ placeholder="Steve"
175
+ value={username}
176
+ onChange={(e) => setUsername(e.target.value)}
177
+ />
178
+ {config.instances.map((inst) => (
179
+ <button
180
+ key={inst.id}
181
+ className="instance-btn"
182
+ disabled={!running.includes(inst.id) && !username.trim()}
183
+ onClick={() => playOrStop(inst.id)}>
184
+ {running.includes(inst.id) ? '■ Stop' : `▶ ${inst.name}`}
185
+ </button>
186
+ ))}
187
+ <p className="status">{message}</p>
188
+ </div>
189
+ );
190
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Launcher</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,5 @@
1
+ import { createRoot } from 'react-dom/client';
2
+ import App from './App.tsx';
3
+ import './style.css';
4
+
5
+ createRoot(document.getElementById('root')!).render(<App />);
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "skipLibCheck": true,
10
+ "useDefineForClassFields": true,
11
+ "lib": ["ES2022", "DOM", "DOM.Iterable"]
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ root: 'src',
6
+ plugins: [react()],
7
+ clearScreen: false,
8
+ server: { port: 5173, strictPort: true },
9
+ build: { outDir: '../dist', emptyOutDir: true, target: 'es2022' },
10
+ });
@@ -0,0 +1,190 @@
1
+ import { For, Show, createSignal, onMount } from 'solid-js';
2
+ import { MC } from './api.js';
3
+
4
+ export default function App() {
5
+ const [config, setConfig] = createSignal(null);
6
+ const [status, setStatus] = createSignal(null);
7
+ const [view, setView] = createSignal('loading');
8
+ const [username, setUsername] = createSignal('');
9
+ const [dir, setDir] = createSignal('');
10
+ const [progress, setProgress] = createSignal({});
11
+ const [installing, setInstalling] = createSignal(false);
12
+ const [running, setRunning] = createSignal([]);
13
+ const [message, setMessage] = createSignal('');
14
+ const [error, setError] = createSignal('');
15
+
16
+ onMount(async () => {
17
+ const [cfg, settings, init] = await Promise.all([
18
+ MC.getConfig(),
19
+ MC.getSettings(),
20
+ MC.getInitStatus(),
21
+ ]);
22
+ document.title = cfg.app_name;
23
+ setConfig(cfg);
24
+ setStatus(init);
25
+ setUsername(settings.username ?? '');
26
+ setDir(init.launcher_dir);
27
+ setView(
28
+ init.java_ok && init.instances.every((i) => i.installed)
29
+ ? 'main'
30
+ : 'setup',
31
+ );
32
+
33
+ MC.on.setupProgress(({ step, current, total, label, error: err }) => {
34
+ setProgress((p) => ({
35
+ ...p,
36
+ [step]: {
37
+ pct: total > 0 ? Math.round((current * 100) / total) : 0,
38
+ label,
39
+ error: err,
40
+ },
41
+ }));
42
+ });
43
+ MC.on.setupDone(async () => {
44
+ const s = await MC.getInitStatus();
45
+ setStatus(s);
46
+ setInstalling(false);
47
+ if (s.java_ok && s.instances.every((i) => i.installed)) setView('main');
48
+ });
49
+ MC.on.gameStarting((id) => setRunning((r) => [...r, id]));
50
+ MC.on.gameExit(({ instance_id, code }) => {
51
+ setRunning((r) => r.filter((x) => x !== instance_id));
52
+ setMessage(code === 0 ? 'Session ended.' : `Exited with code ${code}.`);
53
+ });
54
+ });
55
+
56
+ const steps = () => {
57
+ if (!config() || !status()) return [];
58
+ return [
59
+ { id: 'java', name: 'Java JRE', installed: status().java_ok },
60
+ ...config().instances.map((inst) => ({
61
+ id: inst.id,
62
+ name: inst.name,
63
+ installed:
64
+ status().instances.find((i) => i.id === inst.id)?.installed ?? false,
65
+ })),
66
+ ];
67
+ };
68
+
69
+ async function install() {
70
+ if (!dir().trim()) {
71
+ setError('Choose an install folder.');
72
+ return;
73
+ }
74
+ setError('');
75
+ setInstalling(true);
76
+ try {
77
+ const settings = await MC.getSettings();
78
+ await MC.saveSettings({ ...settings, launcher_dir: dir().trim() });
79
+ await MC.runSetup();
80
+ } catch (e) {
81
+ setError(`Error: ${e}`);
82
+ setInstalling(false);
83
+ }
84
+ }
85
+
86
+ async function playOrStop(id) {
87
+ if (running().includes(id)) {
88
+ await MC.stop(id).catch(() => {});
89
+ return;
90
+ }
91
+ if (!username().trim()) return;
92
+ const settings = await MC.getSettings();
93
+ await MC.saveSettings({ ...settings, username: username().trim() });
94
+ try {
95
+ await MC.verify(id);
96
+ await MC.play(id);
97
+ } catch (e) {
98
+ setMessage(`Error: ${e}`);
99
+ }
100
+ }
101
+
102
+ return (
103
+ <>
104
+ <Show when={view() === 'setup'}>
105
+ <div id="setup-overlay">
106
+ <div class="card">
107
+ <h1>Setup — {config().app_name}</h1>
108
+ <label>Install folder</label>
109
+ <div class="dir-row">
110
+ <input
111
+ type="text"
112
+ value={dir()}
113
+ onInput={(e) => setDir(e.currentTarget.value)}
114
+ />
115
+ <button onClick={async () => setDir(await MC.getDefaultDir())}>
116
+ Default
117
+ </button>
118
+ </div>
119
+ <For each={steps()}>
120
+ {(s) => {
121
+ const p = () => progress()[s.id];
122
+ const pct = () => p()?.pct ?? (s.installed ? 100 : 0);
123
+ return (
124
+ <div class="step">
125
+ <div
126
+ class="step-name"
127
+ style={{ color: s.installed ? '#4caf50' : '#aaa' }}>
128
+ {s.installed ? '✓' : '○'} {s.name}
129
+ </div>
130
+ <div class="bar-track">
131
+ <div
132
+ class="bar-fill"
133
+ style={{
134
+ width: `${pct()}%`,
135
+ background: p()?.error ? '#ef5350' : '#4caf50',
136
+ }}
137
+ />
138
+ </div>
139
+ <div class="step-label">
140
+ {p()?.label ?? (s.installed ? 'Installed' : '')}
141
+ </div>
142
+ </div>
143
+ );
144
+ }}
145
+ </For>
146
+ <button
147
+ class="install-btn"
148
+ disabled={installing()}
149
+ onClick={install}>
150
+ {installing() ? 'Installing…' : 'Install'}
151
+ </button>
152
+ <p class="status" style={{ color: '#ef5350' }}>
153
+ {error()}
154
+ </p>
155
+ </div>
156
+ </div>
157
+ </Show>
158
+
159
+ <Show when={view() === 'main'}>
160
+ <div class="card">
161
+ <div class="brand">
162
+ <Show when={config().logo}>
163
+ <img src={`./${config().logo}`} alt="" />
164
+ </Show>
165
+ <h1>{config().app_name}</h1>
166
+ </div>
167
+ <label>Username</label>
168
+ <input
169
+ type="text"
170
+ maxLength={16}
171
+ placeholder="Steve"
172
+ value={username()}
173
+ onInput={(e) => setUsername(e.currentTarget.value)}
174
+ />
175
+ <For each={config().instances}>
176
+ {(inst) => (
177
+ <button
178
+ class="instance-btn"
179
+ disabled={!running().includes(inst.id) && !username().trim()}
180
+ onClick={() => playOrStop(inst.id)}>
181
+ {running().includes(inst.id) ? '■ Stop' : `▶ ${inst.name}`}
182
+ </button>
183
+ )}
184
+ </For>
185
+ <p class="status">{message()}</p>
186
+ </div>
187
+ </Show>
188
+ </>
189
+ );
190
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Launcher</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="./main.jsx"></script>
11
+ </body>
12
+ </html>