Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "java-runner-client",
"version": "2.1.5",
"version": "2.1.6",
"description": "Run and manage Java processes with profiles, console I/O, and system tray support",
"main": "dist/main/main.js",
"scripts": {
Expand Down
14 changes: 9 additions & 5 deletions src/renderer/components/profiles/ConfigTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export function ConfigTab() {
const { activeProfile, saveProfile, isRunning, startProcess, stopProcess } = useApp();

const [draft, setDraft] = useState<Profile | null>(null);
// savedSnapshot holds what was last persisted — used for dirty detection
const [savedSnapshot, setSavedSnapshot] = useState<Profile | null>(null);
const [saved, setSaved] = useState(false);
const [section, setSection] = useState<Section>('general');
const [pendingArg, setPendingArg] = useState(false);
Expand All @@ -32,23 +34,25 @@ export function ConfigTab() {
useEffect(() => {
if (activeProfile) {
setDraft({ ...activeProfile });
setSavedSnapshot({ ...activeProfile });
setSaved(false);
setPendingArg(false);
}
}, [activeProfile?.id]);

const isDirty = useMemo(() => {
if (!draft || !activeProfile) return false;
return JSON.stringify(draft) !== JSON.stringify(activeProfile);
}, [draft, activeProfile]);
if (!draft || !savedSnapshot) return false;
return JSON.stringify(draft) !== JSON.stringify(savedSnapshot);
}, [draft, savedSnapshot]);

const handleSave = useCallback(async () => {
if (!draft) return;
await saveProfile(draft);
activeProfile && Object.assign(activeProfile, draft);
// Update the saved snapshot so dirty detection resets correctly
setSavedSnapshot({ ...draft });
setSaved(true);
setTimeout(() => setSaved(false), 1800);
}, [draft, saveProfile, activeProfile]);
}, [draft, saveProfile]);

const requestSectionChange = useCallback(
(next: Section) => {
Expand Down
36 changes: 22 additions & 14 deletions src/renderer/components/settings/SettingsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,51 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useMemo, useRef } from 'react';
import { useApp } from '../../AppProvider';
import { Button } from '../common/Button';
import { Toggle } from '../common/Toggle';
import { VersionChecker } from './VersionChecker';
import { REST_API_CONFIG } from '../../../main/shared/config/API.config';
import { version } from '../../../../package.json';
import { AppSettings, JRCEnvironment } from '../../../main/shared/types/App.types';
import { AppSettings } from '../../../main/shared/types/App.types';

export function SettingsTab() {
const { state, saveSettings } = useApp();
const [draft, setDraft] = useState<AppSettings | null>(null);
const [saved, setSaved] = useState(false);
// Track whether the draft was initialized so we don't overwrite user edits
const initializedRef = useRef(false);

const set = (patch: Partial<AppSettings>) => {
setSaved(false);
setDraft((prev) => (prev ? { ...prev, ...patch } : prev));
};

// Initialize draft from store once on mount, then only sync fields the user
// hasn't touched (devModeEnabled) when the env changes externally.
useEffect(() => {
if (!state.settings) return;
if (!initializedRef.current) {
setDraft(state.settings);
initializedRef.current = true;
return;
}
// Only sync devModeEnabled from outside — everything else is user-controlled
setDraft((prev) => {
if (!prev) return state.settings;
return { ...state.settings, ...prev, devModeEnabled: prev.devModeEnabled };
if (prev.devModeEnabled === state.settings!.devModeEnabled) return prev;
return { ...prev, devModeEnabled: state.settings!.devModeEnabled };
});
}, [state.settings]);

// Listen for dev-mode toggled externally (via the DevModeGate shortcut)
useEffect(() => {
const listener = async (e: JRCEnvironment) => {
setSaved(false);
const unsub = window.env.onChange((env) => {
setDraft((prev) => {
if (!prev || prev.devModeEnabled === e.devMode) return prev;
return { ...prev, devModeEnabled: e.devMode };
if (!prev || prev.devModeEnabled === env.devMode) return prev;
return { ...prev, devModeEnabled: env.devMode };
});
if (state.settings && state.settings.devModeEnabled !== e.devMode) {
await saveSettings({ ...state.settings, devModeEnabled: e.devMode });
}
};
window.env.onChange(listener);
}, [state.settings, saveSettings]);
});
return unsub;
}, []);

const isDirty = useMemo(() => {
if (!draft || !state.settings) return false;
Expand All @@ -60,7 +68,7 @@ export function SettingsTab() {
<span className="text-xs text-text-secondary flex-1">
{saved ? 'Settings saved' : 'Unsaved changes'}
</span>
<Button variant="primary" size="sm" onClick={handleSave} disabled={!isDirty && !saved}>
<Button variant="primary" size="sm" onClick={handleSave} disabled={!isDirty}>
{saved ? 'Saved' : 'Save Changes'}
</Button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
img-src 'self' data: https://avatars.githubusercontent.com;
font-src 'self';
connect-src 'self' http://127.0.0.1:* http://localhost:*;
"
Expand Down
Loading