[Feature] Basic Support for Playlists

This commit is contained in:
PrestonN 2018-09-25 14:26:10 -04:00
parent cf74a740b5
commit a2cf6851b8
9 changed files with 383 additions and 117 deletions

View File

@ -8,6 +8,7 @@
<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">
@ -78,6 +79,7 @@
<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>
@ -97,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>

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

@ -0,0 +1,76 @@
/*
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;
invidiousAPI('playlists', playlistId, {}, (data) => {
console.log(data);
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");
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;
});
loadingView.seen = false;
playlistView.seen = true;
});
}

View File

@ -24,6 +24,7 @@ const videoListTemplate = require('./templates/videoTemplate.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
@ -283,6 +284,40 @@ let historyView = new Vue({
template: videoListTemplate
});
let playlistView = new Vue({
el: '#playlistView',
data: {
seen: false,
channelName: '',
channelId: '',
thumbnail: '',
title: '',
videoCount: '',
viewCount: '',
description: '',
lastUpdated: '',
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: playlistViewTemplate
});
let aboutView = new Vue({
el: '#aboutView',
data: {
@ -334,7 +369,10 @@ let searchView = new Vue({
nextPage: () => {
console.log(searchView.page);
search(searchView.page);
}
},
playlist: (playlistId) => {
showPlaylist(playlistId);
},
},
template: videoListTemplate
});
@ -423,7 +461,8 @@ let playerView = new Vue({
videoLikes: 0,
videoDislikes: 0,
playerSeen: true,
recommendedVideoList: []
recommendedVideoList: [],
playlistVideoList: [],
},
methods: {
channel: (channelId) => {
@ -476,7 +515,10 @@ let playerView = new Vue({
player.loop = false;
showToast('Video loop has been turned off.')
}
}
},
playlist: (playlistId) => {
showPlaylist(playlistId);
},
},
template: playerTemplate
});
@ -492,6 +534,7 @@ function hideViews(){
trendingView.seen = false;
savedView.seen = false;
historyView.seen = false;
playlistView.seen = false;
playerView.seen = false;
channelView.seen = false;
channelVideosView.seen = false;

View File

@ -99,6 +99,9 @@ function search(page = 1) {
case 'channel':
displayChannel(video);
break;
case 'playlist':
displayPlaylist(video);
break;
default:
}
});
@ -122,6 +125,10 @@ function displayVideo(videoData, listType = '') {
let video = {};
video.id = videoData.videoId;
if (videoData.type == 'playlist') {
video.isPlaylist = true;
}
historyDb.find({
videoId: video.id
}, (err, docs) => {
@ -248,47 +255,20 @@ function displayChannel(channel) {
searchView.videoList = searchView.videoList.concat(channelData);
}
function displayPlaylists(playlists) {
let playlistIds;
function displayPlaylist(playlist) {
let playListData = {};
playlists.forEach((playlist) => {
if (typeof (playlistIds) === 'undefined') {
playlistIds = playlist.id.playlistId;
} else {
playlistIds = playlistIds + ',' + playlist.id.playlistId;
}
});
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;
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');
});
});
searchView.videoList = searchView.videoList.concat(playListData);
}
/**

View File

@ -50,6 +50,9 @@ iframe {
padding: 6px;
opacity: 0.7;
position: relative;
}
.videoSave {
bottom: 159px;
left: 247px;
}
@ -164,6 +167,40 @@ iframe {
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;

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

@ -0,0 +1,73 @@
.playlistSideView {
float: left;
position: fixed;
width: 30%;
}
.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 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;
margin-left: 10px;
top: 5px;
}
.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;
}

View File

@ -1,27 +0,0 @@
<!--
This file is part of FreeTube.
FreeTube is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FreeTube is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FreeTube. If not, see <http://www.gnu.org/licenses/>.
-->
<div class='video'>
<div class='videoThumbnail'>
<img onclick='console.log("Not Implemented")' src= {{playlistThumbnail}} />
</div>
<p onclick='goToChannel("{{channelId}}")' class='videoTitle'>{{playlistTitle}}</p>
<p onclick='goToChannel("{{channelId}}")' class='channelName'>{{channelName}} - {{publishedDate}}</p>
<p onclick='goToChannel("{{channelId}}")' class='videoDescription'>{{playlistDescription}}</p>
<p>VIEW FULL PLAYLIST ({{videoCount}} videos)</p>
</div>
<hr />

View File

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

View File

@ -1,54 +1,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 v-if='video.watched' class='videoWatched'>
WATCHED
<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'>
<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>
<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>
<!-- 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>