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.
Files changed (128) hide show
  1. package/.eslintrc.json +29 -0
  2. package/.prettierrc +19 -0
  3. package/.vscode/extensions.json +7 -0
  4. package/.vscode/settings.json +23 -0
  5. package/.yarnrc.yml +13 -0
  6. package/LICENSE +21 -0
  7. package/README.md +11 -0
  8. package/package.json +115 -0
  9. package/public/index.html +20 -0
  10. package/public/screenshots/cutting.png +0 -0
  11. package/public/screenshots/downloading.png +0 -0
  12. package/public/screenshots/editing.png +0 -0
  13. package/public/screenshots/errors.png +0 -0
  14. package/public/screenshots/settings.png +0 -0
  15. package/public/screenshots/tracklist.png +0 -0
  16. package/src/@types/global.d.ts +7 -0
  17. package/src/@types/i18next-scanner-webpack.d.ts +1 -0
  18. package/src/@types/stylus.d.ts +4 -0
  19. package/src/@types/svg.d.ts +1 -0
  20. package/src/App.styl +24 -0
  21. package/src/App.tsx +31 -0
  22. package/src/bootstrap.tsx +30 -0
  23. package/src/common/CancellablePromise.ts +22 -0
  24. package/src/common/ComponentDisplayMode.ts +8 -0
  25. package/src/common/Delay.ts +3 -0
  26. package/src/common/FileSystem.ts +171 -0
  27. package/src/common/Helpers.ts +270 -0
  28. package/src/common/Mappings.ts +14 -0
  29. package/src/common/PuppeteerOptions.ts +45 -0
  30. package/src/common/Selectors.ts +21 -0
  31. package/src/common/Store.ts +108 -0
  32. package/src/common/Theme.ts +4 -0
  33. package/src/common/Youtube.ts +80 -0
  34. package/src/components/appBar/AppBar.styl +22 -0
  35. package/src/components/appBar/AppBar.tsx +73 -0
  36. package/src/components/directoryPicker/DirectoryPicker.tsx +44 -0
  37. package/src/components/fileField/FileField.styl +3 -0
  38. package/src/components/fileField/FileField.tsx +152 -0
  39. package/src/components/languagePicker/LanguagePicker.styl +38 -0
  40. package/src/components/languagePicker/LanguagePicker.tsx +145 -0
  41. package/src/components/logo/Logo.tsx +15 -0
  42. package/src/components/modals/DetailsModal.styl +9 -0
  43. package/src/components/modals/DetailsModal.tsx +85 -0
  44. package/src/components/numberField/NumberField.styl +13 -0
  45. package/src/components/numberField/NumberField.tsx +154 -0
  46. package/src/components/progress/Progress.styl +15 -0
  47. package/src/components/progress/Progress.tsx +18 -0
  48. package/src/components/splitButton/SplitButton.styl +0 -0
  49. package/src/components/splitButton/SplitButton.tsx +125 -0
  50. package/src/components/themePicker/ThemePicker.styl +19 -0
  51. package/src/components/themePicker/ThemePicker.tsx +65 -0
  52. package/src/components/themeSwitcher/ThemeSwitcher.styl +10 -0
  53. package/src/components/themeSwitcher/ThemeSwitcher.tsx +43 -0
  54. package/src/components/youtube/formatSelector/FormatSelector.styl +3 -0
  55. package/src/components/youtube/formatSelector/FormatSelector.tsx +202 -0
  56. package/src/components/youtube/inputPanel/InputPanel.styl +7 -0
  57. package/src/components/youtube/inputPanel/InputPanel.tsx +189 -0
  58. package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.styl +80 -0
  59. package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.tsx +113 -0
  60. package/src/components/youtube/trackList/TrackList.styl +64 -0
  61. package/src/components/youtube/trackList/TrackList.tsx +258 -0
  62. package/src/enums/DataResponse.ts +5 -0
  63. package/src/enums/Media.ts +16 -0
  64. package/src/enums/MediaFormat.ts +10 -0
  65. package/src/enums/MimeTypes.ts +14 -0
  66. package/src/hooks/useCancellablePromises.ts +25 -0
  67. package/src/hooks/useClickCounter.ts +24 -0
  68. package/src/hooks/useData.ts +61 -0
  69. package/src/hooks/useMultiClickHandler.ts +41 -0
  70. package/src/i18next.ts +33 -0
  71. package/src/index.ts +65 -0
  72. package/src/react/actions/Action.ts +3 -0
  73. package/src/react/actions/AppActions.ts +41 -0
  74. package/src/react/contexts/AppContext.tsx +51 -0
  75. package/src/react/contexts/AppThemeContext.tsx +38 -0
  76. package/src/react/contexts/DataContext copy.tsx +76 -0
  77. package/src/react/contexts/DataContext.tsx +41 -0
  78. package/src/react/hooks/useAppTheme.ts +14 -0
  79. package/src/react/reducers/AppReducer.tsx +45 -0
  80. package/src/react/reducers/Reducer.ts +7 -0
  81. package/src/react/states/AppState.ts +29 -0
  82. package/src/react/states/State.ts +29 -0
  83. package/src/renderer.tsx +13 -0
  84. package/src/resources/bin/yt-dlp.exe +0 -0
  85. package/src/resources/fonts/Baloo-Regular.ttf +0 -0
  86. package/src/resources/fonts/Lato-Black.ttf +0 -0
  87. package/src/resources/fonts/Lato-BlackItalic.ttf +0 -0
  88. package/src/resources/fonts/Lato-Bold.ttf +0 -0
  89. package/src/resources/fonts/Lato-BoldItalic.ttf +0 -0
  90. package/src/resources/fonts/Lato-Italic.ttf +0 -0
  91. package/src/resources/fonts/Lato-Light.ttf +0 -0
  92. package/src/resources/fonts/Lato-LightItalic.ttf +0 -0
  93. package/src/resources/fonts/Lato-Regular.ttf +0 -0
  94. package/src/resources/fonts/Lato-Thin.ttf +0 -0
  95. package/src/resources/fonts/Lato-ThinItalic.ttf +0 -0
  96. package/src/resources/fonts/Material-Icons.woff2 +0 -0
  97. package/src/resources/icons/favicon-16x16.png +0 -0
  98. package/src/resources/icons/favicon-32x32.png +0 -0
  99. package/src/resources/icons/favicon.ico +0 -0
  100. package/src/resources/icons/logo-shape.png +0 -0
  101. package/src/resources/icons/logo-shape.svg +59 -0
  102. package/src/resources/images/loading.svg +28 -0
  103. package/src/resources/images/logo.png +0 -0
  104. package/src/resources/locales/de-DE/flag.svg +1 -0
  105. package/src/resources/locales/de-DE/translation.json +44 -0
  106. package/src/resources/locales/en-GB/flag.svg +43 -0
  107. package/src/resources/locales/en-GB/translation.json +44 -0
  108. package/src/resources/locales/pl-PL/flag.svg +36 -0
  109. package/src/resources/locales/pl-PL/translation.json +44 -0
  110. package/src/styles/MaterialThemes.ts +331 -0
  111. package/src/styles/fonts.styl +71 -0
  112. package/src/styles/mixins.styl +22 -0
  113. package/src/tests/CompleteTracksMock.ts +17384 -0
  114. package/src/tests/MissingDetailsTracksMock.ts +7737 -0
  115. package/src/theme/ColorThemes.ts +190 -0
  116. package/src/theme/Colors.ts +92 -0
  117. package/src/theme/Shadows.ts +9 -0
  118. package/src/theme/Shape.ts +7 -0
  119. package/src/theme/Theme.ts +24 -0
  120. package/src/theme/Typography.ts +56 -0
  121. package/src/views/development/DevelopmentView.styl +22 -0
  122. package/src/views/development/DevelopmentView.tsx +57 -0
  123. package/src/views/home/HomeView.styl +60 -0
  124. package/src/views/home/HomeView.tsx +505 -0
  125. package/src/views/settings/SettingsView.styl +27 -0
  126. package/src/views/settings/SettingsView.tsx +255 -0
  127. package/tsconfig.json +20 -0
  128. 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,9 @@
1
+ @import "../../styles/mixins.styl"
2
+
3
+ .details-modal {
4
+
5
+ .content {
6
+ justify-content: flex-start;
7
+ }
8
+
9
+ }
@@ -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,13 @@
1
+ .number-field
2
+
3
+ .error
4
+ font-size: .9em
5
+ bottom: .3em
6
+ width: 100%
7
+ position: absolute
8
+ text-align: center
9
+
10
+ input[type=number]::-webkit-inner-spin-button
11
+ input[type=number]::-webkit-outer-spin-button
12
+ -webkit-appearance: none
13
+ margin: 0
@@ -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,15 @@
1
+ .progress {
2
+ position: relative;
3
+ display: inline-flex;
4
+
5
+ .label-wrapper {
6
+ position: absolute;
7
+ top: 0;
8
+ left: 0;
9
+ bottom: 0;
10
+ right: 0;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ }
15
+ }
@@ -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