Merge remote-tracking branch 'origin/development'
|
@ -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",
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
}
|
|
@ -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",
|
|
@ -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",
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
"Search / Go to URL": "Поиск / перейти по URL",
|
||||
"Search Results": "Результаты поиска",
|
||||
"Subscriber": "Подписка",
|
||||
"Subscriber": "Подписчиков",
|
||||
"Subscribers": "Подписчиков",
|
||||
"Video": "Видео",
|
||||
"Videos": "Видео",
|
||||
"View Full Playlist": "Список воспроизведения",
|
||||
|
|
15
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 579 B |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 4.7 KiB |
|
@ -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> Subscriptions</li>
|
||||
<li v-on:click='trending'><i class="fas fa-fire"></i> Trending</li>
|
||||
<li v-on:click='popular'><i class="fas fa-users"></i> Most Popular</li>
|
||||
<li v-on:click='saved'><i class="fas fa-star"></i> Favorites</li>
|
||||
<li v-on:click='history'><i class="fas fa-history"></i> 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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
122
src/js/init.js
|
@ -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,51 +83,83 @@ let init = function () {
|
|||
label: 'File',
|
||||
submenu: [{
|
||||
label: 'Open New Window',
|
||||
click () { init() }
|
||||
},
|
||||
{role: 'quit'}
|
||||
click() {
|
||||
init()
|
||||
}
|
||||
},
|
||||
{
|
||||
role: 'quit'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Open New Window',
|
||||
click () { init() }
|
||||
},
|
||||
{role: 'quit'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{role: 'cut'},
|
||||
{role: 'copy', accelerator: "CmdOrCtrl+C", selector: "copy:" },
|
||||
{role: 'paste', accelerator: "CmdOrCtrl+V", selector: "paste:" },
|
||||
{role: 'pasteandmatchstyle'},
|
||||
{role: 'delete'},
|
||||
{role: 'selectall'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{role: 'reload'},
|
||||
{role: 'forcereload'},
|
||||
{role: 'toggledevtools'},
|
||||
{type: 'separator'},
|
||||
{role: 'resetzoom'},
|
||||
{role: 'zoomin'},
|
||||
{role: 'zoomout'},
|
||||
{type: 'separator'},
|
||||
{role: 'togglefullscreen'}
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{role: 'minimize'},
|
||||
{role: 'close'}
|
||||
]
|
||||
}
|
||||
];
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
role: 'cut'
|
||||
},
|
||||
{
|
||||
role: 'copy',
|
||||
accelerator: "CmdOrCtrl+C",
|
||||
selector: "copy:"
|
||||
},
|
||||
{
|
||||
role: 'paste',
|
||||
accelerator: "CmdOrCtrl+V",
|
||||
selector: "paste:"
|
||||
},
|
||||
{
|
||||
role: 'pasteandmatchstyle'
|
||||
},
|
||||
{
|
||||
role: 'delete'
|
||||
},
|
||||
{
|
||||
role: 'selectall'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [{
|
||||
role: 'reload'
|
||||
},
|
||||
{
|
||||
role: 'forcereload'
|
||||
},
|
||||
{
|
||||
role: 'toggledevtools'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'resetzoom'
|
||||
},
|
||||
{
|
||||
role: 'zoomin'
|
||||
},
|
||||
{
|
||||
role: 'zoomout'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'togglefullscreen'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [{
|
||||
role: 'minimize'
|
||||
},
|
||||
{
|
||||
role: 'close'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
|
|
@ -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
|
||||
|
|
544
src/js/player.js
|
@ -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(/\"\;/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(/\&\;/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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -34,25 +34,3 @@ const updateChecker = require('github-version-checker');
|
|||
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);
|
||||
}
|
||||
});
|
693
src/js/videos.js
|
@ -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, '"');
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ a {
|
|||
height: 3px;
|
||||
background-color: #f44336;
|
||||
display: block;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
|
|
|
@ -29,7 +29,7 @@ iframe {
|
|||
width: 95%;
|
||||
max-width: 1000px;
|
||||
height: auto;
|
||||
padding: 15px;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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/>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<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>
|
||||
|
@ -14,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>
|
||||
|
@ -35,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>
|
||||
|
@ -75,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>
|
||||
|
@ -92,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>
|
||||
|
|
|
@ -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 />
|
|
@ -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>
|
|
@ -1,37 +1,72 @@
|
|||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|