@thomasfarineau/anvil 0.0.2 → 0.0.3

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 +18 -193
  2. package/dist/cli.cjs +554 -0
  3. package/package.json +19 -6
  4. package/src/client/config.schema.json +24 -1
  5. package/src/client/index.d.ts +36 -0
  6. package/src/rust/src/lib.rs +320 -17
  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 +27 -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
@@ -0,0 +1,193 @@
1
+ import { MC } from './api.js';
2
+ import type { InitStatus, LauncherConfig } from './api.js';
3
+
4
+ const $ = <T extends HTMLElement>(id: string): T =>
5
+ document.getElementById(id) as T;
6
+
7
+ // ── Boot ────────────────────────────────────────────────────
8
+ const [config, settings, initStatus] = await Promise.all([
9
+ MC.getConfig(),
10
+ MC.getSettings(),
11
+ MC.getInitStatus(),
12
+ ]);
13
+
14
+ if (settings.username) {
15
+ $<HTMLInputElement>('username-input').value = settings.username;
16
+ }
17
+
18
+ document.title = config.app_name;
19
+
20
+ const allInstalled =
21
+ initStatus.java_ok && initStatus.instances.every((i) => i.installed);
22
+
23
+ if (allInstalled) {
24
+ showMain(config);
25
+ } else {
26
+ showSetup(config, initStatus);
27
+ }
28
+
29
+ // ── Main page ────────────────────────────────────────────────
30
+ function showMain(cfg: LauncherConfig): void {
31
+ $('main-view').style.display = 'flex';
32
+ $('server-name').textContent = cfg.app_name;
33
+
34
+ if (cfg.logo) {
35
+ const logo = $<HTMLImageElement>('logo');
36
+ logo.src = `./${cfg.logo}`;
37
+ logo.style.display = 'block';
38
+ }
39
+
40
+ const container = $('instances-container');
41
+ for (const inst of cfg.instances) {
42
+ const btn = document.createElement('button');
43
+ btn.className = 'instance-btn';
44
+ btn.id = `play-${inst.id}`;
45
+ btn.textContent = `▶ ${inst.name}`;
46
+ btn.disabled = true;
47
+ btn.addEventListener('click', () => onPlay(inst.id));
48
+ container.appendChild(btn);
49
+ }
50
+
51
+ $('username-input').addEventListener('input', updateButtons);
52
+ updateButtons();
53
+
54
+ MC.on.gameStarting((id) => {
55
+ const btn = $<HTMLButtonElement>(`play-${id}`);
56
+ btn.textContent = '■ Stop';
57
+ btn.disabled = false;
58
+ btn.dataset.running = '1';
59
+ });
60
+ MC.on.gameExit(({ instance_id, code }) => {
61
+ const btn = $<HTMLButtonElement>(`play-${instance_id}`);
62
+ btn.textContent = `▶ ${cfg.instances.find((i) => i.id === instance_id)?.name ?? instance_id}`;
63
+ delete btn.dataset.running;
64
+ updateButtons();
65
+ $('game-status').textContent =
66
+ code === 0 ? 'Session ended.' : `Exited with code ${code}.`;
67
+ });
68
+
69
+ // Check for updates silently
70
+ MC.checkUpdate()
71
+ .then((info) => {
72
+ if (info) {
73
+ const ok = confirm(
74
+ `Update ${info.version} available.\n\n${info.notes}\n\nInstall now?`,
75
+ );
76
+ if (ok) MC.doUpdate(info.url);
77
+ }
78
+ })
79
+ .catch(() => {});
80
+ }
81
+
82
+ function updateButtons(): void {
83
+ const username = $<HTMLInputElement>('username-input').value.trim();
84
+ document
85
+ .querySelectorAll<HTMLButtonElement>('.instance-btn')
86
+ .forEach((btn) => {
87
+ if (!btn.dataset.running) btn.disabled = !username;
88
+ });
89
+ }
90
+
91
+ async function onPlay(instanceId: string): Promise<void> {
92
+ const btn = $<HTMLButtonElement>(`play-${instanceId}`);
93
+ if (btn.dataset.running) {
94
+ await MC.stop(instanceId).catch(() => {});
95
+ return;
96
+ }
97
+ const username = $<HTMLInputElement>('username-input').value.trim();
98
+ if (!username) return;
99
+ const current = await MC.getSettings();
100
+ await MC.saveSettings({ ...current, username });
101
+ try {
102
+ await MC.verify(instanceId);
103
+ await MC.play(instanceId);
104
+ } catch (e) {
105
+ alert(`Error: ${e}`);
106
+ }
107
+ }
108
+
109
+ // ── Setup page ───────────────────────────────────────────────
110
+ function showSetup(cfg: LauncherConfig, init: InitStatus): void {
111
+ $('setup-overlay').style.display = 'flex';
112
+ $('setup-title').textContent = `Setup — ${cfg.app_name}`;
113
+ $<HTMLInputElement>('dir-input').value = init.launcher_dir;
114
+
115
+ $('dir-default-btn').addEventListener('click', async () => {
116
+ $<HTMLInputElement>('dir-input').value = await MC.getDefaultDir();
117
+ });
118
+
119
+ renderSteps(init, cfg);
120
+
121
+ $('install-btn').addEventListener('click', onInstall);
122
+
123
+ // Live progress
124
+ MC.on.setupProgress(({ step, current, total, label, error }) => {
125
+ const pct = total > 0 ? Math.round((current * 100) / total) : 0;
126
+ const fill = document.getElementById(`bar-${step}`);
127
+ const lbl = document.getElementById(`lbl-${step}`);
128
+ if (fill) {
129
+ fill.style.width = `${pct}%`;
130
+ fill.style.background = error ? '#ef5350' : '#4caf50';
131
+ }
132
+ if (lbl) lbl.textContent = label;
133
+ });
134
+
135
+ MC.on.setupDone(async () => {
136
+ const status = await MC.getInitStatus();
137
+ if (status.java_ok && status.instances.every((i) => i.installed)) {
138
+ $('setup-overlay').style.display = 'none';
139
+ showMain(cfg);
140
+ } else {
141
+ const btn = $<HTMLButtonElement>('install-btn');
142
+ btn.textContent = 'Retry';
143
+ btn.disabled = false;
144
+ }
145
+ });
146
+ }
147
+
148
+ function renderSteps(init: InitStatus, cfg: LauncherConfig): void {
149
+ const steps = [
150
+ { id: 'java', name: 'Java JRE', installed: init.java_ok },
151
+ ...cfg.instances.map((inst) => ({
152
+ id: inst.id,
153
+ name: inst.name,
154
+ installed:
155
+ init.instances.find((i) => i.id === inst.id)?.installed ?? false,
156
+ })),
157
+ ];
158
+ $('steps-container').innerHTML = steps
159
+ .map(
160
+ (s) => `
161
+ <div class="step">
162
+ <div class="step-name" style="color:${s.installed ? '#4caf50' : '#aaa'}">
163
+ ${s.installed ? '✓' : '○'} ${s.name}
164
+ </div>
165
+ <div class="bar-track"><div class="bar-fill" id="bar-${s.id}" style="width:${s.installed ? 100 : 0}%"></div></div>
166
+ <div class="step-label" id="lbl-${s.id}">${s.installed ? 'Installed' : ''}</div>
167
+ </div>
168
+ `,
169
+ )
170
+ .join('');
171
+ }
172
+
173
+ async function onInstall(): Promise<void> {
174
+ const dir = $<HTMLInputElement>('dir-input').value.trim();
175
+ if (!dir) {
176
+ $('setup-error').textContent = 'Choose an install folder.';
177
+ return;
178
+ }
179
+ const btn = $<HTMLButtonElement>('install-btn');
180
+ btn.disabled = true;
181
+ btn.textContent = 'Installing…';
182
+ $('setup-error').textContent = '';
183
+
184
+ try {
185
+ const current = await MC.getSettings();
186
+ await MC.saveSettings({ ...current, launcher_dir: dir });
187
+ await MC.runSetup();
188
+ } catch (e) {
189
+ $('setup-error').textContent = `Error: ${e}`;
190
+ btn.disabled = false;
191
+ btn.textContent = 'Retry';
192
+ }
193
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "useDefineForClassFields": true,
10
+ "lib": ["ES2022", "DOM", "DOM.Iterable"]
11
+ },
12
+ "include": ["src"]
13
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({
4
+ root: 'src',
5
+ clearScreen: false,
6
+ server: { port: 5173, strictPort: true },
7
+ build: { outDir: '../dist', emptyOutDir: true, target: 'es2022' },
8
+ });
@@ -0,0 +1,155 @@
1
+ <script setup>
2
+ import { computed, onMounted, reactive, ref } from 'vue';
3
+ import { MC } from './api.js';
4
+
5
+ const view = ref('loading');
6
+ const config = ref(null);
7
+ const status = ref(null);
8
+ const username = ref('');
9
+ const dir = ref('');
10
+ const progress = reactive({});
11
+ const installing = ref(false);
12
+ const running = ref([]);
13
+ const message = ref('');
14
+ const error = ref('');
15
+
16
+ const steps = computed(() => {
17
+ if (!config.value || !status.value) return [];
18
+ return [
19
+ { id: 'java', name: 'Java JRE', installed: status.value.java_ok },
20
+ ...config.value.instances.map((inst) => ({
21
+ id: inst.id,
22
+ name: inst.name,
23
+ installed:
24
+ status.value.instances.find((i) => i.id === inst.id)?.installed ??
25
+ false,
26
+ })),
27
+ ];
28
+ });
29
+
30
+ onMounted(async () => {
31
+ const [cfg, settings, init] = await Promise.all([
32
+ MC.getConfig(),
33
+ MC.getSettings(),
34
+ MC.getInitStatus(),
35
+ ]);
36
+ document.title = cfg.app_name;
37
+ config.value = cfg;
38
+ status.value = init;
39
+ username.value = settings.username ?? '';
40
+ dir.value = init.launcher_dir;
41
+ view.value =
42
+ init.java_ok && init.instances.every((i) => i.installed) ? 'main' : 'setup';
43
+
44
+ MC.on.setupProgress(({ step, current, total, label, error: err }) => {
45
+ progress[step] = {
46
+ pct: total > 0 ? Math.round((current * 100) / total) : 0,
47
+ label,
48
+ error: err,
49
+ };
50
+ });
51
+ MC.on.setupDone(async () => {
52
+ const s = await MC.getInitStatus();
53
+ status.value = s;
54
+ installing.value = false;
55
+ if (s.java_ok && s.instances.every((i) => i.installed)) {
56
+ view.value = 'main';
57
+ }
58
+ });
59
+ MC.on.gameStarting((id) => running.value.push(id));
60
+ MC.on.gameExit(({ instance_id, code }) => {
61
+ running.value = running.value.filter((x) => x !== instance_id);
62
+ message.value = code === 0 ? 'Session ended.' : `Exited with code ${code}.`;
63
+ });
64
+ });
65
+
66
+ async function setDefaultDir() {
67
+ dir.value = await MC.getDefaultDir();
68
+ }
69
+
70
+ async function install() {
71
+ if (!dir.value.trim()) {
72
+ error.value = 'Choose an install folder.';
73
+ return;
74
+ }
75
+ error.value = '';
76
+ installing.value = true;
77
+ try {
78
+ const settings = await MC.getSettings();
79
+ await MC.saveSettings({ ...settings, launcher_dir: dir.value.trim() });
80
+ await MC.runSetup();
81
+ } catch (e) {
82
+ error.value = `Error: ${e}`;
83
+ installing.value = false;
84
+ }
85
+ }
86
+
87
+ async function playOrStop(id) {
88
+ if (running.value.includes(id)) {
89
+ await MC.stop(id).catch(() => {});
90
+ return;
91
+ }
92
+ if (!username.value.trim()) return;
93
+ const settings = await MC.getSettings();
94
+ await MC.saveSettings({ ...settings, username: username.value.trim() });
95
+ try {
96
+ await MC.verify(id);
97
+ await MC.play(id);
98
+ } catch (e) {
99
+ message.value = `Error: ${e}`;
100
+ }
101
+ }
102
+ </script>
103
+
104
+ <template>
105
+ <div v-if="view === 'setup'" 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 v-model="dir" type="text" />
111
+ <button @click="setDefaultDir">Default</button>
112
+ </div>
113
+ <div v-for="s in steps" :key="s.id" class="step">
114
+ <div
115
+ class="step-name"
116
+ :style="{ color: s.installed ? '#4caf50' : '#aaa' }">
117
+ {{ s.installed ? '✓' : '○' }} {{ s.name }}
118
+ </div>
119
+ <div class="bar-track">
120
+ <div
121
+ class="bar-fill"
122
+ :style="{
123
+ width: `${progress[s.id]?.pct ?? (s.installed ? 100 : 0)}%`,
124
+ background: progress[s.id]?.error ? '#ef5350' : '#4caf50',
125
+ }" />
126
+ </div>
127
+ <div class="step-label">
128
+ {{ progress[s.id]?.label ?? (s.installed ? 'Installed' : '') }}
129
+ </div>
130
+ </div>
131
+ <button class="install-btn" :disabled="installing" @click="install">
132
+ {{ installing ? 'Installing…' : 'Install' }}
133
+ </button>
134
+ <p class="status" style="color: #ef5350">{{ error }}</p>
135
+ </div>
136
+ </div>
137
+
138
+ <div v-else-if="view === 'main'" class="card">
139
+ <div class="brand">
140
+ <img v-if="config.logo" :src="`./${config.logo}`" alt="" />
141
+ <h1>{{ config.app_name }}</h1>
142
+ </div>
143
+ <label>Username</label>
144
+ <input v-model="username" type="text" maxlength="16" placeholder="Steve" />
145
+ <button
146
+ v-for="inst in config.instances"
147
+ :key="inst.id"
148
+ class="instance-btn"
149
+ :disabled="!running.includes(inst.id) && !username.trim()"
150
+ @click="playOrStop(inst.id)">
151
+ {{ running.includes(inst.id) ? '■ Stop' : `▶ ${inst.name}` }}
152
+ </button>
153
+ <p class="status">{{ message }}</p>
154
+ </div>
155
+ </template>
@@ -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="app"></div>
10
+ <script type="module" src="./main.js"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,5 @@
1
+ import { createApp } from 'vue';
2
+ import App from './App.vue';
3
+ import './style.css';
4
+
5
+ createApp(App).mount('#app');
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vite';
2
+ import vue from '@vitejs/plugin-vue';
3
+
4
+ export default defineConfig({
5
+ root: 'src',
6
+ plugins: [vue()],
7
+ clearScreen: false,
8
+ server: { port: 5173, strictPort: true },
9
+ build: { outDir: '../dist', emptyOutDir: true, target: 'es2022' },
10
+ });
@@ -0,0 +1,158 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, reactive, ref } from 'vue';
3
+ import { MC } from './api.js';
4
+ import type { InitStatus, LauncherConfig } from './api.js';
5
+
6
+ const view = ref<'loading' | 'setup' | 'main'>('loading');
7
+ const config = ref<LauncherConfig | null>(null);
8
+ const status = ref<InitStatus | null>(null);
9
+ const username = ref('');
10
+ const dir = ref('');
11
+ const progress = reactive<
12
+ Record<string, { pct: number; label: string; error: boolean }>
13
+ >({});
14
+ const installing = ref(false);
15
+ const running = ref<string[]>([]);
16
+ const message = ref('');
17
+ const error = ref('');
18
+
19
+ const steps = computed(() => {
20
+ if (!config.value || !status.value) return [];
21
+ return [
22
+ { id: 'java', name: 'Java JRE', installed: status.value.java_ok },
23
+ ...config.value.instances.map((inst) => ({
24
+ id: inst.id,
25
+ name: inst.name,
26
+ installed:
27
+ status.value.instances.find((i) => i.id === inst.id)?.installed ??
28
+ false,
29
+ })),
30
+ ];
31
+ });
32
+
33
+ onMounted(async () => {
34
+ const [cfg, settings, init] = await Promise.all([
35
+ MC.getConfig(),
36
+ MC.getSettings(),
37
+ MC.getInitStatus(),
38
+ ]);
39
+ document.title = cfg.app_name;
40
+ config.value = cfg;
41
+ status.value = init;
42
+ username.value = settings.username ?? '';
43
+ dir.value = init.launcher_dir;
44
+ view.value =
45
+ init.java_ok && init.instances.every((i) => i.installed) ? 'main' : 'setup';
46
+
47
+ MC.on.setupProgress(({ step, current, total, label, error: err }) => {
48
+ progress[step] = {
49
+ pct: total > 0 ? Math.round((current * 100) / total) : 0,
50
+ label,
51
+ error: err,
52
+ };
53
+ });
54
+ MC.on.setupDone(async () => {
55
+ const s = await MC.getInitStatus();
56
+ status.value = s;
57
+ installing.value = false;
58
+ if (s.java_ok && s.instances.every((i) => i.installed)) {
59
+ view.value = 'main';
60
+ }
61
+ });
62
+ MC.on.gameStarting((id) => running.value.push(id));
63
+ MC.on.gameExit(({ instance_id, code }) => {
64
+ running.value = running.value.filter((x) => x !== instance_id);
65
+ message.value = code === 0 ? 'Session ended.' : `Exited with code ${code}.`;
66
+ });
67
+ });
68
+
69
+ async function setDefaultDir() {
70
+ dir.value = await MC.getDefaultDir();
71
+ }
72
+
73
+ async function install() {
74
+ if (!dir.value.trim()) {
75
+ error.value = 'Choose an install folder.';
76
+ return;
77
+ }
78
+ error.value = '';
79
+ installing.value = true;
80
+ try {
81
+ const settings = await MC.getSettings();
82
+ await MC.saveSettings({ ...settings, launcher_dir: dir.value.trim() });
83
+ await MC.runSetup();
84
+ } catch (e) {
85
+ error.value = `Error: ${e}`;
86
+ installing.value = false;
87
+ }
88
+ }
89
+
90
+ async function playOrStop(id: string) {
91
+ if (running.value.includes(id)) {
92
+ await MC.stop(id).catch(() => {});
93
+ return;
94
+ }
95
+ if (!username.value.trim()) return;
96
+ const settings = await MC.getSettings();
97
+ await MC.saveSettings({ ...settings, username: username.value.trim() });
98
+ try {
99
+ await MC.verify(id);
100
+ await MC.play(id);
101
+ } catch (e) {
102
+ message.value = `Error: ${e}`;
103
+ }
104
+ }
105
+ </script>
106
+
107
+ <template>
108
+ <div v-if="view === 'setup'" id="setup-overlay">
109
+ <div class="card">
110
+ <h1>Setup — {{ config.app_name }}</h1>
111
+ <label>Install folder</label>
112
+ <div class="dir-row">
113
+ <input v-model="dir" type="text" />
114
+ <button @click="setDefaultDir">Default</button>
115
+ </div>
116
+ <div v-for="s in steps" :key="s.id" class="step">
117
+ <div
118
+ class="step-name"
119
+ :style="{ color: s.installed ? '#4caf50' : '#aaa' }">
120
+ {{ s.installed ? '✓' : '○' }} {{ s.name }}
121
+ </div>
122
+ <div class="bar-track">
123
+ <div
124
+ class="bar-fill"
125
+ :style="{
126
+ width: `${progress[s.id]?.pct ?? (s.installed ? 100 : 0)}%`,
127
+ background: progress[s.id]?.error ? '#ef5350' : '#4caf50',
128
+ }" />
129
+ </div>
130
+ <div class="step-label">
131
+ {{ progress[s.id]?.label ?? (s.installed ? 'Installed' : '') }}
132
+ </div>
133
+ </div>
134
+ <button class="install-btn" :disabled="installing" @click="install">
135
+ {{ installing ? 'Installing…' : 'Install' }}
136
+ </button>
137
+ <p class="status" style="color: #ef5350">{{ error }}</p>
138
+ </div>
139
+ </div>
140
+
141
+ <div v-else-if="view === 'main'" class="card">
142
+ <div class="brand">
143
+ <img v-if="config.logo" :src="`./${config.logo}`" alt="" />
144
+ <h1>{{ config.app_name }}</h1>
145
+ </div>
146
+ <label>Username</label>
147
+ <input v-model="username" type="text" maxlength="16" placeholder="Steve" />
148
+ <button
149
+ v-for="inst in config.instances"
150
+ :key="inst.id"
151
+ class="instance-btn"
152
+ :disabled="!running.includes(inst.id) && !username.trim()"
153
+ @click="playOrStop(inst.id)">
154
+ {{ running.includes(inst.id) ? '■ Stop' : `▶ ${inst.name}` }}
155
+ </button>
156
+ <p class="status">{{ message }}</p>
157
+ </div>
158
+ </template>
@@ -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="app"></div>
10
+ <script type="module" src="./main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,5 @@
1
+ import { createApp } from 'vue';
2
+ import App from './App.vue';
3
+ import './style.css';
4
+
5
+ createApp(App).mount('#app');
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "useDefineForClassFields": true,
10
+ "lib": ["ES2022", "DOM", "DOM.Iterable"]
11
+ },
12
+ "include": ["src"]
13
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vite';
2
+ import vue from '@vitejs/plugin-vue';
3
+
4
+ export default defineConfig({
5
+ root: 'src',
6
+ plugins: [vue()],
7
+ clearScreen: false,
8
+ server: { port: 5173, strictPort: true },
9
+ build: { outDir: '../dist', emptyOutDir: true, target: 'es2022' },
10
+ });
@@ -1,4 +1,4 @@
1
- // tauri-mc-launcher client API
1
+ // anvil client API — JS ↔ Rust bridge.
2
2
  // Requires withGlobalTauri: true in tauri.conf.json (set by default).
3
3
  // Import this file as an ES module: import { MC } from '/api.js';
4
4
 
@@ -12,6 +12,7 @@ export const MC = {
12
12
  getSettings: () => _invoke('get_settings'),
13
13
  saveSettings: (settings) => _invoke('save_settings', { settings }),
14
14
  getDefaultDir: () => _invoke('get_default_launcher_dir'),
15
+ getVersion: () => _invoke('get_launcher_version'),
15
16
 
16
17
  // ── Init / install ─────────────────────────────────────────
17
18
  getInitStatus: () => _invoke('get_init_status'),
@@ -20,6 +21,28 @@ export const MC = {
20
21
  // ── Game ───────────────────────────────────────────────────
21
22
  verify: (instanceId) => _invoke('verify_game', { instanceId }),
22
23
  play: (instanceId) => _invoke('launch_game', { instanceId }),
24
+ stop: (instanceId) => _invoke('stop_game', { instanceId }),
25
+ getRunning: () => _invoke('get_running_instances'),
26
+ isRunning: (instanceId) =>
27
+ _invoke('get_running_instances').then((ids) => ids.includes(instanceId)),
28
+
29
+ // ── Mods (per instance) ────────────────────────────────────
30
+ mods: {
31
+ list: (instanceId) => _invoke('get_mods', { instanceId }),
32
+ add: (instanceId, url, fileName = null) =>
33
+ _invoke('add_mod', { instanceId, url, fileName }),
34
+ remove: (instanceId, fileName) =>
35
+ _invoke('remove_mod', { instanceId, fileName }),
36
+ enable: (instanceId, fileName) =>
37
+ _invoke('set_mod_enabled', { instanceId, fileName, enabled: true }),
38
+ disable: (instanceId, fileName) =>
39
+ _invoke('set_mod_enabled', { instanceId, fileName, enabled: false }),
40
+ openFolder: (instanceId) => _invoke('open_mods_folder', { instanceId }),
41
+ },
42
+
43
+ // ── Folders ────────────────────────────────────────────────
44
+ openInstanceFolder: (instanceId) =>
45
+ _invoke('open_instance_folder', { instanceId }),
23
46
 
24
47
  // ── Updater ────────────────────────────────────────────────
25
48
  checkUpdate: () => _invoke('check_update'),
@@ -31,6 +54,9 @@ export const MC = {
31
54
 
32
55
  // ── Window ─────────────────────────────────────────────────
33
56
  close: () => _window.getCurrentWindow().close(),
57
+ minimize: () => _window.getCurrentWindow().minimize(),
58
+ toggleMaximize: () => _window.getCurrentWindow().toggleMaximize(),
59
+ startDrag: () => _window.getCurrentWindow().startDragging(),
34
60
 
35
61
  // ── Events ─────────────────────────────────────────────────
36
62
  on: {
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
2
+ <rect width="1024" height="1024" rx="180" fill="#101613"/>
3
+ <path fill="#4caf50" d="M240 300h544a24 24 0 0 1 24 24v48a24 24 0 0 1-24 24H616v72c110 18 196 74 228 168H180c32-94 118-150 228-168v-72H240a24 24 0 0 1-24-24v-48a24 24 0 0 1 24-24z"/>
4
+ <rect x="300" y="672" width="424" height="52" fill="#3d8b40"/>
5
+ <rect x="252" y="724" width="520" height="72" rx="20" fill="#4caf50"/>
6
+ </svg>