HUGE arch changes to make searching work right

This commit is contained in:
2018-03-05 01:08:55 -05:00
parent 96a26ce30d
commit 9f5fb78835
32 changed files with 800 additions and 447 deletions

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1"/>
<title>YT Manager Admin</title>
<!-- Compressed CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/6.4.3/css/foundation.min.css" />
</head>
<body>
<h2>Admin Page</h2>
<div id="admin_app"></div>
<script src="./../dist/build.js"></script>
</body>
</html>

View File

@ -9320,6 +9320,12 @@
"whet.extend": "0.9.9"
}
},
"sweetalert2": {
"version": "7.13.3",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-7.13.3.tgz",
"integrity": "sha512-cClC9uxXg2oTygUbRhVG0ELVouDbKxzZmjM/pjmPsxbdvDWPmijOq350kvcOIClKezqaG7GsyUSxUbZQWQGZDg==",
"dev": true
},
"symbol-observable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",

View File

@ -11,6 +11,7 @@
"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",

View File

@ -0,0 +1,126 @@
<template>
<div>
<div class="grid-x">
<div class="medium-11 cell"></div>
<div class="medium-1 cell">
<button type="button" class="button input-group" v-on:click='Expanded = !Expanded'>
<i class="fa fa-plus" aria-hidden="true"></i> Add
</button>
</div>
</div>
<transition name="fade">
<form v-cloak v-if="Expanded">
<h2>Add new Channel</h2>
<div class="grid-x">
<div class="input-group medium-6 cell">
<span class="input-group-label">Title</span>
<input class="input-group-field" type="text" placeholder="Title" v-model="Title">
</div>
<div class="input-group medium-1 cell"></div>
<div class="input-group medium-5 cell">
<span class="input-group-label">Channel</span>
<input class="input-group-field" v-bind:class="{ error: !Valid }" type="text"
placeholder="URL" v-model="Channel_Identification_Box">
</div>
</div>
<div class="input-group">
<span class="input-group-label">Description</span>
<input class="input-group-field" type="text" placeholder="Description" v-model="Description">
</div>
<div class="grid-x">
<img class="small-4 cell" :src="Thumbnail" />
<div class="small-7"></div>
<button type="button" style="max-height:50px" class="button input-group small-1 cell" @click="Submit">Submit</button>
</div>
</form>
</transition>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import * as Dyt from "../dumbyt";
import Axios from "axios";
import SA2 from "sweetalert2";
// Vue class for keeping state of the videos.
export default Vue.extend({
data() {
return {
Title: "",
Description: "",
ID: "",
Thumbnail: "",
Valid: false,
Expanded: false,
Channel_Identification_Box : ""
}
},
watch: {
Channel_Identification_Box(newcontents: string) {
this.GetChannelFromYT(newcontents);
}
},
methods: {
Submit() : void {
Dyt.channel_modify(this.ID, Dyt.Modification.Add).then((resp) => {
if (resp == null)
SA2(
"Channel Add Success!",
"The channel has been added succesfully, video contents should be updated shortly.",
"success"
);
else
SA2(
"Channel Add Fail!",
"The channel has not been added due to the following: \n" + resp,
"error"
);
});
// Hide the bars and erase internal state.
this.Expanded = false;
this.Title = "";
this.Description = "";
this.ID = "";
this.Thumbnail = "";
this.Valid = false;
this.Channel_Identification_Box = "";
},
GetChannelFromYT(Channel: string) : void {
// Say it failed first so if we exit early then correctly marked fail.
this.Valid = false;
// Copy over to internal ID box.
this.ID = Channel;
// 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)
return;
// Get channel metadata.
const API = 'https://www.googleapis.com/youtube/v3/channels?';
const API_Parts = 'part=snippet%2CcontentDetails%2Cstatistics';
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) => {
this.Description = resp.data.items[0].snippet.description;
this.Title = resp.data.items[0].snippet.title;
this.Thumbnail = resp.data.items[0].snippet.thumbnails.medium.url;
})
.catch(function (error) {
console.log(error);
});
}
}
});
</script>

View File

@ -0,0 +1,44 @@
<template>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Description</th>
<th>User Tags</th>
</tr>
</thead>
<tbody>
<tr v-cloak v-for="channel in Channels" :key=channel.ID class="Grid_Small_Text">
<td>{{channel.ID}}</td>
<td>{{channel.Title}}</td>
<td>{{channel.Description}}</td>
<td>{{channel.User_Tags}}</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts">
import Vue from "vue";
import * as Dyt from "../dumbyt";
// Vue class for keeping state of the videos.
export default Vue.extend({
data() {
return {
Channels: Array<Dyt.Channel>()
}
},
mounted(){
Dyt.search_channels("").then(v => this.Channels = v);
},
});
</script>
<style>
.Grid_Small_Text {
font-size: 0.7em;
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div>
<input id="searchbox0" class="searchbox" type="text" v-model="searchbox_str"
placeholder="Search String goes here, for example: >5m,c++"
/>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import * as _ from "lodash";
import * as Dyt from "../dumbyt";
// Vue class for keeping state of the videos.
export default Vue.extend({
data() {
return {
searchbox_str: "",
Videos: Array<Dyt.Video>()
}
},
// Manually attaching functions to watchers of data.
watch: {
// Searchbox updater.
searchbox_str (s: string) {
var v = _.debounce((s) => {
Dyt.search_videos(s).then(e => this.Videos = e);
}, 400)
v(s);
}
}
});
</script>
<style scoped>
.searchbox {
font-size: 1.2em;
max-width: 70em;
margin-left: auto;
margin-right: auto;
margin-top: 2em;
}
</style>

View File

@ -17,114 +17,15 @@
<script lang="ts">
import Vue from "vue";
import Axios from "axios";
import * as _ from "lodash";
// Wrapper for videos returned from our server.
class Video {
// Title of the video according to youtube.
public Title: string;
// Description of video according to youtube.
public Description: string;
// Youtube ID
public ID: string;
// Thumbnail
public Thumbnail: string;
// What channel made this video.
public Channel: string;
// Duration of the video in seconds.
public Seconds: number;
// Tags relevant to the video.
public Tags: Array<string>;
// Youtube URL of the video.
public URL: string;
// Popuplate this using data from our server.
constructor(v: any){
this.Title = v.title;
this.Description = v.description;
this.ID = v.id;
this.Thumbnail = v.thumbnail;
this.Channel = v.channel;
this.Seconds = v.seconds;
this.Tags = v.tags;
this.URL = "https://www.youtube.com/watch?v=" + v.id;
}
}
async function search_videos(searchstr : string): Promise<Array<Video>> {
// How many chars at most for the description.
const maxcharsdescriptionfield = 100;
// Temporary holder for videos.
let Videos : Array<Video> = [];
// Ask server for videos.
let resp = await Axios.get('http://localhost:62214/api/Videos/search/' + searchstr).catch(e => console.log(e));
// Handle all our nulls.
if (resp == null || resp.data == null)
console.log("server /api/videos/search return is null");
else {
// Parse our videos.
resp.data.forEach((v: any) => {
// Trim description if needed.
if (v.description.length > maxcharsdescriptionfield) {
v.description = v.description.substring(0, maxcharsdescriptionfield) + " ...";
}
// Add it to our array
Videos.push(new Video(v));
});
}
// Send back the resulting videos.
return Promise.resolve(Videos);
}
async function get_videos(): Promise<Array<Video>> {
// How many chars at most for the description.
const maxcharsdescriptionfield = 100;
// Temporary holder for videos.
let Videos : Array<Video> = [];
// Ask server for videos.
let resp = await Axios.get('http://localhost:62214/api/Videos').catch(e => console.log(e));
// Handle all our nulls.
if (resp == null || resp.data == null)
console.log("server /api/videos return is null");
else {
// Parse our videos.
resp.data.forEach((v: any) => {
// Trim description if needed.
if (v.description.length > maxcharsdescriptionfield) {
v.description = v.description.substring(0, maxcharsdescriptionfield) + " ...";
}
// Add it to our array
Videos.push(new Video(v));
});
}
// Send back the resulting videos.
return Promise.resolve(Videos);
}
import * as Dyt from "../dumbyt";
// Vue class for keeping state of the videos.
export default Vue.extend({
data() {
return {
searchbox_str: "",
Videos: Array<Video>()
Videos: Array<Dyt.Video>()
}
},
@ -132,19 +33,15 @@ export default Vue.extend({
watch: {
// Searchbox updater.
searchbox_str (s: string) {
if (s.trim().length > 0){
var v = _.debounce((s) => {
search_videos(s).then(e => this.Videos = e);
})
v(s);
} else {
get_videos().then(e => this.Videos = e );
}
var v = _.debounce((s) => {
Dyt.search_videos(s).then(e => this.Videos = e);
})
v(s);
}
},
// Ugh, vue doesn't have async support for computed, wow ...
mounted() { get_videos().then(e => this.Videos = e ); }
mounted() { Dyt.search_videos("").then(e => this.Videos = e ); }
});
</script>

View File

@ -0,0 +1,47 @@
<template>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Channel</th>
<th>Seconds</th>
<th>Tags</th>
</tr>
</thead>
<tbody>
<tr v-cloak v-for="video in Videos" :key=video.ID class="Grid_Small_Text">
<td>{{video.ID}}</td>
<td>{{video.Title}}</td>
<td>{{video.Channel}}</td>
<td>{{video.Seconds}}</td>
<td>{{video.Tags}}</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts">
import Vue from "vue";
import * as Dyt from "../dumbyt";
// Vue class for keeping state of the videos.
export default Vue.extend({
data() {
return {
Videos: Array<Dyt.Video>()
}
},
mounted (){
Dyt.search_videos("").then(v => this.Videos = v);
},
});
</script>
<style>
.Grid_Small_Text {
font-size: 0.7em;
}
</style>

View File

@ -0,0 +1,210 @@
import Axios from "axios";
// Base URL for API queries.
const API_BASE_URL = "http://localhost:62214/api";
// How many chars at most for the description.
const max_description_length = 100;
// Wrapper for channels returned from our server.
export class Channel {
// Title of the channel according to youtube.
public Title: string;
// Description of channel according to youtube.
public Description: string;
// Youtube ID
public ID: string;
// Tags given to the video.
public User_Tags: Array<string>;
// ID's belonging to the channel.
public Video_IDs: Array<string>;
// Popuplate this using data from our server.
constructor(c : any){
if (c == null) {
this.Title = "NULL"
this.Description = "NULL"
this.ID = "NULL"
this.User_Tags = ["NULL", "NULL", "NULL"]
this.Video_IDs = ["NULL", "NULL"]
} else {
this.Title = c.title;
this.Description = c.description;
this.ID = c.id;
this.User_Tags = c.user_Tags;
this.Video_IDs = c.video_IDs;
}
}
}
// Wrapper for videos returned from our server.
export class Video {
// Title of the video according to youtube.
public Title: string;
// Description of video according to youtube.
public Description: string;
// Youtube ID
public ID: string;
// Thumbnail
public Thumbnail: string;
// What channel made this video.
public Channel: string;
// Duration of the video in seconds.
public Seconds: number;
// Tags relevant to the video.
public Tags: Array<string>;
// Youtube URL of the video.
public URL: string;
// Popuplate this using data from our server.
constructor(v: any){
if (v == null){
this.Title = "NULL";
this.Description = "NULL";
this.ID = "NULL";
this.Thumbnail = "NULL";
this.Channel = "NULL";
this.Seconds = 9999999;
this.Tags = ["NULL", "NULL"];
this.URL = "NULL";
} else {
this.Title = v.title;
this.Description = v.description;
this.ID = v.id;
this.Thumbnail = v.thumbnail;
this.Channel = v.channel;
this.Seconds = v.seconds;
this.Tags = v.tags;
this.URL = "https://www.youtube.com/watch?v=" + v.id;
}
}
}
// Types of modifications which can be applied to various models.
export enum Modification {
Add, Delete, Refresh
}
// Change a channel state.
export async function channel_modify(youtubeID : string, modify : Modification): Promise<void | string> {
switch (modify){
case Modification.Add: {
let URL = API_BASE_URL + '/Channels/' + youtubeID;
let resp = await Axios.post(URL).catch((error) => {
if (error.response)
return error.response.data;
else if (error.request)
return error.request;
else
return "Axios request failed for unkown reason.";
});
if (typeof resp == "string")
return resp;
break;
}
case Modification.Delete: {
let URL = API_BASE_URL + '/Channels/' + youtubeID;
let resp = await Axios.delete(URL).catch(e => console.log(e));
if (resp != null){
if (resp.status != 200)
return resp.data();
else
return;
} else {
return "Response is null";
}
}
case Modification.Refresh: {
let URL = API_BASE_URL + '/Channels/Update/' + youtubeID;
let resp = await Axios.post(URL).catch(e => console.log(e));
if (resp != null){
if (resp.status != 200)
return resp.data();
else
return;
} else {
return "Response is null";
}
}
default: {
console.log("Unknown request type, error ...");
break;
}
}
}
// Delete a video.
export async function video_delete(youtubeID : string) : Promise<void> {
let URL = API_BASE_URL + '/Videos/' + youtubeID;
let resp = await Axios.delete(URL).catch(e => console.log(e));
if ((resp == null) || (resp.data == null)){
console.log("Video delete via " + URL + " FAIL");
};
}
// Search for channels based on a search string.
export async function search_channels(searchstr : string): 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));
// Handle all our nulls.
if (resp == null || resp.data == null)
console.log("server /api/Channels/" + searchstr + " return is null");
else {
// Parse our videos.
resp.data.forEach((c: any) => {
// Trim description if needed.
if (c.description.length > max_description_length) {
c.description = c.description.substring(0, max_description_length) + " ...";
}
// Add it to our array
Channels.push(new Channel(c));
});
}
// Send back the resulting videos.
return Promise.resolve(Channels);
}
// Search for videos based on a search string.
export async function search_videos(searchstr : string): 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));
// Handle all our nulls.
if (resp == null || resp.data == null)
console.log("server /videos/ return is null");
else {
// Parse our videos.
resp.data.forEach((v: any) => {
// Trim description if needed.
if (v.description.length > max_description_length) {
v.description = v.description.substring(0, max_description_length) + " ...";
}
// Add it to our array
Videos.push(new Video(v));
});
}
// Send back the resulting videos.
return Promise.resolve(Videos);
}

View File

@ -1,16 +1,40 @@
import Vue from "vue";
import VGC from "./components/Video_Grid.vue";
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 './index.css';
let v = new Vue({
let MainApp = new Vue({
el: "#app",
template: `
<div>
<VGC/>
</div>
`,
data: { name: "something" },
components: {
VGC
}
});
let AdminApp = new Vue({
el: "#admin_app",
template: `
<div>
<SCH style="max-width:800px; padding-left:100px;" />
<CADD/>
<CTBL/>
<VTBL/>
</div>
`,
components: {
SCH,
CADD,
CTBL,
VTBL
},
methods: {
}
});