From e9852ade1a93e80664fc0752611156af7ebfd034 Mon Sep 17 00:00:00 2001 From: Izuco Date: Sat, 12 Feb 2022 16:52:36 +0100 Subject: [PATCH] See todo for changes --- @types/messageHandler.d.ts | 8 +- TODO.md | 10 +- crunchy.ts | 7 +- gui/electron/src/index.ts | 35 +---- gui/electron/src/menu.ts | 81 ++++++++++ gui/electron/src/messageHandler.ts | 1 + gui/electron/src/serviceHandler/base.ts | 7 +- gui/electron/src/serviceHandler/funimation.ts | 9 +- gui/react/public/index.html | 1 - gui/react/src/components/AuthButton.tsx | 2 +- .../components/MainFrame/Bottom/Bottom.tsx | 5 +- .../Bottom/Listing/EpisodeListing.tsx | 57 ------- .../DownloadSelector/DownloadSelector.tsx | 17 ++- .../MainFrame/Listing/EpisodeListing.tsx | 143 ++++++++++++++++++ .../src/components/MainFrame/MainFrame.tsx | 2 + .../MainFrame/Progress/Progress.tsx | 2 +- .../MainFrame/SearchBox/SearchBox.tsx | 91 ++++++----- .../src/components/reusable/ContextMenu.tsx | 66 ++++++++ .../src/components/reusable/MultiSelect.tsx | 6 +- gui/react/src/index.tsx | 39 ++--- gui/react/src/provider/ErrorHandler.tsx | 35 +++++ gui/react/src/provider/MessageChannel.tsx | 4 +- gui/react/src/provider/Store.tsx | 2 +- 23 files changed, 461 insertions(+), 169 deletions(-) create mode 100644 gui/electron/src/menu.ts delete mode 100644 gui/react/src/components/MainFrame/Bottom/Listing/EpisodeListing.tsx create mode 100644 gui/react/src/components/MainFrame/Listing/EpisodeListing.tsx create mode 100644 gui/react/src/components/reusable/ContextMenu.tsx create mode 100644 gui/react/src/provider/ErrorHandler.tsx diff --git a/@types/messageHandler.d.ts b/@types/messageHandler.d.ts index 4049c82..bcca4b2 100644 --- a/@types/messageHandler.d.ts +++ b/@types/messageHandler.d.ts @@ -11,7 +11,8 @@ export interface MessageHandler { resolveItems: (data: ResolveItemsData) => Promise>, listEpisodes: (id: string) => Promise, 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 diff --git a/TODO.md b/TODO.md index 40b8a58..e323db0 100644 --- a/TODO.md +++ b/TODO.md @@ -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 \ No newline at end of file +- [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 \ No newline at end of file diff --git a/crunchy.ts b/crunchy.ts index 1bc4992..c07b916 100644 --- a/crunchy.ts +++ b/crunchy.ts @@ -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}` } })}; } diff --git a/gui/electron/src/index.ts b/gui/electron/src/index.ts index b2ef857..fdc0926 100644 --- a/gui/electron/src/index.ts +++ b/gui/electron/src/index.ts @@ -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); diff --git a/gui/electron/src/menu.ts b/gui/electron/src/menu.ts new file mode 100644 index 0000000..6bbc5ea --- /dev/null +++ b/gui/electron/src/menu.ts @@ -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)); \ No newline at end of file diff --git a/gui/electron/src/messageHandler.ts b/gui/electron/src/messageHandler.ts index ff22d96..7c15c8d 100644 --- a/gui/electron/src/messageHandler.ts +++ b/gui/electron/src/messageHandler.ts @@ -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()); }; diff --git a/gui/electron/src/serviceHandler/base.ts b/gui/electron/src/serviceHandler/base.ts index 3c95fcf..7f4b799 100644 --- a/gui/electron/src/serviceHandler/base.ts +++ b/gui/electron/src/serviceHandler/base.ts @@ -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; + } + } \ No newline at end of file diff --git a/gui/electron/src/serviceHandler/funimation.ts b/gui/electron/src/serviceHandler/funimation.ts index c922b89..6f49014 100644 --- a/gui/electron/src/serviceHandler/funimation.ts +++ b/gui/electron/src/serviceHandler/funimation.ts @@ -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 })) } } diff --git a/gui/react/public/index.html b/gui/react/public/index.html index 2806239..92cc7d8 100644 --- a/gui/react/public/index.html +++ b/gui/react/public/index.html @@ -2,7 +2,6 @@ - Hello World! { setAuthed((await messageChannel?.checkToken())?.isOk ?? false); } - React.useEffect(() => { checkAuth(); return () => {}; }, []); + React.useEffect(() => { checkAuth() }, []); const handleSubmit = async () => { if (!messageChannel) diff --git a/gui/react/src/components/MainFrame/Bottom/Bottom.tsx b/gui/react/src/components/MainFrame/Bottom/Bottom.tsx index 700f9df..991ee14 100644 --- a/gui/react/src/components/MainFrame/Bottom/Bottom.tsx +++ b/gui/react/src/components/MainFrame/Bottom/Bottom.tsx @@ -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 - + return } diff --git a/gui/react/src/components/MainFrame/Bottom/Listing/EpisodeListing.tsx b/gui/react/src/components/MainFrame/Bottom/Listing/EpisodeListing.tsx deleted file mode 100644 index cf3be1d..0000000 --- a/gui/react/src/components/MainFrame/Bottom/Listing/EpisodeListing.tsx +++ /dev/null @@ -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 - {Object.entries(data).map(([key, items], index) => { - return setExpended(key === expended ? '' : key)}> - } - aria-controls="panel1bh-content" - id="panel1bh-header" - > - - {key} - - - - {items.map((item, index) => { - return - {`[${item.e}] - ${item.name} ( ${item.lang.join(', ')} ) `} - - })} - - ; - })} - -} - -export default EpisodeListing; \ No newline at end of file diff --git a/gui/react/src/components/MainFrame/DownloadSelector/DownloadSelector.tsx b/gui/react/src/components/MainFrame/DownloadSelector/DownloadSelector.tsx index 9fa138b..4fb6a1e 100644 --- a/gui/react/src/components/MainFrame/DownloadSelector/DownloadSelector.tsx +++ b/gui/react/src/components/MainFrame/DownloadSelector/DownloadSelector.tsx @@ -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 { diff --git a/gui/react/src/components/MainFrame/Listing/EpisodeListing.tsx b/gui/react/src/components/MainFrame/Listing/EpisodeListing.tsx new file mode 100644 index 0000000..24a08b8 --- /dev/null +++ b/gui/react/src/components/MainFrame/Listing/EpisodeListing.tsx @@ -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([]); + + 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 0} onClose={close} scroll='paper' maxWidth='xl' sx={{ p: 2 }}> + + + Episodes + + + Season + + + + + {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 { + let arr: string[] = []; + if (isSelected) { + arr = [...selected.filter(a => a !== e)]; + } else { + arr = [...selected, e.toString()]; + } + setSelected(arr); + }}> + + + {e} + + + + + + {item.name} + + + {item.time.startsWith('00:') ? item.time.slice(3) : item.time} + + + + {item.description} + + + + {index < length - 1 && } + + })} + + +} + +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; \ No newline at end of file diff --git a/gui/react/src/components/MainFrame/MainFrame.tsx b/gui/react/src/components/MainFrame/MainFrame.tsx index 773eccf..ca28519 100644 --- a/gui/react/src/components/MainFrame/MainFrame.tsx +++ b/gui/react/src/components/MainFrame/MainFrame.tsx @@ -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 = () => { + } diff --git a/gui/react/src/components/MainFrame/Progress/Progress.tsx b/gui/react/src/components/MainFrame/Progress/Progress.tsx index b5499e3..98ddf45 100644 --- a/gui/react/src/components/MainFrame/Progress/Progress.tsx +++ b/gui/react/src/components/MainFrame/Progress/Progress.tsx @@ -9,7 +9,7 @@ import useDownloadManager from "../DownloadManager/DownloadManager"; const Progress: React.FC = () => { const data = useDownloadManager(); - return data ? + return data ? diff --git a/gui/react/src/components/MainFrame/SearchBox/SearchBox.tsx b/gui/react/src/components/MainFrame/SearchBox/SearchBox.tsx index 6517b73..be8f1d2 100644 --- a/gui/react/src/components/MainFrame/SearchBox/SearchBox.tsx +++ b/gui/react/src/components/MainFrame/SearchBox/SearchBox.tsx @@ -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(); const anchor = React.useRef(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 /* Delay to capture onclick */setTimeout(() => setFocus(false), 100) } onFocusCapture={() => setFocus(true)} sx={{ m: 2 }}> - setSearch(e.target.value)} variant='outlined' label='Search' fullWidth /> - {searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus && - - - {searchResult && searchResult.isOk ? - searchResult.value.map((a, ind, arr) => { - return - selectItem(a.id)}> - - - + return setFocus(false)}> + + setFocus(true)} onChange={e => setSearch(e.target.value)} variant='outlined' label='Search' fullWidth /> + {searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus && + + + {searchResult && searchResult.isOk ? + searchResult.value.map((a, ind, arr) => { + const imageRef = React.createRef(); + return + { + selectItem(a.id); + setFocus(false); + }}> + + + + + + + {a.name} + + {a.desc && + {a.desc} + } + {a.lang && + Languages: {a.lang.join(', ')} + } + + ID: {a.id} + + - - - {a.name} - - {a.desc && - {a.desc} - } - {a.lang && - Languages: {a.lang.join(', ')} - } - - ID: {a.id} - - - - - {(ind < arr.length - 1) && } - - }) - : <>} - - } - + + { + messageHandler?.writeToClipboard(a.image); + enqueueSnackbar('Copied URL to clipboard', { + variant: 'info' + }); + }} ]} popupItem={imageRef} /> + {(ind < arr.length - 1) && } + + }) + : <>} + + } + + } export default SearchBox; \ No newline at end of file diff --git a/gui/react/src/components/reusable/ContextMenu.tsx b/gui/react/src/components/reusable/ContextMenu.tsx new file mode 100644 index 0000000..a1438a1 --- /dev/null +++ b/gui/react/src/components/reusable/ContextMenu.tsx @@ -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 = { + options: ('divider'|Option)[], + popupItem: React.RefObject +} + +const buttonSx: SxProps = { + '&:hover': { + background: 'rgb(0, 30, 60)' + }, + fontSize: '0.7rem', + minHeight: '30px', + justifyContent: 'center', + p: 0 +}; + +function ContextMenu(props: ContextMenuProps) { + 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 ? + + {props.options.map((item, i) => { + return item === 'divider' ? : + + })} + + + + : <> +} + +export default ContextMenu; \ No newline at end of file diff --git a/gui/react/src/components/reusable/MultiSelect.tsx b/gui/react/src/components/reusable/MultiSelect.tsx index 3e8df4c..847c513 100644 --- a/gui/react/src/components/reusable/MultiSelect.tsx +++ b/gui/react/src/components/reusable/MultiSelect.tsx @@ -53,11 +53,7 @@ const MultiSelect: React.FC = (props) => { }} input={} renderValue={(selected) => ( - - {selected.map((value) => ( - - ))} - + selected.join(', ') )} MenuProps={MenuProps} > diff --git a/gui/react/src/index.tsx b/gui/react/src/index.tsx index ac480a3..db355c8 100644 --- a/gui/react/src/index.tsx +++ b/gui/react/src/index.tsx @@ -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(); const onClickDismiss = (key: SnackbarKey | undefined) => () => { @@ -17,24 +18,26 @@ const onClickDismiss = (key: SnackbarKey | undefined) => () => { ReactDOM.render( - - ( - - - - )} - > - - - + + + ( + + + + )} + > + + + + , document.getElementById('root') ); \ No newline at end of file diff --git a/gui/react/src/provider/ErrorHandler.tsx b/gui/react/src/provider/ErrorHandler.tsx new file mode 100644 index 0000000..15c1534 --- /dev/null +++ b/gui/react/src/provider/ErrorHandler.tsx @@ -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 ? + + + {`${this.state.error.er.name}: ${this.state.error.er.message}`} +
+ {this.state.error.stack.componentStack.split('\n').map(a => { + return <> + {a} +
+ + })} +
+
: this.props.children; + } +} \ No newline at end of file diff --git a/gui/react/src/provider/MessageChannel.tsx b/gui/react/src/provider/MessageChannel.tsx index 92a0968..671bda7 100644 --- a/gui/react/src/provider/MessageChannel.tsx +++ b/gui/react/src/provider/MessageChannel.tsx @@ -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 diff --git a/gui/react/src/provider/Store.tsx b/gui/react/src/provider/Store.tsx index 9ceca51..cb78efa 100644 --- a/gui/react/src/provider/Store.tsx +++ b/gui/react/src/provider/Store.tsx @@ -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 },