yt-grabber 1.1.1 → 1.2.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/package.json +1 -2
- package/src/App.styl +0 -13
- package/src/common/Youtube.ts +2 -1
- package/src/components/appBar/AppBar.styl +2 -3
- package/src/components/appBar/AppBar.tsx +13 -8
- package/src/components/languagePicker/LanguagePicker.tsx +6 -6
- package/src/components/themePicker/ThemePicker.tsx +11 -1
- package/src/components/youtube/infoBar/InfoBar.tsx +26 -9
- package/src/components/youtube/inputPanel/InputPanel.styl +9 -2
- package/src/components/youtube/inputPanel/InputPanel.tsx +20 -12
- package/src/components/youtube/logMenu/LogMenu.styl +23 -0
- package/src/components/youtube/logMenu/LogMenu.tsx +135 -0
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.tsx +5 -3
- package/src/components/youtube/trackList/TrackList.tsx +5 -3
- package/src/hooks/useHelp.ts +71 -27
- package/src/react/contexts/DataContext.tsx +18 -1
- package/src/resources/bin/yt-dlp.exe +0 -0
- package/src/resources/locales/de-DE/help.json +17 -3
- package/src/resources/locales/de-DE/translation.json +12 -3
- package/src/resources/locales/en-GB/help.json +17 -3
- package/src/resources/locales/en-GB/translation.json +13 -4
- package/src/resources/locales/pl-PL/help.json +17 -3
- package/src/resources/locales/pl-PL/translation.json +12 -3
- package/src/views/home/HomeView.tsx +43 -21
- package/src/@types/busylight.d.ts +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-grabber",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Youtube Grabber",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -69,7 +69,6 @@
|
|
|
69
69
|
"@mui/icons-material": "^7.0.0",
|
|
70
70
|
"@mui/lab": "^7.0.0-beta.9",
|
|
71
71
|
"@mui/material": "^7.0.0",
|
|
72
|
-
"@pureit/busylight": "^1.0.12",
|
|
73
72
|
"axios": "^1.8.4",
|
|
74
73
|
"classnames": "^2.5.1",
|
|
75
74
|
"electron-devtools-installer": "^4.0.0",
|
package/src/App.styl
CHANGED
|
@@ -50,18 +50,5 @@ html {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
:global(#help-backdrop) {
|
|
55
|
-
position: absolute;
|
|
56
|
-
width: 100vw;
|
|
57
|
-
height: 100vh;
|
|
58
|
-
top: 0;
|
|
59
|
-
left: 0;
|
|
60
|
-
right: 0;
|
|
61
|
-
bottom: 0;
|
|
62
|
-
background: rgba(0, 0, 0, 0.5);
|
|
63
|
-
z-index: 1;
|
|
64
|
-
vendor("backdrop-filter", blur(2px));
|
|
65
|
-
}
|
|
66
53
|
}
|
|
67
54
|
}
|
package/src/common/Youtube.ts
CHANGED
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
.language-picker {
|
|
22
|
-
margin-right: .5em;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
.icon {
|
|
@@ -32,11 +31,11 @@
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
&.help {
|
|
35
|
-
opacity: .
|
|
34
|
+
opacity: .5;
|
|
36
35
|
vendor("transition", opacity .25s ease-in-out);
|
|
37
36
|
|
|
38
37
|
&:hover {
|
|
39
|
-
opacity:
|
|
38
|
+
opacity: 1;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
&.active {
|
|
@@ -4,6 +4,7 @@ import {useTranslation} from "react-i18next";
|
|
|
4
4
|
|
|
5
5
|
import CloseIcon from "@mui/icons-material/Close";
|
|
6
6
|
import HelpIcon from "@mui/icons-material/Help";
|
|
7
|
+
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
|
7
8
|
import SettingsIcon from "@mui/icons-material/Settings";
|
|
8
9
|
import {Stack} from "@mui/material";
|
|
9
10
|
import ApplicationBar from "@mui/material/AppBar";
|
|
@@ -56,15 +57,19 @@ const AppBar = (props: AppBarProps) => {
|
|
|
56
57
|
const createSettingsButton = () => {
|
|
57
58
|
if (state.location !== "/") {
|
|
58
59
|
return (
|
|
59
|
-
<
|
|
60
|
-
<
|
|
61
|
-
|
|
60
|
+
<div>
|
|
61
|
+
<IconButton onClick={handleClose} color="inherit" className={Styles.icon}>
|
|
62
|
+
<CloseIcon />
|
|
63
|
+
</IconButton>
|
|
64
|
+
</div>
|
|
62
65
|
);
|
|
63
66
|
} else {
|
|
64
67
|
return (
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
+
<div>
|
|
69
|
+
<IconButton data-help="settings" disabled={disableNavigation} onClick={handleOpenSettings} color="inherit" className={Styles.icon}>
|
|
70
|
+
<SettingsIcon />
|
|
71
|
+
</IconButton>
|
|
72
|
+
</div>
|
|
68
73
|
);
|
|
69
74
|
}
|
|
70
75
|
};
|
|
@@ -79,8 +84,8 @@ const AppBar = (props: AppBarProps) => {
|
|
|
79
84
|
</Typography>
|
|
80
85
|
<Box sx={{flexGrow: 1}}></Box>
|
|
81
86
|
<Stack direction="row" gap={1}>
|
|
82
|
-
<LanguagePicker data-help="languagePicker" className={Styles.languagePicker} showArrow={false} mode={ComponentDisplayMode.Minimal} />
|
|
83
|
-
<IconButton data-help="help-toggle" className={classnames(Styles.icon, Styles.help, {[Styles.active]: state.help})} onClick={onHelpClick}
|
|
87
|
+
<LanguagePicker data-help="languagePicker" className={Styles.languagePicker} showArrow={false} mode={ComponentDisplayMode.Minimal} sx={{ marginRight: 1}} />
|
|
88
|
+
<IconButton color="inherit" data-help="help-toggle" className={classnames(Styles.icon, Styles.help, {[Styles.active]: state.help})} onClick={onHelpClick}>{state.help ? <HelpIcon/> : <HelpOutlineIcon/>}</IconButton>
|
|
84
89
|
<Tooltip title={state.location === "/settings" ? t("closeSettings") : t("openSettings")}>
|
|
85
90
|
{createSettingsButton()}
|
|
86
91
|
</Tooltip>
|
|
@@ -2,12 +2,12 @@ import classnames from "classnames";
|
|
|
2
2
|
import $_ from "lodash";
|
|
3
3
|
import moment from "moment";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import React, {
|
|
5
|
+
import React, {HTMLAttributes, useState} from "react";
|
|
6
6
|
import {useTranslation} from "react-i18next";
|
|
7
7
|
|
|
8
8
|
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
|
9
9
|
import {
|
|
10
|
-
Button, CircularProgress, ClickAwayListener, Menu, MenuItem, Theme, useMediaQuery
|
|
10
|
+
Box, BoxProps, Button, CircularProgress, ClickAwayListener, Menu, MenuItem, Theme, useMediaQuery
|
|
11
11
|
} from "@mui/material";
|
|
12
12
|
|
|
13
13
|
import {ComponentDisplayMode} from "../../common/ComponentDisplayMode";
|
|
@@ -17,13 +17,13 @@ const isMac = process.platform === "darwin";
|
|
|
17
17
|
const isDev = process.env.NODE_ENV === "development";
|
|
18
18
|
const prependPath = isMac && !isDev ? path.join(process.resourcesPath, "..") : ".";
|
|
19
19
|
|
|
20
|
-
export type LanguagePickerProps = Omit<
|
|
20
|
+
export type LanguagePickerProps = Omit<HTMLAttributes<HTMLDivElement> & BoxProps, "onClick"> & {
|
|
21
21
|
className?: string;
|
|
22
22
|
mode?: ComponentDisplayMode;
|
|
23
23
|
showArrow?: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export type LanguagePickerTriggerProps = Omit<
|
|
26
|
+
export type LanguagePickerTriggerProps = Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> & LanguagePickerProps & {
|
|
27
27
|
loading?: boolean;
|
|
28
28
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
29
29
|
}
|
|
@@ -69,7 +69,7 @@ export const LanguagePicker = (props: LanguagePickerProps) => {
|
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
return (
|
|
72
|
-
<
|
|
72
|
+
<Box className={classnames(Styles.languagePicker,className)} {...rest}>
|
|
73
73
|
<LanguagePickerTrigger showArrow={showArrow} mode={displayMode} loading={loading} onClick={onTriggerClick} />
|
|
74
74
|
<ClickAwayListener onClickAway={onClickAway}>
|
|
75
75
|
<Menu
|
|
@@ -91,7 +91,7 @@ export const LanguagePicker = (props: LanguagePickerProps) => {
|
|
|
91
91
|
))}
|
|
92
92
|
</Menu>
|
|
93
93
|
</ClickAwayListener>
|
|
94
|
-
</
|
|
94
|
+
</Box>
|
|
95
95
|
);
|
|
96
96
|
};
|
|
97
97
|
|
|
@@ -33,6 +33,16 @@ export const ThemePicker = (props: IThemePickerProps) => {
|
|
|
33
33
|
return <SystemModeIcon className={Styles.icon} />;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
const resolveText = (mode: ThemeMode) => {
|
|
37
|
+
if (mode === "dark") {
|
|
38
|
+
return t("modeDark");
|
|
39
|
+
} else if (mode === "light") {
|
|
40
|
+
return t("modeLight");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return t("modeSystem");
|
|
44
|
+
};
|
|
45
|
+
|
|
36
46
|
const onChangeMode = (event: SelectChangeEvent<ThemeMode>) => {
|
|
37
47
|
setMode(event.target.value as ThemeMode);
|
|
38
48
|
};
|
|
@@ -55,7 +65,7 @@ export const ThemePicker = (props: IThemePickerProps) => {
|
|
|
55
65
|
>
|
|
56
66
|
{_map(themeModes, (item) => <MenuItem key={item} value={item} className={Styles.menuItem}>
|
|
57
67
|
{resolveIcon(item)}
|
|
58
|
-
|
|
68
|
+
{resolveText(item)}
|
|
59
69
|
</MenuItem>)}
|
|
60
70
|
</Select>
|
|
61
71
|
</FormControl>
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import _filter from "lodash/filter";
|
|
2
2
|
import _find from "lodash/find";
|
|
3
|
+
import _isEmpty from "lodash/isEmpty";
|
|
3
4
|
import _reject from "lodash/reject";
|
|
4
5
|
import _size from "lodash/size";
|
|
5
6
|
import _some from "lodash/some";
|
|
6
7
|
import React from "react";
|
|
7
8
|
import {useTranslation} from "react-i18next";
|
|
8
9
|
|
|
9
|
-
import {Box, LinearProgress, Stack, Typography} from "@mui/material";
|
|
10
|
+
import {Box, LinearProgress, Stack, Tooltip, Typography} from "@mui/material";
|
|
10
11
|
|
|
11
12
|
import {useAppContext} from "../../../react/contexts/AppContext";
|
|
12
13
|
import {useDataState} from "../../../react/contexts/DataContext";
|
|
14
|
+
import LogMenu from "../logMenu/LogMenu";
|
|
13
15
|
import Styles from "./InfoBar.styl";
|
|
14
16
|
|
|
15
17
|
export type InfoBarProps = {
|
|
@@ -18,7 +20,7 @@ export type InfoBarProps = {
|
|
|
18
20
|
|
|
19
21
|
export const InfoBar: React.FC<InfoBarProps> = (props) => {
|
|
20
22
|
const {hidden} = props;
|
|
21
|
-
const {tracks, playlists, trackStatus} = useDataState();
|
|
23
|
+
const {tracks, playlists, trackStatus, errors, warnings} = useDataState();
|
|
22
24
|
const {state} = useAppContext();
|
|
23
25
|
const {t} = useTranslation();
|
|
24
26
|
|
|
@@ -47,13 +49,28 @@ export const InfoBar: React.FC<InfoBarProps> = (props) => {
|
|
|
47
49
|
if (hidden) return null;
|
|
48
50
|
|
|
49
51
|
return (
|
|
50
|
-
<Stack className={Styles.infoBar} direction="row" spacing={1} padding={1.5} data-help="infoBar">
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
<Stack className={Styles.infoBar} direction="row" justifyContent="space-between" spacing={1} padding={1.5} data-help="infoBar">
|
|
53
|
+
<Stack direction="row" alignItems="start">
|
|
54
|
+
<LogMenu hidden={_isEmpty(errors) && _isEmpty(warnings)} />
|
|
55
|
+
</Stack>
|
|
56
|
+
<Stack direction="row" spacing={1} alignItems="center">
|
|
57
|
+
<Typography className={Styles.label} variant="body2" color="primary.light">{t("playlists")}:</Typography>
|
|
58
|
+
<Tooltip title={t("totalPlaylistsCount", {num: _size(playlists)})} arrow enterDelay={500} leaveDelay={100} enterNextDelay={500}>
|
|
59
|
+
<Typography data-help="allPlaylists" className={Styles.value} variant="body2">{_size(playlists)}</Typography>
|
|
60
|
+
</Tooltip>
|
|
61
|
+
<Tooltip title={t("downloadedPlaylistsCount", {num: getGrabbedPlaylists()})} arrow enterDelay={500} leaveDelay={100} enterNextDelay={500}>
|
|
62
|
+
<Typography data-help="grabbedPlaylists" className={Styles.value} variant="body2">{`(${getGrabbedPlaylists()})`}</Typography>
|
|
63
|
+
</Tooltip>
|
|
64
|
+
<Typography className={Styles.label} variant="body2" color="primary.light">{t("tracks")}:</Typography>
|
|
65
|
+
<Tooltip title={t("totalTracksCount", {num: _size(tracks)})} arrow enterDelay={500} leaveDelay={100} enterNextDelay={500}>
|
|
66
|
+
<Typography data-help="allTracks" className={Styles.value} variant="body2">{_size(tracks)}</Typography>
|
|
67
|
+
</Tooltip>
|
|
68
|
+
<Tooltip title={t("downloadedTracksCount", {num: getGrabbedTracks()})} arrow enterDelay={500} leaveDelay={100} enterNextDelay={500}>
|
|
69
|
+
<Typography data-help="grabbedTracks" className={Styles.value} variant="body2">{`(${getGrabbedTracks()})`}</Typography>
|
|
70
|
+
</Tooltip>
|
|
71
|
+
</Stack>
|
|
72
|
+
<Stack direction="row" spacing={1} alignItems="end">
|
|
73
|
+
</Stack>
|
|
57
74
|
{state.loading &&
|
|
58
75
|
<Box className={Styles.progress}>
|
|
59
76
|
<LinearProgress className={Styles.progressBar} variant="determinate" color="primary" value={getTotalProgress()} />
|
|
@@ -172,27 +172,35 @@ export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) =>
|
|
|
172
172
|
<Grid>
|
|
173
173
|
<Stack direction="row" spacing={1} height={54}>
|
|
174
174
|
<Tooltip title={t("loadFromFile")} arrow enterDelay={2000} leaveDelay={100} enterNextDelay={500} placement="bottom">
|
|
175
|
-
<
|
|
176
|
-
<
|
|
177
|
-
|
|
175
|
+
<div>
|
|
176
|
+
<Button data-help="loadFromFile" disabled={loading} variant="contained" disableElevation color="secondary" onClick={() => handleOpenFromFile()}>
|
|
177
|
+
<FolderIcon/>
|
|
178
|
+
</Button>
|
|
179
|
+
</div>
|
|
178
180
|
</Tooltip>
|
|
179
181
|
<Tooltip title={t("loadInfo")} arrow enterDelay={2000} leaveDelay={100} enterNextDelay={500} placement="bottom">
|
|
180
|
-
<
|
|
181
|
-
<
|
|
182
|
-
|
|
182
|
+
<div>
|
|
183
|
+
<Button data-help="loadInfo" disabled={loading || _isEmpty(urls)} variant="contained" disableElevation color="secondary" onClick={() => onLoadInfo(urls)}>
|
|
184
|
+
<SearchIcon />
|
|
185
|
+
</Button>
|
|
186
|
+
</div>
|
|
183
187
|
</Tooltip>
|
|
184
188
|
{showDownloadFailed &&
|
|
185
189
|
<Tooltip title={t("downloadFailed")} arrow enterDelay={2000} leaveDelay={100} enterNextDelay={500} placement="bottom">
|
|
186
|
-
<
|
|
187
|
-
<
|
|
188
|
-
|
|
190
|
+
<div>
|
|
191
|
+
<Button data-help="downloadFailed" disabled={loading || _isEmpty(urls)} variant="contained" disableElevation color="secondary" onClick={onDownloadFailed}>
|
|
192
|
+
<ReplayIcon />
|
|
193
|
+
</Button>
|
|
194
|
+
</div>
|
|
189
195
|
</Tooltip>
|
|
190
196
|
}
|
|
191
197
|
{!loading &&
|
|
192
198
|
<Tooltip title={t("downloadAll")} arrow enterDelay={2000} leaveDelay={100} enterNextDelay={500} placement="bottom">
|
|
193
|
-
<
|
|
194
|
-
<
|
|
195
|
-
|
|
199
|
+
<div>
|
|
200
|
+
<Button data-help="downloadAll" disabled={loading || _isEmpty(urls)} variant="contained" disableElevation color="secondary" onClick={() => onDownload(urls)}>
|
|
201
|
+
<DownloadIcon />
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
196
204
|
</Tooltip>
|
|
197
205
|
}
|
|
198
206
|
{loading && !_isEmpty(playlists) &&
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@import "../../../styles/mixins.styl"
|
|
2
|
+
|
|
3
|
+
.log-menu {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: row;
|
|
6
|
+
justify-content: start;
|
|
7
|
+
align-items: center;
|
|
8
|
+
|
|
9
|
+
.log-button {
|
|
10
|
+
padding: 2px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.log-menu-list {
|
|
14
|
+
|
|
15
|
+
.log-entry {
|
|
16
|
+
|
|
17
|
+
.log-entry-icon {
|
|
18
|
+
min-width: 0;
|
|
19
|
+
padding-right: 12px;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import classnames from "classnames";
|
|
2
|
+
import _isEmpty from "lodash/isEmpty";
|
|
3
|
+
import _map from "lodash/map";
|
|
4
|
+
import React, {HTMLAttributes} from "react";
|
|
5
|
+
import {useTranslation} from "react-i18next";
|
|
6
|
+
|
|
7
|
+
import CancelIcon from "@mui/icons-material/Cancel";
|
|
8
|
+
import ErrorIcon from "@mui/icons-material/Error";
|
|
9
|
+
import {
|
|
10
|
+
Divider, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tooltip
|
|
11
|
+
} from "@mui/material";
|
|
12
|
+
|
|
13
|
+
import {useDataState} from "../../../react/contexts/DataContext";
|
|
14
|
+
import Styles from "./LogMenu.styl";
|
|
15
|
+
|
|
16
|
+
export type LogMenuProps = HTMLAttributes<HTMLDivElement>;
|
|
17
|
+
|
|
18
|
+
export const LogMenu: React.FC<LogMenuProps> = (props) => {
|
|
19
|
+
const {className, hidden} = props;
|
|
20
|
+
const {errors, warnings} = useDataState();
|
|
21
|
+
const [errorAnchorEl, setErrorAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
22
|
+
const [warningAnchorEl, setWarningAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
23
|
+
const errorMenuOpen = Boolean(errorAnchorEl);
|
|
24
|
+
const warningMenuOpen = Boolean(warningAnchorEl);
|
|
25
|
+
const {t} = useTranslation();
|
|
26
|
+
|
|
27
|
+
const handleErrorClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
28
|
+
setWarningAnchorEl(null);
|
|
29
|
+
setErrorAnchorEl(event.currentTarget);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleWarningClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
33
|
+
setErrorAnchorEl(null);
|
|
34
|
+
setWarningAnchorEl(event.currentTarget);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleErrorClose = () => {
|
|
38
|
+
setErrorAnchorEl(null);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleWarningClose = () => {
|
|
42
|
+
setWarningAnchorEl(null);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (hidden) return null;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={classnames(Styles.logMenu, className)} data-help="logMenu">
|
|
49
|
+
<Tooltip title={t("errors")} arrow enterDelay={500} leaveDelay={100} enterNextDelay={500}>
|
|
50
|
+
<div>
|
|
51
|
+
<IconButton className={Styles.logButton} color="error" disabled={_isEmpty(errors)} onClick={handleErrorClick}>
|
|
52
|
+
<CancelIcon />
|
|
53
|
+
</IconButton>
|
|
54
|
+
</div>
|
|
55
|
+
</Tooltip>
|
|
56
|
+
<Tooltip title={t("warnings")} arrow enterDelay={500} leaveDelay={100} enterNextDelay={500}>
|
|
57
|
+
<div>
|
|
58
|
+
<IconButton className={Styles.logButton} color="warning" disabled={_isEmpty(warnings)} onClick={handleWarningClick}>
|
|
59
|
+
<ErrorIcon />
|
|
60
|
+
</IconButton>
|
|
61
|
+
</div>
|
|
62
|
+
</Tooltip>
|
|
63
|
+
<Menu
|
|
64
|
+
anchorEl={errorAnchorEl}
|
|
65
|
+
open={errorMenuOpen}
|
|
66
|
+
onClose={handleErrorClose}
|
|
67
|
+
anchorReference="anchorEl"
|
|
68
|
+
anchorOrigin={{vertical: "top", horizontal: "center"}}
|
|
69
|
+
transformOrigin={{vertical: "bottom", horizontal: "center"}}
|
|
70
|
+
slotProps={{
|
|
71
|
+
list: {
|
|
72
|
+
className: Styles.logMenuList,
|
|
73
|
+
}
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
{_map(errors, (error, index) =>([
|
|
77
|
+
<MenuItem key={index} dense onClick={handleErrorClose} className={Styles.logEntry}>
|
|
78
|
+
<ListItemIcon className={Styles.logEntryIcon}>
|
|
79
|
+
<CancelIcon />
|
|
80
|
+
</ListItemIcon>
|
|
81
|
+
<ListItemText
|
|
82
|
+
primary={error.url}
|
|
83
|
+
secondary={error.message}
|
|
84
|
+
slotProps={{
|
|
85
|
+
primary: {
|
|
86
|
+
paddingBottom: .5
|
|
87
|
+
},
|
|
88
|
+
secondary: {
|
|
89
|
+
whiteSpace: "normal",
|
|
90
|
+
}
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
</MenuItem>,
|
|
94
|
+
index < errors.length - 1 && <Divider key={index + "_divider"} color="white" variant="middle" />
|
|
95
|
+
]))}
|
|
96
|
+
</Menu>
|
|
97
|
+
<Menu
|
|
98
|
+
anchorEl={warningAnchorEl}
|
|
99
|
+
open={warningMenuOpen}
|
|
100
|
+
onClose={handleWarningClose}
|
|
101
|
+
anchorReference="anchorEl"
|
|
102
|
+
anchorOrigin={{vertical: "top", horizontal: "center"}}
|
|
103
|
+
transformOrigin={{vertical: "bottom", horizontal: "center"}}
|
|
104
|
+
slotProps={{
|
|
105
|
+
list: {
|
|
106
|
+
className: Styles.logMenuList,
|
|
107
|
+
}
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{_map(warnings, (warning, index) =>([
|
|
111
|
+
<MenuItem key={index} dense onClick={handleWarningClose} className={Styles.logEntry}>
|
|
112
|
+
<ListItemIcon className={Styles.logEntryIcon}>
|
|
113
|
+
<ErrorIcon />
|
|
114
|
+
</ListItemIcon>
|
|
115
|
+
<ListItemText
|
|
116
|
+
primary={warning.url}
|
|
117
|
+
secondary={warning.message}
|
|
118
|
+
slotProps={{
|
|
119
|
+
primary: {
|
|
120
|
+
paddingBottom: .5
|
|
121
|
+
},
|
|
122
|
+
secondary: {
|
|
123
|
+
whiteSpace: "normal",
|
|
124
|
+
}
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
</MenuItem>,
|
|
128
|
+
index < warnings.length - 1 && <Divider key={index + "_divider"} color="white" variant="middle" />
|
|
129
|
+
]))}
|
|
130
|
+
</Menu>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export default LogMenu;
|
|
@@ -146,9 +146,11 @@ export const MediaInfoPanel: React.FC<MediaInfoPanelProps> = (props: MediaInfoPa
|
|
|
146
146
|
</Button>
|
|
147
147
|
</Tooltip>
|
|
148
148
|
<Tooltip title={t("downloadPlaylist")} arrow enterDelay={2000} leaveDelay={100} enterNextDelay={500} placement="top">
|
|
149
|
-
<
|
|
150
|
-
<
|
|
151
|
-
|
|
149
|
+
<div>
|
|
150
|
+
<Button disabled={_includes(queue, "load-single") || _includes(queue, "load-multi")} data-help="downloadPlaylist" className={Styles.download} size="large" fullWidth variant="contained" color="secondary" disableElevation onClick={downloadPlaylist}>
|
|
151
|
+
<DownloadIcon />
|
|
152
|
+
</Button>
|
|
153
|
+
</div>
|
|
152
154
|
</Tooltip>
|
|
153
155
|
</Box>
|
|
154
156
|
}
|
|
@@ -266,9 +266,11 @@ export const TrackList: React.FC<TrackListProps> = (props: TrackListProps) => {
|
|
|
266
266
|
</Tooltip>
|
|
267
267
|
{!_includes(queue, item.id) &&
|
|
268
268
|
<Tooltip title={t("download")} arrow enterDelay={2000} leaveDelay={100} enterNextDelay={500} placement="top">
|
|
269
|
-
<
|
|
270
|
-
<
|
|
271
|
-
|
|
269
|
+
<div>
|
|
270
|
+
<Button disabled={_includes(queue, "load-single") || _includes(queue, "load-multi")} data-help="downloadTrack" className={Styles.trackAction} size="small" color="primary" disableElevation variant="contained" data-id={item.id} onClick={onDownloadTrackClick}>
|
|
271
|
+
<DownloadIcon />
|
|
272
|
+
</Button>
|
|
273
|
+
</div>
|
|
272
274
|
</Tooltip>
|
|
273
275
|
}
|
|
274
276
|
{_includes(queue, item.id) &&
|
package/src/hooks/useHelp.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
|
|
2
|
+
import _toInteger from "lodash/toInteger";
|
|
3
|
+
import _toString from "lodash/toString";
|
|
2
4
|
import {useEffect, useState} from "react";
|
|
3
5
|
import {useTranslation} from "react-i18next";
|
|
4
6
|
|
|
@@ -11,6 +13,65 @@ const useHelp = () => {
|
|
|
11
13
|
const [help, setHelp] = useState<{header: string, content: string | string[];}>({header: "", content: ""});
|
|
12
14
|
const {t} = useTranslation();
|
|
13
15
|
|
|
16
|
+
const createBackdropElement = (background = "rgba(0, 0, 0, 0.5)", filter = "blur(2px)") => {
|
|
17
|
+
const backdrop = document.createElement("div");
|
|
18
|
+
|
|
19
|
+
backdrop.style.position = "absolute";
|
|
20
|
+
backdrop.style.width = "100vw";
|
|
21
|
+
backdrop.style.height = "100vh";
|
|
22
|
+
backdrop.style.top = "0";
|
|
23
|
+
backdrop.style.left = "0";
|
|
24
|
+
backdrop.style.zIndex = "1";
|
|
25
|
+
backdrop.style.background = background;
|
|
26
|
+
backdrop.style.backdropFilter = filter;
|
|
27
|
+
|
|
28
|
+
document.body.appendChild(backdrop);
|
|
29
|
+
|
|
30
|
+
return backdrop;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const createAnchorElement = (original: HTMLElement, zIndex = 999) => {
|
|
34
|
+
const bbox = original.getBoundingClientRect();
|
|
35
|
+
const clone = original.cloneNode(true) as HTMLElement;
|
|
36
|
+
|
|
37
|
+
const copyComputedStyle = (src: HTMLElement, dest: HTMLElement) => {
|
|
38
|
+
const computedStyle = window.getComputedStyle(src);
|
|
39
|
+
|
|
40
|
+
for (const key of computedStyle) {
|
|
41
|
+
try {
|
|
42
|
+
dest.style[key as any] = computedStyle.getPropertyValue(key);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.log(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const traverseAndCopy = (srcNode: HTMLElement, destNode: HTMLElement, disableEvents = true) => {
|
|
50
|
+
copyComputedStyle(srcNode, destNode);
|
|
51
|
+
|
|
52
|
+
if (disableEvents) {
|
|
53
|
+
destNode.style.pointerEvents = "none";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const srcChildren = srcNode.children as HTMLCollectionOf<HTMLElement>;
|
|
57
|
+
const destChildren = destNode.children as HTMLCollectionOf<HTMLElement>;
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < srcChildren.length; i++) {
|
|
60
|
+
traverseAndCopy(srcChildren[i], destChildren[i]);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
traverseAndCopy(original, clone);
|
|
65
|
+
document.body.appendChild(clone);
|
|
66
|
+
|
|
67
|
+
clone.style.position = "fixed";
|
|
68
|
+
clone.style.left = bbox.x - clone.offsetLeft + "px";
|
|
69
|
+
clone.style.top = bbox.y - clone.offsetTop + "px";
|
|
70
|
+
clone.style.zIndex = _toString(zIndex + 1);
|
|
71
|
+
|
|
72
|
+
return clone;
|
|
73
|
+
};
|
|
74
|
+
|
|
14
75
|
const handleClick = (event: MouseEvent) => {
|
|
15
76
|
const element = event.target as HTMLElement;
|
|
16
77
|
const elementHelp = element.closest<HTMLElement>("[data-help]");
|
|
@@ -25,11 +86,12 @@ const useHelp = () => {
|
|
|
25
86
|
|
|
26
87
|
const header = t(helpId + "Header", {ns: "help"});
|
|
27
88
|
const content: string | string[] = t(helpId + "Content", {ns: "help", returnObjects: true}) as string | string[];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
89
|
+
|
|
90
|
+
const backdropElement = createBackdropElement();
|
|
91
|
+
const anchorElement = createAnchorElement(elementHelp, _toInteger(backdropElement.style.zIndex));
|
|
92
|
+
|
|
93
|
+
setBackdropEl(backdropElement);
|
|
94
|
+
setAnchorEl(anchorElement);
|
|
33
95
|
setHelp({header, content});
|
|
34
96
|
|
|
35
97
|
event.stopPropagation();
|
|
@@ -43,33 +105,21 @@ const useHelp = () => {
|
|
|
43
105
|
|
|
44
106
|
const handleKeyUpEvent = (event: KeyboardEvent) => {
|
|
45
107
|
if (event.key !== "Escape") return;
|
|
46
|
-
|
|
108
|
+
|
|
47
109
|
actions.setHelp(false);
|
|
48
110
|
setBackdropEl(null);
|
|
49
111
|
setAnchorEl(null);
|
|
50
112
|
};
|
|
51
113
|
|
|
52
114
|
useEffect(() => {
|
|
53
|
-
if (backdropEl) {
|
|
54
|
-
document.body.appendChild(backdropEl);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
115
|
return () => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
document.body.removeChild(backdropEl);
|
|
116
|
+
backdropEl?.remove();
|
|
61
117
|
};
|
|
62
118
|
}, [backdropEl]);
|
|
63
119
|
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
if (anchorEl) {
|
|
66
|
-
anchorEl.style.zIndex = "99";
|
|
67
|
-
}
|
|
68
|
-
|
|
120
|
+
useEffect(() => {
|
|
69
121
|
return () => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
anchorEl.style.zIndex = "initial";
|
|
122
|
+
anchorEl?.remove();
|
|
73
123
|
};
|
|
74
124
|
}, [anchorEl]);
|
|
75
125
|
|
|
@@ -84,9 +134,6 @@ const useHelp = () => {
|
|
|
84
134
|
document.removeEventListener("mouseup", handleMouseEvent, true);
|
|
85
135
|
document.removeEventListener("mousedown", handleMouseEvent, true);
|
|
86
136
|
document.removeEventListener("keyup", handleKeyUpEvent, true);
|
|
87
|
-
if (anchorEl) {
|
|
88
|
-
anchorEl.style.zIndex = "initial";
|
|
89
|
-
}
|
|
90
137
|
}
|
|
91
138
|
|
|
92
139
|
return () => {
|
|
@@ -94,9 +141,6 @@ const useHelp = () => {
|
|
|
94
141
|
document.removeEventListener("mouseup", handleMouseEvent, true);
|
|
95
142
|
document.removeEventListener("mousedown", handleMouseEvent, true);
|
|
96
143
|
document.removeEventListener("keyup", handleKeyUpEvent, true);
|
|
97
|
-
if (anchorEl) {
|
|
98
|
-
anchorEl.style.zIndex = "initial";
|
|
99
|
-
}
|
|
100
144
|
};
|
|
101
145
|
}, [state]);
|
|
102
146
|
|
|
@@ -13,6 +13,11 @@ export type Task = {
|
|
|
13
13
|
dirty?: boolean;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export type LogEntry = {
|
|
17
|
+
url: string;
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
16
21
|
export type DataState = {
|
|
17
22
|
playlists: PlaylistInfo[];
|
|
18
23
|
tracks: TrackInfo[];
|
|
@@ -24,6 +29,8 @@ export type DataState = {
|
|
|
24
29
|
operation: string;
|
|
25
30
|
activeTab: string;
|
|
26
31
|
queue: string[];
|
|
32
|
+
errors: LogEntry[];
|
|
33
|
+
warnings: LogEntry[];
|
|
27
34
|
|
|
28
35
|
setPlaylists: React.Dispatch<React.SetStateAction<PlaylistInfo[]>>;
|
|
29
36
|
setTracks: React.Dispatch<React.SetStateAction<TrackInfo[]>>;
|
|
@@ -35,6 +42,8 @@ export type DataState = {
|
|
|
35
42
|
setQueue: React.Dispatch<React.SetStateAction<string[]>>;
|
|
36
43
|
setOperation: React.Dispatch<React.SetStateAction<string>>;
|
|
37
44
|
setActiveTab: React.Dispatch<React.SetStateAction<string>>;
|
|
45
|
+
setErrors: React.Dispatch<React.SetStateAction<LogEntry[]>>;
|
|
46
|
+
setWarnings: React.Dispatch<React.SetStateAction<LogEntry[]>>;
|
|
38
47
|
clear: () => void;
|
|
39
48
|
};
|
|
40
49
|
|
|
@@ -52,6 +61,8 @@ export function DataProvider(props: any) {
|
|
|
52
61
|
const [queue, setQueue] = useState<string[]>([]);
|
|
53
62
|
const [operation, setOperation] = useState<string>();
|
|
54
63
|
const [activeTab, setActiveTab] = useState<string>();
|
|
64
|
+
const [errors, setErrors] = useState<LogEntry[]>([]);
|
|
65
|
+
const [warnings, setWarnings] = useState<LogEntry[]>([]);
|
|
55
66
|
|
|
56
67
|
const clear = () => {
|
|
57
68
|
setPlaylists([]);
|
|
@@ -62,6 +73,8 @@ export function DataProvider(props: any) {
|
|
|
62
73
|
setQueue([]);
|
|
63
74
|
setFormats({global: {type: MediaFormat.Audio, extension: AudioType.Mp3, audioQuality: 0}});
|
|
64
75
|
setOperation(undefined);
|
|
76
|
+
setErrors([]);
|
|
77
|
+
setWarnings([]);
|
|
65
78
|
};
|
|
66
79
|
|
|
67
80
|
return (
|
|
@@ -76,7 +89,9 @@ export function DataProvider(props: any) {
|
|
|
76
89
|
queue,
|
|
77
90
|
operation,
|
|
78
91
|
activeTab,
|
|
79
|
-
|
|
92
|
+
errors,
|
|
93
|
+
warnings,
|
|
94
|
+
|
|
80
95
|
setOperation,
|
|
81
96
|
setPlaylists,
|
|
82
97
|
setAutoDownload,
|
|
@@ -87,6 +102,8 @@ export function DataProvider(props: any) {
|
|
|
87
102
|
setUrls,
|
|
88
103
|
setQueue,
|
|
89
104
|
setActiveTab,
|
|
105
|
+
setErrors,
|
|
106
|
+
setWarnings,
|
|
90
107
|
clear
|
|
91
108
|
}}>
|
|
92
109
|
{props.children}
|
|
Binary file
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"formatScopeHeader": "Formatbereich",
|
|
66
66
|
"formatScopeContent": [
|
|
67
67
|
"Legen Sie den Geltungsbereich des ausgewählten Formats für Downloads fest.",
|
|
68
|
-
"Wenn der Bereich `
|
|
69
|
-
"Wenn der Bereich `
|
|
68
|
+
"Wenn der Bereich `Allgemein` ist, wird das ausgewählte Format auf alle heruntergeladenen Dateien angewendet, sobald der Download gestartet wird.",
|
|
69
|
+
"Wenn der Bereich `Für jeden Tab unterschiedlich` ist, können Sie für jeden Tab ein anderes Format festlegen, sodass die zugehörigen Downloads unterschiedliche Ausgabeformate haben."
|
|
70
70
|
],
|
|
71
71
|
"alwaysOverwriteHeader": "Dateiüberschreibung",
|
|
72
72
|
"alwaysOverwriteContent": "Legt fest, ob vorhandene heruntergeladene Dateien überschrieben werden.\nWenn deaktiviert, wird kein Download durchgeführt, wenn die entsprechende Datei bereits vorhanden ist.",
|
|
@@ -116,5 +116,19 @@
|
|
|
116
116
|
"allTracksHeader": "Titel",
|
|
117
117
|
"allTracksContent": "Anzahl aller geladenen Titel.",
|
|
118
118
|
"grabbedTracksHeader": "Heruntergeladene Titel",
|
|
119
|
-
"grabbedTracksContent": "Anzahl der heruntergeladenen Titel."
|
|
119
|
+
"grabbedTracksContent": "Anzahl der heruntergeladenen Titel.",
|
|
120
|
+
"logMenuHeader": "Fehler und Warnungen",
|
|
121
|
+
"logMenuContent": "Zeigt Fehler und Warnungen im Zusammenhang mit dem Downloadvorgang an.",
|
|
122
|
+
"downloadFailedHeader": "Download fehlgeschlagen",
|
|
123
|
+
"downloadFailedContent": "Versucht, fehlgeschlagene Elemente erneut herunterzuladen.",
|
|
124
|
+
"cancellAllHeader": "Alle Downloads abbrechen",
|
|
125
|
+
"cancellAllContent": "Alle aktiven und wartenden Downloads abbrechen.",
|
|
126
|
+
"cancelDownloadPlaylistHeader": "Playlist-Download abbrechen",
|
|
127
|
+
"cancelDownloadPlaylistContent": "Alle aktiven und wartenden Downloads, die mit dieser Playlist verbunden sind, abbrechen.",
|
|
128
|
+
"cancelDownloadTrackHeader": "Track-Download abbrechen",
|
|
129
|
+
"cancelDownloadTrackContent": "Allen Sie den Download des Titels ab.",
|
|
130
|
+
"findInFileSystemHeader": "In Dateisystem suchen",
|
|
131
|
+
"findInFileSystemContent": "Offnet die zugehörige Datei im Dateisystem.",
|
|
132
|
+
"openOutputDirectoryHeader": "Ausgabeverzeichnis öffnen",
|
|
133
|
+
"openOutputDirectoryContent": "Offnet das Ausgabeverzeichnis im Datei-Explorer."
|
|
120
134
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"albumOutputTemplate": "Ausgabenvorlage (Alben)",
|
|
3
|
-
"alwaysOverwrite": "
|
|
3
|
+
"alwaysOverwrite": "Heruntergeladene Dateien überschreiben",
|
|
4
4
|
"artist": "Künstler",
|
|
5
5
|
"audioQuality": "Audio-Qualität",
|
|
6
6
|
"cancel": "Stornieren",
|
|
@@ -13,15 +13,18 @@
|
|
|
13
13
|
"cut": "schneiden",
|
|
14
14
|
"debugMode": "Debug-Modus",
|
|
15
15
|
"detailsModalTitle": "Mediendetails bearbeiten",
|
|
16
|
-
"discographyDownload": "Diskografie-
|
|
16
|
+
"discographyDownload": "Diskografie-downloadAll",
|
|
17
17
|
"done": "Fertig",
|
|
18
18
|
"download": "Herunterladen",
|
|
19
19
|
"downloadAll": "Alles herunterladen",
|
|
20
|
+
"downloadedPlaylistsCount": "Heruntergeladene Wiedergabelisten: {{num}}",
|
|
21
|
+
"downloadedTracksCount": "Heruntergeladene Spuren: {{num}}",
|
|
20
22
|
"downloadFailed": "Falsch Herunterladen",
|
|
21
23
|
"downloading": "Herunterladen läuft",
|
|
22
24
|
"downloadPlaylist": "Playlist herunterladen",
|
|
23
25
|
"duration": "Dauer",
|
|
24
26
|
"edit": "Bearbeiten",
|
|
27
|
+
"errors": "Fehler",
|
|
25
28
|
"exceptionGetYoutubeUrls": "GetYoutubeUrls: Fehler",
|
|
26
29
|
"exceptionGetYoutubeUrlsText": "Beim Laden der Ressourcen von YouTube ist ein Fehler aufgetreten",
|
|
27
30
|
"exceptionTimeout": "Timeout-Fehler",
|
|
@@ -29,7 +32,7 @@
|
|
|
29
32
|
"extractingAudio": "Audio Extrahieren",
|
|
30
33
|
"findFileInSystem": "Im Datei-Explorer anzeigen",
|
|
31
34
|
"format": "Format",
|
|
32
|
-
"formatScope": "
|
|
35
|
+
"formatScope": "Download-Parameterumfang",
|
|
33
36
|
"formatScopeGlobal": "Allgemein",
|
|
34
37
|
"formatScopeTab": "Für jeden Tab unterschiedlich",
|
|
35
38
|
"from": "von",
|
|
@@ -40,6 +43,9 @@
|
|
|
40
43
|
"mediaType": "Typ",
|
|
41
44
|
"merging": "Zusammenführung",
|
|
42
45
|
"missingMediaInfoError": "Fehlende Medieninformationen",
|
|
46
|
+
"modeDark": "Dunkel",
|
|
47
|
+
"modeLight": "Licht",
|
|
48
|
+
"modeSystem": "System",
|
|
43
49
|
"ok": "OK",
|
|
44
50
|
"openInBrowser": "Im Browser öffnen",
|
|
45
51
|
"openOutputDirectory": "Verzeichnis im System anzeigen",
|
|
@@ -57,8 +63,11 @@
|
|
|
57
63
|
"themeMode": "Designmodus",
|
|
58
64
|
"title": "Titel",
|
|
59
65
|
"to": "bis",
|
|
66
|
+
"totalPlaylistsCount": "Alle Wiedergabelisten: {{num}}",
|
|
67
|
+
"totalTracksCount": "Alle Spuren: {{num}}",
|
|
60
68
|
"trackOutputTemplate": "Ausgabevorlage (Audiospuren)",
|
|
61
69
|
"tracks": "Spuren",
|
|
62
70
|
"videoOutputTemplate": "Ausgabevorlage (videos)",
|
|
71
|
+
"warnings": "Warnungen",
|
|
63
72
|
"youtubeUrl": "YouTube-URL"
|
|
64
73
|
}
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"formatScopeHeader": "Format Scope",
|
|
66
66
|
"formatScopeContent": [
|
|
67
67
|
"Specify scope of format selected for downloads.",
|
|
68
|
-
"If scope is `
|
|
69
|
-
"If scope is `
|
|
68
|
+
"If scope is `Global` then format you select will be applied to all downloaded files once download is started.",
|
|
69
|
+
"If scope is `Different for each tab` then you can specify different format for each tab so that related downloads will have different output formats."
|
|
70
70
|
],
|
|
71
71
|
"alwaysOverwriteHeader": "File Overwriting",
|
|
72
72
|
"alwaysOverwriteContent": "Specifies whether existing downloaded files are overwritten.\nIf disabled, no download is performed when related file is already present.",
|
|
@@ -116,5 +116,19 @@
|
|
|
116
116
|
"allTracksHeader": "Tracks",
|
|
117
117
|
"allTracksContent": "Number of all loaded tracks.",
|
|
118
118
|
"grabbedTracksHeader": "Grabbed Tracks",
|
|
119
|
-
"grabbedTracksContent": "Number of downloaded tracks."
|
|
119
|
+
"grabbedTracksContent": "Number of downloaded tracks.",
|
|
120
|
+
"logMenuHeader": "Errors and Warnings",
|
|
121
|
+
"logMenuContent": "Displays errors and warnings related to downloading process.",
|
|
122
|
+
"downloadFailedHeader": "Download Failed",
|
|
123
|
+
"downloadFailedContent": "Attempts to download failed items again.",
|
|
124
|
+
"cancellAllHeader": "Cancel All Downloads",
|
|
125
|
+
"cancellAllContent": "Cancels all active and queued downloads.",
|
|
126
|
+
"cancelDownloadPlaylistHeader": "Cancel Playlist Download",
|
|
127
|
+
"cancelDownloadPlaylistContent": "Cancels all active and queued downloads related to this playlist.",
|
|
128
|
+
"cancelDownloadTrackHeader": "Cancel Track Download",
|
|
129
|
+
"cancelDownloadTrackContent": "Cancels downlaoding track.",
|
|
130
|
+
"findInFileSystemHeader": "Find File",
|
|
131
|
+
"findInFileSystemContent": "Finds related file in the file system.",
|
|
132
|
+
"openOutputDirectoryHeader": "Open Output Directory",
|
|
133
|
+
"openOutputDirectoryContent": "Opens output directory in file explorer."
|
|
120
134
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"albumOutputTemplate": "Output template (albums)",
|
|
3
|
-
"alwaysOverwrite": "Overwrite
|
|
3
|
+
"alwaysOverwrite": "Overwrite downloaded files",
|
|
4
4
|
"artist": "Artist",
|
|
5
5
|
"audioQuality": "Audio Quality",
|
|
6
6
|
"cancel": "Cancel",
|
|
@@ -13,15 +13,18 @@
|
|
|
13
13
|
"cut": "cut",
|
|
14
14
|
"debugMode": "Debug Mode",
|
|
15
15
|
"detailsModalTitle": "Edit media details",
|
|
16
|
-
"discographyDownload": "Discography download
|
|
16
|
+
"discographyDownload": "Discography download",
|
|
17
17
|
"done": "Done",
|
|
18
18
|
"download": "Download",
|
|
19
19
|
"downloadAll": "Download All",
|
|
20
|
+
"downloadedPlaylistsCount": "Downloaded playlists: {{num}}",
|
|
21
|
+
"downloadedTracksCount": "Downloaded tracks: {{num}}",
|
|
20
22
|
"downloadFailed": "Download Failed",
|
|
21
23
|
"downloading": "Downloading",
|
|
22
24
|
"downloadPlaylist": "Download Playlist",
|
|
23
25
|
"duration": "Duration",
|
|
24
26
|
"edit": "Edit",
|
|
27
|
+
"errors": "Errors",
|
|
25
28
|
"exceptionGetYoutubeUrls": "GetYoutubeUrls: Error",
|
|
26
29
|
"exceptionGetYoutubeUrlsText": "Error occured while loading resources from youtube",
|
|
27
30
|
"exceptionTimeout": "Timeout error",
|
|
@@ -29,9 +32,9 @@
|
|
|
29
32
|
"extractingAudio": "Extracting Audio",
|
|
30
33
|
"findFileInSystem": "Reveal in file explorer",
|
|
31
34
|
"format": "Format",
|
|
32
|
-
"formatScope": "
|
|
35
|
+
"formatScope": "Download parameters scope",
|
|
33
36
|
"formatScopeGlobal": "Global",
|
|
34
|
-
"formatScopeTab": "
|
|
37
|
+
"formatScopeTab": "Different for each tab",
|
|
35
38
|
"from": "from",
|
|
36
39
|
"invalidTemplateKeys": "Invalid template keys: {{invalidKeys}}",
|
|
37
40
|
"langName": "English",
|
|
@@ -40,6 +43,9 @@
|
|
|
40
43
|
"mediaType": "Type",
|
|
41
44
|
"merging": "merging",
|
|
42
45
|
"missingMediaInfoError": "Missing media info",
|
|
46
|
+
"modeDark": "Dark",
|
|
47
|
+
"modeLight": "Light",
|
|
48
|
+
"modeSystem": "System",
|
|
43
49
|
"ok": "OK",
|
|
44
50
|
"openInBrowser": "Open in browser",
|
|
45
51
|
"openOutputDirectory": "Reveal directory in system",
|
|
@@ -57,8 +63,11 @@
|
|
|
57
63
|
"themeMode": "Theme Mode",
|
|
58
64
|
"title": "Title",
|
|
59
65
|
"to": "to",
|
|
66
|
+
"totalPlaylistsCount": "Total playlists: {{num}}",
|
|
67
|
+
"totalTracksCount": "Total tracks: {{num}}",
|
|
60
68
|
"trackOutputTemplate": "Output template (audio tracks)",
|
|
61
69
|
"tracks": "Tracks",
|
|
62
70
|
"videoOutputTemplate": "Output template (videos)",
|
|
71
|
+
"warnings": "Warnings",
|
|
63
72
|
"youtubeUrl": "YouTube URL"
|
|
64
73
|
}
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"formatScopeHeader": "Zakres formatu",
|
|
66
66
|
"formatScopeContent": [
|
|
67
67
|
"Określ zakres formatu wybranego dla pobrań.",
|
|
68
|
-
"Jeśli
|
|
69
|
-
"Jeśli
|
|
68
|
+
"Jeśli zaznaczono `Globalny`, wybrany format zostanie zastosowany do wszystkich pobieranych plików po rozpoczęciu pobierania.",
|
|
69
|
+
"Jeśli zaznaczono `Oddzielny dla każdej zakładki`, możesz określić inny format dla każdej karty, tak aby powiązane pobrania miały różne formaty wyjściowe."
|
|
70
70
|
],
|
|
71
71
|
"alwaysOverwriteHeader": "Nadpisywanie plików",
|
|
72
72
|
"alwaysOverwriteContent": "Określa, czy istniejące pobrane pliki są nadpisywane.\nJeśli wyłączone, pobieranie nie zostanie wykonane, gdy powiązany plik już istnieje.",
|
|
@@ -116,5 +116,19 @@
|
|
|
116
116
|
"allTracksHeader": "Utwory",
|
|
117
117
|
"allTracksContent": "Liczba wszystkich wczytanych utworów.",
|
|
118
118
|
"grabbedTracksHeader": "Pobrane utwory",
|
|
119
|
-
"grabbedTracksContent": "Liczba pobranych utworów."
|
|
119
|
+
"grabbedTracksContent": "Liczba pobranych utworów.",
|
|
120
|
+
"logMenuHeader": "Błędy i ostrzeżenia",
|
|
121
|
+
"logMenuContent": "Wyświetla błędy i ostrzeżenia związane z procesem pobierania.",
|
|
122
|
+
"downloadFailedHeader": "Pobierz niuedane",
|
|
123
|
+
"downloadFailedContent": "Próbuje ponownie pobrać nieudane elementy.",
|
|
124
|
+
"cancellAllHeader": "Anuluj wszystkie pobierania",
|
|
125
|
+
"cancellAllContent": "Anuluj wszystkie aktywne i oczekujące pobrania.",
|
|
126
|
+
"cancelDownloadPlaylistHeader": "Anuluj pobieranie playlisty",
|
|
127
|
+
"cancelDownloadPlaylistContent": "Anuluj wszystkie aktywne i oczekujące pobrania związane z tą playlistą.",
|
|
128
|
+
"cancelDownloadTrackHeader": "Anuluj pobieranie utworu",
|
|
129
|
+
"cancelDownloadTrackContent": "Anuluj pobieranie utworu.",
|
|
130
|
+
"findInFileSystemHeader": "Znajdź plik",
|
|
131
|
+
"findInFileSystemContent": "Znajdź powiązany plik w systemie plików.",
|
|
132
|
+
"openOutputDirectoryHeader": "Otwórz katalog wyjściowy",
|
|
133
|
+
"openOutputDirectoryContent": "Otwiera katalog wyjściowy w eksploratorze plików."
|
|
120
134
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"albumOutputTemplate": "Szablon wyjściowy (albumy)",
|
|
3
|
-
"alwaysOverwrite": "Nadpisuj
|
|
3
|
+
"alwaysOverwrite": "Nadpisuj pobrane pliki",
|
|
4
4
|
"artist": "Wykonawca",
|
|
5
5
|
"audioQuality": "Jakość audio",
|
|
6
6
|
"cancel": "Anuluj",
|
|
@@ -13,15 +13,18 @@
|
|
|
13
13
|
"cut": "przytnij",
|
|
14
14
|
"debugMode": "Tryb debugowania",
|
|
15
15
|
"detailsModalTitle": "Edycja danych",
|
|
16
|
-
"discographyDownload": "Pobieranie dyskografii
|
|
16
|
+
"discographyDownload": "Pobieranie dyskografii",
|
|
17
17
|
"done": "Gotowe",
|
|
18
18
|
"download": "Pobierz",
|
|
19
19
|
"downloadAll": "Pobierz wszystko",
|
|
20
|
+
"downloadedPlaylistsCount": "Pobrane playlisty: {{num}}",
|
|
21
|
+
"downloadedTracksCount": "Pobrane utwory: {{num}}",
|
|
20
22
|
"downloadFailed": "Pobierz błędne",
|
|
21
23
|
"downloading": "Pobieranie",
|
|
22
24
|
"downloadPlaylist": "Pobierz playlistę",
|
|
23
25
|
"duration": "Czas trwania",
|
|
24
26
|
"edit": "Edytuj",
|
|
27
|
+
"errors": "Błędy",
|
|
25
28
|
"exceptionGetYoutubeUrls": "GetYoutubeUrls: Błąd",
|
|
26
29
|
"exceptionGetYoutubeUrlsText": "Wystąpił błąd podczas wczytywania danych z YouTube",
|
|
27
30
|
"exceptionTimeout": "Błąd czasu oczekiwania",
|
|
@@ -29,7 +32,7 @@
|
|
|
29
32
|
"extractingAudio": "Wyodrębnianie dźwięku",
|
|
30
33
|
"findFileInSystem": "Pokaż w eksploratorze plików",
|
|
31
34
|
"format": "Format",
|
|
32
|
-
"formatScope": "Zakres
|
|
35
|
+
"formatScope": "Zakres parametrów pobierania",
|
|
33
36
|
"formatScopeGlobal": "Globalny",
|
|
34
37
|
"formatScopeTab": "Oddzielny dla każdej zakładki",
|
|
35
38
|
"from": "start",
|
|
@@ -40,6 +43,9 @@
|
|
|
40
43
|
"mediaType": "Typ",
|
|
41
44
|
"merging": "Łączenie",
|
|
42
45
|
"missingMediaInfoError": "Metadane są niekompletne",
|
|
46
|
+
"modeDark": "Ciemny",
|
|
47
|
+
"modeLight": "Jasny",
|
|
48
|
+
"modeSystem": "Systemowy",
|
|
43
49
|
"ok": "OK",
|
|
44
50
|
"openInBrowser": "Otwórz w przeglądarce",
|
|
45
51
|
"openOutputDirectory": "Pokaż katalog w systemie",
|
|
@@ -57,8 +63,11 @@
|
|
|
57
63
|
"themeMode": "Tryb motywu",
|
|
58
64
|
"title": "Tytuł",
|
|
59
65
|
"to": "koniec",
|
|
66
|
+
"totalPlaylistsCount": "Dostępne playlisty: {{num}}",
|
|
67
|
+
"totalTracksCount": "Dostepne utwory: {{num}}",
|
|
60
68
|
"trackOutputTemplate": "Szablon wyjściowy (pliki audio)",
|
|
61
69
|
"tracks": "Ścieżki",
|
|
62
70
|
"videoOutputTemplate": "Szablon wyjściowy (pliki wideo)",
|
|
71
|
+
"warnings": "Ostrzeżenia",
|
|
63
72
|
"youtubeUrl": "YouTube URL"
|
|
64
73
|
}
|
|
@@ -11,11 +11,14 @@ import _isArray from "lodash/isArray";
|
|
|
11
11
|
import _isEmpty from "lodash/isEmpty";
|
|
12
12
|
import _isNaN from "lodash/isNaN";
|
|
13
13
|
import _isNil from "lodash/isNil";
|
|
14
|
+
import _join from "lodash/join";
|
|
14
15
|
import _map from "lodash/map";
|
|
15
16
|
import _min from "lodash/min";
|
|
16
17
|
import _size from "lodash/size";
|
|
17
18
|
import _some from "lodash/some";
|
|
19
|
+
import _split from "lodash/split";
|
|
18
20
|
import _times from "lodash/times";
|
|
21
|
+
import _trim from "lodash/trim";
|
|
19
22
|
import _uniq from "lodash/uniq";
|
|
20
23
|
import path from "path";
|
|
21
24
|
import {LaunchOptions} from "puppeteer";
|
|
@@ -25,7 +28,6 @@ import {useDebounceValue} from "usehooks-ts";
|
|
|
25
28
|
import YTDlpWrap, {Progress as YtDlpProgress} from "yt-dlp-wrap";
|
|
26
29
|
|
|
27
30
|
import {Alert, Box, Grid} from "@mui/material";
|
|
28
|
-
import {BusyLight} from "@pureit/busylight";
|
|
29
31
|
|
|
30
32
|
import {getBinPath} from "../../common/FileSystem";
|
|
31
33
|
import {getAlbumInfo} from "../../common/Formatters";
|
|
@@ -50,7 +52,25 @@ const abortControllers: {[key: string]: AbortController} = {};
|
|
|
50
52
|
|
|
51
53
|
export const HomeView: React.FC = () => {
|
|
52
54
|
const [appOptions, setAppOptions] = useState<ApplicationOptions>(global.store.get("application"));
|
|
53
|
-
const {
|
|
55
|
+
const {
|
|
56
|
+
operation,
|
|
57
|
+
playlists,
|
|
58
|
+
tracks,
|
|
59
|
+
trackStatus,
|
|
60
|
+
trackCuts,
|
|
61
|
+
formats,
|
|
62
|
+
autoDownload,
|
|
63
|
+
queue,
|
|
64
|
+
setOperation,
|
|
65
|
+
setPlaylists,
|
|
66
|
+
setTracks,
|
|
67
|
+
setTrackStatus,
|
|
68
|
+
setAutoDownload,
|
|
69
|
+
setQueue,
|
|
70
|
+
setErrors,
|
|
71
|
+
setWarnings,
|
|
72
|
+
clear
|
|
73
|
+
} = useDataState();
|
|
54
74
|
const {state, actions} = useAppContext();
|
|
55
75
|
const [error, setError] = useState(false);
|
|
56
76
|
const [abort, setAbort] = useState<string>();
|
|
@@ -105,13 +125,6 @@ export const HomeView: React.FC = () => {
|
|
|
105
125
|
|
|
106
126
|
|
|
107
127
|
useEffect(() => {
|
|
108
|
-
const devices = BusyLight.devices();
|
|
109
|
-
const light = new BusyLight(devices[0]);
|
|
110
|
-
light.connect();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// light.blink("#000044", 5, 2);
|
|
114
|
-
light.alert(2, 1, "#ff0000", true, 5, 3);
|
|
115
128
|
ipcRenderer.on("get-youtube-urls-progress", onGetYoutubeUrlsCompleted);
|
|
116
129
|
|
|
117
130
|
return () => {
|
|
@@ -142,11 +155,19 @@ export const HomeView: React.FC = () => {
|
|
|
142
155
|
setAppOptions((prev) => ({...prev, urls}));
|
|
143
156
|
};
|
|
144
157
|
|
|
145
|
-
const update = (item: YoutubeInfoResult) => {
|
|
146
|
-
if (item.
|
|
158
|
+
const update = (item: YoutubeInfoResult) => {
|
|
159
|
+
if (!_isEmpty(item.warnings)) {
|
|
160
|
+
setWarnings((prev) => [...prev, {url: item.url, message: _join(item.warnings, "\n")}]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!_isEmpty(item.errors)) {
|
|
164
|
+
setErrors((prev) => [...prev, {url: item.url, message: _join(item.errors, "\n")}]);
|
|
165
|
+
}
|
|
147
166
|
|
|
148
|
-
|
|
149
|
-
|
|
167
|
+
if (item.value) {
|
|
168
|
+
setTracks((prev) => [...prev, ...item.value]);
|
|
169
|
+
setPlaylists((prev) => [...prev, {url: item.url, album: getAlbumInfo(item.value, item.url), tracks: item.value}]);
|
|
170
|
+
}
|
|
150
171
|
};
|
|
151
172
|
|
|
152
173
|
const loadInfo = (urls: string[]) => {
|
|
@@ -205,18 +226,19 @@ export const HomeView: React.FC = () => {
|
|
|
205
226
|
return resolveMockData(300);
|
|
206
227
|
} else {
|
|
207
228
|
return _map(urls, (url) => new Promise<YoutubeInfoResult>((resolve) => {
|
|
208
|
-
ytDlpWrap.
|
|
229
|
+
ytDlpWrap.execPromise([url, "--dump-json", "--no-check-certificate", "--geo-bypass"])
|
|
209
230
|
.then((result) => {
|
|
210
|
-
|
|
231
|
+
const parsed = _map(_split(_trim(result), "\n"), (item) => JSON.parse(item));
|
|
232
|
+
|
|
233
|
+
resolve({url, value: _isArray(parsed) ? parsed : [parsed]});
|
|
211
234
|
})
|
|
212
235
|
.catch((e) => {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
236
|
+
const warningRegex = /WARNING:\s([\s\S]*?)(?=ERROR|WARNING|$)/gm;
|
|
237
|
+
const errorRegex = /ERROR:\s([\s\S]*?)(?=ERROR|WARNING|$)/gm;
|
|
238
|
+
const warningMatches = e.message.match(warningRegex) ?? [];
|
|
239
|
+
const errorMatches = e.message.match(errorRegex) ?? [];
|
|
218
240
|
|
|
219
|
-
resolve({url,
|
|
241
|
+
resolve({url, errors: _uniq(errorMatches), warnings: _uniq(warningMatches)});
|
|
220
242
|
});
|
|
221
243
|
}));
|
|
222
244
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
declare module "@pureit/busylight";
|