Skip to content

Commit f992e13

Browse files
committed
Ask on exit with unsaved changes
1 parent 501413f commit f992e13

File tree

15 files changed

+236
-66
lines changed

15 files changed

+236
-66
lines changed

src/main/invokables/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import z from 'zod';
22
import { showOpenDialog, showOpenDialogParamsSchema } from './dialogs';
3+
import { confirmClose, confirmCloseSchema } from './toolset';
34

45
type Invokable<I> = {
56
fn: (input: I) => Promise<unknown>;
@@ -11,4 +12,8 @@ export const invokables: Record<string, Invokable<any>> = {
1112
fn: showOpenDialog,
1213
params: showOpenDialogParamsSchema,
1314
},
15+
toolset_close_confirm: {
16+
fn: confirmClose,
17+
params: confirmCloseSchema,
18+
},
1419
};

src/main/invokables/toolset.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { app } from 'electron';
2+
import z from 'zod';
3+
4+
let closeConfirmed = false;
5+
6+
export const confirmCloseSchema = z.unknown();
7+
8+
export type ConfirmCloseParams = z.infer<typeof confirmClose>;
9+
10+
export async function confirmClose(): Promise<void> {
11+
closeConfirmed = true;
12+
app.quit();
13+
}
14+
15+
export function canClose() {
16+
return closeConfirmed;
17+
}

src/main/main.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { resolveHtmlPath } from './util';
2828
import rustInterface from './rust';
2929
import debug from 'electron-debug';
3030
import { invokables } from './invokables';
31+
import { canClose } from './invokables/toolset';
3132

3233
rustInterface.initLogger();
3334

@@ -49,6 +50,9 @@ const handleInvoke = async (event: IpcMainInvokeEvent, payload: unknown) => {
4950
);
5051
return JSON.parse(result);
5152
};
53+
const sendMainAction = (data: any) => {
54+
mainWindow?.webContents.send('actions', data);
55+
};
5256

5357
let mainWindow: BrowserWindow | null = null;
5458

@@ -112,6 +116,13 @@ const createWindow = async () => {
112116
}
113117
});
114118

119+
mainWindow.on('close', (e) => {
120+
if (canClose()) {
121+
return;
122+
}
123+
sendMainAction({ type: 'toolset_close_requested' });
124+
e.preventDefault();
125+
});
115126
mainWindow.on('closed', () => {
116127
mainWindow = null;
117128
});

src/main/preload.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { contextBridge, ipcRenderer } from 'electron';
22

33
const invokeChannel = 'invoke';
4+
const actionsChannel = 'actions';
45

56
contextBridge.exposeInMainWorld('electronAPI', {
67
invoke: ({ func, params }: { func: string; params: unknown }) =>
78
ipcRenderer.invoke(invokeChannel, {
89
func,
910
params,
1011
}),
12+
onMainAction: (callback: (data: any) => void) => {
13+
ipcRenderer.removeAllListeners(actionsChannel);
14+
ipcRenderer.on(actionsChannel, (_, data) => callback(data));
15+
},
1116
});

src/renderer/App.tsx

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
11
import { BrowserRouter, Route, Routes } from 'react-router-dom';
22
import 'antd/dist/reset.css';
33
import '@ant-design/v5-patch-for-react-19';
4-
import { useMemo } from 'react';
4+
import { useEffect, useMemo } from 'react';
55
import { ROUTES } from './EditorRoutes';
66
import './App.css';
77
import { WithToolsetConfig } from './components/WithToolsetConfig';
8-
import { WithOpenMod } from './components/WithOpenMod';
8+
import { WithSelectedMod } from './components/selectedMod/WithSelectedMod';
99
import { EditorLayout } from './components/EditorLayout';
1010
import { Provider } from 'react-redux';
1111
import { appStore } from './state/store';
12+
import { setCloseRequested } from './state/toolset';
13+
import { useAppDispatch } from './hooks/state';
14+
import { useSelectedMod } from './hooks/useSelectedMod';
15+
import { invokeWithSchema } from './lib/invoke';
16+
import z from 'zod';
17+
18+
function MainActionsHandler() {
19+
const dispatch = useAppDispatch();
20+
const { data: selectedMod } = useSelectedMod();
21+
22+
useEffect(() => {
23+
window.electronAPI.onMainAction((payload) => {
24+
console.log('Toolset close confirm', payload);
25+
if (payload.type === 'toolset_close_requested') {
26+
if (!selectedMod) {
27+
invokeWithSchema(z.unknown(), 'toolset_close_confirm');
28+
} else {
29+
dispatch(setCloseRequested(true));
30+
}
31+
}
32+
});
33+
}, [dispatch, selectedMod]);
34+
35+
return null;
36+
}
1237

1338
export function AppWithoutProviders() {
1439
const routes = useMemo(() => {
@@ -18,15 +43,18 @@ export function AppWithoutProviders() {
1843
}, []);
1944

2045
return (
21-
<WithToolsetConfig>
22-
<WithOpenMod>
23-
<BrowserRouter>
24-
<EditorLayout>
25-
<Routes>{routes}</Routes>
26-
</EditorLayout>
27-
</BrowserRouter>
28-
</WithOpenMod>
29-
</WithToolsetConfig>
46+
<>
47+
<MainActionsHandler />
48+
<WithToolsetConfig>
49+
<WithSelectedMod>
50+
<BrowserRouter>
51+
<EditorLayout>
52+
<Routes>{routes}</Routes>
53+
</EditorLayout>
54+
</BrowserRouter>
55+
</WithSelectedMod>
56+
</WithToolsetConfig>
57+
</>
3058
);
3159
}
3260

src/renderer/components/WithOpenMod.tsx

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useAppDispatch, useAppSelector } from '../../hooks/state';
2+
import { selectCloseModRequested } from '../../state/selectors';
3+
import { Button, List, Modal, Typography } from 'antd';
4+
import { setCloseRequested } from '../../state/toolset';
5+
import { persistJSON, selectModifiedFiles } from '../../state/files';
6+
import { invokeWithSchema } from '../../lib/invoke';
7+
import z from 'zod';
8+
import { useCallback, useEffect, useMemo } from 'react';
9+
10+
export function ConfirmCloseMod() {
11+
const dispatch = useAppDispatch();
12+
const closeRequested = useAppSelector(selectCloseModRequested);
13+
const modifiedFiles = useAppSelector(selectModifiedFiles);
14+
const cancel = useCallback(() => {
15+
dispatch(setCloseRequested(false));
16+
}, [dispatch]);
17+
const close = useCallback(async () => {
18+
await invokeWithSchema(z.unknown(), 'toolset_close_confirm');
19+
}, []);
20+
const saveAll = useCallback(async () => {
21+
for (const file of modifiedFiles) {
22+
dispatch(persistJSON(file));
23+
}
24+
}, [dispatch, modifiedFiles]);
25+
const footer = useMemo(() => {
26+
return (
27+
<>
28+
<Button onClick={cancel}>Cancel</Button>
29+
<Button onClick={close}>Discard Changes</Button>
30+
<Button type="primary" onClick={saveAll}>
31+
Save All
32+
</Button>
33+
</>
34+
);
35+
}, [cancel, close, saveAll]);
36+
37+
useEffect(() => {
38+
if (closeRequested) {
39+
if (modifiedFiles.length == 0) {
40+
close();
41+
}
42+
}
43+
}, [close, closeRequested, modifiedFiles]);
44+
45+
return (
46+
<Modal open={closeRequested} onCancel={cancel} footer={footer}>
47+
<Typography.Paragraph>
48+
You have unsaved changes in the following files:
49+
</Typography.Paragraph>
50+
<Typography.Paragraph>
51+
<List
52+
dataSource={modifiedFiles}
53+
renderItem={(item) => <List.Item>{item}</List.Item>}
54+
bordered
55+
/>
56+
</Typography.Paragraph>
57+
<Typography.Paragraph>
58+
Please choose how to continue.
59+
</Typography.Paragraph>
60+
</Modal>
61+
);
62+
}

src/renderer/components/NewMod.tsx renamed to src/renderer/components/selectedMod/NewMod.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
import { useCallback, useMemo, useState } from 'react';
22
import { Button, Typography } from 'antd';
33
import { IChangeEvent } from '@rjsf/core';
4-
import { JsonSchemaForm } from './JsonSchemaForm';
5-
import { useAppSelector } from '../hooks/state';
6-
import { Mod } from '../state/mods';
7-
import { FullSizeDialogLayout } from './FullSizeDialogLayout';
4+
import { JsonSchemaForm } from '../JsonSchemaForm';
5+
import { useAppSelector } from '../../hooks/state';
6+
import { Mod } from '../../state/mods';
7+
import { FullSizeDialogLayout } from '../FullSizeDialogLayout';
88
import { Space } from 'antd/lib';
9-
import { ErrorAlert } from './ErrorAlert';
10-
import { useSelectedMod } from '../hooks/useSelectedMod';
11-
import { MOD_JSON_SCHEMA } from '../state/mods';
9+
import { ErrorAlert } from '../ErrorAlert';
10+
import { useSelectedMod } from '../../hooks/useSelectedMod';
11+
import { MOD_JSON_SCHEMA } from '../../state/mods';
12+
import { selectStracciatellaHome } from '../../state/selectors';
1213

1314
export function NewMod({ onCancel }: { onCancel: () => void }) {
1415
const {
1516
persisting: loading,
1617
persistingError: error,
1718
create,
1819
} = useSelectedMod();
19-
const stracciatellaHome = useAppSelector(
20-
(s) => s.toolset.data?.config.stracciatellaHome,
21-
);
20+
const stracciatellaHome = useAppSelector(selectStracciatellaHome);
2221
const [formData, setFormData] = useState<Mod>({
2322
id: '',
2423
name: '',

src/renderer/components/OpenMod.tsx renamed to src/renderer/components/selectedMod/SelectMod.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { useCallback, useEffect, useState } from 'react';
22
import { List, Button, Typography } from 'antd';
3-
import { EditableMod } from '../state/mods';
3+
import { EditableMod } from '../../state/mods';
44
import { NewMod } from './NewMod';
5-
import { FullSizeDialogLayout } from './FullSizeDialogLayout';
6-
import { ErrorAlert } from './ErrorAlert';
7-
import { useMods } from '../hooks/useMods';
8-
import { useSelectedMod } from '../hooks/useSelectedMod';
9-
import { FullSizeLoader } from './FullSizeLoader';
5+
import { FullSizeDialogLayout } from '../FullSizeDialogLayout';
6+
import { ErrorAlert } from '../ErrorAlert';
7+
import { useMods } from '../../hooks/useMods';
8+
import { useSelectedMod } from '../../hooks/useSelectedMod';
9+
import { FullSizeLoader } from '../FullSizeLoader';
1010

11-
export function OpenMod() {
11+
export function SelectMod() {
1212
const { loading, error: modsError, data, refresh } = useMods();
1313
const { persisting, persistingError, update } = useSelectedMod();
1414
const editableMods = data?.editable ?? [];
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { PropsWithChildren, useEffect } from 'react';
2+
import { SelectMod } from './SelectMod';
3+
import { FullSizeLoader } from '../FullSizeLoader';
4+
import { FullSizeDialogLayout } from '../FullSizeDialogLayout';
5+
import { ErrorAlert } from '../ErrorAlert';
6+
import { useSelectedMod } from '../../hooks/useSelectedMod';
7+
import { ConfirmCloseMod } from './ConfirmCloseMod';
8+
9+
export function WithSelectedMod({ children }: PropsWithChildren<unknown>) {
10+
const { loading, loadingError, data, refresh } = useSelectedMod();
11+
12+
useEffect(() => {
13+
refresh();
14+
}, [refresh]);
15+
16+
if (loadingError) {
17+
return (
18+
<FullSizeDialogLayout>
19+
<ErrorAlert error={loadingError} />
20+
</FullSizeDialogLayout>
21+
);
22+
}
23+
if (loading) {
24+
return <FullSizeLoader />;
25+
}
26+
if (!data) {
27+
return <SelectMod />;
28+
}
29+
return (
30+
<>
31+
<ConfirmCloseMod />
32+
{children}
33+
</>
34+
);
35+
}

0 commit comments

Comments
 (0)