Download options

This commit is contained in:
Izuco 2022-01-28 23:50:39 +01:00
parent 71f7eb2a69
commit df764a8620
No known key found for this signature in database
GPG key ID: E9CBE9E4EF3A1BFA
14 changed files with 389 additions and 39 deletions

View file

@ -5,7 +5,8 @@ import type { AvailableMuxer } from '../modules/module.args';
export interface MessageHandler {
auth: (data: AuthData) => Promise<AuthResponse>;
checkToken: () => Promise<CheckTokenResponse>;
search: (data: SearchData) => Promise<SearchResponse>
search: (data: SearchData) => Promise<SearchResponse>,
dubLangCodes: () => Promise<string[]>
}
export type SearchResponse = ResponseBase<{

View file

@ -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());
}

View file

@ -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<string[]> {
return dubLanguageCodes;
}
public async search(data: SearchData): Promise<SearchResponse> {
const funiSearch = await this.funi.searchShow(false, data);

View file

@ -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",

View file

@ -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<string[]>([]);
React.useEffect(() => {
(async () => {
const codes = await messageHandler?.dubLangCodes();
setDubLangCodes(codes ?? []);
})();
}, []);
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 => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, id: e.target.value }
})
}} label='Item ID' />
<TextField type='number' value={store.downloadOptions.q} required onChange={e => {
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)' />
<TextField disabled={store.downloadOptions.all} value={store.downloadOptions.e} required onChange={e => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, e: e.target.value }
})
}} label='Episode Select' />
<MultiSelect
title='Dub Languages'
values={dubLangCodes}
selected={store.downloadOptions.dubLang}
onChange={(e) => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, dubLang: e }
});
}}
/>
<TextField value={store.downloadOptions.fileName} onChange={e => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, fileName: e.target.value }
})
}} sx={{ width: '50%' }} label='Filename' />
<Button onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, all: !store.downloadOptions.all } })} variant={store.downloadOptions.all ? 'contained' : 'outlined'}>Download all</Button>
<Button onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, but: !store.downloadOptions.but } })} variant={store.downloadOptions.but ? 'contained' : 'outlined'}>Download all but</Button>
</Box>
<Box sx={{ flex: 0, m: 1, mb: 3, display: 'flex', justifyContent: 'center' }}>
<Button variant='contained'>Add to Queue</Button>
</Box>
</Box>
};
export default DownloadSelector;

View file

@ -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 <Box sx={{ border: '2px solid white', width: '75%' }}>
<SearchBox />
<Divider className="divider-width" light sx={{ color: 'text.primary'}}>Text</Divider>
<Divider variant='middle' className="divider-width" light sx={{ color: 'text.primary', fontSize: '1.2rem' }}>Options</Divider>
<DownloadSelector />
</Box>
}

View file

@ -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<undefined|SearchResponse>();
const anchor = React.useRef<HTMLDivElement>(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 <Box sx={{ mt: 2, mb: 2 }}>
<TextField ref={anchor} value={search} onChange={e => setSearch(e.target.value)} variant='outlined' label='Search' sx={{ width: 'calc(100% - 50px)', left: 25 }} />
{searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 &&
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>

View file

@ -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<MultiSelectProps> = (props) => {
const theme = useTheme();
return <div>
<FormControl sx={{ m: 1, width: 300 }}>
<InputLabel id="multi-select-label">{props.title}</InputLabel>
<Select
labelId="multi-select-label"
id="multi-select"
multiple
value={props.selected}
onChange={e => props.onChange(typeof e.target.value === "string" ? e.target.value.split(",") : e.target.value)}
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>
)}
MenuProps={MenuProps}
>
{props.values.map((name) => (
<MenuItem
key={name}
value={name}
style={getStyles(name, props.selected, theme)}
>
{name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
}
export default MultiSelect;

View file

@ -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<StoreAction<keyof StoreState>>]>);
if (!context) {
throw new Error('useStore must be used under Store');
}
return context;
}
export default useStore;

View file

@ -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<SnackbarProvider>();
const onClickDismiss = (key: SnackbarKey | undefined) => () => {
@ -16,22 +17,24 @@ const onClickDismiss = (key: SnackbarKey | undefined) => () => {
ReactDOM.render(
<React.StrictMode>
<SnackbarProvider
ref={notistackRef}
action={(key) => (
<IconButton onClick={onClickDismiss(key)} color="inherit">
<CloseOutlined />
</IconButton>
)}
>
<Style>
<ServiceProvider>
<MessageChannel>
<App />
</MessageChannel>
</ServiceProvider>
</Style>
</SnackbarProvider>
<Store>
<SnackbarProvider
ref={notistackRef}
action={(key) => (
<IconButton onClick={onClickDismiss(key)} color="inherit">
<CloseOutlined />
</IconButton>
)}
>
<Style>
<ServiceProvider>
<MessageChannel>
<App />
</MessageChannel>
</ServiceProvider>
</Style>
</SnackbarProvider>
</Store>
</React.StrictMode>,
document.getElementById('root')
);

View file

@ -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 <messageChannelContext.Provider value={messageHandler}>

View file

@ -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<T extends keyof StoreState> = {
type: T,
payload: StoreState[T]
}
const Reducer = <T extends keyof StoreState,>(state: StoreState, action: StoreAction<T>): 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 (
<StoreContext.Provider value={[state, dispatch]}>
{children}
</StoreContext.Provider>
);
};
/* Importent Notice -- The 'queue' generic will be overriden */
export const StoreContext = React.createContext<[StoreState, React.Dispatch<StoreAction<'queue'>>]>([initialState, undefined as any]);
export default Store;

22
tsc.ts
View file

@ -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);