Add better update handler.
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
provider: generic
|
provider: s3
|
||||||
url: https://example.com/auto-updates
|
bucket: imex-partner
|
||||||
updaterCacheDirName: bodyshop-desktop-updater
|
region: ca-central-1
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ asarUnpack:
|
|||||||
- resources/**
|
- resources/**
|
||||||
win:
|
win:
|
||||||
executableName: bodyshop-desktop
|
executableName: bodyshop-desktop
|
||||||
azureSignOptions:
|
azureSignOptions:
|
||||||
endpoint: https://eus.codesigning.azure.net
|
endpoint: https://eus.codesigning.azure.net
|
||||||
certificateProfileName: ImEXRPS
|
certificateProfileName: ImEXRPS
|
||||||
codeSigningAccountName: ImEX
|
codeSigningAccountName: ImEX
|
||||||
|
|
||||||
nsis:
|
nsis:
|
||||||
artifactName: ${name}-${version}-setup.${ext}
|
artifactName: ${name}-${version}-setup.${ext}
|
||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
@@ -32,7 +32,7 @@ mac:
|
|||||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
target:
|
target:
|
||||||
- target: dmg
|
- target: default
|
||||||
arch:
|
arch:
|
||||||
- universal
|
- universal
|
||||||
dmg:
|
dmg:
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const DecodeAD2 = async (
|
|||||||
"CLMT_PH2",
|
"CLMT_PH2",
|
||||||
"CLMT_PH2X",
|
"CLMT_PH2X",
|
||||||
"CLMT_FAX",
|
"CLMT_FAX",
|
||||||
"CLMT_FAXX",
|
//"CLMT_FAXX",
|
||||||
"CLMT_EA",
|
"CLMT_EA",
|
||||||
//"EST_CO_ID",
|
//"EST_CO_ID",
|
||||||
"EST_CO_NM",
|
"EST_CO_NM",
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { electronApp, is, optimizer } from "@electron-toolkit/utils";
|
import { electronApp, is, optimizer } from "@electron-toolkit/utils";
|
||||||
import { app, BrowserWindow, Menu, shell } from "electron";
|
import { app, BrowserWindow, Menu, shell } from "electron";
|
||||||
import log from "electron-log/main";
|
import log from "electron-log/main";
|
||||||
|
import { autoUpdater } from "electron-updater";
|
||||||
import path, { join } from "path";
|
import path, { join } from "path";
|
||||||
import icon from "../../resources/icon.png?asset";
|
import icon from "../../resources/icon.png?asset";
|
||||||
import ErrorTypeCheck from "../util/errorTypeCheck";
|
import ErrorTypeCheck from "../util/errorTypeCheck";
|
||||||
|
import ipcTypes from "../util/ipcTypes.json";
|
||||||
import client from "./graphql/graphql-client";
|
import client from "./graphql/graphql-client";
|
||||||
import store from "./store/store";
|
import store from "./store/store";
|
||||||
import { autoUpdater } from "electron-updater";
|
import { createPublicKey } from "crypto";
|
||||||
|
|
||||||
log.initialize();
|
log.initialize();
|
||||||
const isMac = process.platform === "darwin";
|
const isMac = process.platform === "darwin";
|
||||||
@@ -131,6 +133,12 @@ function createWindow(): void {
|
|||||||
{
|
{
|
||||||
label: "Development",
|
label: "Development",
|
||||||
submenu: [
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Check for updates",
|
||||||
|
click: (): void => {
|
||||||
|
autoUpdater.checkForUpdates();
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Open Log Folder",
|
label: "Open Log Folder",
|
||||||
click: (): void => {
|
click: (): void => {
|
||||||
@@ -251,6 +259,42 @@ app.whenReady().then(async () => {
|
|||||||
//Check for app updates.
|
//Check for app updates.
|
||||||
|
|
||||||
autoUpdater.logger = log;
|
autoUpdater.logger = log;
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// Useful for some dev/debugging tasks, but download can
|
||||||
|
// not be validated becuase dev app is not signed
|
||||||
|
autoUpdater.updateConfigPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"../../dev-app-update.yml",
|
||||||
|
);
|
||||||
|
autoUpdater.forceDevUpdateConfig = true;
|
||||||
|
autoUpdater.autoDownload = false;
|
||||||
|
}
|
||||||
|
autoUpdater.on("checking-for-update", () => {
|
||||||
|
log.info("Checking for update...");
|
||||||
|
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||||
|
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.checking);
|
||||||
|
});
|
||||||
|
autoUpdater.on("update-available", (info) => {
|
||||||
|
log.info("Update available.", info);
|
||||||
|
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||||
|
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.available, info);
|
||||||
|
});
|
||||||
|
autoUpdater.on("download-progress", (progress) => {
|
||||||
|
log.info(`Download speed: ${progress.bytesPerSecond}`);
|
||||||
|
log.info(`Downloaded ${progress.percent}%`);
|
||||||
|
log.info(`Total downloaded ${progress.transferred}/${progress.total}`);
|
||||||
|
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||||
|
mainWindow?.webContents.send(
|
||||||
|
ipcTypes.toRenderer.updates.downloading,
|
||||||
|
progress,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
autoUpdater.on("update-downloaded", (info) => {
|
||||||
|
log.info("Update downloaded", info);
|
||||||
|
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||||
|
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.downloaded, info);
|
||||||
|
});
|
||||||
|
//autoUpdater.checkForUpdates();
|
||||||
autoUpdater.checkForUpdatesAndNotify();
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
SettingsWatchedFilePathsRemove,
|
SettingsWatchedFilePathsRemove,
|
||||||
} from "./ipcMainHandler.settings";
|
} from "./ipcMainHandler.settings";
|
||||||
import { ipcMainHandleAuthStateChanged } from "./ipcMainHandler.user";
|
import { ipcMainHandleAuthStateChanged } from "./ipcMainHandler.user";
|
||||||
|
import { autoUpdater } from "electron-updater";
|
||||||
|
|
||||||
// Log all IPC messages and their payloads
|
// Log all IPC messages and their payloads
|
||||||
const logIpcMessages = (): void => {
|
const logIpcMessages = (): void => {
|
||||||
@@ -87,4 +88,19 @@ ipcMain.on(ipcTypes.toMain.watcher.stop, () => {
|
|||||||
StopWatcher();
|
StopWatcher();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.on(ipcTypes.toMain.updates.download, () => {
|
||||||
|
log.info("Download update requested from renderer.");
|
||||||
|
autoUpdater.downloadUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on(ipcTypes.toMain.updates.checkForUpdates, () => {
|
||||||
|
log.info("Checking for updates from renderer.");
|
||||||
|
autoUpdater.checkForUpdates();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on(ipcTypes.toMain.updates.apply, () => {
|
||||||
|
log.info("Applying update from renderer.");
|
||||||
|
autoUpdater.quitAndInstall();
|
||||||
|
});
|
||||||
|
|
||||||
logIpcMessages();
|
logIpcMessages();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { HashRouter, Route, Routes } from "react-router";
|
import { HashRouter, Route, Routes } from "react-router";
|
||||||
import ipcTypes from "../../util/ipcTypes";
|
import ipcTypes from "../../util/ipcTypes.json";
|
||||||
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback/ErrorBoundaryFallback";
|
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback/ErrorBoundaryFallback";
|
||||||
import Home from "./components/Home/Home";
|
import Home from "./components/Home/Home";
|
||||||
import NavigationHeader from "./components/NavigationHeader/Navigationheader";
|
import NavigationHeader from "./components/NavigationHeader/Navigationheader";
|
||||||
@@ -13,6 +13,8 @@ import Settings from "./components/Settings/Settings";
|
|||||||
import SignInForm from "./components/SignInForm/SignInForm";
|
import SignInForm from "./components/SignInForm/SignInForm";
|
||||||
import reduxStore from "./redux/redux-store";
|
import reduxStore from "./redux/redux-store";
|
||||||
import { auth } from "./util/firebase";
|
import { auth } from "./util/firebase";
|
||||||
|
import { NotificationProvider } from "./util/notificationContext";
|
||||||
|
import UpdateAvailable from "./components/UpdateAvailable/UpdateAvailable";
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
@@ -39,19 +41,22 @@ const App: React.FC = () => {
|
|||||||
<Provider store={reduxStore}>
|
<Provider store={reduxStore}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||||
<Layout>
|
<NotificationProvider>
|
||||||
{!user ? (
|
<Layout>
|
||||||
<SignInForm />
|
{!user ? (
|
||||||
) : (
|
<SignInForm />
|
||||||
<>
|
) : (
|
||||||
<NavigationHeader />
|
<>
|
||||||
<Routes>
|
<NavigationHeader />
|
||||||
<Route path="/" element={<Home />} />
|
<UpdateAvailable />
|
||||||
<Route path="settings" element={<Settings />} />
|
<Routes>
|
||||||
</Routes>
|
<Route path="/" element={<Home />} />
|
||||||
</>
|
<Route path="settings" element={<Settings />} />
|
||||||
)}
|
</Routes>
|
||||||
</Layout>
|
</>
|
||||||
|
)}
|
||||||
|
</Layout>
|
||||||
|
</NotificationProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ const Home: React.FC = () => {
|
|||||||
>
|
>
|
||||||
Test Decode Estimate
|
Test Decode Estimate
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={(): void => {
|
||||||
|
window.electron.ipcRenderer.send(
|
||||||
|
ipcTypes.toMain.updates.checkForUpdates,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Check for Update
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
selectAppUpdateCompleted,
|
||||||
|
selectAppUpdateProgress,
|
||||||
|
selectAppUpdateSpeed,
|
||||||
|
selectUpdateAvailable,
|
||||||
|
} from "@renderer/redux/app.slice";
|
||||||
|
import { useAppSelector } from "@renderer/redux/reduxHooks";
|
||||||
|
import { Affix, Button, Card, Progress } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import ipcTypes from "../../../../util/ipcTypes.json";
|
||||||
|
|
||||||
|
const UpdateAvailable: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isUpdateAvailable = useAppSelector(selectUpdateAvailable);
|
||||||
|
const updateSpeed = useAppSelector(selectAppUpdateSpeed);
|
||||||
|
const updateProgress = useAppSelector(selectAppUpdateProgress);
|
||||||
|
const isUpdateComplete = useAppSelector(selectAppUpdateCompleted);
|
||||||
|
|
||||||
|
const handleDownload = (): void => {
|
||||||
|
window.electron.ipcRenderer.send(ipcTypes.toMain.updates.download);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApply = (): void => {
|
||||||
|
window.electron.ipcRenderer.send(ipcTypes.toMain.updates.apply);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isUpdateAvailable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Affix offsetTop={40} style={{ position: "absolute", right: 20 }}>
|
||||||
|
<Card title={t("updates.available")} style={{ width: "33vw" }}>
|
||||||
|
{updateProgress === 0 && (
|
||||||
|
<Button onClick={handleDownload}>{t("updates.download")}</Button>
|
||||||
|
)}
|
||||||
|
<Progress
|
||||||
|
percent={updateProgress}
|
||||||
|
percentPosition={{ align: "center", type: "outer" }}
|
||||||
|
/>
|
||||||
|
{formatSpeed(updateSpeed)}
|
||||||
|
{isUpdateComplete && (
|
||||||
|
<Button onClick={handleApply}>{t("updates.apply")}</Button>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Affix>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateAvailable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats bytes into a human-readable string with appropriate units
|
||||||
|
* @param bytes Number of bytes
|
||||||
|
* @returns Formatted string with appropriate unit (B/KB/MB/GB)
|
||||||
|
*/
|
||||||
|
const formatSpeed = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return "0 B/s";
|
||||||
|
|
||||||
|
const units = ["B/s", "KB/s", "MB/s", "GB/s"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
|
||||||
|
// Limit to available units and format with 2 decimal places (rounded)
|
||||||
|
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i] || units[units.length - 1]}`;
|
||||||
|
};
|
||||||
@@ -1,12 +1,21 @@
|
|||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import log from "electron-log/renderer";
|
import log from "electron-log/renderer";
|
||||||
import type { RootState } from "./redux-store";
|
import type { RootState } from "./redux-store";
|
||||||
|
import { update } from "lodash";
|
||||||
|
import { notification } from "antd";
|
||||||
interface AppState {
|
interface AppState {
|
||||||
value: number;
|
value: number;
|
||||||
watcher: {
|
watcher: {
|
||||||
started: boolean;
|
started: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
};
|
};
|
||||||
|
updates: {
|
||||||
|
available: boolean;
|
||||||
|
checking: boolean;
|
||||||
|
progress: number;
|
||||||
|
speed: number;
|
||||||
|
completed: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the initial state using that type
|
// Define the initial state using that type
|
||||||
@@ -16,6 +25,13 @@ const initialState: AppState = {
|
|||||||
started: false,
|
started: false,
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
|
updates: {
|
||||||
|
available: false,
|
||||||
|
checking: false,
|
||||||
|
progress: 0,
|
||||||
|
speed: 0,
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appSlice = createSlice({
|
export const appSlice = createSlice({
|
||||||
@@ -34,11 +50,36 @@ export const appSlice = createSlice({
|
|||||||
state.watcher.started = false;
|
state.watcher.started = false;
|
||||||
log.error("[Redux] AppSlice: Watcher Error", action.payload);
|
log.error("[Redux] AppSlice: Watcher Error", action.payload);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateChecking: (state) => {
|
||||||
|
state.updates.checking = true;
|
||||||
|
},
|
||||||
|
updateAvailable: (state) => {
|
||||||
|
state.updates.available = true;
|
||||||
|
state.updates.checking = false;
|
||||||
|
},
|
||||||
|
updateProgress: (state, action) => {
|
||||||
|
state.updates.available = true;
|
||||||
|
state.updates.progress = action?.progress;
|
||||||
|
state.updates.speed = action?.speed;
|
||||||
|
},
|
||||||
|
updateDownloaded: (state) => {
|
||||||
|
state.updates.completed = true;
|
||||||
|
state.updates.progress = 100;
|
||||||
|
state.updates.speed = 0;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { watcherError, watcherStarted, watcherStopped } =
|
export const {
|
||||||
appSlice.actions;
|
watcherError,
|
||||||
|
watcherStarted,
|
||||||
|
watcherStopped,
|
||||||
|
updateAvailable,
|
||||||
|
updateChecking,
|
||||||
|
updateDownloaded,
|
||||||
|
updateProgress,
|
||||||
|
} = appSlice.actions;
|
||||||
|
|
||||||
// Other code such as selectors can use the imported `RootState` type
|
// Other code such as selectors can use the imported `RootState` type
|
||||||
export const selectWatcherStatus = (state: RootState): boolean =>
|
export const selectWatcherStatus = (state: RootState): boolean =>
|
||||||
@@ -47,6 +88,18 @@ export const selectWatcherStatus = (state: RootState): boolean =>
|
|||||||
export const selectWatcherError = (state: RootState): string | null =>
|
export const selectWatcherError = (state: RootState): string | null =>
|
||||||
state.app.watcher.error;
|
state.app.watcher.error;
|
||||||
|
|
||||||
|
export const selectUpdateAvailable = (state: RootState): boolean =>
|
||||||
|
state.app.updates.available;
|
||||||
|
|
||||||
|
export const selectAppUpdateProgress = (state: RootState): number =>
|
||||||
|
state.app.updates.progress;
|
||||||
|
|
||||||
|
export const selectAppUpdateSpeed = (state: RootState): number =>
|
||||||
|
state.app.updates.speed;
|
||||||
|
|
||||||
|
export const selectAppUpdateCompleted = (state: RootState): boolean =>
|
||||||
|
state.app.updates.completed;
|
||||||
|
|
||||||
//Async Functions - Thunks
|
//Async Functions - Thunks
|
||||||
// Define a thunk that dispatches those action creators
|
// Define a thunk that dispatches those action creators
|
||||||
// const fetchUsers = () => async (dispatch) => {
|
// const fetchUsers = () => async (dispatch) => {
|
||||||
@@ -55,4 +108,12 @@ export const selectWatcherError = (state: RootState): string | null =>
|
|||||||
// // dispatch(incrementByAmount(100));
|
// // dispatch(incrementByAmount(100));
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
const updateAvailableThunk = () => async (dispatch) => {
|
||||||
|
notification.info({
|
||||||
|
message: "Update Available",
|
||||||
|
key: "app-update",
|
||||||
|
description: "An update is available for download.",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default appSlice.reducer;
|
export default appSlice.reducer;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
//Set up all of the IPC handlers.
|
//Set up all of the IPC handlers.
|
||||||
import {
|
import {
|
||||||
|
updateAvailable,
|
||||||
|
updateChecking,
|
||||||
|
updateDownloaded,
|
||||||
|
updateProgress,
|
||||||
watcherError,
|
watcherError,
|
||||||
watcherStarted,
|
watcherStarted,
|
||||||
watcherStopped,
|
watcherStopped,
|
||||||
@@ -52,3 +56,31 @@ ipcRenderer.on(
|
|||||||
dispatch(watcherError(error));
|
dispatch(watcherError(error));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//Update Handlers
|
||||||
|
ipcRenderer.on(
|
||||||
|
ipcTypes.toRenderer.updates.checking,
|
||||||
|
(event: Electron.IpcRendererEvent) => {
|
||||||
|
console.log("Checking for updates...");
|
||||||
|
dispatch(updateChecking());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcRenderer.on(
|
||||||
|
ipcTypes.toRenderer.updates.available,
|
||||||
|
(event: Electron.IpcRendererEvent, arg) => {
|
||||||
|
dispatch(updateAvailable());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ipcRenderer.on(
|
||||||
|
ipcTypes.toRenderer.updates.downloading,
|
||||||
|
(event: Electron.IpcRendererEvent, arg) => {
|
||||||
|
dispatch(updateProgress({ progress: arg.progress, speed: arg.speed }));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ipcRenderer.on(
|
||||||
|
ipcTypes.toRenderer.updates.downloaded,
|
||||||
|
(event: Electron.IpcRendererEvent, arg) => {
|
||||||
|
dispatch(updateDownloaded());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
37
src/renderer/src/util/notificationContext.tsx
Normal file
37
src/renderer/src/util/notificationContext.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
import { notification } from "antd";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create our NotificationContext to store the `api` object
|
||||||
|
* returned by notification.useNotification().
|
||||||
|
*/
|
||||||
|
const NotificationContext = createContext(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom hook to make usage easier in child components.
|
||||||
|
*/
|
||||||
|
export const useNotification = () => {
|
||||||
|
return useContext(NotificationContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Provider itself:
|
||||||
|
* - Call notification.useNotification() to get [api, contextHolder].
|
||||||
|
* - Render contextHolder somewhere high-level in your app (so the notifications mount properly).
|
||||||
|
* - Provide `api` via the NotificationContext.
|
||||||
|
*/
|
||||||
|
export const NotificationProvider = ({ children }) => {
|
||||||
|
const [api, contextHolder] = notification.useNotification({
|
||||||
|
placement: "bottomRight",
|
||||||
|
bottom: 70,
|
||||||
|
showProgress: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationContext.Provider value={api}>
|
||||||
|
{/* contextHolder must be rendered in the DOM so notifications can appear */}
|
||||||
|
{contextHolder}
|
||||||
|
{children}
|
||||||
|
</NotificationContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,6 +5,11 @@
|
|||||||
"debug": {
|
"debug": {
|
||||||
"decodeEstimate": "toMain_debug_decodeEstimate"
|
"decodeEstimate": "toMain_debug_decodeEstimate"
|
||||||
},
|
},
|
||||||
|
"updates": {
|
||||||
|
"checkForUpdates": "toMain_updates_checkForUpdates",
|
||||||
|
"download": "toMain_updates_download",
|
||||||
|
"apply": "toMain_updates_apply"
|
||||||
|
},
|
||||||
"watcher": {
|
"watcher": {
|
||||||
"start": "toMain_watcher_start",
|
"start": "toMain_watcher_start",
|
||||||
"stop": "toMain_watcher_stop"
|
"stop": "toMain_watcher_stop"
|
||||||
@@ -27,6 +32,14 @@
|
|||||||
"stopped": "toRenderer_watcher_stopped",
|
"stopped": "toRenderer_watcher_stopped",
|
||||||
"error": "toRenderer_watcher_error"
|
"error": "toRenderer_watcher_error"
|
||||||
},
|
},
|
||||||
|
"updates": {
|
||||||
|
"checking": "toRenderer_updates_checking",
|
||||||
|
"available": "toRenderer_updates_available",
|
||||||
|
"notAvailable": "toRenderer_updates_notAvailable",
|
||||||
|
"error": "toRenderer_updates_error",
|
||||||
|
"downloading": "toRenderer_updates_downloading",
|
||||||
|
"downloaded": "toRenderer_updates_downloaded"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"getToken": "toRenderer_user_getToken"
|
"getToken": "toRenderer_user_getToken"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"help": "Help"
|
"help": "Help"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"translation": {
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"settings": "Settings"
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"addpath": "Add path",
|
"addpath": "Add path",
|
||||||
"startwatcher": "Start Watcher",
|
"startwatcher": "Start Watcher",
|
||||||
"stopwatcher": "Stop Watcher\n"
|
"stopwatcher": "Stop Watcher\n"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"started": "Started",
|
"started": "Started",
|
||||||
"stopped": "Stopped",
|
"stopped": "Stopped",
|
||||||
"watcherstatus": "Watcher Status"
|
"watcherstatus": "Watcher Status"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"updates": {
|
||||||
|
"apply": "Apply Update",
|
||||||
|
"available": "An update is available.",
|
||||||
|
"download": "Download Update",
|
||||||
|
"downloading": "An update is downloading."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,63 @@
|
|||||||
</folder_node>
|
</folder_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>updates</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>apply</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>available</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>download</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>downloading</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
</children>
|
</children>
|
||||||
|
|||||||
Reference in New Issue
Block a user