diff --git a/@types/messageHandler.d.ts b/@types/messageHandler.d.ts index 9c5a514..bb574ff 100644 --- a/@types/messageHandler.d.ts +++ b/@types/messageHandler.d.ts @@ -5,7 +5,8 @@ import type { AvailableMuxer } from '../modules/module.args'; export interface MessageHandler { auth: (data: AuthData) => Promise; checkToken: () => Promise; - search: (data: SearchData) => Promise + search: (data: SearchData) => Promise, + dubLangCodes: () => Promise } export type SearchResponse = ResponseBase<{ diff --git a/gui/electron/src/messageHandler.ts b/gui/electron/src/messageHandler.ts index 5f8c545..a82bef1 100644 --- a/gui/electron/src/messageHandler.ts +++ b/gui/electron/src/messageHandler.ts @@ -16,4 +16,5 @@ export default () => { ipcMain.handle('auth', async (_, data) => handler?.auth(data)); ipcMain.handle('checkToken', async () => handler?.checkToken()); ipcMain.handle('search', async (_, data) => handler?.search(data)); + ipcMain.handle('dubLangCodes', async () => handler?.dubLangCodes()); } diff --git a/gui/electron/src/serviceHandler/funimation.ts b/gui/electron/src/serviceHandler/funimation.ts index 1684322..6049461 100644 --- a/gui/electron/src/serviceHandler/funimation.ts +++ b/gui/electron/src/serviceHandler/funimation.ts @@ -1,11 +1,16 @@ import { AuthData, CheckTokenResponse, MessageHandler, SearchData, SearchResponse } from "../../../../@types/messageHandler"; import Funimation from '../../../../funi'; +import { dubLanguageCodes } from "../../../../modules/module.langsData"; class FunimationHandler implements MessageHandler { private funi: Funimation; constructor() { this.funi = new Funimation(); } + + public async dubLangCodes(): Promise { + return dubLanguageCodes; + } public async search(data: SearchData): Promise { const funiSearch = await this.funi.searchShow(false, data); diff --git a/gui/react/package-lock.json b/gui/react/package-lock.json index 9bd1b37..1845688 100644 --- a/gui/react/package-lock.json +++ b/gui/react/package-lock.json @@ -20,6 +20,7 @@ "@types/node": "^16.11.21", "@types/react": "^17.0.38", "@types/react-dom": "^17.0.11", + "multi-downloader-nx": "file:../../", "notistack": "^2.0.3", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -28,6 +29,53 @@ "web-vitals": "^2.1.4" } }, + "../..": { + "version": "2.0.18", + "license": "MIT", + "dependencies": { + "cheerio": "^1.0.0-rc.10", + "dotenv": "^14.3.2", + "electron-squirrel-startup": "^1.0.0", + "eslint-plugin-import": "^2.25.4", + "form-data": "^4.0.0", + "fs-extra": "^10.0.0", + "got": "^11.8.3", + "hls-download": "^2.6.7", + "iso-639": "^0.2.2", + "lookpath": "^1.1.0", + "m3u8-parsed": "^1.3.0", + "sei-helper": "^3.3.0", + "typescript-eslint": "^0.0.1-alpha.0", + "yaml": "^1.10.0", + "yargs": "^17.2.1" + }, + "devDependencies": { + "@electron-forge/cli": "^6.0.0-beta.61", + "@electron-forge/maker-deb": "^6.0.0-beta.61", + "@electron-forge/maker-rpm": "^6.0.0-beta.61", + "@electron-forge/maker-squirrel": "^6.0.0-beta.61", + "@electron-forge/maker-zip": "^6.0.0-beta.61", + "@electron-forge/plugin-webpack": "^6.0.0-beta.61", + "@types/fs-extra": "^9.0.13", + "@types/node": "^16.11.9", + "@types/yargs": "^17.0.7", + "@typescript-eslint/eslint-plugin": "^4.33.0", + "@typescript-eslint/parser": "^4.33.0", + "@vercel/webpack-asset-relocator-loader": "^1.7.0", + "css-loader": "^6.5.1", + "electron": "16.0.7", + "eslint": "^7.30.0", + "eslint-plugin-import": "^2.25.4", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "node-loader": "^2.0.0", + "pkg": "^5.4.1", + "removeNPMAbsolutePaths": "^2.0.0", + "style-loader": "^3.3.1", + "ts-loader": "^9.2.6", + "ts-node": "^10.4.0", + "typescript": "^4.5.5" + } + }, "node_modules/@babel/code-frame": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", @@ -11807,6 +11855,10 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/multi-downloader-nx": { + "resolved": "../..", + "link": true + }, "node_modules/multicast-dns": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", @@ -25460,6 +25512,49 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "multi-downloader-nx": { + "version": "file:../..", + "requires": { + "@electron-forge/cli": "^6.0.0-beta.61", + "@electron-forge/maker-deb": "^6.0.0-beta.61", + "@electron-forge/maker-rpm": "^6.0.0-beta.61", + "@electron-forge/maker-squirrel": "^6.0.0-beta.61", + "@electron-forge/maker-zip": "^6.0.0-beta.61", + "@electron-forge/plugin-webpack": "^6.0.0-beta.61", + "@types/fs-extra": "^9.0.13", + "@types/node": "^16.11.9", + "@types/yargs": "^17.0.7", + "@typescript-eslint/eslint-plugin": "^4.33.0", + "@typescript-eslint/parser": "^4.33.0", + "@vercel/webpack-asset-relocator-loader": "^1.7.0", + "cheerio": "^1.0.0-rc.10", + "css-loader": "^6.5.1", + "dotenv": "^14.3.2", + "electron": "16.0.7", + "electron-squirrel-startup": "^1.0.0", + "eslint": "^7.30.0", + "eslint-plugin-import": "^2.25.4", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "form-data": "^4.0.0", + "fs-extra": "^10.0.0", + "got": "^11.8.3", + "hls-download": "^2.6.7", + "iso-639": "^0.2.2", + "lookpath": "^1.1.0", + "m3u8-parsed": "^1.3.0", + "node-loader": "^2.0.0", + "pkg": "^5.4.1", + "removeNPMAbsolutePaths": "^2.0.0", + "sei-helper": "^3.3.0", + "style-loader": "^3.3.1", + "ts-loader": "^9.2.6", + "ts-node": "^10.4.0", + "typescript": "^4.5.5", + "typescript-eslint": "^0.0.1-alpha.0", + "yaml": "^1.10.0", + "yargs": "^17.2.1" + } + }, "multicast-dns": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", diff --git a/gui/react/src/components/MainFrame/DownloadSelector/DownloadSelector.tsx b/gui/react/src/components/MainFrame/DownloadSelector/DownloadSelector.tsx new file mode 100644 index 0000000..7d5e3d2 --- /dev/null +++ b/gui/react/src/components/MainFrame/DownloadSelector/DownloadSelector.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { Box, Button, Checkbox, Chip, FormControl, FormControlLabel, IconButton, InputLabel, MenuItem, OutlinedInput, Select, TextField } from "@mui/material"; +import useStore from "../../../hooks/useStore"; +import MultiSelect from "../../MultiSelect"; +import { messageChannelContext } from "../../../provider/MessageChannel"; +import { Check, Close } from "@mui/icons-material"; + +const DownloadSelector: React.FC = () => { + const messageHandler = React.useContext(messageChannelContext); + const [store, dispatch] = useStore(); + const [dubLangCodes, setDubLangCodes] = React.useState([]); + + React.useEffect(() => { + (async () => { + const codes = await messageHandler?.dubLangCodes(); + setDubLangCodes(codes ?? []); + })(); + }, []); + + return + + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, id: e.target.value } + }) + }} label='Item ID' /> + { + const parsed = parseInt(e.target.value); + if (isNaN(parsed) || parsed < 0 || parsed > 10) + return; + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, q: parsed } + }) + }} label='Quality Level (0 for max)' /> + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, e: e.target.value } + }) + }} label='Episode Select' /> + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, dubLang: e } + }); + }} + /> + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, fileName: e.target.value } + }) + }} sx={{ width: '50%' }} label='Filename' /> + + + + + + + +}; + +export default DownloadSelector; \ 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 641a51a..c6e2877 100644 --- a/gui/react/src/components/MainFrame/MainFrame.tsx +++ b/gui/react/src/components/MainFrame/MainFrame.tsx @@ -1,12 +1,14 @@ import { Box, Divider } from "@mui/material"; import React from "react"; +import DownloadSelector from "./DownloadSelector/DownloadSelector"; import './MainFrame.css'; -import SearchBox from "./SearchBox"; +import SearchBox from "./SearchBox/SearchBox"; const MainFrame: React.FC = () => { return - Text + Options + } diff --git a/gui/react/src/components/MainFrame/SearchBox.css b/gui/react/src/components/MainFrame/SearchBox/SearchBox.css similarity index 100% rename from gui/react/src/components/MainFrame/SearchBox.css rename to gui/react/src/components/MainFrame/SearchBox/SearchBox.css diff --git a/gui/react/src/components/MainFrame/SearchBox.tsx b/gui/react/src/components/MainFrame/SearchBox/SearchBox.tsx similarity index 76% rename from gui/react/src/components/MainFrame/SearchBox.tsx rename to gui/react/src/components/MainFrame/SearchBox/SearchBox.tsx index b11fb81..9f72938 100644 --- a/gui/react/src/components/MainFrame/SearchBox.tsx +++ b/gui/react/src/components/MainFrame/SearchBox/SearchBox.tsx @@ -1,21 +1,29 @@ -import { Image } from "@mui/icons-material"; -import { Box, Button, Divider, List, ListItem, ListItemText, Paper, Popover, TextField, Typography } from "@mui/material"; import React from "react"; -import { SearchResponse } from "../../../../../@types/messageHandler"; -import { messageChannelContext } from "../../provider/MessageChannel"; -import Require from "../Require"; +import { Box, 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'; const SearchBox: React.FC = () => { const messageHandler = React.useContext(messageChannelContext); - + const [store, dispatch] = useStore(); const [search, setSearch] = React.useState(''); + const [focus, setFocus] = React.useState(false); + const [searchResult, setSearchResult] = React.useState(); const anchor = React.useRef(null); const selectItem = (id: string) => { - window.alert(id); //TODO change + console.log(id); + dispatch({ + type: 'downloadOptions', + payload: { + ...store.downloadOptions, + id + } + }); }; React.useEffect(() => { @@ -31,9 +39,10 @@ const SearchBox: React.FC = () => { const anchorBounding = anchor.current?.getBoundingClientRect(); - return - setSearch(e.target.value)} variant='outlined' label='Search' sx={{ width: 'calc(100% - 50px)', left: 25 }} /> - {searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && + + 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 && diff --git a/gui/react/src/components/MultiSelect.tsx b/gui/react/src/components/MultiSelect.tsx new file mode 100644 index 0000000..31ef88d --- /dev/null +++ b/gui/react/src/components/MultiSelect.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { Box, Chip, FormControl, InputLabel, MenuItem, OutlinedInput, Select, Theme, useTheme } from "@mui/material"; + +export type MultiSelectProps = { + values: string[], + selected: string[], + onChange: (values: string[]) => unknown, + title: string +} + +const ITEM_HEIGHT = 48; +const ITEM_PADDING_TOP = 8; +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250 + } + } +}; + +function getStyles(name: string, personName: readonly string[], theme: Theme) { + return { + fontWeight: + personName.indexOf(name) === -1 + ? theme.typography.fontWeightRegular + : theme.typography.fontWeightMedium + }; +} + +const MultiSelect: React.FC = (props) => { + const theme = useTheme(); + + return
+ + {props.title} + + +
+} + +export default MultiSelect; \ No newline at end of file diff --git a/gui/react/src/hooks/useStore.tsx b/gui/react/src/hooks/useStore.tsx new file mode 100644 index 0000000..77d55bd --- /dev/null +++ b/gui/react/src/hooks/useStore.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { StoreAction, StoreContext, StoreState } from "../provider/Store"; + +const useStore = () => { + const context = React.useContext(StoreContext as unknown as React.Context<[StoreState, React.Dispatch>]>); + if (!context) { + throw new Error('useStore must be used under Store'); + } + return context; +} + +export default useStore; \ No newline at end of file diff --git a/gui/react/src/index.tsx b/gui/react/src/index.tsx index 4d923ce..ada18c3 100644 --- a/gui/react/src/index.tsx +++ b/gui/react/src/index.tsx @@ -7,6 +7,7 @@ import MessageChannel from './provider/MessageChannel'; import { IconButton } from "@mui/material"; import { CloseOutlined } from "@mui/icons-material"; import { SnackbarProvider, SnackbarKey } from 'notistack'; +import Store from './provider/Store'; const notistackRef = React.createRef(); const onClickDismiss = (key: SnackbarKey | undefined) => () => { @@ -16,22 +17,24 @@ const onClickDismiss = (key: SnackbarKey | undefined) => () => { ReactDOM.render( - ( - - - - )} - > - - + + ( + + + + )} + > + + + , document.getElementById('root') ); \ No newline at end of file diff --git a/gui/react/src/provider/MessageChannel.tsx b/gui/react/src/provider/MessageChannel.tsx index 332724d..8da6c8f 100644 --- a/gui/react/src/provider/MessageChannel.tsx +++ b/gui/react/src/provider/MessageChannel.tsx @@ -18,7 +18,8 @@ const MessageChannelProvider: React.FC = ({ children }) => { const messageHandler: MessageHandler = { auth: async (data) => await ipcRenderer.invoke('auth', data), checkToken: async () => await ipcRenderer.invoke('checkToken'), - search: async (data) => await ipcRenderer.invoke('search', data) + search: async (data) => await ipcRenderer.invoke('search', data), + dubLangCodes: async () => await ipcRenderer.invoke('dubLangCodes') } return diff --git a/gui/react/src/provider/Store.tsx b/gui/react/src/provider/Store.tsx new file mode 100644 index 0000000..80a1936 --- /dev/null +++ b/gui/react/src/provider/Store.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { dubLanguageCodes } from '../../../../modules/module.langsData'; + +export type QueueItem = { + +} + +export type DownloadOptions = { + q: number, + id: string, + e: string, + dubLang: typeof dubLanguageCodes, + fileName: string, + all: boolean, + but: boolean +} + +export type StoreState = { + queue: QueueItem[], + downloadOptions: DownloadOptions +} + +export type StoreAction = { + type: T, + payload: StoreState[T] +} + +const Reducer = (state: StoreState, action: StoreAction): StoreState => { + switch(action.type) { + case "queue": + return { ...state, queue: state.queue.concat(action.payload) }; + case "downloadOptions": + return { ...state, downloadOptions: action.payload as DownloadOptions }; + } + return state; +}; + +const initialState: StoreState = { + queue: [], + downloadOptions: { + id: '', + q: 0, + e: '', + dubLang: [ 'jpn' ], + fileName: '[${service}] ${showTitle} - S${season}E${episode} [${height}p]', + all: false, + but: false + } +}; + +const Store: React.FC = ({children}) => { + const [state, dispatch] = React.useReducer(Reducer, initialState); + /*React.useEffect(() => { + if (!state.unsavedChanges.has) + return; + const unsavedChanges = (ev: BeforeUnloadEvent, lang: LanguageContextType) => { + ev.preventDefault(); + ev.returnValue = lang.getLang('unsaved_changes'); + return lang.getLang('unsaved_changes'); + }; + + + const windowListener = (ev: BeforeUnloadEvent) => { + return unsavedChanges(ev, state.lang); + }; + + window.addEventListener('beforeunload', windowListener); + + return () => window.removeEventListener('beforeunload', windowListener); + }, [state.unsavedChanges.has]);*/ + + return ( + + {children} + + ); +}; + +/* Importent Notice -- The 'queue' generic will be overriden */ +export const StoreContext = React.createContext<[StoreState, React.Dispatch>]>([initialState, undefined as any]); +export default Store; \ No newline at end of file diff --git a/tsc.ts b/tsc.ts index 4a1f8f3..00751e4 100644 --- a/tsc.ts +++ b/tsc.ts @@ -6,7 +6,9 @@ import { removeSync, copyFileSync } from 'fs-extra'; const argv = process.argv.slice(2); let buildIgnore: string[] = []; -if (argv.length > 0 && argv[0] !== 'test') +const isTest = !(argv.length > 0 && argv[0] !== 'test'); + +if (!isTest) buildIgnore = [ '*/\\.env' ]; @@ -47,15 +49,17 @@ export { ignore }; const tsc = exec('npx tsc'); await waitForProcess(tsc); - - process.stdout.write('✓\nBuilding react... '); - const react = exec('npm run build', { - cwd: path.join(__dirname, 'gui', 'react'), - }); - - await waitForProcess(react); + + if (!isTest) { + process.stdout.write('✓\nBuilding react... '); + const react = exec('npm run build', { + cwd: path.join(__dirname, 'gui', 'react'), + }); + + await waitForProcess(react); + } + process.stdout.write('✓\nCopying files... '); - copyDir(path.join(__dirname, 'gui', 'react', 'build'), path.join(__dirname, 'lib', 'gui', 'electron', 'build')); const files = readDir(__dirname);