mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-03-11 17:45:30 +00:00
244 lines
11 KiB
TypeScript
244 lines
11 KiB
TypeScript
import React from 'react';
|
|
import { MessageHandler } from '../../../../@types/messageHandler';
|
|
import useStore from '../hooks/useStore';
|
|
import type { MessageTypes, WSMessage, WSMessageWithID } from '../../../../@types/ws';
|
|
import type { Handler, RandomEvent, RandomEvents } from '../../../../@types/randomEvents';
|
|
import { Avatar, Box, Button, TextField, Typography } from '@mui/material';
|
|
import { v4 } from 'uuid';
|
|
import { useSnackbar } from 'notistack';
|
|
import { LockOutlined, PowerSettingsNew } from '@mui/icons-material';
|
|
import { GUIConfig } from '../../../../modules/module.cfg-loader';
|
|
|
|
export type FrontEndMessages = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => Promise<boolean> });
|
|
|
|
export class RandomEventHandler {
|
|
private handler: {
|
|
[eventName in keyof RandomEvents]: Handler<eventName>[]
|
|
} = {
|
|
progress: [],
|
|
finish: [],
|
|
queueChange: [],
|
|
current: []
|
|
};
|
|
|
|
public on<T extends keyof RandomEvents>(name: T, listener: Handler<T>) {
|
|
if (Object.prototype.hasOwnProperty.call(this.handler, name)) {
|
|
this.handler[name].push(listener as any);
|
|
} else {
|
|
this.handler[name] = [ listener as any ];
|
|
}
|
|
}
|
|
|
|
public emit<T extends keyof RandomEvents>(name: keyof RandomEvents, data: RandomEvent<T>) {
|
|
(this.handler[name] ?? []).forEach(handler => handler(data as any));
|
|
}
|
|
|
|
public removeListener<T extends keyof RandomEvents>(name: T, listener: Handler<T>) {
|
|
this.handler[name] = (this.handler[name] as Handler<T>[]).filter(a => a !== listener) as any;
|
|
}
|
|
}
|
|
|
|
export const messageChannelContext = React.createContext<FrontEndMessages|undefined>(undefined);
|
|
|
|
async function messageAndResponse<T extends keyof MessageTypes>(socket: WebSocket, msg: WSMessage<T>): Promise<WSMessage<T, 1>> {
|
|
const id = v4();
|
|
const ret = new Promise<WSMessage<T, 1>>((resolve) => {
|
|
const handler = function({ data }: MessageEvent) {
|
|
const parsed = JSON.parse(data.toString()) as WSMessageWithID<T, 1>;
|
|
if (parsed.id === id) {
|
|
socket.removeEventListener('message', handler);
|
|
resolve(parsed);
|
|
}
|
|
};
|
|
socket.addEventListener('message', handler);
|
|
});
|
|
const toSend = msg as WSMessageWithID<T>;
|
|
toSend.id = id;
|
|
|
|
socket.send(JSON.stringify(toSend));
|
|
return ret;
|
|
}
|
|
|
|
const MessageChannelProvider: FCWithChildren = ({ children }) => {
|
|
|
|
const [store, dispatch] = useStore();
|
|
const [socket, setSocket] = React.useState<undefined|WebSocket>();
|
|
const [publicWS, setPublicWS] = React.useState<undefined|WebSocket>();
|
|
const [usePassword, setUsePassword] = React.useState<'waiting'|'yes'|'no'>('waiting');
|
|
const [isSetup, setIsSetup] = React.useState<'waiting'|'yes'|'no'>('waiting');
|
|
|
|
const { enqueueSnackbar } = useSnackbar();
|
|
|
|
React.useEffect(() => {
|
|
const wss = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`);
|
|
wss.addEventListener('open', () => {
|
|
setPublicWS(wss);
|
|
});
|
|
wss.addEventListener('error', () => {
|
|
enqueueSnackbar('Unable to connect to server. Please reload the page to try again.', { variant: 'error' });
|
|
});
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
(async () => {
|
|
if (!publicWS)
|
|
return;
|
|
setUsePassword((await messageAndResponse(publicWS, { name: 'requirePassword', data: undefined })).data ? 'yes' : 'no');
|
|
setIsSetup((await messageAndResponse(publicWS, { name: 'isSetup', data: undefined })).data ? 'yes' : 'no');
|
|
})();
|
|
}, [publicWS]);
|
|
|
|
const connect = (ev?: React.FormEvent<HTMLFormElement>) => {
|
|
let search = new URLSearchParams();
|
|
if (ev) {
|
|
ev.preventDefault();
|
|
const formData = new FormData(ev.currentTarget);
|
|
const password = formData.get('password')?.toString();
|
|
if (!password)
|
|
return enqueueSnackbar('Please provide both a username and password', {
|
|
variant: 'error'
|
|
});
|
|
search = new URLSearchParams({
|
|
password
|
|
});
|
|
}
|
|
|
|
const wws = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/private?${search}`, );
|
|
wws.addEventListener('open', () => {
|
|
console.log('[INFO] [WS] Connected');
|
|
setSocket(wws);
|
|
});
|
|
wws.addEventListener('error', (er) => {
|
|
console.error('[ERROR] [WS]', er);
|
|
enqueueSnackbar('Unable to connect to server. Please check the password and try again.', {
|
|
variant: 'error'
|
|
});
|
|
});
|
|
};
|
|
|
|
const setup = async (ev: React.FormEvent<HTMLFormElement>) => {
|
|
ev.preventDefault();
|
|
if (!socket)
|
|
return enqueueSnackbar('Invalid state: socket not found', { variant: 'error' });
|
|
const formData = new FormData(ev.currentTarget);
|
|
const password = formData.get('password');
|
|
const data = {
|
|
port: parseInt(formData.get('port')?.toString() ?? '') ?? 3000,
|
|
password: password ? password.toString() : undefined
|
|
} as GUIConfig;
|
|
await messageAndResponse(socket, { name: 'setupServer', data });
|
|
enqueueSnackbar(`The following settings have been set: Port=${data.port}, Password=${data.password ?? 'noPasswordRequired'}`, {
|
|
variant: 'success',
|
|
persist: true
|
|
});
|
|
enqueueSnackbar('Please restart the server now.', {
|
|
variant: 'info',
|
|
persist: true
|
|
});
|
|
};
|
|
|
|
const randomEventHandler = React.useMemo(() => new RandomEventHandler(), []);
|
|
|
|
React.useEffect(() => {
|
|
(async () => {
|
|
if (!socket)
|
|
return;
|
|
const currentService = await messageAndResponse(socket, { name: 'type', data: undefined });
|
|
if (currentService.data !== undefined)
|
|
return dispatch({ type: 'service', payload: currentService.data });
|
|
if (store.service !== currentService.data)
|
|
messageAndResponse(socket, { name: 'setup', data: store.service });
|
|
})();
|
|
}, [store.service, dispatch, socket]);
|
|
|
|
React.useEffect(() => {
|
|
if (!socket)
|
|
return;
|
|
/* finish is a placeholder */
|
|
const listener = (initalData: MessageEvent<string>) => {
|
|
const data = JSON.parse(initalData.data) as RandomEvent<'finish'>;
|
|
randomEventHandler.emit(data.name, data);
|
|
};
|
|
socket.addEventListener('message', listener);
|
|
return () => {
|
|
socket.removeEventListener('message', listener);
|
|
};
|
|
}, [ socket ]);
|
|
|
|
if (usePassword === 'waiting')
|
|
return <></>;
|
|
|
|
if (socket === undefined) {
|
|
if (usePassword === 'no') {
|
|
connect(undefined);
|
|
return <></>;
|
|
}
|
|
return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}>
|
|
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
|
|
<LockOutlined />
|
|
</Avatar>
|
|
<Typography component="h1" variant="h5" color="text.primary">
|
|
Login
|
|
</Typography>
|
|
<Box component="form" onSubmit={connect} sx={{ mt: 1 }}>
|
|
<TextField name="password" margin='normal' type="password" fullWidth variant="filled" required label={'Password'} />
|
|
<Button type='submit' variant='contained' sx={{ mt: 3, mb: 2 }} fullWidth>Login</Button>
|
|
<Typography color="text.secondary" align='center' component="p" variant='body2'>
|
|
You need to login in order to use this tool.
|
|
</Typography>
|
|
</Box>
|
|
</Box>;
|
|
}
|
|
|
|
if (isSetup === 'no') {
|
|
return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}>
|
|
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
|
|
<PowerSettingsNew />
|
|
</Avatar>
|
|
<Typography component="h1" variant="h5" color="text.primary">
|
|
Confirm
|
|
</Typography>
|
|
<Box component="form" onSubmit={setup} sx={{ mt: 1 }}>
|
|
<TextField name="port" margin='normal' type="number" fullWidth variant="filled" required label={'Port'} defaultValue={3000} />
|
|
<TextField name="password" margin='normal' type="password" fullWidth variant="filled" label={'Password'} />
|
|
<Button type='submit' variant='contained' sx={{ mt: 3, mb: 2 }} fullWidth>Confirm</Button>
|
|
<Typography color="text.secondary" align='center' component="p" variant='body2'>
|
|
Please enter data that will be set to use this tool.
|
|
<br />
|
|
Leave blank to use no password (NOT RECOMMENDED)!
|
|
</Typography>
|
|
</Box>
|
|
</Box>;
|
|
}
|
|
|
|
const messageHandler: FrontEndMessages = {
|
|
name: 'default',
|
|
auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data,
|
|
version: async () => (await messageAndResponse(socket, { name: 'version', data: undefined })).data,
|
|
checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data,
|
|
search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data,
|
|
handleDefault: async (data) => (await messageAndResponse(socket, { name: 'default', data })).data,
|
|
availableDubCodes: async () => (await messageAndResponse(socket, { name: 'availableDubCodes', data: undefined})).data,
|
|
availableSubCodes: async () => (await messageAndResponse(socket, { name: 'availableSubCodes', data: undefined })).data,
|
|
resolveItems: async (data) => (await messageAndResponse(socket, { name: 'resolveItems', data })).data,
|
|
listEpisodes: async (data) => (await messageAndResponse(socket, { name: 'listEpisodes', data })).data,
|
|
randomEvents: randomEventHandler,
|
|
downloadItem: (data) => messageAndResponse(socket, { name: 'downloadItem', data }),
|
|
isDownloading: async () => (await messageAndResponse(socket, { name: 'isDownloading', data: undefined })).data,
|
|
openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }),
|
|
logout: async () => (await messageAndResponse(socket, { name: 'changeProvider', data: undefined })).data,
|
|
openFile: async (data) => await messageAndResponse(socket, { name: 'openFile', data }),
|
|
openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data }),
|
|
getQueue: async () => (await messageAndResponse(socket, { name: 'getQueue', data: undefined })).data,
|
|
removeFromQueue: async (data) => await messageAndResponse(socket, { name: 'removeFromQueue', data }),
|
|
clearQueue: async () => await messageAndResponse(socket, { name: 'clearQueue', data: undefined }),
|
|
setDownloadQueue: async (data) => await messageAndResponse(socket, { name: 'setDownloadQueue', data }),
|
|
getDownloadQueue: async () => (await messageAndResponse(socket, { name: 'getDownloadQueue', data: undefined })).data,
|
|
};
|
|
|
|
return <messageChannelContext.Provider value={messageHandler}>
|
|
{children}
|
|
</messageChannelContext.Provider>;
|
|
};
|
|
|
|
export default MessageChannelProvider;
|