Skip to content

Commit 63bb61f

Browse files
authored
Thumbnails (#3)
* Show default thumbnail * Add color picker * Customizable logo * It kinda works * Save multiple thumbnails * Support for character icons * Fix unknown characters
1 parent 46374dc commit 63bb61f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

98 files changed

+825
-100
lines changed

assets/MSLogo.png

12.4 KB

package-lock.json

Lines changed: 449 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"@mui/material": "^5.11.6",
120120
"@mui/x-date-pickers": "^5.0.17",
121121
"axios": "^1.2.4",
122+
"buffer": "^6.0.3",
122123
"dotenv": "^16.0.3",
123124
"electron-debug": "^3.2.0",
124125
"electron-is-dev": "^2.0.0",
@@ -128,8 +129,11 @@
128129
"google-auth-library": "^8.7.0",
129130
"googleapis": "^110.0.0",
130131
"graphql": "^16.6.0",
132+
"html2canvas": "^1.4.1",
131133
"moment": "^2.29.4",
134+
"mui-color-input": "^1.0.4",
132135
"react": "^18.2.0",
136+
"react-component-export-image": "^1.0.6",
133137
"react-dom": "^18.2.0",
134138
"react-router-dom": "^6.4.0",
135139
"react-spinners": "^0.13.8",

src/main/main.ts

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ ipcMain.handle('create-folder', async (event, arg) => {
7373
});
7474

7575
ipcMain.handle('open-folder', async (event, arg) => {
76-
shell.openPath(process.cwd() + path.sep + 'downloadedVODs' + path.sep + arg);
76+
shell.openPath(process.cwd() + path.sep + 'downloadedVODs' + path.sep + arg.replace(":", "#"));
7777
});
7878

7979
ipcMain.handle('retrieve-video-information', async (event, arg) => {
@@ -118,37 +118,68 @@ ipcMain.handle('open-google-login', async (event, arg) => {
118118
return myApiOauth.openAuthWindowAndGetTokens();
119119
});
120120

121+
ipcMain.handle('create-thumbnail-folder', async(event, arg) => {
122+
fs.mkdir('./downloadedVODs/' + arg.replace(":", "#") + "/thumbnails", { recursive: true }, (err: Error) => {
123+
if (err) throw err;
124+
})
125+
})
126+
127+
ipcMain.handle('save-thumbnail', async (event, args) => {
128+
console.log("Saving thumbnail: ", args)
129+
// fs.mkdir('./downloadedVODs/' + args.folderName.replace(":", "#") + "/thumbnails", { recursive: true }, (err: Error) => {
130+
// if (err) throw err;
131+
// })
132+
fs.writeFile("./downloadedVODs/" + args.folderName.replace(":", "#") + "/thumbnails/" + args.fileName.replace(":", "#") + ".jpg", args.buf, function (err: any) {
133+
if (err) {
134+
console.log(err);
135+
}
136+
})
137+
})
138+
121139
ipcMain.handle('upload-videos', async (event, args) => {
122-
const fileSize = fs.statSync(args.path).size;
123-
return youtube.videos.insert(
124-
{
125-
part: 'id,snippet,status',
126-
notifySubscribers: false,
127-
requestBody: {
128-
snippet: {
129-
title: args.videoName.replace(/\.[^/.]+$/, ''),
130-
description: args.description,
131-
},
132-
status: {
133-
privacyStatus: 'unlisted',
134-
},
135-
},
136-
media: {
137-
body: fs.createReadStream(args.path),
138-
},
139-
access_token: args.accessToken,
140-
},
141-
{
142-
// Use the `onUploadProgress` event from Axios to track the
143-
// number of bytes uploaded to this point.
144-
onUploadProgress: (evt: any) => {
145-
const progress = (evt.bytesRead / fileSize) * 100;
146-
readline.clearLine(process.stdout, 0);
147-
readline.cursorTo(process.stdout, 0, null);
148-
process.stdout.write(`${Math.round(progress)}% complete`);
149-
},
140+
return fs.readdirSync(args.path).forEach((videoName: any) => {
141+
const fileSize = fs.statSync(args.path + videoName).size;
142+
if (path.extname(videoName).toLowerCase() === '.mp4') {
143+
console.log('videoName', videoName);
144+
return youtube.videos
145+
.insert(
146+
{
147+
part: 'id,snippet,status',
148+
notifySubscribers: true,
149+
requestBody: {
150+
snippet: {
151+
title: videoName.replace(/\.[^/.]+$/, ''),
152+
description: args.description,
153+
},
154+
status: {
155+
privacyStatus: args.visibility,
156+
},
157+
},
158+
media: {
159+
body: fs.createReadStream(args.path + videoName),
160+
},
161+
access_token: args.accessToken,
162+
},
163+
{
164+
// Use the `onUploadProgress` event from Axios to track the
165+
// number of bytes uploaded to this point.
166+
onUploadProgress: (evt: any) => {
167+
const progress = (evt.bytesRead / fileSize) * 100;
168+
readline.clearLine(process.stdout, 0);
169+
readline.cursorTo(process.stdout, 0, null);
170+
process.stdout.write(`${Math.round(progress)}% complete`);
171+
},
172+
}
173+
)
174+
.then((response: any) => {
175+
console.log('response', response);
176+
})
177+
.catch((error: any) => {
178+
console.log('error', error);
179+
return error.errors[0].message;
180+
});
150181
}
151-
)
182+
})
152183
});
153184

154185
ipcMain.handle('get-api-key', async (event, arg) => {

src/main/preload.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ const electronHandler = {
4747
openGoogleLogin(arg: string) {
4848
return ipcRenderer.invoke('open-google-login', arg)
4949
},
50+
createThumbnailFolder(arg: string) {
51+
return ipcRenderer.invoke('create-thumbnail-folder', arg)
52+
},
53+
saveThumbnail(args: object) {
54+
return ipcRenderer.invoke('save-thumbnail', args)
55+
},
5056
uploadVideos(args: object) {
5157
return ipcRenderer.invoke('upload-videos', args)
5258
},

src/renderer/App.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,13 @@ a:hover {
8181
.timepicker {
8282
width: 78vw;
8383
}
84+
85+
.centered-flex {
86+
display: flex;
87+
justify-content: center;
88+
align-items: center;
89+
}
90+
91+
.MuiColorInput-TextField{
92+
width: 50%;
93+
}

src/renderer/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import './App.css';
44
import { Box, Typography, Container, Link, ThemeProvider } from '@mui/material';
55
import VideoSearch from './components/VideoSearch';
66
import YTUploadView from './pages/YTUploadView';
7+
import ThumbnailGenerator from './components/ThumbnailGenerator';
78
import {
89
ApolloClient,
910
InMemoryCache,
@@ -105,6 +106,8 @@ export default function App() {
105106
<Route path="/SetsView" element={<SetsView />} />
106107
{/* <Route path="/" element={<YTUploadView />} /> */}
107108
<Route path="/YTUploadView" element={<YTUploadView />} />
109+
{/* <Route path="/ThumbnailGenerator" element={<ThumbnailGenerator />} />
110+
<Route path="/" element={<ThumbnailGenerator />} /> */}
108111
</Routes>
109112
</Router>
110113
</ThemeProvider>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React, {ReactInstance, ReactNode} from "react";
2+
import { Box, Grid, Typography } from "@mui/material";
3+
import DefaultIcon from '../../../assets/MsLogo.png';
4+
5+
const ThumbnailText = (text: string, variant: any) => {
6+
return (
7+
<Typography variant={variant} sx={{ color: 'white', fontWeight: 'bold', textAlign: 'center'}} noWrap>
8+
{text}
9+
</Typography>
10+
)
11+
}
12+
13+
interface Props {
14+
children?: ReactNode;
15+
scale?: string;
16+
bgColor?: string;
17+
logo?: string;
18+
player1?: string;
19+
player2?: string;
20+
character1?: string;
21+
character2?: string;
22+
bottomText?: string;
23+
visible?: boolean;
24+
title?: string;
25+
}
26+
export type Ref = ReactInstance;
27+
28+
const characterFolder = './characters/';
29+
30+
// Create Document Component
31+
const ThumbnailGenerator = React.forwardRef<Ref, Props>((props, ref) => {
32+
// console.log("images", images)
33+
let displayOption = props.visible ? 'block' : 'none';
34+
return (
35+
<Box id={props.title} ref={ref} sx={{ height: '720px', width: '1280px', backgroundColor: '#B9F3FC', scale: props.scale, transformOrigin: 'top left', display: displayOption }}>
36+
<Grid container>
37+
<Grid item xs={12}>
38+
<Grid container sx={{ width: '100%', height: '110px', backgroundColor: 'black'}}>
39+
<Grid item xs={5} className="centered-flex">
40+
{ThumbnailText(props.player1!, "h3")}
41+
</Grid>
42+
<Grid item xs={2} className="centered-flex">
43+
{ThumbnailText('VS', "h1")}
44+
</Grid>
45+
<Grid item xs={5} className="centered-flex">
46+
{ThumbnailText(props.player2!, "h3")}
47+
</Grid>
48+
</Grid>
49+
</Grid>
50+
<Grid item xs={12}>
51+
<Grid container sx={{ width: '100%', height: '500px', backgroundColor: props.bgColor}}>
52+
<Grid sx={{ position: 'relative', left: '-50px', bottom: '10px'}} item xs={5} className="centered-flex">
53+
<img src={require(`${ characterFolder + props.character1 + '.png' }`)} />
54+
</Grid>
55+
<Grid item xs={2} className="centered-flex">
56+
<img src={props.logo === '' ? DefaultIcon : props.logo} width="250px" height="250px" />
57+
</Grid>
58+
<Grid sx={{ position: 'relative', left: '50px', bottom: '10px'}} item xs={5} className="centered-flex">
59+
<img src={require(`${ characterFolder + props.character2 + '.png' }`)} />
60+
</Grid>
61+
</Grid>
62+
</Grid>
63+
<Grid item xs={12}>
64+
<Grid container sx={{ width: '100%', height: '110px', backgroundColor: 'black'}}>
65+
{/* <Grid item xs={2} className="centered-flex">
66+
</Grid> */}
67+
<Grid item xs={12} className="centered-flex">
68+
{ThumbnailText(props.bottomText!, "h2")}
69+
</Grid>
70+
{/* <Grid item xs={2} className="centered-flex"> */}
71+
{/* </Grid> */}
72+
</Grid>
73+
</Grid>
74+
</Grid>
75+
</Box>
76+
)
77+
});
78+
79+
ThumbnailGenerator.defaultProps = {
80+
scale: '1',
81+
bgColor: '#B9F3FC',
82+
logo: DefaultIcon,
83+
player1: 'Player 1',
84+
player2: 'Player 2',
85+
character1: 'Random Character',
86+
character2: 'Random Character',
87+
bottomText: 'Grand Finals',
88+
visible: true
89+
}
90+
91+
export default ThumbnailGenerator

src/renderer/components/VideoSearch.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from 'react';
1+
import { createRef, useState, useEffect, ReactInstance } from 'react';
22
import { Button, Box, TextField } from '@mui/material';
33
import { GET_SETS_AT_STATION } from 'renderer/common/StartggQueries';
44
import { useLazyQuery } from '@apollo/client';
@@ -13,6 +13,11 @@ export interface VODMetadata {
1313
startTime: string;
1414
endTime: string;
1515
download: boolean;
16+
player1: string;
17+
player2: string;
18+
character1: string;
19+
character2: string;
20+
tournamentName: string;
1621
}
1722

1823
const RetrieveSets = (
@@ -61,6 +66,7 @@ const RetrieveSets = (
6166
let characterMap = window.electron.store.get('characterMap');
6267
const formattedSets = data.event.sets.nodes.map((set: any) => {
6368
let characterStrings = ['', ''];
69+
let characters = ['Random Character', 'Random Character'];
6470
if (set.games != null) {
6571
let characterArrays: string[][] = [[], []];
6672
for (const game of set.games) {
@@ -84,6 +90,8 @@ const RetrieveSets = (
8490
}
8591
characterStrings[0] = ' (' + characterArrays[0].join(', ') + ')';
8692
characterStrings[1] = ' (' + characterArrays[1].join(', ') + ')';
93+
characters[0] = characterArrays[0][0]
94+
characters[1] = characterArrays[1][0]
8795
}
8896
let metadata: VODMetadata = {
8997
title:
@@ -103,6 +111,11 @@ const RetrieveSets = (
103111
.toISOString()
104112
.slice(11, 19),
105113
download: true,
114+
player1: set.slots[0].entrant.name.split('|').pop().trim(),
115+
player2: set.slots[1].entrant.name.split('|').pop().trim(),
116+
character1: characters[0],
117+
character2: characters[1],
118+
tournamentName: data.event.tournament.name,
106119
};
107120
return metadata;
108121
});
364 KB

0 commit comments

Comments
 (0)