Add better update handler.
This commit is contained in:
@@ -5,7 +5,7 @@ import { useEffect, useState } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { Provider } from "react-redux";
|
||||
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 Home from "./components/Home/Home";
|
||||
import NavigationHeader from "./components/NavigationHeader/Navigationheader";
|
||||
@@ -13,6 +13,8 @@ import Settings from "./components/Settings/Settings";
|
||||
import SignInForm from "./components/SignInForm/SignInForm";
|
||||
import reduxStore from "./redux/redux-store";
|
||||
import { auth } from "./util/firebase";
|
||||
import { NotificationProvider } from "./util/notificationContext";
|
||||
import UpdateAvailable from "./components/UpdateAvailable/UpdateAvailable";
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
@@ -39,19 +41,22 @@ const App: React.FC = () => {
|
||||
<Provider store={reduxStore}>
|
||||
<HashRouter>
|
||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||
<Layout>
|
||||
{!user ? (
|
||||
<SignInForm />
|
||||
) : (
|
||||
<>
|
||||
<NavigationHeader />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</>
|
||||
)}
|
||||
</Layout>
|
||||
<NotificationProvider>
|
||||
<Layout>
|
||||
{!user ? (
|
||||
<SignInForm />
|
||||
) : (
|
||||
<>
|
||||
<NavigationHeader />
|
||||
<UpdateAvailable />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</>
|
||||
)}
|
||||
</Layout>
|
||||
</NotificationProvider>
|
||||
</ErrorBoundary>
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
|
||||
@@ -14,6 +14,15 @@ const Home: React.FC = () => {
|
||||
>
|
||||
Test Decode Estimate
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
window.electron.ipcRenderer.send(
|
||||
ipcTypes.toMain.updates.checkForUpdates,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Check for Update
|
||||
</Button>
|
||||
</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 log from "electron-log/renderer";
|
||||
import type { RootState } from "./redux-store";
|
||||
import { update } from "lodash";
|
||||
import { notification } from "antd";
|
||||
interface AppState {
|
||||
value: number;
|
||||
watcher: {
|
||||
started: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
updates: {
|
||||
available: boolean;
|
||||
checking: boolean;
|
||||
progress: number;
|
||||
speed: number;
|
||||
completed: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// Define the initial state using that type
|
||||
@@ -16,6 +25,13 @@ const initialState: AppState = {
|
||||
started: false,
|
||||
error: null,
|
||||
},
|
||||
updates: {
|
||||
available: false,
|
||||
checking: false,
|
||||
progress: 0,
|
||||
speed: 0,
|
||||
completed: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const appSlice = createSlice({
|
||||
@@ -34,11 +50,36 @@ export const appSlice = createSlice({
|
||||
state.watcher.started = false;
|
||||
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 } =
|
||||
appSlice.actions;
|
||||
export const {
|
||||
watcherError,
|
||||
watcherStarted,
|
||||
watcherStopped,
|
||||
updateAvailable,
|
||||
updateChecking,
|
||||
updateDownloaded,
|
||||
updateProgress,
|
||||
} = appSlice.actions;
|
||||
|
||||
// Other code such as selectors can use the imported `RootState` type
|
||||
export const selectWatcherStatus = (state: RootState): boolean =>
|
||||
@@ -47,6 +88,18 @@ export const selectWatcherStatus = (state: RootState): boolean =>
|
||||
export const selectWatcherError = (state: RootState): string | null =>
|
||||
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
|
||||
// Define a thunk that dispatches those action creators
|
||||
// const fetchUsers = () => async (dispatch) => {
|
||||
@@ -55,4 +108,12 @@ export const selectWatcherError = (state: RootState): string | null =>
|
||||
// // 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;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//Set up all of the IPC handlers.
|
||||
import {
|
||||
updateAvailable,
|
||||
updateChecking,
|
||||
updateDownloaded,
|
||||
updateProgress,
|
||||
watcherError,
|
||||
watcherStarted,
|
||||
watcherStopped,
|
||||
@@ -52,3 +56,31 @@ ipcRenderer.on(
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user