Compare commits
8 Commits
7fa1d0edb3
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
4564ca83c1 | |||
51e5e8c981 | |||
2feb4e3d81 | |||
7f1447e57c | |||
b9c4858460 | |||
0a53136f1a | |||
bc8a603d4e | |||
494b9dae2b |
@ -8,7 +8,7 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/6.4.3/css/foundation.min.css" />
|
||||
</head>
|
||||
<body>
|
||||
<h2>Admin Page</h2>
|
||||
<h2><a href="/">DumbYT</a> Admin</h2>
|
||||
<div id="admin_app"></div>
|
||||
<script src="./../dist/build.js"></script>
|
||||
</body>
|
||||
|
@ -10,7 +10,7 @@
|
||||
<body>
|
||||
<div class="pageheader">
|
||||
<h1>Dumb YT Manager</h1>
|
||||
<p>Youtube banned my account and refuses to say why, taking all my subscribed channels with it. This is a simple scrubscribed channel manager, showing recent releases from each channel and finally proper sub-catagory functionality!</p>
|
||||
<p>Youtube banned my account and refuses to say why, taking all my channel subscriptions with it. So here is a simple channel subscription manager of my own, showing recent releases from each channel.</p>
|
||||
</div>
|
||||
|
||||
<div id="app"></div>
|
||||
|
10969
package-lock.json
generated
10969
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@ -7,20 +7,20 @@
|
||||
"author": "hak8or",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.104",
|
||||
"@types/lodash": "^4.14.118",
|
||||
"axios": "^0.18.0",
|
||||
"css-loader": "^0.28.10",
|
||||
"style-loader": "^0.20.2",
|
||||
"sweetalert2": "^7.13.3",
|
||||
"ts-loader": "^4.0.0",
|
||||
"typescript": "^2.7.2",
|
||||
"uglifyjs-webpack-plugin": "^1.2.2",
|
||||
"vue": "^2.5.13",
|
||||
"vue-loader": "^14.1.1",
|
||||
"vue-template-compiler": "^2.5.13",
|
||||
"webpack": "^4.0.1",
|
||||
"webpack-cli": "^2.0.9",
|
||||
"webpack-dev-server": "^3.1.0"
|
||||
"css-loader": "^1.0.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"sweetalert2": "^7.28.2",
|
||||
"ts-loader": "^5.3.0",
|
||||
"typescript": "^2.9.2",
|
||||
"uglifyjs-webpack-plugin": "^2.0.1",
|
||||
"vue": "^2.5.17",
|
||||
"vue-loader": "^15.4.2",
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
"webpack": "^4.25.1",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.10"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --mode development --open",
|
||||
|
15
readme.md
15
readme.md
@ -1,10 +1,19 @@
|
||||
# Frontend
|
||||
|
||||
## Description
|
||||
Oh god, front end web development, specifically javascript.
|
||||
|
||||
Oh god, front end web development, specifically javascript. This is the front end for the API based backend of DumbYT, a simple Youtube manager that keeps track of various channels or videos a user wants to be "subscribed" to.
|
||||
|
||||
This project makes use of the following:
|
||||
|
||||
- Vuejs
|
||||
Seemingly resonable javascript framework that's well documented and actively developed.
|
||||
- **Vuejs** Reasonable javascript framework that's well documented and actively developed.
|
||||
- **Axios** Nice HTTP post/delete/etc helper library
|
||||
- **TypeScript** Making javascript more bearable, adds types to the language and other niceties.
|
||||
- **WebPack** Handles running the Typescript compiler and minifying and running a server for development.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Clone the repository
|
||||
2. Run ```yarn install``` to download all the project dependencies.
|
||||
3. Run ```yarn start``` which will automatically start a server and recompile the typescript based backend when there are file changes. Also, ```/api``` is proxied back to the production server, though this can be edited in ```webpack.config.js```.
|
||||
4. Run ```yarn build``` which generates the frontend files for production.
|
||||
|
@ -197,31 +197,32 @@ export default Vue.extend({
|
||||
|
||||
GetChannelFromYT(Channel: string) : void {
|
||||
// Say it failed first so if we exit early then correctly marked fail.
|
||||
this.Title = "";
|
||||
this.Description = "";
|
||||
this.ID = "";
|
||||
this.Thumbnail = "";
|
||||
this.Valid = false;
|
||||
|
||||
// Copy over to internal ID box.
|
||||
this.ID = Channel;
|
||||
// Remove possible channel inputs.
|
||||
// https://www.youtube.com/channel/UC2DjFE7Xf11URZqWBigcVOQ
|
||||
// UC2DjFE7Xf11URZqWBigcVOQ
|
||||
// https://www.youtube.com/user/EEVblog <-- Take first channel found
|
||||
// EEVblog <-- Take first channel found
|
||||
|
||||
// Remove any potential youtube URL from the field.
|
||||
const ytchurl = "https://www.youtube.com/channel/";
|
||||
if (this.ID.startsWith(ytchurl))
|
||||
this.ID = this.ID.replace(ytchurl, "");
|
||||
|
||||
// Check if what remains looks like a youtube channel ID.
|
||||
if (this.ID.length != "UCyS4xQE6DK4_p3qXQwJQAyA".length){
|
||||
this.Title = "";
|
||||
this.Description = "";
|
||||
this.ID = "";
|
||||
this.Thumbnail = "";
|
||||
this.Valid = false;
|
||||
}
|
||||
Channel = Channel.replace("https://www.youtube.com", "");
|
||||
Channel = Channel.replace("/channel/", "");
|
||||
Channel = Channel.replace("/user/", "");
|
||||
|
||||
// Get channel metadata.
|
||||
const API = 'https://www.googleapis.com/youtube/v3/channels?';
|
||||
const API_Parts = 'part=snippet%2CcontentDetails%2Cstatistics';
|
||||
const API = 'https://www.googleapis.com/youtube/v3/channels/?';
|
||||
const API_Parts = 'part=snippet';
|
||||
const API_Key = '&key=AIzaSyCuIYkMc5SktlnXRXNaDf2ObX-fQvtWCnQ'
|
||||
const API_Search_ID = '&id=' + this.ID;
|
||||
Axios.get(API + API_Parts + API_Search_ID + API_Key).then((resp) => {
|
||||
const API_Search_Query =
|
||||
((Channel.length == "UCyS4xQE6DK4_p3qXQwJQAyA".length) ? "&id=" : "&forUsername=")
|
||||
+ Channel;
|
||||
Axios.get(API + API_Parts + API_Search_Query + API_Key).then((resp) => {
|
||||
this.ID = resp.data.items[0].id;
|
||||
this.Description = _.truncate(resp.data.items[0].snippet.description, {length: 70});
|
||||
this.Title = resp.data.items[0].snippet.title;
|
||||
this.Thumbnail = resp.data.items[0].snippet.thumbnails.high.url;
|
||||
|
@ -6,6 +6,7 @@
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Tags</th>
|
||||
<th>Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -14,6 +15,7 @@
|
||||
<td>{{channel.Title}}</td>
|
||||
<td>{{channel.Description}}</td>
|
||||
<td>{{channel.User_Tags}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
60
src/components/Favorite_Channels.vue
Normal file
60
src/components/Favorite_Channels.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="favtable">
|
||||
<h3>High Priority Videos</h3>
|
||||
<div class="grid-x large-up-6">
|
||||
<div v-for="video in Videos" :key="video.ID" class="cell">
|
||||
<div class="card ytcard">
|
||||
<a :href="video.URL"><img :src="video.Thumbnail"></a>
|
||||
<div class="card-section description">
|
||||
<div class="descriptiontitle">{{ video.Title }}</div>
|
||||
<div class="descriptionchannel">{{ video.Channel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue, {PropOptions} from "vue";
|
||||
import * as Dyt from "../dumbyt";
|
||||
|
||||
// Vue class for keeping state of the videos.
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
Videos: {
|
||||
type: Array,
|
||||
} as PropOptions<Dyt.Video[]>,
|
||||
},
|
||||
|
||||
mounted() {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ytcard {
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
background-color:#ddd9d4;
|
||||
margin: 6px 2px 6px;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 7px 5px 7px;
|
||||
}
|
||||
|
||||
.descriptiontitle {
|
||||
font-size: 14px;
|
||||
padding-bottom:10px;
|
||||
}
|
||||
|
||||
.descriptionchannel {
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.favtable h3 {
|
||||
margin-left: 5rem;
|
||||
}
|
||||
</style>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<input id="searchbox0" class="searchbox" type="text" v-model="searchbox_str"
|
||||
<input class="searchbox" type="text" v-model="searchbox_str"
|
||||
placeholder="Search String goes here, for example: >5m,c++"
|
||||
/>
|
||||
</div>
|
||||
@ -25,7 +25,7 @@ export default Vue.extend({
|
||||
searchbox_str (s: string) {
|
||||
var v = _.debounce((s) => {
|
||||
DumbYT.API.search_videos(s).then(videos => {
|
||||
this.$emit('search_complete', videos)
|
||||
this.$emit('search_complete', s, videos)
|
||||
});
|
||||
}, 400)
|
||||
v(s);
|
||||
@ -40,6 +40,5 @@ export default Vue.extend({
|
||||
max-width: 40em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 2em;
|
||||
}
|
||||
</style>
|
||||
|
@ -58,12 +58,14 @@ export namespace API {
|
||||
}
|
||||
|
||||
// Search for channels based on a search string.
|
||||
export async function search_channels(searchstr : string): Promise<Array<Channel>> {
|
||||
export async function search_channels(searchstr : string, highpriority?: boolean): Promise<Array<Channel>> {
|
||||
// Temporary holder for data.
|
||||
let Channels : Array<Channel> = [];
|
||||
|
||||
// Ask server for data.
|
||||
let resp = await Axios.get(API.Base_URL + '/Channels/' + searchstr).catch(e => console.log(e));
|
||||
let resp = await Axios
|
||||
.get(API.Base_URL + '/Channels/' + searchstr, {params: {High_Priority: highpriority}})
|
||||
.catch(e => console.log(e));
|
||||
|
||||
// Handle all our nulls.
|
||||
if (resp == null || resp.data == null)
|
||||
@ -78,12 +80,14 @@ export namespace API {
|
||||
}
|
||||
|
||||
// Search for videos based on a search string.
|
||||
export async function search_videos(searchstr : string): Promise<Array<Video>> {
|
||||
export async function search_videos(searchstr : string, highpriority?: boolean): Promise<Array<Video>> {
|
||||
// Temporary holder for videos.
|
||||
let Videos : Array<Video> = [];
|
||||
|
||||
// Ask server for videos.
|
||||
let resp = await Axios.get(API.Base_URL + '/Videos/' + searchstr).catch(e => console.log(e));
|
||||
let resp = await Axios
|
||||
.get(API.Base_URL + '/Videos/' + searchstr, {params: {High_Priority: highpriority}})
|
||||
.catch(e => console.log(e));
|
||||
|
||||
// Handle all our nulls.
|
||||
if (resp == null || resp.data == null)
|
||||
|
44
src/index.ts
44
src/index.ts
@ -4,6 +4,7 @@ import SCH from "./components/Search.vue";
|
||||
import VTBL from "./components/Video_Table.vue";
|
||||
import CTBL from "./components/Channel_Table.vue";
|
||||
import CADD from "./components/Channel_Add.vue";
|
||||
import VFAVS from "./components/Favorite_Channels.vue";
|
||||
import './index.css';
|
||||
import * as DumbYT from "./dumbyt";
|
||||
|
||||
@ -11,24 +12,51 @@ let MainApp = new Vue({
|
||||
el: "#app",
|
||||
template: `
|
||||
<div>
|
||||
<SCH v-on:search_complete="search_completed"/>
|
||||
<VGC v-bind:Videos="Videos"/>
|
||||
<VFAVS v-if="VFAVS_Show" v-bind:Videos="Favorite_Videos"/>
|
||||
<div style="padding-top: 5rem;">
|
||||
<SCH v-on:search_complete="search_completed"/>
|
||||
<VGC v-bind:Videos="Videos"/>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
components: {
|
||||
VFAVS,
|
||||
SCH,
|
||||
VGC
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Videos: Array<DumbYT.Video>()
|
||||
Favorite_Videos: Array<DumbYT.Video>(),
|
||||
Videos: Array<DumbYT.Video>(),
|
||||
VFAVS_Show: Boolean()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Callback for when the search component got results back.
|
||||
search_completed(videos : Array<DumbYT.Video>) : void {
|
||||
search_completed(searchstring: string, videos : Array<DumbYT.Video>) : void {
|
||||
this.VFAVS_Show = searchstring.length == 0;
|
||||
this.Videos = videos;
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
let v: DumbYT.Video = new DumbYT.Video(null);
|
||||
v.Title = "Test title";
|
||||
v.Description = "Test description";
|
||||
v.Thumbnail = "https://i.ytimg.com/vi/LPxqHXyZhNY/mqdefault.jpg";
|
||||
v.Channel = "Cool Channel"
|
||||
this.Favorite_Videos.push(v);
|
||||
this.Favorite_Videos.push(v);
|
||||
this.Favorite_Videos.push(v);
|
||||
this.Favorite_Videos.push(v);
|
||||
this.Favorite_Videos.push(v);
|
||||
this.Favorite_Videos.push(v);
|
||||
this.VFAVS_Show = true;
|
||||
|
||||
// Populate some Videos immediatly on start up.
|
||||
DumbYT.API.search_videos("").then(videos => {
|
||||
// Copy the videos over.
|
||||
this.Videos = videos;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -36,9 +64,13 @@ let AdminApp = new Vue({
|
||||
el: "#admin_app",
|
||||
template: `
|
||||
<div>
|
||||
<SCH v-on:search_complete="search_completed"/>
|
||||
<CADD/>
|
||||
<CTBL v-bind:Channels="Channels"/>
|
||||
<SCH v-on:search_complete="search_completed"/>
|
||||
|
||||
<h3 style="padding-top: 3 rem; padding-left: 3rem;">Channels</h3>
|
||||
<CTBL ID="Channels_All" v-bind:Channels="Channels"/>
|
||||
|
||||
<h3 style="padding-top: 3 rem; padding-left: 3rem;">Videos</h3>
|
||||
<VTBL v-bind:Videos="Videos"/>
|
||||
</div>
|
||||
`,
|
||||
|
@ -1,7 +1,11 @@
|
||||
var path = require('path')
|
||||
var webpack = require('webpack')
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
new VueLoaderPlugin()
|
||||
],
|
||||
entry: './src/index.ts',
|
||||
output: {
|
||||
path: path.resolve(__dirname, './dist'),
|
||||
@ -55,8 +59,21 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
devServer: {
|
||||
overlay: {
|
||||
warnings: true,
|
||||
errors: true
|
||||
},
|
||||
historyApiFallback: true,
|
||||
noInfo: true
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:62214",
|
||||
changeOrigin: true
|
||||
},
|
||||
"/hangfire": {
|
||||
target: "http://localhost:62214",
|
||||
changeOrigin: true
|
||||
},
|
||||
}
|
||||
},
|
||||
performance: {
|
||||
hints: false
|
||||
|
Reference in New Issue
Block a user