Skip to content

Commit 1f79903

Browse files
authored
Merge pull request #26 from timonmdy/major/v2.2
Prepare Major
2 parents df2a82a + a54af56 commit 1f79903

64 files changed

Lines changed: 3572 additions & 394 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

languages/en.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"id": "en",
3+
"name": "English",
4+
"version": 1,
5+
"author": "JRC",
6+
"strings": {
7+
"general.save": "Save",
8+
"general.saved": "Saved",
9+
"general.cancel": "Cancel",
10+
"general.delete": "Delete",
11+
"general.close": "Close",
12+
"general.noProfileSelected": "No profile selected",
13+
"sidebar.newProfile": "New Profile",
14+
"sidebar.fromTemplate": "From Template",
15+
"sidebar.settings": "Settings",
16+
"console.run": "Run",
17+
"console.stop": "Stop",
18+
"console.forceKill": "Force Kill",
19+
"console.notRunning": "Process not running. Press Run to start.",
20+
"settings.title": "Application Settings"
21+
}
22+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "java-runner-client",
3-
"version": "2.1.6",
3+
"version": "2.2.1",
44
"description": "Run and manage Java processes with profiles, console I/O, and system tray support",
55
"main": "dist/main/main.js",
66
"scripts": {

src/main/AssetManager.ts

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
import { app } from 'electron';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import https from 'https';
5+
import { GITHUB_CONFIG } from './shared/config/GitHub.config';
6+
import { BUILTIN_THEME, THEME_GITHUB_PATH } from './shared/config/Theme.config';
7+
import { LANGUAGE_GITHUB_PATH } from './shared/config/Language.config';
8+
import { ENGLISH } from './shared/config/DefaultLanguage.config';
9+
import type { ThemeDefinition, LocalThemeState } from './shared/types/Theme.types';
10+
import type { LanguageDefinition, LocalLanguageState } from './shared/types/Language.types';
11+
12+
function dataDir(): string {
13+
return app.getPath('userData');
14+
}
15+
16+
function httpsGetJson(url: string): Promise<unknown> {
17+
return new Promise((resolve, reject) => {
18+
const options = { headers: { 'User-Agent': 'java-runner-client' } };
19+
const req = https.get(url, options, (res) => {
20+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
21+
resolve(httpsGetJson(res.headers.location));
22+
return;
23+
}
24+
let data = '';
25+
res.on('data', (c) => (data += c));
26+
res.on('end', () => {
27+
try { resolve(JSON.parse(data)); }
28+
catch { reject(new Error('JSON parse error')); }
29+
});
30+
});
31+
req.on('error', reject);
32+
req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); });
33+
});
34+
}
35+
36+
function rawUrl(ghPath: string, filename: string): string {
37+
return `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/main/${ghPath}/${filename}`;
38+
}
39+
40+
function contentsUrl(ghPath: string): string {
41+
return `${GITHUB_CONFIG.apiBase}/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/contents/${ghPath}`;
42+
}
43+
44+
// ─── Themes ───────────────────────────────────────────────────────────────────
45+
46+
const THEME_FILE = 'theme-state.json';
47+
48+
function themeFilePath(): string {
49+
return path.join(dataDir(), THEME_FILE);
50+
}
51+
52+
export function loadThemeState(): LocalThemeState {
53+
try {
54+
const raw = fs.readFileSync(themeFilePath(), 'utf8');
55+
const state = JSON.parse(raw) as LocalThemeState;
56+
// Ensure builtin is always present
57+
if (!state.themes.find((t) => t.id === BUILTIN_THEME.id)) {
58+
state.themes.unshift(BUILTIN_THEME);
59+
}
60+
return state;
61+
} catch {
62+
return { activeThemeId: BUILTIN_THEME.id, themes: [BUILTIN_THEME] };
63+
}
64+
}
65+
66+
export function saveThemeState(state: LocalThemeState): void {
67+
fs.writeFileSync(themeFilePath(), JSON.stringify(state, null, 2), 'utf8');
68+
}
69+
70+
export function getActiveTheme(): ThemeDefinition {
71+
const state = loadThemeState();
72+
return state.themes.find((t) => t.id === state.activeThemeId) ?? BUILTIN_THEME;
73+
}
74+
75+
export function setActiveTheme(themeId: string): ThemeDefinition {
76+
const state = loadThemeState();
77+
if (state.themes.find((t) => t.id === themeId)) {
78+
state.activeThemeId = themeId;
79+
saveThemeState(state);
80+
}
81+
return getActiveTheme();
82+
}
83+
84+
export async function fetchRemoteThemes(): Promise<{ ok: boolean; themes?: ThemeDefinition[]; error?: string }> {
85+
try {
86+
const listing = await httpsGetJson(contentsUrl(THEME_GITHUB_PATH));
87+
if (!Array.isArray(listing)) return { ok: false, error: 'Themes folder not found' };
88+
const themes: ThemeDefinition[] = [];
89+
for (const f of (listing as Array<{ name: string }>).filter((f) => f.name.endsWith('.json'))) {
90+
try {
91+
const theme = (await httpsGetJson(rawUrl(THEME_GITHUB_PATH, f.name))) as ThemeDefinition;
92+
if (theme.id && theme.name && theme.colors) themes.push(theme);
93+
} catch { /* skip */ }
94+
}
95+
return { ok: true, themes };
96+
} catch (e) {
97+
return { ok: false, error: String(e) };
98+
}
99+
}
100+
101+
export async function checkThemeUpdate(themeId: string): Promise<{ hasUpdate: boolean; remoteVersion: number; localVersion: number }> {
102+
const state = loadThemeState();
103+
const local = state.themes.find((t) => t.id === themeId);
104+
if (!local) return { hasUpdate: false, remoteVersion: 0, localVersion: 0 };
105+
106+
const result = await fetchRemoteThemes();
107+
if (!result.ok || !result.themes) return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version };
108+
109+
const remote = result.themes.find((t) => t.id === themeId);
110+
if (!remote) return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version };
111+
112+
return {
113+
hasUpdate: remote.version > local.version,
114+
remoteVersion: remote.version,
115+
localVersion: local.version,
116+
};
117+
}
118+
119+
export async function applyThemeUpdate(themeId: string): Promise<{ ok: boolean; error?: string }> {
120+
const result = await fetchRemoteThemes();
121+
if (!result.ok || !result.themes) return { ok: false, error: result.error ?? 'Fetch failed' };
122+
123+
const remote = result.themes.find((t) => t.id === themeId);
124+
if (!remote) return { ok: false, error: 'Theme not found on remote' };
125+
126+
const state = loadThemeState();
127+
const idx = state.themes.findIndex((t) => t.id === themeId);
128+
if (idx >= 0) state.themes[idx] = remote;
129+
else state.themes.push(remote);
130+
saveThemeState(state);
131+
return { ok: true };
132+
}
133+
134+
export function installTheme(theme: ThemeDefinition): void {
135+
const state = loadThemeState();
136+
const idx = state.themes.findIndex((t) => t.id === theme.id);
137+
if (idx >= 0) state.themes[idx] = theme;
138+
else state.themes.push(theme);
139+
saveThemeState(state);
140+
}
141+
142+
// ─── Languages ────────────────────────────────────────────────────────────────
143+
144+
const LANG_FILE = 'language-state.json';
145+
146+
function langFilePath(): string {
147+
return path.join(dataDir(), LANG_FILE);
148+
}
149+
150+
export function loadLanguageState(): LocalLanguageState {
151+
try {
152+
const raw = fs.readFileSync(langFilePath(), 'utf8');
153+
const state = JSON.parse(raw) as LocalLanguageState;
154+
if (!state.languages.find((l) => l.id === ENGLISH.id)) {
155+
state.languages.unshift(ENGLISH);
156+
}
157+
return state;
158+
} catch {
159+
return { activeLanguageId: ENGLISH.id, languages: [ENGLISH] };
160+
}
161+
}
162+
163+
export function saveLanguageState(state: LocalLanguageState): void {
164+
fs.writeFileSync(langFilePath(), JSON.stringify(state, null, 2), 'utf8');
165+
}
166+
167+
export function getActiveLanguage(): LanguageDefinition {
168+
const state = loadLanguageState();
169+
return state.languages.find((l) => l.id === state.activeLanguageId) ?? ENGLISH;
170+
}
171+
172+
export function setActiveLanguage(langId: string): LanguageDefinition {
173+
const state = loadLanguageState();
174+
if (state.languages.find((l) => l.id === langId)) {
175+
state.activeLanguageId = langId;
176+
saveLanguageState(state);
177+
}
178+
return getActiveLanguage();
179+
}
180+
181+
export async function fetchRemoteLanguages(): Promise<{ ok: boolean; languages?: LanguageDefinition[]; error?: string }> {
182+
try {
183+
const listing = await httpsGetJson(contentsUrl(LANGUAGE_GITHUB_PATH));
184+
if (!Array.isArray(listing)) return { ok: false, error: 'Languages folder not found' };
185+
const languages: LanguageDefinition[] = [];
186+
for (const f of (listing as Array<{ name: string }>).filter((f) => f.name.endsWith('.json'))) {
187+
try {
188+
const lang = (await httpsGetJson(rawUrl(LANGUAGE_GITHUB_PATH, f.name))) as LanguageDefinition;
189+
if (lang.id && lang.name && lang.strings) languages.push(lang);
190+
} catch { /* skip */ }
191+
}
192+
return { ok: true, languages };
193+
} catch (e) {
194+
return { ok: false, error: String(e) };
195+
}
196+
}
197+
198+
export async function checkLanguageUpdate(langId: string): Promise<{ hasUpdate: boolean; remoteVersion: number; localVersion: number }> {
199+
const state = loadLanguageState();
200+
const local = state.languages.find((l) => l.id === langId);
201+
if (!local) return { hasUpdate: false, remoteVersion: 0, localVersion: 0 };
202+
203+
const result = await fetchRemoteLanguages();
204+
if (!result.ok || !result.languages) return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version };
205+
206+
const remote = result.languages.find((l) => l.id === langId);
207+
if (!remote) return { hasUpdate: false, remoteVersion: local.version, localVersion: local.version };
208+
209+
return {
210+
hasUpdate: remote.version > local.version,
211+
remoteVersion: remote.version,
212+
localVersion: local.version,
213+
};
214+
}
215+
216+
export async function applyLanguageUpdate(langId: string): Promise<{ ok: boolean; error?: string }> {
217+
const result = await fetchRemoteLanguages();
218+
if (!result.ok || !result.languages) return { ok: false, error: result.error ?? 'Fetch failed' };
219+
220+
const remote = result.languages.find((l) => l.id === langId);
221+
if (!remote) return { ok: false, error: 'Language not found on remote' };
222+
223+
const state = loadLanguageState();
224+
const idx = state.languages.findIndex((l) => l.id === langId);
225+
if (idx >= 0) state.languages[idx] = remote;
226+
else state.languages.push(remote);
227+
saveLanguageState(state);
228+
return { ok: true };
229+
}
230+
231+
export function installLanguage(lang: LanguageDefinition): void {
232+
const state = loadLanguageState();
233+
const idx = state.languages.findIndex((l) => l.id === lang.id);
234+
if (idx >= 0) state.languages[idx] = lang;
235+
else state.languages.push(lang);
236+
saveLanguageState(state);
237+
}
238+
239+
// ─── Dev mode: load from local project directories ────────────────────────────
240+
241+
function projectRoot(): string {
242+
return path.join(__dirname, '..', '..');
243+
}
244+
245+
export function loadLocalDevThemes(): ThemeDefinition[] {
246+
const dir = path.join(projectRoot(), 'themes');
247+
if (!fs.existsSync(dir)) return [];
248+
return fs.readdirSync(dir)
249+
.filter((f) => f.endsWith('.json'))
250+
.map((f) => {
251+
try {
252+
const raw = fs.readFileSync(path.join(dir, f), 'utf8');
253+
const theme = JSON.parse(raw) as ThemeDefinition;
254+
if (theme.id && theme.name && theme.colors) return theme;
255+
} catch { /* skip */ }
256+
return null;
257+
})
258+
.filter((t): t is ThemeDefinition => t !== null);
259+
}
260+
261+
export function loadLocalDevLanguages(): LanguageDefinition[] {
262+
const dir = path.join(projectRoot(), 'languages');
263+
if (!fs.existsSync(dir)) return [];
264+
return fs.readdirSync(dir)
265+
.filter((f) => f.endsWith('.json'))
266+
.map((f) => {
267+
try {
268+
const raw = fs.readFileSync(path.join(dir, f), 'utf8');
269+
const lang = JSON.parse(raw) as LanguageDefinition;
270+
if (lang.id && lang.name && lang.strings) return lang;
271+
} catch { /* skip */ }
272+
return null;
273+
})
274+
.filter((l): l is LanguageDefinition => l !== null);
275+
}
276+
277+
export function syncLocalDevAssets(): { themes: number; languages: number } {
278+
let tc = 0;
279+
let lc = 0;
280+
for (const theme of loadLocalDevThemes()) {
281+
installTheme(theme);
282+
tc++;
283+
}
284+
for (const lang of loadLocalDevLanguages()) {
285+
installLanguage(lang);
286+
lc++;
287+
}
288+
return { themes: tc, languages: lc };
289+
}
290+

0 commit comments

Comments
 (0)