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 @@ + + +