Merge remote-tracking branch 'origin/development'

This commit is contained in:
PrestonN 2018-10-19 18:27:04 -04:00
commit a3e108b7a4
43 changed files with 3552 additions and 2365 deletions

View File

@ -32,7 +32,7 @@
"Search / Go to URL": "Suche / URL besuchen",
"Search Results": "Suchergebnisse",
"Subscriber": "Subscriber",
"Subscriber": "Subscribers",
"Subscribers": "Subscribers",
"Video": "Video",
"Videos": "Videos",
"View Full Playlist": "Vollständige Playlist anzeigen",

View File

@ -32,7 +32,7 @@
"Search / Go to URL": "Search / Go to URL",
"Search Results": "Search Results",
"Subscriber": "Subscriber",
"Subscriber": "Subscribers",
"Subscribers": "Subscribers",
"Video": "Video",
"Videos": "Videos",
"View Full Playlist": "View Full Playlist",
@ -46,7 +46,7 @@
"Copy YouTube Link": "Copy YouTube Link",
"Open in HookTube": "Open in HookTube",
"Copy HookTube Link": "Copy HookTube Link",
"URL has been copied to the clipboard": "URL has been copied to the clipboard",
"URL has been copied to the clipboard": "URL copied to clipboard",
"Found valid URL for 480p, but returned a 404. Video type might be available in the future.": "Found valid URL for 480p, but returned a 404. Video type might be available in the future.",
"Save": "Save",
"Mini Player": "Mini Player",
@ -71,12 +71,12 @@
"Max of 100": "Max of 100",
"Recommendations": "Recommendations",
"Latest Subscriptions": "Latest Subscriptions",
"Getting Subscriptions. Please wait...": "Getting Subscriptions. Please wait...",
"Your Subscription list is currently empty. Start adding subscriptions to see them here.": "Your Subscription list is currently empty. Start adding subscriptions to see them here.",
"Getting Subscriptions. Please wait...": "Getting Subscriptions. Please wait",
"Your Subscription list is currently empty. Start adding subscriptions to see them here.": "Add subscriptions to see them here.",
"Saved Videos": "Saved Videos",
"Watch History": "Watch History",
"API Key": "API Key",
"Set API Key: Leave blank to use default": "Set API Key: Leave blank to use default",
"API Key": "API key",
"Set API Key: Leave blank to use default": "Set API key: Leave blank to use default",
"Use Dark Theme": "Use Dark Theme",
"Import Subscriptions": "Import Subscriptions",
"Export Subscriptions": "Export Subscriptions",
@ -90,6 +90,6 @@
"Yes": "Yes",
"No": "No",
"Beta": "Beta",
"This software is FOSS and released under the GNU Public License v3+.": "This software is FOSS and released under the GNU Public License v3+.",
"Found a bug? Want to suggest a feature? Want to help out? Check out our GitHub page. Pull requests are welcome.": "Found a bug? Want to suggest a feature? Want to help out? Check out our GitHub page. Pull requests are welcome."
"This software is FOSS and released under the GNU Public License v3+.": "This copylefted software is freely licensed GPLv3+.",
"Found a bug? Want to suggest a feature? Want to help out? Check out our GitHub page. Pull requests are welcome.": "Found a bug? Want to suggest a feature? Want to help out? Check out our GitHub page. Pull requests welcome."
}

View File

@ -13,17 +13,17 @@
"Reload": "Recargar",
"Force Reload": "Forzar Recarga",
"Toggle Developer Tools": "Herramientas para desarrolladores",
"Actual size": "Tamaño real",
"Zoom in": "Aumentar zoom",
"Zoom out": "Reducir zoom",
"Toggle fullscreen": "Cambiar a pantalla completa",
"Actual size": "Escala : 100%",
"Zoom in": "Ampliar",
"Zoom out": "Reducir",
"Toggle fullscreen": "Pantalla completa",
"Window": "Ventana",
"Minimize": "Minimizar",
"Close": "Cerrar",
"FreeTube": "FreeTube",
"Subscriptions": "Suscripciones",
"Featured": "Destacados",
"Most Popular": "Más Popular",
"Most Popular": "Popular Vídeos",
"Saved": "Guardados",
"Playlists": "Listas de Reproducción",
"History": "Historial",
@ -32,7 +32,7 @@
"Search / Go to URL": "Buscar / Ir a la URL",
"Search Results": "Resultados de búsqueda",
"Subscriber": "Suscriptor",
"Subscriber": "Suscriptores",
"Subscribers": "Suscriptores",
"Video": "Vídeo",
"Videos": "Vídeos",
"View Full Playlist": "Ver Lista de Reproducción completa",
@ -55,18 +55,18 @@
"Subscribe": "Suscribirse",
"Unsubscribe": "Desuscribirse",
"Published on": "Publicado el",
"Jan": "ene",
"Feb": "feb",
"Mar": "mar",
"Apr": "abr",
"May": "may",
"Jun": "jun",
"Jul": "jul",
"Aug": "ago",
"Sep": "sep",
"Oct": "oct",
"Nov": "nov",
"Dec": "dic",
"Jan": "Ene",
"Feb": "Feb",
"Mar": "Mar",
"Apr": "Abr",
"May": "May",
"Jun": "Jun",
"Jul": "Jul",
"Aug": "Ago",
"Sep": "Sep",
"Oct": "Oct",
"Nov": "Nov",
"Dec": "Dic",
"Show Comments": "Ver Comentarios",
"Max of 100": "Máximo de 100",
"Recommendations": "Recomendaciones",
@ -90,6 +90,6 @@
"Yes": "Sí",
"No": "No",
"Beta": "Beta",
"This software is FOSS and released under the GNU Public License v3+.": "Este software es FOSS y liberado bajo la licencia GNU Public License v3+.",
"This software is FOSS and released under the GNU Public License v3+.": "Este software es FOSS y liberado bajo la licencia GPLv3+",
"Found a bug? Want to suggest a feature? Want to help out? Check out our GitHub page. Pull requests are welcome.": "¿Encontraste un bug? ¿Quieres sugerir una nueva característica? ¿Quieres ayudar? Visita nuestra página de Github. Las contribuciones son bienvenidas"
}

95
locales/fr.json Normal file
View File

@ -0,0 +1,95 @@
{
"File": "Fichier",
"Quit": "Quitter",
"Edit": "Édition",
"Undo": "Annuler",
"Redo": "Rétablir",
"Cut": "Couper",
"Copy": "Copier",
"Paste": "Coller",
"Delete": "Supprimer",
"Select all": "Séléctionner tout",
"View": "Affichage",
"Reload": "Actualiser",
"Force Reload": "Forcer l'actualisation",
"Toggle Developer Tools": "Outils de Développement",
"Actual size": "Zoom 100%",
"Zoom in": "Zoom +",
"Zoom out": "Zoom -",
"Toggle fullscreen": "Plein Écran",
"Window": "Fenêtre",
"Minimize": "Réduire",
"Close": "Fermer",
"FreeTube": "FreeTube",
"Subscriptions": "Abonnements",
"Featured": "Mis en Avant",
"Most Popular": "Vidéos Populaires",
"Saved": "Sauvegardée",
"Playlists": "Playlists",
"History": "Historique",
"Settings": "Paramêtre",
"About": "À Propos",
"Search / Go to URL": "Rechercher / Aller vers le lien",
"Search Results": "Résultats",
"Subscriber": "Abonné(e)",
"Subscribers": "Abonné(e)s",
"Video": "Vidéo",
"Videos": "Vidéos",
"View Full Playlist": "Voir Playlist Complète",
"Live Now": "En Direct",
"Fetch more results": "Voir plus de résultats",
"Fetching results. Please wait": "Récupération des résultats. Patientez",
"Latest Subscriptions": "Dernières Vidéos de vos Abonnements",
"Save Video": "Savegarder cette Vidéo",
"Remove Saved Video": "Supprimer cette Vidéo Sauvegardée",
"Open in YouTube": "Ouvrir sur YouTube",
"Copy YouTube Link": "Copier Lien YouTube",
"Open in HookTube": "Ouvrir sur HookTube",
"Copy HookTube Link": "Ouvrir sur HookTube",
"URL has been copied to the clipboard": "Lien copié dans le Presse-Papier",
"Found valid URL for 480p, but returned a 404. Video type might be available in the future.": "Lien vers 480p trouvé, mais une erreur fut retournée. Ce type de vidéo pourrait être disponnible dans le futur.",
"Save": "Sauvegarder",
"Mini Player": "Mini Lecteur",
"View": "Vue",
"Views": "Vues",
"Subscribe": "S'abonner",
"Unsubscribe": "Se Désabonner",
"Published on": "Publiée le",
"Jan": "Janv.",
"Feb": "Févr.",
"Mar": "Mars",
"Apr": "Avr.",
"May": "Mai",
"Jun": "Juin",
"Jul": "Juill.",
"Aug": "Août",
"Sep": "Sept",
"Oct": "Oct",
"Nov": "Nov",
"Dec": "Déc",
"Show Comments": "Voir Commentaires",
"Max of 100": "100 Maximum",
"Recommendations": "Recommendations",
"Latest Subscriptions": "Dernières Vidéos de vos Abonnements",
"Getting Subscriptions. Please wait...": "Récupération des résultats. Patientez...",
"Your Subscription list is currently empty. Start adding subscriptions to see them here.": "Ajoutez des abonnements pour les voirs ici.",
"Saved Videos": "Vidéos Sauvegardées",
"Watch History": "Historique",
"API Key": "API key",
"Set API Key: Leave blank to use default": "Set API key: Leave blank to use default",
"Use Dark Theme": "Thème Sombre",
"Import Subscriptions": "Importer vos Abonnements",
"Export Subscriptions": "Exporter vos Abonnements",
"Clear History": "Supprimer l'Historique",
"Are you sure you want to delete your history?": "Êtes vous sur de vouloir supprimer votre historique ?",
"Clear Saved Videos": "Supprimer les vidéos sauvegardées",
"Are you sure you want to remove all saved videos?": "Êtes vous sur de vouloir supprimer toutes les vidéos sauvegardées ?",
"Clear Subscriptions": "Supprimer les abonnements",
"Are you sure you want to remove all subscriptions?": "Êtes vous sur de vouloir supprimer toutes les abonnements ?",
"Save Settings": "Sauvegarder",
"Yes": "Oui",
"No": "Non",
"Beta": "Beta",
"This software is FOSS and released under the GNU Public License v3+.": "Ce logiciel est distribué sous licence libre GPLv3+.",
"Found a bug? Want to suggest a feature? Want to help out? Check out our GitHub page. Pull requests are welcome.": "Vous avez trouvé(e) un bug ? Vous voulez suggerer une fonctionnalité ? Vous voulez aider ? N'hesitez pas à aller voir notre page Github. Les pull requests sont les bienvenus."
}

View File

@ -32,7 +32,7 @@
"Search / Go to URL": "Cerca / Vai all' URL",
"Search Results": "Cerca Risultati",
"Subscriber": "Iscritto",
"Subscriber": "Iscritti",
"Subscribers": "Iscritti",
"Video": "Video",
"Videos": "Video",
"View Full Playlist": "Vedi Intera Playlist",

View File

@ -32,7 +32,7 @@
"Search / Go to URL": "Zoeken / Ga naar URL",
"Search Results": "Zoekresultaten",
"Subscriber": "Abonnee",
"Subscriber": "Abonnees",
"Subscribers": "Abonnees",
"Video": "Video",
"Videos": "Videos",
"View Full Playlist": "Volledige afspeellijst weergeven",

View File

@ -32,7 +32,7 @@
"Search / Go to URL": "Поиск / перейти по URL",
"Search Results": "Результаты поиска",
"Subscriber": "Подписка",
"Subscriber": "Подписчиков",
"Subscribers": "Подписчиков",
"Video": "Видео",
"Videos": "Видео",
"View Full Playlist": "Список воспроизведения",

2730
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "FreeTube",
"productName": "FreeTube",
"version": "0.3.2",
"version": "0.4.0",
"description": "An Open Source YouTube app for privacy.",
"main": "src/js/init.js",
"scripts": {
@ -18,7 +18,7 @@
"make:linux:x86:deb": "electron-forge make --platform=linux --targets=deb --arch x64",
"make:linux:x86:rpm": "electron-forge make --platform=linux --targets=rpm --arch x64",
"make:linux:x86:snap": "electron-forge package && electron-installer-snap --src=out/FreeTube-linux-x64 --arch x64",
"make:linux:x86:flatpak": "electron-installer-flatpak --src out/FreeTube-linux-x64/ --dest out/make --arch x64",
"make:linux:x86:flatpak": "electron-installer-flatpak --src out/FreeTube-linux-x64/ --dest out/make --arch x64 --id org.freetube.FreeTube --productName FreeTube --runtime org.freedesktop.Platform//1.6 --runtimeVersion 1.6 --sdk org.freedesktop.Sdk//1.6 --base io.atom.electron.BaseApp --baseVersion stable",
"make:linux:x86:appimage": "electron-forge make --platform=linux --targets=electron-forge-maker-appimage --arch x64",
"make:linux:arm": "electron-forge make --platform=linux --arch arm64",
"make:linux:arm:zip": "electron-forge make --platform=linux --targets=zip --arch arm64",
@ -70,6 +70,9 @@
"electronInstallerDebian": {
"icon": "src/icons/iconColor.png"
},
"electronForgeMakerAppimage": {
"icon": "src/icons/iconColor.png"
},
"repository": {
"type": "git",
"url": "https://github.com/FreeTubeApp/FreeTube"
@ -78,17 +81,18 @@
},
"devDependencies": {
"electron-forge": "^5.2.2",
"electron-forge-maker-appimage": "^20.14.4",
"electron-forge-maker-appimage": "^20.28.3",
"electron-installer-flatpak": "^0.8.0",
"electron-installer-snap": "^2.0.1",
"electron-prebuilt-compile": "2.0.2",
"electron-winstaller": "^2.6.4"
"electron-prebuilt-compile": "3.0.2",
"electron-winstaller": "^2.7.0"
},
"dependencies": {
"autolinker": "^1.6.2",
"commonjs": "0.0.1",
"dateformat": "^3.0.3",
"electron-compile": "6.4.2",
"electron-context-menu": "^0.10.0",
"electron-squirrel-startup": "^1.0.0",
"github-version-checker": "^2.0.1",
"jquery": "^3.3.1",
@ -97,6 +101,7 @@
"nedb": "^1.8.0",
"opml-to-json": "0.0.3",
"tor-request": "^2.1.2",
"vue": "^2.5.17",
"ytdl-core": "^0.20.4"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -8,7 +8,9 @@
<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/playlist.css">
<link rel="stylesheet" href="style/loading.css">
<link rel="stylesheet" href="style/select.css">
<link rel="stylesheet" href="style/fa-solid.min.css">
<link rel="stylesheet" href="style/fontawesome-all.min.css">
<title>Freetube Player</title>
@ -44,6 +46,7 @@
<div class="sideNavContainer">
<ul>
<li v-on:click='subscriptions'><i class="fas fa-rss"></i>&nbsp;&nbsp;Subscriptions</li>
<li v-on:click='trending'><i class="fas fa-fire"></i>&nbsp;&nbsp;Trending</li>
<li v-on:click='popular'><i class="fas fa-users"></i>&nbsp;&nbsp;Most Popular</li>
<li v-on:click='saved'><i class="fas fa-star"></i>&nbsp;&nbsp;Favorites</li>
<li v-on:click='history'><i class="fas fa-history"></i>&nbsp;&nbsp;History</li>
@ -73,8 +76,10 @@
<div id='searchView'></div>
<div id='subscriptionView'></div>
<div id='popularView'></div>
<div id='trendingView'></div>
<div id='savedView'></div>
<div id='historyView'></div>
<div id='playlistView'></div>
<div id='aboutView'></div>
<div id='settingsView'></div>
<div id='playerView'></div>
@ -94,6 +99,7 @@
<script src="js/channels.js"></script>
<script src="js/savedVideos.js"></script>
<script src="js/history.js"></script>
<script src="js/playlist.js"></script>
<script src="js/events.js"></script>
</html>

View File

@ -30,6 +30,9 @@
*/
function goToChannel(channelId) {
channelView.channelId = channelId;
channelView.page = 2;
headerView.title = 'Latest Uploads';
hideViews();
loadingView.seen = true;
@ -39,65 +42,47 @@ function goToChannel(channelId) {
channelView.subButtonText = (subscribed ? "UNSUBSCRIBE" : "SUBSCRIBE");
});
// Grab general channel information
youtubeAPI('channels', {
part: 'snippet,brandingSettings,statistics',
id: channelId,
}, (data) => {
const channelData = data.items[0];
invidiousAPI('channels', channelId, {}, (data) => {
console.log(data);
channelView.id = channelId;
channelView.name = channelData.brandingSettings.channel.title;
channelView.banner = channelData.brandingSettings.image.bannerImageUrl;
channelView.icon = channelData.snippet.thumbnails.high.url;
channelView.subCount = channelData.statistics.subscriberCount.toLocaleString(); //toLocaleString adds commas as thousands separators
channelView.description = autolinker.link(channelData.brandingSettings.channel.description); //autolinker makes URLs clickable
channelView.name = data.author;
channelView.banner = data.authorBanners[0].url;
channelView.icon = data.authorThumbnails[3].url
channelView.subCount = data.subCount.toLocaleString(); //toLocaleString adds commas as thousands separators
channelView.description = autolinker.link(data.description); //autolinker makes URLs clickable
channelVideosView.videoList = [];
// Grab the channel's latest uploads. API forces a max of 50.
youtubeAPI('search', {
part: 'snippet',
channelId: channelId,
type: 'video',
maxResults: 50,
order: 'date',
}, function (data) {
let grabDuration = getDuration(data.items);
if (subscriptionView.seen === false && aboutView.seen === false && headerView.seen === false && searchView.seen === false && settingsView.seen === false && popularView.seen === false && savedView.seen === false && historyView.seen === false) {
channelVideosView.seen = true;
channelView.seen = true;
}
else{
return;
}
grabDuration.then((videoList) => {
channelVideosView.videoList = [];
if (subscriptionView.seen === false && aboutView.seen === false && headerView.seen === false && searchView.seen === false && settingsView.seen === false && popularView.seen === false && savedView.seen === false && historyView.seen === false) {
channelVideosView.seen = true;
channelView.seen = true;
}
else{
return;
}
loadingView.seen = false;
videoList.items.forEach((video) => {
displayVideo(video, 'channel');
});
// Grab the channel's latest uploads. API forces a max of 50.
youtubeAPI('search', {
part: 'snippet',
channelId: channelId,
type: 'video',
maxResults: 50,
order: 'date',
}, function (data) {
// Display recent uploads to #main
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
videoList.items.forEach((video) => {
displayVideo(video);
});
});
});
});
loadingView.seen = false;
data.latestVideos.forEach((video) => {
displayVideo(video, 'channel');
});
});
}
/**
* Grab the next list of videos from a channel.
*
* @return {Void}
*/
function channelNextPage() {
showToast('Fetching results, please wait...');
invidiousAPI('channels/videos', channelView.channelId, {'page': channelView.page}, (data) => {
console.log(data);
data.forEach((video) => {
displayVideo(video, 'channel');
});
});
channelView.page = channelView.page + 1;
}

View File

@ -24,32 +24,35 @@
* Event when user clicks comment box,
* and wants to show/display comments for the user.
*/
let showComments = function (event) {
let showComments = function (event, continuation = '') {
let comments = $('#comments');
if (comments.css('display') === 'none') {
comments.attr('loaded', 'true');
youtubeAPI('commentThreads', {
'videoId': $('#comments').attr('data-video-id'),
'part': 'snippet,replies',
'maxResults': 100,
}, function (data) {
let comments = [];
let items = data.items;
invidiousAPI('comments', $('#comments').attr('data-video-id'), {}, (data) => {
console.log(data);
items.forEach((object) => {
let snippet = object.snippet.topLevelComment.snippet;
let comments = [];
snippet.publishedAt = dateFormat(new Date(snippet.publishedAt), "mmm dS, yyyy");
data.comments.forEach((object) => {
comments.push(snippet);
})
const commentsTemplate = require('./templates/comments.html');
const html = mustache.render(commentsTemplate, {
comments: comments,
});
$('#comments').html(html);
let snippet = {
author: object.author,
authorId: object.authorId,
authorThumbnail: object.authorThumbnails[0].url,
published: object.publishedText,
authorComment: object.content,
}
comments.push(snippet);
})
const commentsTemplate = require('./templates/comments.html');
const html = mustache.render(commentsTemplate, {
comments: comments,
});
$('#comments').html(html);
});
comments.show();
@ -66,9 +69,15 @@ let playPauseVideo = function (event) {
el.paused ? el.play() : el.pause();
};
/**
* Handle keyboard shortcut commands.
*/
let videoShortcutHandler = function (event) {
if (event.which == 68 && event.altKey === true) {
$('#search').focus();
}
let videoPlayer = $('.videoPlayer').get(0);
if (typeof (videoPlayer) !== 'undefined' && !$('#jumpToInput').is(':focus') && !$('#search').is(':focus')) {
switch (event.which) {
@ -92,10 +101,34 @@ let videoShortcutHandler = function (event) {
event.preventDefault();
changeDurationBySeconds(10);
break;
case 79:
// O Key
event.preventDefault();
if (videoPlayer.playbackRate > 0.25){
let rate = videoPlayer.playbackRate - 0.25;
videoPlayer.playbackRate = rate;
$('#currentSpeed').html(rate);
}
break;
case 80:
// P Key
event.preventDefault();
if (videoPlayer.playbackRate < 2){
let rate = videoPlayer.playbackRate + 0.25;
videoPlayer.playbackRate = rate;
$('#currentSpeed').html(rate);
}
break;
case 70:
// F Key
event.preventDefault();
videoPlayer.webkitRequestFullscreen();
if (videoPlayer.webkitDisplayingFullscreen) {
videoPlayer.webkitExitFullscreen
}
else{
videoPlayer.webkitRequestFullscreen();
}
break;
case 77:
// M Key
@ -218,13 +251,24 @@ $(document).on('click', '#confirmNo', hideConfirmFunction);
// Open links externally by default
$(document).on('click', 'a[href^="http"]', (event) => {
let el = event.currentTarget;
event.preventDefault();
shell.openExternal(el.href);
if (!el.href.includes('freetube')) {
event.preventDefault();
shell.openExternal(el.href);
}
else{
window.open(el.href,"_self");
}
});
// Open links externally on middle click.
$(document).on('auxclick', 'a[href^="http"]', (event) => {
let el = event.currentTarget;
event.preventDefault();
shell.openExternal(el.href);
if (!el.href.includes('freetube')) {
event.preventDefault();
}
else{
event.preventDefault();
let url = el.href.replace('freetube://', '');
shell.openExternal(el.href);
}
});

View File

@ -18,12 +18,12 @@
let ft = {};
/**
*
*
* Use this function instead of console.log.
* This function logs the date, time and presents the information in a readable format
*
* @param {*} data
*
*
* @param {*} data
*
* @returns {Void}
*/
ft.log = function (...data) {
@ -36,4 +36,4 @@ ft.log = function (...data) {
currentTime.getSeconds();
console.log('[' + time + '] ' + '[FREETUBE]', data);
}
}

View File

@ -51,41 +51,19 @@ function removeFromHistory(videoId){
* @return {Void}
*/
function showHistory(){
//clearMainContainer();
//startLoadingAnimation();
console.log('checking history');
let videoList = '';
historyView.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'];
});
}
youtubeAPI('videos', {
part: 'snippet',
id: videoList,
maxResults: 50,
}, function (data) {
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
historyView.videoList = [];
loadingView.seen = false;
videoList.items.forEach((video) => {
displayVideo(video, 'history');
docs.forEach((video) => {
invidiousAPI('videos', video.videoId, {}, (data) => {
displayVideo(data, 'history');
});
});
});
loadingView.seen = false;
});
}

View File

@ -29,6 +29,10 @@ const {
const path = require('path');
const url = require('url');
require('electron-context-menu')({
prepend: (params, browserWindow) => []
});
let win;
protocol.registerStandardSchemes(['freetube']);
@ -79,11 +83,15 @@ let init = function () {
label: 'File',
submenu: [{
label: 'Open New Window',
click () { init() }
},
{role: 'quit'}
click() {
init()
}
},
{
role: 'quit'
}
]
},
},
{
label: 'Edit',
submenu: [{

View File

@ -40,8 +40,17 @@ const getOpml = require('opml-to-json'); // Gets the file type for imported file
const fs = require('fs'); // Used to read files. Specifically in the settings page.
const tor = require('tor-request');
// User Defaults
let currentTheme = '';
let useTor = false;
let rememberHistory = true;
let autoplay = true;
let enableSubtitles = false;
let checkForUpdates = true;
let currentVolume = 1;
let defaultQuality = 720;
let defaultPlaybackRate = '1';
let dialog = electron.remote.dialog; // Used for opening file browser to export / import subscriptions.
let toastTimeout; // Timeout for toast notifications.
let mouseTimeout; // Timeout for hiding the mouse cursor on video playback
@ -54,7 +63,7 @@ require.extensions['.html'] = function (module, filename) {
// none are found.
electron.ipcRenderer.on('ping', function(event, message) {
ft.log(message);
console.log(message);
let url = message[1].replace('freetube://', '');
parseSearchText(url);
ft.log(message);

View File

@ -15,8 +15,6 @@
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* File for functions related to videos.
*/
@ -28,160 +26,222 @@
*
* @return {Void}
*/
function playVideo(videoId) {
hideViews();
function playVideo(videoId, playlistId = '') {
hideViews();
playerView.playerSeen = true;
playerView.videoId = videoId;
playerView.video480p = undefined;
playerView.video720p = undefined;
playerView.embededHtml = "<iframe width='560' height='315' src='https://www.youtube-nocookie.com/embed/" + videoId + "?rel=0' frameborder='0' allow='autoplay; encrypted-media' allowfullscreen></iframe>";
playerView.playerSeen = true;
playerView.firstLoad = true;
playerView.videoId = videoId;
playerView.videoAudio = undefined;
playerView.validAudio = true;
playerView.video480p = undefined;
playerView.valid480p = true;
playerView.video720p = undefined;
playerView.valid720p = true;
playerView.embededHtml = "<iframe width='560' height='315' src='https://www.youtube-nocookie.com/embed/" + videoId + "?rel=0' frameborder='0' allow='autoplay; encrypted-media' allowfullscreen></iframe>";
let videoHtml = '';
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) {
playerView.savedText = 'FAVORITE';
playerView.savedIconType = 'far unsaved';
} else {
playerView.savedText = 'FAVORITED';
playerView.savedIconType = 'fas saved';
}
});
youtubeAPI('videos', {
part: 'statistics',
id: videoId,
}, function (data) {
// Figure out the width for the like/dislike bar.
playerView.videoLikes = data['items'][0]['statistics']['likeCount'];
playerView.videoDislikes = data['items'][0]['statistics']['dislikeCount'];
let totalLikes = parseInt(playerView.videoLikes) + parseInt(playerView.videoDislikes);
playerView.likePercentage = parseInt((playerView.videoLikes / totalLikes) * 100);
});
/*
* FreeTube calls youtube-dl to grab the direct video URL.
*/
youtubedlGetInfo(videoId, (info) => {
playerView.videoTitle = info['title'];
playerView.channelName = info['author']['name'];
playerView.channelId = info['author']['id'];
playerView.channelIcon = info['author']['avatar'];
let videoUrls = info['formats'];
// Add commas to the video view count.
playerView.videoViews = info['view_count'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
playerView.videoThumbnail = info['player_response']['videoDetails']['thumbnail']['thumbnails'][3]['url'];
// Format the date to a more readable format.
let dateString = new Date(info['published']);
dateString.setDate(dateString.getDate() + 1);
playerView.publishedDate = dateFormat(dateString, "mmm dS, yyyy");
let description = info['description'];
// Adds clickable links to the description.
playerView.description = autolinker.link(description);
// Search through the returned object to get the 480p and 720p video URLs (If available)
Object.keys(videoUrls).forEach((key) => {
switch (videoUrls[key]['itag']) {
case '18':
playerView.video480p = decodeURIComponent(videoUrls[key]['url']);
//console.log(video480p);
break;
case '22':
playerView.video720p = decodeURIComponent(videoUrls[key]['url']);
//console.log(video720p);
break;
}
// Change the save button icon and text depending on if the user has saved the video or not.
checkSavedVideo.then((results) => {
if (results === false) {
playerView.savedText = 'FAVORITE';
playerView.savedIconType = 'far unsaved';
} else {
playerView.savedText = 'FAVORITED';
playerView.savedIconType = 'fas saved';
}
});
//"kpkXPy_jXmU"
invidiousAPI('videos', videoId, {}, function (data) {
let useEmbedPlayer = false;
console.log(data);
// Default to the embeded player if the URLs cannot be found.
if (typeof(playerView.video720p) === 'undefined' && typeof(playerView.video480p) === 'undefined') {
//useEmbedPlayer = true;
playerView.currentQuality = 'EMBED';
playerView.playerSeen = false;
useEmbedPlayer = true;
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.
playerView.videoUrl = playerView.video480p;
playerView.currentQuality = '480p';
} else {
// Default to the 720p video.
playerView.videoUrl = playerView.video720p;
playerView.currentQuality = '720p';
}
// Figure out the width for the like/dislike bar.
playerView.videoLikes = data.likeCount;
playerView.videoDislikes = data.dislikeCount;
let totalLikes = parseInt(playerView.videoLikes) + parseInt(playerView.videoDislikes);
playerView.likePercentage = parseInt((playerView.videoLikes / totalLikes) * 100);
playerView.videoTitle = data.title;
playerView.channelName = data.author;
playerView.channelId = data.authorId;
playerView.channelIcon = data.authorThumbnails[2].url;
if (!useEmbedPlayer) {
let videoHtml = '';
let videoUrls = data.formatStreams;
if (typeof(info.player_response.captions) === 'object') {
if (typeof(info.player_response.captions.playerCaptionsTracklistRenderer.captionTracks) === 'object') {
const videoSubtitles = info.player_response.captions.playerCaptionsTracklistRenderer.captionTracks;
// Add commas to the video view count.
playerView.videoViews = data.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
videoSubtitles.forEach((subtitle) => {
let subtitleUrl = 'https://www.youtube.com/api/timedtext?lang=' + subtitle.languageCode + '&fmt=vtt&name=&v=' + videoId;
playerView.videoThumbnail = data.videoThumbnails[0].url;
if (subtitle.kind == 'asr') {
//subtitleUrl = subtitle.baseUrl;
return;
// Format the date to a more readable format.
let dateString = new Date(data.published * 1000);
dateString.setDate(dateString.getDate() + 1);
playerView.publishedDate = dateFormat(dateString, "mmm dS, yyyy");
playerView.description = parseDescription(data.descriptionHtml);
// Search through the returned object to get the 480p and 720p video URLs (If available)
Object.keys(videoUrls).forEach((key) => {
switch (videoUrls[key]['itag']) {
case '18':
playerView.video480p = decodeURIComponent(videoUrls[key]['url']);
//console.log(video480p);
break;
case '22':
playerView.video720p = decodeURIComponent(videoUrls[key]['url']);
//console.log(video720p);
break;
case '36':
playerView.videoAudio = decodeURIComponent(videoUrls[key]['url']);
//console.log(video720p);
break;
}
});
if (typeof(playerView.videoAudio) === 'undefined') {
playerView.validAudio = false;
}
let useEmbedPlayer = false;
// Default to the embeded player if the URLs cannot be found.
if (typeof (playerView.video720p) === 'undefined' && typeof (playerView.video480p) === 'undefined') {
//useEmbedPlayer = true;
playerView.currentQuality = 'EMBED';
playerView.playerSeen = false;
//useEmbedPlayer = true;
showToast('Unable to get video file. Reverting to embeded player.');
}
else if (typeof (playerView.video720p) === 'undefined' && typeof (playerView.video480p) !== 'undefined') {
// Default to the 480p video if the 720p URL cannot be found.
console.log('Found');
playerView.videoUrl = playerView.video480p;
playerView.currentQuality = '480p';
} else {
// Default to the 720p video.
playerView.videoUrl = playerView.video720p;
playerView.currentQuality = '720p';
//playerView.videoUrl = playerView.liveManifest;
}
if (!useEmbedPlayer) {
data.captions.forEach((caption) => {
let subtitleUrl = 'https://www.invidio.us/api/v1/captions/' + videoId + '?label=' + caption.label;
videoHtml = videoHtml + '<track kind="subtitles" src="' + subtitleUrl + '" srclang="' + caption.languageCode + '" label="' + caption.label + '">';
});
playerView.subtitleHtml = videoHtml;
}
const checkSubscription = isSubscribed(playerView.channelId);
checkSubscription.then((results) => {
if (results === false) {
if (subscribeButton != null) {
playerView.subscribedText = 'SUBSCRIBE';
}
} else {
if (subscribeButton != null) {
playerView.subscribedText = 'UNSUBSCRIBE';
}
}
});
playerView.recommendedVideoList = [];
data.recommendedVideos.forEach((video) => {
let data = {};
let time = video.lengthSeconds;
let hours = 0;
if (time >= 3600) {
hours = Math.floor(time / 3600);
time = time - hours * 3600;
}
videoHtml = videoHtml + '<track kind="subtitles" src="' + subtitleUrl + '" srclang="' + subtitle.languageCode + '" label="' + subtitle.name.simpleText + '">';
let minutes = Math.floor(time / 60);
let seconds = time - minutes * 60;
if (seconds < 10) {
seconds = '0' + seconds;
}
if (hours > 0) {
data.duration = hours + ":" + minutes + ":" + seconds;
} else {
data.duration = minutes + ":" + seconds;
}
data.id = video.videoId;
data.title = video.title;
data.channelName = video.author;
data.thumbnail = video.videoThumbnails[4].url;
data.viewCount = video.viewCountText;
playerView.recommendedVideoList = playerView.recommendedVideoList.concat(data);
});
if (playlistId != '') {
playerView.playlistSeen = true;
playerView.playlistShowList = true;
playerView.playlistId = playlistId;
playerView.playlistVideoList = [];
invidiousAPI('playlists', playlistId, {}, (data) => {
playerView.playlistTitle = data.title;
playerView.playlistChannelName = data.author;
playerView.playlistChannelId = data.authorId;
playerView.playlistTotal = data.videoCount;
let amountOfPages = Math.ceil(data.videoCount / 100);
for (let i = 1; i <= amountOfPages; i++) {
invidiousAPI('playlists', playlistId, {page: i}, (data) => {
data.videos.forEach((video) => {
let data = {};
if (video.videoId == videoId){
playerView.playlistIndex = video.index + 1;
}
data.title = video.title;
data.videoId = video.videoId;
data.channelName = video.author;
data.index = video.index + 1;
data.thumbnail = video.videoThumbnails[4].url;
playerView.playlistVideoList[video.index] = data;
});
});
}
});
}
}
playerView.subtitleHtml = videoHtml;
}
const checkSubscription = isSubscribed(playerView.channelId);
checkSubscription.then((results) => {
if (results === false) {
if (subscribeButton != null) {
playerView.subscribedText = 'SUBSCRIBE';
else{
playerView.playlistSeen = false;
playerView.playlistShowList = false;
playerView.playlistId = '';
}
} else {
if (subscribeButton != null) {
playerView.subscribedText = 'UNSUBSCRIBE';
loadingView.seen = false;
if (subscriptionView.seen === false && aboutView.seen === false && headerView.seen === false && searchView.seen === false && settingsView.seen === false && popularView.seen === false && savedView.seen === false && historyView.seen === false && channelView.seen === false && channelVideosView.seen === false) {
playerView.seen = true;
} else {
return;
}
}
if (rememberHistory === true){
addToHistory(videoId);
}
window.setTimeout(checkVideoUrls, 5000, playerView.video480p, playerView.video720p, playerView.videoAudio);
});
showVideoRecommendations(videoId);
loadingView.seen = false;
if (subscriptionView.seen === false && aboutView.seen === false && headerView.seen === false && searchView.seen === false && settingsView.seen === false && popularView.seen === false && savedView.seen === false && historyView.seen === false && channelView.seen === false && channelVideosView.seen === false) {
playerView.seen = true;
}
else{
return;
}
addToHistory(videoId);
// Hide subtitles by default
if (typeof(info['subtitles']) !== 'undefined' && Object.keys(info['subtitles']).length > 0) {
let textTracks = $('.videoPlayer').get(0).textTracks;
Object.keys(textTracks).forEach((track) => {
textTracks[track].mode = 'hidden';
});
}
window.setTimeout(checkVideoUrls, 5000, playerView.video480p, playerView.video720p);
});
}
/**
@ -191,98 +251,110 @@ function playVideo(videoId) {
*
* @return {Void}
*/
function openMiniPlayer() {
let lastTime;
let videoHtml;
function openMiniPlayer() {
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;
}
// 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;
// Create a new browser window.
const BrowserWindow = electron.remote.BrowserWindow;
let miniPlayer = new BrowserWindow({
width: 1200,
height: 710
});
let miniPlayer = new BrowserWindow({
width: 1200,
height: 710,
autoHideMenuBar: true
});
// Use the miniPlayer.html template.
$.get('templates/miniPlayer.html', (template) => {
mustache.parse(template);
const rendered = mustache.render(template, {
videoHtml: videoHtml,
videoThumbnail: playerView.thumbnail,
startTime: lastTime,
});
// Render the template to the new browser window.
miniPlayer.loadURL("data:text/html;charset=utf-8," + encodeURI(rendered));
});
}
// Use the miniPlayer.html template.
$.get('templates/miniPlayer.html', (template) => {
mustache.parse(template);
const rendered = mustache.render(template, {
videoHtml: videoHtml,
videoThumbnail: playerView.thumbnail,
startTime: lastTime,
});
// Render the template to the new browser window.
miniPlayer.loadURL("data:text/html;charset=utf-8," + encodeURI(rendered));
});
}
/**
* 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(url, qualityText, isEmbed = false) {
if (videoHtml == '') {
showToast('Video quality type is not available. Unable to change quality.')
function checkVideoSettings() {
let player = document.getElementById('videoPlayer');
if (autoplay) {
player.play();
}
if (enableSubtitles) {
player.textTracks[0].mode = 'showing';
}
if (playerView.firstLoad) {
playerView.firstLoad = false;
switch (defaultQuality) {
case '480':
if (typeof(playerView.video480p) !== 'undefined') {
playerView.videoUrl = playerView.video480p;
playerView.currentQuality = '480p';
}
break;
case '720':
if (typeof(playerView.video720p) !== 'undefined') {
playerView.videoUrl = playerView.video720p;
playerView.currentQuality = '720p';
}
break;
default:
if (typeof(playerView.video720p) !== 'undefined') {
playerView.videoUrl = playerView.video720p;
playerView.currentQuality = '720p';
}
break;
}
player.playbackRate = parseFloat(defaultPlaybackRate);
$('#currentSpeed').html(defaultPlaybackRate);
}
player.volume = currentVolume;
}
function playNextVideo() {
let player = document.getElementById('videoPlayer');
if (player.loop !== false || playerView.playlistSeen === false) {
return;
}
videoHtml = videoHtml.replace(/\&quot\;/g, '"');
if (playerView.playlistShuffle === true) {
let randomVideo = Math.floor(Math.random() * playerView.playlistTotal);
ft.log('HTML Video: ', videoHtml);
ft.log('(Is the video embeded?) isEmbed: ', isEmbed);
loadingView.seen = true;
playVideo(playerView.playlistVideoList[randomVideo].videoId, playerView.playlistId);
return;
}
// The YouTube API creates 2 more iFrames. This is why a boolean value is sent
// with the function.
const embedPlayer = document.getElementsByTagName('IFRAME')[0];
if (playerView.playlistLoop === true && playerView.playlistIndex == playerView.playlistTotal) {
loadingView.seen = true;
playVideo(playerView.playlistVideoList[0].videoId, playerView.playlistId);
return;
}
const html5Player = document.getElementsByClassName('videoPlayer');
ft.log('Embeded Player Element: ', embedPlayer);
ft.log('HTML5 Player Element: ', 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);
}
if (playerView.playlistIndex != playerView.playlistTotal) {
loadingView.seen = true;
playVideo(playerView.playlistVideoList[playerView.playlistIndex].videoId, playerView.playlistId);
return;
}
}
/**
@ -340,3 +412,37 @@ function changeDurationByPercentage(percentage) {
const videoPlayer = $('.videoPlayer').get(0);
videoPlayer.currentTime = videoPlayer.duration * percentage;
}
function changeDuration(seconds) {
const videoPlayer = $('.videoPlayer').get(0);
videoPlayer.currentTime = seconds;
}
function updateVolume(){
let player = document.getElementById('videoPlayer');
currentVolume = player.volume
}
function parseDescription(descriptionText) {
descriptionText = descriptionText.replace(/target\=\"\_blank\"/g, '');
descriptionText = descriptionText.replace(/\/redirect.+?(?=q\=)/g, '');
descriptionText = descriptionText.replace(/q\=/g, '');
descriptionText = descriptionText.replace(/rel\=\"nofollow\snoopener\"/g, '');
descriptionText = descriptionText.replace(/class\=.+?(?=\")./g, '');
descriptionText = descriptionText.replace(/id\=.+?(?=\")./g, '');
descriptionText = descriptionText.replace(/data\-target\-new\-window\=.+?(?=\")./g, '');
descriptionText = descriptionText.replace(/data\-url\=.+?(?=\")./g, '');
descriptionText = descriptionText.replace(/data\-sessionlink\=.+?(?=\")./g, '');
descriptionText = descriptionText.replace(/\&amp\;/g, '&');
descriptionText = descriptionText.replace(/\%3A/g, ':');
descriptionText = descriptionText.replace(/\%2F/g, '/');
descriptionText = descriptionText.replace(/\&v.+?(?=\")/g, '');
descriptionText = descriptionText.replace(/\&redirect\-token.+?(?=\")/g, '');
descriptionText = descriptionText.replace(/\&redir\_token.+?(?=\")/g, '');
descriptionText = descriptionText.replace(/href\=\"http(s)?\:\/\/youtube\.com/g, 'href="freetube://https://youtube.com');
descriptionText = descriptionText.replace(/href\=\"\/watch/g, 'href="freetube://https://youtube.com');
descriptionText = descriptionText.replace(/href\=\"\/results\?search\_query\=/g, 'href="freetube://');
descriptionText = descriptionText.replace(/yt\.www\.watch\.player\.seekTo/g, 'changeDuration');
return descriptionText;
}

102
src/js/playlist.js Normal file
View File

@ -0,0 +1,102 @@
/*
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/>.
*/
function showPlaylist(playlistId) {
hideViews();
loadingView.seen = true;
playlistView.videoList = [];
invidiousAPI('playlists', playlistId, {}, (data) => {
console.log(data);
playlistView.playlistId = playlistId;
playlistView.channelName = data.author;
playlistView.channelId = data.authorId;
playlistView.channelThumbnail = data.authorThumbnails[3].url;
playlistView.title = data.title;
playlistView.videoCount = data.videoCount;
playlistView.viewCount = data.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
playlistView.thumbnail = data.videos[Math.floor((Math.random() * data.videos.length) + 1)].videoThumbnails[0].url;
playlistView.description = data.descriptionHtml;
let dateString = new Date(data.updated * 1000);
dateString.setDate(dateString.getDate() + 1);
playlistView.lastUpdated = dateFormat(dateString, "mmm dS, yyyy");
let amountOfPages = Math.ceil(data.videoCount / 100);
console.log(amountOfPages);
for (let i = 1; i <= amountOfPages; i++) {
invidiousAPI('playlists', playlistId, {page: i}, (data) => {
console.log(data);
data.videos.forEach((video) => {
let videoData = {};
let time = video.lengthSeconds;
let hours = 0;
if (time >= 3600) {
hours = Math.floor(time / 3600);
time = time - hours * 3600;
}
let minutes = Math.floor(time / 60);
let seconds = time - minutes * 60;
if (seconds < 10) {
seconds = '0' + seconds;
}
if (minutes < 10 && hours > 0) {
minutes = '0' + minutes;
}
if (hours > 0) {
videoData.duration = hours + ":" + minutes + ":" + seconds;
} else {
videoData.duration = minutes + ":" + seconds;
}
videoData.id = video.videoId;
videoData.title = video.title;
videoData.channelName = video.author;
videoData.channelId = video.authorId;
videoData.thumbnail = video.videoThumbnails[4].url;
playlistView.videoList[video.index] = videoData;
});
if (playlistView.seen !== false) {
playlistView.seen = false;
playlistView.seen = true;
}
});
loadingView.seen = false;
playlistView.seen = true;
}
});
}
function togglePlaylist() {
if (playerView.playlistShowList !== false) {
playerView.playlistShowList = false;
}
else{
playerView.playlistShowList = true;
}
}

View File

@ -72,19 +72,19 @@ function removeSavedVideo(videoId, string) {
function toggleSavedVideo(videoId) {
event.stopPropagation();
const checkIfSaved = videoIsSaved(videoId);
const checkIfSaved = videoIsSaved(videoId);
checkIfSaved.then((results) => {
if (results === false) {
playerView.savedText = 'FAVORITED';
playerView.savedIconType = 'fas saved';
addSavedVideo(videoId);
} else {
playerView.savedText = 'FAVORITE';
playerView.savedIconType = 'far unsaved';
removeSavedVideo(videoId);
}
});
checkIfSaved.then((results) => {
if (results === false) {
playerView.savedText = 'FAVORITED';
playerView.savedIconType = 'fas saved';
addSavedVideo(videoId);
} else {
playerView.savedText = 'FAVORITE';
playerView.savedIconType = 'far unsaved';
removeSavedVideo(videoId);
}
});
}
/**
@ -109,48 +109,25 @@ function videoIsSaved(videoId) {
}
/**
* Displays a list of the user's saved videos.
*
* @return {Void}
*/
function showSavedVideos(){
//clearMainContainer();
//startLoadingAnimation();
console.log('checking saved videos');
* Displays a list of the user's saved videos.
*
* @return {Void}
*/
function showSavedVideos() {
console.log('checking saved videos');
let videoList = '';
savedView.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;
docs.forEach((video) => {
invidiousAPI('videos', video.videoId, {}, (data) => {
displayVideo(data, 'saved');
});
}
// Call the YouTube API
youtubeAPI('videos', {
part: 'snippet',
id: videoList,
maxResults: 50,
}, (data) => {
// Render the videos to the screen
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
savedView.videoList = [];
loadingView.seen = false;
videoList.items.forEach((video) => {
displayVideo(video, 'saved');
});
});
loadingView.seen = false;
});
});
}

View File

@ -18,9 +18,6 @@ along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
* A file for functions used for settings.
*/
// To any third party devs that fork the project, please be ethical and change the API keys.
const apiKeyBank = ['AIzaSyC9E579nh_qqxg6BH4xIce3k_7a9mT4uQc', 'AIzaSyCKplYT6hZIlm2O9FbWTi1G7rkpsLNTq78', 'AIzaSyAE5xzh5GcA_tEDhXmMFd1pEzrL-W7z51E', 'AIzaSyDoFzqwuO9l386eF6BmNkVapjiTJ93CBy4', 'AIzaSyBljfZFPioB0TRJAj-0LS4tlIKl2iucyY4', 'AIzaSyAiKgR75e3XAznCcb1cj4NUJ5rR_y3uB8E', 'AIzaSyBZL2Ie1masjwbIa74bR2GONF3p518npVU', 'AIzaSyA0CkT2lS1q9HHaFYGNGM4Ycjl1kmRy22s', 'AIzaSyDPy5jq2l1Bgv3-MbpGdZd3W3ik1BMZeDc'];
/**
* Display the settings screen to the user.
*
@ -34,11 +31,6 @@ function updateSettingsView() {
settingsDb.find({}, (err, docs) => {
docs.forEach((setting) => {
switch (setting['_id']) {
case 'apiKey':
if (apiKeyBank.indexOf(setting['value']) == -1) {
settingsView.apiKey = setting['value'];
}
break;
case 'theme':
if (currentTheme == '') {
currentTheme = setting['value'];
@ -58,6 +50,33 @@ function updateSettingsView() {
} else {
settingsView.useTor = false;
}
if (rememberHistory) {
settingsView.history = true;
} else {
settingsView.history = false;
}
if (autoplay) {
settingsView.autoplay = true;
} else {
settingsView.autoplay = false;
}
if (enableSubtitles) {
settingsView.subtitles = true;
} else {
settingsView.subtitles = false;
}
if (checkForUpdates) {
settingsView.updates = true;
} else {
settingsView.updates = false;
}
document.getElementById('qualitySelect').value = defaultQuality;
document.getElementById('rateSelect').value = defaultPlaybackRate;
});
}
@ -68,14 +87,17 @@ function updateSettingsView() {
*/
function checkDefaultSettings() {
// Grab a random API Key.
settingsView.apiKey = apiKeyBank[Math.floor(Math.random() * apiKeyBank.length)];
let newSetting;
let settingDefaults = {
'theme': 'light',
'apiKey': settingsView.apiKey,
'useTor': false
'useTor': false,
'history': true,
'autoplay': true,
'subtitles': false,
'updates': true,
'quality': '720',
'rate': '1',
};
console.log(settingDefaults);
@ -99,17 +121,36 @@ function checkDefaultSettings() {
case 'theme':
setTheme(docs[0]['value']);
break;
case 'apiKey':
if (apiKeyBank.indexOf(docs[0]['value']) == -1) {
settingsView.apiKey = docs[0]['value'];
}
else{
settingsView.apiKey = settingDefaults.apiKey;
}
break;
case 'useTor':
useTor = docs[0]['value'];
break;
case 'history':
rememberHistory = docs[0]['value'];
break;
case 'autoplay':
autoplay = docs[0]['value'];
break;
case 'subtitles':
enableSubtitles = docs[0]['value'];
break;
case 'updates':
checkForUpdates = docs[0]['value'];
if (checkForUpdates) {
updateChecker(options, function (error, update) { // callback function
if (error) throw error;
if (update) { // print some update info if an update is available
confirmFunction(update.name + ' is now available! Would you like to download the update?', openReleasePage);
}
});
}
break;
case 'quality':
defaultQuality = docs[0]['value'];
break;
case 'rate':
defaultPlaybackRate = docs[0]['value'];
break;
default:
break;
}
@ -126,24 +167,27 @@ function checkDefaultSettings() {
function updateSettings() {
let themeSwitch = document.getElementById('themeSwitch').checked;
let torSwitch = document.getElementById('torSwitch').checked;
let key = document.getElementById('api-key').value;
let historySwitch = document.getElementById('historySwitch').checked;
let autoplaySwitch = document.getElementById('autoplaySwitch').checked;
let subtitlesSwitch = document.getElementById('subtitlesSwitch').checked;
let updatesSwitch = document.getElementById('updatesSwitch').checked;
let qualitySelect = document.getElementById('qualitySelect').value;
let rateSelect = document.getElementById('rateSelect').value;
let theme = 'light';
if (apiKeyBank.indexOf(key) == -1 && key !== '') {
settingsView.apiKey = key;
}
else{
settingsView.apiKey = apiKeyBank[Math.floor(Math.random() * apiKeyBank.length)];
}
console.log(themeSwitch);
settingsView.useTor = torSwitch;
settingsView.history = historySwitch;
settingsView.autoplay = autoplaySwitch;
settingsView.subtitles = subtitlesSwitch;
settingsView.updates = updatesSwitch;
rememberHistory = historySwitch;
defaultQuality = qualitySelect;
defaultPlaybackRate = rateSelect;
if (themeSwitch === true) {
theme = 'dark';
}
console.log(theme);
// Update default theme
settingsDb.update({
_id: 'theme'
@ -165,12 +209,71 @@ function updateSettings() {
useTor = torSwitch;
});
// To any third party devs that fork the project, please be ethical and change the API key.
// Update history
settingsDb.update({
_id: 'apiKey'
_id: 'history'
}, {
value: settingsView.apiKey
}, {});
value: historySwitch
}, {}, function(err, numReplaced) {
console.log(err);
console.log(numReplaced);
rememberHistory = historySwitch;
});
// Update autoplay.
settingsDb.update({
_id: 'autoplay'
}, {
value: autoplaySwitch
}, {}, function(err, numReplaced) {
console.log(err);
console.log(numReplaced);
autoplay = autoplaySwitch;
});
// Update subtitles.
settingsDb.update({
_id: 'subtitles'
}, {
value: subtitlesSwitch
}, {}, function(err, numReplaced) {
console.log(err);
console.log(numReplaced);
enableSubtitles = subtitlesSwitch;
});
// Update checkForUpdates.
settingsDb.update({
_id: 'updates'
}, {
value: updatesSwitch
}, {}, function(err, numReplaced) {
console.log(err);
console.log(numReplaced);
checkForUpdates = updatesSwitch;
});
// Update default quality.
settingsDb.update({
_id: 'quality'
}, {
value: qualitySelect
}, {}, function(err, numReplaced) {
console.log(err);
console.log(numReplaced);
defaultQuality = qualitySelect;
});
// Update default playback rate.
settingsDb.update({
_id: 'rate'
}, {
value: rateSelect
}, {}, function(err, numReplaced) {
console.log(err);
console.log(numReplaced);
defaultPlaybackRate = rateSelect;
});
showToast('Settings have been saved.');
}

View File

@ -21,8 +21,8 @@
* File for all functions related to subscriptions.
*/
let subscriptionTimer;
let checkSubscriptions = true;
let subscriptionTimer;
let checkSubscriptions = true;
/**
* Add a channel to the user's subscription database.
@ -33,17 +33,13 @@
*/
function addSubscription(channelId, useToast = true) {
ft.log('Channel ID: ', channelId);
// Request YouTube API
youtubeAPI('channels', {
part: 'snippet',
id: channelId,
}, (data) => {
const channelInfo = data['items'][0]['snippet'];
const channelName = channelInfo['title'];
const thumbnail = channelInfo['thumbnails']['high']['url'];
invidiousAPI('channels', channelId, {}, (data) => {
const channelName = data.author;
const thumbnail = data.authorThumbnails[3].url;
const channel = {
channelId: channelId,
channelId: data.authorId,
channelName: channelName,
channelThumbnail: thumbnail,
};
@ -80,103 +76,75 @@ function removeSubscription(channelId) {
*
* @return {Void}
*/
function loadSubscriptions() {
if (checkSubscriptions === false && subscriptionView.videoList.length > 0){
console.log('Will not load subscriptions. Timer still on.');
loadingView.seen = false;
return;
}
else{
showToast('Refreshing Subscription List. Please wait...');
checkSubscriptions = false;
}
function loadSubscriptions() {
if (checkSubscriptions === false && subscriptionView.videoList.length > 0) {
console.log('Will not load subscriptions. Timer still on.');
loadingView.seen = false;
return;
} else {
showToast('Refreshing Subscription List. Please wait...');
checkSubscriptions = false;
progressView.seen = true;
}
let videoList = [];
let videoList = [];
const subscriptions = returnSubscriptions();
const subscriptions = returnSubscriptions();
subscriptions.then((results) => {
let channelId = '';
let videoList = [];
subscriptions.then((results) => {
let channelId = '';
let videoList = [];
if (results.length > 0) {
let counter = 0;
if (results.length > 0) {
let counter = 0;
for (let i = 0; i < results.length; i++) {
channelId = results[i]['channelId'];
for (let i = 0; i < results.length; i++) {
channelId = results[i]['channelId'];
youtubeAPI('search', {
part: 'snippet',
channelId: channelId,
type: 'video',
maxResults: 15,
order: 'date',
}, (data) => {
console.log(data);
videoList = videoList.concat(data.items);
counter++;
progressView.progressWidth = (counter / results.length) * 100;
if (counter === results.length) {
videoList.sort((a, b) => {
const date1 = Date.parse(a.snippet.publishedAt);
const date2 = Date.parse(b.snippet.publishedAt);
invidiousAPI('channels/videos', channelId, {}, (data) => {
console.log(data);
videoList = videoList.concat(data);
counter = counter + 1;
progressView.progressWidth = (counter / results.length) * 100;
return date2.valueOf() - date1.valueOf();
});
if (counter === results.length) {
videoList.sort((a, b) => {
return b.published - a.published;
});
// 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 < 50) {
let grabDuration = getDuration(videoList.slice(0, 49));
subscriptionView.videoList = [];
console.log(videoList);
grabDuration.then((list) => {
subscriptionView.videoList = [];
list.items.forEach((video) => {
displayVideo(video, 'subscriptions');
});
loadingView.seen = false;
progressView.seen = false;
progressView.progressWidth = 0;
});
} else {
console.log(videoList);
let finishedList = [];
let firstBatchDuration = getDuration(videoList.slice(0, 49));
if (videoList.length > 100) {
for (let i = 0; i < 100; i++) {
displayVideo(videoList[i], 'subscriptions');
}
} else {
videoList.forEach((video) => {
displayVideo(video, 'subscriptions');
});
}
firstBatchDuration.then((list1) => {
finishedList = finishedList.concat(list1.items);
let secondBatchDuration = getDuration(videoList.slice(50, 99));
loadingView.seen = false;
progressView.seen = false;
progressView.progressWidth = 0;
secondBatchDuration.then((list2) => {
finishedList = finishedList.concat(list2.items);
console.log(finishedList);
subscriptionView.videoList = [];
finishedList.forEach((video) => {
displayVideo(video, 'subscriptions');
});
loadingView.seen = false;
progressView.seen = false;
progressView.progressWidth = 0;
subscriptionTimer = window.setTimeout(() => {
checkSubscriptions = true;
}, 60000);
});
});
}
}
}
);
}
subscriptionTimer = window.setTimeout(() => {
checkSubscriptions = true;
}, 60000);
} else {
// User has no subscriptions. Display message.
loadingView.seen = false;
headerView.seen = false;
noSubscriptions.seen = true;
}
});
}
console.log('Done');
}
});
}
} else {
// User has no subscriptions. Display message.
loadingView.seen = false;
headerView.seen = false;
noSubscriptions.seen = true;
}
});
}
/**
* Get the list of subscriptions from the user's subscription database.

View File

@ -21,10 +21,10 @@ const mainHeaderTemplate = require('./templates/mainHeader.html');
const aboutTemplate = require('./templates/about.html');
const settingsTemplate = require('./templates/settings.html');
const videoListTemplate = require('./templates/videoTemplate.html');
const nextPageTemplate = require('./templates/searchNextPage.html');
const playerTemplate = require('./templates/player.html');
const channelTemplate = require('./templates/channelView.html');
const progressViewTemplate = require('./templates/progressView.html');
const playlistViewTemplate = require('./templates/playlistView.html');
/*
* Progress view
@ -83,6 +83,19 @@ let sideNavBar = new Vue({
popularView.seen = true;
showMostPopular();
},
trending: (event) => {
hideViews();
if (loadingView.seen !== false){
loadingView.seen = false;
}
if(trendingView.videoList.length === 0){
loadingView.seen = true;
}
headerView.seen = true;
headerView.title = 'Trending';
trendingView.seen = true;
showTrending();
},
saved: (event) => {
hideViews();
if (loadingView.seen !== false){
@ -190,6 +203,33 @@ let popularView = new Vue({
template: videoListTemplate
});
let trendingView = new Vue({
el: '#trendingView',
data: {
seen: false,
isSearch: false,
videoList: []
},
methods: {
play: (videoId) => {
loadingView.seen = true;
playVideo(videoId);
},
channel: (channelId) => {
goToChannel(channelId);
},
toggleSave: (videoId) => {
addSavedVideo(videoId);
},
copy: (site, videoId) => {
const url = 'https://' + site + '/watch?v=' + videoId;
clipboard.writeText(url);
showToast('URL has been copied to the clipboard');
}
},
template: videoListTemplate
});
let savedView = new Vue({
el: '#savedView',
data: {
@ -244,6 +284,41 @@ let historyView = new Vue({
template: videoListTemplate
});
let playlistView = new Vue({
el: '#playlistView',
data: {
seen: false,
playlistId: '',
channelName: '',
channelId: '',
thumbnail: '',
title: '',
videoCount: '',
viewCount: '',
description: '',
lastUpdated: '',
videoList: []
},
methods: {
play: (videoId) => {
loadingView.seen = true;
playVideo(videoId, playlistView.playlistId);
},
channel: (channelId) => {
goToChannel(channelId);
},
toggleSave: (videoId) => {
addSavedVideo(videoId);
},
copy: (site, videoId) => {
const url = 'https://' + site + '/watch?v=' + videoId;
clipboard.writeText(url);
showToast('URL has been copied to the clipboard');
}
},
template: playlistViewTemplate
});
let aboutView = new Vue({
el: '#aboutView',
data: {
@ -259,7 +334,11 @@ let settingsView = new Vue({
seen: false,
useTheme: false,
useTor: false,
apiKey: ''
apiKey: '',
history: true,
autoplay: true,
subtitles: false,
updates: true,
},
template: settingsTemplate
});
@ -269,7 +348,7 @@ let searchView = new Vue({
data: {
seen: false,
isSearch: true,
nextPageToken: '',
page: 1,
videoList: []
},
methods: {
@ -288,10 +367,13 @@ let searchView = new Vue({
clipboard.writeText(url);
showToast('URL has been copied to the clipboard');
},
nextPage: (nextPageToken) => {
console.log(searchView.nextPageToken);
search(searchView.nextPageToken);
}
nextPage: () => {
console.log(searchView.page);
search(searchView.page);
},
playlist: (playlistId) => {
showPlaylist(playlistId);
},
},
template: videoListTemplate
});
@ -320,7 +402,9 @@ let channelVideosView = new Vue({
el: '#channelVideosView',
data: {
seen: false,
isSearch: false,
channelId: '',
isSearch: true,
page: 2,
videoList: []
},
methods: {
@ -334,6 +418,9 @@ let channelVideosView = new Vue({
toggleSave: (videoId) => {
addSavedVideo(videoId);
},
nextPage: () => {
channelNextPage();
},
copy: (site, videoId) => {
const url = 'https://' + site + '/watch?v=' + videoId;
clipboard.writeText(url);
@ -347,6 +434,8 @@ let playerView = new Vue({
el: '#playerView',
data: {
seen: false,
playlistSeen: false,
firstLoad: true,
publishedDate: '',
videoUrl: '',
videoId: '',
@ -360,8 +449,12 @@ let playerView = new Vue({
videoThumbnail: '',
subtitleHtml: '',
currentQuality: '',
videoAudio: '',
validAudio: false,
video480p: '',
valid480p: false,
video720p: '',
valid720p: false,
embededHtml: '',
currentSpeed: 1,
videoTitle: '',
@ -370,7 +463,15 @@ let playerView = new Vue({
videoLikes: 0,
videoDislikes: 0,
playerSeen: true,
recommendedVideoList: []
playlistTitle: '',
playlistChannelName: '',
playlistIndex: 1,
playlistTotal: 1,
playlistLoop: false,
playlistShuffle: false,
playlistShowList: true,
recommendedVideoList: [],
playlistVideoList: [],
},
methods: {
channel: (channelId) => {
@ -408,10 +509,45 @@ let playerView = new Vue({
save: (videoId) => {
toggleSavedVideo(videoId);
},
play: (videoId) => {
play: (videoId, playlistId = '') => {
loadingView.seen = true;
playVideo(videoId);
}
playVideo(videoId, playlistId);
},
loop: () => {
let player = document.getElementById('videoPlayer');
if (player.loop === false) {
player.loop = true;
showToast('Video loop has been turned on.');
}
else{
player.loop = false;
showToast('Video loop has been turned off.')
}
},
playlist: (playlistId) => {
showPlaylist(playlistId);
},
playlistLoopToggle: () => {
if (playerView.playlistLoop !== false) {
showToast('Playlist will no longer loop');
playerView.playlistLoop = false;
}
else {
showToast('Playlist will now loop');
playerView.playlistLoop = true;
}
},
playlistShuffleToggle: () => {
if (playerView.playlistShuffle !== false) {
showToast('Playlist will no longer shuffle');
playerView.playlistShuffle = false;
}
else{
showToast('Playlist will now shuffle');
playerView.playlistShuffle = true;
}
},
},
template: playerTemplate
});
@ -424,8 +560,10 @@ function hideViews(){
searchView.seen = false;
settingsView.seen = false;
popularView.seen = false;
trendingView.seen = false;
savedView.seen = false;
historyView.seen = false;
playlistView.seen = false;
playerView.seen = false;
channelView.seen = false;
channelVideosView.seen = false;

View File

@ -23,36 +23,14 @@
const updateChecker = require('github-version-checker');
const options = {
token: 'USERACCESSTOKEN', // personal access token. Github will not allow commiting the access token, which is why this is blank.
repo: 'freetube', // repository name
owner: 'freetubeapp', // repository owner
currentVersion: require('electron').remote.app.getVersion(), // your app's current version
fetchTags: false // whether to fetch releases or tags
};
const options = {
token: 'PUTUSERTOKENHERE', // personal access token. Github will not allow commiting the access token, which is why this is blank.
repo: 'freetube', // repository name
owner: 'freetubeapp', // repository owner
currentVersion: require('electron').remote.app.getVersion(), // your app's current version
fetchTags: false // whether to fetch releases or tags
};
const openReleasePage = function () {
shell.openExternal('https://github.com/FreeTubeApp/FreeTube/releases');
}
/*function checkForUpdates() {
updateChecker(options, function(error, update) { // callback function
if (error){
showToast('There was a problem with checking for updates');
freeTubeLog(error);
}
if (update) { // print some update info if an update is available
confirmFunction(update.name + ' is now available! Would you like to download the update?', openReleasePage);
}
else{
showToast('No update is currently available.');
}
});
}*/
updateChecker(options, function (error, update) { // callback function
if (error) throw error;
if (update) { // print some update info if an update is available
confirmFunction(update.name + ' is now available! Would you like to download the update?', openReleasePage);
}
});

View File

@ -15,119 +15,63 @@
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
*/
let popularTimer;
let checkPopular = true;
let trendingTimer;
let checkTrending = true;
/**
* 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.
* @param {string} page - Optional: The page token to be inlcuded in the search.
*
* @return {Void}
*/
function search(nextPageToken = '') {
function search(page = 1) {
const query = document.getElementById('search').value;
if (query === '') {
return;
}
if (nextPageToken === '') {
hideViews();
headerView.seen = true;
headerView.title = 'Search Results';
searchView.videoList = [];
searchView.seen = true;
} else {
console.log(nextPageToken);
showToast('Fetching results. Please wait...');
}
if (page === 1) {
hideViews();
headerView.seen = true;
headerView.title = 'Search Results';
searchView.videoList = [];
searchView.seen = true;
} else {
console.log(page);
showToast('Fetching results. Please wait...');
}
youtubeAPI('search', {
invidiousAPI('search', '', {
q: query,
part: 'id',
pageToken: nextPageToken,
maxResults: 25,
page: page,
type: 'all',
}, function (data) {
ft.log('Search Data: ', data);
console.log(data);
let channels = data.items.filter((item) => {
if (item.id.kind === 'youtube#channel') {
return true;
}
data.forEach((video) => {
switch (video.type) {
case 'video':
displayVideo(video, 'search');
break;
case 'channel':
displayChannel(video);
break;
case 'playlist':
if (video.videoCount > 0) {
displayPlaylist(video);
}
break;
default:
}
});
let playlists = data.items.filter((item) => {
if (item.id.kind === 'youtube#playlist') {
return true;
}
});
let videos = data.items.filter((item) => {
if (item.id.kind === 'youtube#video') {
return true;
}
});
ft.log('Channels: ', channels);
ft.log('Typeof object above (channels) ^^', typeof (channels));
ft.log('Playlists', playlists);
if (playlists.length > 0) {
//displayPlaylists(playlists);
}
if (channels.length > 0) {
displayChannels(channels);
}
let grabDuration = getDuration(videos);
grabDuration.then((videoList) => {
console.log(videoList);
videoList.items.forEach((video) => {
displayVideo(video, 'search');
});
});
searchView.nextPageToken = data.nextPageToken;
loadingView.seen = false;
})
}
/**
* Grab the duration of the videos
*
* @param {array} data - An array of videos to get the duration from
*
* @return {promise} - The list of videos with the duration included.
*/
function getDuration(data) {
return new Promise((resolve, reject) => {
let videoIdList = '';
for (let i = 0; i < data.length; i++) {
if (videoIdList === '') {
if (typeof (data[i]['id']) === 'string') {
videoIdList = data[i]['id'];
} else {
videoIdList = data[i]['id']['videoId'];
}
} else {
if (typeof (data[i]['id']) === 'string') {
videoIdList = videoIdList + ', ' + data[i]['id'];
} else {
videoIdList = videoIdList + ', ' + data[i]['id']['videoId'];
}
}
}
youtubeAPI('videos', {
part: 'snippet, contentDetails',
id: videoIdList
}, (data) => {
resolve(data);
});
});
searchView.page = searchView.page + 1;
loadingView.seen = false;
})
}
/**
@ -140,211 +84,159 @@ function getDuration(data) {
* @return {Void}
*/
function displayVideo(videoData, listType = '') {
let video = {};
const videoSnippet = videoData.snippet;
video.duration = parseVideoDuration(videoData.contentDetails.duration);
// Grab the published date for the video and convert to a user readable state.
const dateString = new Date(videoSnippet.publishedAt);
video.publishedDate = dateFormat(dateString, "mmm dS, yyyy");
const searchMenu = $('#videoListContainer').html();
// Include a remove icon in the list if the application is displaying the history list or saved videos.
video.deleteHtml = () => {
switch (listType) {
case 'saved':
return `<li onclick="removeSavedVideo('${videoId}'); showSavedVideos();">Remove Saved Video</li>`;
case 'history':
return `<li onclick="removeFromHistory('${videoId}'); showHistory();">Remove From History</li>`;
}
};
video.id = videoData.id;
video.youtubeUrl = 'https://youtube.com/watch?v=' + video.id;
video.invidiousUrl = 'https://invidio.us/watch?v=' + video.id;
// Includes text if the video is live.
video.liveText = (videoSnippet.liveBroadcastContent === 'live') ? 'LIVE NOW' : '';
video.thumbnail = videoSnippet.thumbnails.medium.url;
video.title = videoSnippet.title;
video.channelName = videoSnippet.channelTitle;
video.channelId = videoSnippet.channelId;
video.description = videoSnippet.description;
video.isVideo = true;
switch (listType) {
case 'subscriptions':
subscriptionView.videoList = subscriptionView.videoList.concat(video);
video.removeFromSave = true;
break;
case 'search':
searchView.videoList = searchView.videoList.concat(video);
video.removeFromSave = false;
break;
case 'popular':
popularView.videoList = popularView.videoList.concat(video);
video.removeFromSave = false;
break;
case 'saved':
savedView.videoList = savedView.videoList.concat(video);
video.removeFromSave = false;
break;
case 'history':
historyView.videoList = historyView.videoList.concat(video);
video.removeFromSave = false;
break;
case 'channel':
channelVideosView.videoList = channelVideosView.videoList.concat(video);
video.removeFromSave = false;
break;
}
}
function displayChannels(channels) {
let channelIds;
channels.forEach((channel) => {
if (typeof (channelIds) === 'undefined') {
channelIds = channel.id.channelId;
} else {
channelIds = channelIds + ',' + channel.id.channelId;
}
});
ft.log('Channel IDs: ', channelIds);
youtubeAPI('channels', {
part: 'snippet,statistics',
id: channelIds,
}, function (data) {
ft.log('Channel Data: ', data);
let items = data['items'].reverse();
ft.log('Channel Items: ', items);
items.forEach((item) => {
let channelData = {};
channelData.channelId = item.id;
channelData.thumbnail = item.snippet.thumbnails.medium.url;
channelData.channelName = item.snippet.title;
channelData.description = item.snippet.description;
channelData.subscriberCount = item.statistics.subscriberCount;
channelData.videoCount = item.statistics.videoCount;
channelData.isVideo = false;
console.log(searchView.videoList);
console.log(channelData);
searchView.videoList = searchView.videoList.concat(channelData);
});
});
}
function displayPlaylists(playlists) {
let playlistIds;
playlists.forEach((playlist) => {
if (typeof (playlistIds) === 'undefined') {
playlistIds = playlist.id.playlistId;
} else {
playlistIds = playlistIds + ',' + playlist.id.playlistId;
}
});
ft.log('Playlist IDs: ', playlistIds);
youtubeAPI('playlists', {
part: 'snippet,contentDetails',
id: playlistIds,
}, function (data) {
ft.log('Playlist Data: ', data);
let items = data['items'].reverse();
const playlistListTemplate = require('./templates/playlistList.html');
ft.log('Playlist Items: ', items);
items.forEach((item) => {
let dateString = new Date(item.snippet.publishedAt);
let publishedDate = dateFormat(dateString, "mmm dS, yyyy");
mustache.parse(playlistListTemplate);
let rendered = mustache.render(playlistListTemplate, {
channelId: item.snippet.channelId,
channelName: item.snippet.channelTitle,
playlistThumbnail: item.snippet.thumbnails.medium.url,
playlistTitle: item.snippet.title,
playlistDescription: item.snippet.description,
videoCount: item.contentDetails.itemCount,
publishedDate: publishedDate,
});
$(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);
if (videoData.paid) {
return;
}
// Update the on click method of the button.
$(document).off('click', '#getNextPage');
$(document).on('click', '#getNextPage', (event) => {
search(nextPageToken);
let video = {};
video.id = videoData.videoId;
if (videoData.type == 'playlist') {
video.isPlaylist = true;
}
historyDb.find({
videoId: video.id
}, (err, docs) => {
if (jQuery.isEmptyObject(docs)) {
// Do nothing
} else {
video.watched = true;
}
video.views = videoData.viewCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
if (videoData.liveNow === true){
video.liveText = (videoData.liveNow === true) ? 'LIVE NOW' : '';
video.duration = '';
video.publishedDate = '';
video.viewText = 'watching';
}
else{
video.liveText = '';
if (video.views <= 1) {
video.viewText = 'view';
}
else{
video.viewText = 'views';
}
let time = videoData.lengthSeconds;
let hours = 0;
if (time >= 3600) {
hours = Math.floor(time / 3600);
time = time - hours * 3600;
}
let minutes = Math.floor(time / 60);
let seconds = time - minutes * 60;
if (seconds < 10) {
seconds = '0' + seconds;
}
if (minutes < 10 && hours > 0) {
minutes = '0' + minutes;
}
if (hours > 0) {
video.duration = hours + ":" + minutes + ":" + seconds;
} else {
video.duration = minutes + ":" + seconds;
}
video.publishedDate = videoData.publishedText;
}
//const searchMenu = $('#videoListContainer').html();
// Include a remove icon in the list if the application is displaying the history list or saved videos.
video.deleteHtml = () => {
switch (listType) {
case 'saved':
return `<li onclick="removeSavedVideo('${video.id}'); showSavedVideos();">Remove Saved Video</li>`;
case 'history':
return `<li onclick="removeFromHistory('${video.id}'); showHistory();">Remove From History</li>`;
}
};
video.youtubeUrl = 'https://youtube.com/watch?v=' + video.id;
video.invidiousUrl = 'https://invidio.us/watch?v=' + video.id;
video.thumbnail = videoData.videoThumbnails[4].url;
video.title = videoData.title;
video.channelName = videoData.author;
video.channelId = videoData.authorId;
video.description = videoData.description;
video.isVideo = true;
switch (listType) {
case 'subscriptions':
subscriptionView.videoList = subscriptionView.videoList.concat(video);
video.removeFromSave = true;
break;
case 'search':
searchView.videoList = searchView.videoList.concat(video);
video.removeFromSave = false;
break;
case 'popular':
popularView.videoList = popularView.videoList.concat(video);
video.removeFromSave = false;
break;
case 'trending':
trendingView.videoList = trendingView.videoList.concat(video);
video.removeFromSave = false
break;
case 'saved':
savedView.videoList = savedView.videoList.concat(video);
video.removeFromSave = false;
break;
case 'history':
historyView.videoList = historyView.videoList.concat(video);
video.removeFromSave = false;
break;
case 'channel':
channelVideosView.videoList = channelVideosView.videoList.concat(video);
video.removeFromSave = false;
break;
}
});
}
/**
* 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) {
playerView.recommendedVideoList = [];
function displayChannel(channel) {
let channelData = {};
youtubeAPI('search', {
part: 'id',
type: 'video',
relatedToVideoId: videoId,
maxResults: 15,
}, function(data) {
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
videoList.items.forEach((video) => {
let data = {}
const snippet = video.snippet;
channelData.channelId = channel.authorId;
channelData.thumbnail = channel.authorThumbnails[4].url;
channelData.channelName = channel.author;
channelData.description = channel.description;
channelData.subscriberCount = channel.subCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
channelData.videoCount = channel.videoCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
channelData.isVideo = false;
data.duration = parseVideoDuration(video.contentDetails.duration);
data.id = video.id;
data.title = snippet.title;
data.channelName = snippet.channelTitle;
data.thumbnail = snippet.thumbnails.medium.url;
data.publishedDate = dateFormat(snippet.publishedAt, "mmm dS, yyyy");
searchView.videoList = searchView.videoList.concat(channelData);
}
playerView.recommendedVideoList = playerView.recommendedVideoList.concat(data);
});
});
});
function displayPlaylist(playlist) {
let playListData = {};
playListData.isPlaylist = true;
playListData.isVideo = false;
playListData.thumbnail = playlist.videos[0].videoThumbnails[4].url;
playListData.channelName = playlist.author;
playListData.channelId = playlist.authorId;
playListData.id = playlist.playlistId;
playListData.description = playlist.videos[0].title + "\r\n" + playlist.videos[1].title;
playListData.title = playlist.title;
playListData.videoCount = playlist.videoCount;
if (playListData.channelName == 'YouTube' && playListData.title.includes('Mix')){
// Hide Mix playlists.
return;
}
searchView.videoList = searchView.videoList.concat(playListData);
}
/**
@ -359,7 +251,7 @@ function parseSearchText(url = '') {
if (url === '') {
input = document.getElementById('search').value;
} else {
input = url;
input = url.replace(/freetube\:\/\//, '');
}
if (input === '') {
@ -383,69 +275,15 @@ function parseSearchText(url = '') {
goToChannel(urlSplit[4]);
} else if (urlSplit[3] == 'user') {
ft.log('user found');
// call api to get the ID and then call goToChannel(id)
youtubeAPI('channels', {
part: 'id',
forUsername: urlSplit[4]
}, (data) => {
ft.log('Channel Data: ', data.items[0].id);
let channelID = data.items[0].id;
loadingView.seen = true;
goToChannel(channelID);
});
loadingView.seen = true;
goToChannel(urlSplit[4]);
} else {
ft.log('Video not found');
document.getElementById('search').value = decodeURIComponent(input);
loadingView.seen = true;
search();
}
}
/**
* Convert duration into a more readable format
*
* @param {string} durationString - The string containing the video duration. Formated as 'PT12H34M56S'
*
* @return {string} - The formated string. Ex: 12:34:56
*/
function parseVideoDuration(durationString) {
let match = durationString.match(/P.*T(\d+H)?(\d+M)?(\d+S)?/);
let duration = '';
match = match.slice(1).map(function (x) {
if (x != null) {
return x.replace(/\D/, '');
}
});
let hours = (parseInt(match[0]) || 0);
let minutes = (parseInt(match[1]) || 0);
let seconds = (parseInt(match[2]) || 0);
if (hours != 0) {
duration = hours + ':';
} else {
duration = minutes + ':';
}
if (hours != 0 && minutes < 10) {
duration = duration + '0' + minutes + ':';
} else if (hours != 0 && minutes > 10) {
duration = duration + minutes + ':';
} else if (hours != 0 && minutes == 0) {
duration = duration + '00:';
}
if (seconds == 0) {
duration = duration + '00';
} else if (seconds < 10) {
duration = duration + '0' + seconds;
} else {
duration = duration + seconds;
}
return duration;
}
/**
@ -454,35 +292,58 @@ function parseVideoDuration(durationString) {
* @return {Void}
*/
function showMostPopular() {
// Get the date of 2 days ago.
var d = new Date();
d.setDate(d.getDate() - 2);
// 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.
youtubeAPI('search', {
part: 'id',
order: 'viewCount',
type: 'video',
publishedAfter: d.toISOString(),
maxResults: 50,
}, function(data) {
//createVideoListContainer('Most Popular:');
console.log(data);
let grabDuration = getDuration(data.items);
grabDuration.then((videoList) => {
console.log(videoList);
popularView.videoList = [];
if (checkPopular === false && popularView.videoList.length > 0) {
console.log('Will not load popular. Timer still on.');
loadingView.seen = false;
videoList.items.forEach((video) => {
displayVideo(video, 'popular');
});
return;
} else {
checkPopular = false;
}
invidiousAPI('top', '', {}, function (data) {
console.log(data);
popularView.videoList = [];
data.forEach((video) => {
loadingView.seen = false;
console.log(video);
displayVideo(video, 'popular');
});
});
});
popularTimer = window.setTimeout(() => {
checkPopular = true;
}, 60000);
}
/**
* Grab trending videos over the last couple of days and display them.
*
* @return {Void}
*/
function showTrending() {
if (checkTrending === false && trendingView.videoList.length > 0) {
console.log('Will not load trending. Timer still on.');
loadingView.seen = false;
return;
} else {
checkTrending = false;
}
invidiousAPI('trending', '', {}, function (data) {
console.log(data);
popularView.videoList = [];
data.forEach((video) => {
loadingView.seen = false;
console.log(video);
displayVideo(video, 'trending');
});
});
trendingTimer = window.setTimeout(() => {
checkTrending = true;
}, 60000);
}
/**
@ -511,30 +372,6 @@ function copyLink(website, videoId) {
}
/**
* Get the YouTube embeded player of a video as well as channel information..
*
* @param {string} videoId - The video ID of the video to get.
*
* @return {promise} - The HTML of the embeded player
*/
function getChannelAndPlayer(videoId) {
ft.log('Video ID: ', videoId);
return new Promise((resolve, reject) => {
youtubeAPI('videos', {
part: 'snippet,player',
id: videoId,
}, function (data) {
let embedHtml = data.items[0].player.embedHtml;
embedHtml = embedHtml.replace('src="', 'src="https:');
embedHtml = embedHtml.replace('width="480px"', '');
embedHtml = embedHtml.replace('height="270px"', '');
embedHtml = embedHtml.replace(/\"/g, '&quot;');
resolve([embedHtml, data.items[0].snippet.channelId]);
});
});
}
/**
* 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
@ -543,42 +380,61 @@ function getChannelAndPlayer(videoId) {
* @param {string} video480p - The URL to the 480p video.
* @param {string} video720p - The URL to the 720p video.
*/
function checkVideoUrls(video480p, video720p) {
function checkVideoUrls(video480p, video720p, videoAudio) {
const currentQuality = $('#currentQuality').html();
let buttonEmbed = document.getElementById('qualityEmbed');
let valid480 = false;
if (typeof (videoAudio) !== 'undefined') {
let getAudioUrl = fetch(videoAudio);
getAudioUrl.then((status) => {
switch (status.status) {
case 404:
playerView.validAudio = false;
break;
case 403:
showToast('This video is unavailable in your country.');
playerView.validAudio = false;
return;
break;
default:
ft.log('Audio is valid');
break;
}
});
}
else{
playerView.validAudio = 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('');
});
playerView.valid480p = false;
buttonEmbed.click();
return;
break;
case 403:
showToast('This video is unavailable in your country.');
$(document).off('click', '#quality480p');
$(document).on('click', '#quality480p', (event) => {
changeQuality('');
});
playerView.valid480p = false;
return;
break;
default:
ft.log('480p is valid');
if (currentQuality === '720p' && typeof (video720p) === 'undefined') {
changeQuality(video480p);
playerView.currentQuality = '480p';
}
break;
}
});
}
else{
playerView.valid480p = false;
}
if (typeof (video720p) !== 'undefined') {
let get720pUrl = fetch(video720p);
@ -586,20 +442,14 @@ function checkVideoUrls(video480p, video720p) {
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('');
});
playerView.valid720p = false;
if (typeof (valid480) !== 'undefined') {
changeQuality(video480p, '480p');
playerView.currentQuality = '480p';
}
break;
case 403:
showToast('This video is unavailable in your country.');
$(document).off('click', '#quality720p');
$(document).on('click', '#quality720p', (event) => {
changeQuality('');
});
playerView.valid720p = false;
return;
break;
default:
@ -608,4 +458,7 @@ function checkVideoUrls(video480p, video720p) {
}
});
}
else{
playerView.valid720p = false;
}
}

View File

@ -15,10 +15,8 @@
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* List a YouTube HTTP API resource.
* List an Invidious HTTP API resource.
*
* @param {string} resource - The path of the resource.
* @param {object} params - The API parameters.
@ -27,8 +25,8 @@
* @return {Void}
*/
function youtubeAPI(resource, params, success) {
params.key = settingsView.apiKey;
function invidiousAPI(resource, id, params, success) {
let requestUrl = 'https://www.invidio.us/api/v1/' + resource + '/' + id + '?' + $.param(params);
let requestUrl = 'https://www.googleapis.com/youtube/v3/' + resource + '?' + $.param(params);
@ -49,15 +47,14 @@ function youtubeAPI(resource, params, success) {
requestUrl,
success
).fail((xhr, textStatus, error) => {
showToast('There was an error calling the YouTube API.');
showToast('There was an error calling the Invidious API.');
console.log(error);
console.log(xhr);
console.log(textStatus);
console.log(requestUrl);
loadingView.seen = false;
});
}
}
/**

View File

@ -43,6 +43,27 @@ input[type=text] {
background-color: #f44336;
}
.select-text {
border-bottom: 1px solid #E0E0E0;
color: white;
}
.select-text option {
color: black;
}
.select-text:focus {
border-bottom: 1px solid #EEEEEE;
}
.select:after {
border-top: 6px solid #E0E0E0;
}
.select-label {
color: #F5F5F5;
}
.searchBar ::-webkit-input-placeholder {
color: #E0E0E0;
}
@ -246,3 +267,7 @@ input[type=text] {
#comments {
background-color: #424242;
}
#miniPL {
background-color: #424242;
}

View File

@ -39,6 +39,22 @@ body {
background-color: #f44336;
}
.select-text {
border-bottom: 1px solid rgba(0,0,0, 0.12);
}
.select-text:focus {
border-bottom: 1px solid rgba(0,0,0, 0);
}
.select:after {
border-top: 6px solid rgba(0, 0, 0, 0.12);
}
.select-label {
color: rgba(0,0,0, 0.26);
}
.searchBar ::-webkit-input-placeholder {
color: #ddd;
}
@ -195,3 +211,7 @@ body {
#comments {
background: #eee;
}
#miniPL {
background-color: white;
}

View File

@ -46,7 +46,7 @@ a {
height: 3px;
background-color: #f44336;
display: block;
position: absolute;
position: fixed;
bottom: 0;
left: 0;
z-index: 1;

View File

@ -29,7 +29,7 @@ iframe {
width: 95%;
max-width: 1000px;
height: auto;
padding: 15px;
padding: 10px;
overflow: hidden;
}
@ -41,7 +41,7 @@ iframe {
}
.videoThumbnail img {
width: 100%;
width: 100%;
}
.videoThumbnail i {
@ -50,6 +50,9 @@ iframe {
padding: 6px;
opacity: 0.7;
position: relative;
}
.videoSave {
bottom: 159px;
left: 247px;
}
@ -117,6 +120,7 @@ iframe {
font-weight: bold;
margin-left: 285px;
margin-top: 5px;
margin-bottom: -10px;
cursor: pointer;
}
@ -126,11 +130,18 @@ iframe {
cursor: pointer;
}
.videoViews {
margin-left: 285px;
font-size: 13px;
cursor: pointer;
}
.videoDescription {
margin-left: 285px;
font-size: 13px;
cursor: pointer;
height: 45px;
margin-bottom: -60px;
height: 60px;
overflow: hidden;
}
@ -146,6 +157,51 @@ iframe {
text-align: right;
}
.videoWatched {
width: 100%;
height: 155px;
background-color: black;
color: white;
position: relative;
bottom: 187px;
opacity: 0.4;
pointer-events: none;
}
.videoPlaylist {
float: right;
width: 45%;
position: relative;
bottom: 159px;
height: 155px;
background-color: black;
opacity: 0.7;
}
.videoPlaylistTotals {
font-size: 20px;
position: relative;
left: 50px;
top: 40px;
color: white;
}
.videoPlaylistIcon {
font-size: 25px;
top: 70px;
left: 10px;
background-color: inherit;
opacity: 1;
bottom: 0px;
}
.viewPlaylist {
position: relative;
left: 10px;
top: 60px;
cursor: pointer;
}
.videoPlayer {
width: 100%;
max-height: 1100px;

156
src/style/playlist.css Normal file
View File

@ -0,0 +1,156 @@
.playlistSideView {
float: left;
position: fixed;
width: 30%;
height: 100%;
overflow-y: auto;
}
.playlistSideView img {
width: 100%;
}
.playlistVideo {
height: 115px;
overflow: hidden;
}
.playlistVideoView {
float: right;
width: 60%;
}
.playlistVideoThumbnail {
width: 200px;
cursor: pointer;
}
.playlistVideoThumbnail img {
width: 100%;
cursor: pointer;
}
.playlistChannel {
height: 70px;
}
.playlistChannel img {
width: 70px;
float: left;
cursor: pointer;
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
}
.playlistChannel h3 {
float: left;
position: relative;
cursor: pointer;
width: 200px;
margin-left: 10px;
top: 5px;
font-size: 15px;
}
.playlistVideoSave {
background-color: black;
color: white;
opacity: 0.7;
padding: 4px;
position: relative;
left: 176px;
bottom: 117px;
cursor: pointer;
}
.playlistVideoTitle {
position: relative;
bottom: 160px;
margin-left: 210px;
font-weight: bold;
width: 50%;
cursor: pointer;
}
.playlistChannelName {
position: relative;
bottom: 160px;
margin-left: 210px;
font-size: 12px;
cursor: pointer;
}
#miniPL {
width: 100%;
}
.miniPLVideo {
width: 100%;
cursor: pointer;
height: 70px;
}
.miniPLThumbnail {
width: 120px;
height: 80px;
margin-right: 5px;
float: left;
}
.miniPLIndex {
float: left;
position: relative;
width: 15px;
top: 30px;
margin-right: 35px;
}
.miniPLVideoTitle {
font-weight: bold;
}
.miniPLVideoChannelName {
font-size: 12px;
position: relative;
bottom: 10px;
}
#miniPLTitle {
font-weight: bold;
font-size: 20px;
margin-left: 20px;
padding-top: 25px;
cursor: pointer;
}
#miniPLChannelName {
font-size: 15px;
font-weight: normal;
}
#miniPLVideoList {
padding: 10px;
overflow: scroll;
height: 375px;
}
#miniPLLoop {
margin-left: 20px;
font-size: 20px;
cursor: pointer;
}
#miniPLShuffle {
margin-left: 20px;
font-size: 20px;
cursor: pointer;
}
#miniPLDropdown {
float: right;
cursor: pointer;
font-size: 20px;
position: relative;
top: 20px;
right: 15px;
}

124
src/style/select.css Normal file
View File

@ -0,0 +1,124 @@
/*
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/>.
*/
/*
* Credit goes to pavelvaravko for making this css.
* https://codepen.io/pavelvaravko/pen/qjojOr
*/
/* select starting stylings ------------------------------*/
.select {
position: relative;
width: 350px;
}
.select-text {
position: relative;
font-family: inherit;
background-color: transparent;
width: 350px;
padding: 10px 10px 10px 0;
font-size: 18px;
border-radius: 0;
border: none;
}
/* Remove focus */
.select-text:focus {
outline: none;
}
/* Use custom arrow */
.select .select-text {
appearance: none;
-webkit-appearance:none
}
.select:after {
position: absolute;
top: 18px;
right: 10px;
/* Styling the down arrow */
width: 0;
height: 0;
padding: 0;
content: '';
border-left: 6px solid transparent;
border-right: 6px solid transparent;
pointer-events: none;
}
/* LABEL ======================================= */
.select-label {
font-size: 18px;
font-weight: normal;
position: absolute;
pointer-events: none;
left: 0;
top: 10px;
transition: 0.2s ease all;
}
/* active state */
.select-text:focus ~ .select-label, .select-text:valid ~ .select-label {
color: #2196F3;
top: -20px;
transition: 0.2s ease all;
font-size: 14px;
}
/* BOTTOM BARS ================================= */
.select-bar {
position: relative;
display: block;
width: 350px;
}
.select-bar:before, .select-bar:after {
content: '';
height: 2px;
width: 0;
bottom: 1px;
position: absolute;
background: #2196F3;
transition: 0.2s ease all;
}
.select-bar:before {
left: 50%;
}
.select-bar:after {
right: 50%;
}
/* active state */
.select-text:focus ~ .select-bar:before, .select-text:focus ~ .select-bar:after {
width: 50%;
}
/* HIGHLIGHTER ================================== */
.select-highlight {
position: absolute;
height: 60%;
width: 100px;
top: 25%;
left: 0;
pointer-events: none;
opacity: 0.5;
}

View File

@ -1,19 +1,3 @@
<!--
This file is part of FreeTube.
FreeTube is free software: you can redistribute it and/or modify
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/>.
-->
<div class='center' v-if="seen">
<img src='icons/logoColor.png' width='500px;'/>
<h1>{{ versionNumber }} Beta</h1>

View File

@ -1,20 +1,3 @@
<!--
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
(at your option) any later version.
the Free Software Foundation, either version 3 of the License, or
FreeTube is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
GNU General Public License for more details.
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
You should have received a copy of the GNU General Public License
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
-->
<div v-if='seen'>
<img class='channelViewBanner' :src='banner' />
<br />

View File

@ -17,13 +17,13 @@
{{#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 onclick='goToChannel("{{authorId}}")' style="float:left;margin-right: 1em; width:48px">
<img class="userIcon" src="{{authorThumbnail}}" 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>
<p onclick='goToChannel("{{authorId}}")' style='cursor: pointer; margin-left: 65px; font-weight: bold;'>{{author}}</p>
<p style="margin-bottom: 0; margin-left: 65px; word-break: break-word;">{{authorComment}}</p>
<p style="font-size:80%; padding: 0; margin-left: 65px;">{{published}}</p>
</div>
</div>
<br/>

View File

@ -1,23 +1,6 @@
<!--
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.
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License
-->
<div v-if='seen'>
<div v-if='playerSeen'>
<video class="videoPlayer" type="application/x-mpegURL" onmousemove="hideMouseTimeout()" onmouseleave="removeMouseTimeout()" controls="" :src='videoUrl' :poster="videoThumbnail" v-html="subtitleHtml" autoplay>
<video class="videoPlayer" id='videoPlayer' type="application/x-mpegURL" object-fit='cover' onmousemove="hideMouseTimeout()" onmouseleave="removeMouseTimeout()" onloadstart='checkVideoSettings()' onvolumechange='updateVolume()' controls="" onended='playNextVideo()' :src='videoUrl' :poster="videoThumbnail" v-html="subtitleHtml">
</video>
</div>
<div v-else>
@ -31,8 +14,9 @@
<span id='currentQuality'>{{currentQuality}}</span> <i class="fas fa-angle-down"></i>
<div class='qualityTypes'>
<ul>
<li id='quality480p' v-on:click='quality(video480p, "480p")'>480p</li>
<li id='quality720p' v-on:click='quality(video720p, "720p")'>720p</li>
<li v-if='valid480p' id='quality480p' v-on:click='quality(video480p, "480p")'>480p</li>
<li v-if='valid720p' id='quality720p' v-on:click='quality(video720p, "720p")'>720p</li>
<li v-if='validAudio' id='qualityAudio' v-on:click='quality(videoAudio, "AUDIO")'>AUDIO</li>
<li id='qualityEmbed' v-on:click='embededPlayer()'>EMBED</li>
</ul>
</div>
@ -52,6 +36,9 @@
</ul>
</div>
</div>
<div class='smallButton' v-on:click='loop'>
<i id='loopIcon' :class='savedIconType' class="fas fa-sync-alt"></i> LOOP
</div>
<div class='smallButton' v-on:click='save(videoId)'>
<i id='saveIcon' :class='savedIconType' class="fa-star"></i> <span id='savedText'>{{savedText}}</span>
</div>
@ -92,6 +79,27 @@
<span v-html="description"></span>
</div>
</div>
<div v-if='playlistSeen' id='miniPL'>
<i id='miniPLDropdown' onclick='togglePlaylist()' class='fas fa-angle-down'></i>
<p id='miniPLTitle'><span v-on:click='playlist(playlistId)'>{{playlistTitle}}</span><br /><span id='miniPLChannelName' v-on:click='channel(playlistChannelId)'>{{playlistChannelName}} - {{playlistIndex}} / {{playlistTotal}}</span></p>
<i id='miniPLLoop' v-on:click='playlistLoopToggle' class='fas fa-redo'></i>
<i id='miniPLShuffle' v-on:click='playlistShuffleToggle' class='fas fa-random'></i>
<br /><br />
<hr v-if='playlistShowList' style='width: 97%' />
<div v-if='playlistShowList' id='miniPLVideoList'>
<div v-for='video in playlistVideoList'>
<div v-on:click='play(video.videoId, playlistId)' class='miniPLVideo'>
<span class='miniPLIndex'>{{video.index}}</span>
<img :src='video.thumbnail' class='miniPLThumbnail'></>
<p class='miniPLVideoTitle'>{{video.title}}</p>
<p class='miniPLVideoChannelName'>{{video.channelName}}</p>
</div>
</div>
</a>
<div class='smallButton' v-on:click='copy("invidio.us", videoId)'>
COPY INVIDIOUS LINK
</div>
</div>
<div id='showComments'>
Show Comments <i class="far fa-comments"></i> (Max of 100)
</div>
@ -109,7 +117,7 @@
</div>
<p class='recommendTitle'>{{video.title}}</p>
<p class='recommendChannel'>{{video.channelName}}</p>
<p class='recommendDate'>{{video.publishedDate}}</p>
<p class='recommendDate'>{{video.viewCount}}</p>
</div>
<hr />
</div>

View File

@ -1,27 +0,0 @@
<!--
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/>.
-->
<div class='video'>
<div class='videoThumbnail'>
<img onclick='console.log("Not Implemented")' src= {{playlistThumbnail}} />
</div>
<p onclick='goToChannel("{{channelId}}")' class='videoTitle'>{{playlistTitle}}</p>
<p onclick='goToChannel("{{channelId}}")' class='channelName'>{{channelName}} - {{publishedDate}}</p>
<p onclick='goToChannel("{{channelId}}")' class='videoDescription'>{{playlistDescription}}</p>
<p>VIEW FULL PLAYLIST ({{videoCount}} videos)</p>
</div>
<hr />

View File

@ -0,0 +1,66 @@
<!--
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/>.
-->
<div>
<div v-if='seen'>
<div class='playlistSideView'>
<div class='playlistThumbnail'>
<img :src='thumbnail' />
</div>
<h2>{{title}}</h2>
<hr />
<div v-on:click='channel(channelId)' class='playlistChannel'>
<img :src='channelThumbnail' />
<h3>{{channelName}}</h3>
</div>
<p>{{videoCount}} videos - {{viewCount}} views - Last updated on {{lastUpdated}}</p>
<hr />
<p v-html='description'></p>
</div>
<div class='playlistVideoView'>
<div v-for="video in videoList">
<div class='playlistVideo'>
<div class='videoOptions'>
<i class="fas fa-ellipsis-v" onclick='showVideoOptions(this)'></i>
<ul>
<a :href='video.youtubeUrl' onclick='showVideoOptions(this.parentNode.previousSibling);'>
<li>Open in YouTube</li>
</a>
<li v-on:click='copy("youtube.com", video.id)' onclick='showVideoOptions(this.parentNode.previousSibling);'>Copy YouTube Link</li>
<a :href='video.invidiousUrl' onclick='showVideoOptions(this.parentNode.previousSibling);'>
<li>Open in Invidious</li>
</a>
<li v-on:click='copy("invidio.us", video.id)' onclick='showVideoOptions(this.parentNode.previousSibling);'>Copy Invidious Link</li>
</ul>
</div>
<div class='playlistVideoThumbnail'>
<img v-on:click='play(video.id)' :src='video.thumbnail' />
<p v-on:click='play(video.id)' class='videoDuration'>{{video.duration}}</p>
<i class="fas fa-history playlistVideoSave" v-on:click='toggleSave(video.id)'></i>
<div v-if='video.watched' class='videoWatched'>
WATCHED
</div>
</div>
<p v-on:click='play(video.id)' class='playlistVideoTitle'>{{video.title}}</p>
<p v-on:click='channel(video.channelId)' class='playlistChannelName'>{{video.channelName}}</p>
<p v-on:click='play(video.id)' class='live'>{{video.liveText}}</p>
</div>
<hr />
</div>
</div>
</div>
</div>

View File

@ -1,54 +1,72 @@
<!--
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/>.
-->
<div v-if="seen">
<h1 class="center">Settings</h1>
<div class='center'>
<input type='text' name='api-key' id='api-key' class='settingsInput' :value='apiKey' placeholder='API Key' />
<h1 class="center">Settings</h1>
<div class='center'>
<input type="checkbox" id="themeSwitch" name="set-name" class="switch-input" onchange='toggleTheme(this)' :checked='useTheme'>
<label for="themeSwitch" class="switch-label">Use Dark Theme</label>
<input type="checkbox" id="torSwitch" name="set-name" class="switch-input" :checked='useTor'>
<label for="torSwitch" class="switch-label">Use Tor for API calls</label>
<input type="checkbox" id="updatesSwitch" name="set-name" class="switch-input" :checked='updates'>
<label for="updatesSwitch" class="switch-label">Check for Updates</label>
<br />
<input type="checkbox" id="historySwitch" name="set-name" class="switch-input" :checked='history'>
<label for="historySwitch" class="switch-label">Remember History</label>
<input type="checkbox" id="autoplaySwitch" name="set-name" class="switch-input" :checked='autoplay'>
<label for="autoplaySwitch" class="switch-label">Autoplay Videos</label>
<input type="checkbox" id="subtitlesSwitch" name="set-name" class="switch-input" :checked='subtitles'>
<label for="subtitlesSwitch" class="switch-label">Turn on Subtitles by Default</label>
<br />
<br />
<div class="select center">
<select id='qualitySelect' class="select-text" required>
<option value="480">480p</option>
<option value="720" selected>720p</option>
</select>
<span class="select-highlight"></span>
<span class="select-bar"></span>
<label class="select-label">Default Video Quality</label>
</div>
<br /><br />
<div class="select center">
<select id='rateSelect' class="select-text" required>
<option value="0.25">0.25x</option>
<option value="0.5">0.5x</option>
<option value="0.75">0.75x</option>
<option value="1" selected>1x</option>
<option value="1.25">1.25x</option>
<option value="1.5">1.5x</option>
<option value="1.75">1.75x</option>
<option value="2">2x</option>
</select>
<span class="select-highlight"></span>
<span class="select-bar"></span>
<label class="select-label">Default Video Speed</label>
</div>
<br />
</div>
<div class='center'>
<div onclick='importSubscriptions()' class='settingsButton'>
IMPORT SUBSCRIPTIONS
</div>
<div onclick='exportSubscriptions();' class='settingsButton'>
EXPORT SUBSCRIPTIONS
</div>
</div>
<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)' :checked='useTheme'>
<label for="themeSwitch" class="switch-label">Use Dark Theme</label>
<input type="checkbox" id="torSwitch" name="set-name" class="switch-input" :checked='useTor'>
<label for="torSwitch" class="switch-label">Use Tor for API calls</label>
</div>
<div class='center'>
<div onclick='importSubscriptions()' class='settingsButton'>
IMPORT SUBSCRIPTIONS
<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>
<div onclick='exportSubscriptions();' class='settingsButton'>
EXPORT SUBSCRIPTIONS
<br />
<br />
<div onclick='updateSettings()' class='center settingsSubmit'>
SAVE SETTINGS
</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>
</div>

View File

@ -1,50 +1,70 @@
<div>
<div v-if='seen'>
<div v-for="video in videoList">
<div v-if='video.isVideo'>
<div class='video'>
<div class='videoOptions'>
<i class="fas fa-ellipsis-v" onclick='showVideoOptions(this)'></i>
<ul>
<a :href='video.youtubeUrl' onclick='showVideoOptions(this.parentNode.previousSibling);'>
<li>Open in YouTube</li>
</a>
<li v-on:click='copy("youtube.com", video.id)' onclick='showVideoOptions(this.parentNode.previousSibling);'>Copy YouTube Link</li>
<a :href='video.invidiousUrl' onclick='showVideoOptions(this.parentNode.previousSibling);'>
<li>Open in Invidious</li>
</a>
<li v-on:click='copy("invidio.us", video.id)' onclick='showVideoOptions(this.parentNode.previousSibling);'>Copy Invidious Link</li>
</ul>
</div>
<div class='videoThumbnail'>
<img v-on:click='play(video.id)' :src='video.thumbnail' />
<p v-on:click='play(video.id)' class='videoDuration'>{{video.duration}}</p>
<i class="fas fa-history" v-on:click='toggleSave(video.id)'></i>
</div>
<p v-on:click='play(video.id)' class='videoTitle'>{{video.title}}</p>
<p v-on:click='channel(video.channelId)' class='channelName'>{{video.channelName}} - {{video.publishedDate}}</p>
<p v-on:click='play(video.id)' class='videoDescription'>{{video.description}}</p>
<p v-on:click='play(video.id)' class='live'>{{video.liveText}}</p>
<div v-if='seen'>
<div v-for="video in videoList">
<div v-if='video.isVideo'>
<div class='video'>
<div class='videoOptions'>
<i class="fas fa-ellipsis-v" onclick='showVideoOptions(this)'></i>
<ul>
<a :href='video.youtubeUrl' onclick='showVideoOptions(this.parentNode.previousSibling);'>
<li>Open in YouTube</li>
</a>
<li v-on:click='copy("youtube.com", video.id)' onclick='showVideoOptions(this.parentNode.previousSibling);'>Copy YouTube Link</li>
<a :href='video.invidiousUrl' onclick='showVideoOptions(this.parentNode.previousSibling);'>
<li>Open in Invidious</li>
</a>
<li v-on:click='copy("invidio.us", video.id)' onclick='showVideoOptions(this.parentNode.previousSibling);'>Copy Invidious Link</li>
</ul>
</div>
<div class='videoThumbnail'>
<img v-on:click='play(video.id)' :src='video.thumbnail' />
<p v-on:click='play(video.id)' class='videoDuration'>{{video.duration}}</p>
<i class="fas fa-history videoSave" v-on:click='toggleSave(video.id)'></i>
<div v-if='video.watched' class='videoWatched'>
WATCHED
</div>
</div>
<p v-on:click='play(video.id)' class='videoTitle'>{{video.title}}</p>
<p v-on:click='channel(video.channelId)' class='channelName'>{{video.channelName}}</p>
<p v-on:click='play(video.id)' class='videoViews'>{{video.views}} {{video.viewText}} - {{video.publishedDate}}</p>
<p v-on:click='play(video.id)' class='videoDescription'>{{video.description}}</p>
<p v-on:click='play(video.id)' class='live'>{{video.liveText}}</p>
</div>
<hr />
</div>
<div v-else-if='video.isPlaylist'>
<div class='video'>
<div class='videoThumbnail'>
<img v-on:click='playlist(video.id)' :src='video.thumbnail' />
<div class='videoPlaylist' v-on:click='playlist(video.id)'>
<span class='videoPlaylistTotals'>{{video.videoCount}}</span>
<i class='fas fa-list-ol videoPlaylistIcon'></i>
</div>
</div>
<p v-on:click='playlist(video.id)' class='videoTitle'>{{video.title}}</p>
<p v-on:click='channel(video.channelId)' class='channelName'>{{video.channelName}}</p>
<p v-on:click='playlist(video.id)' class='videoDescription'>{{video.description}}</p>
<p v-on:click='playlist(video.id)' class='viewPlaylist'>VIEW FULL PLAYLIST ({{video.videoCount}} videos)</p>
</div>
<hr />
</div>
<!-- Channel View -->
<div v-else>
<div class='video'>
<div class='channelThumbnail'>
<img v-on:click='channel(video.channelId)' :src='video.thumbnail' />
</div>
<p v-on:click='channel(video.channelId)' class='videoTitle'>{{video.channelName}}</p>
<p v-on:click='channel(video.channelId)' class='channelName'>{{video.subscriberCount}} subscribers - {{video.videoCount}} videos</p>
<p v-on:click='channel(video.channelId)' class='videoDescription'>{{video.channelDescription}}</p>
</div>
<hr />
</div>
</div>
<hr />
</div>
<!-- Channel View -->
<div v-else>
<div class='video'>
<div class='channelThumbnail'>
<img v-on:click='channel(video.channelId)' :src='video.thumbnail' />
</div>
<p v-on:click='channel(video.channelId)' class='videoTitle'>{{video.channelName}}</p>
<p v-on:click='channel(video.channelId)' class='channelName'>{{video.subscriberCount}} subscribers - {{video.videoCount}} videos</p>
<p v-on:click='channel(video.channelId)' class='videoDescription'>{{video.channelDescription}}</p>
<div v-if='isSearch'>
<div v-on:click='nextPage' id='getNextPage'>
<i class="fas fa-search"></i> Fetch more results...
</div>
</div>
<hr />
</div>
</div>
<div v-if='isSearch'>
<div v-on:click='nextPage' id='getNextPage'>
<i class="fas fa-search"></i> Fetch more results...
</div>
</div>
</div>
</div>