yt-grabber 1.8.6 → 1.8.8
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/README.md +14 -0
- package/package.json +2 -1
- package/src/@types/global.d.ts +2 -1
- package/src/common/Helpers.ts +62 -1
- package/src/common/Logger.ts +73 -0
- package/src/common/YtdplUtils.ts +1 -1
- package/src/components/languagePicker/LanguagePicker.tsx +6 -6
- package/src/components/modals/cutModal/CutModal.styl +21 -0
- package/src/components/modals/cutModal/CutModal.tsx +200 -0
- package/src/components/numberField/NumberField.tsx +10 -9
- package/src/components/youtube/inputPanel/InputPanel.tsx +10 -1
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.styl +5 -0
- package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.tsx +72 -5
- package/src/components/youtube/playlistTabs/PlaylistTabs.tsx +1 -0
- package/src/components/youtube/trackList/TrackList.tsx +3 -23
- package/src/hooks/useHelp.ts +1 -1
- package/src/index.ts +27 -14
- package/src/renderer.tsx +11 -0
- package/src/resources/bin/yt-dlp.exe +0 -0
- package/src/resources/locales/de-DE/translation.json +2 -0
- package/src/resources/locales/en-GB/translation.json +2 -0
- package/src/resources/locales/pl-PL/translation.json +2 -0
- package/src/styles/MaterialThemes.ts +2 -2
package/README.md
CHANGED
|
@@ -28,8 +28,10 @@ Each download can be customized to your needs for easy workflow automation.
|
|
|
28
28
|
- [Running](#running)
|
|
29
29
|
- [Packaging](#packaging)
|
|
30
30
|
- [Testing](#testing)
|
|
31
|
+
- [Debugging](#debugging)
|
|
31
32
|
- [License](#license)
|
|
32
33
|
- [Legal Disclaimer](#legal-disclaimer)
|
|
34
|
+
- [Support Disclaimer](#support-disclaimer)
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
## Features
|
|
@@ -111,6 +113,12 @@ To execute unit tests run `npm test` or `yarn test` command (unit tests are also
|
|
|
111
113
|
|
|
112
114
|
To execute end2end tests run `npm playwright test` or `yarn playwright test` command.
|
|
113
115
|
|
|
116
|
+
## Debugging
|
|
117
|
+
|
|
118
|
+
Logs are written to `init.log` and `application.log` files.
|
|
119
|
+
By default only errors are logged.
|
|
120
|
+
To get more detailed logs run the application with `--debug-mode` arguments.
|
|
121
|
+
|
|
114
122
|
## License
|
|
115
123
|
|
|
116
124
|
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -118,3 +126,9 @@ This project is licensed under the [MIT License](LICENSE).
|
|
|
118
126
|
## Legal Disclaimer
|
|
119
127
|
|
|
120
128
|
All music files downloaded through this software must be legally owned and purchased by the user. By downloading music via this software, you represent that you have purchased and fully own the rights to any downloaded content or an active subscription to YouTube Music. Downloading or distributing pirated or illegal music copies is strictly prohibited. I claim no ownership rights to any downloaded music files - all such rights remain with the content owner. I accept no liability for the illegal use of any files downloaded through this software.
|
|
129
|
+
|
|
130
|
+
## Support Disclaimer
|
|
131
|
+
|
|
132
|
+
In the era of greedy services like Spotify, omnipresent AI slop and other bs it is very difficult, especially for younger, less recognizable creators to make money off of their creations. There are a lot of talented people who struggle with that (and their number is growing).
|
|
133
|
+
If you like certain artists and value their content please show them your support.
|
|
134
|
+
Thank You!
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-grabber",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.8",
|
|
4
4
|
"description": "Youtube Grabber - robust desktop application designed to retrieve multimedia from YouTube and YouTube Music services",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"youtube",
|
|
@@ -121,6 +121,7 @@
|
|
|
121
121
|
"react-router-dom": "^7.9.4",
|
|
122
122
|
"usehooks-ts": "^3.1.1",
|
|
123
123
|
"win-version-info": "^6.0.1",
|
|
124
|
+
"winston": "^3.18.3",
|
|
124
125
|
"yt-dlp-wrap": "^2.3.12"
|
|
125
126
|
},
|
|
126
127
|
"devDependencies": {
|
package/src/@types/global.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import ElectronStore from "electron-store";
|
|
2
2
|
|
|
3
|
+
import {ILogger} from "../common/Logger";
|
|
3
4
|
import {IStore} from "../common/Store";
|
|
4
5
|
|
|
5
6
|
declare global {
|
|
6
7
|
var store: ElectronStore<IStore>;
|
|
7
|
-
|
|
8
|
+
var logger: ILogger;
|
|
8
9
|
interface Global {
|
|
9
10
|
[key: string]: any;
|
|
10
11
|
}
|
package/src/common/Helpers.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {App} from "electron";
|
|
2
|
+
import {forEach, groupBy, includes, indexOf, isNumber, keys, map, replace} from "lodash-es";
|
|
3
|
+
import moment from "moment";
|
|
2
4
|
|
|
3
5
|
import {VideoType} from "./Media";
|
|
4
6
|
import {TrackInfo, UrlType, YoutubeInfoResult} from "./Youtube";
|
|
@@ -11,6 +13,65 @@ type NonDataAttributes<T> = Omit<T, keyof DataAttributes<T>>;
|
|
|
11
13
|
|
|
12
14
|
export const isDev = () => process.env.NODE_ENV === "development";
|
|
13
15
|
|
|
16
|
+
export const isDevApplication = (app: App) => !app.isPackaged;
|
|
17
|
+
|
|
18
|
+
export const isDebugMode = () => {
|
|
19
|
+
const args = getProcessArgs();
|
|
20
|
+
|
|
21
|
+
return args["debug-mode"] === true;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const getProcessArgs = () => {
|
|
25
|
+
const args = process.argv.slice(1);
|
|
26
|
+
const filteredArgs: Record<string, string | boolean> = {};
|
|
27
|
+
|
|
28
|
+
forEach(args, (arg) => {
|
|
29
|
+
if (arg.startsWith("--")) {
|
|
30
|
+
const argWithoutPrefix = arg.slice(2);
|
|
31
|
+
const [key, value] = argWithoutPrefix.split("=");
|
|
32
|
+
|
|
33
|
+
filteredArgs[key] = value !== undefined ? value : true;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return filteredArgs;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const formatTime = (value: string) => {
|
|
41
|
+
const formatted = moment.duration(value, "seconds").format("HH:mm:ss", {trim: "left"});
|
|
42
|
+
const onlyTwoDigitsRegex = /^\d{2}$/;
|
|
43
|
+
|
|
44
|
+
if (onlyTwoDigitsRegex.test(formatted)) {
|
|
45
|
+
return `00:${formatted}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return formatted;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const unformatTime = (value: string) => {
|
|
52
|
+
const onlyTwoDigitsRegex = /^\d{2}$/;
|
|
53
|
+
const atLeastTwoColonsRegex = /^([^:]*:){2}/;
|
|
54
|
+
|
|
55
|
+
if (onlyTwoDigitsRegex.test(value)) {
|
|
56
|
+
return moment.duration(`00:00:${value}`).asSeconds() + "";
|
|
57
|
+
}
|
|
58
|
+
if (atLeastTwoColonsRegex.test(value)) {
|
|
59
|
+
return moment.duration(`${value}`).asSeconds() + "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return moment.duration(`00:${value}`).asSeconds() + "";
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const timeStringToNumber = (value: string) => {
|
|
66
|
+
const atLeastTwoColonsRegex = /^([^:]*:){2}/;
|
|
67
|
+
|
|
68
|
+
if (atLeastTwoColonsRegex.test(value)) {
|
|
69
|
+
return moment.duration(`${value}`).asSeconds();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return moment.duration(`00:${value}`).asSeconds();
|
|
73
|
+
};
|
|
74
|
+
|
|
14
75
|
export const formatFileSize = (sizeInBytes: number, decimals = 2) => {
|
|
15
76
|
if (!isNumber(sizeInBytes)) return "";
|
|
16
77
|
if (sizeInBytes === 0) return "0 Bytes";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLogger as createWinston, format, LeveledLogMethod, Logger, LoggerOptions, transport,
|
|
3
|
+
transports
|
|
4
|
+
} from "winston";
|
|
5
|
+
|
|
6
|
+
export interface ILoggerOptions extends LoggerOptions {
|
|
7
|
+
logFile?: boolean;
|
|
8
|
+
logFilePath?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface ILogger extends Logger {
|
|
12
|
+
success: LeveledLogMethod;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const levels = {
|
|
16
|
+
error: 0,
|
|
17
|
+
warn: 1,
|
|
18
|
+
success: 1,
|
|
19
|
+
info: 3,
|
|
20
|
+
verbose: 4,
|
|
21
|
+
debug: 5,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const colorizer = format.colorize({
|
|
25
|
+
colors: {
|
|
26
|
+
error: "red",
|
|
27
|
+
warn: "yellow",
|
|
28
|
+
success: "green",
|
|
29
|
+
info: "blue",
|
|
30
|
+
verbose: "grey",
|
|
31
|
+
debug: "magenta",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const timeFormat = format.timestamp({format: "HH:mm:ss:SSS"});
|
|
36
|
+
const fileFormat = format.printf(({level, message, timestamp, ...meta}) => {
|
|
37
|
+
const metaString = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : "";
|
|
38
|
+
|
|
39
|
+
return `${timestamp} [${level.toUpperCase()}]: ${message} ${metaString}`;
|
|
40
|
+
});
|
|
41
|
+
const consoleFormat = format.printf(({level, message, timestamp, ...meta}) => {
|
|
42
|
+
const metaString = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : "";
|
|
43
|
+
const symbols = Object.getOwnPropertySymbols(meta);
|
|
44
|
+
const splatSymbol = symbols.find(sym => sym.description === "splat");
|
|
45
|
+
|
|
46
|
+
if (splatSymbol && typeof message === "string") {
|
|
47
|
+
const splat = meta[splatSymbol] as any[];
|
|
48
|
+
|
|
49
|
+
for (const item of splat) {
|
|
50
|
+
message = (message as string).replace(item, (match) => colorizer.colorize("verbose", match));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `${timestamp} [${colorizer.colorize(level, level.toUpperCase())}]: ${message}${colorizer.colorize("verbose", metaString)}`;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const createLogger = (options: ILoggerOptions): ILogger => {
|
|
58
|
+
const {level = "error", logFile, logFilePath = "log.log"} = options;
|
|
59
|
+
const transportsArray: transport[] = [
|
|
60
|
+
new transports.Console({format: format.combine(format.splat(), timeFormat, consoleFormat), level})
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
if (logFile) {
|
|
64
|
+
transportsArray.push(new transports.File({filename: logFilePath, level, format: format.combine(format.splat(), timeFormat, fileFormat)}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return createWinston({
|
|
68
|
+
level,
|
|
69
|
+
levels,
|
|
70
|
+
format: format.combine(timeFormat, format.json()),
|
|
71
|
+
transports: transportsArray,
|
|
72
|
+
}) as ILogger;
|
|
73
|
+
};
|
package/src/common/YtdplUtils.ts
CHANGED
|
@@ -69,7 +69,7 @@ const getPostProcessorArgs = (track: TrackInfo, album: AlbumInfo) => {
|
|
|
69
69
|
const title = track.title.replace(/"/g, "\\\"");
|
|
70
70
|
const artist = album.artist.replace(/"/g, "\\\"");
|
|
71
71
|
const albumTitle = album.title.replace(/"/g, "\\\"");
|
|
72
|
-
return getCutsPostProcessorArgs() + `-metadata title="${title}" -metadata artist="${artist}" -metadata album="${albumTitle}" -metadata track="${track.playlist_autonumber}" -metadata date="${album.releaseYear}" -metadata release_year="${album.releaseYear}"`;
|
|
72
|
+
return getCutsPostProcessorArgs() + `-metadata title="${title}" -metadata artist="${artist}" -metadata album="${albumTitle}" -metadata track="${track.playlist_autonumber}/${album.tracksNumber}" -metadata date="${album.releaseYear}" -metadata release_year="${album.releaseYear}"`;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
return getCutsPostProcessorArgs() + `-metadata title="${track.title}"`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import classnames from "classnames";
|
|
2
|
-
import
|
|
2
|
+
import {defaultTo, get, isEqual, map, without} from "lodash-es";
|
|
3
3
|
import moment from "moment";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import React, {HTMLAttributes, useState} from "react";
|
|
@@ -38,9 +38,9 @@ export const LanguagePicker = (props: LanguagePickerProps) => {
|
|
|
38
38
|
const { i18n } = useTranslation();
|
|
39
39
|
const [loading, setLoading] = useState(false);
|
|
40
40
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
|
41
|
-
const langs =
|
|
42
|
-
const availableLocales: string[] = !langs ? [] :
|
|
43
|
-
const displayMode =
|
|
41
|
+
const langs = get(i18n, "options.supportedLngs");
|
|
42
|
+
const availableLocales: string[] = !langs ? [] : without(langs, "cimode").sort();
|
|
43
|
+
const displayMode = defaultTo(
|
|
44
44
|
mode,
|
|
45
45
|
useMediaQuery((theme: Theme) => theme.breakpoints.down("md"))
|
|
46
46
|
? ComponentDisplayMode.Minimal
|
|
@@ -58,7 +58,7 @@ export const LanguagePicker = (props: LanguagePickerProps) => {
|
|
|
58
58
|
const onTriggerClick = (event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget);
|
|
59
59
|
|
|
60
60
|
const onItemClick = async (lang: string) => {
|
|
61
|
-
if (
|
|
61
|
+
if (!isEqual(lang, i18n.language)) {
|
|
62
62
|
setLoading(true);
|
|
63
63
|
i18n.changeLanguage(lang, () => setLoading(false));
|
|
64
64
|
moment.locale(lang);
|
|
@@ -81,7 +81,7 @@ export const LanguagePicker = (props: LanguagePickerProps) => {
|
|
|
81
81
|
open={Boolean(anchorEl)}
|
|
82
82
|
onClose={onClose}
|
|
83
83
|
>
|
|
84
|
-
{
|
|
84
|
+
{map(availableLocales, (item) => (
|
|
85
85
|
<LanguagePickerItem
|
|
86
86
|
key={item}
|
|
87
87
|
lang={item}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
@import "../../../styles/mixins.styl"
|
|
2
|
+
|
|
3
|
+
.cut-modal {
|
|
4
|
+
.content {
|
|
5
|
+
justify-content: flex-start;
|
|
6
|
+
|
|
7
|
+
.add-button {
|
|
8
|
+
min-width: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.delete-button {
|
|
12
|
+
min-width: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.entry-content {
|
|
16
|
+
width: 100%;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
align-items: center;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {filter, isEmpty, last, map, some, uniqueId} from "lodash-es";
|
|
2
|
+
import React, {KeyboardEvent, useState} from "react";
|
|
3
|
+
import {useTranslation} from "react-i18next";
|
|
4
|
+
import {NumberFormatBase} from "react-number-format";
|
|
5
|
+
|
|
6
|
+
import AddIcon from "@mui/icons-material/Add";
|
|
7
|
+
import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
|
|
8
|
+
import {
|
|
9
|
+
Avatar, Grid, IconButton, List, ListItem, ListItemAvatar, ListItemText, Stack, TextField,
|
|
10
|
+
useTheme
|
|
11
|
+
} from "@mui/material";
|
|
12
|
+
import Button from "@mui/material/Button";
|
|
13
|
+
import Dialog from "@mui/material/Dialog";
|
|
14
|
+
import DialogActions from "@mui/material/DialogActions";
|
|
15
|
+
import DialogContent from "@mui/material/DialogContent";
|
|
16
|
+
import DialogTitle from "@mui/material/DialogTitle";
|
|
17
|
+
|
|
18
|
+
import {formatTime, timeStringToNumber, unformatTime} from "../../../common/Helpers";
|
|
19
|
+
import Styles from "./CutModal.styl";
|
|
20
|
+
|
|
21
|
+
export type TrackCut = {
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
startTime: number;
|
|
25
|
+
endTime: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type CutModalProps = {
|
|
29
|
+
id: string;
|
|
30
|
+
duration: number;
|
|
31
|
+
open?: boolean;
|
|
32
|
+
onClose?: (data: TrackCut[]) => void;
|
|
33
|
+
onCancel?: () => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const CutModal: React.FC<CutModalProps> = (props: CutModalProps) => {
|
|
37
|
+
const {onClose, onCancel, duration, open, ...other} = props;
|
|
38
|
+
const {t} = useTranslation();
|
|
39
|
+
const theme = useTheme();
|
|
40
|
+
const [entries, setEntries] = useState<TrackCut[]>([
|
|
41
|
+
{
|
|
42
|
+
id: uniqueId(),
|
|
43
|
+
title: "",
|
|
44
|
+
startTime: 0,
|
|
45
|
+
endTime: duration,
|
|
46
|
+
}
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const onCutStartTimeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
50
|
+
const id = event.target.dataset.id;
|
|
51
|
+
|
|
52
|
+
setEntries((prev) => map(prev, (entry) => {
|
|
53
|
+
if (entry.id === id) {
|
|
54
|
+
return {...entry, startTime: timeStringToNumber(event.target.value)};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return entry;
|
|
58
|
+
}));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const onCutEndTimeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
62
|
+
const id = event.target.dataset.id;
|
|
63
|
+
|
|
64
|
+
setEntries((prev) => map(prev, (entry) => {
|
|
65
|
+
if (entry.id === id) {
|
|
66
|
+
return {...entry, endTime: timeStringToNumber(event.target.value)};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return entry;
|
|
70
|
+
}));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const onAddClick = () => {
|
|
74
|
+
setEntries((prev) => {
|
|
75
|
+
const lastEntry = last(prev);
|
|
76
|
+
const updated = [...prev, {
|
|
77
|
+
id: uniqueId(),
|
|
78
|
+
title: "",
|
|
79
|
+
startTime: lastEntry ? lastEntry.endTime + 1 : 0,
|
|
80
|
+
endTime: duration,
|
|
81
|
+
}];
|
|
82
|
+
|
|
83
|
+
return updated;
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const onTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
88
|
+
const id = event.target.dataset.id;
|
|
89
|
+
|
|
90
|
+
setEntries((prev) => map(prev, (entry) => {
|
|
91
|
+
if (entry.id === id) {
|
|
92
|
+
return {...entry, title: event.target.value};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return entry;
|
|
96
|
+
}));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const onDeleteClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
100
|
+
const id = event.currentTarget.dataset.id;
|
|
101
|
+
|
|
102
|
+
setEntries((prev) => filter(prev, (entry) => entry.id !== id));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleClose = () => {
|
|
106
|
+
if (onClose) {
|
|
107
|
+
onClose(entries);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleCancel = () => {
|
|
112
|
+
if (onCancel) {
|
|
113
|
+
onCancel();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
|
118
|
+
if (e.key === "Enter") {
|
|
119
|
+
onClose(entries);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Dialog
|
|
125
|
+
open={open}
|
|
126
|
+
disablePortal
|
|
127
|
+
onClose={handleCancel}
|
|
128
|
+
fullWidth
|
|
129
|
+
maxWidth="md"
|
|
130
|
+
className={Styles.cutModal}
|
|
131
|
+
onKeyUp={handleKeyUp}
|
|
132
|
+
{...other}
|
|
133
|
+
>
|
|
134
|
+
<DialogTitle textAlign="center">{t("cutModalTitle")}</DialogTitle>
|
|
135
|
+
<DialogContent dividers className={Styles.content}>
|
|
136
|
+
<Grid container display="flex" justifyItems="flex-end">
|
|
137
|
+
<Grid size={12}>
|
|
138
|
+
<List>
|
|
139
|
+
{map(entries, (entry, index) => <ListItem sx={{paddingX: 0}} divider key={entry.id}>
|
|
140
|
+
<ListItemAvatar><Avatar variant="circular" sx={{bgcolor: theme.palette.text.secondary}}>{index + 1}</Avatar></ListItemAvatar>
|
|
141
|
+
<ListItemText disableTypography>
|
|
142
|
+
<Stack direction="row" spacing={2} className={Styles.entryContent}>
|
|
143
|
+
<NumberFormatBase
|
|
144
|
+
slotProps={{
|
|
145
|
+
htmlInput: {"data-id": entry.id}
|
|
146
|
+
}}
|
|
147
|
+
value={entry.startTime}
|
|
148
|
+
onChange={onCutStartTimeChange}
|
|
149
|
+
format={formatTime}
|
|
150
|
+
removeFormatting={unformatTime}
|
|
151
|
+
customInput={TextField}
|
|
152
|
+
variant="outlined"
|
|
153
|
+
label={t("from")}
|
|
154
|
+
/>
|
|
155
|
+
<NumberFormatBase
|
|
156
|
+
slotProps={{
|
|
157
|
+
htmlInput: {"data-id": entry.id}
|
|
158
|
+
}}
|
|
159
|
+
value={entry.endTime}
|
|
160
|
+
onChange={onCutEndTimeChange}
|
|
161
|
+
format={formatTime}
|
|
162
|
+
removeFormatting={unformatTime}
|
|
163
|
+
customInput={TextField}
|
|
164
|
+
variant="outlined"
|
|
165
|
+
label={t("to")}
|
|
166
|
+
/>
|
|
167
|
+
<TextField
|
|
168
|
+
data-id={entry.id}
|
|
169
|
+
value={entry.title}
|
|
170
|
+
fullWidth
|
|
171
|
+
variant="outlined"
|
|
172
|
+
label={t("title")}
|
|
173
|
+
onChange={onTitleChange}
|
|
174
|
+
slotProps={{
|
|
175
|
+
htmlInput: {"data-id": entry.id}
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
<IconButton className={Styles.deleteButton} data-id={entry.id} color="inherit" onClick={onDeleteClick}>
|
|
179
|
+
<DeleteForeverIcon />
|
|
180
|
+
</IconButton>
|
|
181
|
+
</Stack>
|
|
182
|
+
</ListItemText>
|
|
183
|
+
</ListItem>)}
|
|
184
|
+
</List>
|
|
185
|
+
</Grid>
|
|
186
|
+
<Grid size={12} display="flex" justifyContent="center" alignItems="stretch">
|
|
187
|
+
<Button startIcon={<AddIcon />} className={Styles.addButton} variant="contained" disableElevation color="secondary" onClick={onAddClick}>{t("add")}</Button>
|
|
188
|
+
</Grid>
|
|
189
|
+
</Grid>
|
|
190
|
+
</DialogContent>
|
|
191
|
+
<DialogActions sx={{justifyContent: "end"}}>
|
|
192
|
+
<Button disabled={some(entries, (entry) => isEmpty(entry.title))} variant="contained" disableElevation autoFocus color="secondary" onClick={handleClose}>
|
|
193
|
+
{t("ok")}
|
|
194
|
+
</Button>
|
|
195
|
+
</DialogActions>
|
|
196
|
+
</Dialog>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export default CutModal;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {defaultTo, isUndefined, max as _max, min as _min, toNumber} from "lodash-es";
|
|
2
2
|
import React, {useEffect, useState} from "react";
|
|
3
3
|
import {NumberFormatValues, NumericFormat, NumericFormatProps} from "react-number-format";
|
|
4
4
|
import {useInterval} from "usehooks-ts";
|
|
@@ -53,7 +53,7 @@ export const NumberField = (props: INumberFieldProps) => {
|
|
|
53
53
|
() => {
|
|
54
54
|
if (decreasePressed) onDecreaseClick();
|
|
55
55
|
if (increasePressed) onIncreaseClick();
|
|
56
|
-
setDelay(
|
|
56
|
+
setDelay(_max([50, delay - 50]));
|
|
57
57
|
},
|
|
58
58
|
decreasePressed || increasePressed ? delay : null,
|
|
59
59
|
);
|
|
@@ -63,15 +63,15 @@ export const NumberField = (props: INumberFieldProps) => {
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
const onDecreaseClick = () => {
|
|
66
|
-
const predicted =
|
|
66
|
+
const predicted = toNumber(value) - toNumber(step);
|
|
67
67
|
|
|
68
|
-
setText(loop && predicted < min ? max :
|
|
68
|
+
setText(loop && predicted < min ? max : _max([predicted, min]));
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
const onIncreaseClick = () => {
|
|
72
|
-
const predicted =
|
|
72
|
+
const predicted = toNumber(value) + toNumber(step);
|
|
73
73
|
|
|
74
|
-
setText(loop && predicted > max ? min :
|
|
74
|
+
setText(loop && predicted > max ? min : _min([predicted, max]));
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
const onDecreaseMouseDown = () => {
|
|
@@ -93,7 +93,7 @@ export const NumberField = (props: INumberFieldProps) => {
|
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
const isAllowed = (values: NumberFormatValues) => {
|
|
96
|
-
if (
|
|
96
|
+
if (isUndefined(values.floatValue) && !allowEmpty) {
|
|
97
97
|
return false;
|
|
98
98
|
};
|
|
99
99
|
|
|
@@ -101,7 +101,7 @@ export const NumberField = (props: INumberFieldProps) => {
|
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
useEffect(() => {
|
|
104
|
-
const value =
|
|
104
|
+
const value = toNumber(text) ?? 0;
|
|
105
105
|
|
|
106
106
|
if (onChange) {
|
|
107
107
|
onChange(value);
|
|
@@ -146,7 +146,7 @@ export const NumberField = (props: INumberFieldProps) => {
|
|
|
146
146
|
label,
|
|
147
147
|
style: {textAlign: "center", width},
|
|
148
148
|
},
|
|
149
|
-
inputLabel:
|
|
149
|
+
inputLabel: defaultTo(inputLabelProps, {className: "upperfirst"})
|
|
150
150
|
}}
|
|
151
151
|
customInput={TextField}
|
|
152
152
|
variant="outlined"
|
|
@@ -155,6 +155,7 @@ export const NumberField = (props: INumberFieldProps) => {
|
|
|
155
155
|
decimalScale={decimalScale}
|
|
156
156
|
fixedDecimalScale={fixedDecimalScale}
|
|
157
157
|
isAllowed={isAllowed}
|
|
158
|
+
label={label}
|
|
158
159
|
{...rest}
|
|
159
160
|
/>
|
|
160
161
|
);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
compact, every, filter, isEmpty, isFunction, map, replace, truncate, uniq, without
|
|
3
3
|
} from "lodash-es";
|
|
4
|
-
import React, {
|
|
4
|
+
import React, {
|
|
5
|
+
ChangeEvent, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState
|
|
6
|
+
} from "react";
|
|
5
7
|
import {useTranslation} from "react-i18next";
|
|
6
8
|
import {useDebounceValue} from "usehooks-ts";
|
|
7
9
|
|
|
@@ -132,6 +134,12 @@ export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) =>
|
|
|
132
134
|
event.target.value = "";
|
|
133
135
|
};
|
|
134
136
|
|
|
137
|
+
const onTextFieldKeyUp = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
138
|
+
if (event.key === "Enter" && event.ctrlKey) {
|
|
139
|
+
handleLoadInfo();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
135
143
|
const onShowAdvancedSearchOptionsChange = (e: React.SyntheticEvent<HTMLDivElement>, isExpanded: boolean) => {
|
|
136
144
|
setApplicationOptions((prev) => ({...prev, showAdvancedSearchOptions: isExpanded}));
|
|
137
145
|
};
|
|
@@ -229,6 +237,7 @@ export const InputPanel: React.FC<InputPanelProps> = (props: InputPanelProps) =>
|
|
|
229
237
|
variant="outlined"
|
|
230
238
|
label={getInputLabel()}
|
|
231
239
|
error={containsInvalidValues}
|
|
240
|
+
onKeyUp={onTextFieldKeyUp}
|
|
232
241
|
slotProps={{
|
|
233
242
|
input: {
|
|
234
243
|
...params.InputProps,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import classnames from "classnames";
|
|
2
2
|
import {ipcRenderer} from "electron";
|
|
3
|
-
import {assign, includes, isFunction, pick, some} from "lodash-es";
|
|
3
|
+
import {assign, cloneDeep, forEach, includes, isFunction, last, map, pick, some} from "lodash-es";
|
|
4
4
|
import moment from "moment";
|
|
5
5
|
import React, {useCallback, useState} from "react";
|
|
6
6
|
import {useTranslation} from "react-i18next";
|
|
@@ -9,15 +9,17 @@ import AspectRatioIcon from "@mui/icons-material/AspectRatio";
|
|
|
9
9
|
import CloseIcon from "@mui/icons-material/Close";
|
|
10
10
|
import DownloadIcon from "@mui/icons-material/Download";
|
|
11
11
|
import EditIcon from "@mui/icons-material/Edit";
|
|
12
|
+
import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered";
|
|
12
13
|
import LaunchIcon from "@mui/icons-material/Launch";
|
|
13
14
|
import YouTubeIcon from "@mui/icons-material/YouTube";
|
|
14
15
|
import {
|
|
15
16
|
Box, Button, Card, CardContent, CardMedia, Grid, LinearProgress, Tooltip, Typography
|
|
16
17
|
} from "@mui/material";
|
|
17
18
|
|
|
18
|
-
import {AlbumInfo} from "../../../common/Youtube";
|
|
19
|
+
import {AlbumInfo, PlaylistInfo} from "../../../common/Youtube";
|
|
19
20
|
import {Messages} from "../../../messaging/Messages";
|
|
20
21
|
import {useDataState} from "../../../react/contexts/DataContext";
|
|
22
|
+
import CutModal, {TrackCut} from "../../modals/cutModal/CutModal";
|
|
21
23
|
import DetailsModal from "../../modals/detailsModal/DetailsModal";
|
|
22
24
|
import ImageModal from "../../modals/imageModal/ImageModal";
|
|
23
25
|
import Progress from "../../progress/Progress";
|
|
@@ -25,6 +27,7 @@ import Styles from "./MediaInfoPanel.styl";
|
|
|
25
27
|
|
|
26
28
|
export type MediaInfoPanelProps = {
|
|
27
29
|
item?: AlbumInfo;
|
|
30
|
+
playlist?: PlaylistInfo;
|
|
28
31
|
className?: string;
|
|
29
32
|
loading?: boolean;
|
|
30
33
|
progress?: number;
|
|
@@ -34,12 +37,14 @@ export type MediaInfoPanelProps = {
|
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export const MediaInfoPanel: React.FC<MediaInfoPanelProps> = (props: MediaInfoPanelProps) => {
|
|
37
|
-
const {item, className, onCancel, onDownload, onOpenOutput, loading, progress = 0} = props;
|
|
38
|
-
const {trackStatus, queue} = useDataState();
|
|
40
|
+
const {item, playlist, className, onCancel, onDownload, onOpenOutput, loading, progress = 0} = props;
|
|
41
|
+
const {trackStatus, setPlaylists, setTrackCuts, queue} = useDataState();
|
|
39
42
|
const [detailsModalOpen, setDetailsModalOpen] = useState(false);
|
|
43
|
+
const [cutTrackModalOpen, setCutTrackModalOpen] = useState(false);
|
|
40
44
|
const [imageModalOpen, setImageModalOpen] = useState(false);
|
|
41
45
|
const {t} = useTranslation();
|
|
42
46
|
const [value, setValue] = useState(item);
|
|
47
|
+
const [tracksSeparated, setTracksSeparated] = useState<boolean>();
|
|
43
48
|
|
|
44
49
|
const onDetailsModalClose = (data: AlbumInfo) => {
|
|
45
50
|
setValue((prev) => assign(prev, data));
|
|
@@ -49,6 +54,52 @@ export const MediaInfoPanel: React.FC<MediaInfoPanelProps> = (props: MediaInfoPa
|
|
|
49
54
|
const onImageModalClose = () => {
|
|
50
55
|
setImageModalOpen(false);
|
|
51
56
|
};
|
|
57
|
+
|
|
58
|
+
const onCutTrackModalCancel = () => {
|
|
59
|
+
setCutTrackModalOpen(false);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const onCutTrackModalClose = (cuts: TrackCut[]) => {
|
|
63
|
+
setPlaylists((prev) => {
|
|
64
|
+
const found = prev.find((p) => p.url === playlist.url);
|
|
65
|
+
const currentTrack = last(found.tracks);
|
|
66
|
+
|
|
67
|
+
return map(prev, (p) => {
|
|
68
|
+
if (p.url === playlist.url) {
|
|
69
|
+
const newTracks = map(cuts, (cutTrack, index) => {
|
|
70
|
+
const clonedTrack = cloneDeep(currentTrack);
|
|
71
|
+
|
|
72
|
+
return assign({}, clonedTrack, {
|
|
73
|
+
id: cutTrack.id,
|
|
74
|
+
title: cutTrack.title,
|
|
75
|
+
playlist: p.url,
|
|
76
|
+
playlist_autonumber: index + 1,
|
|
77
|
+
playlist_count: cuts.length,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
const updated = assign({}, found, {album: assign({}, found.album, {tracksNumber: cuts.length}), tracks: newTracks});
|
|
81
|
+
|
|
82
|
+
return updated;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return p;
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
setTrackCuts((prev) => {
|
|
90
|
+
const newCuts: {[key: string]: [number, number][]} = {};
|
|
91
|
+
|
|
92
|
+
forEach(cuts, (c) => {
|
|
93
|
+
newCuts[c.id] = [[c.startTime, c.endTime]];
|
|
94
|
+
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return {...prev, ...newCuts};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
setTracksSeparated(true);
|
|
101
|
+
setCutTrackModalOpen(false);
|
|
102
|
+
};
|
|
52
103
|
|
|
53
104
|
const cancel = () => {
|
|
54
105
|
if (isFunction(onCancel)) {
|
|
@@ -68,7 +119,11 @@ export const MediaInfoPanel: React.FC<MediaInfoPanelProps> = (props: MediaInfoPa
|
|
|
68
119
|
|
|
69
120
|
const editInfo = useCallback(() => {
|
|
70
121
|
setDetailsModalOpen(true);
|
|
71
|
-
}, [
|
|
122
|
+
}, [cutTrackModalOpen, setCutTrackModalOpen]);
|
|
123
|
+
|
|
124
|
+
const cutTrack = useCallback(() => {
|
|
125
|
+
setCutTrackModalOpen(true);
|
|
126
|
+
}, [cutTrackModalOpen, setCutTrackModalOpen]);
|
|
72
127
|
|
|
73
128
|
const showCoverImage = useCallback(() => {
|
|
74
129
|
setImageModalOpen(true);
|
|
@@ -121,6 +176,11 @@ export const MediaInfoPanel: React.FC<MediaInfoPanelProps> = (props: MediaInfoPa
|
|
|
121
176
|
<EditIcon />
|
|
122
177
|
</Button>
|
|
123
178
|
</Tooltip>
|
|
179
|
+
{(tracksSeparated || playlist.tracks.length === 1) && <Tooltip title={t("cut")} arrow enterDelay={2000} leaveDelay={100} enterNextDelay={500} placement="top">
|
|
180
|
+
<Button data-help="cutTrack" className={Styles.cutTrack} size="large" fullWidth variant="contained" color="primary" disableElevation onClick={cutTrack}>
|
|
181
|
+
<FormatListNumberedIcon />
|
|
182
|
+
</Button>
|
|
183
|
+
</Tooltip>}
|
|
124
184
|
{some(trackStatus, (s) => s.completed) &&
|
|
125
185
|
<Tooltip title={t("openOutputDirectory")} arrow enterDelay={2000} leaveDelay={100} enterNextDelay={500} placement="top">
|
|
126
186
|
<Button data-help="openOutputDirectory" className={Styles.openOutput} size="large" fullWidth variant="contained" color="primary" disableElevation onClick={openOutputFolder}>
|
|
@@ -175,6 +235,13 @@ export const MediaInfoPanel: React.FC<MediaInfoPanelProps> = (props: MediaInfoPa
|
|
|
175
235
|
onClose={onImageModalClose}
|
|
176
236
|
title={value.title}
|
|
177
237
|
/>
|
|
238
|
+
<CutModal
|
|
239
|
+
id="cut-modal"
|
|
240
|
+
duration={value.duration}
|
|
241
|
+
open={cutTrackModalOpen}
|
|
242
|
+
onClose={onCutTrackModalClose}
|
|
243
|
+
onCancel={onCutTrackModalCancel}
|
|
244
|
+
/>
|
|
178
245
|
</>
|
|
179
246
|
);
|
|
180
247
|
};
|
|
@@ -220,6 +220,7 @@ export const PlaylistTabs: React.FC<PlaylistTabsProps> = (props: PlaylistTabsPro
|
|
|
220
220
|
{map(filter(playlists, (p) => !isEmpty(p.album)), (item) =>
|
|
221
221
|
<TabPanel className={Styles.tabPanel} value={item.url} key={item.url}>
|
|
222
222
|
<MediaInfoPanel
|
|
223
|
+
playlist={item}
|
|
223
224
|
item={item.album}
|
|
224
225
|
loading={isPlaylistLoading(item.album.id)}
|
|
225
226
|
progress={getTotalProgress(item.album.id)}
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
Tooltip, Typography
|
|
22
22
|
} from "@mui/material";
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
formatFileSize, formatTime, timeStringToNumber, unformatTime
|
|
26
|
+
} from "../../../common/Helpers";
|
|
25
27
|
import {TrackInfo, TrackStatusInfo} from "../../../common/Youtube";
|
|
26
28
|
import {useDataState} from "../../../react/contexts/DataContext";
|
|
27
29
|
import DetailsModal from "../../modals/detailsModal/DetailsModal";
|
|
@@ -50,28 +52,6 @@ export const TrackList: React.FC<TrackListProps> = (props: TrackListProps) => {
|
|
|
50
52
|
setValue(items ?? tracks);
|
|
51
53
|
}, [items, tracks]);
|
|
52
54
|
|
|
53
|
-
const formatTime = (value: string) => {
|
|
54
|
-
const formatted = moment.duration(value, "seconds").format("HH:mm:ss", {trim: "left"});
|
|
55
|
-
|
|
56
|
-
if (/^\d{2}$/.test(formatted)) {
|
|
57
|
-
return `00:${formatted}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return formatted;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const unformatTime = (value: string) => {
|
|
64
|
-
if (/^\d{2}$/.test(value)) {
|
|
65
|
-
return moment.duration(`00:00:${value}`).asSeconds() + "";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return moment.duration(`00:${value}`).asSeconds() + "";
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const timeStringToNumber = (value: string) => {
|
|
72
|
-
return moment.duration(`00:${value}`).asSeconds();
|
|
73
|
-
};
|
|
74
|
-
|
|
75
55
|
const sanitizeTrackCuts = (source: {[key: string]: [number, number][]}) => {
|
|
76
56
|
forEach(source, (v, k) => {
|
|
77
57
|
const track = find(value, ["id", k]);
|
package/src/hooks/useHelp.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,24 +4,32 @@ import electronReload from "electron-reload";
|
|
|
4
4
|
import Store from "electron-store";
|
|
5
5
|
import path from "path";
|
|
6
6
|
|
|
7
|
+
import {isDebugMode, isDevApplication} from "./common/Helpers";
|
|
8
|
+
import {createLogger} from "./common/Logger";
|
|
7
9
|
import {MessagingService} from "./messaging/MessagingService";
|
|
8
10
|
|
|
9
|
-
const
|
|
11
|
+
const logger = createLogger({
|
|
12
|
+
level: isDebugMode() ? "debug" : "error",
|
|
13
|
+
logFile: true,
|
|
14
|
+
logFilePath: "init.log",
|
|
15
|
+
});
|
|
10
16
|
|
|
11
|
-
if (
|
|
17
|
+
if (isDevApplication(app)) {
|
|
12
18
|
electronReload(__dirname, {
|
|
13
19
|
electron: path.join(__dirname, "..", "node_modules", "electron", "dist", "electron.exe"),
|
|
14
20
|
interval: 2000,
|
|
15
21
|
});
|
|
16
|
-
}
|
|
17
22
|
|
|
18
|
-
/* Alternative reload using different electron binary */
|
|
23
|
+
/* Alternative reload using different electron binary */
|
|
19
24
|
|
|
20
|
-
// require("electron-reload")(__dirname, {
|
|
21
|
-
// electron: path.join(__dirname, "..", "node_modules", ".bin", "electron.cmd"),
|
|
22
|
-
// hardReset: true,
|
|
23
|
-
// livenessThreshold: 2000,
|
|
24
|
-
// });
|
|
25
|
+
// require("electron-reload")(__dirname, {
|
|
26
|
+
// electron: path.join(__dirname, "..", "node_modules", ".bin", "electron.cmd"),
|
|
27
|
+
// hardReset: true,
|
|
28
|
+
// livenessThreshold: 2000,
|
|
29
|
+
// });
|
|
30
|
+
|
|
31
|
+
logger.debug("Electron reload initialized.");
|
|
32
|
+
}
|
|
25
33
|
|
|
26
34
|
let mainWindow: BrowserWindow | null;
|
|
27
35
|
|
|
@@ -41,9 +49,11 @@ const createWindow = async () => {
|
|
|
41
49
|
},
|
|
42
50
|
});
|
|
43
51
|
mainWindow.loadFile(path.join(__dirname, "index.html"));
|
|
52
|
+
logger.debug("Main window created.");
|
|
44
53
|
|
|
45
|
-
if (
|
|
46
|
-
mainWindow.webContents.openDevTools({mode: "detach"
|
|
54
|
+
if (isDevApplication(app)) {
|
|
55
|
+
mainWindow.webContents.openDevTools({mode: "detach"});
|
|
56
|
+
logger.debug("DevTools opened.");
|
|
47
57
|
} else {
|
|
48
58
|
mainWindow.removeMenu();
|
|
49
59
|
mainWindow.setMenu(null);
|
|
@@ -51,9 +61,12 @@ const createWindow = async () => {
|
|
|
51
61
|
|
|
52
62
|
mainWindow.on("closed", () => {
|
|
53
63
|
mainWindow = null;
|
|
64
|
+
logger.debug("Main window closed.");
|
|
54
65
|
});
|
|
55
66
|
|
|
67
|
+
|
|
56
68
|
const messaggingService = new MessagingService(ipcMain, mainWindow);
|
|
69
|
+
logger.debug("Messaging service initialized.");
|
|
57
70
|
};
|
|
58
71
|
|
|
59
72
|
app.on("ready", createWindow);
|
|
@@ -77,9 +90,9 @@ app.on("before-quit", () => {
|
|
|
77
90
|
});
|
|
78
91
|
|
|
79
92
|
app.whenReady().then(() => {
|
|
80
|
-
if (
|
|
93
|
+
if (isDevApplication(app)) {
|
|
81
94
|
installExtension(REACT_DEVELOPER_TOOLS)
|
|
82
|
-
.then((name) =>
|
|
83
|
-
.catch((err) =>
|
|
95
|
+
.then((name) => logger.debug("Added extension: %s", name))
|
|
96
|
+
.catch((err) => logger.error("An error occurred: %s", err));
|
|
84
97
|
}
|
|
85
98
|
});
|
package/src/renderer.tsx
CHANGED
|
@@ -3,11 +3,22 @@ import * as React from "react";
|
|
|
3
3
|
import * as ReactDOM from "react-dom/client";
|
|
4
4
|
|
|
5
5
|
import {Bootstrap} from "./bootstrap";
|
|
6
|
+
import {getProcessArgs} from "./common/Helpers";
|
|
7
|
+
import {createLogger} from "./common/Logger";
|
|
6
8
|
import schema from "./common/Store";
|
|
7
9
|
|
|
10
|
+
const args = getProcessArgs();
|
|
11
|
+
|
|
12
|
+
global.logger = createLogger({
|
|
13
|
+
level: args["debug"] ? "debug" : "error",
|
|
14
|
+
logFile: true,
|
|
15
|
+
logFilePath: "application.log",
|
|
16
|
+
});
|
|
8
17
|
global.store = new Store({ schema, clearInvalidConfig: true });
|
|
18
|
+
logger.debug("Electron store initialized.");
|
|
9
19
|
|
|
10
20
|
const container = document.getElementById("root");
|
|
11
21
|
const root = ReactDOM.createRoot(container!);
|
|
12
22
|
|
|
13
23
|
root.render(React.createElement(Bootstrap));
|
|
24
|
+
logger.debug("Application rendered.");
|
|
Binary file
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"add": "Hinzufügen",
|
|
2
3
|
"albumOrAlbums": "Albumname/Albumnamen",
|
|
3
4
|
"albumOutputTemplate": "Ausgabenvorlage (Alben)",
|
|
4
5
|
"albums": "Alben",
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
"convertingOutput": "Ausgabedatei konvertieren",
|
|
19
20
|
"customYtdlpArgs": "Benutzerdefinierte Argumente für yt-dlp",
|
|
20
21
|
"cut": "Schneiden",
|
|
22
|
+
"cutModalTitle": "Track Extraktion",
|
|
21
23
|
"debugMode": "Debug-Modus",
|
|
22
24
|
"default": "Default",
|
|
23
25
|
"detailsModalTitle": "Mediendetails bearbeiten",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"add": "Add",
|
|
2
3
|
"albumOrAlbums": "Album/albums",
|
|
3
4
|
"albumOutputTemplate": "Output template (albums)",
|
|
4
5
|
"albums": "Albums",
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
"convertingOutput": "Converting output",
|
|
19
20
|
"customYtdlpArgs": "Custom arguments for yt-dlp",
|
|
20
21
|
"cut": "Cut",
|
|
22
|
+
"cutModalTitle": "Track Extraction",
|
|
21
23
|
"debugMode": "Debug Mode",
|
|
22
24
|
"default": "Default",
|
|
23
25
|
"detailsModalTitle": "Edit media details",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"add": "Dodaj",
|
|
2
3
|
"albumOrAlbums": "Nazwa lub nazwy albumów",
|
|
3
4
|
"albumOutputTemplate": "Szablon wyjściowy (albumy)",
|
|
4
5
|
"albums": "Albumy",
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
"convertingOutput": "Konwertowanie pliku",
|
|
19
20
|
"customYtdlpArgs": "Dodatkowe argumenty dla yt-dlp",
|
|
20
21
|
"cut": "Przytnij",
|
|
22
|
+
"cutModalTitle": "Wyodrębnianie ścieżek",
|
|
21
23
|
"debugMode": "Tryb debugowania",
|
|
22
24
|
"default": "Domyślna",
|
|
23
25
|
"detailsModalTitle": "Edycja danych",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {assign, get} from "lodash-es";
|
|
2
2
|
|
|
3
3
|
import {Theme} from "@mui/material";
|
|
4
4
|
import {grey} from "@mui/material/colors";
|
|
@@ -325,5 +325,5 @@ const themes: Record<string, Partial<Theme | any>> = {
|
|
|
325
325
|
export const getThemeDefinition = (name: string, mode: "light" | "dark") => {
|
|
326
326
|
const theme = themes[name];
|
|
327
327
|
|
|
328
|
-
return
|
|
328
|
+
return assign({}, theme, { palette: get(theme, `palette.${mode}`) });
|
|
329
329
|
};
|