yt-grabber 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +29 -0
- package/.prettierrc +19 -0
- package/.vscode/extensions.json +7 -0
- package/.vscode/settings.json +23 -0
- package/.yarnrc.yml +13 -0
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/package.json +115 -0
- package/public/index.html +20 -0
- package/public/screenshots/cutting.png +0 -0
- package/public/screenshots/downloading.png +0 -0
- package/public/screenshots/editing.png +0 -0
- package/public/screenshots/errors.png +0 -0
- package/public/screenshots/settings.png +0 -0
- package/public/screenshots/tracklist.png +0 -0
- package/src/@types/global.d.ts +7 -0
- package/src/@types/i18next-scanner-webpack.d.ts +1 -0
- package/src/@types/stylus.d.ts +4 -0
- package/src/@types/svg.d.ts +1 -0
- package/src/App.styl +24 -0
- package/src/App.tsx +31 -0
- package/src/bootstrap.tsx +30 -0
- package/src/common/CancellablePromise.ts +22 -0
- package/src/common/ComponentDisplayMode.ts +8 -0
- package/src/common/Delay.ts +3 -0
- package/src/common/FileSystem.ts +171 -0
- package/src/common/Helpers.ts +270 -0
- package/src/common/Mappings.ts +14 -0
- package/src/common/PuppeteerOptions.ts +45 -0
- package/src/common/Selectors.ts +21 -0
- package/src/common/Store.ts +108 -0
- package/src/common/Theme.ts +4 -0
- package/src/common/Youtube.ts +80 -0
- package/src/components/appBar/AppBar.styl +22 -0
- package/src/components/appBar/AppBar.tsx +73 -0
- package/src/components/directoryPicker/DirectoryPicker.tsx +44 -0
- package/src/components/fileField/FileField.styl +3 -0
- package/src/components/fileField/FileField.tsx +152 -0
- package/src/components/languagePicker/LanguagePicker.styl +38 -0
- package/src/components/languagePicker/LanguagePicker.tsx +145 -0
- package/src/components/logo/Logo.tsx +15 -0
- package/src/components/modals/DetailsModal.styl +9 -0
- package/src/components/modals/DetailsModal.tsx +85 -0
- package/src/components/numberField/NumberField.styl +13 -0
- package/src/components/numberField/NumberField.tsx +154 -0
- package/src/components/progress/Progress.styl +15 -0
- package/src/components/progress/Progress.tsx +18 -0
- package/src/components/splitButton/SplitButton.styl +0 -0
- package/src/components/splitButton/SplitButton.tsx +125 -0
- package/src/components/themePicker/ThemePicker.styl +19 -0
- package/src/components/themePicker/ThemePicker.tsx +65 -0
- package/src/components/themeSwitcher/ThemeSwitcher.styl +10 -0
- package/src/components/themeSwitcher/ThemeSwitcher.tsx +43 -0
- package/src/components/youtube/formatSelector/FormatSelector.styl +3 -0
- package/src/components/youtube/formatSelector/FormatSelector.tsx +202 -0
- package/src/components/youtube/inputPanel/InputPanel.styl +7 -0
- package/src/components/youtube/inputPanel/InputPanel.tsx +189 -0
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.styl +80 -0
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.tsx +113 -0
- package/src/components/youtube/trackList/TrackList.styl +64 -0
- package/src/components/youtube/trackList/TrackList.tsx +258 -0
- package/src/enums/DataResponse.ts +5 -0
- package/src/enums/Media.ts +16 -0
- package/src/enums/MediaFormat.ts +10 -0
- package/src/enums/MimeTypes.ts +14 -0
- package/src/hooks/useCancellablePromises.ts +25 -0
- package/src/hooks/useClickCounter.ts +24 -0
- package/src/hooks/useData.ts +61 -0
- package/src/hooks/useMultiClickHandler.ts +41 -0
- package/src/i18next.ts +33 -0
- package/src/index.ts +65 -0
- package/src/react/actions/Action.ts +3 -0
- package/src/react/actions/AppActions.ts +41 -0
- package/src/react/contexts/AppContext.tsx +51 -0
- package/src/react/contexts/AppThemeContext.tsx +38 -0
- package/src/react/contexts/DataContext copy.tsx +76 -0
- package/src/react/contexts/DataContext.tsx +41 -0
- package/src/react/hooks/useAppTheme.ts +14 -0
- package/src/react/reducers/AppReducer.tsx +45 -0
- package/src/react/reducers/Reducer.ts +7 -0
- package/src/react/states/AppState.ts +29 -0
- package/src/react/states/State.ts +29 -0
- package/src/renderer.tsx +13 -0
- package/src/resources/bin/yt-dlp.exe +0 -0
- package/src/resources/fonts/Baloo-Regular.ttf +0 -0
- package/src/resources/fonts/Lato-Black.ttf +0 -0
- package/src/resources/fonts/Lato-BlackItalic.ttf +0 -0
- package/src/resources/fonts/Lato-Bold.ttf +0 -0
- package/src/resources/fonts/Lato-BoldItalic.ttf +0 -0
- package/src/resources/fonts/Lato-Italic.ttf +0 -0
- package/src/resources/fonts/Lato-Light.ttf +0 -0
- package/src/resources/fonts/Lato-LightItalic.ttf +0 -0
- package/src/resources/fonts/Lato-Regular.ttf +0 -0
- package/src/resources/fonts/Lato-Thin.ttf +0 -0
- package/src/resources/fonts/Lato-ThinItalic.ttf +0 -0
- package/src/resources/fonts/Material-Icons.woff2 +0 -0
- package/src/resources/icons/favicon-16x16.png +0 -0
- package/src/resources/icons/favicon-32x32.png +0 -0
- package/src/resources/icons/favicon.ico +0 -0
- package/src/resources/icons/logo-shape.png +0 -0
- package/src/resources/icons/logo-shape.svg +59 -0
- package/src/resources/images/loading.svg +28 -0
- package/src/resources/images/logo.png +0 -0
- package/src/resources/locales/de-DE/flag.svg +1 -0
- package/src/resources/locales/de-DE/translation.json +44 -0
- package/src/resources/locales/en-GB/flag.svg +43 -0
- package/src/resources/locales/en-GB/translation.json +44 -0
- package/src/resources/locales/pl-PL/flag.svg +36 -0
- package/src/resources/locales/pl-PL/translation.json +44 -0
- package/src/styles/MaterialThemes.ts +331 -0
- package/src/styles/fonts.styl +71 -0
- package/src/styles/mixins.styl +22 -0
- package/src/tests/CompleteTracksMock.ts +17384 -0
- package/src/tests/MissingDetailsTracksMock.ts +7737 -0
- package/src/theme/ColorThemes.ts +190 -0
- package/src/theme/Colors.ts +92 -0
- package/src/theme/Shadows.ts +9 -0
- package/src/theme/Shape.ts +7 -0
- package/src/theme/Theme.ts +24 -0
- package/src/theme/Typography.ts +56 -0
- package/src/views/development/DevelopmentView.styl +22 -0
- package/src/views/development/DevelopmentView.tsx +57 -0
- package/src/views/home/HomeView.styl +60 -0
- package/src/views/home/HomeView.tsx +505 -0
- package/src/views/settings/SettingsView.styl +27 -0
- package/src/views/settings/SettingsView.tsx +255 -0
- package/tsconfig.json +20 -0
- package/webpack.config.ts +226 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
@import "../../../styles/mixins.styl"
|
|
2
|
+
|
|
3
|
+
.media-info-panel {
|
|
4
|
+
.header {
|
|
5
|
+
display: flex;
|
|
6
|
+
|
|
7
|
+
.content {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: row;
|
|
10
|
+
flex-grow: 1;
|
|
11
|
+
position: relative;
|
|
12
|
+
z-index: 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.info {
|
|
16
|
+
flex: 1 0 auto;
|
|
17
|
+
padding: .2rem .5rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.actions {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
flex-direction: row;
|
|
24
|
+
opacity: 0;
|
|
25
|
+
vendor("transition", all .25s ease-in-out)
|
|
26
|
+
|
|
27
|
+
.edit {
|
|
28
|
+
min-width: 12px;
|
|
29
|
+
padding: 12px;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.loading {
|
|
34
|
+
z-index: 1;
|
|
35
|
+
align-items: center;
|
|
36
|
+
column-gap: 2rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.progress-indicator {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
flex-direction: row;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.progress {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
flex-direction: row;
|
|
49
|
+
position: absolute;
|
|
50
|
+
width: 100%;
|
|
51
|
+
height: 100%;
|
|
52
|
+
z-index: 0;
|
|
53
|
+
opacity: 0.25;
|
|
54
|
+
|
|
55
|
+
.progress-bar {
|
|
56
|
+
width: 100%;
|
|
57
|
+
height: 100%;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.row {
|
|
62
|
+
display: flex;
|
|
63
|
+
|
|
64
|
+
.label {
|
|
65
|
+
padding-right: .3rem;
|
|
66
|
+
capitalize();
|
|
67
|
+
|
|
68
|
+
& .bold {
|
|
69
|
+
font-weight: bold;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
&:hover {
|
|
76
|
+
.actions {
|
|
77
|
+
opacity: 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import classnames from "classnames";
|
|
2
|
+
import _assign from "lodash/assign";
|
|
3
|
+
import _isFunction from "lodash/isFunction";
|
|
4
|
+
import moment from "moment";
|
|
5
|
+
import React, {useCallback, useState} from "react";
|
|
6
|
+
import {useTranslation} from "react-i18next";
|
|
7
|
+
|
|
8
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
9
|
+
import EditIcon from "@mui/icons-material/Edit";
|
|
10
|
+
import {Box, Button, Card, CardContent, CardMedia, LinearProgress, Typography} from "@mui/material";
|
|
11
|
+
import Grid from "@mui/material/Grid2";
|
|
12
|
+
|
|
13
|
+
import {AlbumInfo} from "../../../common/Youtube";
|
|
14
|
+
import {useDataState} from "../../../react/contexts/DataContext";
|
|
15
|
+
import DetailsModal from "../../modals/DetailsModal";
|
|
16
|
+
import Progress from "../../progress/Progress";
|
|
17
|
+
import Styles from "./MediaInfoPanel.styl";
|
|
18
|
+
|
|
19
|
+
export type MediaInfoPanelProps = {
|
|
20
|
+
className?: string;
|
|
21
|
+
loading?: boolean;
|
|
22
|
+
progress?: number;
|
|
23
|
+
onCancel?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const MediaInfoPanel: React.FC<MediaInfoPanelProps> = (props: MediaInfoPanelProps) => {
|
|
27
|
+
const {className, onCancel, loading, progress = 0} = props;
|
|
28
|
+
const {album, setAlbum} = useDataState();
|
|
29
|
+
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
|
|
30
|
+
const {t} = useTranslation();
|
|
31
|
+
|
|
32
|
+
const onDetailsModalClose = (data: AlbumInfo) => {
|
|
33
|
+
setAlbum((prev) => _assign(prev, data));
|
|
34
|
+
setDetailsModalOpen(false);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const cancel = () => {
|
|
38
|
+
if (_isFunction(onCancel)) {
|
|
39
|
+
onCancel();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const editInfo = useCallback(() => {
|
|
44
|
+
setDetailsModalOpen(true);
|
|
45
|
+
}, [detailsModalOpen, setDetailsModalOpen]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<Grid className={classnames(className, Styles.mediaInfoPanel)} size={12}>
|
|
50
|
+
<Card variant="outlined" className={Styles.header}>
|
|
51
|
+
<CardMedia
|
|
52
|
+
component="img"
|
|
53
|
+
sx={{width: 100, height: "auto", marginRight: 1}}
|
|
54
|
+
image={album.thumbnail}
|
|
55
|
+
alt={album.title}
|
|
56
|
+
/>
|
|
57
|
+
<Box className={Styles.content}>
|
|
58
|
+
<CardContent className={Styles.info}>
|
|
59
|
+
<div className={classnames(Styles.row, Styles, Styles.title)}>
|
|
60
|
+
<Typography variant="subtitle2" className={Styles.label}>{t("title")}:</Typography>
|
|
61
|
+
<Typography variant="subtitle2" sx={{color: "text.secondary"}} className={classnames(Styles.label, Styles.bold)}>{album.title}</Typography>
|
|
62
|
+
</div>
|
|
63
|
+
<div className={classnames(Styles.row, Styles, Styles.artist)}>
|
|
64
|
+
<Typography variant="body1" className={Styles.label}>{t("artist")}:</Typography>
|
|
65
|
+
<Typography variant="body1" sx={{color: "text.secondary"}}>{album.artist}</Typography>
|
|
66
|
+
</div>
|
|
67
|
+
<div className={Styles.row}>
|
|
68
|
+
<Typography variant="subtitle2" className={Styles.label}>{t("releaseYear")}:</Typography>
|
|
69
|
+
<Typography variant="subtitle2" sx={{color: "text.secondary"}}>{album.releaseYear}</Typography>
|
|
70
|
+
</div>
|
|
71
|
+
<div className={Styles.row}>
|
|
72
|
+
<Typography variant="subtitle2" className={Styles.label}>{t("duration")}:</Typography>
|
|
73
|
+
<Typography variant="subtitle2" sx={{color: "text.secondary"}}>{moment.duration(album.duration, "seconds").format("m:ss")}</Typography>
|
|
74
|
+
</div>
|
|
75
|
+
</CardContent>
|
|
76
|
+
{!loading &&
|
|
77
|
+
<Box className={Styles.actions} padding={2} gap={1}>
|
|
78
|
+
<Button className={Styles.edit} title={t("edit")} size="large" fullWidth variant="contained" color="secondary" disableElevation onClick={editInfo}>
|
|
79
|
+
<EditIcon />
|
|
80
|
+
</Button>
|
|
81
|
+
</Box>
|
|
82
|
+
}
|
|
83
|
+
{loading &&
|
|
84
|
+
<Grid container className={Styles.loading}>
|
|
85
|
+
<Grid>
|
|
86
|
+
<Button variant="contained" size="large" color="secondary" disableElevation startIcon={<CloseIcon />} onClick={cancel}>{t("cancel")}</Button>
|
|
87
|
+
</Grid>
|
|
88
|
+
<Grid>
|
|
89
|
+
<Box className={Styles.progressIndicator} padding={2} gap={1}>
|
|
90
|
+
<Progress size={60} thickness={6.5} color="primary" value={progress} />
|
|
91
|
+
</Box>
|
|
92
|
+
</Grid>
|
|
93
|
+
</Grid>
|
|
94
|
+
}
|
|
95
|
+
{loading &&
|
|
96
|
+
<Box className={Styles.progress}>
|
|
97
|
+
<LinearProgress className={Styles.progressBar} variant="determinate" color="primary" value={progress} />
|
|
98
|
+
</Box>
|
|
99
|
+
}
|
|
100
|
+
</Box>
|
|
101
|
+
</Card>
|
|
102
|
+
</Grid>
|
|
103
|
+
<DetailsModal
|
|
104
|
+
id="details-modal"
|
|
105
|
+
details={album}
|
|
106
|
+
open={detailsModalOpen}
|
|
107
|
+
onClose={onDetailsModalClose}
|
|
108
|
+
/>
|
|
109
|
+
</>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export default MediaInfoPanel;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
.track-list {
|
|
2
|
+
flex-grow: 1;
|
|
3
|
+
|
|
4
|
+
.track {
|
|
5
|
+
padding-top: 0;
|
|
6
|
+
padding-bottom: 0;
|
|
7
|
+
padding-left: 5px;
|
|
8
|
+
padding-right: 5px;
|
|
9
|
+
|
|
10
|
+
&:hover {
|
|
11
|
+
background-color: var(--theme-palette-divider);
|
|
12
|
+
|
|
13
|
+
.track-action {
|
|
14
|
+
visibility: visible;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// :global(.MuiListItemSecondaryAction-root) {
|
|
19
|
+
// padding-right: 8px;
|
|
20
|
+
// }
|
|
21
|
+
|
|
22
|
+
.track-action {
|
|
23
|
+
visibility: hidden;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.number-column {
|
|
28
|
+
width: 1rem;
|
|
29
|
+
align-content: center;
|
|
30
|
+
justify-items: center;
|
|
31
|
+
|
|
32
|
+
.number {
|
|
33
|
+
font-size: 1rem;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.image-column {
|
|
38
|
+
width: 4rem;
|
|
39
|
+
align-content: center;
|
|
40
|
+
justify-items: center;
|
|
41
|
+
|
|
42
|
+
.image > img {
|
|
43
|
+
width: 110%;
|
|
44
|
+
height: 110%;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.column {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-self: center;
|
|
52
|
+
align-items: center;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.completed-icon {
|
|
56
|
+
width: 2.3rem;
|
|
57
|
+
height: 2.3rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.cut-icon {
|
|
61
|
+
font-size: 18px;
|
|
62
|
+
margin-right: 0.25rem;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import _filter from "lodash/filter";
|
|
2
|
+
import _find from "lodash/find";
|
|
3
|
+
import _get from "lodash/get";
|
|
4
|
+
import _includes from "lodash/includes";
|
|
5
|
+
import _isEmpty from "lodash/isEmpty";
|
|
6
|
+
import _isFunction from "lodash/isFunction";
|
|
7
|
+
import _map from "lodash/map";
|
|
8
|
+
import _omit from "lodash/omit";
|
|
9
|
+
import _toInteger from "lodash/toInteger";
|
|
10
|
+
import _toString from "lodash/toString";
|
|
11
|
+
import moment from "moment";
|
|
12
|
+
import React, {useCallback, useState} from "react";
|
|
13
|
+
import {useTranslation} from "react-i18next";
|
|
14
|
+
import {NumberFormatBase} from "react-number-format";
|
|
15
|
+
|
|
16
|
+
import CheckIcon from "@mui/icons-material/Check";
|
|
17
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
18
|
+
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
|
19
|
+
import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
|
|
20
|
+
import DownloadIcon from "@mui/icons-material/Download";
|
|
21
|
+
import {
|
|
22
|
+
Avatar, Button, List, ListItem, ListItemText, Popover, Slider, Stack, TextField, Typography
|
|
23
|
+
} from "@mui/material";
|
|
24
|
+
import Grid from "@mui/material/Grid2";
|
|
25
|
+
|
|
26
|
+
import {formatFileSize} from "../../../common/Helpers";
|
|
27
|
+
import {TrackInfo, TrackStatusInfo} from "../../../common/Youtube";
|
|
28
|
+
import {useDataState} from "../../../react/contexts/DataContext";
|
|
29
|
+
import Progress from "../../progress/Progress";
|
|
30
|
+
import Styles from "./TrackList.styl";
|
|
31
|
+
|
|
32
|
+
export type TrackListProps = {
|
|
33
|
+
onDownloadTrack?: (id: string) => void;
|
|
34
|
+
onCancelTrack?: (id: string) => void;
|
|
35
|
+
queue: string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const TrackList: React.FC<TrackListProps> = (props: TrackListProps) => {
|
|
39
|
+
const {onDownloadTrack, onCancelTrack, queue} = props;
|
|
40
|
+
const {tracks, trackStatus, trackCuts, setTrackStatus, setTrackCuts} = useDataState();
|
|
41
|
+
const [cutAnchorEl, setCutAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
|
42
|
+
const [cutOpen, setCutOpen] = useState<string>();
|
|
43
|
+
const {t} = useTranslation();
|
|
44
|
+
|
|
45
|
+
const formatTime = (value: string) => {
|
|
46
|
+
const formatted = moment.duration(value, "seconds").format("HH:mm:ss", {trim: "left"});
|
|
47
|
+
|
|
48
|
+
if (/^\d{2}$/.test(formatted)) {
|
|
49
|
+
return `00:${formatted}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return formatted;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onDownloadTrackClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
56
|
+
const trackId = event.currentTarget.getAttribute("data-id");
|
|
57
|
+
setTrackStatus((prev) => _filter(prev, (item) => item.trackId !== trackId));
|
|
58
|
+
|
|
59
|
+
if (_isFunction(onDownloadTrack)) {
|
|
60
|
+
onDownloadTrack(trackId);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onOpenTrackCut = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
65
|
+
const trackId = event.currentTarget.getAttribute("data-id");
|
|
66
|
+
const track = _find(tracks, ["id", trackId]);
|
|
67
|
+
|
|
68
|
+
setCutAnchorEl(event.currentTarget);
|
|
69
|
+
setCutOpen(trackId);
|
|
70
|
+
setTrackCuts((prev) => ({...prev, [trackId]: prev[trackId] ?? [0, track.duration]}));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const onDeleteTrackCut = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
74
|
+
const trackId = event.currentTarget.getAttribute("data-id");
|
|
75
|
+
|
|
76
|
+
setTrackCuts((prev) => _omit(prev, [trackId]));
|
|
77
|
+
onCloseTrackCut();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const onCloseTrackCut = () => {
|
|
81
|
+
setCutAnchorEl(null);
|
|
82
|
+
setCutOpen(null);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const onCancelTrackClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
86
|
+
const trackId = event.currentTarget.getAttribute("data-id");
|
|
87
|
+
|
|
88
|
+
if (_isFunction(onCancelTrack)) {
|
|
89
|
+
onCancelTrack(trackId);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const onCutStartTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
94
|
+
setTrackCuts((prev) => ({...prev, [cutOpen]: [_toInteger(e.target.value), prev[cutOpen][1]]}));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const onCutEndTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
98
|
+
setTrackCuts((prev) => ({...prev, [cutOpen]: [prev[cutOpen][0],_toInteger(e.target.value)]}));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const onCutTimeChange = (event: Event, value: number | number[], activeThumb: number) => {
|
|
102
|
+
if (!Array.isArray(value)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (activeThumb === 0) {
|
|
107
|
+
setTrackCuts((prev) => ({...prev, [cutOpen]: [value[0], prev[cutOpen][1]]}));
|
|
108
|
+
} else {
|
|
109
|
+
setTrackCuts((prev) => ({...prev, [cutOpen]: [prev[cutOpen][0], value[1]]}));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const getTrackStatusInfo = useCallback((track: TrackInfo): TrackStatusInfo => {
|
|
114
|
+
const trackStatusInfo = _find(trackStatus, ["trackId", track.id]);
|
|
115
|
+
|
|
116
|
+
if (!trackStatusInfo) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return trackStatusInfo;
|
|
121
|
+
}, [trackStatus]);
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Grid size={12} className={Styles.trackList}>
|
|
126
|
+
<List className={Styles.trackList} dense>
|
|
127
|
+
{_map(tracks, (item) => {
|
|
128
|
+
const info = getTrackStatusInfo(item);
|
|
129
|
+
|
|
130
|
+
return (<ListItem
|
|
131
|
+
divider
|
|
132
|
+
dense
|
|
133
|
+
key={item.id}
|
|
134
|
+
className={Styles.track}
|
|
135
|
+
// disableGutters
|
|
136
|
+
// sx={{backgroundColor: info?.completed ? "success.main" : info?.error ? "error.main" : "inherit"}}
|
|
137
|
+
secondaryAction={
|
|
138
|
+
<Stack direction="row" spacing={2}>
|
|
139
|
+
{_includes(queue, item.id) &&
|
|
140
|
+
<Button size="small" className={Styles.trackAction} color="secondary" disableElevation variant="contained" data-id={item.id} onClick={onCancelTrackClick}>
|
|
141
|
+
<CloseIcon />
|
|
142
|
+
</Button>
|
|
143
|
+
}
|
|
144
|
+
{!_includes(queue, item.id) &&
|
|
145
|
+
<div>
|
|
146
|
+
<Button size="small" className={Styles.trackAction} color="secondary" disableElevation variant="contained" data-id={item.id} onClick={onOpenTrackCut}>
|
|
147
|
+
<ContentCutIcon />
|
|
148
|
+
</Button>
|
|
149
|
+
<Popover
|
|
150
|
+
id={item.id}
|
|
151
|
+
open={Boolean(cutAnchorEl) && cutOpen === item.id}
|
|
152
|
+
anchorEl={cutAnchorEl}
|
|
153
|
+
onClose={onCloseTrackCut}
|
|
154
|
+
anchorOrigin={{
|
|
155
|
+
vertical: "center",
|
|
156
|
+
horizontal: "left",
|
|
157
|
+
}}
|
|
158
|
+
transformOrigin={{
|
|
159
|
+
vertical: "center",
|
|
160
|
+
horizontal: "right",
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<Grid container padding={2} spacing={1} className={Styles.trackCutPopup}>
|
|
164
|
+
<Grid size={5}>
|
|
165
|
+
<NumberFormatBase
|
|
166
|
+
value={_get(trackCuts, `${item.id}.0`, 0) + ""}
|
|
167
|
+
onChange={onCutStartTimeChange}
|
|
168
|
+
format={formatTime}
|
|
169
|
+
customInput={TextField}
|
|
170
|
+
variant="outlined"
|
|
171
|
+
label={t("from")}
|
|
172
|
+
/>
|
|
173
|
+
</Grid>
|
|
174
|
+
<Grid size={5}>
|
|
175
|
+
<NumberFormatBase
|
|
176
|
+
value={_get(trackCuts, `${item.id}.1`, item.duration) + ""}
|
|
177
|
+
onChange={onCutEndTimeChange}
|
|
178
|
+
format={formatTime}
|
|
179
|
+
customInput={TextField}
|
|
180
|
+
variant="outlined"
|
|
181
|
+
label={t("to")}
|
|
182
|
+
/>
|
|
183
|
+
</Grid>
|
|
184
|
+
<Grid size={2} display="flex">
|
|
185
|
+
<Button data-id={item.id} disableElevation variant="contained" fullWidth color="secondary" onClick={onDeleteTrackCut}>
|
|
186
|
+
<DeleteForeverIcon />
|
|
187
|
+
</Button>
|
|
188
|
+
</Grid>
|
|
189
|
+
<Grid size={12} display="flex" paddingX={2}>
|
|
190
|
+
<Slider
|
|
191
|
+
value={trackCuts[item.id] ?? [0, item.duration]}
|
|
192
|
+
onChange={onCutTimeChange}
|
|
193
|
+
valueLabelDisplay="off"
|
|
194
|
+
min={0}
|
|
195
|
+
max={item.duration}
|
|
196
|
+
step={1}
|
|
197
|
+
/>
|
|
198
|
+
</Grid>
|
|
199
|
+
</Grid>
|
|
200
|
+
</Popover>
|
|
201
|
+
</div>
|
|
202
|
+
}
|
|
203
|
+
{!_includes(queue, item.id) &&
|
|
204
|
+
<Button className={Styles.trackAction} size="small" color="secondary" disableElevation variant="contained" data-id={item.id} onClick={onDownloadTrackClick}>
|
|
205
|
+
<DownloadIcon />
|
|
206
|
+
</Button>
|
|
207
|
+
}
|
|
208
|
+
</Stack>
|
|
209
|
+
}
|
|
210
|
+
>
|
|
211
|
+
<Grid container direction="row" flexGrow={1}>
|
|
212
|
+
{item.playlist_autonumber &&
|
|
213
|
+
<Grid size={1} className={Styles.numberColumn}>
|
|
214
|
+
<Typography className={Styles.number} variant="body1" color="primary.main">{item.playlist_autonumber}</Typography>
|
|
215
|
+
</Grid>
|
|
216
|
+
}
|
|
217
|
+
<Grid size={1} className={Styles.imageColumn}>
|
|
218
|
+
<Avatar className={Styles.image} src={item.thumbnail}>{item.playlist_autonumber}</Avatar>
|
|
219
|
+
</Grid>
|
|
220
|
+
<Grid size={4}>
|
|
221
|
+
<ListItemText primary={item.title} secondary={moment.duration(item.duration, "seconds").format("m:ss")} />
|
|
222
|
+
</Grid>
|
|
223
|
+
<Grid size={2} className={Styles.column}>
|
|
224
|
+
{!_isEmpty(trackCuts[item.id]) &&
|
|
225
|
+
<>
|
|
226
|
+
<ContentCutIcon className={Styles.cutIcon} color="action"/>
|
|
227
|
+
<Typography variant="caption">
|
|
228
|
+
{formatTime(_toString(trackCuts[item.id][0]))} - {formatTime(_toString(trackCuts[item.id][1]))}
|
|
229
|
+
</Typography>
|
|
230
|
+
</>
|
|
231
|
+
}
|
|
232
|
+
</Grid>
|
|
233
|
+
{info && <>
|
|
234
|
+
<Grid size={1} className={Styles.column}>
|
|
235
|
+
<Typography variant="body1">{formatFileSize(info.totalSize ?? item.filesize_approx)}</Typography>
|
|
236
|
+
</Grid>
|
|
237
|
+
<Grid size={1} className={Styles.column}>
|
|
238
|
+
{info.completed ?
|
|
239
|
+
<CheckIcon className={Styles.completedIcon} color="success" />
|
|
240
|
+
: info.error ?
|
|
241
|
+
<CloseIcon className={Styles.completedIcon} color="error" />
|
|
242
|
+
: <Progress color="primary" value={info.percent} />
|
|
243
|
+
}
|
|
244
|
+
</Grid>
|
|
245
|
+
<Grid size={2} className={Styles.column}>
|
|
246
|
+
<Typography variant="body1">{info.status}</Typography>
|
|
247
|
+
</Grid>
|
|
248
|
+
</>
|
|
249
|
+
}
|
|
250
|
+
</Grid>
|
|
251
|
+
</ListItem>);
|
|
252
|
+
})}
|
|
253
|
+
</List>
|
|
254
|
+
</Grid>
|
|
255
|
+
);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export default TrackList;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export enum MimeTypes {
|
|
2
|
+
Jpg = ".jpg,image/jpg",
|
|
3
|
+
Bin = ".bin,application/octet-stream",
|
|
4
|
+
Jpeg = ".jpeg,image/jpeg",
|
|
5
|
+
Json = ".json,application/json",
|
|
6
|
+
Png = ".png,image/png",
|
|
7
|
+
Svg = ".svg,image/svg+xml",
|
|
8
|
+
Pdf = ".pdf,application/pdf",
|
|
9
|
+
Word = ".doc,.docx,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
10
|
+
Excel = ".xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
11
|
+
Zip = ".zip",
|
|
12
|
+
Midi = ".mid",
|
|
13
|
+
Text = ".txt,text/plain",
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, {useRef} from "react";
|
|
2
|
+
|
|
3
|
+
import {ICancelablePromise} from "../common/CancellablePromise";
|
|
4
|
+
|
|
5
|
+
const useCancellablePromises = () => {
|
|
6
|
+
const pendingPromises = useRef([]);
|
|
7
|
+
|
|
8
|
+
const appendPendingPromise = (promise: ICancelablePromise) =>
|
|
9
|
+
(pendingPromises.current = [...pendingPromises.current, promise]);
|
|
10
|
+
|
|
11
|
+
const removePendingPromise = (promise: ICancelablePromise) =>
|
|
12
|
+
(pendingPromises.current = pendingPromises.current.filter((p) => p !== promise));
|
|
13
|
+
|
|
14
|
+
const clearPendingPromises = () => pendingPromises.current.map((p) => p.cancel());
|
|
15
|
+
|
|
16
|
+
const api = {
|
|
17
|
+
appendPendingPromise,
|
|
18
|
+
removePendingPromise,
|
|
19
|
+
clearPendingPromises,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return api;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default useCancellablePromises;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import $_ from "lodash";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export const useClickCounter = (callback: () => void, clicks = 3, timeout = 500) => {
|
|
5
|
+
const [clickCounter, setClickCount] = useState(0);
|
|
6
|
+
|
|
7
|
+
const onReset = useRef($_.debounce(() => setClickCount(0), timeout, { leading: false, trailing: true }));
|
|
8
|
+
|
|
9
|
+
const onClick = useCallback(() => {
|
|
10
|
+
setClickCount(clickCounter + 1);
|
|
11
|
+
onReset.current();
|
|
12
|
+
}, [callback]);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (clickCounter >= clicks) {
|
|
16
|
+
onReset.current.flush();
|
|
17
|
+
callback();
|
|
18
|
+
}
|
|
19
|
+
}, [clickCounter]);
|
|
20
|
+
|
|
21
|
+
return { clickCounter, onClick };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default useClickCounter;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import _assign from "lodash/assign";
|
|
4
|
+
import _merge from "lodash/merge";
|
|
5
|
+
|
|
6
|
+
import {AlbumInfo, TrackInfo, TrackStatusInfo} from "../common/Youtube";
|
|
7
|
+
|
|
8
|
+
export type useDataType = {
|
|
9
|
+
album?: AlbumInfo;
|
|
10
|
+
tracks: TrackInfo[];
|
|
11
|
+
trackStatus: TrackStatusInfo[];
|
|
12
|
+
setAlbum: React.Dispatch<React.SetStateAction<AlbumInfo>>;
|
|
13
|
+
setTracks: React.Dispatch<React.SetStateAction<TrackInfo[]>>;
|
|
14
|
+
setTrackStatus: React.Dispatch<React.SetStateAction<TrackStatusInfo[]>>;
|
|
15
|
+
clear: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const useData = (id?: string): any => {
|
|
19
|
+
// const {data, getData, setData} = useDataState();
|
|
20
|
+
// const state = getData(id);
|
|
21
|
+
|
|
22
|
+
// const [album, setAlbum] = useState<AlbumInfo>();
|
|
23
|
+
// const [tracks, setTracks] = useState<TrackInfo[]>();
|
|
24
|
+
// const [trackStatus, setTrackStatus] = useState<TrackStatusInfo[]>();
|
|
25
|
+
|
|
26
|
+
// useEffect(() => {
|
|
27
|
+
// setData((prev) => {
|
|
28
|
+
// return {...prev, [id]: _merge({}, prev[id], {album})};
|
|
29
|
+
// });
|
|
30
|
+
// }, [album]);
|
|
31
|
+
|
|
32
|
+
// useEffect(() => {
|
|
33
|
+
// setData((prev) => {
|
|
34
|
+
// return {...prev, [id]: _assign({}, prev[id], {tracks})};
|
|
35
|
+
// });
|
|
36
|
+
// }, [tracks]);
|
|
37
|
+
|
|
38
|
+
// useEffect(() => {
|
|
39
|
+
// setData((prev) => {
|
|
40
|
+
// return {...prev, [id]: _assign({}, prev[id], {trackStatus})};
|
|
41
|
+
// });
|
|
42
|
+
// }, [trackStatus]);
|
|
43
|
+
|
|
44
|
+
// const clear = () => {
|
|
45
|
+
// setAlbum(null);
|
|
46
|
+
// setTracks(null);
|
|
47
|
+
// setTrackStatus(null);
|
|
48
|
+
// };
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
// album: state.album,
|
|
52
|
+
// tracks: state.tracks,
|
|
53
|
+
// trackStatus: state.trackStatus,
|
|
54
|
+
// setAlbum,
|
|
55
|
+
// setTracks,
|
|
56
|
+
// setTrackStatus,
|
|
57
|
+
// clear
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default useData;
|