Skip to content

Commit

Permalink
Convert update and nxm dialogs to dialog context
Browse files Browse the repository at this point in the history
  • Loading branch information
olegbl committed Jul 5, 2024
1 parent 6f2dc4e commit 3b6f32c
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 152 deletions.
12 changes: 6 additions & 6 deletions src/main/worker/UpdaterAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ async function getUpdate(): Promise<Update | null> {
}

const appDirectoryPath = path.dirname(getExecutablePath());
const isPreleaseEnabled = existsSync(
path.join(appDirectoryPath, 'ENABLE_PRE_RELEASE_UPDATES'),
);
const isSameVersionUpdateEnabled = existsSync(
path.join(appDirectoryPath, 'ENABLE_SAME_VERSION_UPDATES'),
);
const isPreleaseEnabled =
existsSync(path.join(appDirectoryPath, 'ENABLE_PRE_RELEASE_UPDATES')) ??
true;
const isSameVersionUpdateEnabled =
existsSync(path.join(appDirectoryPath, 'ENABLE_SAME_VERSION_UPDATES')) ??
true;

const release =
(isPreleaseEnabled ? await getLatestPrerelease() : null) ??
Expand Down
188 changes: 128 additions & 60 deletions src/renderer/react/UpdaterDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { IUpdaterAPI, Update } from 'bridge/Updater';
import { useEventAPIListener } from 'renderer/EventAPI';
import { consumeAPI } from 'renderer/IPC';
import { useCallback, useEffect, useState } from 'react';
import {
useDialog,
useDialogContext,
} from 'renderer/react/context/DialogContext';
import useAsyncCallback from 'renderer/react/hooks/useAsyncCallback';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Button,
Dialog,
Expand Down Expand Up @@ -40,77 +45,33 @@ function useUpdaterState(): UpdaterState | null {
return updaterState;
}

export default function UpdaterDialog() {
const [isUpdateIgnored, setIsUpdateIgnored] = useState<boolean>(false);
const [update] = useUpdate();
const updaterState = useUpdaterState();
function NotificationDialog({
onIgnore: onIgnoreFromProps,
onInstall,
version,
}: {
onIgnore: () => void;
onInstall: () => void;
version: string;
}) {
const { close: onClose, isOpen } = useDialogContext();

const onIgnore = useCallback(() => {
setIsUpdateIgnored(true);
}, []);

const onInstall = useCallback(() => {
if (update != null) {
UpdaterAPI.installUpdate(update).catch(console.error);
}
}, [update]);

if (update == null || isUpdateIgnored) {
return null;
}

if (updaterState != null) {
// show loading dialog with the current updater state
return (
<Dialog
aria-describedby="alert-dialog-description"
aria-labelledby="alert-dialog-title"
onClose={onIgnore}
open={true}
>
<DialogTitle id="alert-dialog-title">Updating</DialogTitle>
<DialogContent sx={{ width: 400 }}>
<DialogContentText id="alert-dialog-description">
{updaterState.event === 'cleanup'
? 'Cleaning up...'
: updaterState.event === 'download'
? 'Downloading update...'
: updaterState.event === 'download-progress'
? `Downloading update... ${Math.round(
(updaterState.bytesDownloaded / updaterState.bytesTotal) *
100,
)}%`
: updaterState.event === 'extract'
? 'Extracting update...'
: updaterState.event === 'apply'
? 'Applying update...'
: 'Please wait...'}
</DialogContentText>
{updaterState.event === 'download-progress' && (
<LinearProgress
sx={{ marginTop: 2 }}
value={
(updaterState.bytesDownloaded / updaterState.bytesTotal) * 100
}
variant="determinate"
/>
)}
</DialogContent>
</Dialog>
);
}
onIgnoreFromProps();
onClose();
}, [onClose, onIgnoreFromProps]);

return (
<Dialog
aria-describedby="alert-dialog-description"
aria-labelledby="alert-dialog-title"
onClose={onIgnore}
open={true}
open={isOpen}
>
<DialogTitle id="alert-dialog-title">New Update Available</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Do you want to update to version {update.version} of D2RMM?
Do you want to update to version {version} of D2RMM?
</DialogContentText>
</DialogContent>
<DialogActions>
Expand All @@ -122,3 +83,110 @@ export default function UpdaterDialog() {
</Dialog>
);
}

function ProgressDialog({
updaterState,
}: {
updaterState: UpdaterState | null;
}) {
const { close: onClose, isOpen } = useDialogContext();

if (updaterState == null) {
return null;
}

return (
<Dialog
aria-describedby="alert-dialog-description"
aria-labelledby="alert-dialog-title"
onClose={onClose}
open={isOpen}
>
<DialogTitle id="alert-dialog-title">Updating</DialogTitle>
<DialogContent sx={{ width: 400 }}>
<DialogContentText id="alert-dialog-description">
{updaterState.event === 'cleanup'
? 'Cleaning up...'
: updaterState.event === 'download'
? 'Downloading update...'
: updaterState.event === 'download-progress'
? `Downloading update... ${Math.round(
(updaterState.bytesDownloaded / updaterState.bytesTotal) *
100,
)}%`
: updaterState.event === 'extract'
? 'Extracting update...'
: updaterState.event === 'apply'
? 'Applying update...'
: 'Please wait...'}
</DialogContentText>
{updaterState.event === 'download-progress' && (
<LinearProgress
sx={{ marginTop: 2 }}
value={
(updaterState.bytesDownloaded / updaterState.bytesTotal) * 100
}
variant="determinate"
/>
)}
</DialogContent>
</Dialog>
);
}

export default function UpdaterDialog() {
const [isUpdateIgnored, setIsUpdateIgnored] = useState<boolean>(false);
const [update] = useUpdate();
const updaterState = useUpdaterState();

const onIgnore = useCallback(() => {
setIsUpdateIgnored(true);
}, []);

const onInstall = useAsyncCallback(async () => {
if (update != null) {
await UpdaterAPI.installUpdate(update);
}
}, [update]);

const [showNotificationDialog] = useDialog(
useMemo(
() => (
<NotificationDialog
onIgnore={onIgnore}
onInstall={onInstall}
version={update?.version ?? ''}
/>
),
[onIgnore, onInstall, update?.version],
),
);

const [showProgressDialog] = useDialog(
useMemo(
() => <ProgressDialog updaterState={updaterState} />,
[updaterState],
),
);

useEffect(() => {
if (update == null || isUpdateIgnored) {
return;
}

if (updaterState != null) {
showProgressDialog();
return;
}

showNotificationDialog();
}, [
isUpdateIgnored,
showNotificationDialog,
showProgressDialog,
update,
updaterState,
]);

return null;
}
80 changes: 45 additions & 35 deletions src/renderer/react/context/DialogContext.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { v4 as uuidv4 } from 'uuid';
import React, {
useCallback,
useContext,
useMemo,
useRef,
useState,
} from 'react';
import React, { useCallback, useContext, useMemo, useState } from 'react';

export type Dialog = React.ReactNode;

export type IDialogManagerContext = {
hideDialog: (id: string) => void;
showDialog: (dialog: Dialog) => string;
showDialog: (id: string, dialog: Dialog) => void;
};

const DialogManagerContext = React.createContext<IDialogManagerContext | null>(
Expand All @@ -27,16 +21,32 @@ type Props = {
export function DialogManagerContextProvider({ children }: Props): JSX.Element {
const [dialogs, setDialogs] = useState<[string, Dialog][]>([]);

const showDialog = useCallback((dialog: Dialog): string => {
const id = uuidv4();
setDialogs((oldDialogs) => [...oldDialogs, [id, dialog]]);
return id;
const showDialog = useCallback((id: string, dialog: Dialog): void => {
setDialogs((oldDialogs) => {
const newDialogs = [...oldDialogs];
const index = newDialogs.findIndex(([oldDialogID]) => oldDialogID === id);
if (index !== -1) {
// updating
if (newDialogs[index][1] === dialog) {
// unchanged
return oldDialogs;
}
newDialogs[index] = [id, dialog];
} else {
// adding
newDialogs.push([id, dialog]);
}
return newDialogs;
});
}, []);

const hideDialog = useCallback((id: string): void => {
setDialogs((oldDialogs) =>
oldDialogs.filter(([oldDialogID]) => oldDialogID !== id),
);
setDialogs((oldDialogs) => {
const newDialogs = oldDialogs.filter(
([oldDialogID]) => oldDialogID !== id,
);
return newDialogs.length === oldDialogs.length ? oldDialogs : newDialogs;
});
}, []);

const context = useMemo(
Expand Down Expand Up @@ -64,6 +74,16 @@ export function DialogManagerContextProvider({ children }: Props): JSX.Element {
);
}

function useDialogManagerContext(): IDialogManagerContext {
const context = useContext(DialogManagerContext);
if (context == null) {
throw new Error(
'useDialogManagerContext must be used within a DialogManagerContextProvider',
);
}
return context;
}

type IDialogContext = {
id: string;
isOpen: boolean;
Expand Down Expand Up @@ -96,29 +116,19 @@ function DialogContextProvider({

export function useDialog(
dialog: React.ReactNode,
): [open: () => void, close: () => void] {
const context = useContext(DialogManagerContext);
if (context == null) {
throw new Error(
'useDialog must be used within a DialogManagerContextProvider',
);
}
const { showDialog, hideDialog } = context;

const idRef = useRef<string | null>(null);
): [show: () => void, hide: () => void] {
const id = useMemo(() => uuidv4(), []);
const { showDialog, hideDialog } = useDialogManagerContext();

const close = useCallback(() => {
if (idRef.current != null) {
hideDialog(idRef.current);
}
}, [hideDialog]);
const hide = useCallback(() => {
hideDialog(id);
}, [hideDialog, id]);

const open = useCallback(() => {
close();
idRef.current = showDialog(dialog);
}, [showDialog, close, dialog]);
const show = useCallback(() => {
showDialog(id, dialog);
}, [id, showDialog, dialog]);

return [open, close];
return [show, hide];
}

export function useDialogContext(): IDialogContext {
Expand Down
Loading

0 comments on commit 3b6f32c

Please sign in to comment.