diff --git a/css/styles.css b/css/styles.css index 4ac7e0ef..781ca2d3 100644 --- a/css/styles.css +++ b/css/styles.css @@ -16,7 +16,6 @@ header { header { max-width: 450px; } - } .tt-menu, footer { @@ -62,7 +61,6 @@ a { #container { max-width: 450px; } - } #v { @@ -97,11 +95,23 @@ a { text-align: center; } -#demo { - display: block; +#demo, #youtube-login { + display: inline-block; + width: 49%; margin: 0 auto; } +#youtube-login { + display: none; +} + +/* +#demo { + display: block; + margin: 0 auto; +} +*/ + #zen-video-description { white-space: pre-line; } @@ -109,8 +119,8 @@ a { #form { margin-bottom: 20px; } - /* Media controls */ + #pause { display: none; } @@ -122,6 +132,7 @@ a { #volumeSliderControl .slider-handle.custom::before { /* Unicode bar */ + content: "\258C"; color: #000000; } @@ -141,6 +152,7 @@ a { /* Input field fixes and search suggestion style. * typeahead conflicts with primercss */ + .twitter-typeahead { width: 100%; } @@ -190,7 +202,6 @@ a { .fa-search { font-size: 20px; } - } .sponsor { @@ -220,7 +231,6 @@ a { .sponsor-a:first-of-type { margin: 0; } - } .color-grey { @@ -254,3 +264,98 @@ a { #audioplayer .plyr__volume--display, #audioplayer .plyr__progress--buffer { background-color: #f9f9f9; } + +.queue::-webkit-scrollbar { + width: 14px; + height: 12px; +} + +.queue::-webkit-scrollbar-thumb { + border: 4px solid transparent; + background-clip: padding-box; + border-radius: 7px; + background-color: #ffffff; +} + +.queue::-webkit-scrollbar-button { + width: 0; + height: 0; + display: none; +} + +.queue::-webkit-scrollbar-corner { + background-color: transparent; +} + +.queue { + border-radius: 4px; + padding: 5px; + background: rgb(15, 15, 20); + height: 100px; + overflow: auto; + overflow-y: visible; + margin: 30px auto 0 auto; + display: flex; + text-align: center; + visibility: hidden; +} + +.queue__vid { + color: #ffffff; + padding: 4px; + width: 80px; + height: 80px; + word-wrap: break-word; + display: inline-block; + text-align: center; + cursor: pointer; + transition: 0.5s; + position: relative; +} + +.queue__vid:hover { + background: rgb(57, 57, 57); + transition: 0.5s; +} + +.queue__vid:not(:last-child) { + border-right: 1px rgb(57, 57, 57) solid; +} + +.queue__vid__span { + text-align: center; + display: block; + width: 70px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.queue__vid__img { + width: 70px; +} + +.queue__vid__cancel { + color: #efefef; + font-size: 15px; + width: 18px; + height: 18px; + position: absolute; + right: 4px; + bottom: 8px; + border-radius: 5px; + background: #101010; + visibility: hidden; +} + +.queue__vid__cancel:hover { + color: #ffff00; +} + +.queue__vid:hover .queue__vid__cancel { + visibility: visible; +} + +.buttons-div { + text-align: center; +} diff --git a/index.html b/index.html index 94beb75b..88a9e13b 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,6 @@ + @@ -17,7 +18,12 @@ - + @@ -26,9 +32,10 @@ + - +
@@ -86,16 +93,35 @@


- + + +
- +

Created by @_Shakeel and others.

@@ -125,6 +151,10 @@

Sponsored by:

"url": "https://zenplayer.audio", "logo": "https://zenplayer.audio/img/zen-audio-player-905.png" } - + + + + + diff --git a/js/everything.js b/js/everything.js index c82e9a78..95deb3f9 100644 --- a/js/everything.js +++ b/js/everything.js @@ -1,5 +1,4 @@ -/* global URI getSearchResults, getAutocompleteSuggestions, parseYoutubeVideoID, getYouTubeVideoDescription */ - +/* global URI getSearchResults, getAutocompleteSuggestions, parseYoutubeVideoID, getYouTubeVideoDescription, gapi */ // Pointer to Keen client var client; /** @@ -59,10 +58,10 @@ var youTubeDataApiKey = "AIzaSyCxVxsC5k46b8I-CLXlF3cZHjpiqP_myVk"; var currentVideoID; var errorMessage = { - init: function() { + init: function () { // nothing for now }, - show: function(message) { + show: function (message) { $("#zen-error").text("ERROR: " + message); $("#zen-error").show(); @@ -74,9 +73,11 @@ var errorMessage = { // Send the error to Google Analytics ga("send", "event", "error", message); - sendKeenEvent("error", {"message": message}); + sendKeenEvent("error", { + "message": message + }); }, - hide: function() { + hide: function () { $("#zen-error").text("").hide(); ZenPlayer.show(); } @@ -122,7 +123,11 @@ function handleYouTubeError(details) { // Update the UI w/ error errorMessage.show(message); ga("send", "event", "YouTube iframe API error", verboseMessage); - sendKeenEvent("YouTube iframe API error", {verbose: verboseMessage, message: message, code: details.code}); + sendKeenEvent("YouTube iframe API error", { + verbose: verboseMessage, + message: message, + code: details.code + }); // Log debug info console.log("Verbose debug error message: ", verboseMessage); @@ -133,12 +138,11 @@ function handleYouTubeError(details) { var ZenPlayer = { updated: false, isPlaying: false, - init: function(videoID) { + init: function (videoID) { // Inject svg with control icons $("#plyr-svg").load("../bower_components/plyr/dist/plyr.svg"); plyrPlayer = document.querySelector(".plyr"); - plyr.setup(plyrPlayer, { autoplay: true, controls: ["play", "progress", "current-time", "duration", "mute", "volume"], @@ -148,19 +152,18 @@ var ZenPlayer = { // Load video into Plyr player if (plyrPlayer.plyr) { var that = this; - plyrPlayer.addEventListener("error", function(event) { + plyrPlayer.addEventListener("error", function (event) { if (event && event.detail && typeof event.detail.code === "number") { handleYouTubeError(event.detail); ZenPlayer.hide(); } }); - plyrPlayer.addEventListener("ready", function() { + plyrPlayer.addEventListener("ready", function () { // Noop if we have nothing to play if (!currentVideoID || currentVideoID.length === 0) { return; } - // Gather video info that.videoTitle = plyrPlayer.plyr.embed.getVideoData().title; that.videoAuthor = plyrPlayer.plyr.embed.getVideoData().author; @@ -180,9 +183,11 @@ var ZenPlayer = { that.setupTitle(); that.setupVideoDescription(videoID); that.setupPlyrToggle(); + + // Add the video to the playlist if not already in list }); - plyrPlayer.addEventListener("playing", function() { + plyrPlayer.addEventListener("playing", function () { if (that.updated) { return; } @@ -215,7 +220,7 @@ var ZenPlayer = { updateTweetMessage(); }); - plyrPlayer.addEventListener("timeupdate", function() { + plyrPlayer.addEventListener("timeupdate", function () { // Store the current time of the video. var resumeTime = 0; if (window.sessionStorage) { @@ -243,31 +248,33 @@ var ZenPlayer = { $("#zen-video-title").attr("href", updatedUrl); }); - plyrPlayer.addEventListener("playing", function() { + plyrPlayer.addEventListener("playing", function () { this.isPlaying = true; }.bind(this)); - plyrPlayer.addEventListener("pause", function() { + plyrPlayer.addEventListener("pause", function () { this.isPlaying = false; }.bind(this)); + plyrPlayer.plyr.source({ type: "video", title: "Title", - sources: [{ - src: currentVideoID, - type: "youtube" - }] + sources: [ + { + src: currentVideoID, + type: "youtube" + }] }); } }, - show: function() { + show: function () { $("#audioplayer").show(); }, - hide: function() { + hide: function () { $("#audioplayer").hide(); }, - setupTitle: function() { + setupTitle: function () { // Prepend music note only if title does not already begin with one. var tmpVideoTitle = this.videoTitle; if (!/^[\u2669\u266A\u266B\u266C\u266D\u266E\u266F]/.test(tmpVideoTitle)) { @@ -276,23 +283,23 @@ var ZenPlayer = { $("#zen-video-title").html(tmpVideoTitle); $("#zen-video-title").attr("href", this.videoUrl); }, - setupVideoDescription: function(videoID) { + setupVideoDescription: function (videoID) { var description = anchorURLs(this.videoDescription); description = anchorTimestamps(description, videoID); $("#zen-video-description").html(description); $("#zen-video-description").hide(); - $("#toggleDescription").click(function(event) { + $("#toggleDescription").click(function (event) { toggleElement(event, "#zen-video-description", "Description"); }); }, - setupPlyrToggle: function() { + setupPlyrToggle: function () { // Show player button click event - $("#togglePlayer").click(function(event) { + $("#togglePlayer").click(function (event) { toggleElement(event, ".plyr__video-wrapper", "Player"); }); }, - getVideoDescription: function(videoID) { + getVideoDescription: function (videoID) { var description = ""; if (isFileProtocol()) { @@ -303,7 +310,7 @@ var ZenPlayer = { getYouTubeVideoDescription( videoID, youTubeDataApiKey, - function(data) { + function (data) { if (data.items.length === 0) { errorMessage.show("Video description not found"); } @@ -311,7 +318,7 @@ var ZenPlayer = { description = data.items[0].snippet.description; } }, - function(jqXHR, textStatus, errorThrown) { + function (jqXHR, textStatus, errorThrown) { logError(jqXHR, textStatus, errorThrown, "Video Description error"); } ); @@ -324,10 +331,10 @@ var ZenPlayer = { return description; }, - play: function() { + play: function () { plyrPlayer.plyr.embed.playVideo(); }, - pause: function() { + pause: function () { plyrPlayer.plyr.embed.pauseVideo(); } }; @@ -459,10 +466,10 @@ function makeSearchURL(searchQuery) { function anchorURLs(text) { /* RegEx to match http or https addresses - * This will currently only match TLD of two or three letters - * Ends capture when: - * (1) it encounters a TLD - * (2) it encounters a period (.) or whitespace, if the TLD was followed by a forwardslash (/) */ + * This will currently only match TLD of two or three letters + * Ends capture when: + * (1) it encounters a TLD + * (2) it encounters a period (.) or whitespace, if the TLD was followed by a forwardslash (/) */ var re = /((?:http|https)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(?:\/\S*[^\.\s])?)/g; /* Wraps all found URLs in tags */ return text.replace(re, "$1"); @@ -481,7 +488,7 @@ function anchorTimestamps(text, videoID) { (?:$|\:[0-5]\d)) either the string ends or is a a number between 00-59 */ var re = /((?:[0-5]\d|\d|)(?:\d|\:[0-5]\d)(?:$|\:[0-5]\d))/g; - return text.replace(re, function(match) { + return text.replace(re, function (match) { return "" + match + ""; }); } @@ -526,14 +533,14 @@ var demos = [ "koJv-j1usoI", // The Glitch Mob - Starve the Ego, Feed the Soul "EBerFisqduk", // Cazzette - Together (Lost Kings Remix) "jxKjOOR9sPU", // The Temper Trap - Sweet Disposition - "03O2yKUgrKw" // Mike Mago & Dragonette - Outlines + "03O2yKUgrKw" // Mike Mago & Dragonette - Outlines ]; function pickDemo() { return demos[Math.floor(Math.random() * demos.length)]; } -$(function() { +$(function () { if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { $("#container").hide(); $("#mobile-message").html("Sorry, we don't support mobile devices."); @@ -563,7 +570,7 @@ $(function() { getSearchResults( currentSearchQuery, youTubeDataApiKey, - function(data) { + function (data) { if (data.pageInfo.totalResults === 0) { errorMessage.show("No results."); return; @@ -577,7 +584,7 @@ $(function() { $("#search-results ul").append(start + result.id.videoId + ">" + result.snippet.title + "

" + result.snippet.title + ""); }); }, - function(jqXHR, textStatus, errorThrown) { + function (jqXHR, textStatus, errorThrown) { logError(jqXHR, textStatus, errorThrown, "Search error"); } ); @@ -591,18 +598,18 @@ $(function() { minLength: 1 }, { source: function (query, processSync, processAsync) { - getAutocompleteSuggestions(query, function(data) { - return processAsync($.map(data[1], function(item) { + getAutocompleteSuggestions(query, function (data) { + return processAsync($.map(data[1], function (item) { return item[0]; })); }); } - }).bind("typeahead:selected", function(obj, datum) { + }).bind("typeahead:selected", function (obj, datum) { window.location.href = makeSearchURL(datum); }); // Handle form submission - $("#form").submit(function(event) { + $("#form").submit(function (event) { event.preventDefault(); var formValue = $.trim($("#v").val()); var formValueTime = /&t=(\d*)$/g.exec(formValue); @@ -613,7 +620,9 @@ $(function() { if (formValue) { var videoID = wrapParseYouTubeVideoID(formValue, true); ga("send", "event", "form submitted", videoID); - sendKeenEvent("Form submitted", {videoID: videoID}); + sendKeenEvent("Form submitted", { + videoID: videoID + }); if (isFileProtocol()) { errorMessage.show("Skipping video lookup request as we're running the site locally."); } @@ -628,7 +637,7 @@ $(function() { fields: "items/snippet/description", id: videoID }, - success: function(data) { + success: function (data) { if (data.items.length === 0) { window.location.href = makeSearchURL(formValue); } @@ -636,7 +645,7 @@ $(function() { window.location.href = makeListenURL(videoID, formValueTime); } } - }).fail(function(jqXHR, textStatus, errorThrown) { + }).fail(function (jqXHR, textStatus, errorThrown) { logError(jqXHR, textStatus, errorThrown, "Lookup error"); }); } @@ -652,10 +661,12 @@ $(function() { } // Handle demo link click - $("#demo").click(function(event) { + $("#demo").click(function (event) { event.preventDefault(); ga("send", "event", "demo", "clicked"); - sendKeenEvent("Demo", {action: "clicked"}); + sendKeenEvent("Demo", { + action: "clicked" + }); // Don't continue appending to the URL if it appears "good enough". // This is likely only a problem if the demo link didn't work right the first time @@ -665,14 +676,16 @@ $(function() { } else { ga("send", "event", "demo", "already had video ID in URL"); - sendKeenEvent("demo", {action: "already had video ID in URL"}); + sendKeenEvent("demo", { + action: "already had video ID in URL" + }); } }); // Load the player ZenPlayer.init(currentVideoID); - $(document).on("keyup", function(evt) { + $(document).on("keyup", function (evt) { // 32 = spacebar, toggle play/pause if not typing in the search box if (evt.keyCode === 32 && !$("#v").is(":focus")) { evt.preventDefault(); @@ -685,20 +698,282 @@ $(function() { } }); - $(document).on("keydown", function(evt) { + $(document).on("keydown", function (evt) { // 32 = spacebar, if not typing in the search prevent "page down" scrolling if (evt.keyCode === 32 && !$("#v").is(":focus")) { evt.preventDefault(); } }); }); +// Youtube Authentication and Playlist code +var OAUTH2_CLIENT_ID = "763547295554-ubbn5qqth37ov4j11a2jt8mjl95eqm2l.apps.googleusercontent.com"; // ToDo: use another client id +var OAUTH2_SCOPES = "https://www.googleapis.com/auth/youtube"; +var tmpYouTubeApiKey = "AIzaSyBvkfhXrqTMk8WJuhN4CeRrIg4BUm5Md0E"; // ToDo: assign to youTubeDataApiKey +var YOUTUBE_PLAYLIST_NAME = "Zen Audio Player"; +var LOCALSTORAGE_PLAYLIST_ID_KEY = "zap_playlist_id"; +var youtubeVideos; +/** + * This function will be called after loading the page + */ +/* eslint-disable no-unused-vars */ +function handleClientLoad() { + // Load the API client and auth library + if (!isFileProtocol()) { + gapi.load("client:auth2", checkAuth); + } +} +/* eslint-enable no-unused-vars */ + +/** + * This function checks the authentication using YouTube API, exactly after the page loads + */ +function checkAuth() { + /* eslint camelcase: [2, {properties: "never"}] */ + gapi.client.setApiKey(tmpYouTubeApiKey); + gapi.auth.authorize({ + client_id: OAUTH2_CLIENT_ID, + scope: OAUTH2_SCOPES, + immediate: true + }, handleAuthResult); +} + +/** + * This function handles the result of the authentication call + * @param {object} authResult authentication result - Generated by library + */ +function handleAuthResult(authResult) { + if (authResult && !authResult.error) { + // Authorization was successful. Hide authorization prompts and show + // content that should be visible after authorization succeeds. + changeSignInButtonVisibility(true); + makeApiCall(); + } + else { + changeSignInButtonVisibility(false); + // Make the #login-link clickable. Attempt a non-immediate OAuth 2.0 + // client flow. The current function is called when that flow completes. + $("#youtube-login").click(function () { + gapi.auth.authorize({ + client_id: OAUTH2_CLIENT_ID, + scope: OAUTH2_SCOPES, + immediate: false + }, handleAuthResult); + }); + } +} + +/** + * This function calls the youtube API v3 and then execute the requests (withing handleAPILoaded function) after a successful load + */ +function makeApiCall() { + gapi.client.load("youtube", "v3", function () { + handleAPILoaded(); + }); +} +/** + * This function gets user videos from Zap playlist and store them into array + * It also checks if the current playing video isn't in the Youtube playlist, it calls the fucntion which adds it + */ +function getUserVideosFromZapPlaylist() { + if (localStorage.getItem(LOCALSTORAGE_PLAYLIST_ID_KEY) && !youtubeVideos && !getCurrentSearchQuery()) { + var request = gapi.client.youtube.playlistItems.list({ + part: "snippet", + mine: true, + maxResults: 50, + playlistId: localStorage.getItem(LOCALSTORAGE_PLAYLIST_ID_KEY) + }); + request.execute(function (response) { + if (response.code === 404) { + // playlist not found + checkIfUserAlreadyHasZapYoutubePlaylist(); + } + else { + youtubeVideos = response.items; + if (youtubeVideos.length > 0) { + // Check if current played video not in playlist add it + if (getCurrentVideoID()) { + var doesTheVideoExist = false; + for (var i = 0; i < youtubeVideos.length; i++) { + if (youtubeVideos[i].snippet.resourceId.videoId === currentVideoID) { + doesTheVideoExist = true; + break; + } + } + if (!doesTheVideoExist) { + addVideoToPlaylist(currentVideoID); + } + } + // Add videos to queue + fetchYoutubeVideosIntoQueue(); + } + } + }); + } +} + + +/** + * This function uses the filled youtubeVideos array and fills the UI videos queue + */ +function fetchYoutubeVideosIntoQueue() { + var i = 0; + youtubeVideos.forEach(function (video) { + var $div = $("
", { + "class": "queue__vid" + }); + $div.append($("" + video.snippet.title + "")); + $div.append($("")); + $div.append($("")); + + $div.click(function () { + window.location.href = makeListenURL(video.snippet.resourceId.videoId, 0); + }); + $(".queue").append($div); + i++; + }); + $(".queue__vid__cancel").click(function (e) { + if (confirm("Do you want to delete it for sure ?")) { + youtubeVideos.splice($(this).data("vidindex"), 1); + $(this).parent().hide(); + if (youtubeVideos.length === 0) { + $(".queue").hide(); + } + removeYoutubeVideoFromPlaylist($(this).data("vidid")); + } + e.stopPropagation(); + return true; + }); + $(".queue").css("visibility", "visible"); +} +/** + * This function contains the requests that will be executed after the load is completed + * IF the localStorage doesn't contain zap_playlist_id: create the playlist in the user YouTube account and then store the zap_playlist_id + * IF the current page displays video: check if it doesn't exist in the play list then add it + */ +function handleAPILoaded() { + // if not video is currently playing + if (!getCurrentVideoID() && !localStorage.getItem(LOCALSTORAGE_PLAYLIST_ID_KEY)) { + checkIfUserAlreadyHasZapYoutubePlaylist(); + } + // Get user videos and store them into array + getUserVideosFromZapPlaylist(); + if (getCurrentSearchQuery()) { + $(".queue").hide(); // Hide queue in case of search keyword exists + } +} +/** + * If the user for some reason cleared the browser localstorage, we don't want to re-create another playlist. So we will list all the playlists, searching for a playlist called "Zen Audio Player", if found get its id and store it + */ +function checkIfUserAlreadyHasZapYoutubePlaylist() { + var request = gapi.client.youtube.playlists.list({ + part: "snippet", + mine: true, + maxResults: 50 + }); + request.execute(function (response) { + var isZenPlaylistFound = false; + for (var i = 0; i < response.items.length; i++) { + if (response.items[i].snippet.title === YOUTUBE_PLAYLIST_NAME) { + localStorage.setItem(LOCALSTORAGE_PLAYLIST_ID_KEY, response.items[i].id); + isZenPlaylistFound = true; + break; + } + } + getUserVideosFromZapPlaylist(); + if (!isZenPlaylistFound) { + addNewPlaylistToYoutube(); + } + }); +} +/** + * This function adds new playlist in youtube account of the current user + */ +function addNewPlaylistToYoutube() { + // Request to insert playlist + var request = gapi.client.youtube.playlists.insert({ + part: "snippet,status", + resource: { + snippet: { + title: YOUTUBE_PLAYLIST_NAME, + description: "A private playlist created with the YouTube API via Zen Audio Player" + }, + status: { + privacyStatus: "private" + } + } + }); + request.execute(function (response) { + var result = response.result; + if (result) { // get and store playlist ID + var playlistId = result.id; + localStorage.setItem(LOCALSTORAGE_PLAYLIST_ID_KEY, playlistId); + getUserVideosFromZapPlaylist(); + } + else { + alert("Could not create Zen Audio Player playlist"); + } + }); +} + +/** + * This function adds video to YouTube playlist + * @param {string} videoID videoID to be added + */ +function addVideoToPlaylist(videoID) { + var details = { + videoId: videoID, + kind: "youtube#video" + }; + var request = gapi.client.youtube.playlistItems.insert({ + part: "snippet", + resource: { + snippet: { + playlistId: localStorage.getItem(LOCALSTORAGE_PLAYLIST_ID_KEY), + resourceId: details + } + } + }); + request.execute(); +} +/** + * This function removes video from ZAP YouTube playlist + * @param {string} videoID videoID to be removed + */ +function removeYoutubeVideoFromPlaylist(videoID) { + var request = gapi.client.youtube.playlistItems.delete({ + id: videoID + }); + request.execute(); +} +/** + * This function shows th login button from the page + * @param {boolean} hide if true the button will be hidden + */ +function changeSignInButtonVisibility(hide) { + $(document).ready(function () { + if (hide) { + $("#youtube-login").hide(); + } + else { + $("#youtube-login").show(); + } + }); +} /*eslint-disable */ // Google Analytics goodness -(function(i,s,o,g,r,a,m){i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){ -(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), -m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) -})(window,document,"script","//www.google-analytics.com/analytics.js","ga"); +(function (i, s, o, g, r, a, m) { + i["GoogleAnalyticsObject"] = r; + i[r] = i[r] || function () { + (i[r].q = i[r].q || []).push(arguments) + }, i[r].l = 1 * new Date(); + a = s.createElement(o), + m = s.getElementsByTagName(o)[0]; + a.async = 1; + a.src = g; + m.parentNode.insertBefore(a, m) +})(window, document, "script", "//www.google-analytics.com/analytics.js", "ga"); ga("create", "UA-62983413-1", "auto"); ga("send", "pageview"); /*eslint-enable */ + diff --git a/test/index.js b/test/index.js index 8dce557a..944bf8fc 100644 --- a/test/index.js +++ b/test/index.js @@ -162,4 +162,4 @@ describe("Form", function () { done(); }); }); -}); +}); \ No newline at end of file