mirror of https://github.com/FreeTubeApp/FreeTube
Initial Push of Project
This commit is contained in:
parent
9eac7a0a3f
commit
d222f3edb4
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"env": {
|
||||
"development": {
|
||||
"application/javascript": {
|
||||
"presets": [
|
||||
["env", { "targets": { "electron": "1.4" } }],
|
||||
"react"
|
||||
],
|
||||
"plugins": ["transform-async-to-generator"],
|
||||
"sourceMaps": "inline"
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"application/javascript": {
|
||||
"presets": [
|
||||
["env", { "targets": { "electron": "1.4" } }],
|
||||
"react"
|
||||
],
|
||||
"plugins": ["transform-async-to-generator"],
|
||||
"sourceMaps": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
node_modules
|
||||
out
|
||||
\.idea/
|
||||
subscriptions\.db
|
||||
.vscode/
|
||||
.eslintrc*
|
||||
*.db
|
||||
|
||||
electron-packager/win32-x64/FreeTube-win32-x64/
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"name": "FreeTube",
|
||||
"productName": "FreeTube",
|
||||
"version": "0.1.0",
|
||||
"description": "An Open Source YouTube app for privacy.",
|
||||
"main": "src/js/init.js",
|
||||
"scripts": {
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"publish": "electron-forge publish",
|
||||
"make:darwin": "electron-forge make --platform=darwin",
|
||||
"make:linux": "electron-forge make --platform=linux",
|
||||
"make:win32": "electron-forge make --platform=win32"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": {
|
||||
"name": "PrestonN",
|
||||
"email": "FreeTubeApp@protonmail.com",
|
||||
"url": "https://github.com/FreeTubeApp/FreeTube"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"config": {
|
||||
"forge": {
|
||||
"make_targets": {
|
||||
"win32": [
|
||||
"squirrel"
|
||||
],
|
||||
"darwin": [
|
||||
"zip"
|
||||
],
|
||||
"linux": [
|
||||
"deb",
|
||||
"rpm"
|
||||
]
|
||||
},
|
||||
"electronPackagerConfig": {
|
||||
"packageManager": "yarn",
|
||||
"icon": "./src/icon.icns"
|
||||
},
|
||||
"electronWinstallerConfig": {
|
||||
"name": "freetube",
|
||||
"iconUrl": "https://raw.githubusercontent.com/FreeTubeApp/FreeTubeApp.github.io/master/images/icon.ico",
|
||||
"setupIcon": "./src/icon.ico"
|
||||
},
|
||||
"electronInstallerDebian": {
|
||||
"icon": "src/icon.svg"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/FreeTubeApp/FreeTube"
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron-forge": "^5.1.1",
|
||||
"electron-prebuilt-compile": "1.8.2",
|
||||
"electron-winstaller": "^2.6.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"autolinker": "^1.6.2",
|
||||
"dateformat": "^3.0.3",
|
||||
"electron-compile": "^6.4.2",
|
||||
"jquery": "^3.3.1",
|
||||
"mustache": "^2.3.0",
|
||||
"nedb": "^1.8.0",
|
||||
"node-async-loop": "^1.2.2"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
After Width: | Height: | Size: 361 KiB |
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="222mm"
|
||||
height="222mm"
|
||||
viewBox="0 0 222 222"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
|
||||
sodipodi:docname="icon.svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-100"
|
||||
inkscape:cy="445.71429"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:pagecheckerboard="false"
|
||||
inkscape:window-width="1720"
|
||||
inkscape:window-height="1336"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-75)">
|
||||
<circle
|
||||
id="path3711"
|
||||
cx="110.74702"
|
||||
cy="186.25298"
|
||||
r="102.43154"
|
||||
style="fill:#eeeeee;fill-opacity:1;stroke:#f44336;stroke-width:15.02900028;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;stroke-linecap:round;stroke-linejoin:bevel" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#f44336;fill-opacity:1;stroke-width:0.26458332"
|
||||
id="path4520"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="108.25239"
|
||||
sodipodi:cy="186.70654"
|
||||
sodipodi:r1="65.785233"
|
||||
sodipodi:r2="32.892616"
|
||||
sodipodi:arg1="0"
|
||||
sodipodi:arg2="1.0471976"
|
||||
inkscape:flatsided="false"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 174.03762,186.70654 -49.33893,28.48585 -49.338918,28.48584 0,-56.97169 0,-56.97168 49.338928,28.48584 z"
|
||||
inkscape:transform-center-x="-16.446307"
|
||||
inkscape:transform-center-y="-1.0179227e-06" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="style/main.css">
|
||||
<link rel="stylesheet" href="style/lightTheme.css">
|
||||
<link rel="stylesheet" href="style/player.css">
|
||||
<link rel="stylesheet" href="style/videoList.css">
|
||||
<link rel="stylesheet" href="style/channel.css">
|
||||
<link rel="stylesheet" href="style/loading.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.0.2/js/all.js"></script>
|
||||
<script src="https://apis.google.com/js/api.js"></script>
|
||||
<script src="https://www.youtube.com/iframe_api"></script>
|
||||
<script src="js/settings.js"></script>
|
||||
<script src="js/layout.js"></script>
|
||||
<script src="js/videos.js"></script>
|
||||
<script src="js/subscriptions.js"></script>
|
||||
<script src="js/channels.js"></script>
|
||||
<script src="js/savedVideos.js"></script>
|
||||
<script src="js/history.js"></script>
|
||||
<script src="js/events.js"></script>
|
||||
<title>Freetube Player</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id='loading'>
|
||||
<div class="spinner">
|
||||
<div class="double-bounce1"></div>
|
||||
<div class="double-bounce2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='confirmFunction'>
|
||||
<span id='confirmMessage'>Would you like to perform the function?</span>
|
||||
<div class='confirmButton' id='confirmYes'>Yes</div>
|
||||
<div class='confirmButton' id='confirmNo'>No</div>
|
||||
</div>
|
||||
<div id='toast'>
|
||||
<span id='toastMessage'></span>
|
||||
<i onclick='hideToast()' class="closeToast fas fa-times"></i>
|
||||
</div>
|
||||
<div class="topNav">
|
||||
<span class="logo"><i onclick='toggleSideNavigation()' class="menuButton fas fa-bars"></i> FreeTube <i class="far fa-play-circle"></i></span>
|
||||
<div class="searchBar">
|
||||
<input id='jumpToInput' class='jumpToInput' type='text' placeholder="Jump to Video Link / Id" /><i onclick='parseVideoLink()' class="fas fa-search searchButton" style='margin-right: 50px; cursor: pointer;'></i>
|
||||
<input id='search' class="search" type="text" placeholder="Search">
|
||||
<i onclick='search()' class="fas fa-search searchButton" style='margin-right: -10px; cursor: pointer'></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id='sideNavDisabled'></div>
|
||||
<div id="sideNav">
|
||||
<div class="sideNavContainer">
|
||||
<ul>
|
||||
<li onclick='loadSubscriptions()'><i class="fas fa-users"></i> Subscriptions</li>
|
||||
<li onclick='showMostPopular()'><i class="fas fa-fire"></i> Most Popular</li>
|
||||
<li onclick='showSavedVideos()'><i class="fas fa-star"></i> Saved</li>
|
||||
<li onclick='showHistory()'><i class="fas fa-history"></i> History</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<ul>
|
||||
<li onclick='showSettings()'><i class="fas fa-cog"></i> Settings</li>
|
||||
<li onclick='showAbout()'><i class="far fa-question-circle"></i> About</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<ul id='subscriptions'>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main">
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
This file is part of FreeTube.
|
||||
|
||||
FreeTube is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FreeTube is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* File for all functions related specifically for channels.
|
||||
*/
|
||||
|
||||
/*function getChannelThumbnail(channelId, callback) {
|
||||
let url = '';
|
||||
|
||||
let request = gapi.client.youtube.channels.list({
|
||||
'id': channelId,
|
||||
'part': 'snippet',
|
||||
});
|
||||
|
||||
request.execute((response) => {
|
||||
url = response['items'][0]['snippet']['thumbnails']['high']['url'];
|
||||
callback(url);
|
||||
});
|
||||
}*/
|
||||
|
||||
/**
|
||||
* View a channel page, displaying recent uplaods.
|
||||
*
|
||||
* @param {string} channelId - The channel ID to go to.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function goToChannel(channelId) {
|
||||
event.stopPropagation();
|
||||
clearMainContainer();
|
||||
toggleLoading();
|
||||
|
||||
|
||||
// Check if the user is subscribed to the channel. Display different text based on the information
|
||||
let subscribeText = '';
|
||||
const checkSubscription = isSubscribed(channelId);
|
||||
|
||||
checkSubscription.then((results) => {
|
||||
if(results === false){
|
||||
subscribeText = 'SUBSCRIBE';
|
||||
}
|
||||
else{
|
||||
subscribeText = 'UNSUBSCRIBE';
|
||||
}
|
||||
});
|
||||
|
||||
// Call YouTube API to grab channel information
|
||||
let request = gapi.client.youtube.channels.list({
|
||||
part: 'snippet, brandingSettings, statistics',
|
||||
id: channelId,
|
||||
});
|
||||
|
||||
// Perform API Execution
|
||||
request.execute((response) => {
|
||||
// Set variables of extracted information
|
||||
const brandingSettings = response['items'][0]['brandingSettings'];
|
||||
const statistics = response['items'][0]['statistics'];
|
||||
const snippet = response['items'][0]['snippet'];
|
||||
const channelName = brandingSettings['channel']['title'];
|
||||
const channelBanner = brandingSettings['image']['bannerImageUrl'];
|
||||
const channelImage = snippet['thumbnails']['high']['url'];
|
||||
|
||||
// Channels normally have links in their channel description. This makes them clickable.
|
||||
const channelDescription = autolinker.link(brandingSettings['channel']['description']);
|
||||
|
||||
// Add commas to sub count to make them more readable.
|
||||
let subCount = statistics['subscriberCount'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
|
||||
// Grab the channelView.html template and fill it in with the above variables.
|
||||
$.get('templates/channelView.html', (template) => {
|
||||
mustache.parse(template);
|
||||
const rendered = mustache.render(template, {
|
||||
channelName: channelName,
|
||||
channelImage: channelImage,
|
||||
channelBanner: channelBanner,
|
||||
channelId: channelId,
|
||||
subCount: subCount,
|
||||
channelDescription: channelDescription,
|
||||
isSubscribed: subscribeText,
|
||||
});
|
||||
// Render the template on to #main
|
||||
$('#main').html(rendered);
|
||||
toggleLoading();
|
||||
});
|
||||
|
||||
// Grab the channel's latest upload. API forces a max of 50.
|
||||
let request = gapi.client.youtube.search.list({
|
||||
part: 'snippet',
|
||||
channelId: channelId,
|
||||
type: 'video',
|
||||
maxResults: 50,
|
||||
order: 'date',
|
||||
});
|
||||
|
||||
// Execute API request
|
||||
request.execute((response) => {
|
||||
// Display recent uploads to #main
|
||||
response['items'].forEach((video) => {
|
||||
displayVideos(video);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
This file is part of FreeTube.
|
||||
|
||||
FreeTube is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FreeTube is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* File for events within application. Work needs to be done throughout the application
|
||||
* to use this style more. Please use this style going forward if possible.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Event when user clicks comment box,
|
||||
* and wants to show/display comments for the user.
|
||||
*/
|
||||
let showComments = function(event) {
|
||||
let comments = $('#comments');
|
||||
|
||||
if (comments.css('display') === 'none') {
|
||||
comments.attr('loaded', 'true');
|
||||
|
||||
let commentsTemplate = $.get('templates/comments.html');
|
||||
|
||||
commentsTemplate.done((template) => {
|
||||
let request = gapi.client.youtube.commentThreads.list({
|
||||
'videoId': $('#comments').attr('data-video-id'),
|
||||
'part': 'snippet,replies',
|
||||
'maxResults': 100,
|
||||
});
|
||||
|
||||
request.execute((data) => {
|
||||
let comments = [];
|
||||
let items = data.items;
|
||||
|
||||
items.forEach((object) => {
|
||||
let snippet = object['snippet']['topLevelComment']['snippet'];
|
||||
let dateString = new Date(snippet.publishedAt);
|
||||
let publishedDate = dateFormat(dateString, "mmm dS, yyyy");
|
||||
|
||||
snippet.publishedAt = publishedDate;
|
||||
|
||||
comments.push(snippet);
|
||||
})
|
||||
const html = mustache.render(template, {
|
||||
comments: comments,
|
||||
});
|
||||
$('#comments').html(html);
|
||||
});
|
||||
});
|
||||
|
||||
comments.show();
|
||||
} else {
|
||||
comments.hide();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Play / Pause the video player upon click.
|
||||
*/
|
||||
let playPauseVideo = function(event) {
|
||||
let el = event.currentTarget;
|
||||
el.paused ? el.play() : el.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* ---------------------------
|
||||
* Bind click events
|
||||
* --------------------------
|
||||
*/
|
||||
$(document).on('click', '#showComments', showComments);
|
||||
|
||||
$(document).on('click', '.videoPlayer', playPauseVideo);
|
||||
|
||||
$(document).on('click', '#confirmNo', hideConfirmFunction);
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
This file is part of FreeTube.
|
||||
|
||||
FreeTube is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FreeTube is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* File used for functions related to video history.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add a video to the history database file
|
||||
*
|
||||
* @param {string} videoId - The video ID of the video to be saved.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function addToHistory(videoId){
|
||||
const data = {
|
||||
videoId: videoId,
|
||||
timeWatched: new Date().getTime(),
|
||||
};
|
||||
historyDb.insert(data, (err, newDoc) => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a video from the history database file
|
||||
*
|
||||
* @param {string} videoId - The video ID of the video to be removed.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function removeFromHistory(videoId){
|
||||
const data = {videoId: videoId};
|
||||
historyDb.remove(data, {}, (err, numRemoved) => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the videos within the history database.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function showHistory(){
|
||||
clearMainContainer();
|
||||
toggleLoading();
|
||||
console.log('checking history');
|
||||
|
||||
let videoList = '';
|
||||
|
||||
historyDb.find({}).sort({
|
||||
timeWatched: -1
|
||||
}).exec((err, docs) => {
|
||||
if(docs.length > 49){
|
||||
// The YouTube API limits the search to 50 videos, so grab 50 most recent.
|
||||
for (let i = 0; i < 49; i++) {
|
||||
videoList = videoList + ',' + docs[i]['videoId'];
|
||||
}
|
||||
}
|
||||
else{
|
||||
docs.forEach((video) => {
|
||||
videoList = videoList + ',' + video['videoId'];
|
||||
});
|
||||
}
|
||||
|
||||
let request = gapi.client.youtube.videos.list({
|
||||
part: 'snippet',
|
||||
id: videoList,
|
||||
maxResults: 50,
|
||||
});
|
||||
|
||||
request.execute((response) => {
|
||||
createVideoListContainer('Watch History:');
|
||||
response['items'].forEach((video) => {
|
||||
displayVideos(video, 'history');
|
||||
});
|
||||
toggleLoading();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
This file is part of FreeTube.
|
||||
|
||||
FreeTube is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FreeTube is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* File used to initializing the application
|
||||
*/
|
||||
const {app, BrowserWindow} = require('electron');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
let win;
|
||||
|
||||
/**
|
||||
* initialize the electron application
|
||||
* 1. create the browser window
|
||||
* 2. load the index.html
|
||||
*/
|
||||
let init = function() {
|
||||
const Menu = require('electron').Menu;
|
||||
win = new BrowserWindow({width: 1200, height: 800});
|
||||
|
||||
win.loadURL(url.format({
|
||||
pathname: path.join(__dirname, '../index.html'),
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
}));
|
||||
|
||||
if (process.env = 'development') {
|
||||
//win.webContents.openDevTools();
|
||||
}
|
||||
|
||||
win.on('closed', () => {
|
||||
win = null;
|
||||
});
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{role: 'undo'},
|
||||
{role: 'redo'},
|
||||
{type: 'separator'},
|
||||
{role: 'cut'},
|
||||
{role: 'copy'},
|
||||
{role: 'paste'},
|
||||
{role: 'pasteandmatchstyle'},
|
||||
{role: 'delete'},
|
||||
{role: 'selectall'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{role: 'reload'},
|
||||
{role: 'forcereload'},
|
||||
{role: 'toggledevtools'},
|
||||
{type: 'separator'},
|
||||
{role: 'resetzoom'},
|
||||
{role: 'zoomin'},
|
||||
{role: 'zoomout'},
|
||||
{type: 'separator'},
|
||||
{role: 'togglefullscreen'}
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{role: 'minimize'},
|
||||
{role: 'close'}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
};
|
||||
|
||||
/**
|
||||
* quit the application
|
||||
*/
|
||||
let allWindowsClosed = function() {
|
||||
app.quit();
|
||||
};
|
||||
|
||||
/**
|
||||
* on mac, when dock icon is clicked,
|
||||
* create a new window and launch the editor
|
||||
*/
|
||||
let active = function() {
|
||||
if (win === null) {
|
||||
init();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* bind events, ready (initialize),
|
||||
* window-all-closed (what happens when the windows closed),
|
||||
* activate
|
||||
*/
|
||||
app.on('ready', init);
|
||||
app.on('window-all-closed', allWindowsClosed);
|
||||
app.on('activate', active);
|
|
@ -0,0 +1,294 @@
|
|||
/*
|
||||
This file is part of FreeTube.
|
||||
|
||||
FreeTube is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FreeTube is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* File for main layout manipulation and general variable configuration.
|
||||
* There are some functions from other files that will probably need to be moved to here.
|
||||
*/
|
||||
|
||||
// Add general variables. Please put all require statements here.
|
||||
const Datastore = require('nedb'); // Database logic
|
||||
window.$ = window.jQuery = require('jquery');
|
||||
const mustache = require('mustache'); // Templating
|
||||
const dateFormat = require('dateformat'); // Formating Dates
|
||||
|
||||
// Used for finding links within text and making them clickable. Used mostly for video descriptions.
|
||||
const autolinker = require('autolinker');
|
||||
const electron = require('electron');
|
||||
|
||||
// Used for getting the user's subscriptions. Can probably remove this when that function
|
||||
// is rewritten.
|
||||
const asyncLoop = require('node-async-loop');
|
||||
const shell = electron.shell; // Used to open external links into the user's native browser.
|
||||
const localDataStorage = electron.remote.app.getPath('userData'); // Grabs the userdata directory based on the user's OS
|
||||
const clipboard = electron.clipboard;
|
||||
const fs = require('fs'); // Used to read files. Specifically in the settings page.
|
||||
let currentTheme = '';
|
||||
let apiKey;
|
||||
let dialog = require('electron').remote.dialog; // Used for opening file browser to export / import subscriptions.
|
||||
let toastTimeout; // Timeout for toast notifications.
|
||||
|
||||
// Subscriptions database file
|
||||
const subDb = new Datastore({
|
||||
filename: localDataStorage + '/subscriptions.db',
|
||||
autoload: true
|
||||
});
|
||||
|
||||
// History database file
|
||||
const historyDb = new Datastore({
|
||||
filename: localDataStorage + '/videohistory.db',
|
||||
autoload: true
|
||||
});
|
||||
|
||||
// Saved videos database file
|
||||
const savedVidsDb = new Datastore({
|
||||
filename: localDataStorage + '/savedvideos.db',
|
||||
autoload: true
|
||||
});
|
||||
|
||||
// Settings database file.
|
||||
const settingsDb = new Datastore({
|
||||
filename: localDataStorage + 'settings.db',
|
||||
autoload: true
|
||||
});
|
||||
|
||||
// Grabs the default settings from the settings database file. Makes defaults if
|
||||
// none are found.
|
||||
checkDefaultSettings();
|
||||
|
||||
// Ppen links externally by default
|
||||
$(document).on('click', 'a[href^="http"]', (event) =>{
|
||||
let el = event.currentTarget;
|
||||
event.preventDefault();
|
||||
shell.openExternal(el.href);
|
||||
});
|
||||
|
||||
// Open links externally on middle click.
|
||||
$(document).on('auxclick', 'a[href^="http"]', (event) =>{
|
||||
let el = event.currentTarget;
|
||||
event.preventDefault();
|
||||
shell.openExternal(el.href);
|
||||
});
|
||||
|
||||
|
||||
$(document).ready(() => {
|
||||
const searchBar = document.getElementById('search');
|
||||
const jumpToInput = document.getElementById('jumpToInput');
|
||||
|
||||
// Displays the list of subscriptions in the side bar.
|
||||
displaySubs();
|
||||
|
||||
// Allow user to use the 'enter' key to search for a video.
|
||||
searchBar.onkeypress = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
search();
|
||||
}
|
||||
};
|
||||
|
||||
// Allow user to use the 'enter' key to open a video link.
|
||||
jumpToInput.onkeypress = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
parseVideoLink();
|
||||
}
|
||||
};
|
||||
|
||||
// Display subscriptions upon the app opening up. May allow user to specify.
|
||||
// Home page in the future.
|
||||
loadSubscriptions();
|
||||
});
|
||||
|
||||
/**
|
||||
* Start the YouTube API.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function start() {
|
||||
// Initializes the client with the API key and the Translate API.
|
||||
gapi.client.init({
|
||||
'apiKey': apiKey,
|
||||
})
|
||||
|
||||
gapi.client.load('youtube', 'v3', () => {
|
||||
let isLoad = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Toggle the ability to view the side navigation bar.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function toggleSideNavigation() {
|
||||
const sideNav = document.getElementById('sideNav');
|
||||
const mainContainer = document.getElementById('main');
|
||||
|
||||
if (sideNav.style.display === 'none') {
|
||||
sideNav.style.display = 'inline';
|
||||
mainContainer.style.marginLeft = '250px';
|
||||
} else {
|
||||
sideNav.style.display = 'none';
|
||||
mainContainer.style.marginLeft = '0px';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears out the #main container to allow other information to be shown.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function clearMainContainer() {
|
||||
const container = document.getElementById('main');
|
||||
container.innerHTML = '';
|
||||
hideConfirmFunction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the loading animation before / after a function runs. Also disables / enables input
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function toggleLoading() {
|
||||
const loading = document.getElementById('loading');
|
||||
const sideNavDisabled = document.getElementById('sideNavDisabled');
|
||||
const searchBar = document.getElementById('search');
|
||||
const goToVideoInput = document.getElementById('jumpToInput');
|
||||
|
||||
if (loading.style.display === 'none' || loading.style.display === '') {
|
||||
loading.style.display = 'inherit';
|
||||
sideNavDisabled.style.display = 'inherit';
|
||||
searchBar.disabled = true;
|
||||
goToVideoInput.disabled = true;
|
||||
} else {
|
||||
loading.style.display = 'none';
|
||||
sideNavDisabled.style.display = 'none';
|
||||
searchBar.disabled = false;
|
||||
goToVideoInput.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a div container in #main meant to be a container for video lists.
|
||||
*
|
||||
* @param {string} headerLabel - The header of the container. Not used for showing video recommendations.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function createVideoListContainer(headerLabel = '') {
|
||||
const videoListContainer = document.createElement("div");
|
||||
videoListContainer.id = 'videoListContainer';
|
||||
let headerSpacer;
|
||||
if (headerLabel != '') {
|
||||
const headerElement = document.createElement("h2");
|
||||
headerElement.innerHTML = headerLabel;
|
||||
headerElement.style.marginLeft = '15px';
|
||||
headerElement.appendChild(document.createElement("hr"));
|
||||
videoListContainer.appendChild(headerElement);
|
||||
}
|
||||
document.getElementById("main").appendChild(videoListContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the about page to #main
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function showAbout(){
|
||||
// Remove current information and display loading animation
|
||||
clearMainContainer();
|
||||
toggleLoading();
|
||||
|
||||
// Grab about.html to be used as a template
|
||||
$.get('templates/about.html', (template) => {
|
||||
mustache.parse(template);
|
||||
const rendered = mustache.render(template, {
|
||||
versionNumber: require('electron').remote.app.getVersion(),
|
||||
});
|
||||
// Render to #main and remove loading animation
|
||||
$('#main').html(rendered);
|
||||
toggleLoading();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a toast message in the bottom right corner of the page. Toast will
|
||||
* automatically disappear after 5 seconds.
|
||||
*
|
||||
* @param {string} message - The message to be displayed in the toast.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function showToast(message){
|
||||
let toast = document.getElementById('toast');
|
||||
let toastMessage = document.getElementById('toastMessage');
|
||||
|
||||
// If a toast message is already being displayed, this will remove the previous timer that was set.
|
||||
clearTimeout(toastTimeout);
|
||||
|
||||
toastMessage.innerHTML = message;
|
||||
toast.style.visibility = 'visible';
|
||||
toast.style.opacity = 0.9;
|
||||
|
||||
// Set the timer for the toast to be removed.
|
||||
toastTimeout = window.setTimeout(hideToast, 5000); // 5 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the toast notification from the page.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function hideToast(){
|
||||
let toast = document.getElementById('toast');
|
||||
toast.style.opacity = 0;
|
||||
toast.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a confirmation box before performing an action. The action will be performed
|
||||
* if the user clicks 'yes'.
|
||||
*
|
||||
* @param {string} message - The message to be displayed in the confirmation box
|
||||
* @param {function} performFunction - The function to be performed upon confirmation
|
||||
* @param {*} parameters - The parameters that will be sent to performFunction
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function confirmFunction(message, performFunction, parameters){
|
||||
let confirmContainer = document.getElementById('confirmFunction');
|
||||
let confirmMessage = document.getElementById('confirmMessage');
|
||||
|
||||
confirmMessage.innerHTML = message;
|
||||
confirmContainer.style.visibility = 'visible';
|
||||
|
||||
$(document).on('click', '#confirmYes', (event) => {
|
||||
performFunction(parameters);
|
||||
hideConfirmFunction();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the confirmation box. Happens when the user clicks on 'no'.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function hideConfirmFunction(){
|
||||
let confirmContainer = document.getElementById('confirmFunction');
|
||||
confirmContainer.style.visibility = 'hidden';
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
This file is part of FreeTube.
|
||||
|
||||
FreeTube is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FreeTube is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Video Comment Model
|
||||
*/
|
||||
export class comment {
|
||||
authorDisplayName: string;
|
||||
authorProfileImageUrl: string;
|
||||
authorChannelId: string;
|
||||
textDisplay: string;
|
||||
publishedAt: string;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
This file is part of FreeTube.
|
||||
|
||||
FreeTube is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FreeTube is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import {comment} from "./comment.model";
|
||||
|
||||
/**
|
||||
* Entire Comment Threads for a Video
|
||||
*/
|
||||
export class commentThread {
|
||||
videoId: ?string;
|
||||
nextPageToken: ?string;
|
||||
pageInfo: {
|
||||
totalResults: number,
|
||||
resultsPerPage: number
|
||||
};
|
||||
items: comment[];
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
This file is part of FreeTube.
|
||||
|
||||
FreeTube is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FreeTube is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* File used for functions related to saving videos
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds a video to the user's saved video database.
|
||||
*
|
||||
* @param {string} videoId - The video ID of the video that will be saved.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function addSavedVideo(videoId){
|
||||
let data = {
|
||||
videoId: videoId,
|
||||
timeSaved: new Date().getTime(),
|
||||
};
|
||||
|
||||
savedVidsDb.insert(data, (err, newDoc) => {
|
||||
showToast('Video has been saved!');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a video from the user's saved video database.
|
||||
*
|
||||
* @param {string} {videoId} - The video ID of the video that will be removed.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function removeSavedVideo(videoId){
|
||||
savedVidsDb.remove({
|
||||
videoId: videoId
|
||||
}, {}, (err, numRemoved) => {
|
||||
showToast('Video has been removed from saved list.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the save video button styling and saved / remove a video based on the current status.
|
||||
*
|
||||
* @param {string} videoId - The video ID to toggle between.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function toggleSavedVideo(videoId) {
|
||||
event.stopPropagation();
|
||||
|
||||
const checkIfSaved = videoIsSaved(videoId);
|
||||
const saveIcon = document.getElementById('saveIcon');
|
||||
const savedText = document.getElementById('savedText');
|
||||
|
||||
checkIfSaved.then((results) => {
|
||||
if (results === false) {
|
||||
savedText.innerHTML = 'SAVED';
|
||||
saveIcon.classList.remove('far');
|
||||
saveIcon.classList.remove('unsaved');
|
||||
saveIcon.classList.add('fas');
|
||||
saveIcon.classList.add('saved');
|
||||
addSavedVideo(videoId);
|
||||
} else {
|
||||
savedText.innerHTML = 'SAVE';
|
||||
saveIcon.classList.remove('fas');
|
||||
saveIcon.classList.remove('saved');
|
||||
saveIcon.classList.add('far');
|
||||
saveIcon.classList.add('unsaved');
|
||||
removeSavedVideo(videoId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a video was saved in the user's saved video database
|
||||
*
|
||||
* @param {string} videoId - The video ID to check
|
||||
*
|
||||
* @return {promise} - A boolean value if the video was found or not.
|
||||
*/
|
||||
function videoIsSaved(videoId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
savedVidsDb.find({videoId: videoId}, (err, docs) => {
|
||||
if (jQuery.isEmptyObject(docs)) {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a list of the user's saved videos.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function showSavedVideos(){
|
||||
clearMainContainer();
|
||||
toggleLoading();
|
||||
console.log('checking saved videos');
|
||||
|
||||
let videoList = '';
|
||||
|
||||
// Check the database for the list of videos.
|
||||
savedVidsDb.find({}).sort({
|
||||
timeSaved: -1
|
||||
}).exec((err, docs) => {
|
||||
// The YouTube API requires a max of 50 videos to be shown. Don't show more than 50.
|
||||
// TODO: Allow the app to show more than 50 saved videos.
|
||||
if(docs.length > 49){
|
||||
for (let i = 0; i < 49; i++) {
|
||||
videoList = videoList + ',' + docs[i]['videoId'];
|
||||
}
|
||||
}
|
||||
else{
|
||||
docs.forEach((video) => {
|
||||
videoList = videoList + ',' + video['videoId'];
|
||||
});
|
||||
}
|
||||
|
||||
// Call the YouTube API
|
||||
let request = gapi.client.youtube.videos.list({
|
||||
part: 'snippet',
|
||||
id: videoList,
|
||||
maxResults: 50,
|
||||
});
|
||||
|
||||
// Execute the API request
|
||||
request.execute((response) => {
|
||||
// Render the videos to the screen
|
||||
createVideoListContainer('Saved Videos:');
|
||||
response['items'].forEach((video) => {
|
||||
displayVideos(video, 'history');
|
||||
});
|
||||
toggleLoading();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
This file is part of FreeTube.
|
||||
|
||||
FreeTube is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FreeTube is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* A file for functions used for settings.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Display the settings screen to the user.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function showSettings(){
|
||||
clearMainContainer();
|
||||
toggleLoading();
|
||||
|
||||
let isChecked = '';
|
||||
let key = '';
|
||||
|
||||
/*
|
||||
* Check the settings database for the user's current settings. This is so the
|
||||
* settings page has the correct toggles related when it is rendered.
|
||||
*/
|
||||
settingsDb.find({}, (err, docs) => {
|
||||
docs.forEach((setting) => {
|
||||
switch (setting['_id']) {
|
||||
case 'apiKey':
|
||||
// If a third party forks the application, please be ethical and change the API key.
|
||||
if(setting['value'] !== 'AIzaSyDjszXMCw44W_k-pdNoOxUHFyKGtU_ejwE'){
|
||||
key = setting['value'];
|
||||
}
|
||||
break;
|
||||
case 'theme':
|
||||
if (currentTheme == ''){
|
||||
currentTheme = setting['value'];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Grab the settings.html template to prepare for rendering
|
||||
$.get('templates/settings.html', (template) => {
|
||||
mustache.parse(template);
|
||||
const rendered = mustache.render(template, {
|
||||
isChecked: isChecked,
|
||||
key: key,
|
||||
});
|
||||
// Render template to application
|
||||
$('#main').html(rendered);
|
||||
toggleLoading();
|
||||
|
||||
// Check / uncheck the switch depending on the user's settings.
|
||||
if (currentTheme === 'light'){
|
||||
document.getElementById('themeSwitch').checked = false;
|
||||
}
|
||||
else{
|
||||
document.getElementById('themeSwitch').checked = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the user's default settings. Set the the default settings if none are found.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function checkDefaultSettings(){
|
||||
// Check settings database
|
||||
settingsDb.find({}, (err, docs) => {
|
||||
if (jQuery.isEmptyObject(docs)) {
|
||||
// Set User Defaults
|
||||
let themeDefault = {
|
||||
_id: 'theme',
|
||||
value: 'light',
|
||||
};
|
||||
|
||||
let apiDefault = {
|
||||
_id: 'apiKey',
|
||||
// To any third party devs that fork the project, please be ethical and change the API key.
|
||||
value: 'AIzaSyDjszXMCw44W_k-pdNoOxUHFyKGtU_ejwE',
|
||||
};
|
||||
|
||||
// Set API Key and default theme
|
||||
let apiKey = apiDefault.value;
|
||||
setTheme('light');
|
||||
|
||||
// Inset default settings into the settings database.
|
||||
settingsDb.insert(themeDefault);
|
||||
settingsDb.insert(apiDefault);
|
||||
} else {
|
||||
// Use user current defaults
|
||||
docs.forEach((setting) => {
|
||||
switch (setting['_id']) {
|
||||
case 'theme':
|
||||
setTheme(setting['value']);
|
||||
break;
|
||||
case 'apiKey':
|
||||
apiKey = setting['value'];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Loads the JavaScript client library and invokes `start` afterwards.
|
||||
gapi.load('client', start);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the settings based on what the user has changed.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function updateSettings(){
|
||||
var themeSwitch = document.getElementById('themeSwitch').checked;
|
||||
var key = document.getElementById('api-key').value;
|
||||
|
||||
if (themeSwitch == true){
|
||||
var theme = 'dark';
|
||||
}
|
||||
else{
|
||||
var theme = 'light';
|
||||
}
|
||||
|
||||
// Update default theme
|
||||
settingsDb.update({_id: 'theme'},{value: theme}, {}, function(err, numReplaced){
|
||||
console.log(err);
|
||||
console.log(numReplaced);
|
||||
});
|
||||
|
||||
if(key != ''){
|
||||
settingsDb.update({_id: 'apiKey'},{value: key}, {});
|
||||
}
|
||||
else{
|
||||
// To any third party devs that fork the project, please be ethical and change the API key.
|
||||
settingsDb.update({_id: 'apiKey'},{value: 'AIzaSyDjszXMCw44W_k-pdNoOxUHFyKGtU_ejwE'}, {});
|
||||
}
|
||||
|
||||
showToast('Settings have been saved.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle back and forth with the current theme
|
||||
*
|
||||
* @param {boolean} themeValue - The value of the switch based on if it was turned on or not.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function toggleTheme(themeValue){
|
||||
if(themeValue.checked === true){
|
||||
setTheme('dark');
|
||||
currentTheme = 'dark';
|
||||
}
|
||||
else{
|
||||
setTheme('light');
|
||||
currentTheme = 'light';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the theme of the application
|
||||
*
|
||||
* @param {string} option - The theme to be changed to.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function setTheme(option){
|
||||
let cssFile;
|
||||
const currentTheme = document.getElementsByTagName("link").item(1);
|
||||
|
||||
// Create a link element
|
||||
const newTheme = document.createElement("link");
|
||||
newTheme.setAttribute("rel", "stylesheet");
|
||||
newTheme.setAttribute("type", "text/css");
|
||||
|
||||
// Grab the css file to be used.
|
||||
switch (option) {
|
||||
case 'light':
|
||||
cssFile = 'style/lightTheme.css';
|
||||
break;
|
||||
case 'dark':
|
||||
cssFile = 'style/darkTheme.css';
|
||||
break;
|
||||
default:
|
||||
// Default to the light theme
|
||||
cssFile = 'style/lightTheme.css';
|
||||
break;
|
||||
}
|
||||
newTheme.setAttribute("href", cssFile);
|
||||
|
||||
// Replace the current theme with the new theme
|
||||
document.getElementsByTagName("head").item(0).replaceChild(newTheme, currentTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a subscriptions file that the user provides.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function importSubscriptions(){
|
||||
const appDatabaseFile = localDataStorage + '/subscriptions.db';
|
||||
|
||||
// Open user's file browser. Only show .db files.
|
||||
dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{name: 'Database File', extensions: ['db']},
|
||||
]
|
||||
}, function(fileLocation){
|
||||
if(typeof(fileLocation) === 'undefined'){
|
||||
console.log('Import Aborted');
|
||||
return;
|
||||
}
|
||||
if(typeof(fileLocation) !== 'object'){
|
||||
showToast('Incorrect filetype. Import Aborted.');
|
||||
return;
|
||||
}
|
||||
fs.readFile(fileLocation[0], function(readErr, data){
|
||||
if(readErr){
|
||||
showToast('Unable to read file. File may be corrupt or have invalid permissions.');
|
||||
throw readErr;
|
||||
}
|
||||
fs.writeFile(appDatabaseFile, data, function(writeErr){
|
||||
if(writeErr){
|
||||
showToast('Unable to create file. Please check your permissions and try again.');
|
||||
throw writeErr;
|
||||
}
|
||||
showToast('Susbcriptions have been successfully imported. Please restart FreeTube for the changes to take effect.');
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the susbcriptions database to a file.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function exportSubscriptions(){
|
||||
const appDatabaseFile = localDataStorage + '/subscriptions.db';
|
||||
|
||||
// Open user file browser. User gives location of file to be created.
|
||||
dialog.showSaveDialog({
|
||||
filters: [
|
||||
{name: 'Database File', extensions: ['db']},
|
||||
]
|
||||
}, function(fileLocation){
|
||||
console.log(fileLocation);
|
||||
if(typeof(fileLocation) === 'undefined'){
|
||||
console.log('Export Aborted');
|
||||
return;
|
||||
}
|
||||
fs.readFile(appDatabaseFile, function(readErr, data){
|
||||
if(readErr){
|
||||
throw readErr;
|
||||
}
|
||||
fs.writeFile(fileLocation, data, function(writeErr){
|
||||
if(writeErr){
|
||||
throw writeErr;
|
||||
}
|
||||
showToast('Susbcriptions have been successfully exported');
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear out the data in a file.
|
||||
*
|
||||
* @param {string} type - The type of file to be cleared.
|
||||
*/
|
||||
function clearFile(type){
|
||||
console.log(type);
|
||||
let dataBaseFile;
|
||||
|
||||
switch (type) {
|
||||
case 'subscriptions':
|
||||
dataBaseFile = localDataStorage + '/subscriptions.db';
|
||||
break;
|
||||
case 'history':
|
||||
dataBaseFile = localDataStorage + '/videohistory.db';
|
||||
break;
|
||||
case 'saved':
|
||||
dataBaseFile = localDataStorage + '/savedvideos.db';
|
||||
break;
|
||||
default:
|
||||
showToast('Unknown file: ' + type)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace data with an empty string.
|
||||
fs.writeFile(dataBaseFile, '', function(err){
|
||||
if(err){
|
||||
throw err;
|
||||
}
|
||||
showToast('File has been cleared. Restart FreeTube to see the changes');
|
||||
})
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
This file is part of FreeTube.
|
||||
|
||||
FreeTube is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FreeTube is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* File for all functions related to subscriptions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add a channel to the user's subscription database.
|
||||
*
|
||||
* @param {string} channelId - The channel ID to add to the subscriptions database.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function addSubscription(channelId) {
|
||||
console.log(channelId);
|
||||
// Request YouTube API
|
||||
let request = gapi.client.youtube.channels.list({
|
||||
part: 'snippet',
|
||||
id: channelId,
|
||||
});
|
||||
|
||||
// Execute API request
|
||||
request.execute((response) => {
|
||||
const channelInfo = response['items'][0]['snippet'];
|
||||
const channelName = channelInfo['title'];
|
||||
const thumbnail = channelInfo['thumbnails']['high']['url'];
|
||||
|
||||
const data = {
|
||||
channelId: channelId,
|
||||
channelName: channelName,
|
||||
channelThumbnail: thumbnail,
|
||||
};
|
||||
|
||||
// Refresh the list of subscriptions on the side navigation bar.
|
||||
subDb.insert(data, (err, newDoc) => {
|
||||
displaySubs();
|
||||
});
|
||||
|
||||
showToast('Added ' + channelName + ' to subscriptions.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a channel from the subscriptions database.
|
||||
*
|
||||
* @param {string} channelId - The channel ID to be removed.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function removeSubscription(channelId) {
|
||||
subDb.remove({
|
||||
channelId: channelId
|
||||
}, {}, (err, numRemoved) => {
|
||||
// Refresh the list of subscriptions on the side navigation bar.
|
||||
displaySubs();
|
||||
showToast('Removed channel from subscriptions.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the recent uploads of the user's subscriptions.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function loadSubscriptions() {
|
||||
clearMainContainer();
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
/*
|
||||
* It is possible for the function to be called several times. This prevents the loading
|
||||
* from being turned off when the situation occurs.
|
||||
*/
|
||||
if (loading.style.display !== 'inherit'){
|
||||
toggleLoading();
|
||||
}
|
||||
|
||||
let videoList = [];
|
||||
|
||||
const subscriptions = returnSubscriptions();
|
||||
|
||||
// Welcome to callback hell, we hope you enjoy your stay.
|
||||
subscriptions.then((results) => {
|
||||
|
||||
// Yes, This function is the thing that needs to most improvment
|
||||
if (results.length > 0) {
|
||||
showToast('Getting Subscriptions. This may take a while...');
|
||||
|
||||
/*
|
||||
* If this loop gets rewritten, we can remove the asyncLoop dependency from the project.
|
||||
*
|
||||
* I wasn't able to figure out a way to loop through the list of channels and grab the recent uploads
|
||||
* while then sorting them afterwards, this was my best solution at the time. I'm sure someone more
|
||||
* experienced in Node can help out with this.
|
||||
*/
|
||||
asyncLoop(results, (sub, next) => {
|
||||
const channelId = sub['channelId'];
|
||||
|
||||
/*
|
||||
* Grab the channels 15 most recent uploads. Typically this should be enough.
|
||||
* This number can be changed if we feel necessary.
|
||||
*/
|
||||
try {
|
||||
let request = gapi.client.youtube.search.list({
|
||||
part: 'snippet',
|
||||
channelId: channelId,
|
||||
type: 'video',
|
||||
maxResults: 15,
|
||||
order: 'date',
|
||||
});
|
||||
|
||||
request.execute((response) => {
|
||||
videoList = videoList.concat(response['items']);
|
||||
// Iterate through the next object in the loop.
|
||||
next();
|
||||
});
|
||||
} catch (err) {
|
||||
/*
|
||||
* The above API requests sometimes forces an error for some reason. Restart
|
||||
* the function to prevent this. This should be changed if possible.
|
||||
*/
|
||||
loadSubscriptions();
|
||||
return;
|
||||
}
|
||||
}, (err) => {
|
||||
// Sort the videos by date
|
||||
videoList.sort((a, b) => {
|
||||
const date1 = Date.parse(a.snippet.publishedAt);
|
||||
const date2 = Date.parse(b.snippet.publishedAt);
|
||||
|
||||
return date2.valueOf() - date1.valueOf();
|
||||
});
|
||||
|
||||
// Render the videos to the application.
|
||||
createVideoListContainer('Latest Subscriptions:');
|
||||
|
||||
// The YouTube website limits the subscriptions to 100 before grabbing more so we only show 100
|
||||
// to keep the app running at a good speed.
|
||||
if(videoList.length < 100){
|
||||
videoList.forEach((video) => {
|
||||
console.log('Getting all videos');
|
||||
displayVideos(video);
|
||||
});
|
||||
}
|
||||
else{
|
||||
console.log('Getting top 100 videos');
|
||||
for(let i = 0; i < 100; i++){
|
||||
displayVideos(videoList[i]);
|
||||
}
|
||||
}
|
||||
toggleLoading();
|
||||
});
|
||||
} else {
|
||||
// User has no subscriptions. Display message.
|
||||
const container = document.getElementById('main');
|
||||
toggleLoading();
|
||||
|
||||
container.innerHTML = `<h2 class="message">Your Subscription list is currently empty. Start adding subscriptions
|
||||
to see them here.<br /><br /><i class="far fa-frown" style="font-size: 200px"></i></h2>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of subscriptions from the user's subscription database.
|
||||
*
|
||||
* @return {promise} The list of subscriptions.
|
||||
*/
|
||||
function returnSubscriptions() {
|
||||
return new Promise((resolve, reject) => {
|
||||
subDb.find({}, (err, subs) => {
|
||||
resolve(subs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the list of subscriptions on the side navigation bar.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function displaySubs() {
|
||||
const subList = document.getElementById('subscriptions');
|
||||
|
||||
subList.innerHTML = '';
|
||||
|
||||
// Sort alphabetically
|
||||
subDb.find({}).sort({
|
||||
channelName: 1
|
||||
}).exec((err, subs) => {
|
||||
subs.forEach((channel) => {
|
||||
// Grab subscriptions.html to be used as a template.
|
||||
$.get('templates/subscriptions.html', (template) => {
|
||||
mustache.parse(template);
|
||||
const rendered = mustache.render(template, {
|
||||
channelIcon: channel['channelThumbnail'],
|
||||
channelName: channel['channelName'],
|
||||
channelId: channel['channelId'],
|
||||
});
|
||||
// Render template to page.
|
||||
const subscriptionsHtml = $('#subscriptions').html();
|
||||
$('#subscriptions').html(subscriptionsHtml + rendered);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add onclick function
|
||||
$('#subscriptions .fa-times').onClick = removeSubscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds / Removes a subscription based on if the channel is in the database or not.
|
||||
* @param {string} channelId - The channel ID to check
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function toggleSubscription(channelId) {
|
||||
event.stopPropagation();
|
||||
|
||||
const checkIfSubscribed = isSubscribed(channelId);
|
||||
const subscribeButton = document.getElementById('subscribeButton');
|
||||
|
||||
checkIfSubscribed.then((results) => {
|
||||
|
||||
if (results === false) {
|
||||
if(subscribeButton != null){
|
||||
subscribeButton.innerHTML = 'UNSUBSCRIBE';
|
||||
}
|
||||
addSubscription(channelId);
|
||||
} else {
|
||||
if(subscribeButton != null){
|
||||
subscribeButton.innerHTML = 'SUBSCRIBE';
|
||||
}
|
||||
removeSubscription(channelId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is subscribed to a channel or not.
|
||||
*
|
||||
* @param {string} channelId - The channel ID to check
|
||||
*
|
||||
* @return {promise} - A boolean value if the channel is currently subscribed or not.
|
||||
*/
|
||||
function isSubscribed(channelId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
subDb.find({channelId: channelId}, (err, docs) => {
|
||||
if (jQuery.isEmptyObject(docs)) {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,662 @@
|
|||
/*
|
||||
This file is part of FreeTube.
|
||||
|
||||
FreeTube is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FreeTube is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* File for functions related to videos.
|
||||
* TODO: Split some of these functions into their own file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Perform a search using the YouTube API. The search query is grabbed from the #search element.
|
||||
*
|
||||
* @param {string} nextPageToken - Optional: The page token to be inlcuded in the search.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function search(nextPageToken = '') {
|
||||
const query = document.getElementById('search').value;
|
||||
|
||||
if (query === '') {
|
||||
showToast('Search Field empty. Please input a search term.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextPageToken === '') {
|
||||
clearMainContainer();
|
||||
toggleLoading();
|
||||
} else {
|
||||
console.log(nextPageToken);
|
||||
showToast('Fetching results. Please wait...');
|
||||
}
|
||||
|
||||
// Start API request
|
||||
let request = gapi.client.youtube.search.list({
|
||||
q: query,
|
||||
part: 'id, snippet',
|
||||
type: 'video',
|
||||
pageToken: nextPageToken,
|
||||
maxResults: 25,
|
||||
});
|
||||
|
||||
// Execute API Request
|
||||
request.execute((response) => {
|
||||
console.log(response);
|
||||
if (nextPageToken === '') {
|
||||
createVideoListContainer('Search Results:');
|
||||
toggleLoading();
|
||||
}
|
||||
response['items'].forEach(displayVideos);
|
||||
addNextPage(response['result']['nextPageToken']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a video on the page. Function is typically contained in a loop.
|
||||
*
|
||||
* @param {string} video - The video ID of the video to be displayed.
|
||||
* @param {string} listType - Optional: Specifies the list type of the video
|
||||
* Used for displaying the remove icon for history and saved videos.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function displayVideos(video, listType = null) {
|
||||
// Grab the search template for the video.
|
||||
$.get('templates/videoList.html', (template) => {
|
||||
|
||||
const videoSnippet = video['snippet']
|
||||
|
||||
// Grab the published date for the video and convert to a user readable state.
|
||||
const dateString = new Date(videoSnippet['publishedAt']);
|
||||
const publishedDate = dateFormat(dateString, "mmm dS, yyyy");
|
||||
let deleteHtml = '';
|
||||
let liveText = '';
|
||||
let videoId = video['id']['videoId'];
|
||||
|
||||
const searchMenu = $('#videoListContainer').html();
|
||||
|
||||
// Include a remove icon in the list if the application is displaying the history list or saved videos.
|
||||
if (listType === 'saved') {
|
||||
videoId = video['id'];
|
||||
deleteHtml = '<i onclick="removeSavedVideo(\'' + videoId + '\'); showSavedVideos();" class="videoDelete fas fa-times"></i>';
|
||||
} else if (listType === 'history') {
|
||||
videoId = video['id'];
|
||||
deleteHtml = '<i onclick="removeFromHistory(\'' + videoId + '\'); showHistory()" class="videoDelete fas fa-times"></i>';
|
||||
}
|
||||
|
||||
// Includes text if the video is live.
|
||||
if (videoSnippet['liveBroadcastContent'] === 'live') {
|
||||
liveText = 'LIVE NOW';
|
||||
}
|
||||
|
||||
// Render / Manipulate the template. Replace variables with data from the video.
|
||||
mustache.parse(template);
|
||||
const rendered = mustache.render(template, {
|
||||
videoThumbnail: videoSnippet['thumbnails']['medium']['url'],
|
||||
videoTitle: videoSnippet['title'],
|
||||
channelName: videoSnippet['channelTitle'],
|
||||
videoDescription: videoSnippet['description'],
|
||||
publishedDate: publishedDate,
|
||||
liveText: liveText,
|
||||
videoId: videoId,
|
||||
channelId: videoSnippet['channelId'],
|
||||
deleteHtml: deleteHtml,
|
||||
});
|
||||
// Apply the render to the page
|
||||
let nextButton = document.getElementById('getNextPage');
|
||||
if (nextButton === null) {
|
||||
$('#videoListContainer').append(rendered);
|
||||
} else {
|
||||
$(rendered).insertBefore('#getNextPage');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the page token to the next page button during a video search.
|
||||
*
|
||||
* @param {string} nextPageToken - The page token to replace the button function.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function addNextPage(nextPageToken) {
|
||||
let oldFetchButton = document.getElementById('getNextPage');
|
||||
|
||||
// Creates the element if it doesn't exist.
|
||||
if (oldFetchButton === null) {
|
||||
let fetchButton = document.createElement('div');
|
||||
fetchButton.id = 'getNextPage';
|
||||
fetchButton.innerHTML = '<i class="fas fa-search"></i> Fetch more results...';
|
||||
|
||||
$('#videoListContainer').append(fetchButton);
|
||||
}
|
||||
|
||||
// Update the on click method of the button.
|
||||
$(document).off('click', '#getNextPage');
|
||||
$(document).on('click', '#getNextPage', (event) => {
|
||||
search(nextPageToken);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the video player and play a video
|
||||
*
|
||||
* @param {string} videoId - The video ID of the video to be played.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function playVideo(videoId) {
|
||||
clearMainContainer();
|
||||
toggleLoading();
|
||||
|
||||
let subscribeText = '';
|
||||
let savedText = '';
|
||||
let savedIconClass = '';
|
||||
let savedIconColor = '';
|
||||
let video480p;
|
||||
let video720p;
|
||||
let defaultUrl;
|
||||
let defaultQuality;
|
||||
let videoHtml;
|
||||
let videoThumbnail;
|
||||
let videoType = 'video';
|
||||
let embedPlayer;
|
||||
let validUrl;
|
||||
|
||||
// Grab the embeded player. Used as fallback if the video URL cannot be found.
|
||||
try {
|
||||
let getEmbedFunction = getEmbedPlayer(videoId);
|
||||
|
||||
getEmbedFunction.then((url) => {
|
||||
embedPlayer = url;
|
||||
});
|
||||
} catch (ex) {
|
||||
showToast('Video not found. ID may be invalid.');
|
||||
toggleLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* FreeTube calls the HookTube API so that it can get the direct video URL instead of the embeded player.
|
||||
* If anyone knows how to grab these files without relying on their API it would be very welcome.
|
||||
* It helps that HookTube returns mostly the same information as a YouTube API call so performance
|
||||
* shouldn't be hindered by this.
|
||||
*/
|
||||
const url = 'https://hooktube.com/api?mode=video&id=' + videoId;
|
||||
$.getJSON(url, (response) => {
|
||||
console.log(response);
|
||||
|
||||
const videoSummary = response['json_1'];
|
||||
const videoSnippet = response['json_2']['items'][0]['snippet'];
|
||||
const videoStatistics = response['json_2']['items'][0]['statistics'];
|
||||
|
||||
// Sometimes the max resolution URL isn't found. Grab the default one as a fallback.
|
||||
try {
|
||||
videoThumbnail = videoSnippet['thumbnails']['maxres']['url'];
|
||||
} catch (e) {
|
||||
videoThumbnail = videoSnippet['thumbnails']['default']['url'];
|
||||
}
|
||||
|
||||
// Search through the returned object to get the 480p and 720p video URLs (If available)
|
||||
Object.keys(videoSummary['link']).forEach((key) => {
|
||||
console.log(key);
|
||||
switch (videoSummary['link'][key][2]) {
|
||||
case 'medium':
|
||||
video480p = videoSummary['link'][key][0];
|
||||
break;
|
||||
case 'hd720':
|
||||
video720p = videoSummary['link'][key][0];
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Default to the embeded player if the URLs cannot be found.
|
||||
if (typeof(video720p) === 'undefined' && typeof(video480p) === 'undefined') {
|
||||
defaultQuality = 'EMBED';
|
||||
videoHtml = embedPlayer.replace(/\"\;/g, '"');
|
||||
showToast('Unable to get video file. Reverting to embeded player.');
|
||||
} else if (typeof(video720p) === 'undefined' && typeof(video480p) !== 'undefined') {
|
||||
// Default to the 480p video if the 720p URL cannot be found.
|
||||
videoHtml = '<video class="videoPlayer" controls="" src="' + video480p + '" poster="' + videoThumbnail + '" autoplay></video>';
|
||||
defaultQuality = '480p';
|
||||
} else {
|
||||
// Default to the 720p video.
|
||||
videoHtml = '<video class="videoPlayer" controls="" src="' + video720p + '" poster="' + videoThumbnail + '" autoplay></video>';
|
||||
defaultQuality = '720p';
|
||||
// Force the embeded player if needed.
|
||||
//videoHtml = embedPlayer;
|
||||
}
|
||||
|
||||
// Add commas to the video view count.
|
||||
const videoViews = videoSummary['view_count'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
|
||||
// Format the date to a more readable format.
|
||||
const dateString = videoSnippet['publishedAt'];
|
||||
const publishedDate = dateFormat(dateString, "mmm dS, yyyy");
|
||||
const channelId = videoSnippet['channelId'];
|
||||
let description = videoSnippet['description'];
|
||||
|
||||
const checkSubscription = isSubscribed(channelId);
|
||||
|
||||
// Change the subscribe button text depending on if the user has subscribed to the channel or not.
|
||||
checkSubscription.then((results) => {
|
||||
if (results === false) {
|
||||
subscribeText = 'SUBSCRIBE';
|
||||
} else {
|
||||
subscribeText = 'UNSUBSCRIBE';
|
||||
}
|
||||
});
|
||||
|
||||
const checkSavedVideo = videoIsSaved(videoId);
|
||||
|
||||
// Change the save button icon and text depending on if the user has saved the video or not.
|
||||
checkSavedVideo.then((results) => {
|
||||
if (results === false) {
|
||||
savedText = 'SAVE';
|
||||
savedIconClass = 'far unsaved';
|
||||
} else {
|
||||
savedText = 'SAVED';
|
||||
savedIconClass = 'fas saved';
|
||||
}
|
||||
});
|
||||
|
||||
// Figure out the width for the like/dislike bar.
|
||||
const videoLikes = parseInt(videoStatistics['likeCount']);
|
||||
const videoDislikes = parseInt(videoStatistics['dislikeCount']);
|
||||
const totalLikes = videoLikes + videoDislikes;
|
||||
const likePercentage = parseInt((videoLikes / totalLikes) * 100);
|
||||
|
||||
// Adds clickable links to the description.
|
||||
description = autolinker.link(description);
|
||||
|
||||
// API Request
|
||||
let request = gapi.client.youtube.channels.list({
|
||||
'id': channelId,
|
||||
'part': 'snippet,contentDetails,statistics'
|
||||
});
|
||||
|
||||
// Execute request
|
||||
request.execute((response) => {
|
||||
console.log(response);
|
||||
const channelThumbnail = response['items'][0]['snippet']['thumbnails']['high']['url'];
|
||||
|
||||
$.get('templates/player.html', (template) => {
|
||||
mustache.parse(template);
|
||||
const rendered = mustache.render(template, {
|
||||
videoHtml: videoHtml,
|
||||
videoQuality: defaultQuality,
|
||||
videoTitle: videoSummary['title'],
|
||||
videoViews: videoViews,
|
||||
videoThumbnail: videoThumbnail,
|
||||
channelName: videoSummary['author'],
|
||||
videoLikes: videoLikes,
|
||||
videoDislikes: videoDislikes,
|
||||
likePercentage: likePercentage,
|
||||
videoId: videoId,
|
||||
channelId: channelId,
|
||||
channelIcon: channelThumbnail,
|
||||
publishedDate: publishedDate,
|
||||
description: description,
|
||||
isSubscribed: subscribeText,
|
||||
savedText: savedText,
|
||||
savedIconClass: savedIconClass,
|
||||
savedIconColor: savedIconColor,
|
||||
video480p: video480p,
|
||||
video720p: video720p,
|
||||
embedPlayer: embedPlayer,
|
||||
});
|
||||
$('#main').html(rendered);
|
||||
toggleLoading();
|
||||
showVideoRecommendations(videoId);
|
||||
console.log('done');
|
||||
});
|
||||
});
|
||||
// Sometimes a video URL is found, but the video will not play. I believe the issue is
|
||||
// that the video has yet to render for that quality, as the video will be available at a later time.
|
||||
// This will check the URLs and switch video sources if there is an error.
|
||||
checkVideoUrls(video480p, video720p);
|
||||
// Add the video to the user's history
|
||||
addToHistory(videoId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab the video recommendations for a video. This does not get recommendations based on what you watch,
|
||||
* as that would defeat the main purpose of using FreeTube. At any time you can check the video on HookTube
|
||||
* and compare the recommendations there. They should be nearly identical.
|
||||
*
|
||||
* @param {string} videoId - The video ID of the video to get recommendations from.
|
||||
*/
|
||||
function showVideoRecommendations(videoId) {
|
||||
let request = gapi.client.youtube.search.list({
|
||||
part: 'snippet',
|
||||
type: 'video',
|
||||
relatedToVideoId: videoId,
|
||||
maxResults: 15,
|
||||
});
|
||||
|
||||
request.execute((response) => {
|
||||
const recommendations = response['items'];
|
||||
recommendations.forEach((data) => {
|
||||
const snippet = data['snippet'];
|
||||
const videoId = data['id']['videoId'];
|
||||
const videoTitle = snippet['title'];
|
||||
const channelName = snippet['channelTitle'];
|
||||
const videoThumbnail = snippet['thumbnails']['medium']['url'];
|
||||
const dateString = snippet['publishedAt'];
|
||||
const publishedDate = dateFormat(dateString, "mmm dS, yyyy");
|
||||
|
||||
$.get('templates/recommendations.html', (template) => {
|
||||
mustache.parse(template);
|
||||
const rendered = mustache.render(template, {
|
||||
videoId: videoId,
|
||||
videoTitle: videoTitle,
|
||||
channelName: channelName,
|
||||
videoThumbnail: videoThumbnail,
|
||||
publishedDate: publishedDate,
|
||||
});
|
||||
const recommendationHtml = $('#recommendations').html();
|
||||
$('#recommendations').html(recommendationHtml + rendered);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open up the mini player to watch the video outside of the main application.
|
||||
*
|
||||
* @param {string} videoThumbnail - The URL of the video thumbnail. Used to prevent another API call.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function openMiniPlayer(videoThumbnail) {
|
||||
let lastTime;
|
||||
let videoHtml;
|
||||
|
||||
// Grabs whatever the HTML is for the current video player. Done this way to grab
|
||||
// the HTML5 player (with varying qualities) as well as the YouTube embeded player.
|
||||
if ($('.videoPlayer').length > 0) {
|
||||
$('.videoPlayer').get(0).pause();
|
||||
lastTime = $('.videoPlayer').get(0).currentTime;
|
||||
videoHtml = $('.videoPlayer').get(0).outerHTML;
|
||||
} else {
|
||||
videoHtml = $('iframe').get(0).outerHTML;
|
||||
}
|
||||
|
||||
// Create a new browser window.
|
||||
const BrowserWindow = electron.remote.BrowserWindow;
|
||||
|
||||
let miniPlayer = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 700
|
||||
});
|
||||
|
||||
// Use the miniPlayer.html template.
|
||||
$.get('templates/miniPlayer.html', (template) => {
|
||||
mustache.parse(template);
|
||||
const rendered = mustache.render(template, {
|
||||
videoHtml: videoHtml,
|
||||
videoThumbnail: videoThumbnail,
|
||||
startTime: lastTime,
|
||||
});
|
||||
// Render the template to the new browser window.
|
||||
miniPlayer.loadURL("data:text/html;charset=utf-8," + encodeURI(rendered));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a link is a valid YouTube/HookTube link and play that video. Gets input
|
||||
* from the #jumpToInput element.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function parseVideoLink() {
|
||||
let input = document.getElementById('jumpToInput').value;
|
||||
|
||||
if (input === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// The regex to get the video id from a YouTube link. Thanks StackOverflow.
|
||||
let rx = /^.*(?:(?:(you|hook)tu\.?be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/;
|
||||
|
||||
let match = input.match(rx);
|
||||
|
||||
console.log(match);
|
||||
|
||||
// Play video if a match is found.
|
||||
try {
|
||||
playVideo(match[2]);
|
||||
} catch (err) {
|
||||
showToast('Video Not Found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab the most popular videos over the last couple of days and display them.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function showMostPopular() {
|
||||
clearMainContainer();
|
||||
toggleLoading();
|
||||
|
||||
// Get the date of 2 days ago.
|
||||
var d = new Date();
|
||||
d.setDate(d.getDate() - 2);
|
||||
console.log(d.toString());
|
||||
|
||||
// Grab all videos published 2 days ago and after and order them by view count.
|
||||
// These are the videos that are considered as 'most popular' and is how similar
|
||||
// Applications grab these. Videos in the 'Trending' tab on YouTube will be different.
|
||||
// And there is no way to grab those videos.
|
||||
let request = gapi.client.youtube.search.list({
|
||||
part: 'snippet',
|
||||
order: 'viewCount',
|
||||
type: 'video',
|
||||
publishedAfter: d.toISOString(),
|
||||
maxResults: 50,
|
||||
});
|
||||
|
||||
request.execute((response) => {
|
||||
console.log(response);
|
||||
createVideoListContainer('Most Popular:');
|
||||
toggleLoading();
|
||||
response['items'].forEach(displayVideos);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a link of the video to HookTube or YouTube and copy it to the user's clipboard.
|
||||
*
|
||||
* @param {string} website - The website to watch the video on.
|
||||
* @param {string} videoId - The video ID of the video to add to the URL
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function copyLink(website, videoId) {
|
||||
// Create the URL and copy to the clipboard.
|
||||
const url = 'https://' + website + '.com/watch?v=' + videoId;
|
||||
clipboard.writeText(url);
|
||||
showToast('URL has been copied to the clipboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the YouTube embeded player of a video.
|
||||
*
|
||||
* @param {string} videoId - The video ID of the video to get.
|
||||
*
|
||||
* @return {promise} - The HTML of the embeded player
|
||||
*/
|
||||
function getEmbedPlayer(videoId) {
|
||||
console.log(videoId);
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = gapi.client.youtube.videos.list({
|
||||
part: 'player',
|
||||
id: videoId,
|
||||
});
|
||||
|
||||
request.execute((response) => {
|
||||
console.log(response);
|
||||
let embedHtml = response['items'][0]['player']['embedHtml'];
|
||||
embedHtml = embedHtml.replace('src="', 'src="https:');
|
||||
embedHtml = embedHtml.replace('width="480px"', '');
|
||||
embedHtml = embedHtml.replace('height="270px"', '');
|
||||
embedHtml = embedHtml.replace(/\"/g, '"');
|
||||
resolve(embedHtml);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the quality of the current video.
|
||||
*
|
||||
* @param {string} videoHtml - The HTML of the video player to be set.
|
||||
* @param {string} qualityType - The Quality Type of the video. Ex: 720p, 480p
|
||||
* @param {boolean} isEmbed - Optional: Value on if the videoHtml is the embeded player.
|
||||
*
|
||||
* @return {Void}
|
||||
*/
|
||||
function changeQuality(videoHtml, qualityType, isEmbed = false) {
|
||||
if (videoHtml == '') {
|
||||
showToast('Video quality type is not available. Unable to change quality.')
|
||||
return;
|
||||
}
|
||||
|
||||
videoHtml = videoHtml.replace(/\"\;/g, '"');
|
||||
|
||||
console.log(videoHtml);
|
||||
console.log(isEmbed);
|
||||
|
||||
// The YouTube API creates 2 more iFrames. This is why a boolean value is sent
|
||||
// with the function.
|
||||
const embedPlayer = document.getElementsByTagName('IFRAME')[0];
|
||||
|
||||
const html5Player = document.getElementsByClassName('videoPlayer');
|
||||
|
||||
console.log(embedPlayer);
|
||||
console.log(html5Player);
|
||||
|
||||
if (isEmbed && html5Player.length == 0) {
|
||||
// The embeded player is already playing. Return.
|
||||
showToast('You are already using the embeded player.')
|
||||
return;
|
||||
} else if (isEmbed) {
|
||||
// Switch from HTML 5 player to embeded Player
|
||||
html5Player[0].remove();
|
||||
const mainHtml = $('#main').html();
|
||||
$('#main').html(videoHtml + mainHtml);
|
||||
$('#currentQuality').html(qualityType);
|
||||
} else if (html5Player.length == 0) {
|
||||
// Switch from embeded player to HTML 5 player
|
||||
embedPlayer.remove();
|
||||
let videoPlayer = document.createElement('video');
|
||||
videoPlayer.className = 'videoPlayer';
|
||||
videoPlayer.src = videoHtml;
|
||||
videoPlayer.controls = true;
|
||||
videoPlayer.autoplay = true;
|
||||
$('#main').prepend(videoPlayer);
|
||||
$('#currentQuality').html(qualityType);
|
||||
} else {
|
||||
// Switch src on HTML 5 player
|
||||
const currentPlayBackTime = $('.videoPlayer').get(0).currentTime;
|
||||
html5Player[0].src = videoHtml;
|
||||
html5Player[0].load();
|
||||
$('.videoPlayer').get(0).currentTime = currentPlayBackTime;
|
||||
$('#currentQuality').html(qualityType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the video URLs are valid. Change the video quality if one is not.
|
||||
* The API will grab video URLs, but they will sometimes return a 404. This
|
||||
* is why this check is needed. The video URL will typically be resolved over time.
|
||||
*
|
||||
* @param {string} video480p - The URL to the 480p video.
|
||||
* @param {string} video720p - The URL to the 720p video.
|
||||
*/
|
||||
function checkVideoUrls(video480p, video720p) {
|
||||
const currentQuality = $('#currentQuality').html();
|
||||
let buttonEmbed = document.getElementById('qualityEmbed');
|
||||
|
||||
let valid480 = false;
|
||||
|
||||
if (typeof(video480p) !== 'undefined'){
|
||||
let get480pUrl = fetch(video480p);
|
||||
get480pUrl.then((status) => {
|
||||
switch (status.status) {
|
||||
case 404:
|
||||
showToast('Found valid URL for 480p, but returned a 404. Video type might be available in the future.');
|
||||
$(document).off('click', '#quality480p');
|
||||
$(document).on('click', '#quality480p', (event) => {
|
||||
changeQuality('');
|
||||
});
|
||||
buttonEmbed.click();
|
||||
return;
|
||||
break;
|
||||
case 403:
|
||||
showToast('This video is unavailable in your country.');
|
||||
$(document).off('click', '#quality480p');
|
||||
$(document).on('click', '#quality480p', (event) => {
|
||||
changeQuality('');
|
||||
});
|
||||
return;
|
||||
break;
|
||||
default:
|
||||
console.log('480p is valid');
|
||||
if (currentQuality === '720p' && typeof(video720p) === 'undefined'){
|
||||
changeQuality(video480p);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof(video720p) !== 'undefined'){
|
||||
let get720pUrl = fetch(video720p);
|
||||
get720pUrl.then((status) => {
|
||||
switch (status.status) {
|
||||
case 404:
|
||||
showToast('Found valid URL for 720p, but returned a 404. Video type might be available in the future.');
|
||||
$(document).off('click', '#quality720p');
|
||||
$(document).on('click', '#quality720p', (event) => {
|
||||
changeQuality('');
|
||||
});
|
||||
if (typeof(valid480) !== 'undefined'){
|
||||
changeQuality(video480p, '480p');
|
||||
}
|
||||
break;
|
||||
case 403:
|
||||
showToast('This video is unavailable in your country.');
|
||||
$(document).off('click', '#quality720p');
|
||||
$(document).on('click', '#quality720p', (event) => {
|
||||
changeQuality('');
|
||||
});
|
||||
return;
|
||||
break;
|
||||
default:
|
||||
console.log('720p is valid');
|
||||
if (currentQuality === '720p'){
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
.channelViewBanner{
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.channelViewImage{
|
||||
float: left;
|
||||
width: 100px;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
-webkit-border-radius: 200px 200px 200px 200px;
|
||||
}
|
||||
|
||||
.channelViewTitle{
|
||||
height: 100px;
|
||||
margin-left: 100px;
|
||||
}
|
||||
|
||||
.channelViewName{
|
||||
font-weight: bold;
|
||||
font-size: 25px;
|
||||
margin-left: 20px;
|
||||
position: relative;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.channelViewSubs{
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.channelSubButton{
|
||||
float: right;
|
||||
width: 125px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.channelViewDescription{
|
||||
white-space: pre-line;
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
background: #262626;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #212121;
|
||||
-webkit-border-radius: 1ex;
|
||||
-webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: #262626;
|
||||
}
|
||||
|
||||
body{background-color: #212121;}
|
||||
input[type=text] {color: #EEEEEE;}
|
||||
.sk-cube-grid .sk-cube {background-color: #f44336;}
|
||||
.searchBar ::-webkit-input-placeholder {color: #E0E0E0;}
|
||||
.topNav{background-color: #262626; -webkit-box-shadow: 0px -4px 20px 0px rgba(0,0,0,0.75);}
|
||||
.logo{color: #f44336;}
|
||||
.searchBar input{border-bottom: 1px solid #ddd;}
|
||||
.searchButton{color: #E0E0E0;}
|
||||
.jumpToInput{border-bottom: 1px solid #E0E0E0;}
|
||||
.message{color: #757575;}
|
||||
.videoDelete{color: white;}
|
||||
.channelViewImage{border: 0px solid white;}
|
||||
.channelViewName{color: #EEEEEE;}
|
||||
.channelViewSubs{color: #BDBDBD;}
|
||||
.channelViewDescription{color: #E0E0E0;}
|
||||
.channelSubButton{color: #E0E0E0; background-color: #f44336;}
|
||||
.videoTitle{color: #EEEEEE;}
|
||||
.channelName{color: #E0E0E0;}
|
||||
.videoDescription{color: #E0E0E0;}
|
||||
.statistics{background-color: #424242; color: #EEEEEE;}
|
||||
.views{color: #E0E0E0;}
|
||||
.details{background-color: #424242; color: #EEEEEE;}
|
||||
.playerSubButton{color: #E0E0E0; background-color: #f44336;}
|
||||
.smallButton{color: #E0E0E0; background-color: #757575;}
|
||||
.recommendDate{color: #E0E0E0;}
|
||||
.settingsButton {color: #BDBDBD; background-color: #424242;}
|
||||
.qualityTypes{color: #E0E0E0; background-color: #757575;}
|
||||
.unsaved{color: #E0E0E0;}
|
||||
|
||||
#main{color: #EEEEEE;}
|
||||
#main hr{border-bottom: 1px solid #212121;}
|
||||
#subscriptions img{border: 0px solid white;}
|
||||
#sideNav{background-color: #262626; color: #E0E0E0; -webkit-box-shadow: 4px -2px 51px -6px rgba(0,0,0,0.75);}
|
||||
#sideNav hr{background-color: #f44336;}
|
||||
#sideNav li:hover{background-color: #212121;}
|
||||
#channelIcon{border: 0px solid white;}
|
||||
#channelName{color: #E0E0E0;}
|
||||
#publishDate{color: #E0E0E0;}
|
||||
#description{color: #E0E0E0;}
|
||||
#showComments{background-color: #757575; color: #E0E0E0;}
|
||||
#recommendations{color: #EEEEEE;}
|
||||
#videoListContainer{color: #EEEEEE;}
|
||||
#toast{background-color: #616161; color: white;}
|
||||
#confirmFunction{background-color: #616161; color: white;}
|
||||
#getNextPage{background-color: #616161}
|
||||
#comments{background-color: #424242;}
|
|
@ -0,0 +1,51 @@
|
|||
::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #BDBDBD;
|
||||
-webkit-border-radius: 1ex;
|
||||
-webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: white;
|
||||
}
|
||||
|
||||
body{background-color: #e0e0e0;}
|
||||
.sk-cube-grid .sk-cube {background-color: #f44336;}
|
||||
.searchBar ::-webkit-input-placeholder {color: #ddd;}
|
||||
.topNav{background-color: #f44336; -webkit-box-shadow: 0px -4px 32px 0px rgba(0, 0, 0, 0.75);}
|
||||
.searchBar input{border-bottom: 1px solid #ddd;}
|
||||
.searchButton{color: black;}
|
||||
.jumpToInput{border-bottom: 1px solid #757575;}
|
||||
.message{color: #757575;}
|
||||
.videoDelete{color: black;}
|
||||
.channelViewImage{border: 0px solid #000000;}
|
||||
.channelViewSubs{color: #757575;}
|
||||
.channelSubButton{color: #616161; background-color: #eeeeee;}
|
||||
.videoTitle{color: #000000;}
|
||||
.channelName{color: #424242;}
|
||||
.videoDescription{color: #424242;}
|
||||
.statistics{background-color: white;}
|
||||
.views{color: #424242;}
|
||||
.details{background-color: white;}
|
||||
.playerSubButton{color: #616161; background-color: #eeeeee;}
|
||||
.smallButton{color: #616161; background-color: #eeeeee;}
|
||||
.recommendDate{color: #616161;}
|
||||
.settingsButton {color: #424242; background-color: #BDBDBD;}
|
||||
.qualityTypes{background-color: #eeeeee;}
|
||||
.unsaved{color: #616161;}
|
||||
|
||||
#subscriptions img{border: 0px solid #000000;}
|
||||
#sideNav{background-color: white; -webkit-box-shadow: 4px -2px 51px -6px rgba(0,0,0,0.75);}
|
||||
#sideNav hr{background-color: #f44336;}
|
||||
#sideNav li:hover{background-color: #e0e0e0;}
|
||||
#channelIcon{border: 0px solid #000000;}
|
||||
#showComments{background-color: #eeeeee;}
|
||||
#toast{background-color: #212121; color: #f5f5f5;;}
|
||||
#confirmFunction{background-color: #f5f5f5;}
|
||||
#getNextPage{background-color: #f5f5f5;}
|
||||
#comments{background: #eee;}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Thanks to @tobiasahlin for the loading animation.
|
||||
* Find it here: http://tobiasahlin.com/spinkit/
|
||||
* Twitter: https://twitter.com/tobiasahlin
|
||||
*/
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
position: relative;
|
||||
margin: 100px auto;
|
||||
}
|
||||
|
||||
.double-bounce1, .double-bounce2 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: #f44336;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
-webkit-animation: sk-bounce 2.0s infinite ease-in-out;
|
||||
animation: sk-bounce 2.0s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.double-bounce2 {
|
||||
-webkit-animation-delay: -1.0s;
|
||||
animation-delay: -1.0s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-bounce {
|
||||
0%, 100% { -webkit-transform: scale(0.0) }
|
||||
50% { -webkit-transform: scale(1.0) }
|
||||
}
|
||||
|
||||
@keyframes sk-bounce {
|
||||
0%, 100% {
|
||||
transform: scale(0.0);
|
||||
-webkit-transform: scale(0.0);
|
||||
} 50% {
|
||||
transform: scale(1.0);
|
||||
-webkit-transform: scale(1.0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,355 @@
|
|||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
padding: 7px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
margin-bottom: 10px;
|
||||
font: 16px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.topNav {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.topNav span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.searchBar {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.searchBar input {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.jumpToInput {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
#sideNav {
|
||||
height: 100vh;
|
||||
width: 250px;
|
||||
overflow-y: auto;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0px;
|
||||
-webkit-box-shadow: 4px -2px 51px -6px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
#sideNav hr {
|
||||
width: 95%;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#sideNav ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
#sideNav li {
|
||||
padding: 20px;
|
||||
width: 205px;
|
||||
position: relative;
|
||||
right: 50px;
|
||||
cursor: pointer;
|
||||
-webkit-transition: background 0.2s ease-out;
|
||||
-moz-transition: background 0.2s ease-out;
|
||||
-o-transition: background 0.2s ease-out;
|
||||
transition: background 0.2s ease-out;
|
||||
}
|
||||
|
||||
#sideNav li:hover {
|
||||
-moz-transition: background 0.2s ease-in;
|
||||
-o-transition: background 0.2s ease-in;
|
||||
transition: background 0.2s ease-in;
|
||||
}
|
||||
|
||||
#sideNavDisabled {
|
||||
height: 100vh;
|
||||
width: 250px;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0px;
|
||||
z-index: 1;
|
||||
margin-top: 85px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sideNavContainer {
|
||||
margin-top: 85px;
|
||||
}
|
||||
|
||||
#subscriptions img {
|
||||
float: left;
|
||||
width: 40px;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
-webkit-border-radius: 200px 200px 200px 200px;
|
||||
position: relative;
|
||||
top: -12px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#subscriptions .fa-times {
|
||||
float: right;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#subscriptions li {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.message {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.videoDelete {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#loading {
|
||||
width: 75%;
|
||||
height: 20%;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 5;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settingsInput {
|
||||
width: 50%;
|
||||
border-bottom: 1px solid #616161;
|
||||
}
|
||||
|
||||
.settingsButton {
|
||||
padding: 10px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
-webkit-box-shadow: 4px 4px 10px 0px rgba(0, 0, 0, 0.75);
|
||||
-moz-box-shadow: 4px 4px 10px 0px rgba(0, 0, 0, 0.75);
|
||||
box-shadow: 4px 4px 10px 0px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.settingsSubmit {
|
||||
padding: 15px;
|
||||
color: #212121;
|
||||
background-color: #f44336;
|
||||
cursor: pointer;
|
||||
width: 150px;
|
||||
-webkit-box-shadow: 4px 4px 10px 0px rgba(0, 0, 0, 0.75);
|
||||
-moz-box-shadow: 4px 4px 10px 0px rgba(0, 0, 0, 0.75);
|
||||
box-shadow: 4px 4px 10px 0px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
.help {
|
||||
margin: 30px;
|
||||
}
|
||||
|
||||
.live {
|
||||
font-size: 12px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
width: 57px;
|
||||
position: relative;
|
||||
left: 284px;
|
||||
color: #f44336;
|
||||
bottom: 15px;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
#toast{
|
||||
min-width: 400px;
|
||||
height: 50px;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-size: 17px;
|
||||
line-height: 50px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
-webkit-transition: opacity 0.5s ease-in-out;
|
||||
-moz-transition: opacity 0.5s ease-in-out;
|
||||
-ms-transition: opacity 0.5s ease-in-out;
|
||||
-o-transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.closeToast{
|
||||
font-size: 15px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#confirmFunction{
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 30%;
|
||||
width: 800px;
|
||||
height: 65px;
|
||||
font-weight: bold;
|
||||
line-height: 65px;
|
||||
visibility: hidden;
|
||||
-webkit-box-shadow: 5px 5px 15px -5px rgba(0,0,0,0.75);
|
||||
-moz-box-shadow: 5px 5px 15px -5px rgba(0,0,0,0.75);
|
||||
box-shadow: 5px 5px 15px -5px rgba(0,0,0,0.75);
|
||||
}
|
||||
|
||||
#confirmMessage{
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.confirmButton{
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#confirmYes{
|
||||
right: 90px;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
#confirmNo{
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
#getNextPage{
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
line-height: 45px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#comments{
|
||||
display:none;
|
||||
padding:1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.saved{color: #FFEB3B;}
|
||||
|
||||
/* Thanks to Guus Lieben for the Material Design Switch */
|
||||
|
||||
.switch-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
min-width: 112px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
margin: 16px;
|
||||
padding: 16px 0 16px 44px;
|
||||
}
|
||||
|
||||
.switch-label:before, .switch-label:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
top: 50%;
|
||||
-ms-transform: translate(0, -50%);
|
||||
-webkit-transform: translate(0, -50%);
|
||||
transform: translate(0, -50%);
|
||||
-webkit-transition: all 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.switch-label:before {
|
||||
left: 1px;
|
||||
width: 34px;
|
||||
height: 14px;
|
||||
background-color: #9E9E9E;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.switch-label:after {
|
||||
left: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #FAFAFA;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.14), 0 2px 2px 0 rgba(0, 0, 0, 0.098), 0 1px 5px 0 rgba(0, 0, 0, 0.084);
|
||||
}
|
||||
|
||||
.switch-label .toggle--on {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.switch-label .toggle--off {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.switch-input:checked+.switch-label:before {
|
||||
background-color: #90CAF9;
|
||||
}
|
||||
|
||||
.switch-input:checked+.switch-label:after {
|
||||
background-color: #2196F3;
|
||||
-ms-transform: translate(80%, -50%);
|
||||
-webkit-transform: translate(80%, -50%);
|
||||
transform: translate(80%, -50%);
|
||||
}
|
||||
|
||||
.switch-input:checked+.switch-label .toggle--on {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.switch-input:checked+.switch-label .toggle--off {
|
||||
display: none;
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
iframe{
|
||||
width: 100%;
|
||||
height: 41.25vw;
|
||||
}
|
||||
|
||||
#main{
|
||||
margin-top: 80px;
|
||||
margin-left: 250px;
|
||||
}
|
||||
|
||||
.video{
|
||||
width: 95%;
|
||||
max-width: 1000px;
|
||||
height: 145px;
|
||||
padding: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.videoThumbnail{
|
||||
width: 275px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.videoTitle{
|
||||
font-weight: bold;
|
||||
margin-left: 285px;
|
||||
margin-top: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.channelName{
|
||||
margin-left: 285px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.videoDescription{
|
||||
margin-left: 285px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.videoPlayer{width: 100%;}
|
||||
|
||||
.statistics{
|
||||
padding: 20px;
|
||||
padding-bottom: 45px;
|
||||
}
|
||||
|
||||
.title{
|
||||
font-weight: bold;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.views{
|
||||
margin-top: -10px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.details{
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.likeContainer{
|
||||
width: 300px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.likes{
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dislikes{
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dislikeBar{
|
||||
background-color: #9E9E9E;
|
||||
width: 300px;
|
||||
height: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
-webkit-border-radius: 200px 200px 200px 200px;
|
||||
}
|
||||
|
||||
.likeBar{
|
||||
height: 5px;
|
||||
background-color: #2196F3;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
-webkit-border-radius: 200px 200px 200px 200px;
|
||||
}
|
||||
|
||||
#channelIcon{
|
||||
float: left;
|
||||
width: 80px;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
-webkit-border-radius: 200px 200px 200px 200px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#comments .line {
|
||||
clear: both;
|
||||
height: 1px;
|
||||
background: #d8d8d8;
|
||||
}
|
||||
|
||||
.userIcon {
|
||||
float: left;
|
||||
width: 48px;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
-webkit-border-radius: 200px 200px 200px 200px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#channelName{
|
||||
font-weight: bold;
|
||||
margin-left: 95px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#publishDate{
|
||||
margin-left: 95px;
|
||||
font-size: 13px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
#description{
|
||||
white-space: pre-line;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.playerSubButton{
|
||||
float: right;
|
||||
width: 125px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
margin-top: -65px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.smallButton{
|
||||
float: right;
|
||||
height: 30px;
|
||||
font-size: 10px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.videoQuality{
|
||||
width: 42px;
|
||||
}
|
||||
|
||||
.videoQuality:hover .qualityTypes{
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.qualityTypes{
|
||||
visibility: hidden;
|
||||
width: 72px;
|
||||
position: relative;
|
||||
bottom: 10px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.qualityTypes ul{
|
||||
list-style-type: none;
|
||||
position: relative;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
#showComments{
|
||||
text-align: center;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#recommendations{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.recommendVideo{
|
||||
cursor: pointer;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.recommendThumbnail{
|
||||
width: 250px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.recommendTitle{
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-left: 260px;
|
||||
}
|
||||
|
||||
.recommendChannel{
|
||||
margin-left: 260px;
|
||||
font-size: 14px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.recommendDate{
|
||||
margin-left: 260px;
|
||||
font-size: 11px;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.videoListContainer{width: 100%;}
|
||||
|
||||
.videoListContainer hr{
|
||||
width: 95%;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<h1 class='center'>FreeTube {{versionNumber}} Beta</h1>
|
||||
<br />
|
||||
<h2 class='center'>This software is FOSS and released under the <a href='https://www.gnu.org/licenses/quick-guide-gplv3.html'>GNU Public License v3</a></h2>
|
||||
<br />
|
||||
<p class='center'>
|
||||
Found a bug? Want to suggest a feature? Want to help out? Check out our <a href='https://github.com/FreeTubeApp/FreeTube'>GitHub</a> page. Pull requests are welcome.
|
||||
</p>
|
|
@ -0,0 +1,21 @@
|
|||
<img class='channelViewBanner' src='{{channelBanner}}' />
|
||||
<br />
|
||||
<div class='channelViewTitle'>
|
||||
<img class='channelViewImage' src='{{channelImage}}' />
|
||||
<span class='channelViewName'>{{channelName}}</span>
|
||||
<br />
|
||||
<span class='channelViewSubs'>{{subCount}} Subscribers</span>
|
||||
<div id='subscribeButton' class='channelSubButton' onclick='toggleSubscription("{{channelId}}");'>
|
||||
{{isSubscribed}}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<hr />
|
||||
<div class='channelViewDescription'>
|
||||
{{{channelDescription}}}
|
||||
</div>
|
||||
<br />
|
||||
<hr />
|
||||
<div id='videoListContainer'>
|
||||
<h2>Latest Uploads</h2>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
{{#comments}}
|
||||
<div class="user-comment">
|
||||
<div onclick='goToChannel("{{authorChannelId.value}}")' style="float:left;margin-right: 1em; width:48px">
|
||||
<img class="userIcon" src="{{authorProfileImageUrl}}" alt="" class="user-icon" />
|
||||
</div>
|
||||
<div class="comment-data">
|
||||
<p onclick='goToChannel("{{authorChannelId.value}}")' style='cursor: pointer; margin-left: 65px; font-weight: bold;'>{{authorDisplayName}}</p>
|
||||
<p style="margin-bottom: 0; margin-left: 65px; word-break: break-word;">{{textOriginal}}</p>
|
||||
<p style="font-size:80%; padding: 0; margin-left: 65px;">{{publishedAt}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="line"></div>
|
||||
{{/comments}}
|
|
@ -0,0 +1,30 @@
|
|||
<title>Freetube Mini-Player</title>
|
||||
<style>
|
||||
body{
|
||||
background-color: #424242;
|
||||
}
|
||||
.videoPlayer{
|
||||
width: 100%;
|
||||
}
|
||||
iframe{
|
||||
width: 100%;
|
||||
height: 54.9vw;
|
||||
}
|
||||
</style>
|
||||
<script src="../js/events.js"></script>
|
||||
<script>
|
||||
window.$ = window.jQuery = require('jquery');
|
||||
let startTime = '{{startTime}}';
|
||||
|
||||
$('.videoPlayer').ready(() => {
|
||||
$('.videoPlayer').get(0).currentTime = startTime;
|
||||
});
|
||||
|
||||
let playPauseVideo = function(event) {
|
||||
let el = event.currentTarget;
|
||||
el.paused ? el.play() : el.pause();
|
||||
}
|
||||
|
||||
$(document).on('click', '.videoPlayer', playPauseVideo);
|
||||
</script>
|
||||
{{{videoHtml}}}
|
|
@ -0,0 +1,55 @@
|
|||
{{{videoHtml}}}
|
||||
<div class='statistics'>
|
||||
<div class='smallButton' onclick="openMiniPlayer('{{videoThumbnail}}')">
|
||||
MINI PLAYER <i class="fas fa-external-link-alt"></i>
|
||||
</div>
|
||||
<div class='smallButton videoQuality'>
|
||||
<span id='currentQuality'>{{videoQuality}}</span> <i class="fas fa-angle-down"></i>
|
||||
<div class='qualityTypes'>
|
||||
<ul>
|
||||
<li id='quality480p' onclick='changeQuality("{{video480p}}", "480p")'>480p</li>
|
||||
<li id='quality720p' onclick='changeQuality("{{video720p}}", "720p")'>720p</li>
|
||||
<li id='qualityEmbed' onclick='changeQuality("{{embedPlayer}}", "EMBED", true)'>EMBED</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div onclick='toggleSavedVideo("{{videoId}}")' class='smallButton'>
|
||||
<i id='saveIcon' style='color: {{savedIconColor}};' class="{{savedIconClass}} fa-star"></i> <span id='savedText'>{{savedText}}</span>
|
||||
</div>
|
||||
<div class='smallButton' onclick='copyLink("youtube", "{{videoId}}")'>
|
||||
COPY YOUTUBE LINK
|
||||
</div>
|
||||
<div class='smallButton' onclick='copyLink("hooktube", "{{videoId}}")'>
|
||||
COPY HOOKTUBE LINK
|
||||
</div>
|
||||
<br />
|
||||
<p class='title'>{{videoTitle}}</p>
|
||||
<p class='views'>{{videoViews}} views</p>
|
||||
<div class='likeContainer'>
|
||||
<div class='dislikeBar'>
|
||||
<div class='likeBar' style='width: {{likePercentage}}%;'>
|
||||
</div>
|
||||
</div>
|
||||
<span class='likes'><i class="fas fa-thumbs-up"></i> {{videoLikes}}</span><span class='dislikes'><i class="fas fa-thumbs-down"></i> {{videoDislikes}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="details">
|
||||
<img id='channelIcon' onclick='goToChannel("{{channelId}}")' src="{{channelIcon}}" />
|
||||
<p id='channelName' onclick='goToChannel("{{channelId}}")'>{{channelName}}</p>
|
||||
<p id='publishDate'>Published on {{publishedDate}}</p>
|
||||
<div id='subscribeButton' class='playerSubButton' onclick='toggleSubscription("{{channelId}}")'>{{isSubscribed}}</div>
|
||||
<br /><br />
|
||||
<div id='description'>
|
||||
{{{description}}}
|
||||
</div>
|
||||
</div>
|
||||
<div id='showComments'>
|
||||
Show Comments <i class="far fa-comments"></i> (Max of 100)
|
||||
</div>
|
||||
|
||||
<div id='comments' data-video-id="{{videoId}}">
|
||||
</div>
|
||||
|
||||
<div id='recommendations'>
|
||||
<strong>Recommendations</strong>
|
||||
</div>
|
|
@ -0,0 +1,7 @@
|
|||
<div class='recommendVideo' onclick='playVideo("{{videoId}}")'>
|
||||
<image class='recommendThumbnail' src='{{videoThumbnail}}'></image>
|
||||
<p class='recommendTitle'>{{videoTitle}}</p>
|
||||
<p class='recommendChannel'>{{channelName}}</p>
|
||||
<p class='recommendDate'>{{publishedDate}}</p>
|
||||
</div>
|
||||
<hr />
|
|
@ -0,0 +1,33 @@
|
|||
<h1 class="center">Settings</h1>
|
||||
<div class='center'>
|
||||
<input type='text' name='api-key' id='api-key' class='settingsInput' value='{{key}}' placeholder='API Key' />
|
||||
<br />
|
||||
<label for='api-key'>Set API Key: Leave blank to use default</label>
|
||||
<br />
|
||||
<input type="checkbox" id="themeSwitch" name="set-name" class="switch-input" onchange='toggleTheme(this)' {{isChecked}}>
|
||||
<label for="themeSwitch" class="switch-label">Use <span class="toggle--on">Light</span><span class="toggle--off">Dark</span> Theme</label>
|
||||
</div>
|
||||
<div class='center'>
|
||||
<div onclick='importSubscriptions()' class='settingsButton'>
|
||||
IMPORT SUBSCRIPTIONS
|
||||
</div>
|
||||
<div onclick='exportSubscriptions();' class='settingsButton'>
|
||||
EXPORT SUBSCRIPTIONS
|
||||
</div>
|
||||
</div>
|
||||
<br /><br />
|
||||
<div class='center'>
|
||||
<div onclick='confirmFunction("Are you sure you want to delete your history?", clearFile, "history")' class='settingsButton'>
|
||||
CLEAR HISTORY
|
||||
</div>
|
||||
<div onclick='confirmFunction("Are you sure you want to remove all saved videos?", clearFile, "saved")' class='settingsButton'>
|
||||
CLEAR SAVED VIDEOS
|
||||
</div>
|
||||
<div onclick='confirmFunction("Are you sure you want to remove all subscriptions?", clearFile, "subscriptions")' class='settingsButton'>
|
||||
CLEAR SUBSCRIPTIONS
|
||||
</div>
|
||||
</div>
|
||||
<br /><br />
|
||||
<div onclick='updateSettings()' class='center settingsSubmit'>
|
||||
SAVE SETTINGS
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
<li onclick='goToChannel("{{channelId}}")'>
|
||||
<img src='{{channelIcon}}' />
|
||||
{{channelName}}
|
||||
<i class='fas fa-times' onclick='toggleSubscription("{{channelId}}")'></i>
|
||||
</li>
|
|
@ -0,0 +1,9 @@
|
|||
<div class='video'>
|
||||
{{{deleteHtml}}}
|
||||
<img onclick='playVideo("{{videoId}}")' src={{videoThumbnail}} class='videoThumbnail' />
|
||||
<p onclick='playVideo("{{videoId}}")' class='videoTitle'>{{videoTitle}}</p>
|
||||
<p onclick='goToChannel("{{channelId}}")' class='channelName'>{{channelName}} - {{publishedDate}}</p>
|
||||
<p onclick='playVideo("{{videoId}}")' class='videoDescription'>{{videoDescription}}</p>
|
||||
<p class='live'>{{liveText}}</p>
|
||||
</div>
|
||||
<hr />
|
Loading…
Reference in New Issue