diff --git a/_scripts/webpack.main.config.js b/_scripts/webpack.main.config.js index 9d64228c..8da85eb9 100644 --- a/_scripts/webpack.main.config.js +++ b/_scripts/webpack.main.config.js @@ -65,17 +65,29 @@ if (isDevMode) { ) } else { config.plugins.push( - new CopyWebpackPlugin([ - { - from: path.join(__dirname, '../src/data'), - to: path.join(__dirname, '../dist/data'), - }, - { - from: path.join(__dirname, '../static'), - to: path.join(__dirname, '../dist/static'), - ignore: ['.*'], - }, - ]), + new CopyWebpackPlugin({ + patterns: [ + { + from: path.join(__dirname, '../static/pwabuilder-sw.js'), + to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'), + }, + { + from: path.join(__dirname, '../static'), + to: path.join(__dirname, '../dist/web/static'), + globOptions: { + ignore: ['.*', 'pwabuilder-sw.js'], + }, + }, + { + from: path.join(__dirname, '../_icons'), + to: path.join(__dirname, '../dist/web/_icons'), + globOptions: { + ignore: ['.*'], + }, + }, + ] + } + ), new webpack.LoaderOptionsPlugin({ minimize: true, }) diff --git a/_scripts/webpack.renderer.config.js b/_scripts/webpack.renderer.config.js index 2063d767..42aa7fb4 100644 --- a/_scripts/webpack.renderer.config.js +++ b/_scripts/webpack.renderer.config.js @@ -151,13 +151,29 @@ if (isDevMode) { ) } else { config.plugins.push( - new CopyWebpackPlugin([ - { - from: path.join(__dirname, '../static'), - to: path.join(__dirname, '../dist/static'), - ignore: ['.*'], - }, - ]), + new CopyWebpackPlugin({ + patterns: [ + { + from: path.join(__dirname, '../static/pwabuilder-sw.js'), + to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'), + }, + { + from: path.join(__dirname, '../static'), + to: path.join(__dirname, '../dist/web/static'), + globOptions: { + ignore: ['.*', 'pwabuilder-sw.js'], + }, + }, + { + from: path.join(__dirname, '../_icons'), + to: path.join(__dirname, '../dist/web/_icons'), + globOptions: { + ignore: ['.*'], + }, + }, + ] + } + ), new webpack.LoaderOptionsPlugin({ minimize: true, }) diff --git a/_scripts/webpack.web.config.js b/_scripts/webpack.web.config.js index 0529c014..5cd167a0 100644 --- a/_scripts/webpack.web.config.js +++ b/_scripts/webpack.web.config.js @@ -154,22 +154,29 @@ if (isDevMode) { ) } else { config.plugins.push( - new CopyWebpackPlugin([ - { - from: path.join(__dirname, '../static/pwabuilder-sw.js'), - to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'), - }, - { - from: path.join(__dirname, '../static'), - to: path.join(__dirname, '../dist/web/static'), - ignore: ['.*', 'pwabuilder-sw.js'], - }, - { - from: path.join(__dirname, '../_icons'), - to: path.join(__dirname, '../dist/web/_icons'), - ignore: ['.*'], - }, - ]), + new CopyWebpackPlugin({ + patterns: [ + { + from: path.join(__dirname, '../static/pwabuilder-sw.js'), + to: path.join(__dirname, '../dist/web/pwabuilder-sw.js'), + }, + { + from: path.join(__dirname, '../static'), + to: path.join(__dirname, '../dist/web/static'), + globOptions: { + ignore: ['.*', 'pwabuilder-sw.js'], + }, + }, + { + from: path.join(__dirname, '../_icons'), + to: path.join(__dirname, '../dist/web/_icons'), + globOptions: { + ignore: ['.*'], + }, + }, + ] + } + ), new webpack.LoaderOptionsPlugin({ minimize: true, }) diff --git a/package.json b/package.json index d99770ee..cefa0e90 100644 --- a/package.json +++ b/package.json @@ -95,9 +95,9 @@ }, "license": "GPL-3.0-or-later", "main": "./dist/main.js", - "name": "freetube", + "name": "freetube-vue", "private": true, - "productName": "FreeTube", + "productName": "FreeTube-Vue", "repository": { "type": "git", "url": "git+https://github.com/mubaidr/vue-electron-template.git" diff --git a/src/renderer/components/ft-list-video/ft-list-video.js b/src/renderer/components/ft-list-video/ft-list-video.js index a89be5b2..0ef6accf 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.js +++ b/src/renderer/components/ft-list-video/ft-list-video.js @@ -191,11 +191,7 @@ export default Vue.extend({ this.hideViews = true } - if (typeof (this.data.uploaded_at) !== 'undefined' && this.data.uploaded_at !== null && this.data.uploaded_at.includes('watching')) { - const uploadSplit = this.data.uploaded_at.split(' ') - this.viewCount = parseInt(uploadSplit[0]) - this.isLive = true - } + this.isLive = this.data.live } } }) diff --git a/src/renderer/components/ft-video-player/ft-video-player.js b/src/renderer/components/ft-video-player/ft-video-player.js index a5282dee..14313e19 100644 --- a/src/renderer/components/ft-video-player/ft-video-player.js +++ b/src/renderer/components/ft-video-player/ft-video-player.js @@ -217,7 +217,7 @@ export default Vue.extend({ src: this.storyboardSrc }) - if (this.useDash) { + if (this.useDash || this.useHls) { this.dataSetup.plugins.httpSourceSelector = { default: 'auto' } diff --git a/src/renderer/components/watch-video-live-chat/watch-video-live-chat.css b/src/renderer/components/watch-video-live-chat/watch-video-live-chat.css new file mode 100644 index 00000000..bc2923d3 --- /dev/null +++ b/src/renderer/components/watch-video-live-chat/watch-video-live-chat.css @@ -0,0 +1,211 @@ +.relative { + position: relative; +} + +.messageContainer { + width: 100%; + height: 100%; + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + text-align: center; +} + +.message { + font-size: 18px; + color: var(--tertiary-text-color); + padding: 0; + margin: 0; +} + +.errorIcon { + width: 100%; + color: var(--tertiary-text-color); + font-size: 100px; +} + +.enableLiveChat { + display: flex; + justify-content: center; + align-items: center; + text-align: center; +} + +.superChatComments { + width: 100%; + height: 50px; + overflow-x: auto; + white-space: nowrap; +} + +.superChat { + display: inline-block; + padding: 1px; + padding-right: 10px; + margin-left: 2px; + margin-right: 2px; + height: 30px; + cursor: pointer; + border-radius: 200px 200px 200px 200px; + -webkit-border-radius: 200px 200px 200px 200px; +} + +.superChatContent { + margin-left: 32px; + margin-top: 6px; +} + +.superChat .channelThumbnail { + margin-top: 3px; + margin-left: 3px; +} + +.donationAmount { + color: var(--text-with-main-color); +} + +.openedSuperChat { + background-color: rgba(0, 0, 0, 0.7); + width: 100%; + height: 415px; + position: absolute; + margin-left: -16px; + padding-right: 32px; + bottom: -15px; + cursor: auto; + z-index: 1; +} + +.openedSuperChat .superChatMessage { + position: absolute; +} + +.superChatMessage { + width: 90%; + margin-left: 5%; + margin-right: 5%; + margin-top: 10px; + background-color: var(--primary-color); + border-radius: 5px 5px 5px 5px; + -webkit-border-radius: 5px 5px 5px 5px; +} + +.upperSuperChatMessage { + margin-top: -15px; + width: 100%; + height: 55px; + background-color: var(--primary-color-hover); +} + +.upperSuperChatMessage .channelThumbnail { + width: 45px; + margin-left: 10px; + margin-top: 5px; +} + +.upperSuperChatMessage .channelName { + color: var(--text-with-main-color); + opacity: 0.7; + position: relative; + top: 5px; + margin-left: 60px; +} + +.upperSuperChatMessage .donationAmount { + color: var(--text-with-main-color); + font-weight: bold; + margin-left: 65px; + position: relative; + bottom: 5px; +} + +.superChatMessage .chatMessage { + color: var(--text-with-main-color); + margin-left: 20px; +} + +.liveChatComments { + width: 100%; + overflow-y: auto; +} + +.comment .superChatMessage { + padding: 5px; +} + +.comment .upperSuperChatMessage { + padding: 0px; +} + +.comment { + width: 100%; + padding-top: 5px; + padding-bottom: 7px; +} + +.channelThumbnail { + width: 25px; + float: left; + border-radius: 200px 200px 200px 200px; + -webkit-border-radius: 200px 200px 200px 200px; +} + +.chatContent { + margin-left: 30px; + margin-top: 5px; + margin-bottom: 2px; + font-size: 12px; + word-wrap: break-word; +} + +.channelName { + color: var(--tertiary-text-color); + font-weight: bold; + padding-left: 5px; + padding-right: 5px; +} + +.member { + color: #4CAF50; +} + +.moderator { + color: #2196F3; +} + +.owner { + margin-right: 2px; + background-color: var(--primary-color); + color: var(--text-with-main-color); +} + +.badgeImage { + width: 14px; +} + +.scrollToBottom { + background-color: var(--accent-color); + width: 35px; + height: 35px; + position: absolute; + left: 45%; + bottom: 20px; + cursor: pointer; + border-radius: 200px 200px 200px 200px; + -webkit-border-radius: 200px 200px 200px 200px; + transition: background 0.2s ease-out; +} + +.scrollToBottom:hover { + background-color: var(--accent-color-light); + transition: background 0.2s ease-in; +} + +.icon { + color: var(--text-with-accent-color); + font-size: 22px; + position: relative; + left: 0.5rem; + top: 0.45rem; +} diff --git a/src/renderer/components/watch-video-live-chat/watch-video-live-chat.js b/src/renderer/components/watch-video-live-chat/watch-video-live-chat.js new file mode 100644 index 00000000..7cd22ecf --- /dev/null +++ b/src/renderer/components/watch-video-live-chat/watch-video-live-chat.js @@ -0,0 +1,240 @@ +import Vue from 'vue' +import FtLoader from '../ft-loader/ft-loader.vue' +import FtCard from '../ft-card/ft-card.vue' +import FtButton from '../ft-button/ft-button.vue' +import FtListVideo from '../ft-list-video/ft-list-video.vue' + +import $ from 'jquery' +import autolinker from 'autolinker' +import { LiveChat } from 'youtube-chat' + +export default Vue.extend({ + name: 'WatchVideoLiveChat', + components: { + 'ft-loader': FtLoader, + 'ft-card': FtCard, + 'ft-button': FtButton, + 'ft-list-video': FtListVideo + }, + props: { + videoId: { + type: String, + required: true + }, + channelName: { + type: String, + required: true + } + }, + data: function () { + return { + liveChat: null, + isLoading: true, + hasError: false, + hasEnded: false, + showEnableChat: false, + errorMessage: '', + stayAtBottom: true, + showSuperChat: false, + showScrollToBottom: false, + comments: [], + superChatComments: [], + superChat: { + author: { + name: '', + thumbnail: '' + }, + message: [ + '' + ], + superChat: { + amount: '' + } + } + } + }, + computed: { + usingElectron: function () { + return this.$store.getters.getUsingElectron + }, + + backendPreference: function () { + return this.$store.getters.getBackendPreference + }, + + backendFallback: function () { + return this.$store.getters.getBackendFallback + }, + + chatHeight: function () { + if (this.superChatComments.length > 0) { + return '390px' + } else { + return '445px' + } + } + }, + created: function () { + if (!this.usingElectron) { + this.hasError = true + this.errorMessage = 'Live Chat is currently not supported in this build.' + } else { + switch (this.backendPreference) { + case 'local': + console.log('Getting Chat') + this.getLiveChatLocal() + break + case 'invidious': + if (this.backendFallback) { + this.getLiveChatLocal() + } else { + this.hasError = true + this.errorMessage = 'Live Chat is currently not supported with the Invidious API. A direct connection to YouTube is required.' + this.showEnableChat = true + this.isLoading = false + } + break + } + } + }, + methods: { + enableLiveChat: function () { + this.hasError = false + this.showEnableChat = false + this.isLoading = true + this.getLiveChatLocal() + }, + + getLiveChatLocal: function () { + this.liveChat = new LiveChat({ liveId: this.videoId }) + + this.isLoading = false + + this.liveChat.on('start', (liveId) => { + console.log('Live chat is enabled') + this.isLoading = false + }) + + this.liveChat.on('end', (reason) => { + console.log('Live chat has ended') + console.log(reason) + }) + + this.liveChat.on('error', (err) => { + this.hasError = true + this.errorMessage = err + this.showEnableChat = false + }) + + this.liveChat.on('comment', (comment) => { + this.parseLiveChatComment(comment) + }) + + this.liveChat.start() + }, + + parseLiveChatComment: function (comment) { + if (this.hasEnded) { + return + } + + comment.messageHtml = '' + + comment.message.forEach((text) => { + comment.messageHtml = comment.messageHtml + text.text + }) + + comment.messageHtml = autolinker.link(comment.messageHtml) + + const liveChatComments = $('.liveChatComments') + + if (typeof (liveChatComments.get(0)) === 'undefined' && this.comments.length !== 0) { + this.liveChat.stop() + return + } + + this.comments.push(comment) + + if (typeof (comment.superchat) !== 'undefined') { + this.$store.dispatch('getRandomColorClass').then((data) => { + comment.superchat.colorClass = data + + this.superChatComments.unshift(comment) + + setTimeout(() => { + this.removeFromSuperChat(comment.id) + }, 120000) + }) + } + + if (comment.author.name[0] === 'Ge' || comment.author.name[0] === 'Ne') { + this.$store.dispatch('getRandomColorClass').then((data) => { + comment.superChat = { + amount: '$5.00', + colorClass: data + } + + this.superChatComments.unshift(comment) + + setTimeout(() => { + this.removeFromSuperChat(comment.id) + }, 120000) + }) + } + + if (this.stayAtBottom) { + liveChatComments.animate({ scrollTop: liveChatComments.prop('scrollHeight') }) + } + }, + + removeFromSuperChat: function (id) { + this.superChatComments = this.superChatComments.filter((comment) => { + return comment.id !== id + }) + }, + + showSuperChatComment: function (comment) { + if (this.superChat.id === comment.id && this.showSuperChat) { + this.showSuperChat = false + } else { + this.superChat = comment + this.showSuperChat = true + } + }, + + onScroll: function (event) { + const liveChatComments = $('.liveChatComments').get(0) + const scrollTop = liveChatComments.scrollTop + const scrollHeight = liveChatComments.scrollHeight + const clientHeight = liveChatComments.clientHeight + if (event.wheelDelta >= 0 && this.stayAtBottom) { + $('.liveChatComments').data('animating', 0) + this.stayAtBottom = false + + if (liveChatComments.scrollHeight > liveChatComments.clientHeight) { + this.showScrollToBottom = true + } + } else if (event.wheelDelta < 0 && !this.stayAtBottom) { + if ((liveChatComments.scrollHeight - liveChatComments.scrollTop) === liveChatComments.clientHeight) { + this.scrollToBottom() + } + } + }, + + scrollToBottom: function () { + const liveChatComments = $('.liveChatComments') + liveChatComments.animate({ scrollTop: liveChatComments.prop('scrollHeight') }) + this.stayAtBottom = true + this.showScrollToBottom = false + }, + + preventDefault: function (event) { + event.stopPropagation() + event.preventDefault() + } + }, + beforeRouteLeave: function () { + this.liveChat.stop() + this.hasEnded = true + } +}) diff --git a/src/renderer/components/watch-video-live-chat/watch-video-live-chat.vue b/src/renderer/components/watch-video-live-chat/watch-video-live-chat.vue new file mode 100644 index 00000000..4b0a542f --- /dev/null +++ b/src/renderer/components/watch-video-live-chat/watch-video-live-chat.vue @@ -0,0 +1,198 @@ + + + + + + {{ errorMessage }} + + + + + + + Live chat is enabled. Chat messages will appear here once sent. + + + + Live Chat + + + + + + {{ comment.superchat.amount }} + + + + + + preventDefault(e)" + > + + + + {{ superChat.author.name }} + + + {{ superChat.superchat.amount }} + + + + + + + onScroll(e)" + > + + + + + + {{ comment.author.name }} + + + {{ comment.superchat.amount }} + + + + + + + + + + {{ comment.author.name }} + + + + + + + + + + + + + + + + + + + diff --git a/src/renderer/store/modules/utils.js b/src/renderer/store/modules/utils.js index 07b72755..c45e956b 100644 --- a/src/renderer/store/modules/utils.js +++ b/src/renderer/store/modules/utils.js @@ -8,7 +8,25 @@ const state = { time: '', type: 'all', duration: '' - } + }, + colorClasses: [ + 'mainRed', + 'mainPink', + 'mainPurple', + 'mainDeepPurple', + 'mainIndigo', + 'mainBlue', + 'mainLightBlue', + 'mainCyan', + 'mainTeal', + 'mainGreen', + 'mainLightGreen', + 'mainLime', + 'mainYellow', + 'mainAmber', + 'mainOrange', + 'mainDeepOrange' + ] } const getters = { @@ -29,7 +47,12 @@ const getters = { } } -const actions = {} +const actions = { + getRandomColorClass () { + const randomInt = Math.floor(Math.random() * state.colorClasses.length) + return state.colorClasses[randomInt] + } +} const mutations = { toggleSideNav (state) { diff --git a/src/renderer/views/Watch/Watch.css b/src/renderer/views/Watch/Watch.css index c7bac958..0500876b 100644 --- a/src/renderer/views/Watch/Watch.css +++ b/src/renderer/views/Watch/Watch.css @@ -107,6 +107,7 @@ .watchVideoPlaylist { float: none; margin: 0 auto; + margin-bottom: 10px; width: 85%; max-width: none; position: static; diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 840f6e76..7b268fbf 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -8,6 +8,7 @@ import FtVideoPlayer from '../../components/ft-video-player/ft-video-player.vue' import WatchVideoInfo from '../../components/watch-video-info/watch-video-info.vue' import WatchVideoDescription from '../../components/watch-video-description/watch-video-description.vue' import WatchVideoComments from '../../components/watch-video-comments/watch-video-comments.vue' +import WatchVideoLiveChat from '../../components/watch-video-live-chat/watch-video-live-chat.vue' import WatchVideoPlaylist from '../../components/watch-video-playlist/watch-video-playlist.vue' import WatchVideoRecommendations from '../../components/watch-video-recommendations/watch-video-recommendations.vue' @@ -21,6 +22,7 @@ export default Vue.extend({ 'watch-video-info': WatchVideoInfo, 'watch-video-description': WatchVideoDescription, 'watch-video-comments': WatchVideoComments, + 'watch-video-live-chat': WatchVideoLiveChat, 'watch-video-playlist': WatchVideoPlaylist, 'watch-video-recommendations': WatchVideoRecommendations, }, @@ -185,16 +187,21 @@ export default Vue.extend({ this.isLive = result.player_response.videoDetails.isLive if (this.isLive) { - this.showLegacyPlayer = false - this.showDashPlayer = true - this.videoSourceList = [ - { - url: 'https://invidious.snopyta.org/api/manifest/dash/id/EEIk7gwjgIM', - type: 'application/dash+xml', + this.showLegacyPlayer = true + this.showDashPlayer = false + + this.videoSourceList = result.formats.filter((format) => { + if (typeof (format.mimeType) !== 'undefined') { + return format.mimeType.includes('video/ts') + } + }).map((format) => { + return { + url: format.url, + type: 'application/x-mpegURL', label: 'Dash', - qualityLabel: 'Auto' - }, - ] + qualityLabel: format.qualityLabel + } + }) } else { this.videoSourceList = result.player_response.streamingData.formats } @@ -271,6 +278,7 @@ export default Vue.extend({ this.videoPublished = result.published * 1000 this.videoDescriptionHtml = result.descriptionHtml this.recommendedVideos = result.recommendedVideos + this.isLive = result.liveNow this.captionSourceList = result.captions.map(caption => { caption.url = this.invidiousInstance + caption.url caption.type = '' @@ -278,7 +286,35 @@ export default Vue.extend({ return caption }) - if (this.forceLocalBackendForLegacy) { + if (this.isLive) { + this.showLegacyPlayer = true + this.showDashPlayer = false + this.activeFormat = 'legacy' + + this.videoSourceList = [ + { + url: result.hlsUrl, + type: 'application/x-mpegURL', + label: 'Dash', + qualityLabel: 'Live' + } + ] + + // Grabs the adaptive formats from Invidious. Might be worth making these work. + // The type likely needs to be changed in order for these to be played properly. + // this.videoSourceList = result.adaptiveFormats.filter((format) => { + // if (typeof (format.type) !== 'undefined') { + // return format.type.includes('video/mp4') + // } + // }).map((format) => { + // return { + // url: format.url, + // type: 'application/x-mpegURL', + // label: 'Dash', + // qualityLabel: format.qualityLabel + // } + // }) + } else if (this.forceLocalBackendForLegacy) { this.getLegacyFormats() } else { this.videoSourceList = result.formatStreams.reverse() @@ -302,8 +338,6 @@ export default Vue.extend({ checkIfPlaylist: function () { if (typeof (this.$route.query) !== 'undefined') { - console.log('defined') - console.log(this.$route.query) this.playlistId = this.$route.query.playlistId if (typeof (this.playlistId) !== 'undefined') { @@ -325,7 +359,7 @@ export default Vue.extend({ }, enableDashFormat: function () { - if (this.activeFormat === 'dash') { + if (this.activeFormat === 'dash' || this.isLive) { return } @@ -361,6 +395,10 @@ export default Vue.extend({ handleVideoError: function(error) { console.log(error) + if (this.isLive) { + return + } + if (error.code === 4) { if (this.activeFormat === 'dash') { console.log( diff --git a/src/renderer/views/Watch/Watch.vue b/src/renderer/views/Watch/Watch.vue index 03fcacbd..cbc276f3 100644 --- a/src/renderer/views/Watch/Watch.vue +++ b/src/renderer/views/Watch/Watch.vue @@ -41,11 +41,18 @@ :class="{ theatreWatchVideo: useTheatreMode }" /> +
+ {{ errorMessage }} +
+ Live chat is enabled. Chat messages will appear here once sent. +
+ + {{ comment.superchat.amount }} + +
+ {{ superChat.author.name }} +
+ {{ superChat.superchat.amount }} +
+
+ {{ comment.author.name }} +
+ {{ comment.superchat.amount }} +
+ + {{ comment.author.name }} + + + + + + +
+ {{ comment.author.name }} +
++ {{ comment.superchat.amount }} +
++
++ + {{ comment.author.name }} + + + + + + +
+