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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-grabber",
3
- "version": "1.1.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
  }
@@ -3,7 +3,8 @@ import {MediaFormat} from "./Media";
3
3
  export type YoutubeInfoResult = {
4
4
  url: string,
5
5
  value?: TrackInfo[];
6
- error?: string,
6
+ errors?: string[],
7
+ warnings?: string[],
7
8
  };
8
9
 
9
10
  export type TrackInfo = {
@@ -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: .15;
34
+ opacity: .5;
36
35
  vendor("transition", opacity .25s ease-in-out);
37
36
 
38
37
  &:hover {
39
- opacity: .35;
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
- <IconButton onClick={handleClose} color="inherit" className={Styles.icon}>
60
- <CloseIcon />
61
- </IconButton>
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
- <IconButton data-help="settings" disabled={disableNavigation} onClick={handleOpenSettings} color="inherit" className={Styles.icon}>
66
- <SettingsIcon />
67
- </IconButton>
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}><HelpIcon/></IconButton>
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, {HTMLProps, useState} from "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<HTMLProps<HTMLDivElement>, "onClick"> & {
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<HTMLProps<HTMLButtonElement>, "onClick"> & LanguagePickerProps & {
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
- <div className={classnames(Styles.languagePicker,className)} {...rest}>
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
- </div>
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
- <div>{item}</div>
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
- <Typography className={Styles.label} variant="body2" color="primary.light">{t("playlists")}:</Typography>
52
- <Typography data-help="allPlaylists" className={Styles.value} variant="body2">{_size(playlists)}</Typography>
53
- <Typography data-help="grabbedPlaylists" className={Styles.value} variant="body2">{`(${getGrabbedPlaylists()})`}</Typography>
54
- <Typography className={Styles.label} variant="body2" color="primary.light">{t("tracks")}:</Typography>
55
- <Typography data-help="allTracks" className={Styles.value} variant="body2">{_size(tracks)}</Typography>
56
- <Typography data-help="grabbedTracks" className={Styles.value} variant="body2">{`(${getGrabbedTracks()})`}</Typography>
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()} />
@@ -1,8 +1,15 @@
1
1
  .input-panel {
2
2
  display: flex;
3
3
 
4
- > :global(.MuiGrid2-root) {
5
- display: flex;
4
+ > :global(.MuiGrid-root) {
5
+
6
+ > :global(.MuiStack-root) {
7
+
8
+ > * {
9
+ display: flex;
10
+ }
11
+ }
12
+
6
13
  }
7
14
 
8
15
  .mode-buttons {
@@ -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
- <Button data-help="loadFromFile" disabled={loading} variant="contained" disableElevation color="secondary" onClick={() => handleOpenFromFile()}>
176
- <FolderIcon/>
177
- </Button>
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
- <Button data-help="loadInfo" disabled={loading || _isEmpty(urls)} variant="contained" disableElevation color="secondary" onClick={() => onLoadInfo(urls)}>
181
- <SearchIcon />
182
- </Button>
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
- <Button data-help="downloadFailed" disabled={loading || _isEmpty(urls)} variant="contained" disableElevation color="secondary" onClick={onDownloadFailed}>
187
- <ReplayIcon />
188
- </Button>
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
- <Button data-help="downloadAll" disabled={loading || _isEmpty(urls)} variant="contained" disableElevation color="secondary" onClick={() => onDownload(urls)}>
194
- <DownloadIcon />
195
- </Button>
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
- <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}>
150
- <DownloadIcon />
151
- </Button>
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
- <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}>
270
- <DownloadIcon />
271
- </Button>
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) &&
@@ -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
- const backdrop = document.createElement("div");
29
-
30
- backdrop.id = "help-backdrop";
31
- setBackdropEl(backdrop);
32
- setAnchorEl(elementHelp);
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
- if (!backdropEl) return;
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
- if (!anchorEl) return;
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 `global` ist, wird das ausgewählte Format auf alle heruntergeladenen Dateien angewendet, sobald der Download gestartet wird.",
69
- "Wenn der Bereich `pro Tab` ist, können Sie für jeden Tab ein anderes Format festlegen, sodass die zugehörigen Downloads unterschiedliche Ausgabeformate haben."
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": "Immer Überschreiben",
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-Download (experimentell)",
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": "Ausgewählter Formatumfang",
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 `global` then format you select will be applied to all downloaded files once download is started.",
69
- "If scope is `per tab` then you can specify different format for each tab so that related downloads will have different output formats."
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 existing files",
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 (experimental)",
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": "Selected format scope",
35
+ "formatScope": "Download parameters scope",
33
36
  "formatScopeGlobal": "Global",
34
- "formatScopeTab": "Per Tab",
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 zakres to `globalny`, wybrany format zostanie zastosowany do wszystkich pobieranych plików po rozpoczęciu pobierania.",
69
- "Jeśli zakres to `per karta`, możesz określić inny format dla każdej karty, tak aby powiązane pobrania miały różne formaty wyjściowe."
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 istniejące pliki",
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 (eksperymentalnie)",
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 wybranego formatu",
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 {operation, playlists, tracks, trackStatus, trackCuts, formats, autoDownload, queue, setOperation, setPlaylists, setTracks, setTrackStatus, setAutoDownload, setQueue, clear} = useDataState();
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.error) return;
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
- setTracks((prev) => [...prev, ...item.value]);
149
- setPlaylists((prev) => [...prev, {url: item.url, album: getAlbumInfo(item.value, item.url), tracks: item.value}]);
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.getVideoInfo(url)
229
+ ytDlpWrap.execPromise([url, "--dump-json", "--no-check-certificate", "--geo-bypass"])
209
230
  .then((result) => {
210
- resolve({url, value: _isArray(result) ? result : [result]});
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 authErrRegex = /ERROR: \[youtube\].*Sign in to confirm your age/gm;
214
-
215
- if (authErrRegex.test(e.message)) {
216
- return resolve({url, error: "Age restriction detected. Sign in required."});
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, error: e.message});
241
+ resolve({url, errors: _uniq(errorMatches), warnings: _uniq(warningMatches)});
220
242
  });
221
243
  }));
222
244
  }
@@ -1 +0,0 @@
1
- declare module "@pureit/busylight";