See todo for changes

This commit is contained in:
Izuco 2022-02-12 16:52:36 +01:00
parent f076a7af7e
commit e9852ade1a
No known key found for this signature in database
GPG key ID: E9CBE9E4EF3A1BFA
23 changed files with 461 additions and 169 deletions

View file

@ -11,7 +11,8 @@ export interface MessageHandler {
resolveItems: (data: ResolveItemsData) => Promise<ResponseBase<QueueItem[]>>,
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
downloadItem: (data) => void,
isDownloading: () => boolean
isDownloading: () => boolean,
writeToClipboard: (text: string) => void
}
export type QueueItem = {
@ -53,7 +54,10 @@ export type Episode = {
season: string,
seasonTitle: string,
episode: string,
id: string
id: string,
img: string,
description: string,
time: string
}
export type SearchResponse = ResponseBase<SearchResponseItem[]>

10
TODO.md
View file

@ -1,4 +1,12 @@
- [ ] Hls-Download force yes or no on rewrite promt as well as for mkvmerge/ffmpeg
- [x] Pick up if a download is currently in progress
- [x] Send more information with the progress event like the title and image to display more information
- [ ] Use Click away listener for the search popup
- [x] Use Click away listener for the search popup
- [x] Quality select button is uncrontrolled/controlled
- [ ] Set Options font in divider
- [x] Window title
- [x] Only open dev tools in test version
- [ ] Add help information (version, contributor, documentation...)
- [ ] App Icon with electron-forge make
- [x] ContextMenu
- [x] Better episode listing with selectio via left mouse button

View file

@ -1346,6 +1346,8 @@ export default class Crunchy implements ServiceClass {
}
return { data: episodes, list: Object.entries(episodes).map(([key, value]) => {
const images = (value.items[0].images.thumbnail ?? [[ { source: '/notFound.png' } ]])[0];
const seconds = Math.floor(value.items[0].duration_ms / 1000);
return {
e: key.startsWith('E') ? key.slice(1) : key,
lang: value.langs.map(a => a.code),
@ -1353,7 +1355,10 @@ export default class Crunchy implements ServiceClass {
season: value.items[0].season_number.toString(),
seasonTitle: value.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd(),
episode: value.items[0].episode_number?.toString() ?? value.items[0].episode ?? '?',
id: value.items[0].season_id
id: value.items[0].season_id,
img: images[Math.floor(images.length / 2)].source,
description: value.items[0].description,
time: `${Math.floor(seconds / 60)}:${seconds % 60}`
}
})};
}

View file

@ -5,6 +5,7 @@ import fs from 'fs';
import dotenv from 'dotenv';
import express from "express";
import { Console } from 'console';
import './menu';
if (fs.existsSync(path.join(__dirname, '.env')))
dotenv.config({ path: path.join(__dirname, '.env'), debug: true });
@ -21,37 +22,12 @@ export { mainWindow };
const icon = path.join(__dirname, 'images', `Logo_Inverted.${isWindows ? 'ico' : 'png'}`);
if (!process.env.TEST) {
console = ((oldConsole: Console) => {
console = (() => {
const logFolder = path.join(__dirname, 'logs');
if (!fs.existsSync(logFolder))
fs.mkdirSync(logFolder);
return new Console(fs.createWriteStream(path.join(logFolder, `${Date.now()}.log`)));
const writeLogFile = (type: 'log'|'info'|'warn'|'error', args: any[]) => {
const file = path.join(logFolder, `${type}.log`);
args = args.map(a => {
const type = typeof a;
if (type === 'function' || type === 'symbol')
return '';
if (type === 'object')
return JSON.stringify(a);
if (type === 'undefined')
return 'undefined';
return a;
});
fs.appendFileSync(file, args.join(' ') + '\n');
}
return {
...oldConsole,
log: (...data: any[]) => writeLogFile('log', data),
info: (...data: any[]) => writeLogFile('info', data),
warn: (...data: any[]) => writeLogFile('warn', data),
error: (...data: any[]) => writeLogFile('error', data),
} as Console;
})(console);
})();
}
const createWindow = async () => {
@ -62,7 +38,7 @@ const createWindow = async () => {
width: 800,
title: 'AniDL GUI BETA',
webPreferences: {
nodeIntegration: true,
nodeIntegration: false,
preload: path.join(__dirname, 'preload.js')
},
icon,
@ -112,7 +88,8 @@ const createWindow = async () => {
}
mainWindow.loadURL('http://localhost:3000');
mainWindow.webContents.openDevTools();
if (process.env.TEST)
mainWindow.webContents.openDevTools();
};
app.on('ready', createWindow);

81
gui/electron/src/menu.ts Normal file
View file

@ -0,0 +1,81 @@
import { Menu, MenuItem, MenuItemConstructorOptions, shell } from "electron";
import path from 'path';
import json from '../../../package.json';
const template: (MenuItemConstructorOptions | MenuItem)[] = [
{
label: 'Edit',
submenu: [
{
role: 'undo'
},
{
role: 'redo'
},
{
type: 'separator'
},
{
role: 'cut'
},
{
role: 'copy'
},
{
role: 'paste'
}
]
},
{
label: 'Debug',
submenu: [
{
role: 'toggleDevTools'
},
{
label: 'Open log folder',
click: () => {
shell.openPath(path.join(__dirname, 'logs'))
}
}
]
},
{
label: 'Help',
submenu: [
{
label: 'Version',
sublabel: json.version
},
{
label: 'GitHub',
click: () => {
shell.openExternal('https://github.com/anidl/multi-downloader-nx')
}
},
{
label: 'Report a Bug',
click: () => {
shell.openExternal(`https://github.com/anidl/multi-downloader-nx/issues/new?assignees=izu-co&labels=bug&template=bug.yml&title=BUG&version=${json.version}`)
}
},
{
type: 'separator'
},
{
label: 'Contributors',
click: () => {
shell.openExternal('https://github.com/anidl/multi-downloader-nx/graphs/contributors')
}
},
{
label: 'Discord',
click: () => {
shell.openExternal('https://discord.gg/qEpbWen5vq')
}
}
]
}
]
Menu.setApplicationMenu(Menu.buildFromTemplate(template));

View file

@ -22,5 +22,6 @@ export default (window: BrowserWindow) => {
ipcMain.handle('resolveItems', async (_, data) => handler?.resolveItems(data));
ipcMain.handle('listEpisodes', async (_, data) => handler?.listEpisodes(data));
ipcMain.handle('downloadItem', async (_, data) => handler?.downloadItem(data));
ipcMain.handle('writeToClipboard', async (_, data) => handler?.writeToClipboard(data));
ipcMain.on('isDownloading', (ev) => ev.returnValue = handler?.isDownloading());
};

View file

@ -1,4 +1,4 @@
import { BrowserWindow, dialog } from "electron";
import { BrowserWindow, clipboard, dialog } from "electron";
import { DownloadInfo, ExtendedProgress, ProgressData } from "../../../../@types/messageHandler";
import { RandomEvent, RandomEvents } from "../../../../@types/randomEvents";
@ -49,4 +49,9 @@ export default class Base {
return this.downloading;
}
async writeToClipboard(text: string) {
clipboard.writeText(text, 'clipboard');
return true;
}
}

View file

@ -23,11 +23,14 @@ class FunimationHandler extends Base implements MessageHandler {
return { isOk: true, value: request.value.map(item => ({
e: item.id_split.join(''),
lang: item.audio ?? [],
name: item.episodeSlug,
season: item.seasonNum,
name: item.title,
season: item.seasonNum ?? item.seasonTitle ?? item.item.seasonNum ?? item.item.seasonTitle,
seasonTitle: item.seasonTitle,
episode: item.episodeNum,
id: item.id
id: item.id,
img: item.thumb,
description: item.synopsis,
time: item.runtime ?? item.item.runtime
})) }
}

View file

@ -2,7 +2,6 @@
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-eval'"

View file

@ -28,7 +28,7 @@ const AuthButton: React.FC = () => {
setAuthed((await messageChannel?.checkToken())?.isOk ?? false);
}
React.useEffect(() => { checkAuth(); return () => {}; }, []);
React.useEffect(() => { checkAuth() }, []);
const handleSubmit = async () => {
if (!messageChannel)

View file

@ -1,11 +1,10 @@
import Queue from "./Queue/Queue";
import { Box } from "@mui/material";
import React from "react";
import EpisodeListing from "./Listing/EpisodeListing";
import EpisodeListing from "../Listing/EpisodeListing";
const Bottom: React.FC = () => {
return <Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<EpisodeListing />
return <Box sx={{ display: 'grid', gridTemplateColumns: '1fr' }}>
<Queue />
</Box>
}

View file

@ -1,57 +0,0 @@
import { Accordion, AccordionSummary, AccordionDetails, Box, List, ListItem, Typography } from "@mui/material";
import { ExpandMore } from '@mui/icons-material'
import React from "react";
import useStore from "../../../../hooks/useStore";
import { Episode } from "../../../../../../../@types/messageHandler";
const EpisodeListing: React.FC = () => {
const [store] = useStore();
const [expended, setExpended] = React.useState('');
const [data, setData] = React.useState<{
[seasonHeader: string]: Episode[]
}>({});
React.useEffect(() => {
const map: {
[seasonHeader: string]: Episode[]
} = {};
store.episodeListing.forEach(item => {
const title = `S${item.season} - ${item.seasonTitle}`;
if (Object.prototype.hasOwnProperty.call(map, title)) {
map[title].push(item);
} else {
map[title] = [ item ];
}
})
setData(map);
}, [store]);
return <Box>
{Object.entries(data).map(([key, items], index) => {
return <Accordion key={`Season_${index}`} expanded={expended === key} onChange={() => setExpended(key === expended ? '' : key)}>
<AccordionSummary
expandIcon={<ExpandMore />}
aria-controls="panel1bh-content"
id="panel1bh-header"
>
<Typography sx={{ width: '80%', flexShrink: 0 }}>
{key}
</Typography>
</AccordionSummary>
<AccordionDetails>
{items.map((item, index) => {
return <Typography key={`Season_Item_${index}`} sx={{ paddingBottom: 1 }}>
{`[${item.e}] - ${item.name} ( ${item.lang.join(', ')} ) `}
</Typography>
})}
</AccordionDetails>
</Accordion>;
})}
</Box>
}
export default EpisodeListing;

View file

@ -1,5 +1,5 @@
import React from "react";
import { Box, Button, Checkbox, Chip, FormControl, FormControlLabel, IconButton, InputLabel, MenuItem, OutlinedInput, Select, TextField } from "@mui/material";
import { Backdrop, Box, Button, Checkbox, Chip, FormControl, FormControlLabel, IconButton, InputLabel, MenuItem, OutlinedInput, Select, TextField } from "@mui/material";
import useStore from "../../../hooks/useStore";
import MultiSelect from "../../reusable/MultiSelect";
import { messageChannelContext } from "../../../provider/MessageChannel";
@ -15,13 +15,20 @@ const DownloadSelector: React.FC = () => {
React.useEffect(() => {
(async () => {
/* If we don't wait the response is undefined? */
await new Promise((resolve) => setTimeout(() => resolve(undefined), 100));
const dubLang = messageHandler?.handleDefault('dubLang');
const q = messageHandler?.handleDefault('q');
const fileName = messageHandler?.handleDefault('fileName');
const result = await Promise.all([dubLang, q, fileName]);
console.log(result);
dispatch({
type: 'downloadOptions',
payload: {
...store.downloadOptions,
dubLang: await messageHandler?.handleDefault('dubLang'),
q: await messageHandler?.handleDefault('q'),
fileName: await messageHandler?.handleDefault('fileName')
dubLang: result[0],
q: result[1],
fileName: result[2]
}
});
setAvailableDubs(await messageHandler?.availableDubCodes() ?? []);
@ -69,8 +76,6 @@ const DownloadSelector: React.FC = () => {
setLoading(false);
}
console.log(store.queue, store.currentDownload);
return <Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Box sx={{ m: 2, gap: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', flexWrap: 'wrap' }}>
<TextField value={store.downloadOptions.id} required onChange={e => {

View file

@ -0,0 +1,143 @@
import { Accordion, AccordionSummary, AccordionDetails, Box, List, ListItem, Typography, Backdrop, Divider, Container, Dialog, Select, MenuItem, FormControl, InputLabel } from "@mui/material";
import { ExpandMore } from '@mui/icons-material'
import React from "react";
import useStore from "../../../hooks/useStore";
import { Episode } from "../../../../../../@types/messageHandler";
const EpisodeListing: React.FC = () => {
const [store, dispatch] = useStore();
const [season, setSeason] = React.useState<'all'|string>('all');
const seasons = React.useMemo(() => {
const s: string[] = [];
for (const {season} of store.episodeListing) {
if (s.includes(season))
continue;
s.push(season);
}
return s;
}, [ store.episodeListing ])
const [selected, setSelected] = React.useState<string[]>([]);
React.useEffect(() => {
setSelected(parseSelect(store.downloadOptions.e));
}, [ store.episodeListing ])
const close = () => {
dispatch({
type: 'episodeListing',
payload: []
});
dispatch({
type: 'downloadOptions',
payload: {
...store.downloadOptions,
e: `${([...new Set([...parseSelect(store.downloadOptions.e), ...selected])]).join(',')}`
}
})
}
return <Dialog open={store.episodeListing.length > 0} onClose={close} scroll='paper' maxWidth='xl' sx={{ p: 2 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 200px 20px' }}>
<Typography color='text.primary' variant="h5" sx={{ textAlign: 'center', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
Episodes
</Typography>
<FormControl sx={{ mr: 2, mt: 2 }}>
<InputLabel id='seasonSelectLabel'>Season</InputLabel>
<Select labelId="seasonSelectLabel" label='Season' value={season} onChange={(e) => setSeason(e.target.value)}>
<MenuItem value='all'>Show all Epsiodes</MenuItem>
{seasons.map((a, index) => {
return <MenuItem value={a} key={`MenuItem_SeasonSelect_${index}`}>
{a}
</MenuItem>
})}
</Select>
</FormControl>
</Box>
<List>
{store.episodeListing.filter((a) => season === 'all' ? true : a.season === season).map((item, index, { length }) => {
const e = isNaN(parseInt(item.e)) ? item.e : parseInt(item.e);
const isSelected = selected.includes(e.toString());
return <Box key={`Episode_List_Item_${index}`} sx={{
backdropFilter: isSelected ? 'brightness(1.5)' : '',
'&:hover': {
backdropFilter: 'brightness(1.5)'
}
}}
onClick={() => {
let arr: string[] = [];
if (isSelected) {
arr = [...selected.filter(a => a !== e)];
} else {
arr = [...selected, e.toString()];
}
setSelected(arr);
}}>
<ListItem sx={{ display: 'grid', gridTemplateColumns: '50px 1fr 5fr' }}>
<Typography color='text.primary' sx={{ textAlign: 'center' }}>
{e}
</Typography>
<img style={{ width: 'inherit', maxHeight: '200px' }} src={item.img}></img>
<Box sx={{ display: 'flex', flexDirection: 'column', pl: 1 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr min-content' }}>
<Typography color='text.primary' variant="h5">
{item.name}
</Typography>
<Typography color='text.primary'>
{item.time.startsWith('00:') ? item.time.slice(3) : item.time}
</Typography>
</Box>
<Typography color='text.primary'>
{item.description}
</Typography>
</Box>
</ListItem>
{index < length - 1 && <Divider />}
</Box>
})}
</List>
</Dialog>
}
const parseSelect = (s: string): string[] => {
const ret: string[] = [];
s.split(',').forEach(item => {
if (item.includes('-')) {
let split = item.split('-');
if (split.length !== 2)
return;
const match = split[0].match(/[A-Za-z]+/);
if (match && match.length > 0) {
if (match.index && match.index !== 0) {
return;
}
const letters = split[0].substring(0, match[0].length);
const number = parseInt(split[0].substring(match[0].length));
const b = parseInt(split[1]);
if (isNaN(number) || isNaN(b)) {
return;
}
for (let i = number; i <= b; i++) {
ret.push(`${letters}${i}`);
}
} else {
const a = parseInt(split[0]);
const b = parseInt(split[1]);
if (isNaN(a) || isNaN(b)) {
return;
}
for (let i = a; i <= b; i++) {
ret.push(`${i}`);
}
}
} else {
ret.push(item);
}
})
return [...new Set(ret)];
}
export default EpisodeListing;

View file

@ -2,6 +2,7 @@ import { Box, Divider } from "@mui/material";
import React from "react";
import Bottom from "./Bottom/Bottom";
import DownloadSelector from "./DownloadSelector/DownloadSelector";
import EpisodeListing from "./Listing/EpisodeListing";
import './MainFrame.css';
import Progress from "./Progress/Progress";
import SearchBox from "./SearchBox/SearchBox";
@ -18,6 +19,7 @@ const MainFrame: React.FC = () => {
<Box sx={{ marginLeft: 1 }}>
<Progress />
</Box>
<EpisodeListing />
</Box>
}

View file

@ -9,7 +9,7 @@ import useDownloadManager from "../DownloadManager/DownloadManager";
const Progress: React.FC = () => {
const data = useDownloadManager();
return data ? <Box sx={{ display: 'grid', gridTemplateRows: '1fr 2fr', height: '100%' }}>
return data ? <Box sx={{ display: 'grid', gridTemplateRows: '1fr 2fr', height: 'auto' }}>
<img style={{ maxWidth: '100%', maxHeight: '100%', border: '2px solid white', padding: 8 }} src={data.downloadInfo.image}></img>
<Box sx={{ display: 'grid', gridTemplateRows: '1ft fit-content', gap: 1 }}>
<table>

View file

@ -1,9 +1,11 @@
import React from "react";
import { Box, Divider, List, ListItem, Paper, TextField, Typography } from "@mui/material";
import { Box, ClickAwayListener, Divider, List, ListItem, Paper, TextField, Typography } from "@mui/material";
import { SearchResponse } from "../../../../../../@types/messageHandler";
import useStore from "../../../hooks/useStore";
import { messageChannelContext } from "../../../provider/MessageChannel";
import './SearchBox.css';
import ContextMenu from "../../reusable/ContextMenu";
import { useSnackbar } from "notistack";
const SearchBox: React.FC = () => {
const messageHandler = React.useContext(messageChannelContext);
@ -15,6 +17,8 @@ const SearchBox: React.FC = () => {
const [searchResult, setSearchResult] = React.useState<undefined|SearchResponse>();
const anchor = React.useRef<HTMLDivElement>(null);
const { enqueueSnackbar } = useSnackbar();
const selectItem = (id: string) => {
console.log(id);
dispatch({
@ -39,44 +43,55 @@ const SearchBox: React.FC = () => {
const anchorBounding = anchor.current?.getBoundingClientRect();
return <Box onBlurCapture={() => /* Delay to capture onclick */setTimeout(() => setFocus(false), 100) } onFocusCapture={() => setFocus(true)} sx={{ m: 2 }}>
<TextField ref={anchor} value={search} onChange={e => setSearch(e.target.value)} variant='outlined' label='Search' fullWidth />
{searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus &&
<Paper sx={{ position: 'absolute', maxHeight: '50%', width: `${anchorBounding?.width}px`,
left: anchorBounding?.x, top: (anchorBounding?.y ?? 0) + (anchorBounding?.height ?? 0), zIndex: 99, overflowY: 'scroll'}}>
<List>
{searchResult && searchResult.isOk ?
searchResult.value.map((a, ind, arr) => {
return <Box key={a.id}>
<ListItem className='listitem-hover' onClick={() => selectItem(a.id)}>
<Box sx={{ display: 'flex' }}>
<Box sx={{ width: '20%', height: '100%', pr: 2 }}>
<img src={a.image} style={{ width: '100%', height: '100%' }}/>
return <ClickAwayListener onClickAway={() => setFocus(false)}>
<Box sx={{ m: 2 }}>
<TextField ref={anchor} value={search} onClick={() => setFocus(true)} onChange={e => setSearch(e.target.value)} variant='outlined' label='Search' fullWidth />
{searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus &&
<Paper sx={{ position: 'absolute', maxHeight: '50%', width: `${anchorBounding?.width}px`,
left: anchorBounding?.x, top: (anchorBounding?.y ?? 0) + (anchorBounding?.height ?? 0), zIndex: 99, overflowY: 'scroll'}}>
<List>
{searchResult && searchResult.isOk ?
searchResult.value.map((a, ind, arr) => {
const imageRef = React.createRef<HTMLImageElement>();
return <Box key={a.id}>
<ListItem className='listitem-hover' onClick={(e) => {
selectItem(a.id);
setFocus(false);
}}>
<Box sx={{ display: 'flex' }}>
<Box sx={{ width: '20%', height: '100%', pr: 2 }}>
<img ref={imageRef} src={a.image} style={{ width: '100%', height: '100%' }}/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', maxWidth: '70%' }}>
<Typography variant='h6' component='h6' color='text.primary' sx={{ }}>
{a.name}
</Typography>
{a.desc && <Typography variant='caption' component='p' color='text.primary' sx={{ pt: 1, pb: 1 }}>
{a.desc}
</Typography>}
{a.lang && <Typography variant='caption' component='p' color='text.primary' sx={{ }}>
Languages: {a.lang.join(', ')}
</Typography>}
<Typography variant='caption' component='p' color='text.primary' sx={{ }}>
ID: {a.id}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', maxWidth: '70%' }}>
<Typography variant='h6' component='h6' color='text.primary' sx={{ }}>
{a.name}
</Typography>
{a.desc && <Typography variant='caption' component='p' color='text.primary' sx={{ pt: 1, pb: 1 }}>
{a.desc}
</Typography>}
{a.lang && <Typography variant='caption' component='p' color='text.primary' sx={{ }}>
Languages: {a.lang.join(', ')}
</Typography>}
<Typography variant='caption' component='p' color='text.primary' sx={{ }}>
ID: {a.id}
</Typography>
</Box>
</Box>
</ListItem>
{(ind < arr.length - 1) && <Divider />}
</Box>
})
: <></>}
</List>
</Paper>}
</Box>
</ListItem>
<ContextMenu options={[ { text: 'Copy image URL', onClick: () => {
messageHandler?.writeToClipboard(a.image);
enqueueSnackbar('Copied URL to clipboard', {
variant: 'info'
});
}} ]} popupItem={imageRef} />
{(ind < arr.length - 1) && <Divider />}
</Box>
})
: <></>}
</List>
</Paper>}
</Box>
</ClickAwayListener>
}
export default SearchBox;

View file

@ -0,0 +1,66 @@
import { StyledOptions } from "@emotion/styled";
import { Box, Button, Card, Divider, List, Typography, SxProps } from "@mui/material";
import React from "react";
export type Option = {
text: string,
onClick: () => unknown
}
export type ContextMenuProps<T extends HTMLElement> = {
options: ('divider'|Option)[],
popupItem: React.RefObject<T>
}
const buttonSx: SxProps = {
'&:hover': {
background: 'rgb(0, 30, 60)'
},
fontSize: '0.7rem',
minHeight: '30px',
justifyContent: 'center',
p: 0
};
function ContextMenu<T extends HTMLElement, >(props: ContextMenuProps<T>) {
const [anchor, setAnchor] = React.useState( { x: 0, y: 0 } );
const [show, setShow] = React.useState(false);
React.useEffect(() => {
const { popupItem: ref } = props;
if (ref.current === null)
return;
const listener = (ev: MouseEvent) => {
ev.preventDefault();
setAnchor({ x: ev.x + 10, y: ev.y + 10 });
setShow(true);
}
ref.current.addEventListener('contextmenu', listener);
return () => {
if (ref.current)
ref.current.removeEventListener('contextmenu', listener)
};
}, [ props.popupItem ])
return show ? <Box sx={{ p: 1, background: 'rgba(0, 0, 0, 0.75)', backdropFilter: 'blur(5px)', position: 'fixed', left: anchor.x, top: anchor.y }}>
<List sx={{ p: 0, m: 0 }}>
{props.options.map((item, i) => {
return item === 'divider' ? <Divider key={`ContextMenu_Divider_${i}_${item}`}/> :
<Button color='inherit' key={`ContextMenu_Value_${i}_${item}`} onClick={() => {
item.onClick();
setShow(false);
}} sx={buttonSx}>
{item.text}
</Button>
})}
<Divider />
<Button fullWidth color='inherit' onClick={() => setShow(false)} sx={buttonSx} >
Close
</Button>
</List>
</Box> : <></>
}
export default ContextMenu;

View file

@ -53,11 +53,7 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
}}
input={<OutlinedInput id="select-multiple-chip" label={props.title} />}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
selected.join(', ')
)}
MenuProps={MenuProps}
>

View file

@ -8,6 +8,7 @@ import { IconButton } from "@mui/material";
import { CloseOutlined } from "@mui/icons-material";
import { SnackbarProvider, SnackbarKey } from 'notistack';
import Store from './provider/Store';
import ErrorHandler from './provider/ErrorHandler';
const notistackRef = React.createRef<SnackbarProvider>();
const onClickDismiss = (key: SnackbarKey | undefined) => () => {
@ -17,24 +18,26 @@ const onClickDismiss = (key: SnackbarKey | undefined) => () => {
ReactDOM.render(
<React.StrictMode>
<Store>
<SnackbarProvider
ref={notistackRef}
action={(key) => (
<IconButton onClick={onClickDismiss(key)} color="inherit">
<CloseOutlined />
</IconButton>
)}
>
<Style>
<MessageChannel>
<ServiceProvider>
<App />
</ServiceProvider>
</MessageChannel>
</Style>
</SnackbarProvider>
</Store>
<ErrorHandler>
<Store>
<SnackbarProvider
ref={notistackRef}
action={(key) => (
<IconButton onClick={onClickDismiss(key)} color="inherit">
<CloseOutlined />
</IconButton>
)}
>
<Style>
<MessageChannel>
<ServiceProvider>
<App />
</ServiceProvider>
</MessageChannel>
</Style>
</SnackbarProvider>
</Store>
</ErrorHandler>
</React.StrictMode>,
document.getElementById('root')
);

View file

@ -0,0 +1,35 @@
import { Backdrop, Box, Typography } from "@mui/material";
import React from "react";
export default class ErrorHandler extends React.Component<{}, {
error?: {
er: Error,
stack: React.ErrorInfo
}
}> {
constructor(props: React.PropsWithChildren<{}>) {
super(props);
this.state = { error: undefined }
}
componentDidCatch(er: Error, stack: React.ErrorInfo) {
this.setState({ error: { er, stack } })
}
render(): React.ReactNode {
return this.state.error ?
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', p: 2 }}>
<Typography variant='body1' color='red'>
{`${this.state.error.er.name}: ${this.state.error.er.message}`}
<br/>
{this.state.error.stack.componentStack.split('\n').map(a => {
return <>
{a}
<br/>
</>
})}
</Typography>
</Box> : this.props.children;
}
}

View file

@ -4,6 +4,7 @@ import type { IpcRenderer, IpcRendererEvent } from "electron";
import useStore from '../hooks/useStore';
import type { Handler, RandomEvent, RandomEvents } from '../../../../@types/randomEvents';
import { Backdrop, Typography } from '@mui/material';
export type FrontEndMessanges = (MessageHandler & { randomEvents: RandomEventHandler });
@ -77,7 +78,8 @@ const MessageChannelProvider: React.FC = ({ children }) => {
listEpisodes: async (data) => await ipcRenderer.invoke('listEpisodes', data),
randomEvents: randomEventHandler,
downloadItem: (data) => ipcRenderer.invoke('downloadItem', data),
isDownloading: () => ipcRenderer.sendSync('isDownloading')
isDownloading: () => ipcRenderer.sendSync('isDownloading'),
writeToClipboard: async (data) => await ipcRenderer.invoke('writeToClipboard', data)
}
return <messageChannelContext.Provider value={messageHandler}>

View file

@ -55,7 +55,7 @@ const initialState: StoreState = {
q: 0,
e: '',
dubLang: [ 'jpn' ],
fileName: '[${service}] ${showTitle} - S${season}E${episode} [${height}p]',
fileName: '',
all: false,
but: false
},