diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index d8838c5..365c891 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -1,6 +1,6 @@ name: Code Formatting -on: [push, pull_request] +on: [pull_request] jobs: check: diff --git a/.prettierrc.json b/.prettierrc.json index 3cbe1bf..d7ad161 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -6,5 +6,6 @@ "tabWidth": 2, "endOfLine": "lf", "objectWrap": "preserve", - "bracketSameLine": false + "bracketSameLine": false, + "plugins": ["prettier-plugin-organize-imports"] } diff --git a/languages/de.json b/languages/de.json deleted file mode 100644 index 7bc8a4b..0000000 --- a/languages/de.json +++ /dev/null @@ -1,335 +0,0 @@ -{ - "id": "de", - "name": "Deutsch", - "version": 2, - "author": "JRC", - "strings": { - "general.save": "Speichern", - "general.saved": "Gespeichert", - "general.cancel": "Abbrechen", - "general.delete": "Löschen", - "general.close": "Schließen", - "general.retry": "Erneut versuchen", - "general.copy": "Kopieren", - "general.copyAll": "Alles kopieren", - "general.cut": "Ausschneiden", - "general.paste": "Einfügen", - "general.selectAll": "Alles auswählen", - "general.enabled": "Aktiviert", - "general.disabled": "Deaktiviert", - "general.on": "An", - "general.off": "Aus", - "general.yes": "Ja", - "general.no": "Nein", - "general.add": "Hinzufügen", - "general.noProfileSelected": "Kein Profil ausgewählt", - "general.back": "Zurück", - "general.confirm": "Bestätigen", - "general.loading": "Laden...", - "general.load": "Laden", - "general.refresh": "Aktualisieren", - "general.error": "Fehler", - "general.clear": "Leeren", - "general.none": "keine", - - "sidebar.newProfile": "Neues Profil", - "sidebar.fromTemplate": "Aus Vorlage", - "sidebar.noProfiles": "Noch keine Profile.", - "sidebar.utilities": "Werkzeuge", - "sidebar.faq": "FAQ", - "sidebar.settings": "Einstellungen", - "sidebar.developer": "Entwickleroptionen", - - "tabs.console": "Konsole", - "tabs.configure": "Konfigurieren", - "tabs.logs": "Protokolle", - "tabs.profile": "Profil", - - "ctx.select": "Auswählen", - "ctx.clearConsole": "Konsole leeren", - - "console.run": "Starten", - "console.stop": "Stoppen", - "console.forceKill": "Sofort beenden", - "console.forceKillHint": "Sofort beenden (ohne sauberes Herunterfahren)", - "console.openWorkDir": "Arbeitsverzeichnis öffnen", - "console.scrollToBottom": "nach unten scrollen", - "console.search": "Suchen (Strg+F)", - "console.clear": "Leeren (Strg+L)", - "console.lines": "Zeilen", - "console.noMatches": "Keine Treffer", - "console.searchPlaceholder": "Konsole durchsuchen... (Enter nächster, Shift+Enter vorheriger)", - "console.inputPlaceholder": "Befehl senden... (Hoch/Runter Verlauf, Strg+L leeren, Strg+F suchen)", - "console.inputDisabled": "Prozess starten um Befehle zu senden", - "console.waiting": "Warte auf Ausgabe...", - "console.notRunning": "Prozess läuft nicht. Drücke Starten um zu beginnen.", - "console.noJar": "Keine JAR konfiguriert. Gehe zu Konfigurieren.", - "console.copyLine": "Zeile kopieren", - "console.copyAll": "Gesamte Ausgabe kopieren", - - "config.general": "Allgemein", - "config.files": "Dateien & Pfade", - "config.jvm": "JVM-Argumente", - "config.props": "Eigenschaften (-D)", - "config.args": "Programmargumente", - "config.env": "Umgebung", - "config.unsavedChanges": "Nicht gespeicherte Änderungen", - "config.restartNeeded": "Neustart erforderlich", - "config.autoStart": "Automatisch starten beim App-Start", - "config.autoStartHint": "Diese JAR wird automatisch gestartet wenn Java Runner Client geöffnet wird.", - "config.autoRestart": "Automatischer Neustart bei Absturz", - "config.autoRestartHint": "Startet den Prozess nach einem unerwarteten Beenden automatisch neu.", - "config.autoRestartInterval": "Neustart-Verzögerung", - "config.fileLogging": "Sitzungsprotokolle in Datei speichern", - "config.fileLoggingHint": "Schreibt Konsolenausgaben in .log-Dateien im Konfigurationsverzeichnis pro Sitzung", - "config.restartProcess": "Prozess neustarten", - "config.logging": "Protokollierung", - "config.process": "Prozess", - "config.jvmTitle": "JVM-Argumente", - "config.jvmHint": "Flags die vor -jar an die JVM übergeben werden, z.B. -Xmx2g -XX:+UseG1GC", - "config.propsTitle": "Systemeigenschaften", - "config.propsHint": "Übergeben als -Dkey=value. Spring-Profile, Ports, Logging-Level usw.", - "config.argsTitle": "Programmargumente", - "config.argsHint": "Argumente die nach der JAR übergeben werden, z.B. --nogui --world myWorld", - "config.envTitle": "Umgebungsvariablen", - "config.envHint": "Werden in die Prozessumgebung eingefügt. Überschreiben System-Umgebungsvariablen mit gleichem Schlüssel.", - "config.commandPreview": "Befehlsvorschau", - "config.pendingArgTitle": "Nicht gespeicherte Eingabe", - "config.pendingArgMessage": "Du hast Text im Eingabefeld der noch nicht hinzugefügt wurde.\n\nKlicke zuerst \"+ Hinzufügen\", sonst wird er nicht übernommen.\n\nTrotzdem wechseln?", - "config.pendingArgConfirm": "Wechseln", - "config.pendingArgCancel": "Bleiben", - "config.autoRestartIntervalHint": "Sekunden bis zum Neustart", - "config.sec": "Sek", - "config.jarSelection": "JAR-Auswahl", - "config.jarSelectionMethod": "Auswahlmethode", - "config.static": "Statisch", - "config.dynamic": "Dynamisch", - "config.jarFile": "JAR-Datei", - "config.jarFilePlaceholder": "Pfad zur .jar-Datei", - "config.jarFileHint": "Die auszuführende JAR-Datei", - "config.workDir": "Arbeitsverzeichnis", - "config.workDirPlaceholder": "Standard: JAR-Verzeichnis", - "config.workDirHint": "Leer lassen um das Verzeichnis der JAR zu verwenden", - "config.javaExe": "Java-Programm", - "config.javaExePlaceholder": "java (verwendet System-PATH)", - "config.javaExeHint": "Leer lassen um das java aus dem PATH zu verwenden", - "config.baseDir": "Basisverzeichnis", - "config.baseDirPlaceholder": "Verzeichnis mit deinen JARs", - "config.baseDirHint": "Ordner der nach passenden JAR-Dateien durchsucht wird", - "config.strategy": "Strategie", - "config.filenamePattern": "Dateinamenmuster", - "config.filenamePatternHint": "{version} als Platzhalter verwenden — z.B. \"myapp-{version}.jar\"", - "config.regex": "Regulärer Ausdruck", - "config.regexHint": "Wird gegen Dateinamen im Basisverzeichnis geprüft (Groß-/Kleinschreibung egal)", - "config.resolving": "Wird aufgelöst...", - "config.noMatchFound": "Kein Treffer gefunden", - "config.autoStartTitle": "Autostart", - "config.autoRestartTitle": "Auto-Neustart", - "config.strategyHighestVersion": "Höchste Version", - "config.strategyHighestVersionHint": "Wählt die JAR mit der höchsten semantischen oder numerischen Version aus dem Dateinamen.", - "config.strategyLatestModified": "Zuletzt geändert", - "config.strategyLatestModifiedHint": "Wählt die zuletzt geänderte JAR im Verzeichnis die dem Muster entspricht.", - "config.strategyRegex": "Regex-Übereinstimmung", - "config.strategyRegexHint": "Wählt die erste JAR deren Dateiname dem regulären Ausdruck entspricht.", - "config.otherSingular": "+{count} weitere", - "config.otherPlural": "+{count} weitere", - - "profile.identity": "Profil-Identität", - "profile.name": "Name", - "profile.accentColour": "Akzentfarbe", - "profile.accentColourHint": "Wird in der Seitenleiste und als Tab-Hervorhebung verwendet.", - "profile.customColour": "Eigene Farbe wählen", - "profile.dangerZone": "Gefahrenzone", - "profile.deleteProfile": "Profil löschen", - "profile.deleteHint": "Entfernt dieses Profil und alle Einstellungen dauerhaft. Shift halten um Bestätigung zu überspringen.", - "profile.deleteConfirmTitle": "Profil löschen?", - "profile.deleteConfirmMessage": "\"{name}\" wird dauerhaft entfernt. Dies kann nicht rückgängig gemacht werden.", - - "logs.title": "Sitzungsprotokolle", - "logs.files": "Dateien", - "logs.openDir": "Protokollverzeichnis öffnen", - "logs.noFiles": "Noch keine Protokolldateien. Starte und stoppe einen Prozess um eine zu erstellen.", - "logs.selectFile": "Wähle eine Protokolldatei um den Inhalt anzuzeigen", - "logs.deleteHint": "Protokolldatei löschen (Shift halten um Bestätigung zu überspringen)", - "logs.deleteTitle": "Protokolldatei löschen?", - "logs.deleteMessage": "\"{name}\" wird dauerhaft gelöscht.", - "logs.disabled": "Dateiprotokollierung ist für dieses Profil deaktiviert.", - "logs.disabledHint": "Aktiviere sie unter Konfigurieren > Allgemein > Sitzungsprotokolle in Datei speichern.", - - "settings.title": "Anwendungseinstellungen", - "settings.saved": "Einstellungen gespeichert", - "settings.unsaved": "Nicht gespeicherte Änderungen", - "settings.saveChanges": "Änderungen speichern", - "settings.general": "Allgemein", - "settings.startup": "Autostart", - "settings.launchOnStartup": "Beim Windows-Start starten", - "settings.launchOnStartupHint": "Java Runner Client startet automatisch beim Anmelden", - "settings.startMinimized": "Minimiert in den Tray starten", - "settings.startMinimizedHint": "Das Fenster erscheint nicht beim Start -- nur das Tray-Symbol", - "settings.minimizeToTray": "Beim Schließen in den Tray minimieren", - "settings.minimizeToTrayHint": "Das Schließen des Fensters hält die App und laufende JARs im Hintergrund aktiv", - "settings.console": "Konsole", - "settings.fontSize": "Schriftgröße", - "settings.fontSizeHint": "Schriftgröße der Konsolenausgabe in Pixeln", - "settings.lineNumbers": "Zeilennummern anzeigen", - "settings.lineNumbersHint": "Zeigt eine Zeilennummern-Spalte in der Konsolenausgabe", - "settings.timestamps": "Zeitstempel anzeigen", - "settings.timestampsHint": "Zeigt einen Zeitstempel für jede Konsolenzeile", - "settings.wordWrap": "Zeilenumbruch", - "settings.wordWrapHint": "Lange Zeilen umbrechen statt horizontal zu scrollen", - "settings.maxLines": "Maximale Zeilen im Puffer", - "settings.maxLinesHint": "Ältere Zeilen werden verworfen wenn das Limit erreicht ist", - "settings.historySize": "Befehlsverlauf-Größe", - "settings.historySizeHint": "Gespeicherte Befehle pro Sitzung (Hoch/Runter zum Navigieren)", - "settings.appearance": "Darstellung", - "settings.theme": "Design", - "settings.themeHint": "Visuelles Design auswählen", - "settings.themeBuiltin": "Eingebaut", - "settings.themeCheckUpdate": "Nach Design-Updates suchen", - "settings.language": "Sprache", - "settings.languageHint": "Anzeigesprache auswählen", - "settings.languageCheckUpdate": "Nach Sprach-Updates suchen", - - "appearance.fetchThemesFailed": "Designs konnten nicht abgerufen werden.", - "appearance.fetchLangsFailed": "Sprachen konnten nicht abgerufen werden.", - "appearance.development": "Entwicklung", - "appearance.syncTitle": "Lokale Projektdateien synchronisieren", - "appearance.syncHint": "Designs und Sprachen aus /themes und /languages im Projektverzeichnis laden", - "appearance.sync": "Synchronisieren", - "appearance.synced": "Synchronisiert", - "appearance.localOnly": "Nur lokal", - "appearance.localOverride": "Überschreibt Remote", - - "settings.advanced": "Erweitert", - "settings.devMode": "Entwickleroptionen", - "settings.devModeLabel": "Entwicklermodus umschalten (Rechts-Shift + 7)", - "settings.devModeHint": "Aktiviert den Entwickler-Tab und DevTools. Mit Vorsicht verwenden.", - "settings.restApi": "REST-API", - "settings.restApiLabel": "REST-API aktivieren", - "settings.restApiHint": "Stellt eine lokale HTTP-API für Automatisierung bereit (Standard-Port {port})", - "settings.restApiPort": "Port", - "settings.restApiPortHint": "Neustart erforderlich um den Port zu ändern", - "settings.listeningOn": "Lauscht auf", - "settings.endpoints": "Endpunkte", - - "settings.updates": "Aktualisierungen", - "settings.updateCenter": "Aktualisierungscenter", - "settings.checkAll": "Alle prüfen", - "settings.updateAll": "Alle aktualisieren", - "settings.upToDate": "Aktuell", - "settings.updateAvailable": "Update verfügbar", - "settings.checking": "Prüfe...", - "settings.checkFailed": "Prüfung fehlgeschlagen", - "settings.updatesHint": "Nach Updates für App, Designs und Sprachpakete suchen", - "settings.allUpToDate": "Alles ist auf dem neuesten Stand.", - "settings.updatedSuccess": "Erfolgreich aktualisiert", - "settings.applyingUpdate": "Update wird angewendet...", - "settings.check": "Prüfen", - "settings.update": "Aktualisieren", - "settings.versionCurrent": "Aktuell:", - "settings.checkForUpdates": "Nach Updates suchen", - "settings.viewUpdate": "Update ansehen", - "settings.upToDateTooltip": "Du hast die neueste Version ({version})", - "settings.updateAvailableTooltip": "{tag} ist verfügbar — für Details klicken", - - "settings.about": "Über", - "settings.version": "Version", - "settings.stack": "Technologie", - "settings.configPath": "Konfiguration", - "settings.openConfigFolder": "Konfigurationsordner öffnen", - - "release.title": "Release-Details", - "release.preRelease": "Vorabversion", - "release.stable": "Stabil", - "release.trustedDev": "Vertrauenswürdiger Entwickler", - "release.automation": "Automatisiertes Release", - "release.unknownPublisher": "Unbekannter Herausgeber", - "release.unknownPublisherHint": "Dieses Release wurde von einem GitHub-Benutzer veröffentlicht der nicht in der vertrauenswürdigen Liste steht. Es wurde dennoch durch die GitHub-Repository-Sicherheit genehmigt.", - "release.downloads": "Downloads", - "release.otherAssets": "Weitere Dateien", - "release.releaseNotes": "Release-Hinweise", - "release.viewOnGithub": "Auf GitHub ansehen", - "release.allAssets": "Alle Dateien", - "release.download": "Herunterladen", - "release.downloadAgain": "Erneut herunterladen", - "release.resume": "Fortsetzen", - "release.pause": "Pausieren", - "release.cancelled": "Abgebrochen", - "release.complete": "Abgeschlossen", - "release.paused": "Pausiert", - - "utilities.title": "Werkzeuge", - "utilities.activityLog": "Aktivitätsprotokoll", - "utilities.processScanner": "Prozess-Scanner", - - "activity.description": "Alle von JRC gestarteten Prozesse dieser Sitzung", - "activity.empty": "Noch keine Prozesse in dieser Sitzung gestartet", - "activity.clearTitle": "Aktivitätsprotokoll leeren?", - "activity.clearMessage": "Alle aufgezeichneten Prozesseinträge werden entfernt. Laufende Prozesse sind nicht betroffen.", - "activity.stopped": "gestoppt", - "activity.running": "läuft", - - "scanner.foundProcesses": "{count} Prozesse gefunden — {javaCount} Java", - "scanner.killedPid": "PID {pid} beendet", - "scanner.killFailed": "PID {pid} konnte nicht beendet werden: {error}", - "scanner.killedAll": "{killed} Java-Prozess beendet (geschützte übersprungen)", - "scanner.killedAllPlural": "{killed} Java-Prozesse beendet (geschützte übersprungen)", - "scanner.killProtectedMessage": "Dieser Prozess ist als geschützt markiert.\n\nBefehl: {command}\n\nBist du sicher?", - "scanner.killNonJavaMessage": "Warnung: kein Java-Prozess.\n\nBefehl: {command}\n\nDas gewaltsame Beenden unbekannter Prozesse kann zu Datenverlust führen.", - "scanner.killPidMessage": "PID {pid} gewaltsam beenden?\n\nBefehl: {command}", - "scanner.javaOnly": "Nur Java", - "scanner.all": "Alle", - "scanner.searchPlaceholder": "PID oder Befehl suchen...", - "scanner.killAll": "Alle Java beenden", - "scanner.scan": "Scannen", - "scanner.rescan": "Erneut scannen", - "scanner.scanHint": "Klicke \"Scannen\" um alle laufenden Prozesse aufzulisten", - "scanner.scanning": "Alle Prozesse werden gescannt...", - "scanner.noJava": "Keine Java-Prozesse gefunden", - "scanner.noProcesses": "Keine Prozesse gefunden", - "scanner.killProtectedTitle": "Geschützten Prozess beenden?", - "scanner.killNonJavaTitle": "Nicht-Java-Prozess beenden?", - "scanner.killPidTitle": "PID {pid} beenden?", - "scanner.killAllTitle": "Alle Java-Prozesse beenden?", - "scanner.killAllMessage": "Dies beendet gewaltsam jeden nicht geschützten Java-Prozess. Geschützte Prozesse werden übersprungen. Laufende Server verlieren nicht gespeicherte Daten.", - "scanner.killAnyway": "Trotzdem beenden", - "scanner.killProcess": "Prozess beenden", - "scanner.killAllLabel": "Alle beenden", - "scanner.kill": "Beenden", - "scanner.fullCommand": "Vollständiger Befehl", - "scanner.memory": "Arbeitsspeicher", - "scanner.threads": "Threads", - "scanner.started": "Gestartet", - "scanner.managedByJrc": "Verwaltet von JRC", - "scanner.protectedLabel": "Geschützt", - "scanner.protectedYes": "Ja — ausgenommen von Alle Java beenden", - "scanner.managedBadge": "Verwaltet", - "scanner.javaBadge": "Java", - "scanner.nonJavaBadge": "Nicht-Java", - - "panels.settings": "Anwendungseinstellungen", - "panels.faq": "FAQ", - "panels.utilities": "Werkzeuge", - "panels.developer": "Entwickleroptionen", - - "dev.mode": "Entwicklermodus", - "dev.dashboard": "Dashboard", - "dev.apiExplorer": "API-Explorer", - "dev.storage": "Speicher", - "dev.diagnostics": "Diagnose", - - "template.title": "Profil-Vorlagen", - "template.searchPlaceholder": "Vorlagen suchen...", - "template.noTemplates": "Keine Vorlagen gefunden.", - "template.selectHint": "Wähle eine Vorlage um die Konfiguration anzuzeigen", - "template.createProfile": "Profil erstellen", - "template.jvmArgs": "JVM-Argumente", - "template.systemProperties": "Systemeigenschaften", - "template.programArgs": "Programmargumente", - "template.versionInfo": "Vorlagenversion {version} · Erfordert App {appVersion}+", - - "faq.searchPlaceholder": "FAQ durchsuchen...", - "faq.noResults": "Keine Ergebnisse gefunden.", - "faq.noItems": "Keine Einträge in diesem Thema." - } -} diff --git a/package-lock.json b/package-lock.json index d62714c..cf5f5c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "java-runner-client", - "version": "2.1.4", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "java-runner-client", - "version": "2.1.4", + "version": "2.2.0", "dependencies": { "electron-store": "^8.1.0", "framer-motion": "^12.38.0", @@ -28,6 +28,7 @@ "electron-builder": "^24.9.0", "postcss": "^8.4.0", "prettier": "^3.8.1", + "prettier-plugin-organize-imports": "^4.3.0", "rimraf": "^5.0.0", "tailwindcss": "^3.4.0", "terser": "^5.28.0", @@ -5137,6 +5138,23 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-organize-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.3.0.tgz", + "integrity": "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9", + "vue-tsc": "^2.1.0 || 3" + }, + "peerDependenciesMeta": { + "vue-tsc": { + "optional": true + } + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "dev": true, diff --git a/package.json b/package.json index 7ef013f..e7ef34e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "electron-builder": "^24.9.0", "postcss": "^8.4.0", "prettier": "^3.8.1", + "prettier-plugin-organize-imports": "^4.3.0", "rimraf": "^5.0.0", "tailwindcss": "^3.4.0", "terser": "^5.28.0", diff --git a/src/main/AssetManager.ts b/src/main/AssetManager.ts deleted file mode 100644 index 068b10a..0000000 --- a/src/main/AssetManager.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { app } from 'electron'; -import fs from 'fs'; -import path from 'path'; -import https from 'https'; -import { GITHUB_CONFIG } from './shared/config/GitHub.config'; -import { BUILTIN_THEME, THEME_GITHUB_PATH } from './shared/config/Theme.config'; -import { ENGLISH } from './shared/config/DefaultLanguage.config'; -import type { ThemeDefinition, LocalThemeState, ThemePreview } from './shared/types/Theme.types'; -import type { - LanguageDefinition, - LocalLanguageState, - LanguagePreview, -} from './shared/types/Language.types'; - -function dataDir(): string { - return app.getPath('userData'); -} - -function httpsGetJson(url: string): Promise { - return new Promise((resolve, reject) => { - const options = { headers: { 'User-Agent': 'java-runner-client' } }; - const req = https.get(url, options, (res) => { - if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - resolve(httpsGetJson(res.headers.location)); - return; - } - let data = ''; - res.on('data', (c) => (data += c)); - res.on('end', () => { - try { - resolve(JSON.parse(data)); - } catch { - reject(new Error('JSON parse error')); - } - }); - }); - req.on('error', reject); - req.setTimeout(10000, () => { - req.destroy(); - reject(new Error('Timeout')); - }); - }); -} - -function rawUrl(ghPath: string, filename: string): string { - return `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/main/${ghPath}/${filename}`; -} - -function contentsUrl(ghPath: string): string { - return `${GITHUB_CONFIG.apiBase}/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/contents/${ghPath}`; -} - -// ─── Themes ─────────────────────────────────────────────────────────────────── - -const THEME_FILE = 'theme-state.json'; - -function themeFilePath(): string { - return path.join(dataDir(), THEME_FILE); -} - -export function getActiveTheme(): ThemeDefinition { - try { - const raw = fs.readFileSync(themeFilePath(), 'utf8'); - const state = JSON.parse(raw) as LocalThemeState; - return state.activeTheme ?? BUILTIN_THEME; - } catch { - return BUILTIN_THEME; - } -} - -export function setActiveTheme(theme: ThemeDefinition): ThemeDefinition { - const state: LocalThemeState = { activeThemeId: theme.id, activeTheme: theme }; - fs.writeFileSync(themeFilePath(), JSON.stringify(state, null, 2), 'utf8'); - return theme; -} - -export async function fetchRemoteThemes(): Promise<{ - ok: boolean; - themes?: ThemeDefinition[]; - error?: string; -}> { - try { - const listing = await httpsGetJson(contentsUrl(THEME_GITHUB_PATH)); - if (!Array.isArray(listing)) return { ok: false, error: 'Themes folder not found' }; - const themes: ThemeDefinition[] = []; - for (const f of (listing as Array<{ name: string }>).filter((f) => f.name.endsWith('.json'))) { - try { - const theme = (await httpsGetJson(rawUrl(THEME_GITHUB_PATH, f.name))) as ThemeDefinition; - if (theme.id && theme.name && theme.colors) themes.push(theme); - } catch { - /* skip */ - } - } - return { ok: true, themes }; - } catch (e) { - return { ok: false, error: String(e) }; - } -} - -export async function fetchRemoteThemePreviews(): Promise<{ - ok: boolean; - themes?: ThemePreview[]; - error?: string; -}> { - try { - const listing = await httpsGetJson(contentsUrl(THEME_GITHUB_PATH)); - if (!Array.isArray(listing)) return { ok: false, error: 'Themes folder not found' }; - const themes: ThemePreview[] = []; - for (const f of (listing as Array<{ name: string }>).filter((f) => f.name.endsWith('.json'))) { - try { - const theme = (await httpsGetJson(rawUrl(THEME_GITHUB_PATH, f.name))) as ThemeDefinition; - if (theme.id && theme.name && theme.colors) { - themes.push({ - id: theme.id, - name: theme.name, - filename: f.name, - previewColors: { - accent: theme.colors.accent, - 'base-900': theme.colors['base-900'], - 'surface-raised': theme.colors['surface-raised'], - 'text-primary': theme.colors['text-primary'], - }, - }); - } - } catch { - /* skip */ - } - } - return { ok: true, themes }; - } catch (e) { - return { ok: false, error: String(e) }; - } -} - -export async function fetchRemoteThemeByFile( - filename: string -): Promise<{ ok: boolean; theme?: ThemeDefinition; error?: string }> { - try { - const theme = (await httpsGetJson(rawUrl(THEME_GITHUB_PATH, filename))) as ThemeDefinition; - if (theme.id && theme.name && theme.colors) return { ok: true, theme }; - return { ok: false, error: 'Invalid theme' }; - } catch (e) { - return { ok: false, error: String(e) }; - } -} - -// ─── Languages ──────────────────────────────────────────────────────────────── - -const LANG_FILE = 'language-state.json'; - -function langFilePath(): string { - return path.join(dataDir(), LANG_FILE); -} - -export function getActiveLanguage(): LanguageDefinition { - try { - const raw = fs.readFileSync(langFilePath(), 'utf8'); - const state = JSON.parse(raw) as LocalLanguageState; - return state.activeLanguage ?? ENGLISH; - } catch { - return ENGLISH; - } -} - -export function setActiveLanguage(lang: LanguageDefinition): LanguageDefinition { - const state: LocalLanguageState = { activeLanguageId: lang.id, activeLanguage: lang }; - fs.writeFileSync(langFilePath(), JSON.stringify(state, null, 2), 'utf8'); - return lang; -} - -export async function fetchRemoteLanguages(): Promise<{ - ok: boolean; - languages?: LanguageDefinition[]; - error?: string; -}> { - try { - const listing = await httpsGetJson(contentsUrl(GITHUB_CONFIG.languagesPath)); - if (!Array.isArray(listing)) return { ok: false, error: 'Languages folder not found' }; - const languages: LanguageDefinition[] = []; - for (const f of (listing as Array<{ name: string }>).filter((f) => f.name.endsWith('.json'))) { - try { - const lang = (await httpsGetJson( - rawUrl(GITHUB_CONFIG.languagesPath, f.name) - )) as LanguageDefinition; - if (lang.id && lang.name && lang.strings) languages.push(lang); - } catch { - /* skip */ - } - } - return { ok: true, languages }; - } catch (e) { - return { ok: false, error: String(e) }; - } -} - -export async function fetchRemoteLanguagePreviews(): Promise<{ - ok: boolean; - languages?: LanguagePreview[]; - error?: string; -}> { - try { - const listing = await httpsGetJson(contentsUrl(GITHUB_CONFIG.languagesPath)); - if (!Array.isArray(listing)) return { ok: false, error: 'Languages folder not found' }; - const languages: LanguagePreview[] = []; - for (const f of (listing as Array<{ name: string }>).filter((f) => f.name.endsWith('.json'))) { - try { - const lang = (await httpsGetJson( - rawUrl(GITHUB_CONFIG.languagesPath, f.name) - )) as LanguageDefinition; - if (lang.id && lang.name) { - languages.push({ id: lang.id, name: lang.name, filename: f.name }); - } - } catch { - /* skip */ - } - } - return { ok: true, languages }; - } catch (e) { - return { ok: false, error: String(e) }; - } -} - -export async function fetchRemoteLanguageByFile( - filename: string -): Promise<{ ok: boolean; language?: LanguageDefinition; error?: string }> { - try { - const lang = (await httpsGetJson( - rawUrl(GITHUB_CONFIG.languagesPath, filename) - )) as LanguageDefinition; - if (lang.id && lang.name && lang.strings) return { ok: true, language: lang }; - return { ok: false, error: 'Invalid language' }; - } catch (e) { - return { ok: false, error: String(e) }; - } -} - -// ─── Dev mode: load from local project directories ──────────────────────────── - -function projectRoot(): string { - return path.join(__dirname, '..', '..'); -} - -function loadLocalDevThemes(): ThemeDefinition[] { - const dir = path.join(projectRoot(), 'themes'); - if (!fs.existsSync(dir)) return []; - return fs - .readdirSync(dir) - .filter((f) => f.endsWith('.json')) - .map((f) => { - try { - const raw = fs.readFileSync(path.join(dir, f), 'utf8'); - const theme = JSON.parse(raw) as ThemeDefinition; - if (theme.id && theme.name && theme.colors) return theme; - } catch { - /* skip */ - } - return null; - }) - .filter((t): t is ThemeDefinition => t !== null); -} - -function loadLocalDevLanguages(): LanguageDefinition[] { - const dir = path.join(projectRoot(), 'languages'); - if (!fs.existsSync(dir)) return []; - return fs - .readdirSync(dir) - .filter((f) => f.endsWith('.json')) - .map((f) => { - try { - const raw = fs.readFileSync(path.join(dir, f), 'utf8'); - const lang = JSON.parse(raw) as LanguageDefinition; - if (lang.id && lang.name && lang.strings) return lang; - } catch { - /* skip */ - } - return null; - }) - .filter((l): l is LanguageDefinition => l !== null); -} - -export function loadDevAssets(): { themes: ThemeDefinition[]; languages: LanguageDefinition[] } { - return { - themes: loadLocalDevThemes(), - languages: loadLocalDevLanguages(), - }; -} diff --git a/src/main/rest-api/Base.routes.ts b/src/main/api/Base.routes.ts similarity index 81% rename from src/main/rest-api/Base.routes.ts rename to src/main/api/Base.routes.ts index de3292b..b948696 100644 --- a/src/main/rest-api/Base.routes.ts +++ b/src/main/api/Base.routes.ts @@ -1,9 +1,9 @@ -import { ok } from '../RestAPI'; -import { processManager } from '../ProcessManager'; -import { defineRoute, RouteMap } from '../shared/types/RestAPI.types'; -import { getAllProfiles, getSettings, saveSettings } from '../Store'; -import { AppSettings } from '../shared/types/App.types'; import { version } from '../../../package.json'; +import { ok } from '../core/RestAPI'; +import { getAllProfiles, getSettings, saveSettings } from '../core/Store'; +import { processManager } from '../core/process/ProcessManager'; +import { AppSettings } from '../shared/types/App.types'; +import { defineRoute, RouteMap } from '../shared/types/RestAPI.types'; export const BaseRoutes: RouteMap = { status: defineRoute('status', ({ res }) => diff --git a/src/main/rest-api/Log.routes.ts b/src/main/api/Log.routes.ts similarity index 86% rename from src/main/rest-api/Log.routes.ts rename to src/main/api/Log.routes.ts index 5ee90cc..46804f5 100644 --- a/src/main/rest-api/Log.routes.ts +++ b/src/main/api/Log.routes.ts @@ -1,6 +1,6 @@ -import { err, ok } from '../RestAPI'; +import { deleteLogFile, getLogFiles, readLogFile } from '../core/process/FileLogger'; +import { err, ok } from '../core/RestAPI'; import { defineRoute, RouteMap } from '../shared/types/RestAPI.types'; -import { getLogFiles, readLogFile, deleteLogFile } from '../FileLogger'; export const LogRoutes: RouteMap = { logs_list: defineRoute('logs_list', ({ res, params }) => { diff --git a/src/main/rest-api/Process.routes.ts b/src/main/api/Process.routes.ts similarity index 86% rename from src/main/rest-api/Process.routes.ts rename to src/main/api/Process.routes.ts index 69931ef..091ade3 100644 --- a/src/main/rest-api/Process.routes.ts +++ b/src/main/api/Process.routes.ts @@ -1,7 +1,7 @@ -import { err, ok } from '../RestAPI'; +import { processManager } from '../core/process/ProcessManager'; +import { err, ok } from '../core/RestAPI'; +import { getAllProfiles } from '../core/Store'; import { defineRoute, RouteMap } from '../shared/types/RestAPI.types'; -import { processManager } from '../ProcessManager'; -import { getAllProfiles } from '../Store'; export const ProcessRoutes: RouteMap = { processes_list: defineRoute('processes_list', ({ res }) => ok(res, processManager.getStates())), diff --git a/src/main/rest-api/Profile.routes.ts b/src/main/api/Profile.routes.ts similarity index 91% rename from src/main/rest-api/Profile.routes.ts rename to src/main/api/Profile.routes.ts index 3f1fb54..c153388 100644 --- a/src/main/rest-api/Profile.routes.ts +++ b/src/main/api/Profile.routes.ts @@ -1,9 +1,9 @@ -import { processManager } from '../ProcessManager'; -import { err, ok } from '../RestAPI'; +import { v4 as uuidv4 } from 'uuid'; +import { processManager } from '../core/process/ProcessManager'; +import { err, ok } from '../core/RestAPI'; +import { deleteProfile, getAllProfiles, saveProfile } from '../core/Store'; import { Profile } from '../shared/types/Profile.types'; -import { deleteProfile, getAllProfiles, saveProfile } from '../Store'; import { defineRoute, RouteMap } from '../shared/types/RestAPI.types'; -import { v4 as uuidv4 } from 'uuid'; export const ProfileRoutes: RouteMap = { profiles_list: defineRoute('profiles_list', ({ res }) => ok(res, getAllProfiles())), diff --git a/src/main/rest-api/_index.ts b/src/main/api/_index.ts similarity index 89% rename from src/main/rest-api/_index.ts rename to src/main/api/_index.ts index f920998..780f128 100644 --- a/src/main/rest-api/_index.ts +++ b/src/main/api/_index.ts @@ -1,10 +1,10 @@ import { RouteKey } from '../shared/config/API.config'; -import { RouteMap, BuiltRoute } from '../shared/types/RestAPI.types'; +import { BuiltRoute, RouteMap } from '../shared/types/RestAPI.types'; import { BaseRoutes } from './Base.routes'; -import { ProfileRoutes } from './Profile.routes'; -import { ProcessRoutes } from './Process.routes'; import { LogRoutes } from './Log.routes'; +import { ProcessRoutes } from './Process.routes'; +import { ProfileRoutes } from './Profile.routes'; const merged: RouteMap = { ...BaseRoutes, diff --git a/src/main/IPCController.ts b/src/main/core/IPCController.ts similarity index 98% rename from src/main/IPCController.ts rename to src/main/core/IPCController.ts index cc4f542..c0dd394 100644 --- a/src/main/IPCController.ts +++ b/src/main/core/IPCController.ts @@ -13,7 +13,7 @@ * main pushes via webContents.send(channel, ...args) */ -import { ipcMain, ipcRenderer, IpcMainInvokeEvent, IpcMainEvent, IpcRendererEvent } from 'electron'; +import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, ipcRenderer, IpcRendererEvent } from 'electron'; // ─── Route descriptors ──────────────────────────────────────────────────────── diff --git a/src/main/JRCEnvironment.ts b/src/main/core/JRCEnvironment.ts similarity index 90% rename from src/main/JRCEnvironment.ts rename to src/main/core/JRCEnvironment.ts index b544fcc..e475cd6 100644 --- a/src/main/JRCEnvironment.ts +++ b/src/main/core/JRCEnvironment.ts @@ -1,6 +1,6 @@ import { app, BrowserWindow } from 'electron'; -import { EnvironmentIPC } from './ipc/Environment.ipc'; -import { JRCEnvironment } from './shared/types/App.types'; +import { EnvironmentIPC } from '../ipc/Environment.ipc'; +import { JRCEnvironment } from '../shared/types/App.types'; import { getSettings } from './Store'; let env: JRCEnvironment = { diff --git a/src/main/RestAPI.ts b/src/main/core/RestAPI.ts similarity index 93% rename from src/main/RestAPI.ts rename to src/main/core/RestAPI.ts index 7c8445d..ef7fe9d 100644 --- a/src/main/RestAPI.ts +++ b/src/main/core/RestAPI.ts @@ -1,7 +1,7 @@ import http from 'http'; -import { routes } from './rest-api/_index'; -import { REST_API_CONFIG, routeConfig, RouteKey } from './shared/config/API.config'; -import { CompiledRoute, Params } from './shared/types/RestAPI.types'; +import { routes } from '../api/_index'; +import { REST_API_CONFIG } from '../shared/config/API.config'; +import { CompiledRoute, Params } from '../shared/types/RestAPI.types'; // ─── Helpers ────────────────────────────────────────────────────────────────── diff --git a/src/main/Store.ts b/src/main/core/Store.ts similarity index 91% rename from src/main/Store.ts rename to src/main/core/Store.ts index 6a10ea8..3231cdc 100644 --- a/src/main/Store.ts +++ b/src/main/core/Store.ts @@ -1,8 +1,8 @@ import { app } from 'electron'; import Store from 'electron-store'; -import { DEFAULT_SETTINGS } from './shared/config/App.config'; -import { Profile } from './shared/types/Profile.types'; -import { AppSettings } from './shared/types/App.types'; +import { DEFAULT_SETTINGS } from '../shared/config/App.config'; +import { AppSettings } from '../shared/types/App.types'; +import { Profile } from '../shared/types/Profile.types'; interface StoreSchema { profiles: Profile[]; diff --git a/src/main/FileLogger.ts b/src/main/core/process/FileLogger.ts similarity index 100% rename from src/main/FileLogger.ts rename to src/main/core/process/FileLogger.ts diff --git a/src/main/GracefulStop.ts b/src/main/core/process/GracefulStop.ts similarity index 100% rename from src/main/GracefulStop.ts rename to src/main/core/process/GracefulStop.ts diff --git a/src/main/ProcessManager.ts b/src/main/core/process/ProcessManager.ts similarity index 96% rename from src/main/ProcessManager.ts rename to src/main/core/process/ProcessManager.ts index 71e9826..a3d90ea 100644 --- a/src/main/ProcessManager.ts +++ b/src/main/core/process/ProcessManager.ts @@ -1,26 +1,23 @@ -import { spawn, execSync, ChildProcess } from 'child_process'; +import { ChildProcess, execSync, spawn } from 'child_process'; import { BrowserWindow } from 'electron'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; -import { PROTECTED_PROCESS_NAMES } from './shared/config/Scanner.config'; +import { ProcessIPC } from '../../ipc/Process.ipc'; +import { DEFAULT_JAR_RESOLUTION } from '../../shared/config/JarResolution.config'; +import { PROTECTED_PROCESS_NAMES } from '../../shared/config/Scanner.config'; import { ConsoleLine, JavaProcessInfo, ProcessLogEntry, ProcessState, -} from './shared/types/Process.types'; -import { Profile } from './shared/types/Profile.types'; -import { ProcessIPC } from './ipc/Process.ipc'; -import { DEFAULT_JAR_RESOLUTION } from './shared/config/JarResolution.config'; -import { processChunk, stripAnsi } from './shared/utils/AnsiParser'; +} from '../../shared/types/Process.types'; +import { Profile } from '../../shared/types/Profile.types'; +import { processChunk, stripAnsi } from '../../shared/utils/AnsiParser'; +import { startLogSession, stopLogSession, writeLogLine } from './FileLogger'; import { gracefulStop } from './GracefulStop'; -import { startLogSession, writeLogLine, stopLogSession } from './FileLogger'; import fs from 'fs'; -import { patternToRegex } from './shared/config/JarResolution.config'; -import type { JarResolutionConfig } from './shared/types/JarResolution.types'; -import { ProfileIPC } from './ipc/Profile.ipc'; -import { saveProfile } from './Store'; +import { patternToRegex } from '../../shared/config/JarResolution.config'; const SELF_PROCESS_NAME = 'Java Client Runner'; diff --git a/src/main/ipc/Asset.ipc.ts b/src/main/ipc/Asset.ipc.ts deleted file mode 100644 index 6b83e5c..0000000 --- a/src/main/ipc/Asset.ipc.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { RouteMap } from '../IPCController'; -import { - getActiveTheme, - setActiveTheme, - fetchRemoteThemes, - fetchRemoteThemePreviews, - fetchRemoteThemeByFile, - getActiveLanguage, - setActiveLanguage, - fetchRemoteLanguages, - fetchRemoteLanguagePreviews, - fetchRemoteLanguageByFile, - loadDevAssets, -} from '../AssetManager'; -import type { ThemeDefinition } from '../shared/types/Theme.types'; -import type { LanguageDefinition } from '../shared/types/Language.types'; - -export const AssetIPC = { - // Themes - getActiveTheme: { - type: 'invoke', - channel: 'asset:activeTheme', - handler: () => getActiveTheme(), - }, - setActiveTheme: { - type: 'invoke', - channel: 'asset:setTheme', - handler: (_e: any, theme: ThemeDefinition) => setActiveTheme(theme), - }, - fetchRemoteThemes: { - type: 'invoke', - channel: 'asset:fetchThemes', - handler: () => fetchRemoteThemes(), - }, - fetchThemePreviews: { - type: 'invoke', - channel: 'asset:themePreviews', - handler: () => fetchRemoteThemePreviews(), - }, - fetchThemeByFile: { - type: 'invoke', - channel: 'asset:themeByFile', - handler: (_e: any, filename: string) => fetchRemoteThemeByFile(filename), - }, - - // Languages - getActiveLanguage: { - type: 'invoke', - channel: 'asset:activeLang', - handler: () => getActiveLanguage(), - }, - setActiveLanguage: { - type: 'invoke', - channel: 'asset:setLang', - handler: (_e: any, lang: LanguageDefinition) => setActiveLanguage(lang), - }, - fetchRemoteLanguages: { - type: 'invoke', - channel: 'asset:fetchLangs', - handler: () => fetchRemoteLanguages(), - }, - fetchLanguagePreviews: { - type: 'invoke', - channel: 'asset:langPreviews', - handler: () => fetchRemoteLanguagePreviews(), - }, - fetchLanguageByFile: { - type: 'invoke', - channel: 'asset:langByFile', - handler: (_e: any, filename: string) => fetchRemoteLanguageByFile(filename), - }, - - // Dev - loadDevAssets: { - type: 'invoke', - channel: 'asset:loadDevAssets', - handler: () => loadDevAssets(), - }, -} satisfies RouteMap; diff --git a/src/main/ipc/Dev.ipc.ts b/src/main/ipc/Dev.ipc.ts index 840ff7b..c6eb8cd 100644 --- a/src/main/ipc/Dev.ipc.ts +++ b/src/main/ipc/Dev.ipc.ts @@ -1,7 +1,7 @@ import { BrowserWindow } from 'electron'; +import type { RouteMap } from '../core/IPCController'; +import { getAllProfiles } from '../core/Store'; import { DEFAULT_SETTINGS } from '../shared/config/App.config'; -import type { RouteMap } from '../IPCController'; -import { getAllProfiles, getSettings, toggleDevMode } from '../Store'; let getWindow: () => BrowserWindow | null = () => null; diff --git a/src/main/ipc/Environment.ipc.ts b/src/main/ipc/Environment.ipc.ts index 3d9ebbb..4b36396 100644 --- a/src/main/ipc/Environment.ipc.ts +++ b/src/main/ipc/Environment.ipc.ts @@ -1,8 +1,8 @@ import { BrowserWindow } from 'electron'; -import { getEnvironment, loadEnvironment } from '../JRCEnvironment'; -import { RouteMap } from '../IPCController'; +import { RouteMap } from '../core/IPCController'; +import { getEnvironment, loadEnvironment } from '../core/JRCEnvironment'; +import { toggleDevMode } from '../core/Store'; import { JRCEnvironment } from '../shared/types/App.types'; -import { toggleDevMode } from '../Store'; export const EnvironmentIPC = { get: { diff --git a/src/main/ipc/GitHub.ipc.ts b/src/main/ipc/GitHub.ipc.ts index 9245349..a32ab05 100644 --- a/src/main/ipc/GitHub.ipc.ts +++ b/src/main/ipc/GitHub.ipc.ts @@ -1,8 +1,8 @@ +import { dialog, shell } from 'electron'; import fs from 'fs'; import https from 'https'; -import { dialog, shell, BrowserWindow } from 'electron'; -import type { RouteMap } from '../IPCController'; -import { latestReleaseUrl, templateListUrl, rawTemplateUrl } from '../shared/config/GitHub.config'; +import type { RouteMap } from '../core/IPCController'; +import { latestReleaseUrl, rawTemplateUrl, templateListUrl } from '../shared/config/GitHub.config'; import type { GitHubRelease, ProfileTemplate } from '../shared/types/GitHub.types'; function httpsGet(url: string): Promise { diff --git a/src/main/ipc/JarResolution.ipc.ts b/src/main/ipc/JarResolution.ipc.ts index 3f6817f..fb19cf2 100644 --- a/src/main/ipc/JarResolution.ipc.ts +++ b/src/main/ipc/JarResolution.ipc.ts @@ -1,8 +1,8 @@ import fs from 'fs'; import path from 'path'; -import type { RouteMap } from '../IPCController'; -import type { JarResolutionConfig, JarResolutionResult } from '../shared/types/JarResolution.types'; +import type { RouteMap } from '../core/IPCController'; import { patternToRegex } from '../shared/config/JarResolution.config'; +import type { JarResolutionConfig, JarResolutionResult } from '../shared/types/JarResolution.types'; function parseVersion(str: string): number[] { return str diff --git a/src/main/ipc/Logging.ipc.ts b/src/main/ipc/Logging.ipc.ts index 6aaaa00..1e3e179 100644 --- a/src/main/ipc/Logging.ipc.ts +++ b/src/main/ipc/Logging.ipc.ts @@ -1,12 +1,12 @@ import { shell } from 'electron'; -import type { RouteMap } from '../IPCController'; +import type { RouteMap } from '../core/IPCController'; import { - getLogFiles, - readLogFile, deleteLogFile, + getLogFiles, getLogsDirectory, LogFileInfo, -} from '../FileLogger'; + readLogFile, +} from '../core/process/FileLogger'; export const LoggingIPC = { getLogFiles: { diff --git a/src/main/ipc/Process.ipc.ts b/src/main/ipc/Process.ipc.ts index fc7f5a0..b63349a 100644 --- a/src/main/ipc/Process.ipc.ts +++ b/src/main/ipc/Process.ipc.ts @@ -1,9 +1,9 @@ import { shell } from 'electron'; -import type { RouteMap } from '../IPCController'; -import { processManager } from '../ProcessManager'; -import { Profile } from '../shared/types/Profile.types'; +import type { RouteMap } from '../core/IPCController'; +import { getAllProfiles } from '../core/Store'; +import { processManager } from '../core/process/ProcessManager'; import { ConsoleLine, ProcessState } from '../shared/types/Process.types'; -import { getAllProfiles } from '../Store'; +import { Profile } from '../shared/types/Profile.types'; export const ProcessIPC = { startProcess: { diff --git a/src/main/ipc/Profile.ipc.ts b/src/main/ipc/Profile.ipc.ts index f69036b..2a79d21 100644 --- a/src/main/ipc/Profile.ipc.ts +++ b/src/main/ipc/Profile.ipc.ts @@ -1,6 +1,6 @@ -import type { RouteMap } from '../IPCController'; -import { getAllProfiles, saveProfile, deleteProfile, reorderProfiles } from '../Store'; -import { processManager } from '../ProcessManager'; +import type { RouteMap } from '../core/IPCController'; +import { deleteProfile, getAllProfiles, reorderProfiles, saveProfile } from '../core/Store'; +import { processManager } from '../core/process/ProcessManager'; import type { Profile } from '../shared/types/Profile.types'; export const ProfileIPC = { diff --git a/src/main/ipc/System.ipc.ts b/src/main/ipc/System.ipc.ts index 74a8744..0f08a6f 100644 --- a/src/main/ipc/System.ipc.ts +++ b/src/main/ipc/System.ipc.ts @@ -1,9 +1,8 @@ import { app, dialog, shell } from 'electron'; -import { restApiServer } from '../RestAPI'; -import type { RouteMap } from '../IPCController'; -import type { AppSettings, JRCEnvironment } from '../shared/types/App.types'; -import { getSettings, saveSettings } from '../Store'; -import { getEnvironment } from './../JRCEnvironment'; +import type { RouteMap } from '../core/IPCController'; +import { restApiServer } from '../core/RestAPI'; +import { getSettings, saveSettings } from '../core/Store'; +import type { AppSettings } from '../shared/types/App.types'; // mainWindow is needed for dialogs — set via initSystemIPC() called from main.ts let getWindow: () => Electron.BrowserWindow | null = () => null; diff --git a/src/main/ipc/Window.ipc.ts b/src/main/ipc/Window.ipc.ts index 7cc2eb6..b2b155e 100644 --- a/src/main/ipc/Window.ipc.ts +++ b/src/main/ipc/Window.ipc.ts @@ -1,6 +1,6 @@ import { app } from 'electron'; -import type { RouteMap } from '../IPCController'; -import { getSettings } from '../Store'; +import type { RouteMap } from '../core/IPCController'; +import { getSettings } from '../core/Store'; // Injected from main.ts — avoids a circular import on mainWindow/forceQuit let getWindow: () => Electron.BrowserWindow | null = () => null; diff --git a/src/main/ipc/_index.ts b/src/main/ipc/_index.ts index ecf2a12..3e47bf1 100644 --- a/src/main/ipc/_index.ts +++ b/src/main/ipc/_index.ts @@ -1,25 +1,23 @@ import { EnvironmentIPC } from './Environment.ipc'; -export { GitHubIPC } from './GitHub.ipc'; -export { ProcessIPC } from './Process.ipc'; -export { ProfileIPC } from './Profile.ipc'; -export { SystemIPC, initSystemIPC } from './System.ipc'; -export { WindowIPC, initWindowIPC } from './Window.ipc'; export { DevIPC, initDevIPC } from './Dev.ipc'; +export { GitHubIPC } from './GitHub.ipc'; export { JarResolutionIPC } from './JarResolution.ipc'; export { LoggingIPC } from './Logging.ipc'; -export { AssetIPC } from './Asset.ipc'; +export { ProcessIPC } from './Process.ipc'; +export { ProfileIPC } from './Profile.ipc'; +export { initSystemIPC, SystemIPC } from './System.ipc'; +export { initWindowIPC, WindowIPC } from './Window.ipc'; +import type { InferAPI } from '../core/IPCController'; +import { DevIPC } from './Dev.ipc'; import { GitHubIPC } from './GitHub.ipc'; +import { JarResolutionIPC } from './JarResolution.ipc'; +import { LoggingIPC } from './Logging.ipc'; import { ProcessIPC } from './Process.ipc'; import { ProfileIPC } from './Profile.ipc'; import { SystemIPC } from './System.ipc'; import { WindowIPC } from './Window.ipc'; -import { DevIPC } from './Dev.ipc'; -import { JarResolutionIPC } from './JarResolution.ipc'; -import { LoggingIPC } from './Logging.ipc'; -import { AssetIPC } from './Asset.ipc'; -import type { InferAPI } from '../IPCController'; export const allRoutes = [ GitHubIPC, @@ -30,7 +28,6 @@ export const allRoutes = [ DevIPC, JarResolutionIPC, LoggingIPC, - AssetIPC, ] as const; export type API = InferAPI< @@ -41,8 +38,7 @@ export type API = InferAPI< typeof WindowIPC & typeof DevIPC & typeof JarResolutionIPC & - typeof LoggingIPC & - typeof AssetIPC + typeof LoggingIPC >; export type Environment = InferAPI; diff --git a/src/main/main.ts b/src/main/main.ts index 266bf3f..2696dbc 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,20 +1,20 @@ import { app, BrowserWindow, Input, Menu, nativeImage, Tray } from 'electron'; import fs from 'fs'; import path from 'path'; +import { registerIPC } from './core/IPCController'; +import { getEnvironment, loadEnvironment, shouldStartMinimized } from './core/JRCEnvironment'; +import { processManager } from './core/process/ProcessManager'; +import { restApiServer } from './core/RestAPI'; +import { getAllProfiles, getSettings, syncLoginItem } from './core/Store'; import { allRoutes, initDevIPC, initSystemIPC, initWindowIPC } from './ipc/_index'; import { EnvironmentIPC } from './ipc/Environment.ipc'; -import { getActiveTheme } from './AssetManager'; -import { getEnvironment, loadEnvironment, shouldStartMinimized } from './JRCEnvironment'; -import { processManager } from './ProcessManager'; -import { restApiServer } from './RestAPI'; -import { registerIPC } from './IPCController'; -import { getAllProfiles, getSettings, syncLoginItem } from './Store'; +import { ALL_THEMES, BUILTIN_THEME } from './shared/config/Theme.config'; import { hasJarConfigured } from './shared/types/Profile.types'; loadEnvironment(); if (process.platform === 'win32') { - app.setAppUserModelId('com.timonmdy.java-runner-client'); + app.setAppUserModelId('Java Runner Client'); } const RESOURCES = @@ -48,7 +48,8 @@ function createWindow(): void { minWidth: 900, minHeight: 600, frame: false, - backgroundColor: getActiveTheme().colors['base-950'] ?? '#08090d', + backgroundColor: (ALL_THEMES.find((t) => t.id === getSettings().themeId) ?? BUILTIN_THEME) + .colors['base-950'], icon: getIconImage(), show: getEnvironment().startUpSource !== 'withSystem', webPreferences: { diff --git a/src/main/preload.ts b/src/main/preload.ts index d146002..da04c88 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -1,17 +1,7 @@ import { contextBridge } from 'electron'; +import { buildPreloadAPI } from './core/IPCController'; import { allRoutes } from './ipc/_index'; import { EnvironmentIPC } from './ipc/Environment.ipc'; -import { buildPreloadAPI } from './IPCController'; - -// Apply the stored theme background before React renders to avoid a flash of the wrong color. -try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const g = globalThis as any; - const bg = g.localStorage?.getItem('jrc:theme-bg'); - if (bg) g.document.documentElement.style.background = bg; -} catch { - /* ignore */ -} contextBridge.exposeInMainWorld('api', buildPreloadAPI([...allRoutes])); diff --git a/src/main/shared/config/App.config.ts b/src/main/shared/config/App.config.ts index 95c4b17..98ecf13 100644 --- a/src/main/shared/config/App.config.ts +++ b/src/main/shared/config/App.config.ts @@ -11,7 +11,8 @@ export const DEFAULT_SETTINGS: AppSettings = { consoleLineNumbers: false, consoleTimestamps: false, consoleHistorySize: 200, - theme: 'dark', + themeId: 'dark-default', + languageId: 'en', restApiEnabled: false, restApiPort: REST_API_CONFIG.defaultPort, devModeEnabled: false, diff --git a/src/main/shared/config/GitHub.config.ts b/src/main/shared/config/GitHub.config.ts index 5c6fcb9..3da94d2 100644 --- a/src/main/shared/config/GitHub.config.ts +++ b/src/main/shared/config/GitHub.config.ts @@ -1,28 +1,20 @@ export type TrustLevel = 'trusted' | 'automation' | 'unknown'; -export interface TrustedPublisher { - login: string; - label: string; -} - export const GITHUB_CONFIG = { owner: 'timonmdy', repo: 'java-runner-client', templatesPath: 'profile-templates', - themesPath: 'themes', - languagesPath: 'languages', templateMinVersion: 1, apiBase: 'https://api.github.com', - trustedPublishers: [{ login: 'timonmdy', label: 'Lead Developer' }] as TrustedPublisher[], + trustedPublishers: [{ login: 'timonmdy', label: 'Lead Developer' }] as { + login: string; + label: string; + }[], automationAccounts: ['github-actions[bot]', 'github-actions'], } as const; -export function releasesUrl(): string { - return `${GITHUB_CONFIG.apiBase}/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases`; -} - export function latestReleaseUrl(): string { return `${GITHUB_CONFIG.apiBase}/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/releases/latest`; } diff --git a/src/main/shared/config/Language.config.ts b/src/main/shared/config/Language.config.ts new file mode 100644 index 0000000..baf1313 --- /dev/null +++ b/src/main/shared/config/Language.config.ts @@ -0,0 +1,7 @@ +import type { LanguageDefinition } from '../types/Language.types'; +import { GERMAN } from './languages/de.lang'; +import { ENGLISH, ENGLISH_STRINGS } from './languages/en.lang'; + +export const ALL_LANGUAGES: LanguageDefinition[] = [ENGLISH, GERMAN]; + +export { ENGLISH, ENGLISH_STRINGS }; diff --git a/src/main/shared/config/Settings.config.ts b/src/main/shared/config/Settings.config.ts index 9e443f1..1a7b9cc 100644 --- a/src/main/shared/config/Settings.config.ts +++ b/src/main/shared/config/Settings.config.ts @@ -5,6 +5,5 @@ export const SETTINGS_TOPICS: SidebarTopic[] = [ { id: 'console', label: 'Console' }, { id: 'appearance', label: 'Appearance' }, { id: 'advanced', label: 'Advanced' }, - { id: 'updates', label: 'Updates' }, { id: 'about', label: 'About' }, ]; diff --git a/src/main/shared/config/Theme.config.ts b/src/main/shared/config/Theme.config.ts index 6dee664..ec3eef0 100644 --- a/src/main/shared/config/Theme.config.ts +++ b/src/main/shared/config/Theme.config.ts @@ -1,28 +1,11 @@ import type { ThemeDefinition } from '../types/Theme.types'; +import { DARK_DEFAULT_THEME } from './themes/dark-default.theme'; +import { LIGHT_THEME } from './themes/light.theme'; +import { MIDNIGHT_BLUE_THEME } from './themes/midnight-blue.theme'; -export const BUILTIN_THEME: ThemeDefinition = { - id: 'dark-default', - name: 'Dark (Default)', - version: 1, - author: 'JRC', - colors: { - accent: '#4ade80', - 'base-950': '#08090d', - 'base-900': '#0d0f14', - 'base-800': '#11141b', - 'surface-raised': '#1a1d26', - 'surface-border': '#242736', - 'text-primary': '#e8eaf2', - 'text-secondary': '#b0b4c8', - 'text-muted': '#6b7094', - 'console-error': '#f87171', - 'console-warn': '#fbbf24', - 'console-input': '#60a5fa', - 'console-system': '#6b7094', - }, -}; +export const BUILTIN_THEME: ThemeDefinition = DARK_DEFAULT_THEME; -export const THEME_GITHUB_PATH = 'themes'; +export const ALL_THEMES: ThemeDefinition[] = [DARK_DEFAULT_THEME, LIGHT_THEME, MIDNIGHT_BLUE_THEME]; /** CSS variable prefix used in tailwind.config.js */ export const CSS_VAR_PREFIX = '--c-'; diff --git a/src/main/shared/config/UpdateCenter.config.ts b/src/main/shared/config/UpdateCenter.config.ts deleted file mode 100644 index 056312a..0000000 --- a/src/main/shared/config/UpdateCenter.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface UpdatableDefinition { - id: string; - label: string; - description: string; -} - -export const UPDATE_ITEMS: UpdatableDefinition[] = [ - { id: 'app', label: 'Application', description: 'Java Runner Client core application' }, - { id: 'theme', label: 'Theme', description: 'Currently active visual theme' }, - { id: 'language', label: 'Language', description: 'Currently active language pack' }, -]; diff --git a/src/main/shared/config/languages/de.lang.ts b/src/main/shared/config/languages/de.lang.ts new file mode 100644 index 0000000..bf91ae8 --- /dev/null +++ b/src/main/shared/config/languages/de.lang.ts @@ -0,0 +1,327 @@ +import type { LanguageDefinition } from '../../types/Language.types'; + +export const GERMAN: LanguageDefinition = { + id: 'de', + name: 'Deutsch', + strings: { + 'general.save': 'Speichern', + 'general.saved': 'Gespeichert', + 'general.cancel': 'Abbrechen', + 'general.delete': 'Löschen', + 'general.close': 'Schließen', + 'general.retry': 'Erneut versuchen', + 'general.copy': 'Kopieren', + 'general.copyAll': 'Alles kopieren', + 'general.cut': 'Ausschneiden', + 'general.paste': 'Einfügen', + 'general.selectAll': 'Alles auswählen', + 'general.enabled': 'Aktiviert', + 'general.disabled': 'Deaktiviert', + 'general.on': 'An', + 'general.off': 'Aus', + 'general.yes': 'Ja', + 'general.no': 'Nein', + 'general.add': 'Hinzufügen', + 'general.noProfileSelected': 'Kein Profil ausgewählt', + 'general.back': 'Zurück', + 'general.confirm': 'Bestätigen', + 'general.loading': 'Laden...', + 'general.load': 'Laden', + 'general.refresh': 'Aktualisieren', + 'general.error': 'Fehler', + 'general.clear': 'Leeren', + 'general.none': 'keine', + 'sidebar.newProfile': 'Neues Profil', + 'sidebar.fromTemplate': 'Aus Vorlage', + 'sidebar.noProfiles': 'Noch keine Profile.', + 'sidebar.utilities': 'Werkzeuge', + 'sidebar.faq': 'FAQ', + 'sidebar.settings': 'Einstellungen', + 'sidebar.developer': 'Entwickleroptionen', + 'tabs.console': 'Konsole', + 'tabs.configure': 'Konfigurieren', + 'tabs.logs': 'Protokolle', + 'tabs.profile': 'Profil', + 'ctx.select': 'Auswählen', + 'ctx.clearConsole': 'Konsole leeren', + 'console.run': 'Starten', + 'console.stop': 'Stoppen', + 'console.forceKill': 'Sofort beenden', + 'console.forceKillHint': 'Sofort beenden (ohne sauberes Herunterfahren)', + 'console.openWorkDir': 'Arbeitsverzeichnis öffnen', + 'console.scrollToBottom': 'nach unten scrollen', + 'console.search': 'Suchen (Strg+F)', + 'console.clear': 'Leeren (Strg+L)', + 'console.lines': 'Zeilen', + 'console.noMatches': 'Keine Treffer', + 'console.searchPlaceholder': 'Konsole durchsuchen... (Enter nächster, Shift+Enter vorheriger)', + 'console.inputPlaceholder': + 'Befehl senden... (Hoch/Runter Verlauf, Strg+L leeren, Strg+F suchen)', + 'console.inputDisabled': 'Prozess starten um Befehle zu senden', + 'console.waiting': 'Warte auf Ausgabe...', + 'console.notRunning': 'Prozess läuft nicht. Drücke Starten um zu beginnen.', + 'console.noJar': 'Keine JAR konfiguriert. Gehe zu Konfigurieren.', + 'console.copyLine': 'Zeile kopieren', + 'console.copyAll': 'Gesamte Ausgabe kopieren', + 'config.general': 'Allgemein', + 'config.files': 'Dateien & Pfade', + 'config.jvm': 'JVM-Argumente', + 'config.props': 'Eigenschaften (-D)', + 'config.args': 'Programmargumente', + 'config.env': 'Umgebung', + 'config.unsavedChanges': 'Nicht gespeicherte Änderungen', + 'config.restartNeeded': 'Neustart erforderlich', + 'config.autoStart': 'Automatisch starten beim App-Start', + 'config.autoStartHint': + 'Diese JAR wird automatisch gestartet wenn Java Runner Client geöffnet wird.', + 'config.autoRestart': 'Automatischer Neustart bei Absturz', + 'config.autoRestartHint': + 'Startet den Prozess nach einem unerwarteten Beenden automatisch neu.', + 'config.autoRestartInterval': 'Neustart-Verzögerung', + 'config.fileLogging': 'Sitzungsprotokolle in Datei speichern', + 'config.fileLoggingHint': + 'Schreibt Konsolenausgaben in .log-Dateien im Konfigurationsverzeichnis pro Sitzung', + 'config.restartProcess': 'Prozess neustarten', + 'config.logging': 'Protokollierung', + 'config.process': 'Prozess', + 'config.jvmTitle': 'JVM-Argumente', + 'config.jvmHint': 'Flags die vor -jar an die JVM übergeben werden, z.B. -Xmx2g -XX:+UseG1GC', + 'config.propsTitle': 'Systemeigenschaften', + 'config.propsHint': 'Übergeben als -Dkey=value. Spring-Profile, Ports, Logging-Level usw.', + 'config.argsTitle': 'Programmargumente', + 'config.argsHint': 'Argumente die nach der JAR übergeben werden, z.B. --nogui --world myWorld', + 'config.envTitle': 'Umgebungsvariablen', + 'config.envHint': + 'Werden in die Prozessumgebung eingefügt. Überschreiben System-Umgebungsvariablen mit gleichem Schlüssel.', + 'config.commandPreview': 'Befehlsvorschau', + 'config.pendingArgTitle': 'Nicht gespeicherte Eingabe', + 'config.pendingArgMessage': + 'Du hast Text im Eingabefeld der noch nicht hinzugefügt wurde.\n\nKlicke zuerst "+ Hinzufügen", sonst wird er nicht übernommen.\n\nTrotzdem wechseln?', + 'config.pendingArgConfirm': 'Wechseln', + 'config.pendingArgCancel': 'Bleiben', + 'config.autoRestartIntervalHint': 'Sekunden bis zum Neustart', + 'config.sec': 'Sek', + 'config.jarSelection': 'JAR-Auswahl', + 'config.jarSelectionMethod': 'Auswahlmethode', + 'config.static': 'Statisch', + 'config.dynamic': 'Dynamisch', + 'config.jarFile': 'JAR-Datei', + 'config.jarFilePlaceholder': 'Pfad zur .jar-Datei', + 'config.jarFileHint': 'Die auszuführende JAR-Datei', + 'config.workDir': 'Arbeitsverzeichnis', + 'config.workDirPlaceholder': 'Standard: JAR-Verzeichnis', + 'config.workDirHint': 'Leer lassen um das Verzeichnis der JAR zu verwenden', + 'config.javaExe': 'Java-Programm', + 'config.javaExePlaceholder': 'java (verwendet System-PATH)', + 'config.javaExeHint': 'Leer lassen um das java aus dem PATH zu verwenden', + 'config.baseDir': 'Basisverzeichnis', + 'config.baseDirPlaceholder': 'Verzeichnis mit deinen JARs', + 'config.baseDirHint': 'Ordner der nach passenden JAR-Dateien durchsucht wird', + 'config.strategy': 'Strategie', + 'config.filenamePattern': 'Dateinamenmuster', + 'config.filenamePatternHint': + '{version} als Platzhalter verwenden — z.B. "myapp-{version}.jar"', + 'config.regex': 'Regulärer Ausdruck', + 'config.regexHint': + 'Wird gegen Dateinamen im Basisverzeichnis geprüft (Groß-/Kleinschreibung egal)', + 'config.resolving': 'Wird aufgelöst...', + 'config.noMatchFound': 'Kein Treffer gefunden', + 'config.autoStartTitle': 'Autostart', + 'config.autoRestartTitle': 'Auto-Neustart', + 'config.strategyHighestVersion': 'Höchste Version', + 'config.strategyHighestVersionHint': + 'Wählt die JAR mit der höchsten semantischen oder numerischen Version aus dem Dateinamen.', + 'config.strategyLatestModified': 'Zuletzt geändert', + 'config.strategyLatestModifiedHint': + 'Wählt die zuletzt geänderte JAR im Verzeichnis die dem Muster entspricht.', + 'config.strategyRegex': 'Regex-Übereinstimmung', + 'config.strategyRegexHint': + 'Wählt die erste JAR deren Dateiname dem regulären Ausdruck entspricht.', + 'config.otherSingular': '+{count} weitere', + 'config.otherPlural': '+{count} weitere', + 'profile.identity': 'Profil-Identität', + 'profile.name': 'Name', + 'profile.accentColour': 'Akzentfarbe', + 'profile.accentColourHint': 'Wird in der Seitenleiste und als Tab-Hervorhebung verwendet.', + 'profile.customColour': 'Eigene Farbe wählen', + 'profile.dangerZone': 'Gefahrenzone', + 'profile.deleteProfile': 'Profil löschen', + 'profile.deleteHint': + 'Entfernt dieses Profil und alle Einstellungen dauerhaft. Shift halten um Bestätigung zu überspringen.', + 'profile.deleteConfirmTitle': 'Profil löschen?', + 'profile.deleteConfirmMessage': + '"{name}" wird dauerhaft entfernt. Dies kann nicht rückgängig gemacht werden.', + 'logs.title': 'Sitzungsprotokolle', + 'logs.files': 'Dateien', + 'logs.openDir': 'Protokollverzeichnis öffnen', + 'logs.noFiles': + 'Noch keine Protokolldateien. Starte und stoppe einen Prozess um eine zu erstellen.', + 'logs.selectFile': 'Wähle eine Protokolldatei um den Inhalt anzuzeigen', + 'logs.deleteHint': 'Protokolldatei löschen (Shift halten um Bestätigung zu überspringen)', + 'logs.deleteTitle': 'Protokolldatei löschen?', + 'logs.deleteMessage': '"{name}" wird dauerhaft gelöscht.', + 'logs.disabled': 'Dateiprotokollierung ist für dieses Profil deaktiviert.', + 'logs.disabledHint': + 'Aktiviere sie unter Konfigurieren > Allgemein > Sitzungsprotokolle in Datei speichern.', + 'settings.title': 'Anwendungseinstellungen', + 'settings.saved': 'Einstellungen gespeichert', + 'settings.unsaved': 'Nicht gespeicherte Änderungen', + 'settings.saveChanges': 'Änderungen speichern', + 'settings.general': 'Allgemein', + 'settings.startup': 'Autostart', + 'settings.launchOnStartup': 'Beim Windows-Start starten', + 'settings.launchOnStartupHint': 'Java Runner Client startet automatisch beim Anmelden', + 'settings.startMinimized': 'Minimiert in den Tray starten', + 'settings.startMinimizedHint': 'Das Fenster erscheint nicht beim Start -- nur das Tray-Symbol', + 'settings.minimizeToTray': 'Beim Schließen in den Tray minimieren', + 'settings.minimizeToTrayHint': + 'Das Schließen des Fensters hält die App und laufende JARs im Hintergrund aktiv', + 'settings.console': 'Konsole', + 'settings.fontSize': 'Schriftgröße', + 'settings.fontSizeHint': 'Schriftgröße der Konsolenausgabe in Pixeln', + 'settings.lineNumbers': 'Zeilennummern anzeigen', + 'settings.lineNumbersHint': 'Zeigt eine Zeilennummern-Spalte in der Konsolenausgabe', + 'settings.timestamps': 'Zeitstempel anzeigen', + 'settings.timestampsHint': 'Zeigt einen Zeitstempel für jede Konsolenzeile', + 'settings.wordWrap': 'Zeilenumbruch', + 'settings.wordWrapHint': 'Lange Zeilen umbrechen statt horizontal zu scrollen', + 'settings.maxLines': 'Maximale Zeilen im Puffer', + 'settings.maxLinesHint': 'Ältere Zeilen werden verworfen wenn das Limit erreicht ist', + 'settings.historySize': 'Befehlsverlauf-Größe', + 'settings.historySizeHint': 'Gespeicherte Befehle pro Sitzung (Hoch/Runter zum Navigieren)', + 'settings.appearance': 'Darstellung', + 'settings.theme': 'Design', + 'settings.themeHint': 'Visuelles Design auswählen', + 'settings.language': 'Sprache', + 'settings.languageHint': 'Anzeigesprache auswählen', + 'appearance.searchThemes': 'Designs suchen...', + 'appearance.searchLanguages': 'Sprachen suchen...', + 'appearance.noThemesFound': 'Keine Designs gefunden', + 'appearance.noLanguagesFound': 'Keine Sprachen gefunden', + 'settings.advanced': 'Erweitert', + 'settings.devMode': 'Entwickleroptionen', + 'settings.devModeLabel': 'Entwicklermodus umschalten (Rechts-Shift + 7)', + 'settings.devModeHint': 'Aktiviert den Entwickler-Tab und DevTools. Mit Vorsicht verwenden.', + 'settings.restApi': 'REST-API', + 'settings.restApiLabel': 'REST-API aktivieren', + 'settings.restApiHint': + 'Stellt eine lokale HTTP-API für Automatisierung bereit (Standard-Port {port})', + 'settings.restApiPort': 'Port', + 'settings.restApiPortHint': 'Neustart erforderlich um den Port zu ändern', + 'settings.listeningOn': 'Lauscht auf', + 'settings.endpoints': 'Endpunkte', + 'settings.upToDate': 'Aktuell', + 'settings.updateAvailable': 'Update verfügbar', + 'settings.checking': 'Prüfe...', + 'settings.checkFailed': 'Prüfung fehlgeschlagen', + 'settings.versionCurrent': 'Aktuell:', + 'settings.checkForUpdates': 'Nach Updates suchen', + 'settings.viewUpdate': 'Update ansehen', + 'settings.upToDateTooltip': 'Du hast die neueste Version ({version})', + 'settings.updateAvailableTooltip': '{tag} ist verfügbar — für Details klicken', + 'settings.about': 'Über', + 'settings.version': 'Version', + 'settings.stack': 'Technologie', + 'settings.configPath': 'Konfiguration', + 'settings.openConfigFolder': 'Konfigurationsordner öffnen', + 'release.title': 'Release-Details', + 'release.preRelease': 'Vorabversion', + 'release.stable': 'Stabil', + 'release.trustedDev': 'Vertrauenswürdiger Entwickler', + 'release.automation': 'Automatisiertes Release', + 'release.unknownPublisher': 'Unbekannter Herausgeber', + 'release.unknownPublisherHint': + 'Dieses Release wurde von einem GitHub-Benutzer veröffentlicht der nicht in der vertrauenswürdigen Liste steht. Es wurde dennoch durch die GitHub-Repository-Sicherheit genehmigt.', + 'release.downloads': 'Downloads', + 'release.otherAssets': 'Weitere Dateien', + 'release.releaseNotes': 'Release-Hinweise', + 'release.viewOnGithub': 'Auf GitHub ansehen', + 'release.allAssets': 'Alle Dateien', + 'release.download': 'Herunterladen', + 'release.downloadAgain': 'Erneut herunterladen', + 'release.resume': 'Fortsetzen', + 'release.pause': 'Pausieren', + 'release.cancelled': 'Abgebrochen', + 'release.complete': 'Abgeschlossen', + 'release.paused': 'Pausiert', + 'utilities.title': 'Werkzeuge', + 'utilities.activityLog': 'Aktivitätsprotokoll', + 'utilities.processScanner': 'Prozess-Scanner', + 'activity.description': 'Alle von JRC gestarteten Prozesse dieser Sitzung', + 'activity.empty': 'Noch keine Prozesse in dieser Sitzung gestartet', + 'activity.clearTitle': 'Aktivitätsprotokoll leeren?', + 'activity.clearMessage': + 'Alle aufgezeichneten Prozesseinträge werden entfernt. Laufende Prozesse sind nicht betroffen.', + 'activity.stopped': 'gestoppt', + 'activity.running': 'läuft', + 'scanner.foundProcesses': '{count} Prozesse gefunden — {javaCount} Java', + 'scanner.killedPid': 'PID {pid} beendet', + 'scanner.killFailed': 'PID {pid} konnte nicht beendet werden: {error}', + 'scanner.killedAll': '{killed} Java-Prozess beendet (geschützte übersprungen)', + 'scanner.killedAllPlural': '{killed} Java-Prozesse beendet (geschützte übersprungen)', + 'scanner.killProtectedMessage': + 'Dieser Prozess ist als geschützt markiert.\n\nBefehl: {command}\n\nBist du sicher?', + 'scanner.killNonJavaMessage': + 'Warnung: kein Java-Prozess.\n\nBefehl: {command}\n\nDas gewaltsame Beenden unbekannter Prozesse kann zu Datenverlust führen.', + 'scanner.killPidMessage': 'PID {pid} gewaltsam beenden?\n\nBefehl: {command}', + 'scanner.javaOnly': 'Nur Java', + 'scanner.all': 'Alle', + 'scanner.searchPlaceholder': 'PID oder Befehl suchen...', + 'scanner.killAll': 'Alle Java beenden', + 'scanner.scan': 'Scannen', + 'scanner.rescan': 'Erneut scannen', + 'scanner.scanHint': 'Klicke "Scannen" um alle laufenden Prozesse aufzulisten', + 'scanner.scanning': 'Alle Prozesse werden gescannt...', + 'scanner.noJava': 'Keine Java-Prozesse gefunden', + 'scanner.noProcesses': 'Keine Prozesse gefunden', + 'scanner.killProtectedTitle': 'Geschützten Prozess beenden?', + 'scanner.killNonJavaTitle': 'Nicht-Java-Prozess beenden?', + 'scanner.killPidTitle': 'PID {pid} beenden?', + 'scanner.killAllTitle': 'Alle Java-Prozesse beenden?', + 'scanner.killAllMessage': + 'Dies beendet gewaltsam jeden nicht geschützten Java-Prozess. Geschützte Prozesse werden übersprungen. Laufende Server verlieren nicht gespeicherte Daten.', + 'scanner.killAnyway': 'Trotzdem beenden', + 'scanner.killProcess': 'Prozess beenden', + 'scanner.killAllLabel': 'Alle beenden', + 'scanner.kill': 'Beenden', + 'scanner.fullCommand': 'Vollständiger Befehl', + 'scanner.memory': 'Arbeitsspeicher', + 'scanner.threads': 'Threads', + 'scanner.started': 'Gestartet', + 'scanner.managedByJrc': 'Verwaltet von JRC', + 'scanner.protectedLabel': 'Geschützt', + 'scanner.protectedYes': 'Ja — ausgenommen von Alle Java beenden', + 'scanner.managedBadge': 'Verwaltet', + 'scanner.javaBadge': 'Java', + 'scanner.nonJavaBadge': 'Nicht-Java', + 'panels.settings': 'Anwendungseinstellungen', + 'panels.faq': 'FAQ', + 'panels.utilities': 'Werkzeuge', + 'panels.developer': 'Entwickleroptionen', + 'dev.mode': 'Entwicklermodus', + 'dev.dashboard': 'Dashboard', + 'dev.apiExplorer': 'API-Explorer', + 'dev.storage': 'Speicher', + 'dev.diagnostics': 'Diagnose', + 'dev.assets': 'Assets', + 'dev.translationCoverage': 'Übersetzungsabdeckung', + 'dev.keysTranslated': '{count} von {total} Schlüsseln übersetzt', + 'dev.missingKeys': 'Fehlende Schlüssel', + 'dev.apply': 'Anwenden', + 'dev.activeTheme': 'Aktives Design', + 'dev.activeLang': 'Aktive Sprache', + 'dev.bundled': 'Eingebaut', + 'template.title': 'Profil-Vorlagen', + 'template.searchPlaceholder': 'Vorlagen suchen...', + 'template.noTemplates': 'Keine Vorlagen gefunden.', + 'template.selectHint': 'Wähle eine Vorlage um die Konfiguration anzuzeigen', + 'template.createProfile': 'Profil erstellen', + 'template.jvmArgs': 'JVM-Argumente', + 'template.systemProperties': 'Systemeigenschaften', + 'template.programArgs': 'Programmargumente', + 'template.versionInfo': 'Vorlagenversion {version} · Erfordert App {appVersion}+', + 'faq.searchPlaceholder': 'FAQ durchsuchen...', + 'faq.noResults': 'Keine Ergebnisse gefunden.', + 'faq.noItems': 'Keine Einträge in diesem Thema.', + }, +}; diff --git a/src/main/shared/config/DefaultLanguage.config.ts b/src/main/shared/config/languages/en.lang.ts similarity index 91% rename from src/main/shared/config/DefaultLanguage.config.ts rename to src/main/shared/config/languages/en.lang.ts index 83a3271..5acabd2 100644 --- a/src/main/shared/config/DefaultLanguage.config.ts +++ b/src/main/shared/config/languages/en.lang.ts @@ -1,4 +1,4 @@ -import type { LanguageDefinition } from '../types/Language.types'; +import type { LanguageDefinition } from '../../types/Language.types'; const ENGLISH_STRINGS = { // General @@ -173,8 +173,6 @@ const ENGLISH_STRINGS = { 'settings.saved': 'Settings saved', 'settings.unsaved': 'Unsaved changes', 'settings.saveChanges': 'Save Changes', - - // Settings: General 'settings.general': 'General', 'settings.startup': 'Startup', 'settings.launchOnStartup': 'Launch on Windows startup', @@ -184,8 +182,6 @@ const ENGLISH_STRINGS = { 'settings.minimizeToTray': 'Minimize to tray on close', 'settings.minimizeToTrayHint': 'Closing the window keeps the app and running JARs alive in the background', - - // Settings: Console 'settings.console': 'Console', 'settings.fontSize': 'Font size', 'settings.fontSizeHint': 'Console output font size in pixels', @@ -199,30 +195,15 @@ const ENGLISH_STRINGS = { 'settings.maxLinesHint': 'Older lines are discarded when the limit is reached', 'settings.historySize': 'Command history size', 'settings.historySizeHint': 'Commands stored per session (Up/Down to navigate)', - - // Settings: Appearance 'settings.appearance': 'Appearance', 'settings.theme': 'Theme', 'settings.themeHint': 'Select a visual theme', - 'settings.themeBuiltin': 'Built-in', - 'settings.themeCheckUpdate': 'Check for theme update', 'settings.language': 'Language', 'settings.languageHint': 'Select a display language', - 'settings.languageCheckUpdate': 'Check for language update', - - // Appearance section - 'appearance.fetchThemesFailed': 'Failed to fetch themes.', - 'appearance.fetchLangsFailed': 'Failed to fetch languages.', - 'appearance.development': 'Development', - 'appearance.syncTitle': 'Sync local project files', - 'appearance.syncHint': - 'Load themes and languages from /themes and /languages in the project root', - 'appearance.sync': 'Sync', - 'appearance.synced': 'Synced', - 'appearance.localOnly': 'Local only', - 'appearance.localOverride': 'Overrides remote', - - // Settings: Advanced + 'appearance.searchThemes': 'Search themes...', + 'appearance.searchLanguages': 'Search languages...', + 'appearance.noThemesFound': 'No themes match your search', + 'appearance.noLanguagesFound': 'No languages match your search', 'settings.advanced': 'Advanced', 'settings.devMode': 'Developer Options', 'settings.devModeLabel': 'Toggle Developer Mode (Right-Shift + 7)', @@ -234,29 +215,15 @@ const ENGLISH_STRINGS = { 'settings.restApiPortHint': 'Restart required to change the port', 'settings.listeningOn': 'Listening on', 'settings.endpoints': 'Endpoints', - - // Settings: Updates - 'settings.updates': 'Updates', - 'settings.updateCenter': 'Update Center', - 'settings.checkAll': 'Check All', - 'settings.updateAll': 'Update All', 'settings.upToDate': 'Up to date', 'settings.updateAvailable': 'Update available', 'settings.checking': 'Checking...', 'settings.checkFailed': 'Check failed', - 'settings.updatesHint': 'Check for updates to the app, themes, and language packs', - 'settings.allUpToDate': 'Everything is up to date.', - 'settings.updatedSuccess': 'Updated successfully', - 'settings.applyingUpdate': 'Applying update...', - 'settings.check': 'Check', - 'settings.update': 'Update', 'settings.versionCurrent': 'Current:', 'settings.checkForUpdates': 'Check for updates', 'settings.viewUpdate': 'View update', 'settings.upToDateTooltip': 'You are on the latest version ({version})', 'settings.updateAvailableTooltip': '{tag} is available — click for details', - - // Settings: About 'settings.about': 'About', 'settings.version': 'Version', 'settings.stack': 'Stack', @@ -289,8 +256,6 @@ const ENGLISH_STRINGS = { 'utilities.title': 'Utilities', 'utilities.activityLog': 'Activity Log', 'utilities.processScanner': 'Process Scanner', - - // Activity Log 'activity.description': 'All processes started by JRC this session', 'activity.empty': 'No processes started yet this session', 'activity.clearTitle': 'Clear activity log?', @@ -341,7 +306,7 @@ const ENGLISH_STRINGS = { 'scanner.javaBadge': 'Java', 'scanner.nonJavaBadge': 'Non-Java', - // Panel headers + // Panels 'panels.settings': 'Application Settings', 'panels.faq': 'FAQ', 'panels.utilities': 'Utilities', @@ -353,6 +318,14 @@ const ENGLISH_STRINGS = { 'dev.apiExplorer': 'API Explorer', 'dev.storage': 'Storage', 'dev.diagnostics': 'Diagnostics', + 'dev.assets': 'Assets', + 'dev.translationCoverage': 'Translation Coverage', + 'dev.keysTranslated': '{count} of {total} keys translated', + 'dev.missingKeys': 'Missing Keys', + 'dev.apply': 'Apply', + 'dev.activeTheme': 'Active theme', + 'dev.activeLang': 'Active language', + 'dev.bundled': 'Bundled', // Template modal 'template.title': 'Profile Templates', @@ -374,8 +347,6 @@ const ENGLISH_STRINGS = { export const ENGLISH: LanguageDefinition = { id: 'en', name: 'English', - version: 2, - author: 'JRC', strings: ENGLISH_STRINGS, }; diff --git a/src/main/shared/config/themes/dark-default.theme.ts b/src/main/shared/config/themes/dark-default.theme.ts new file mode 100644 index 0000000..df112ef --- /dev/null +++ b/src/main/shared/config/themes/dark-default.theme.ts @@ -0,0 +1,22 @@ +import type { ThemeDefinition } from '../../types/Theme.types'; + +export const DARK_DEFAULT_THEME: ThemeDefinition = { + id: 'dark-default', + name: 'Dark (Default)', + author: 'timonmdy', + colors: { + accent: '#4ade80', + 'base-950': '#08090d', + 'base-900': '#0d0f14', + 'base-800': '#11141b', + 'surface-raised': '#1a1d26', + 'surface-border': '#242736', + 'text-primary': '#e8eaf2', + 'text-secondary': '#b0b4c8', + 'text-muted': '#6b7094', + 'console-error': '#f87171', + 'console-warn': '#fbbf24', + 'console-input': '#60a5fa', + 'console-system': '#6b7094', + }, +}; diff --git a/src/main/shared/config/themes/light.theme.ts b/src/main/shared/config/themes/light.theme.ts new file mode 100644 index 0000000..592e781 --- /dev/null +++ b/src/main/shared/config/themes/light.theme.ts @@ -0,0 +1,22 @@ +import type { ThemeDefinition } from '../../types/Theme.types'; + +export const LIGHT_THEME: ThemeDefinition = { + id: 'light', + name: 'Light', + author: 'timonmdy', + colors: { + accent: '#7c3aed', + 'base-950': '#e8ecf5', + 'base-900': '#f0f3fb', + 'base-800': '#dce1ef', + 'surface-raised': '#ffffff', + 'surface-border': '#c8cedf', + 'text-primary': '#181b2d', + 'text-secondary': '#454d6e', + 'text-muted': '#7980a0', + 'console-error': '#dc2626', + 'console-warn': '#b45309', + 'console-input': '#1d4ed8', + 'console-system': '#7980a0', + }, +}; diff --git a/src/main/shared/config/themes/midnight-blue.theme.ts b/src/main/shared/config/themes/midnight-blue.theme.ts new file mode 100644 index 0000000..c75147c --- /dev/null +++ b/src/main/shared/config/themes/midnight-blue.theme.ts @@ -0,0 +1,22 @@ +import type { ThemeDefinition } from '../../types/Theme.types'; + +export const MIDNIGHT_BLUE_THEME: ThemeDefinition = { + id: 'midnight-blue', + name: 'Midnight Blue', + author: 'timonmdy', + colors: { + accent: '#60a5fa', + 'base-950': '#0a0c14', + 'base-900': '#0e1220', + 'base-800': '#131929', + 'surface-raised': '#1c2438', + 'surface-border': '#283350', + 'text-primary': '#e2e8f4', + 'text-secondary': '#94a3c8', + 'text-muted': '#5b6b8a', + 'console-error': '#fb7185', + 'console-warn': '#fbbf24', + 'console-input': '#818cf8', + 'console-system': '#5b6b8a', + }, +}; diff --git a/src/main/shared/types/App.types.ts b/src/main/shared/types/App.types.ts index 38f77fe..0b5a010 100644 --- a/src/main/shared/types/App.types.ts +++ b/src/main/shared/types/App.types.ts @@ -8,7 +8,8 @@ export interface AppSettings { consoleLineNumbers: boolean; consoleTimestamps: boolean; consoleHistorySize: number; - theme: 'dark'; + themeId: string; + languageId: string; restApiEnabled: boolean; restApiPort: number; devModeEnabled: boolean; diff --git a/src/main/shared/types/Language.types.ts b/src/main/shared/types/Language.types.ts index 626ed5a..433eaf5 100644 --- a/src/main/shared/types/Language.types.ts +++ b/src/main/shared/types/Language.types.ts @@ -1,18 +1,5 @@ export interface LanguageDefinition { id: string; name: string; - version: number; - author: string; strings: Record; } - -export interface LocalLanguageState { - activeLanguageId: string; - activeLanguage: LanguageDefinition; -} - -export interface LanguagePreview { - id: string; - name: string; - filename: string; -} diff --git a/src/main/shared/types/Theme.types.ts b/src/main/shared/types/Theme.types.ts index 4ff4688..8a567e1 100644 --- a/src/main/shared/types/Theme.types.ts +++ b/src/main/shared/types/Theme.types.ts @@ -17,24 +17,6 @@ export interface ThemeColors { export interface ThemeDefinition { id: string; name: string; - version: number; author: string; colors: ThemeColors; } - -export interface LocalThemeState { - activeThemeId: string; - activeTheme: ThemeDefinition; -} - -export type ThemePreviewColors = Pick< - ThemeColors, - 'accent' | 'base-900' | 'surface-raised' | 'text-primary' ->; - -export interface ThemePreview { - id: string; - name: string; - filename: string; - previewColors: ThemePreviewColors; -} diff --git a/src/main/shared/types/UpdateCenter.types.ts b/src/main/shared/types/UpdateCenter.types.ts deleted file mode 100644 index 718fbab..0000000 --- a/src/main/shared/types/UpdateCenter.types.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type UpdateStatus = - | 'idle' - | 'checking' - | 'up-to-date' - | 'update-available' - | 'updating' - | 'done' - | 'error'; - -export interface UpdateCheckResult { - hasUpdate: boolean; - currentVersion: string | number; - remoteVersion: string | number; - error?: string; -} - -export interface Updatable { - id: string; - label: string; - description: string; - check: () => Promise; - apply: () => Promise<{ ok: boolean; error?: string }>; -} diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index f8c6595..45120a0 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,11 +1,10 @@ -import React from 'react'; import { HashRouter, Navigate, Route, Routes } from 'react-router-dom'; import { AppProvider } from './AppProvider'; -import { ThemeProvider } from './hooks/ThemeProvider'; -import { I18nProvider } from './i18n/I18nProvider'; +import { DevModeGate } from './components/developer/DevModeGate'; import { TitleBar } from './components/layout/TitleBar'; import { MainLayout } from './components/MainLayout'; -import { DevModeGate } from './components/developer/DevModeGate'; +import { ThemeProvider } from './hooks/ThemeProvider'; +import { I18nProvider } from './i18n/I18nProvider'; function Fallback() { return ( diff --git a/src/renderer/AppProvider.tsx b/src/renderer/AppProvider.tsx index 247ab37..2c41efe 100644 --- a/src/renderer/AppProvider.tsx +++ b/src/renderer/AppProvider.tsx @@ -1,3 +1,6 @@ +import { AppSettings } from '@shared/types/App.types'; +import { ConsoleLine, ProcessState } from '@shared/types/Process.types'; +import { Profile } from '@shared/types/Profile.types'; import { createContext, useCallback, @@ -7,9 +10,6 @@ import { type ReactNode, } from 'react'; import { v4 as uuidv4 } from 'uuid'; -import { AppSettings } from '../main/shared/types/App.types'; -import { ConsoleLine, ProcessState } from '../main/shared/types/Process.types'; -import { Profile } from '../main/shared/types/Profile.types'; // ─── Session log helpers ────────────────────────────────────────────────────── diff --git a/src/renderer/components/MainLayout.tsx b/src/renderer/components/MainLayout.tsx index 7bf3b28..ddcc592 100644 --- a/src/renderer/components/MainLayout.tsx +++ b/src/renderer/components/MainLayout.tsx @@ -1,21 +1,21 @@ import React, { useEffect } from 'react'; -import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom'; -import { ProfileSidebar } from './profiles/ProfileSidebar'; +import { LuList, LuScrollText } from 'react-icons/lu'; +import { VscAccount, VscTerminal } from 'react-icons/vsc'; +import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; +import { useApp } from '../AppProvider'; +import { useDevMode } from '../hooks/useDevMode'; +import { useTranslation } from '../i18n/I18nProvider'; +import type { TranslationKey } from '../i18n/TranslationKeys'; import { ConsoleTab } from './console/ConsoleTab'; +import { DeveloperTab } from './developer/DeveloperTab'; +import { FaqPanel } from './faq/FaqPanel'; +import { PanelHeader } from './layout/PanelHeader'; import { ConfigTab } from './profiles/ConfigTab'; -import { ProfileTab } from './profiles/ProfileTab'; import { LogsTab } from './profiles/LogsTab'; +import { ProfileSidebar } from './profiles/ProfileSidebar'; +import { ProfileTab } from './profiles/ProfileTab'; import { SettingsTab } from './settings/SettingsTab'; import { UtilitiesTab } from './utils/UtilitiesTab'; -import { FaqPanel } from './faq/FaqPanel'; -import { DeveloperTab } from './developer/DeveloperTab'; -import { PanelHeader } from './layout/PanelHeader'; -import { useApp } from '../AppProvider'; -import { useDevMode } from '../hooks/useDevMode'; -import { useTranslation } from '../i18n/I18nProvider'; -import type { TranslationKey } from '../i18n/TranslationKeys'; -import { VscTerminal, VscAccount } from 'react-icons/vsc'; -import { LuList, LuScrollText } from 'react-icons/lu'; const SIDE_PANELS = ['settings', 'faq', 'utilities', 'developer'] as const; type SidePanel = (typeof SIDE_PANELS)[number]; diff --git a/src/renderer/components/common/ArgList.tsx b/src/renderer/components/common/ArgList.tsx index fd6c1bf..47b81c5 100644 --- a/src/renderer/components/common/ArgList.tsx +++ b/src/renderer/components/common/ArgList.tsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; -import { Button } from './Button'; +import { useState } from 'react'; import { useInputContextMenu } from '../../hooks/useInputContextMenu'; +import { Button } from './Button'; export interface ArgItem { value: string; diff --git a/src/renderer/components/common/Dialog.tsx b/src/renderer/components/common/Dialog.tsx index fbffe54..381eb4f 100644 --- a/src/renderer/components/common/Dialog.tsx +++ b/src/renderer/components/common/Dialog.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Button } from './Button'; interface Props { diff --git a/src/renderer/components/common/EnvVarList.tsx b/src/renderer/components/common/EnvVarList.tsx index eeea4f0..9a427ad 100644 --- a/src/renderer/components/common/EnvVarList.tsx +++ b/src/renderer/components/common/EnvVarList.tsx @@ -1,8 +1,8 @@ -import React, { useState, useRef } from 'react'; -import { VscTrash, VscAdd } from 'react-icons/vsc'; -import { Toggle } from './Toggle'; -import type { EnvVariable } from '../../../main/shared/types/Profile.types'; +import type { EnvVariable } from '@shared/types/Profile.types'; +import React, { useRef, useState } from 'react'; +import { VscAdd, VscTrash } from 'react-icons/vsc'; import { useInputContextMenu } from '../../hooks/useInputContextMenu'; +import { Toggle } from './Toggle'; interface Props { items: EnvVariable[]; diff --git a/src/renderer/components/common/PropList.tsx b/src/renderer/components/common/PropList.tsx index 65001be..9b1eea4 100644 --- a/src/renderer/components/common/PropList.tsx +++ b/src/renderer/components/common/PropList.tsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; -import { Button } from './Button'; +import { useState } from 'react'; import { useInputContextMenu } from '../../hooks/useInputContextMenu'; +import { Button } from './Button'; export interface PropItem { key: string; diff --git a/src/renderer/components/common/Toggle.tsx b/src/renderer/components/common/Toggle.tsx index ed22c64..0932a73 100644 --- a/src/renderer/components/common/Toggle.tsx +++ b/src/renderer/components/common/Toggle.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - interface Props { checked: boolean; onChange: (v: boolean) => void; diff --git a/src/renderer/components/common/Tooltip.tsx b/src/renderer/components/common/Tooltip.tsx index a0d81f1..2c50f1b 100644 --- a/src/renderer/components/common/Tooltip.tsx +++ b/src/renderer/components/common/Tooltip.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; interface Props { content: React.ReactNode; diff --git a/src/renderer/components/console/ConsoleInput.tsx b/src/renderer/components/console/ConsoleInput.tsx index 40f3cfd..69ab8e2 100644 --- a/src/renderer/components/console/ConsoleInput.tsx +++ b/src/renderer/components/console/ConsoleInput.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useCallback, KeyboardEvent } from 'react'; +import React, { KeyboardEvent, useCallback, useRef } from 'react'; interface Props { running: boolean; diff --git a/src/renderer/components/console/ConsoleOutput.tsx b/src/renderer/components/console/ConsoleOutput.tsx index 3d1af0e..0bed7ab 100644 --- a/src/renderer/components/console/ConsoleOutput.tsx +++ b/src/renderer/components/console/ConsoleOutput.tsx @@ -1,6 +1,6 @@ -import React, { useRef, useEffect, useCallback, useMemo } from 'react'; -import type { ConsoleLine } from '../../../main/shared/types/Process.types'; -import { parseAnsi, hasAnsi, AnsiSpan } from '../../utils/ansi'; +import type { ConsoleLine } from '@shared/types/Process.types'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import { AnsiSpan, hasAnsi, parseAnsi } from '../../utils/ansi'; interface Props { lines: ConsoleLine[]; diff --git a/src/renderer/components/console/ConsoleSearch.tsx b/src/renderer/components/console/ConsoleSearch.tsx index f961bde..e304434 100644 --- a/src/renderer/components/console/ConsoleSearch.tsx +++ b/src/renderer/components/console/ConsoleSearch.tsx @@ -1,5 +1,5 @@ -import React, { useRef, useEffect } from 'react'; -import { VscChevronUp, VscChevronDown, VscClose } from 'react-icons/vsc'; +import { useEffect, useRef } from 'react'; +import { VscChevronDown, VscChevronUp, VscClose } from 'react-icons/vsc'; interface Props { query: string; diff --git a/src/renderer/components/console/ConsoleTab.tsx b/src/renderer/components/console/ConsoleTab.tsx index 51038bf..b8ba429 100644 --- a/src/renderer/components/console/ConsoleTab.tsx +++ b/src/renderer/components/console/ConsoleTab.tsx @@ -1,20 +1,20 @@ -import React, { useRef, useEffect, useState, useCallback, useMemo, KeyboardEvent } from 'react'; -import { useApp } from '../../AppProvider'; -import { useTranslation } from '../../i18n/I18nProvider'; -import { Button } from '../common/Button'; -import { ContextMenu, ContextMenuItem } from '../common/ContextMenu'; -import { useInputContextMenu } from '../../hooks/useInputContextMenu'; +import { ConsoleLine } from '@shared/types/Process.types'; +import { hasJarConfigured } from '@shared/types/Profile.types'; +import React, { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { - VscSearch, - VscChevronUp, VscChevronDown, - VscClose, - VscFolderOpened, + VscChevronUp, VscClearAll, + VscClose, VscCopy, + VscFolderOpened, + VscSearch, } from 'react-icons/vsc'; -import { ConsoleLine } from '../../../main/shared/types/Process.types'; -import { hasJarConfigured } from '../../../main/shared/types/Profile.types'; +import { useApp } from '../../AppProvider'; +import { useInputContextMenu } from '../../hooks/useInputContextMenu'; +import { useTranslation } from '../../i18n/I18nProvider'; +import { Button } from '../common/Button'; +import { ContextMenu, ContextMenuItem } from '../common/ContextMenu'; function formatTimestamp(ts: number): string { const d = new Date(ts); diff --git a/src/renderer/components/console/ConsoleToolbar.tsx b/src/renderer/components/console/ConsoleToolbar.tsx index 7dbffa8..a8037a0 100644 --- a/src/renderer/components/console/ConsoleToolbar.tsx +++ b/src/renderer/components/console/ConsoleToolbar.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { VscSearch } from 'react-icons/vsc'; import { Button } from '../common/Button'; diff --git a/src/renderer/components/developer/DevApiExplorer.tsx b/src/renderer/components/developer/DevApiExplorer.tsx index 1a7f22f..76e041a 100644 --- a/src/renderer/components/developer/DevApiExplorer.tsx +++ b/src/renderer/components/developer/DevApiExplorer.tsx @@ -1,12 +1,12 @@ -import React, { useState, useCallback } from 'react'; -import { VscCheck, VscCopy, VscPlay, VscEdit, VscCode } from 'react-icons/vsc'; -import { routeConfig, REST_API_CONFIG } from '../../../main/shared/config/API.config'; +import { REST_API_CONFIG, routeConfig } from '@shared/config/API.config'; +import { RouteDefinition } from '@shared/types/RestAPI.types'; +import React, { useCallback, useState } from 'react'; +import { VscCheck, VscCode, VscCopy, VscEdit, VscPlay } from 'react-icons/vsc'; import { useApp } from '../../AppProvider'; +import { useInputContextMenu } from '../../hooks/useInputContextMenu'; import { useTranslation } from '../../i18n/I18nProvider'; import { Button } from '../common/Button'; import { ContextMenu, ContextMenuItem } from '../common/ContextMenu'; -import { RouteDefinition } from '../../..//main/shared/types/RestAPI.types'; -import { useInputContextMenu } from '../../hooks/useInputContextMenu'; const METHOD_COLORS: Record = { GET: 'text-accent border-accent/30 bg-accent/10', diff --git a/src/renderer/components/developer/DevAssets.tsx b/src/renderer/components/developer/DevAssets.tsx new file mode 100644 index 0000000..d86089a --- /dev/null +++ b/src/renderer/components/developer/DevAssets.tsx @@ -0,0 +1,168 @@ +import { ALL_LANGUAGES, ENGLISH_STRINGS } from '@shared/config/Language.config'; +import { ALL_THEMES } from '@shared/config/Theme.config'; +import type { LanguageDefinition } from '@shared/types/Language.types'; +import React from 'react'; +import { VscCheck, VscGlobe } from 'react-icons/vsc'; +import { useTheme } from '../../hooks/ThemeProvider'; +import { useTranslation } from '../../i18n/I18nProvider'; + +export function DevAssets() { + const { theme, setTheme } = useTheme(); + const { language, t, setLanguage } = useTranslation(); + + const totalKeys = Object.keys(ENGLISH_STRINGS).length; + + return ( +
+
+ {/* ── Themes ──────────────────────────────────────────────────── */} +
+
+ {ALL_THEMES.map((item) => ( +
+
+ {( + ['accent', 'base-900', 'base-800', 'surface-raised', 'text-primary'] as const + ).map((key) => ( + + ))} +
+
+

{item.name}

+

{item.author}

+
+ + {theme.id === item.id ? ( + + {t('dev.activeTheme')} + + ) : ( + + )} +
+ ))} +
+
+ + {/* ── Languages ───────────────────────────────────────────────── */} +
+
+ {ALL_LANGUAGES.map((item) => { + const translated = Object.keys(item.strings).filter( + (k) => k in ENGLISH_STRINGS + ).length; + const coverage = totalKeys > 0 ? Math.round((translated / totalKeys) * 100) : 100; + return ( +
+ +
+

{item.name}

+

+ {coverage}% ({translated}/{totalKeys}) +

+
+ + {language.id === item.id ? ( + + {t('dev.activeLang')} + + ) : ( + + )} +
+ ); + })} +
+
+ + {/* ── Missing Keys ────────────────────────────────────────────── */} + {language.id !== 'en' && ( +
+ +
+ )} +
+
+ ); +} + +// ─── Reusable pieces ───────────────────────────────────────────────────────── + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+ {children} +
+ ); +} + +function SourceBadge({ label }: { label: string }) { + return ( + + {label} + + ); +} + +function MissingKeysPanel({ + language, + totalKeys, +}: { + language: LanguageDefinition; + totalKeys: number; +}) { + const allKeys = Object.keys(ENGLISH_STRINGS); + const missing = allKeys.filter((k) => !(k in language.strings)); + + if (missing.length === 0) { + return ( +
+ +

All {totalKeys} keys translated

+
+ ); + } + + return ( +
+
+

+ {missing.length} missing key{missing.length !== 1 ? 's' : ''} +

+
+
+ {missing.map((key) => ( +
+ + {key} + +
+ ))} +
+
+ ); +} diff --git a/src/renderer/components/developer/DevDashboard.tsx b/src/renderer/components/developer/DevDashboard.tsx index b0e7209..9a12886 100644 --- a/src/renderer/components/developer/DevDashboard.tsx +++ b/src/renderer/components/developer/DevDashboard.tsx @@ -1,7 +1,7 @@ +import { JRCEnvironment } from '@shared/types/App.types'; import React, { useEffect, useState } from 'react'; +import { VscCircle, VscCircleFilled } from 'react-icons/vsc'; import { useApp } from '../../AppProvider'; -import { VscCircleFilled, VscCircle } from 'react-icons/vsc'; -import { JRCEnvironment } from '../../../main/shared/types/App.types'; declare const __APP_VERSION__: string; diff --git a/src/renderer/components/developer/DevModeGate.tsx b/src/renderer/components/developer/DevModeGate.tsx index 4653176..4b8d17c 100644 --- a/src/renderer/components/developer/DevModeGate.tsx +++ b/src/renderer/components/developer/DevModeGate.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; +import { VscCode } from 'react-icons/vsc'; import { useDevMode } from '../../hooks/useDevMode'; import { Button } from '../common/Button'; -import { VscCode } from 'react-icons/vsc'; export function DevModeGate() { const devEnabled = useDevMode(); diff --git a/src/renderer/components/developer/DevStorage.tsx b/src/renderer/components/developer/DevStorage.tsx index aff3ba0..3104a26 100644 --- a/src/renderer/components/developer/DevStorage.tsx +++ b/src/renderer/components/developer/DevStorage.tsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; +import { VscRefresh, VscTrash } from 'react-icons/vsc'; +import { useApp } from '../../AppProvider'; import { Button } from '../common/Button'; import { Dialog } from '../common/Dialog'; -import { useApp } from '../../AppProvider'; -import { VscRefresh, VscTrash } from 'react-icons/vsc'; interface SessionEntry { key: string; diff --git a/src/renderer/components/developer/DeveloperTab.tsx b/src/renderer/components/developer/DeveloperTab.tsx index d0b0dc7..6aaa16e 100644 --- a/src/renderer/components/developer/DeveloperTab.tsx +++ b/src/renderer/components/developer/DeveloperTab.tsx @@ -1,12 +1,13 @@ import React, { useState } from 'react'; +import { VscBeaker, VscDashboard, VscDatabase, VscPlug, VscSymbolColor } from 'react-icons/vsc'; import { useTranslation } from '../../i18n/I18nProvider'; -import { VscDashboard, VscPlug, VscDatabase, VscBeaker } from 'react-icons/vsc'; -import { DevDashboard } from './DevDashboard'; import { DevApiExplorer } from './DevApiExplorer'; -import { DevStorage } from './DevStorage'; +import { DevAssets } from './DevAssets'; +import { DevDashboard } from './DevDashboard'; import { DevDiagnostics } from './DevDiagnostics'; +import { DevStorage } from './DevStorage'; -type Panel = 'dashboard' | 'api' | 'storage' | 'diagnostics'; +type Panel = 'dashboard' | 'api' | 'storage' | 'diagnostics' | 'assets'; export function DeveloperTab() { const { t } = useTranslation(); @@ -17,6 +18,7 @@ export function DeveloperTab() { { id: 'api', label: t('dev.apiExplorer'), Icon: VscPlug }, { id: 'storage', label: t('dev.storage'), Icon: VscDatabase }, { id: 'diagnostics', label: t('dev.diagnostics'), Icon: VscBeaker }, + { id: 'assets', label: t('dev.assets'), Icon: VscSymbolColor }, ]; return ( @@ -51,6 +53,7 @@ export function DeveloperTab() { {panel === 'api' && } {panel === 'storage' && } {panel === 'diagnostics' && } + {panel === 'assets' && } ); diff --git a/src/renderer/components/faq/FaqPanel.tsx b/src/renderer/components/faq/FaqPanel.tsx index 79acb0f..61c6ab7 100644 --- a/src/renderer/components/faq/FaqPanel.tsx +++ b/src/renderer/components/faq/FaqPanel.tsx @@ -1,37 +1,32 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import type { FaqItem } from '@shared/config/faq/_index'; +import { getFAQ } from '@shared/config/faq/_index'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from '../../i18n/I18nProvider'; -import type { FaqItem } from '../../../main/shared/config/faq/_index'; import { SidebarLayout } from '../layout/SidebarLayout'; -import { FaqTopic, getFAQ } from '../../../main/shared/config/faq/_index'; export function FaqPanel() { - const { t } = useTranslation(); - const [faqTopics, setFaqTopics] = useState(null); + const { language, t } = useTranslation(); const [search, setSearch] = useState(''); const [activeTopic, setActiveTopic] = useState(''); const [expandedIdx, setExpandedIdx] = useState(null); const searchTrimmed = search.trim().toLowerCase(); + const faqTopics = useMemo(() => getFAQ(language.id), [language.id]); + useEffect(() => { - window.api.getActiveLanguage().then((l) => { - const topics = getFAQ(l.id); - setFaqTopics(topics); - if (topics.length > 0) setActiveTopic(topics[0].id); - }); - }, []); + if (faqTopics.length > 0 && !activeTopic) setActiveTopic(faqTopics[0].id); + }, [faqTopics, activeTopic]); const searchResults = useMemo(() => { if (!searchTrimmed) return []; - return ( - faqTopics - ?.flatMap((t) => t.items) - .filter( - (item) => - item.q.toLowerCase().includes(searchTrimmed) || - item.a.toLowerCase().includes(searchTrimmed) - ) ?? [] - ); + return faqTopics + .flatMap((t) => t.items) + .filter( + (item) => + item.q.toLowerCase().includes(searchTrimmed) || + item.a.toLowerCase().includes(searchTrimmed) + ); }, [searchTrimmed, faqTopics]); const activeTopic_ = faqTopics?.find((t) => t.id === activeTopic) ?? faqTopics?.[0]; @@ -43,8 +38,6 @@ export function FaqPanel() { setSearch(''); }; - if (!faqTopics) return null; - return (
diff --git a/src/renderer/components/layout/PanelHeader.tsx b/src/renderer/components/layout/PanelHeader.tsx index 1d3a842..8d12cbf 100644 --- a/src/renderer/components/layout/PanelHeader.tsx +++ b/src/renderer/components/layout/PanelHeader.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from '../../i18n/I18nProvider'; diff --git a/src/renderer/components/layout/SidebarLayout.tsx b/src/renderer/components/layout/SidebarLayout.tsx index 9b4b9f3..9860a58 100644 --- a/src/renderer/components/layout/SidebarLayout.tsx +++ b/src/renderer/components/layout/SidebarLayout.tsx @@ -1,5 +1,5 @@ +import type { SidebarTopic } from '@shared/types/Sidebar.types'; import React from 'react'; -import type { SidebarTopic } from '../../../main/shared/types/Sidebar.types'; export type { SidebarTopic }; diff --git a/src/renderer/components/layout/TitleBar.tsx b/src/renderer/components/layout/TitleBar.tsx index 3edec18..fb6518a 100644 --- a/src/renderer/components/layout/TitleBar.tsx +++ b/src/renderer/components/layout/TitleBar.tsx @@ -1,6 +1,6 @@ import React from 'react'; +import { VscChromeClose, VscChromeMinimize } from 'react-icons/vsc'; import { useApp } from '../../AppProvider'; -import { VscChromeMinimize, VscChromeClose } from 'react-icons/vsc'; export function TitleBar() { const { state } = useApp(); diff --git a/src/renderer/components/profiles/ConfigTab.tsx b/src/renderer/components/profiles/ConfigTab.tsx index 8431e02..45c3ccc 100644 --- a/src/renderer/components/profiles/ConfigTab.tsx +++ b/src/renderer/components/profiles/ConfigTab.tsx @@ -1,12 +1,12 @@ +import { Profile } from '@shared/types/Profile.types'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Profile } from '../../../main/shared/types/Profile.types'; import { useApp } from '../../AppProvider'; +import { useTranslation } from '../../i18n/I18nProvider'; import { ArgList } from '../common/ArgList'; import { Button } from '../common/Button'; import { Dialog } from '../common/Dialog'; import { EnvVarList } from '../common/EnvVarList'; import { PropList } from '../common/PropList'; -import { useTranslation } from '../../i18n/I18nProvider'; import { FilesSection } from './FilesSection'; import { GeneralSection } from './GeneralSection'; @@ -132,15 +132,7 @@ export function ConfigTab() {
- {section === 'general' && ( - - )} + {section === 'general' && } {section === 'files' && } {section === 'jvm' && ( diff --git a/src/renderer/components/profiles/FilesSection.tsx b/src/renderer/components/profiles/FilesSection.tsx index 5056f54..1441b99 100644 --- a/src/renderer/components/profiles/FilesSection.tsx +++ b/src/renderer/components/profiles/FilesSection.tsx @@ -1,9 +1,8 @@ -import React from 'react'; -import { Input } from '../common/Input'; -import { Profile } from '../../../main/shared/types/Profile.types'; +import { Profile } from '@shared/types/Profile.types'; +import { useInputContextMenu } from '../../hooks/useInputContextMenu'; import { useTranslation } from '../../i18n/I18nProvider'; +import { Input } from '../common/Input'; import { JarSelector } from './jar/JarSelector'; -import { useInputContextMenu } from '../../hooks/useInputContextMenu'; interface Props { draft: Profile; diff --git a/src/renderer/components/profiles/GeneralSection.tsx b/src/renderer/components/profiles/GeneralSection.tsx index ba0577a..6f4cfbd 100644 --- a/src/renderer/components/profiles/GeneralSection.tsx +++ b/src/renderer/components/profiles/GeneralSection.tsx @@ -1,20 +1,13 @@ -import React from 'react'; -import { Toggle } from '../common/Toggle'; -import { Profile } from '../../../main/shared/types/Profile.types'; +import { Profile } from '@shared/types/Profile.types'; import { useTranslation } from '../../i18n/I18nProvider'; +import { Toggle } from '../common/Toggle'; export function GeneralSection({ draft, update, - running, - color, - onRestart, }: { draft: Profile; update: (p: Partial) => void; - running: boolean; - color: string; - onRestart: () => void; }) { const { t } = useTranslation(); @@ -90,20 +83,6 @@ export function GeneralSection({ />
- - {running && ( -
-

- {t('config.process')} -

- -
- )} ); } diff --git a/src/renderer/components/profiles/LogsTab.tsx b/src/renderer/components/profiles/LogsTab.tsx index 142e870..489622d 100644 --- a/src/renderer/components/profiles/LogsTab.tsx +++ b/src/renderer/components/profiles/LogsTab.tsx @@ -1,10 +1,10 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { VscCopy, VscFolderOpened, VscRefresh, VscTrash } from 'react-icons/vsc'; import { useApp } from '../../AppProvider'; import { useTranslation } from '../../i18n/I18nProvider'; import { Button } from '../common/Button'; -import { Dialog } from '../common/Dialog'; import { ContextMenu } from '../common/ContextMenu'; -import { VscTrash, VscRefresh, VscFolderOpened, VscCopy } from 'react-icons/vsc'; +import { Dialog } from '../common/Dialog'; interface LogFileInfo { filename: string; diff --git a/src/renderer/components/profiles/ProfileItem.tsx b/src/renderer/components/profiles/ProfileItem.tsx index 0cdc4d1..280c8e3 100644 --- a/src/renderer/components/profiles/ProfileItem.tsx +++ b/src/renderer/components/profiles/ProfileItem.tsx @@ -1,6 +1,6 @@ +import { Profile } from '@shared/types/Profile.types'; import React from 'react'; import { PROFILE_COLORS } from '../../AppProvider'; -import { Profile } from '../../../main/shared/types/Profile.types'; interface Props { profile: Profile; diff --git a/src/renderer/components/profiles/ProfileSidebar.tsx b/src/renderer/components/profiles/ProfileSidebar.tsx index f6d094a..c6aba8d 100644 --- a/src/renderer/components/profiles/ProfileSidebar.tsx +++ b/src/renderer/components/profiles/ProfileSidebar.tsx @@ -1,26 +1,26 @@ -import React, { useState, useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { hasJarConfigured, Profile } from '@shared/types/Profile.types'; import { Reorder } from 'framer-motion'; +import React, { useCallback, useState } from 'react'; import { - VscPlay, - VscDebugStop, + VscAdd, VscCheck, VscClearAll, - VscTrash, - VscSettings, + VscCode, + VscDebugStop, + VscLayout, + VscPlay, VscQuestion, + VscSettings, VscTools, - VscAdd, - VscLayout, - VscCode, + VscTrash, } from 'react-icons/vsc'; -import { useApp, PROFILE_COLORS } from '../../AppProvider'; +import { useNavigate } from 'react-router-dom'; +import { PROFILE_COLORS, useApp } from '../../AppProvider'; import { useDevMode } from '../../hooks/useDevMode'; import { useTranslation } from '../../i18n/I18nProvider'; -import { Dialog } from '../common/Dialog'; import { ContextMenu, ContextMenuItem } from '../common/ContextMenu'; +import { Dialog } from '../common/Dialog'; import { TemplateModal } from './TemplateModal'; -import { hasJarConfigured, Profile } from '../../../main/shared/types/Profile.types'; interface Props { activeSidePanel: string | null; diff --git a/src/renderer/components/profiles/ProfileTab.tsx b/src/renderer/components/profiles/ProfileTab.tsx index 31783f8..7d671ac 100644 --- a/src/renderer/components/profiles/ProfileTab.tsx +++ b/src/renderer/components/profiles/ProfileTab.tsx @@ -1,10 +1,10 @@ -import React, { useState, useEffect } from 'react'; -import { useApp, PROFILE_COLORS } from '../../AppProvider'; +import { Profile } from '@shared/types/Profile.types'; +import React, { useEffect, useState } from 'react'; +import { PROFILE_COLORS, useApp } from '../../AppProvider'; import { useTranslation } from '../../i18n/I18nProvider'; import { Button } from '../common/Button'; -import { Input } from '../common/Input'; import { Dialog } from '../common/Dialog'; -import { Profile } from '../../../main/shared/types/Profile.types'; +import { Input } from '../common/Input'; export function ProfileTab() { const { activeProfile, saveProfile, deleteProfile } = useApp(); diff --git a/src/renderer/components/profiles/TemplateModal.tsx b/src/renderer/components/profiles/TemplateModal.tsx index a2856d7..9d305b7 100644 --- a/src/renderer/components/profiles/TemplateModal.tsx +++ b/src/renderer/components/profiles/TemplateModal.tsx @@ -1,11 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import { Modal } from '../common/Modal'; -import { Button } from '../common/Button'; +import { ProfileTemplate } from '@shared/types/GitHub.types'; +import React, { useEffect, useState } from 'react'; +import { LuShield } from 'react-icons/lu'; +import { VscAdd, VscPackage, VscRefresh, VscTag } from 'react-icons/vsc'; import { useApp } from '../../AppProvider'; import { useTranslation } from '../../i18n/I18nProvider'; -import { VscPackage, VscTag, VscRefresh, VscAdd } from 'react-icons/vsc'; -import { LuShield } from 'react-icons/lu'; -import { ProfileTemplate } from '../../../main/shared/types/GitHub.types'; +import { Button } from '../common/Button'; +import { Modal } from '../common/Modal'; const APP_TEMPLATE_VERSION = 1; diff --git a/src/renderer/components/profiles/jar/DynamicJarConfig.tsx b/src/renderer/components/profiles/jar/DynamicJarConfig.tsx index 46bc93f..b7a3cba 100644 --- a/src/renderer/components/profiles/jar/DynamicJarConfig.tsx +++ b/src/renderer/components/profiles/jar/DynamicJarConfig.tsx @@ -1,13 +1,12 @@ -import React from 'react'; -import { Input } from '../../common/Input'; -import { FolderBtn } from './FolderBtn'; -import { ResolutionPreview } from './ResolutionPreview'; +import { JAR_RESOLUTION_STRATEGIES } from '@shared/config/JarResolution.config'; +import type { JarResolutionConfig } from '@shared/types/JarResolution.types'; +import { useInputContextMenu } from '../../../hooks/useInputContextMenu'; import { useJarResolutionPreview } from '../../../hooks/useJarResolutionPreview'; -import { JAR_RESOLUTION_STRATEGIES } from '../../../../main/shared/config/JarResolution.config'; -import type { JarResolutionConfig } from '../../../../main/shared/types/JarResolution.types'; import { useTranslation } from '../../../i18n/I18nProvider'; import type { TranslationKey } from '../../../i18n/TranslationKeys'; -import { useInputContextMenu } from '../../../hooks/useInputContextMenu'; +import { Input } from '../../common/Input'; +import { FolderBtn } from './FolderBtn'; +import { ResolutionPreview } from './ResolutionPreview'; const STRATEGY_KEYS: Record = { 'highest-version': { diff --git a/src/renderer/components/profiles/jar/FolderBtn.tsx b/src/renderer/components/profiles/jar/FolderBtn.tsx index 80889eb..14e005d 100644 --- a/src/renderer/components/profiles/jar/FolderBtn.tsx +++ b/src/renderer/components/profiles/jar/FolderBtn.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - interface Props { onClick: () => void; } diff --git a/src/renderer/components/profiles/jar/JarSelector.tsx b/src/renderer/components/profiles/jar/JarSelector.tsx index df2b32e..97f9dbc 100644 --- a/src/renderer/components/profiles/jar/JarSelector.tsx +++ b/src/renderer/components/profiles/jar/JarSelector.tsx @@ -1,9 +1,8 @@ -import React from 'react'; -import { StaticJarPicker } from './StaticJarPicker'; -import { DynamicJarConfig } from './DynamicJarConfig'; -import { DEFAULT_JAR_RESOLUTION } from '../../../../main/shared/config/JarResolution.config'; -import type { JarResolutionConfig } from '../../../../main/shared/types/JarResolution.types'; +import { DEFAULT_JAR_RESOLUTION } from '@shared/config/JarResolution.config'; +import type { JarResolutionConfig } from '@shared/types/JarResolution.types'; import { useTranslation } from '../../../i18n/I18nProvider'; +import { DynamicJarConfig } from './DynamicJarConfig'; +import { StaticJarPicker } from './StaticJarPicker'; interface Props { jarPath: string; diff --git a/src/renderer/components/profiles/jar/ResolutionPreview.tsx b/src/renderer/components/profiles/jar/ResolutionPreview.tsx index bb000d7..321337e 100644 --- a/src/renderer/components/profiles/jar/ResolutionPreview.tsx +++ b/src/renderer/components/profiles/jar/ResolutionPreview.tsx @@ -1,6 +1,5 @@ -import React from 'react'; -import { VscCheck, VscWarning, VscSync } from 'react-icons/vsc'; -import type { JarResolutionResult } from '../../../../main/shared/types/JarResolution.types'; +import type { JarResolutionResult } from '@shared/types/JarResolution.types'; +import { VscCheck, VscSync, VscWarning } from 'react-icons/vsc'; import { useTranslation } from '../../../i18n/I18nProvider'; interface Props { diff --git a/src/renderer/components/profiles/jar/StaticJarPicker.tsx b/src/renderer/components/profiles/jar/StaticJarPicker.tsx index e079780..070112e 100644 --- a/src/renderer/components/profiles/jar/StaticJarPicker.tsx +++ b/src/renderer/components/profiles/jar/StaticJarPicker.tsx @@ -1,8 +1,7 @@ -import React from 'react'; +import { useInputContextMenu } from '../../../hooks/useInputContextMenu'; +import { useTranslation } from '../../../i18n/I18nProvider'; import { Input } from '../../common/Input'; import { FolderBtn } from './FolderBtn'; -import { useTranslation } from '../../../i18n/I18nProvider'; -import { useInputContextMenu } from '../../../hooks/useInputContextMenu'; interface Props { jarPath: string; diff --git a/src/renderer/components/settings/SettingsTab.tsx b/src/renderer/components/settings/SettingsTab.tsx index 3b1fa84..c0d4170 100644 --- a/src/renderer/components/settings/SettingsTab.tsx +++ b/src/renderer/components/settings/SettingsTab.tsx @@ -1,17 +1,16 @@ -import React, { useState, useEffect, useMemo, useRef } from 'react'; +import { SETTINGS_TOPICS } from '@shared/config/Settings.config'; +import { AppSettings } from '@shared/types/App.types'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useApp } from '../../AppProvider'; import { useTranslation } from '../../i18n/I18nProvider'; +import type { TranslationKey } from '../../i18n/TranslationKeys'; import { Button } from '../common/Button'; import { SidebarLayout } from '../layout/SidebarLayout'; -import { SETTINGS_TOPICS } from '../../../main/shared/config/Settings.config'; -import type { TranslationKey } from '../../i18n/TranslationKeys'; -import { GeneralSection } from './sections/GeneralSection'; -import { ConsoleSection } from './sections/ConsoleSection'; -import { AppearanceSection } from './sections/AppearanceSection'; import { AdvancedSection } from './sections/AdvancedSection'; -import { UpdatesSection } from './sections/UpdatesSection'; -import { AboutSection } from './sections/AboutSection'; -import { AppSettings } from '../../../main/shared/types/App.types'; +import { AppearanceSection } from './sections/AppearanceSection'; +import { ConsoleSection } from './sections/ConsoleSection'; +import { GeneralSection } from './sections/GeneralSection'; +import { AboutSection } from './sections/about/AboutSection'; type SetFn = (patch: Partial) => void; @@ -75,9 +74,6 @@ export function SettingsTab() { setTimeout(() => setSaved(false), 2000); }; - const isStandaloneSection = - activeTopic === 'appearance' || activeTopic === 'updates' || activeTopic === 'about'; - return (
{(isDirty || saved) && ( @@ -101,7 +97,6 @@ export function SettingsTab() { {activeTopic === 'console' && } {activeTopic === 'appearance' && } {activeTopic === 'advanced' && } - {activeTopic === 'updates' && } {activeTopic === 'about' && }
diff --git a/src/renderer/components/settings/sections/AdvancedSection.tsx b/src/renderer/components/settings/sections/AdvancedSection.tsx index 67cc634..d061222 100644 --- a/src/renderer/components/settings/sections/AdvancedSection.tsx +++ b/src/renderer/components/settings/sections/AdvancedSection.tsx @@ -1,9 +1,8 @@ -import React from 'react'; +import { REST_API_CONFIG } from '@shared/config/API.config'; +import { AppSettings } from '@shared/types/App.types'; import { useTranslation } from '../../../i18n/I18nProvider'; import { Toggle } from '../../common/Toggle'; -import { Section, Row, NumInput } from '../SettingsRow'; -import { REST_API_CONFIG } from '../../../../main/shared/config/API.config'; -import { AppSettings } from '../../../../main/shared/types/App.types'; +import { NumInput, Row, Section } from '../SettingsRow'; interface Props { draft: AppSettings; diff --git a/src/renderer/components/settings/sections/AppearanceSection.tsx b/src/renderer/components/settings/sections/AppearanceSection.tsx index 1d49d8c..5b8864e 100644 --- a/src/renderer/components/settings/sections/AppearanceSection.tsx +++ b/src/renderer/components/settings/sections/AppearanceSection.tsx @@ -1,490 +1,144 @@ -import { useState, useEffect } from 'react'; -import { Section } from '../SettingsRow'; +import { ALL_LANGUAGES } from '@shared/config/Language.config'; +import { ALL_THEMES } from '@shared/config/Theme.config'; +import type { ThemeColors } from '@shared/types/Theme.types'; +import { useMemo, useState } from 'react'; +import { VscCheck, VscSearch } from 'react-icons/vsc'; import { useTheme } from '../../../hooks/ThemeProvider'; import { useTranslation } from '../../../i18n/I18nProvider'; -import { Tooltip } from '../../common/Tooltip'; -import { VscSync, VscCheck, VscBeaker, VscArrowSwap } from 'react-icons/vsc'; -import type { - ThemeDefinition, - ThemePreview, - ThemePreviewColors, -} from '../../../../main/shared/types/Theme.types'; -import type { - LanguageDefinition, - LanguagePreview, -} from '../../../../main/shared/types/Language.types'; -import { ENGLISH } from '../../../../main/shared/config/DefaultLanguage.config'; -import { BUILTIN_THEME } from '../../../../main/shared/config/Theme.config'; -import type { JRCEnvironment } from '../../../../main/shared/types/App.types'; -type FetchState = 'idle' | 'loading' | 'done' | 'error'; - -const THEME_SESSION_KEY = 'jrc:theme-previews'; -const LANG_SESSION_KEY = 'jrc:lang-previews'; - -function readSession(key: string): T[] { - try { - return JSON.parse(sessionStorage.getItem(key) ?? '[]'); - } catch { - return []; - } -} - -function writeSession(key: string, items: T[]): void { - sessionStorage.setItem(key, JSON.stringify(items)); -} - -// ─── List item types ───────────────────────────────────────────────────────── - -interface ThemeItem { - id: string; - name: string; - previewColors: ThemePreviewColors; - hasRemote: boolean; - hasLocal: boolean; - filename?: string; - fullTheme?: ThemeDefinition; -} - -interface LangItem { - id: string; - name: string; - hasRemote: boolean; - hasLocal: boolean; - filename?: string; - fullLang?: LanguageDefinition; -} - -// ─── Component ─────────────────────────────────────────────────────────────── +const PREVIEW_KEYS: (keyof ThemeColors)[] = [ + 'accent', + 'base-900', + 'surface-raised', + 'text-primary', +]; export function AppearanceSection() { const { theme, setTheme } = useTheme(); const { language, t, setLanguage } = useTranslation(); - const [themePreviews, setThemePreviews] = useState(() => - readSession(THEME_SESSION_KEY) - ); - const [langPreviews, setLangPreviews] = useState(() => - readSession(LANG_SESSION_KEY) - ); - - const [themesFetch, setThemesFetch] = useState(() => - readSession(THEME_SESSION_KEY).length > 0 ? 'done' : 'idle' - ); - const [langsFetch, setLangsFetch] = useState(() => - readSession(LANG_SESSION_KEY).length > 0 ? 'done' : 'idle' - ); - - const [loadingId, setLoadingId] = useState(null); - - const [isDev, setIsDev] = useState(false); - const [devThemes, setDevThemes] = useState([]); - const [devLangs, setDevLangs] = useState([]); - const [devSynced, setDevSynced] = useState(false); - - // ─── Effects ───────────────────────────────────────────────────────────── - - useEffect(() => { - window.env.get().then((env: JRCEnvironment) => setIsDev(env.type === 'dev')); - }, []); - - // Auto-fetch previews on mount if not cached in session - useEffect(() => { - if (readSession(THEME_SESSION_KEY).length === 0) fetchThemePreviews(); - if (readSession(LANG_SESSION_KEY).length === 0) fetchLangPreviews(); - }, []); - - // Auto-load dev assets when dev mode detected - useEffect(() => { - if (isDev) syncDevAssets(); - }, [isDev]); - - // ─── Fetch handlers ────────────────────────────────────────────────────── - - const fetchThemePreviews = async () => { - setThemesFetch('loading'); - const res = await window.api.fetchThemePreviews(); - if (res.ok && res.themes) { - writeSession(THEME_SESSION_KEY, res.themes); - setThemePreviews(res.themes); - setThemesFetch('done'); - } else { - setThemesFetch('error'); - } - }; - - const fetchLangPreviews = async () => { - setLangsFetch('loading'); - const res = await window.api.fetchLanguagePreviews(); - if (res.ok && res.languages) { - writeSession(LANG_SESSION_KEY, res.languages); - setLangPreviews(res.languages); - setLangsFetch('done'); - } else { - setLangsFetch('error'); - } - }; - - const refreshThemes = () => { - sessionStorage.removeItem(THEME_SESSION_KEY); - setThemePreviews([]); - fetchThemePreviews(); - }; - - const refreshLangs = () => { - sessionStorage.removeItem(LANG_SESSION_KEY); - setLangPreviews([]); - fetchLangPreviews(); - }; - - const syncDevAssets = async () => { - const assets = await window.api.loadDevAssets(); - setDevThemes(assets.themes); - setDevLangs(assets.languages); - }; - - const handleDevSync = async () => { - await syncDevAssets(); - setDevSynced(true); - setTimeout(() => setDevSynced(false), 2000); - }; - - // ─── Selection handlers ────────────────────────────────────────────────── - - const selectTheme = async (item: ThemeItem) => { - if (item.id === theme.id || loadingId) return; - if (item.fullTheme) { - setTheme(item.fullTheme); - return; - } - if (!item.filename) return; - setLoadingId(item.id); - const res = await window.api.fetchThemeByFile(item.filename); - setLoadingId(null); - if (res.ok && res.theme) setTheme(res.theme); - }; - - const selectLang = async (item: LangItem) => { - if (item.id === language.id || loadingId) return; - if (item.fullLang) { - setLanguage(item.fullLang); - return; - } - if (!item.filename) return; - setLoadingId(item.id); - const res = await window.api.fetchLanguageByFile(item.filename); - setLoadingId(null); - if (res.ok && res.language) setLanguage(res.language); - }; - - // ─── Build lists ───────────────────────────────────────────────────────── + const [themeSearch, setThemeSearch] = useState(''); + const [langSearch, setLangSearch] = useState(''); - const themeItems = buildThemeList(theme, themePreviews, isDev ? devThemes : []); - const langItems = buildLangList(language, langPreviews, isDev ? devLangs : []); + const filteredThemes = useMemo(() => { + if (!themeSearch) return ALL_THEMES; + const q = themeSearch.toLowerCase(); + return ALL_THEMES.filter( + (t) => t.name.toLowerCase().includes(q) || t.id.toLowerCase().includes(q) + ); + }, [themeSearch]); - // ─── Render ────────────────────────────────────────────────────────────── + const filteredLangs = useMemo(() => { + if (!langSearch) return ALL_LANGUAGES; + const q = langSearch.toLowerCase(); + return ALL_LANGUAGES.filter( + (l) => l.name.toLowerCase().includes(q) || l.id.toLowerCase().includes(q) + ); + }, [langSearch]); return ( - <> -
-
-

{t('settings.themeHint')}

- -
- {themesFetch === 'error' && ( -

{t('appearance.fetchThemesFailed')}

- )} -
- {themeItems.map((item) => ( - - ))} - {themesFetch === 'loading' && themePreviews.length === 0 && ( -

- {t('general.loading')} +

+ {/* ── Themes panel ─────────────────────────────────────────────────── */} +
+

+ {t('settings.theme')} +

+ +
+ {filteredThemes.length === 0 ? ( +

+ {t('appearance.noThemesFound')}

+ ) : ( + filteredThemes.map((item) => ( + + )) )}
-
- -
-
-

{t('settings.languageHint')}

- -
- {langsFetch === 'error' && ( -

{t('appearance.fetchLangsFailed')}

- )} -
- {langItems.map((item) => ( - - ))} - {langsFetch === 'loading' && langPreviews.length === 0 && ( -

- {t('general.loading')} +

+ + {/* ── Languages panel ──────────────────────────────────────────────── */} +
+

+ {t('settings.language')} +

+ +
+ {filteredLangs.length === 0 ? ( +

+ {t('appearance.noLanguagesFound')}

+ ) : ( + filteredLangs.map((item) => ( + + )) )}
-
- - {isDev && ( -
-
-
-

{t('appearance.syncTitle')}

-

{t('appearance.syncHint')}

-
- -
-
- - {t('appearance.localOnly')} - - - {t('appearance.localOverride')} - -
-
- )} - + + ); } -// ─── Dev badge component ────────────────────────────────────────────────────── - -function DevBadge({ hasLocal, hasRemote }: { hasLocal: boolean; hasRemote: boolean }) { - const { t } = useTranslation(); - if (hasLocal && hasRemote) { - return ( - - - - - - ); - } - if (hasLocal && !hasRemote) { - return ( - - - - - - ); - } - return null; -} - -// ─── List builders ──────────────────────────────────────────────────────────── - -function previewColorsFromTheme(th: ThemeDefinition): ThemePreviewColors { - return { - accent: th.colors.accent, - 'base-900': th.colors['base-900'], - 'surface-raised': th.colors['surface-raised'], - 'text-primary': th.colors['text-primary'], - }; -} - -function buildThemeList( - active: ThemeDefinition, - remotePreviews: ThemePreview[], - devThemes: ThemeDefinition[] -): ThemeItem[] { - const items = new Map(); - - // Active theme always first - items.set(active.id, { - id: active.id, - name: active.name, - previewColors: previewColorsFromTheme(active), - hasRemote: false, - hasLocal: false, - fullTheme: active, - }); - - // Builtin theme always available (like English for languages) - if (!items.has(BUILTIN_THEME.id)) { - items.set(BUILTIN_THEME.id, { - id: BUILTIN_THEME.id, - name: BUILTIN_THEME.name, - previewColors: previewColorsFromTheme(BUILTIN_THEME), - hasRemote: false, - hasLocal: false, - fullTheme: BUILTIN_THEME, - }); - } - - // Remote previews - for (const p of remotePreviews) { - const existing = items.get(p.id); - if (existing) { - existing.hasRemote = true; - existing.filename = p.filename; - } else { - items.set(p.id, { - id: p.id, - name: p.name, - previewColors: p.previewColors, - hasRemote: true, - hasLocal: false, - filename: p.filename, - }); - } - } - - // Dev-local themes - for (const dt of devThemes) { - const existing = items.get(dt.id); - if (existing) { - existing.hasLocal = true; - existing.fullTheme = dt; - existing.previewColors = previewColorsFromTheme(dt); - } else { - items.set(dt.id, { - id: dt.id, - name: dt.name, - previewColors: previewColorsFromTheme(dt), - hasRemote: false, - hasLocal: true, - fullTheme: dt, - }); - } - } - - return Array.from(items.values()); -} - -function buildLangList( - active: LanguageDefinition, - remotePreviews: LanguagePreview[], - devLangs: LanguageDefinition[] -): LangItem[] { - const items = new Map(); - - // Active language always first - items.set(active.id, { - id: active.id, - name: active.name, - hasRemote: false, - hasLocal: false, - fullLang: active, - }); - - // English always available - if (!items.has(ENGLISH.id)) { - items.set(ENGLISH.id, { - id: ENGLISH.id, - name: ENGLISH.name, - hasRemote: false, - hasLocal: false, - fullLang: ENGLISH, - }); - } - - // Remote previews - for (const p of remotePreviews) { - const existing = items.get(p.id); - if (existing) { - existing.hasRemote = true; - existing.filename = p.filename; - } else { - items.set(p.id, { - id: p.id, - name: p.name, - hasRemote: true, - hasLocal: false, - filename: p.filename, - }); - } - } - - // Dev-local languages - for (const dl of devLangs) { - const existing = items.get(dl.id); - if (existing) { - existing.hasLocal = true; - existing.fullLang = dl; - } else { - items.set(dl.id, { - id: dl.id, - name: dl.name, - hasRemote: false, - hasLocal: true, - fullLang: dl, - }); - } - } - - return Array.from(items.values()); +function SearchInput({ + value, + onChange, + placeholder, +}: { + value: string; + onChange: (v: string) => void; + placeholder: string; +}) { + return ( +
+ + onChange(e.target.value)} + placeholder={placeholder} + className="w-full bg-base-900 border border-surface-border rounded-md pl-7 pr-3 py-1.5 text-xs text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:border-accent/40 transition-colors" + /> +
+ ); } diff --git a/src/renderer/components/settings/sections/ConsoleSection.tsx b/src/renderer/components/settings/sections/ConsoleSection.tsx index 80997ba..afc807c 100644 --- a/src/renderer/components/settings/sections/ConsoleSection.tsx +++ b/src/renderer/components/settings/sections/ConsoleSection.tsx @@ -1,8 +1,7 @@ -import React from 'react'; -import { Toggle } from '../../common/Toggle'; -import { Section, Row, NumInput } from '../SettingsRow'; +import { AppSettings } from '@shared/types/App.types'; import { useTranslation } from '../../../i18n/I18nProvider'; -import { AppSettings } from '../../../../main/shared/types/App.types'; +import { Toggle } from '../../common/Toggle'; +import { NumInput, Row, Section } from '../SettingsRow'; interface Props { draft: AppSettings; diff --git a/src/renderer/components/settings/sections/GeneralSection.tsx b/src/renderer/components/settings/sections/GeneralSection.tsx index 797ccb1..b4dc6e6 100644 --- a/src/renderer/components/settings/sections/GeneralSection.tsx +++ b/src/renderer/components/settings/sections/GeneralSection.tsx @@ -1,8 +1,7 @@ -import React from 'react'; -import { Toggle } from '../../common/Toggle'; -import { Section, Row } from '../SettingsRow'; +import { AppSettings } from '@shared/types/App.types'; import { useTranslation } from '../../../i18n/I18nProvider'; -import { AppSettings } from '../../../../main/shared/types/App.types'; +import { Toggle } from '../../common/Toggle'; +import { Row, Section } from '../SettingsRow'; interface Props { draft: AppSettings; diff --git a/src/renderer/components/settings/sections/UpdatesSection.tsx b/src/renderer/components/settings/sections/UpdatesSection.tsx deleted file mode 100644 index 11956d8..0000000 --- a/src/renderer/components/settings/sections/UpdatesSection.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import { useTranslation } from '../../../i18n/I18nProvider'; -import { useUpdateRegistry } from '../../../hooks/useUpdateRegistry'; -import type { - UpdateStatus, - UpdateCheckResult, -} from '../../../../main/shared/types/UpdateCenter.types'; -import { Button } from '../../common/Button'; -import { Section } from '../SettingsRow'; -import { VscSync, VscCheck, VscWarning, VscCircleSlash, VscCloudDownload } from 'react-icons/vsc'; - -interface ItemState { - status: UpdateStatus; - result?: UpdateCheckResult; - error?: string; -} - -export function UpdatesSection() { - const { t } = useTranslation(); - const registry = useUpdateRegistry(); - const [items, setItems] = useState>(() => - Object.fromEntries(registry.map((u) => [u.id, { status: 'idle' as UpdateStatus }])) - ); - const [globalChecking, setGlobalChecking] = useState(false); - const [globalUpdating, setGlobalUpdating] = useState(false); - - const updateItem = (id: string, patch: Partial) => { - setItems((prev) => ({ ...prev, [id]: { ...prev[id], ...patch } })); - }; - - const checkOne = useCallback( - async (id: string) => { - const updatable = registry.find((u) => u.id === id); - if (!updatable) return; - updateItem(id, { status: 'checking' }); - try { - const result = await updatable.check(); - updateItem(id, { - status: result.hasUpdate ? 'update-available' : 'up-to-date', - result, - error: result.error, - }); - } catch (e) { - updateItem(id, { status: 'error', error: String(e) }); - } - }, - [registry] - ); - - const applyOne = useCallback( - async (id: string) => { - const updatable = registry.find((u) => u.id === id); - if (!updatable) return; - updateItem(id, { status: 'updating' }); - try { - const res = await updatable.apply(); - updateItem(id, { status: res.ok ? 'done' : 'error', error: res.error }); - } catch (e) { - updateItem(id, { status: 'error', error: String(e) }); - } - }, - [registry] - ); - - const checkAll = useCallback(async () => { - setGlobalChecking(true); - for (const u of registry) await checkOne(u.id); - setGlobalChecking(false); - }, [registry, checkOne]); - - const updateAll = useCallback(async () => { - setGlobalUpdating(true); - for (const u of registry) { - if (items[u.id]?.status === 'update-available') await applyOne(u.id); - } - setGlobalUpdating(false); - }, [registry, items, applyOne]); - - const hasAnyUpdate = Object.values(items).some((s) => s.status === 'update-available'); - const allChecked = Object.values(items).every( - (s) => s.status !== 'idle' && s.status !== 'checking' - ); - - return ( -
-
-

{t('settings.updatesHint')}

-
- - {hasAnyUpdate && ( - - )} -
-
- -
- {registry.map((updatable) => { - const state = items[updatable.id] ?? { status: 'idle' }; - return ( - checkOne(updatable.id)} - onApply={() => applyOne(updatable.id)} - /> - ); - })} -
- - {allChecked && !hasAnyUpdate && ( -
- -

{t('settings.allUpToDate')}

-
- )} -
- ); -} - -function UpdateItem({ - label, - description, - state, - onCheck, - onApply, -}: { - label: string; - description: string; - state: ItemState; - onCheck: () => void; - onApply: () => void; -}) { - const { t } = useTranslation(); - const { status, result, error } = state; - - const StatusIcon = { - idle: () => , - checking: () => , - 'up-to-date': () => , - 'update-available': () => , - updating: () => , - done: () => , - error: () => , - }[status]; - - return ( -
-
- -
-
-

{label}

-

- {status === 'up-to-date' && result && `v${result.currentVersion} -- latest`} - {status === 'update-available' && - result && - `v${result.currentVersion} -> v${result.remoteVersion}`} - {status === 'done' && t('settings.updatedSuccess')} - {status === 'error' && (error ?? t('settings.checkFailed'))} - {status === 'idle' && description} - {status === 'checking' && t('settings.checking')} - {status === 'updating' && t('settings.applyingUpdate')} -

-
-
- {(status === 'idle' || status === 'error' || status === 'up-to-date') && ( - - )} - {status === 'update-available' && ( - - )} -
-
- ); -} diff --git a/src/renderer/components/settings/sections/AboutSection.tsx b/src/renderer/components/settings/sections/about/AboutSection.tsx similarity index 77% rename from src/renderer/components/settings/sections/AboutSection.tsx rename to src/renderer/components/settings/sections/about/AboutSection.tsx index 5527c6f..003760b 100644 --- a/src/renderer/components/settings/sections/AboutSection.tsx +++ b/src/renderer/components/settings/sections/about/AboutSection.tsx @@ -1,10 +1,9 @@ -import React from 'react'; -import { useTranslation } from '../../../i18n/I18nProvider'; -import { Section, Row } from '../SettingsRow'; -import { Tooltip } from '../../common/Tooltip'; -import { VersionChecker } from '../VersionChecker'; import { VscFolderOpened } from 'react-icons/vsc'; -import { version } from '../../../../../package.json'; +import { version } from '../../../../../../package.json'; +import { useTranslation } from '../../../../i18n/I18nProvider'; +import { Tooltip } from '../../../common/Tooltip'; +import { Row, Section } from '../../SettingsRow'; +import { VersionChecker } from './VersionChecker'; export function AboutSection() { const { t } = useTranslation(); diff --git a/src/renderer/components/settings/PublisherBadge.tsx b/src/renderer/components/settings/sections/about/PublisherBadge.tsx similarity index 93% rename from src/renderer/components/settings/PublisherBadge.tsx rename to src/renderer/components/settings/sections/about/PublisherBadge.tsx index fdc972c..2d2944d 100644 --- a/src/renderer/components/settings/PublisherBadge.tsx +++ b/src/renderer/components/settings/sections/about/PublisherBadge.tsx @@ -1,7 +1,7 @@ +import { getPublisherTrust, TrustLevel } from '@shared/config/GitHub.config'; import React from 'react'; -import { VscVerified, VscWarning } from 'react-icons/vsc'; import { LuBot } from 'react-icons/lu'; -import { getPublisherTrust, TrustLevel } from '../../../main/shared/config/GitHub.config'; +import { VscVerified, VscWarning } from 'react-icons/vsc'; interface Props { login: string; diff --git a/src/renderer/components/settings/ReleaseModal.tsx b/src/renderer/components/settings/sections/about/ReleaseModal.tsx similarity index 97% rename from src/renderer/components/settings/ReleaseModal.tsx rename to src/renderer/components/settings/sections/about/ReleaseModal.tsx index b5e781e..32e8c23 100644 --- a/src/renderer/components/settings/ReleaseModal.tsx +++ b/src/renderer/components/settings/sections/about/ReleaseModal.tsx @@ -1,20 +1,20 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { useTranslation } from '../../i18n/I18nProvider'; -import { Modal } from '../common/Modal'; -import { Button } from '../common/Button'; +import { GitHubAsset, GitHubRelease } from '@shared/types/GitHub.types'; +import { useCallback, useEffect, useState } from 'react'; +import { LuCheck, LuDownload, LuExternalLink, LuRotateCcw } from 'react-icons/lu'; import { - VscPackage, - VscGithub, + VscBeaker, VscCalendar, + VscClose, + VscDebugContinue, + VscDebugPause, + VscGithub, + VscPackage, VscTag, VscVerified, - VscBeaker, - VscDebugPause, - VscDebugContinue, - VscClose, } from 'react-icons/vsc'; -import { LuDownload, LuExternalLink, LuCheck, LuRotateCcw } from 'react-icons/lu'; -import { GitHubAsset, GitHubRelease } from '../../../main/shared/types/GitHub.types'; +import { useTranslation } from '../../../../i18n/I18nProvider'; +import { Button } from '../../../common/Button'; +import { Modal } from '../../../common/Modal'; interface Props { release: GitHubRelease; diff --git a/src/renderer/components/settings/VersionChecker.tsx b/src/renderer/components/settings/sections/about/VersionChecker.tsx similarity index 92% rename from src/renderer/components/settings/VersionChecker.tsx rename to src/renderer/components/settings/sections/about/VersionChecker.tsx index b1fe7f0..ac00fa7 100644 --- a/src/renderer/components/settings/VersionChecker.tsx +++ b/src/renderer/components/settings/sections/about/VersionChecker.tsx @@ -1,10 +1,9 @@ -import React, { useState, useCallback } from 'react'; -import { useTranslation } from '../../i18n/I18nProvider'; -import { Button } from '../common/Button'; -import { Tooltip } from '../common/Tooltip'; +import { GitHubRelease } from '@shared/types/GitHub.types'; +import { useCallback, useState } from 'react'; +import { VscCheck, VscCircleSlash, VscSync, VscWarning } from 'react-icons/vsc'; +import { useTranslation } from '../../../../i18n/I18nProvider'; +import { Tooltip } from '../../../common/Tooltip'; import { ReleaseModal } from './ReleaseModal'; -import { VscCheck, VscWarning, VscSync, VscCircleSlash } from 'react-icons/vsc'; -import { GitHubRelease } from '../../../main/shared/types/GitHub.types'; interface Props { currentVersion: string; diff --git a/src/renderer/components/utils/ActivityLogPanel.tsx b/src/renderer/components/utils/ActivityLogPanel.tsx index 4515b9c..4cf2c83 100644 --- a/src/renderer/components/utils/ActivityLogPanel.tsx +++ b/src/renderer/components/utils/ActivityLogPanel.tsx @@ -1,10 +1,10 @@ -import React, { useState, useCallback, useEffect } from 'react'; +import { ProcessLogEntry } from '@shared/types/Process.types'; +import { useCallback, useEffect, useState } from 'react'; +import { VscListUnordered } from 'react-icons/vsc'; import { useTranslation } from '../../i18n/I18nProvider'; import { Button } from '../common/Button'; import { Dialog } from '../common/Dialog'; import { EmptyState } from '../common/EmptyState'; -import { VscListUnordered } from 'react-icons/vsc'; -import { ProcessLogEntry } from '../../../main/shared/types/Process.types'; export function ActivityLogPanel() { const { t } = useTranslation(); diff --git a/src/renderer/components/utils/ScannerPanel.tsx b/src/renderer/components/utils/ScannerPanel.tsx index dfa18d2..c0f7acc 100644 --- a/src/renderer/components/utils/ScannerPanel.tsx +++ b/src/renderer/components/utils/ScannerPanel.tsx @@ -1,11 +1,11 @@ -import React, { useState, useCallback } from 'react'; +import { JavaProcessInfo } from '@shared/types/Process.types'; +import { useCallback, useState } from 'react'; +import { LuScanLine } from 'react-icons/lu'; +import { VscCheck } from 'react-icons/vsc'; import { useTranslation } from '../../i18n/I18nProvider'; import { Button } from '../common/Button'; import { Dialog } from '../common/Dialog'; import { EmptyState } from '../common/EmptyState'; -import { VscCheck } from 'react-icons/vsc'; -import { LuScanLine } from 'react-icons/lu'; -import { JavaProcessInfo } from '../../../main/shared/types/Process.types'; type Filter = 'java' | 'all'; interface KillIntent { diff --git a/src/renderer/components/utils/UtilitiesTab.tsx b/src/renderer/components/utils/UtilitiesTab.tsx index 69e843d..7868c74 100644 --- a/src/renderer/components/utils/UtilitiesTab.tsx +++ b/src/renderer/components/utils/UtilitiesTab.tsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react'; -import { useTranslation } from '../../i18n/I18nProvider'; -import { VscListUnordered } from 'react-icons/vsc'; +import { useState } from 'react'; import { LuScanLine } from 'react-icons/lu'; +import { VscListUnordered } from 'react-icons/vsc'; +import { useTranslation } from '../../i18n/I18nProvider'; import { ActivityLogPanel } from './ActivityLogPanel'; import { ScannerPanel } from './ScannerPanel'; diff --git a/src/renderer/hooks/ThemeProvider.tsx b/src/renderer/hooks/ThemeProvider.tsx index 3037d94..ad3be0f 100644 --- a/src/renderer/hooks/ThemeProvider.tsx +++ b/src/renderer/hooks/ThemeProvider.tsx @@ -1,10 +1,10 @@ -import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; -import type { ThemeDefinition, ThemeColors } from '../../main/shared/types/Theme.types'; -import { BUILTIN_THEME } from '../../main/shared/config/Theme.config'; +import { ALL_THEMES, BUILTIN_THEME } from '@shared/config/Theme.config'; +import type { ThemeColors, ThemeDefinition } from '@shared/types/Theme.types'; +import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; interface ThemeContextValue { theme: ThemeDefinition; - setTheme: (theme: ThemeDefinition) => Promise; + setTheme: (theme: ThemeDefinition) => void; } const ThemeContext = createContext(null); @@ -90,12 +90,10 @@ function applyThemeToDOM(theme: ThemeDefinition) { if (theme.id === BUILTIN_THEME.id) { existing?.remove(); - localStorage.removeItem('jrc:theme-bg'); document.documentElement.style.background = ''; return; } - localStorage.setItem('jrc:theme-bg', theme.colors['base-950']); const css = buildThemeCSS(theme.colors); if (existing) { existing.textContent = css; @@ -107,22 +105,29 @@ function applyThemeToDOM(theme: ThemeDefinition) { } } +function resolveTheme(id: string): ThemeDefinition { + return ALL_THEMES.find((t) => t.id === id) ?? BUILTIN_THEME; +} + export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setThemeState] = useState(BUILTIN_THEME); useEffect(() => { if (!window.api) return; - window.api.getActiveTheme().then((t) => { + window.api.getSettings().then((s) => { + const t = resolveTheme(s.themeId); setThemeState(t); applyThemeToDOM(t); }); }, []); - const setTheme = useCallback(async (theme: ThemeDefinition) => { + const setTheme = useCallback((theme: ThemeDefinition) => { if (!window.api) return; - const t = await window.api.setActiveTheme(theme); - setThemeState(t); - applyThemeToDOM(t); + setThemeState(theme); + applyThemeToDOM(theme); + window.api.getSettings().then((s) => { + window.api.saveSettings({ ...s, themeId: theme.id }); + }); }, []); return {children}; diff --git a/src/renderer/hooks/useAutoScroll.ts b/src/renderer/hooks/useAutoScroll.ts index c772dd5..6bc912b 100644 --- a/src/renderer/hooks/useAutoScroll.ts +++ b/src/renderer/hooks/useAutoScroll.ts @@ -1,4 +1,4 @@ -import { useRef, useState, useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; export function useAutoScroll(deps: unknown[]) { const scrollRef = useRef(null); diff --git a/src/renderer/hooks/useDevMode.ts b/src/renderer/hooks/useDevMode.ts index 353d223..120e32a 100644 --- a/src/renderer/hooks/useDevMode.ts +++ b/src/renderer/hooks/useDevMode.ts @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; -import { JRCEnvironment } from '../../main/shared/types/App.types'; +import { JRCEnvironment } from '@shared/types/App.types'; +import { useEffect, useState } from 'react'; export function useDevMode(): boolean { const [enabled, setEnabled] = useState(false); diff --git a/src/renderer/hooks/useJarResolutionPreview.ts b/src/renderer/hooks/useJarResolutionPreview.ts index 59cef3b..776981e 100644 --- a/src/renderer/hooks/useJarResolutionPreview.ts +++ b/src/renderer/hooks/useJarResolutionPreview.ts @@ -1,8 +1,5 @@ -import { useState, useCallback, useEffect } from 'react'; -import type { - JarResolutionConfig, - JarResolutionResult, -} from '../../main/shared/types/JarResolution.types'; +import type { JarResolutionConfig, JarResolutionResult } from '@shared/types/JarResolution.types'; +import { useCallback, useEffect, useState } from 'react'; export function useJarResolutionPreview(config: JarResolutionConfig, enabled: boolean) { const [result, setResult] = useState(null); diff --git a/src/renderer/hooks/useUpdateRegistry.ts b/src/renderer/hooks/useUpdateRegistry.ts deleted file mode 100644 index 74cba8c..0000000 --- a/src/renderer/hooks/useUpdateRegistry.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { UPDATE_ITEMS } from '../../main/shared/config/UpdateCenter.config'; -import type { UpdateCheckResult } from '../../main/shared/types/UpdateCenter.types'; -import { version } from '../../../package.json'; - -function semverGt(a: string, b: string): boolean { - const parse = (v: string) => v.replace(/^v/, '').split('.').map(Number); - const [am, an, ap] = parse(a); - const [bm, bn, bp] = parse(b); - if (am !== bm) return am > bm; - if (an !== bn) return an > bn; - return ap > bp; -} - -type CheckFn = () => Promise; -type ApplyFn = () => Promise<{ ok: boolean; error?: string }>; - -interface ResolvedUpdatable { - id: string; - label: string; - description: string; - check: CheckFn; - apply: ApplyFn; -} - -const LOGIC: Record = { - app: { - check: async () => { - const res = await window.api.fetchLatestRelease(); - if (!res.ok || !res.data) - return { - hasUpdate: false, - currentVersion: version, - remoteVersion: version, - error: res.error, - }; - const remote = (res.data.tag_name ?? '').replace(/^v/, ''); - return { - hasUpdate: semverGt(remote, version), - currentVersion: version, - remoteVersion: remote, - }; - }, - apply: async () => ({ ok: false, error: 'Use the release modal to download the installer' }), - }, - theme: { - check: async () => { - const active = await window.api.getActiveTheme(); - const res = await window.api.fetchRemoteThemes(); - if (!res.ok || !res.themes) - return { hasUpdate: false, currentVersion: active.version, remoteVersion: active.version }; - const remote = res.themes.find((t) => t.id === active.id); - return { - hasUpdate: remote ? remote.version > active.version : false, - currentVersion: active.version, - remoteVersion: remote?.version ?? active.version, - }; - }, - apply: async () => { - const active = await window.api.getActiveTheme(); - const res = await window.api.fetchRemoteThemes(); - if (!res.ok || !res.themes) return { ok: false, error: res.error ?? 'Fetch failed' }; - const remote = res.themes.find((t) => t.id === active.id); - if (!remote) return { ok: false, error: 'Theme not found on remote' }; - await window.api.setActiveTheme(remote); - return { ok: true }; - }, - }, - language: { - check: async () => { - const active = await window.api.getActiveLanguage(); - const res = await window.api.fetchRemoteLanguages(); - if (!res.ok || !res.languages) - return { hasUpdate: false, currentVersion: active.version, remoteVersion: active.version }; - const remote = res.languages.find((l) => l.id === active.id); - return { - hasUpdate: remote ? remote.version > active.version : false, - currentVersion: active.version, - remoteVersion: remote?.version ?? active.version, - }; - }, - apply: async () => { - const active = await window.api.getActiveLanguage(); - const res = await window.api.fetchRemoteLanguages(); - if (!res.ok || !res.languages) return { ok: false, error: res.error ?? 'Fetch failed' }; - const remote = res.languages.find((l) => l.id === active.id); - if (!remote) return { ok: false, error: 'Language not found on remote' }; - await window.api.setActiveLanguage(remote); - return { ok: true }; - }, - }, -}; - -export function useUpdateRegistry(): ResolvedUpdatable[] { - return UPDATE_ITEMS.map((item) => ({ - ...item, - check: - LOGIC[item.id]?.check ?? - (async () => ({ hasUpdate: false, currentVersion: '?', remoteVersion: '?' })), - apply: LOGIC[item.id]?.apply ?? (async () => ({ ok: false, error: 'Not implemented' })), - })); -} diff --git a/src/renderer/i18n/I18nProvider.tsx b/src/renderer/i18n/I18nProvider.tsx index 8cc5535..cfc8d20 100644 --- a/src/renderer/i18n/I18nProvider.tsx +++ b/src/renderer/i18n/I18nProvider.tsx @@ -1,16 +1,20 @@ -import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'; -import type { LanguageDefinition } from '../../main/shared/types/Language.types'; -import { ENGLISH } from '../../main/shared/config/DefaultLanguage.config'; +import { ALL_LANGUAGES, ENGLISH } from '@shared/config/Language.config'; +import type { LanguageDefinition } from '@shared/types/Language.types'; +import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; import type { TranslationKey } from './TranslationKeys'; interface I18nContextValue { language: LanguageDefinition; t: (key: TranslationKey, params?: Record) => string; - setLanguage: (lang: LanguageDefinition) => Promise; + setLanguage: (lang: LanguageDefinition) => void; } const I18nContext = createContext(null); +function resolveLanguage(id: string): LanguageDefinition { + return ALL_LANGUAGES.find((l) => l.id === id) ?? ENGLISH; +} + export function I18nProvider({ children }: { children: React.ReactNode }) { const [language, setLang] = useState(ENGLISH); const langRef = useRef(ENGLISH); @@ -21,7 +25,8 @@ export function I18nProvider({ children }: { children: React.ReactNode }) { useEffect(() => { if (!window.api) return; - window.api.getActiveLanguage().then((l) => { + window.api.getSettings().then((s) => { + const l = resolveLanguage(s.languageId); setLang(l); langRef.current = l; }); @@ -41,11 +46,13 @@ export function I18nProvider({ children }: { children: React.ReactNode }) { [language] ); - const setLanguage = useCallback(async (lang: LanguageDefinition) => { + const setLanguage = useCallback((lang: LanguageDefinition) => { if (!window.api) return; - const result = await window.api.setActiveLanguage(lang); - langRef.current = result; - setLang(result); + langRef.current = lang; + setLang(lang); + window.api.getSettings().then((s) => { + window.api.saveSettings({ ...s, languageId: lang.id }); + }); }, []); return ( diff --git a/src/renderer/i18n/TranslationKeys.ts b/src/renderer/i18n/TranslationKeys.ts index 64cd441..d5d99e9 100644 --- a/src/renderer/i18n/TranslationKeys.ts +++ b/src/renderer/i18n/TranslationKeys.ts @@ -1,3 +1,3 @@ -import { ENGLISH_STRINGS } from '../../main/shared/config/DefaultLanguage.config'; +import { ENGLISH_STRINGS } from '@shared/config/Language.config'; export type TranslationKey = keyof typeof ENGLISH_STRINGS; diff --git a/themes/dark-default.json b/themes/dark-default.json deleted file mode 100644 index 895a346..0000000 --- a/themes/dark-default.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "dark-default", - "name": "Dark (Default)", - "version": 1, - "author": "JRC", - "colors": { - "accent": "#4ade80", - "base-950": "#08090d", - "base-900": "#0d0f14", - "base-800": "#11141b", - "surface-raised": "#1a1d26", - "surface-border": "#242736", - "text-primary": "#e8eaf2", - "text-secondary": "#b0b4c8", - "text-muted": "#6b7094", - "console-error": "#f87171", - "console-warn": "#fbbf24", - "console-input": "#60a5fa", - "console-system": "#6b7094" - } -} diff --git a/themes/light.json b/themes/light.json deleted file mode 100644 index 2751460..0000000 --- a/themes/light.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "light", - "name": "Light", - "version": 1, - "author": "JRC", - "colors": { - "accent": "#7c3aed", - "base-950": "#e8ecf5", - "base-900": "#f0f3fb", - "base-800": "#dce1ef", - "surface-raised": "#ffffff", - "surface-border": "#c8cedf", - "text-primary": "#181b2d", - "text-secondary": "#454d6e", - "text-muted": "#7980a0", - "console-error": "#dc2626", - "console-warn": "#b45309", - "console-input": "#1d4ed8", - "console-system": "#7980a0" - } -} diff --git a/themes/midnight-blue.json b/themes/midnight-blue.json deleted file mode 100644 index 35b8803..0000000 --- a/themes/midnight-blue.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "midnight-blue", - "name": "Midnight Blue", - "version": 1, - "author": "JRC", - "colors": { - "accent": "#60a5fa", - "base-950": "#0a0c14", - "base-900": "#0e1220", - "base-800": "#131929", - "surface-raised": "#1c2438", - "surface-border": "#283350", - "text-primary": "#e2e8f4", - "text-secondary": "#94a3c8", - "text-muted": "#5b6b8a", - "console-error": "#fb7185", - "console-warn": "#fbbf24", - "console-input": "#818cf8", - "console-system": "#5b6b8a" - } -} diff --git a/tsconfig.json b/tsconfig.json index 8e0cbdf..d2b5132 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,10 @@ "noUnusedLocals": false, "noUnusedParameters": false, "baseUrl": ".", - "paths": { "@/*": ["src/renderer/*"] } + "paths": { + "@/*": ["src/renderer/*"], + "@shared/*": ["src/main/shared/*"] + } }, "include": ["src/renderer", "src/main/shared"] } diff --git a/vite.config.ts b/vite.config.ts index 36ef9f6..4122dec 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; -import path from 'path'; import { readFileSync } from 'fs'; +import path from 'path'; +import { defineConfig } from 'vite'; const { version } = JSON.parse(readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8')); export default defineConfig({ plugins: [react()], @@ -25,6 +25,11 @@ export default defineConfig({ }, reportCompressedSize: true, }, - resolve: { alias: { '@': path.resolve(__dirname, 'src/renderer') } }, + resolve: { + alias: { + '@': path.resolve(__dirname, 'src/renderer'), + '@shared': path.resolve(__dirname, 'src/main/shared'), + }, + }, server: { port: 5173 }, });