yt-grabber 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +29 -0
- package/.prettierrc +19 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/settings.json +23 -0
- package/.yarnrc.yml +13 -0
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/package.json +115 -0
- package/public/index.html +20 -0
- package/public/screenshots/cutting.png +0 -0
- package/public/screenshots/downloading.png +0 -0
- package/public/screenshots/editing.png +0 -0
- package/public/screenshots/errors.png +0 -0
- package/public/screenshots/settings.png +0 -0
- package/public/screenshots/tracklist.png +0 -0
- package/src/@types/global.d.ts +7 -0
- package/src/@types/i18next-scanner-webpack.d.ts +1 -0
- package/src/@types/stylus.d.ts +4 -0
- package/src/@types/svg.d.ts +1 -0
- package/src/App.styl +24 -0
- package/src/App.tsx +31 -0
- package/src/bootstrap.tsx +30 -0
- package/src/common/CancellablePromise.ts +22 -0
- package/src/common/ComponentDisplayMode.ts +8 -0
- package/src/common/Delay.ts +3 -0
- package/src/common/FileSystem.ts +171 -0
- package/src/common/Helpers.ts +270 -0
- package/src/common/Mappings.ts +14 -0
- package/src/common/PuppeteerOptions.ts +45 -0
- package/src/common/Selectors.ts +21 -0
- package/src/common/Store.ts +108 -0
- package/src/common/Theme.ts +4 -0
- package/src/common/Youtube.ts +80 -0
- package/src/components/appBar/AppBar.styl +22 -0
- package/src/components/appBar/AppBar.tsx +73 -0
- package/src/components/directoryPicker/DirectoryPicker.tsx +44 -0
- package/src/components/fileField/FileField.styl +3 -0
- package/src/components/fileField/FileField.tsx +152 -0
- package/src/components/languagePicker/LanguagePicker.styl +38 -0
- package/src/components/languagePicker/LanguagePicker.tsx +145 -0
- package/src/components/logo/Logo.tsx +15 -0
- package/src/components/modals/DetailsModal.styl +9 -0
- package/src/components/modals/DetailsModal.tsx +85 -0
- package/src/components/numberField/NumberField.styl +13 -0
- package/src/components/numberField/NumberField.tsx +154 -0
- package/src/components/progress/Progress.styl +15 -0
- package/src/components/progress/Progress.tsx +18 -0
- package/src/components/splitButton/SplitButton.styl +0 -0
- package/src/components/splitButton/SplitButton.tsx +125 -0
- package/src/components/themePicker/ThemePicker.styl +19 -0
- package/src/components/themePicker/ThemePicker.tsx +65 -0
- package/src/components/themeSwitcher/ThemeSwitcher.styl +10 -0
- package/src/components/themeSwitcher/ThemeSwitcher.tsx +43 -0
- package/src/components/youtube/formatSelector/FormatSelector.styl +3 -0
- package/src/components/youtube/formatSelector/FormatSelector.tsx +202 -0
- package/src/components/youtube/inputPanel/InputPanel.styl +7 -0
- package/src/components/youtube/inputPanel/InputPanel.tsx +189 -0
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.styl +80 -0
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.tsx +113 -0
- package/src/components/youtube/trackList/TrackList.styl +64 -0
- package/src/components/youtube/trackList/TrackList.tsx +258 -0
- package/src/enums/DataResponse.ts +5 -0
- package/src/enums/Media.ts +16 -0
- package/src/enums/MediaFormat.ts +10 -0
- package/src/enums/MimeTypes.ts +14 -0
- package/src/hooks/useCancellablePromises.ts +25 -0
- package/src/hooks/useClickCounter.ts +24 -0
- package/src/hooks/useData.ts +61 -0
- package/src/hooks/useMultiClickHandler.ts +41 -0
- package/src/i18next.ts +33 -0
- package/src/index.ts +65 -0
- package/src/react/actions/Action.ts +3 -0
- package/src/react/actions/AppActions.ts +41 -0
- package/src/react/contexts/AppContext.tsx +51 -0
- package/src/react/contexts/AppThemeContext.tsx +38 -0
- package/src/react/contexts/DataContext copy.tsx +76 -0
- package/src/react/contexts/DataContext.tsx +41 -0
- package/src/react/hooks/useAppTheme.ts +14 -0
- package/src/react/reducers/AppReducer.tsx +45 -0
- package/src/react/reducers/Reducer.ts +7 -0
- package/src/react/states/AppState.ts +29 -0
- package/src/react/states/State.ts +29 -0
- package/src/renderer.tsx +13 -0
- package/src/resources/bin/yt-dlp.exe +0 -0
- package/src/resources/fonts/Baloo-Regular.ttf +0 -0
- package/src/resources/fonts/Lato-Black.ttf +0 -0
- package/src/resources/fonts/Lato-BlackItalic.ttf +0 -0
- package/src/resources/fonts/Lato-Bold.ttf +0 -0
- package/src/resources/fonts/Lato-BoldItalic.ttf +0 -0
- package/src/resources/fonts/Lato-Italic.ttf +0 -0
- package/src/resources/fonts/Lato-Light.ttf +0 -0
- package/src/resources/fonts/Lato-LightItalic.ttf +0 -0
- package/src/resources/fonts/Lato-Regular.ttf +0 -0
- package/src/resources/fonts/Lato-Thin.ttf +0 -0
- package/src/resources/fonts/Lato-ThinItalic.ttf +0 -0
- package/src/resources/fonts/Material-Icons.woff2 +0 -0
- package/src/resources/icons/favicon-16x16.png +0 -0
- package/src/resources/icons/favicon-32x32.png +0 -0
- package/src/resources/icons/favicon.ico +0 -0
- package/src/resources/icons/logo-shape.png +0 -0
- package/src/resources/icons/logo-shape.svg +59 -0
- package/src/resources/images/loading.svg +28 -0
- package/src/resources/images/logo.png +0 -0
- package/src/resources/locales/de-DE/flag.svg +1 -0
- package/src/resources/locales/de-DE/translation.json +44 -0
- package/src/resources/locales/en-GB/flag.svg +43 -0
- package/src/resources/locales/en-GB/translation.json +44 -0
- package/src/resources/locales/pl-PL/flag.svg +36 -0
- package/src/resources/locales/pl-PL/translation.json +44 -0
- package/src/styles/MaterialThemes.ts +331 -0
- package/src/styles/fonts.styl +71 -0
- package/src/styles/mixins.styl +22 -0
- package/src/tests/CompleteTracksMock.ts +17384 -0
- package/src/tests/MissingDetailsTracksMock.ts +7737 -0
- package/src/theme/ColorThemes.ts +190 -0
- package/src/theme/Colors.ts +92 -0
- package/src/theme/Shadows.ts +9 -0
- package/src/theme/Shape.ts +7 -0
- package/src/theme/Theme.ts +24 -0
- package/src/theme/Typography.ts +56 -0
- package/src/views/development/DevelopmentView.styl +22 -0
- package/src/views/development/DevelopmentView.tsx +57 -0
- package/src/views/home/HomeView.styl +60 -0
- package/src/views/home/HomeView.tsx +505 -0
- package/src/views/settings/SettingsView.styl +27 -0
- package/src/views/settings/SettingsView.tsx +255 -0
- package/tsconfig.json +20 -0
- package/webpack.config.ts +226 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import classnames from "classnames";
|
|
2
|
+
// import fs from "fs-extra";
|
|
3
|
+
import _first from "lodash/first";
|
|
4
|
+
import _get from "lodash/get";
|
|
5
|
+
import _isFunction from "lodash/isFunction";
|
|
6
|
+
import _join from "lodash/join";
|
|
7
|
+
import _union from "lodash/union";
|
|
8
|
+
import React, {useRef} from "react";
|
|
9
|
+
|
|
10
|
+
import FolderIcon from "@mui/icons-material/Folder";
|
|
11
|
+
import {IconButton, InputAdornment, TextField, TextFieldProps} from "@mui/material";
|
|
12
|
+
|
|
13
|
+
import Styles from "./FileField.styl";
|
|
14
|
+
|
|
15
|
+
export type FileFieldProps = Omit<TextFieldProps, "onChange" | "onBlur"> & {
|
|
16
|
+
mode?: "file" | "directory";
|
|
17
|
+
value?: string;
|
|
18
|
+
multiple?: boolean;
|
|
19
|
+
fileTypes?: string[];
|
|
20
|
+
onChange?: (value: string[]) => void;
|
|
21
|
+
onBlur?: (value: string[]) => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const FileField: React.FC<FileFieldProps> = (props) => {
|
|
25
|
+
const {mode = "file", fileTypes, value, multiple, className, onChange, onBlur, ...rest} = props;
|
|
26
|
+
const rootPath = "./";
|
|
27
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
28
|
+
|
|
29
|
+
const handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
30
|
+
if (_isFunction(onChange)) {
|
|
31
|
+
onChange([event.target.value]);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
|
36
|
+
if (_isFunction(onBlur)) {
|
|
37
|
+
onBlur([event.target.value]);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleButtonClick = () => {
|
|
42
|
+
fileInputRef.current?.click();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const resolveDirectory = (files: FileList) => {
|
|
46
|
+
if (multiple) {
|
|
47
|
+
const paths = [];
|
|
48
|
+
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
paths.push(rootPath + file.webkitRelativePath.substring(0, file.webkitRelativePath.lastIndexOf("/")));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return _union(paths);
|
|
54
|
+
} else {
|
|
55
|
+
const firstFilePath = _get(_first(files), "webkitRelativePath");
|
|
56
|
+
|
|
57
|
+
return [rootPath + _first(firstFilePath.split("/"))];
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const resolveFile = (files: FileList): string[] => {
|
|
62
|
+
if (multiple) {
|
|
63
|
+
const paths = [];
|
|
64
|
+
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
paths.push(rootPath + file.webkitRelativePath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return paths;
|
|
70
|
+
} else {
|
|
71
|
+
return [rootPath + _get(_first(files), "webkitRelativePath")];
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// async function readDirectory(dirHandle: any, path: any) {
|
|
76
|
+
// for await (const entry of dirHandle.values()) {
|
|
77
|
+
// const fullPath = path ? `${path}/${entry.name}` : entry.name;
|
|
78
|
+
|
|
79
|
+
// if (entry.kind === "file") {
|
|
80
|
+
// console.log("File:", fullPath);
|
|
81
|
+
// } else if (entry.kind === "directory") {
|
|
82
|
+
// console.log("Directory:", fullPath); // Logs even empty directories
|
|
83
|
+
// await readDirectory(entry, fullPath); // Recursively read subdirectories
|
|
84
|
+
// }
|
|
85
|
+
// }
|
|
86
|
+
// }
|
|
87
|
+
|
|
88
|
+
const onSelectFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
89
|
+
// const dirHandle = await (window as any).showDirectoryPicker();
|
|
90
|
+
// await readDirectory(dirHandle, "");
|
|
91
|
+
|
|
92
|
+
const result = mode === "directory" ? resolveDirectory(event.target.files) : resolveFile(event.target.files);
|
|
93
|
+
// const outPath = fs.realpathSync(result[0]);
|
|
94
|
+
|
|
95
|
+
// setFieldValue(result);
|
|
96
|
+
// if (_isFunction(onChange)) {
|
|
97
|
+
// onChange(fs.existsSync(outPath) ? [outPath] : result);
|
|
98
|
+
// }
|
|
99
|
+
if (_isFunction(onChange)) {
|
|
100
|
+
onChange(result);
|
|
101
|
+
}
|
|
102
|
+
event.target.value = "";
|
|
103
|
+
|
|
104
|
+
// const reader = new FileReader();
|
|
105
|
+
|
|
106
|
+
// reader.onload = (e) => {
|
|
107
|
+
// const content = e.target?.result as string;
|
|
108
|
+
// const lines = content.split("\n");
|
|
109
|
+
|
|
110
|
+
// if (_isFunction(onChange)) {
|
|
111
|
+
// onChange(lines);
|
|
112
|
+
// }
|
|
113
|
+
// setFieldValue(lines);
|
|
114
|
+
// };
|
|
115
|
+
// reader.readAsText(file);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// useEffect(() => {
|
|
119
|
+
// if (_isFunction(onChange)) {
|
|
120
|
+
// onChange(fieldValue);
|
|
121
|
+
// }
|
|
122
|
+
// }, [fieldValue]);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<>
|
|
126
|
+
<TextField
|
|
127
|
+
{...rest}
|
|
128
|
+
className={classnames(Styles.fileField, className)}
|
|
129
|
+
value={value}
|
|
130
|
+
onChange={handleValueChange}
|
|
131
|
+
onBlur={handleBlur}
|
|
132
|
+
slotProps={{
|
|
133
|
+
input: {
|
|
134
|
+
endAdornment: <InputAdornment position="end">
|
|
135
|
+
<IconButton
|
|
136
|
+
onClick={handleButtonClick}
|
|
137
|
+
edge="end"
|
|
138
|
+
>
|
|
139
|
+
<FolderIcon />
|
|
140
|
+
</IconButton>
|
|
141
|
+
</InputAdornment>,
|
|
142
|
+
},
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
{/* eslint-disable react/no-unknown-property */ }
|
|
146
|
+
{/* @ts-expect-error fix for webkitdirectory */}
|
|
147
|
+
<input ref={fileInputRef} type="file" webkitdirectory="" directory="" multiple hidden onChange={onSelectFile} accept={mode === "file" ? _join(fileTypes) : undefined} />
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export default FileField;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
@import "../../styles/mixins.styl"
|
|
2
|
+
|
|
3
|
+
.language-picker
|
|
4
|
+
position: relative
|
|
5
|
+
|
|
6
|
+
.language-picker-menu
|
|
7
|
+
opacity: 1
|
|
8
|
+
|
|
9
|
+
// .language-picker-backdrop
|
|
10
|
+
// z-index: -1
|
|
11
|
+
|
|
12
|
+
.language-picker-trigger
|
|
13
|
+
border-radius: 0
|
|
14
|
+
height: 100%
|
|
15
|
+
width: 100%
|
|
16
|
+
min-width: 5px
|
|
17
|
+
padding: 0
|
|
18
|
+
|
|
19
|
+
.language-picker-item
|
|
20
|
+
height: 100%
|
|
21
|
+
width: 100%
|
|
22
|
+
|
|
23
|
+
.icon
|
|
24
|
+
display: inline-block
|
|
25
|
+
vertical-align: middle
|
|
26
|
+
width: 1.8em
|
|
27
|
+
height: 1.8em
|
|
28
|
+
background-size: 3em 3em
|
|
29
|
+
background-position: 50% 50%
|
|
30
|
+
border-radius: 50%
|
|
31
|
+
border-width: 0.05em
|
|
32
|
+
border-style: solid
|
|
33
|
+
|
|
34
|
+
.name
|
|
35
|
+
padding-left: .5em
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import classnames from "classnames";
|
|
2
|
+
import $_ from "lodash";
|
|
3
|
+
import moment from "moment";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import React, {useState} from "react";
|
|
6
|
+
import {useTranslation} from "react-i18next";
|
|
7
|
+
|
|
8
|
+
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
|
9
|
+
import {
|
|
10
|
+
Button, CircularProgress, ClickAwayListener, Menu, MenuItem, Theme, useMediaQuery
|
|
11
|
+
} from "@mui/material";
|
|
12
|
+
|
|
13
|
+
import {ComponentDisplayMode} from "../../common/ComponentDisplayMode";
|
|
14
|
+
import Styles from "./LanguagePicker.styl";
|
|
15
|
+
|
|
16
|
+
const isMac = process.platform === "darwin";
|
|
17
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
18
|
+
const prependPath = isMac && !isDev ? path.join(process.resourcesPath, "..") : ".";
|
|
19
|
+
|
|
20
|
+
export interface ILanguagePickerProps {
|
|
21
|
+
className?: string;
|
|
22
|
+
mode?: ComponentDisplayMode;
|
|
23
|
+
showArrow?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ILanguagePickerTriggerProps extends ILanguagePickerProps {
|
|
27
|
+
loading?: boolean;
|
|
28
|
+
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ILanguagePickerItemProps extends ILanguagePickerProps {
|
|
32
|
+
lang?: string;
|
|
33
|
+
onClick?: (lang: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const LanguagePicker = (props: ILanguagePickerProps) => {
|
|
37
|
+
const { mode, showArrow = true } = props;
|
|
38
|
+
const { i18n } = useTranslation();
|
|
39
|
+
const [loading, setLoading] = useState(false);
|
|
40
|
+
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
41
|
+
const langs = $_.get(i18n, "options.supportedLngs");
|
|
42
|
+
const availableLocales: string[] = !langs ? [] : $_.without(langs, "cimode").sort();
|
|
43
|
+
const displayMode = $_.defaultTo(
|
|
44
|
+
mode,
|
|
45
|
+
useMediaQuery((theme: Theme) => theme.breakpoints.down("md"))
|
|
46
|
+
? ComponentDisplayMode.Minimal
|
|
47
|
+
: ComponentDisplayMode.Full,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const onClose = () => setAnchorEl(null);
|
|
51
|
+
|
|
52
|
+
const onClickAway = (event: any) => {
|
|
53
|
+
if (anchorEl && anchorEl.contains(event.target)) return;
|
|
54
|
+
|
|
55
|
+
onClose();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const onTriggerClick = (event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget);
|
|
59
|
+
|
|
60
|
+
const onItemClick = async (lang: string) => {
|
|
61
|
+
if (!$_.isEqual(lang, i18n.language)) {
|
|
62
|
+
setLoading(true);
|
|
63
|
+
i18n.changeLanguage(lang, () => setLoading(false));
|
|
64
|
+
moment.locale(lang);
|
|
65
|
+
global.store.set("application.language", lang);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
onClose();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className={classnames(Styles.languagePicker, $_.get(props, "className"))}>
|
|
73
|
+
<LanguagePickerTrigger showArrow={showArrow} mode={displayMode} loading={loading} onClick={onTriggerClick} />
|
|
74
|
+
<ClickAwayListener onClickAway={onClickAway}>
|
|
75
|
+
<Menu
|
|
76
|
+
anchorEl={anchorEl}
|
|
77
|
+
disablePortal={true}
|
|
78
|
+
anchorOrigin={{ vertical: "top", horizontal: "center" }}
|
|
79
|
+
PopoverClasses={{ root: Styles.languagePickerBackdrop, paper: Styles.languagePickerMenu }}
|
|
80
|
+
transformOrigin={{ vertical: -40, horizontal: "center" }}
|
|
81
|
+
open={Boolean(anchorEl)}
|
|
82
|
+
onClose={onClose}
|
|
83
|
+
>
|
|
84
|
+
{$_.map(availableLocales, (item) => (
|
|
85
|
+
<LanguagePickerItem
|
|
86
|
+
key={item}
|
|
87
|
+
lang={item}
|
|
88
|
+
onClick={onItemClick}
|
|
89
|
+
mode={displayMode}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
</Menu>
|
|
93
|
+
</ClickAwayListener>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const LanguagePickerTrigger = (props: ILanguagePickerTriggerProps) => {
|
|
99
|
+
const { loading, onClick, mode, showArrow } = props;
|
|
100
|
+
const { i18n, t } = useTranslation();
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Button
|
|
104
|
+
fullWidth={true}
|
|
105
|
+
onClick={onClick}
|
|
106
|
+
className={Styles.languagePickerTrigger}
|
|
107
|
+
variant="text"
|
|
108
|
+
disableElevation={true}
|
|
109
|
+
color="inherit"
|
|
110
|
+
>
|
|
111
|
+
<span
|
|
112
|
+
className={Styles.icon}
|
|
113
|
+
style={{
|
|
114
|
+
backgroundImage: `url("${prependPath}/resources/locales/${i18n.language}/flag.svg")`,
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
{mode > ComponentDisplayMode.Compact && (
|
|
118
|
+
<React.Fragment>
|
|
119
|
+
<span className={classnames("uppercase", Styles.name)}>{t("langName", { lng: i18n.language })}</span>
|
|
120
|
+
{loading && <CircularProgress />}
|
|
121
|
+
</React.Fragment>
|
|
122
|
+
)}
|
|
123
|
+
{showArrow && <ArrowDropDownIcon fontSize="medium" color="inherit" />}
|
|
124
|
+
</Button>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const LanguagePickerItem = (props: ILanguagePickerItemProps) => {
|
|
129
|
+
const { lang, onClick } = props;
|
|
130
|
+
const { t } = useTranslation();
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<MenuItem key={lang} data-id={lang} onClick={() => onClick(lang)} className={Styles.languagePickerItem}>
|
|
134
|
+
<span
|
|
135
|
+
className={Styles.icon}
|
|
136
|
+
style={{
|
|
137
|
+
backgroundImage: `url("${prependPath}/resources/locales/${lang}/flag.svg")`,
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
<span className={classnames("capitalize", Styles.name)}>{t("langName", { lng: lang })}</span>
|
|
141
|
+
</MenuItem>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export default LanguagePicker;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import {Box, BoxProps} from "@mui/material";
|
|
4
|
+
|
|
5
|
+
import IconSvg from "../../resources/icons/logo-shape.svg";
|
|
6
|
+
|
|
7
|
+
const Logo: React.FC<BoxProps> = (props) => {
|
|
8
|
+
return (
|
|
9
|
+
<Box {...props}>
|
|
10
|
+
<img width="100%" height="100%" src={IconSvg} />
|
|
11
|
+
</Box>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default Logo;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import _map from "lodash/map";
|
|
2
|
+
import React, {useEffect, useState} from "react";
|
|
3
|
+
import {useTranslation} from "react-i18next";
|
|
4
|
+
|
|
5
|
+
import {Stack, TextField} from "@mui/material";
|
|
6
|
+
import Button from "@mui/material/Button";
|
|
7
|
+
import Dialog from "@mui/material/Dialog";
|
|
8
|
+
import DialogActions from "@mui/material/DialogActions";
|
|
9
|
+
import DialogContent from "@mui/material/DialogContent";
|
|
10
|
+
import DialogTitle from "@mui/material/DialogTitle";
|
|
11
|
+
|
|
12
|
+
import Styles from "./DetailsModal.styl";
|
|
13
|
+
|
|
14
|
+
export type Details = {
|
|
15
|
+
[key: string]: string | number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type DetailsModalProps = {
|
|
19
|
+
id: string;
|
|
20
|
+
details?: Details;
|
|
21
|
+
open?: boolean;
|
|
22
|
+
onClose?: (data: Details) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const DetailsModal: React.FC<DetailsModalProps> = (props: DetailsModalProps) => {
|
|
26
|
+
const {onClose, details, open, ...other} = props;
|
|
27
|
+
const {t} = useTranslation();
|
|
28
|
+
const [value, setValue] = useState<Details>();
|
|
29
|
+
|
|
30
|
+
const onValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
31
|
+
const key = event.target.dataset.key;
|
|
32
|
+
|
|
33
|
+
setValue((prev) => ({...prev, [key]: event.target.value}));
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleClose = () => {
|
|
37
|
+
if (onClose) {
|
|
38
|
+
onClose(value);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setValue(details);
|
|
44
|
+
}, [details]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Dialog
|
|
48
|
+
open={open}
|
|
49
|
+
disablePortal
|
|
50
|
+
onClose={handleClose}
|
|
51
|
+
fullWidth
|
|
52
|
+
maxWidth="md"
|
|
53
|
+
className={Styles.detailsModal}
|
|
54
|
+
{...other}
|
|
55
|
+
>
|
|
56
|
+
<DialogTitle textAlign="center">{t("detailsModalTitle")}</DialogTitle>
|
|
57
|
+
<DialogContent dividers className={Styles.content}>
|
|
58
|
+
<Stack direction="column" spacing={2}>
|
|
59
|
+
{_map(value, (v, k) =>
|
|
60
|
+
<TextField
|
|
61
|
+
key={k}
|
|
62
|
+
value={v ?? ""}
|
|
63
|
+
fullWidth
|
|
64
|
+
variant="outlined"
|
|
65
|
+
label={t(k)}
|
|
66
|
+
onChange={onValueChange}
|
|
67
|
+
slotProps={{
|
|
68
|
+
htmlInput: {
|
|
69
|
+
"data-key": k,
|
|
70
|
+
}
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
</Stack>
|
|
75
|
+
</DialogContent>
|
|
76
|
+
<DialogActions sx={{justifyContent: "center"}}>
|
|
77
|
+
<Button variant="contained" disableElevation color="secondary" onClick={handleClose}>
|
|
78
|
+
{t("ok")}
|
|
79
|
+
</Button>
|
|
80
|
+
</DialogActions>
|
|
81
|
+
</Dialog>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default DetailsModal;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import $_ from "lodash";
|
|
2
|
+
import React, {useEffect, useState} from "react";
|
|
3
|
+
import {NumberFormatValues, NumericFormat, NumericFormatProps} from "react-number-format";
|
|
4
|
+
import {useInterval} from "usehooks-ts";
|
|
5
|
+
|
|
6
|
+
import AddIcon from "@mui/icons-material/Add";
|
|
7
|
+
import RemoveIcon from "@mui/icons-material/Remove";
|
|
8
|
+
import {
|
|
9
|
+
IconButton, InputAdornment, InputLabelProps, TextField, TextFieldProps
|
|
10
|
+
} from "@mui/material";
|
|
11
|
+
|
|
12
|
+
import Styles from "./NumberField.styl";
|
|
13
|
+
|
|
14
|
+
export interface INumberFieldProps extends Omit<NumericFormatProps<TextFieldProps<"outlined">>, "onChange"> {
|
|
15
|
+
label?: string;
|
|
16
|
+
readOnly?: boolean;
|
|
17
|
+
allowEmpty?: boolean;
|
|
18
|
+
showIncreaseDecreaseButtons?: boolean;
|
|
19
|
+
inputLabelProps?: Partial<InputLabelProps>;
|
|
20
|
+
initialPressedDelay?: number;
|
|
21
|
+
onChange: (value: number) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const NumberField = (props: INumberFieldProps) => {
|
|
25
|
+
const {
|
|
26
|
+
value = 0,
|
|
27
|
+
label,
|
|
28
|
+
readOnly,
|
|
29
|
+
inputLabelProps,
|
|
30
|
+
allowEmpty,
|
|
31
|
+
decimalScale = 2,
|
|
32
|
+
fullWidth,
|
|
33
|
+
fixedDecimalScale = true,
|
|
34
|
+
onChange,
|
|
35
|
+
initialPressedDelay = 300,
|
|
36
|
+
showIncreaseDecreaseButtons = true,
|
|
37
|
+
min,
|
|
38
|
+
max,
|
|
39
|
+
step = 0.5,
|
|
40
|
+
...rest
|
|
41
|
+
} = props;
|
|
42
|
+
const [decreasePressed, setDecreasePressed] = useState(false);
|
|
43
|
+
const [increasePressed, setIncreasePressed] = useState(false);
|
|
44
|
+
const [delay, setDelay] = useState(initialPressedDelay);
|
|
45
|
+
const [text, setText] = useState(value);
|
|
46
|
+
|
|
47
|
+
useInterval(
|
|
48
|
+
() => {
|
|
49
|
+
if (decreasePressed) onDecreaseClick();
|
|
50
|
+
if (increasePressed) onIncreaseClick();
|
|
51
|
+
setDelay($_.max([50, delay - 50]));
|
|
52
|
+
},
|
|
53
|
+
decreasePressed || increasePressed ? delay : null,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const handleValueChange = (values: NumberFormatValues) => {
|
|
57
|
+
setText(values.floatValue);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const onDecreaseClick = () => {
|
|
61
|
+
setText($_.max([$_.toNumber(value) - $_.toNumber(step), min]));
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onIncreaseClick = () => {
|
|
65
|
+
setText($_.min([$_.toNumber(value) + $_.toNumber(step), max]));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const onDecreaseMouseDown = () => {
|
|
69
|
+
setDecreasePressed(true);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const onIncreaseMouseDown = () => {
|
|
73
|
+
setIncreasePressed(true);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const onDecreaseMouseUp = () => {
|
|
77
|
+
setDecreasePressed(false);
|
|
78
|
+
setDelay(initialPressedDelay);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const onIncreaseMouseUp = () => {
|
|
82
|
+
setIncreasePressed(false);
|
|
83
|
+
setDelay(initialPressedDelay);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const isAllowed = (values: NumberFormatValues) => {
|
|
87
|
+
if ($_.isUndefined(values.floatValue) && !allowEmpty) {
|
|
88
|
+
return false;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return true;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const value = $_.toNumber(text) ?? 0;
|
|
96
|
+
|
|
97
|
+
if (onChange) {
|
|
98
|
+
onChange(value);
|
|
99
|
+
}
|
|
100
|
+
}, [text]);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<NumericFormat
|
|
104
|
+
value={value}
|
|
105
|
+
onValueChange={handleValueChange}
|
|
106
|
+
customInput={TextField}
|
|
107
|
+
className={Styles.numberField}
|
|
108
|
+
fullWidth={fullWidth}
|
|
109
|
+
label={label}
|
|
110
|
+
InputLabelProps={$_.defaultTo(inputLabelProps, {className: "upperfirst"})}
|
|
111
|
+
variant="outlined"
|
|
112
|
+
decimalScale={decimalScale}
|
|
113
|
+
fixedDecimalScale={fixedDecimalScale}
|
|
114
|
+
inputProps={{
|
|
115
|
+
style: {textAlign: "center"},
|
|
116
|
+
}}
|
|
117
|
+
isAllowed={isAllowed}
|
|
118
|
+
InputProps={
|
|
119
|
+
showIncreaseDecreaseButtons ? {
|
|
120
|
+
startAdornment: (
|
|
121
|
+
<InputAdornment position="start">
|
|
122
|
+
<IconButton
|
|
123
|
+
color="primary"
|
|
124
|
+
edge="start"
|
|
125
|
+
onClick={onDecreaseClick}
|
|
126
|
+
onMouseDown={onDecreaseMouseDown}
|
|
127
|
+
onMouseUp={onDecreaseMouseUp}
|
|
128
|
+
>
|
|
129
|
+
<RemoveIcon />
|
|
130
|
+
</IconButton>
|
|
131
|
+
</InputAdornment>
|
|
132
|
+
),
|
|
133
|
+
endAdornment: (
|
|
134
|
+
<InputAdornment position="end">
|
|
135
|
+
<IconButton
|
|
136
|
+
color="primary"
|
|
137
|
+
edge="end"
|
|
138
|
+
onClick={onIncreaseClick}
|
|
139
|
+
onMouseDown={onIncreaseMouseDown}
|
|
140
|
+
onMouseUp={onIncreaseMouseUp}
|
|
141
|
+
>
|
|
142
|
+
<AddIcon />
|
|
143
|
+
</IconButton>
|
|
144
|
+
</InputAdornment>
|
|
145
|
+
),
|
|
146
|
+
readOnly,
|
|
147
|
+
} : {}
|
|
148
|
+
}
|
|
149
|
+
{...rest}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export default NumberField;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import {Box, CircularProgress, CircularProgressProps, Typography} from "@mui/material";
|
|
4
|
+
|
|
5
|
+
import Styles from "./Progress.styl";
|
|
6
|
+
|
|
7
|
+
export const Progress: React.FC<CircularProgressProps> = (props) => {
|
|
8
|
+
return (
|
|
9
|
+
<Box className={Styles.progress}>
|
|
10
|
+
<CircularProgress variant="determinate" {...props} />
|
|
11
|
+
<Box className={Styles.labelWrapper}>
|
|
12
|
+
<Typography variant="caption">{`${Math.round(props.value)}%`}</Typography>
|
|
13
|
+
</Box>
|
|
14
|
+
</Box>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default Progress;
|
|
File without changes
|