Merge branch 'master' into ft-toast

This commit is contained in:
Kyle Watson 2020-06-27 16:10:05 +01:00 committed by GitHub
commit f31bf4f881
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 2587 additions and 2077 deletions

View File

@ -6,8 +6,6 @@ name: Build
on: on:
push: push:
branches: [ master ] branches: [ master ]
pull_request:
branches: [ master ]
jobs: jobs:
build: build:
@ -25,6 +23,7 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm ci - run: npm ci
- run: npm run lint
- run: npm run build --if-present - run: npm run build --if-present
- name: Upload .deb Artifact - name: Upload .deb Artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2

26
.github/workflows/linter.yml vendored Normal file
View File

@ -0,0 +1,26 @@
# This is a basic workflow to help you get started with Actions
name: Linter
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
pull_request:
branches: [ master ]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
lint:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.X
uses: actions/setup-node@v1
with:
node-version: 12.X
- run: npm ci
- run: npm run lint

2338
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,9 +8,9 @@
"url": "https://github.com/FreeTubeApp/FreeTube/issues" "url": "https://github.com/FreeTubeApp/FreeTube/issues"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.28", "@fortawesome/fontawesome-svg-core": "^1.2.29",
"@fortawesome/free-solid-svg-icons": "^5.13.0", "@fortawesome/free-solid-svg-icons": "^5.13.1",
"@fortawesome/vue-fontawesome": "^0.1.9", "@fortawesome/vue-fontawesome": "^0.1.10",
"@silvermine/videojs-quality-selector": "^1.2.4", "@silvermine/videojs-quality-selector": "^1.2.4",
"autolinker": "^3.14.1", "autolinker": "^3.14.1",
"bulma-pro": "^0.2.0", "bulma-pro": "^0.2.0",
@ -31,56 +31,58 @@
"videojs-vtt-thumbnails": "0.0.13", "videojs-vtt-thumbnails": "0.0.13",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-electron": "^1.0.6", "vue-electron": "^1.0.6",
"vue-router": "^3.3.2", "vue-router": "^3.3.4",
"vuex": "^3.4.0", "vuex": "^3.4.0",
"xml2json": "^0.12.0", "xml2json": "^0.12.0",
"youtube-chat": "^1.1.0", "youtube-chat": "^1.1.0",
"youtube-comments-fetch": "^1.0.1", "youtube-comments-fetch": "^1.0.1",
"youtube-comments-task": "^1.3.15", "youtube-comments-task": "^1.3.15",
"youtube-suggest": "^1.1.0", "youtube-suggest": "^1.1.0",
"yt-xml2vtt": "^1.0.1", "yt-channel-info": "git+https://github.com/FreeTubeApp/yt-channel-info.git",
"yt-xml2vtt": "^1.1.1",
"ytdl-core": "^3.1.1", "ytdl-core": "^3.1.1",
"ytpl": "^0.1.21", "ytpl": "^0.1.21",
"ytsr": "^0.1.15" "ytsr": "^0.1.15"
}, },
"description": "A private YouTube client", "description": "A private YouTube client",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.10.2", "@babel/core": "^7.10.3",
"@babel/plugin-proposal-class-properties": "^7.10.1", "@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-proposal-object-rest-spread": "^7.10.1", "@babel/plugin-proposal-object-rest-spread": "^7.10.3",
"@babel/preset-env": "^7.10.2", "@babel/preset-env": "^7.10.3",
"@babel/preset-typescript": "^7.10.1", "@babel/preset-typescript": "^7.10.1",
"@typescript-eslint/eslint-plugin": "^3.1.0", "@typescript-eslint/eslint-plugin": "^3.3.0",
"@typescript-eslint/parser": "^3.1.0", "@typescript-eslint/parser": "^3.3.0",
"acorn": "^7.2.0", "acorn": "^7.3.1",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"copy-webpack-plugin": "^6.0.2", "copy-webpack-plugin": "^6.0.2",
"css-loader": "^3.5.3", "css-loader": "^3.6.0",
"devtron": "^1.4.0", "devtron": "^1.4.0",
"electron": "^8.3.0", "electron": "^9.0.4",
"electron-builder": "^22.7.0", "electron-builder": "^22.7.0",
"electron-builder-squirrel-windows": "^22.7.0", "electron-builder-squirrel-windows": "^22.7.0",
"electron-debug": "^3.1.0", "electron-debug": "^3.1.0",
"electron-rebuild": "^1.11.0", "electron-rebuild": "^1.11.0",
"eslint": "^7.1.0", "eslint": "^7.3.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-config-standard": "^14.1.1", "eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.20.2", "eslint-plugin-import": "^2.21.2",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1", "eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"fast-glob": "^3.2.2", "fast-glob": "^3.2.4",
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
"html-webpack-plugin": "^4.3.0", "html-webpack-plugin": "^4.3.0",
"jest": "^26.0.1", "jest": "^26.0.1",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"node-abi": "^2.18.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"sass": "^1.26.7", "sass": "^1.26.8",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"style-loader": "^1.2.1", "style-loader": "^1.2.1",
"tree-kill": "1.2.2", "tree-kill": "1.2.2",
@ -92,7 +94,7 @@
"vue-style-loader": "^4.1.2", "vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11",
"webpack": "^4.43.0", "webpack": "^4.43.0",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0" "webpack-dev-server": "^3.11.0"
}, },
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
@ -116,7 +118,8 @@
"jest": "jest", "jest": "jest",
"jest:coverage": "jest --collect-coverage", "jest:coverage": "jest --collect-coverage",
"jest:watch": "jest --watch", "jest:watch": "jest --watch",
"lint": "eslint --fix --ext .js,.ts,.vue ./", "lint-fix": "eslint --fix --ext .js,.ts,.vue ./",
"lint": "eslint --ext .js,.ts,.vue ./",
"pack": "run-p pack:main pack:renderer pack:web pack:workers", "pack": "run-p pack:main pack:renderer pack:web pack:workers",
"pack:main": "webpack --mode=production --env.NODE_ENV=production --hide-modules --config _scripts/webpack.main.config.js", "pack:main": "webpack --mode=production --env.NODE_ENV=production --hide-modules --config _scripts/webpack.main.config.js",
"pack:renderer": "webpack --mode=production --env.NODE_ENV=production --hide-modules --config _scripts/webpack.renderer.config.js", "pack:renderer": "webpack --mode=production --env.NODE_ENV=production --hide-modules --config _scripts/webpack.renderer.config.js",

View File

@ -14,30 +14,36 @@ app.setName(productName)
// disable electron warning // disable electron warning
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true' process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
const gotTheLock = app.requestSingleInstanceLock() // const gotTheLock = app.requestSingleInstanceLock()
const isDev = process.env.NODE_ENV === 'development' const isDev = process.env.NODE_ENV === 'development'
const isDebug = process.argv.includes('--debug') const isDebug = process.argv.includes('--debug')
let mainWindow let mainWindow
// CORS somehow gets re-enabled in Electron v9.0.4
// This line disables it.
// This line can possible be removed if the issue is fixed upstream
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')
// TODO: Uncomment if needed
// only allow single instance of application // only allow single instance of application
if (!isDev) { // if (!isDev) {
if (gotTheLock) { // if (gotTheLock) {
app.on('second-instance', () => { // app.on('second-instance', () => {
// Someone tried to run a second instance, we should focus our window. // // Someone tried to run a second instance, we should focus our window.
if (mainWindow && mainWindow.isMinimized()) { // if (mainWindow && mainWindow.isMinimized()) {
mainWindow.restore() // mainWindow.restore()
} // }
mainWindow.focus() // mainWindow.focus()
}) // })
} else { // } else {
app.quit() // app.quit()
process.exit(0) // process.exit(0)
} // }
} else { // } else {
require('electron-debug')({ // require('electron-debug')({
showDevTools: !(process.env.RENDERER_REMOTE_DEBUGGING === 'true') // showDevTools: !(process.env.RENDERER_REMOTE_DEBUGGING === 'true')
}) // })
} // }
async function installDevTools () { async function installDevTools () {
try { try {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="129.158" height="129.158" viewBox="0 0 34.173 34.173"><g transform="translate(26.909 -78.793)" paint-order="fill markers stroke"><circle cx="-9.822" cy="95.88" r="16.557" fill="none" stroke="#ddd" stroke-width="1.058" stroke-linecap="round" stroke-linejoin="round"/><path style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" d="M-10.713 89.306l-.743 2.64 6.893 13.75h2.034zm-.743 2.64l-3.976 13.423.508.15 4.49-15.177zm-4.933 13.228v.53h2.813v-.53z" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#ddd"/><circle cx="-10.763" cy="87.186" r="1.105" fill="#00b6f0"/></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="129.158" height="129.158" viewBox="0 0 34.173 34.173"><g transform="translate(26.909 -78.793)" paint-order="fill markers stroke"><circle cx="-9.822" cy="95.88" r="16.557" fill="none" stroke="#212121" stroke-width="1.058" stroke-linecap="round" stroke-linejoin="round"/><path style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" d="M-10.713 89.306l-.743 2.64 6.893 13.75h2.034zm-.743 2.64l-3.976 13.423.508.15 4.49-15.177zm-4.933 13.228v.53h2.813v-.53z" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#212121"/><circle cx="-10.763" cy="87.186" r="1.105" fill="#00b6f0"/></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -17,7 +17,7 @@
font-weight: 500; font-weight: 500;
vertical-align: middle; vertical-align: middle;
margin: 5px; margin: 5px;
box-shadow: 0 0 2px -2px rgba(29, 39, 231, .1), 0 0 3px 0 rgba(29, 39, 231, .1), 0 0 5px 0 rgba(29, 39, 231, .1), 0 2px 2px -4px rgba(29, 39, 231, .1), 0 4px 8px 0 rgba(29, 39, 231, .1), 0 2px 15px 0 rgba(29, 39, 231, .1); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
} }
.ripple { .ripple {

View File

@ -15,7 +15,7 @@ export default Vue.extend({
props: { props: {
label: { label: {
type: String, type: String,
required: true default: ''
}, },
textColor: { textColor: {
type: String, type: String,

View File

@ -8,7 +8,9 @@
}" }"
@click="$emit('click')" @click="$emit('click')"
> >
<slot>
{{ label }} {{ label }}
</slot>
</button> </button>
</template> </template>

View File

@ -1,116 +0,0 @@
.ftIconButton {
display: flex;
flex-flow: row wrap;
justify-content: space-evenly;
position: relative;
}
.iconButton {
width: 1em;
height: 1em;
padding: 10px;
font-size: 20px;
border-radius: 50%;
cursor: pointer;
-moz-transition: background 0.2s ease-out;
-o-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
}
.shadow {
box-shadow: 0 1px 2px rgba(0,0,0,.5);
}
.iconButton:hover {
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.base {
background-color: var(--card-bg-color);
color: var(--primary-text-color);
}
.base:hover {
background-color: var(--side-nav-hover-color);
}
.base:active {
background-color: var(--side-nav-active-color);
}
.primary {
background-color: var(--primary-color);
color: var(--text-with-main-color);
}
.primary:hover {
background-color: var(--primary-color-hover);
}
.primary:active {
background-color: var(--primary-color-active);
}
.secondary {
background-color: var(--accent-color);
color: var(--text-with-accent-color);
}
.secondary:hover {
background-color: var(--accent-color-hover);
}
.secondary:active {
background-color: var(--accent-color-active);
}
.iconDropdown {
position: absolute;
text-align: center;
list-style-type: none;
z-index: 100;
margin-top: 45px;
font-size: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,.5);
background-color: var(--card-bg-color);
color: var(--secondary-text-color);
}
.iconDropdown p {
padding: 10px;
margin: 0;
white-space: nowrap;
cursor: pointer;
-moz-transition: background 0.2s ease-out;
-o-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
}
.iconDropdown p:hover {
background-color: var(--side-nav-hover-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.iconDropdown p:active {
background-color: var(--side-nav-active-color);
-moz-transition: background 0.1s ease-in;
-o-transition: background 0.1s ease-in;
transition: background 0.1s ease-in;
}
.iconDropdown p a {
text-decoration: none;
color: inherit;
}
.left {
right: 50%;
}
.right {
left: 50%;
}

View File

@ -19,10 +19,18 @@ export default Vue.extend({
type: Boolean, type: Boolean,
default: true default: true
}, },
dropdownPosition: { forceDropdown: {
type: Boolean,
default: false
},
dropdownPositionX: {
type: String, type: String,
default: 'center' default: 'center'
}, },
dropdownPositionY: {
type: String,
default: 'bottom'
},
dropdownNames: { dropdownNames: {
type: Array, type: Array,
default: () => { return [] } default: () => { return [] }
@ -43,7 +51,7 @@ export default Vue.extend({
}, },
handleIconClick: function () { handleIconClick: function () {
if (this.dropdownNames.length > 0 && this.dropdownValues.length > 0) { if (this.forceDropdown || (this.dropdownNames.length > 0 && this.dropdownValues.length > 0)) {
this.toggleDropdown() this.toggleDropdown()
} else { } else {
this.$emit('click') this.$emit('click')

View File

@ -0,0 +1,85 @@
.ftIconButton
display: flex
flex-flow: row wrap
justify-content: space-evenly
position: relative
.iconButton
width: 1em
height: 1em
padding: 10px
font-size: 20px
border-radius: 50%
cursor: pointer
transition: background 0.15s ease-out
&.shadow
box-shadow: 0 1px 2px rgba(0,0,0,.5)
&.base
background-color: var(--card-bg-color)
color: var(--primary-text-color)
&:hover
background-color: var(--side-nav-hover-color)
&:active
background-color: var(--side-nav-active-color)
&.primary
background-color: var(--primary-color)
color: var(--text-with-main-color)
&:hover
background-color: var(--primary-color-hover)
&:active
background-color: var(--primary-color-active)
&.secondary
background-color: var(--accent-color)
color: var(--text-with-accent-color)
&:hover
background-color: var(--accent-color-hover)
&:active
background-color: var(--accent-color-active)
.iconDropdown
position: absolute
text-align: center
list-style-type: none
z-index: 3
margin-top: 45px
font-size: 12px
box-shadow: 0 1px 2px rgba(0,0,0,.5)
background-color: var(--side-nav-color)
color: var(--secondary-text-color)
user-select: none
&.left
right: calc(50% - 20px)
&.right
left: calc(50% - 20px)
.list
margin: 0
padding: 0
list-style-type: none
.listItem
padding: 10px
margin: 0
white-space: nowrap
cursor: pointer
transition: background 0.2s ease-out
&:hover
background-color: var(--side-nav-hover-color)
transition: background 0.2s ease-in
&:active
background-color: var(--side-nav-active-color)
transition: background 0.1s ease-in

View File

@ -13,24 +13,34 @@
@click="handleIconClick" @click="handleIconClick"
/> />
<div <div
v-if="dropdownNames.length > 0 && showDropdown" v-if="showDropdown"
class="iconDropdown" class="iconDropdown"
:class="{ :class="{
left: dropdownPosition === 'left', left: dropdownPositionX === 'left',
right: dropdownPosition === 'right', right: dropdownPositionX === 'right',
center: dropdownPosition === 'center' center: dropdownPositionX === 'center',
bottom: dropdownPositionY === 'bottom',
top: dropdownPositionY === 'top'
}" }"
> >
<p <slot>
<ul
v-if="dropdownNames.length > 0"
class="list"
>
<li
v-for="(label, index) in dropdownNames" v-for="(label, index) in dropdownNames"
:key="index" :key="index"
class="listItem"
@click="handleDropdownClick(index)" @click="handleDropdownClick(index)"
> >
{{ label }} {{ label }}
</p> </li>
</ul>
</slot>
</div> </div>
</div> </div>
</template> </template>
<script src="./ft-icon-button.js" /> <script src="./ft-icon-button.js" />
<style scoped src="./ft-icon-button.css" /> <style scoped lang="sass" src="./ft-icon-button.sass" />

View File

@ -49,6 +49,7 @@ export default Vue.extend({
}, },
mounted: function () { mounted: function () {
this.id = this._uid this.id = this._uid
this.inputData = this.value
setTimeout(this.addListener, 200) setTimeout(this.addListener, 200)
}, },
@ -57,9 +58,8 @@ export default Vue.extend({
this.$emit('click', this.inputData) this.$emit('click', this.inputData)
}, },
handleInput: function (input) { handleInput: function () {
this.inputData = input this.$emit('input', this.inputData)
this.$emit('input', input)
}, },
addListener: function () { addListener: function () {

View File

@ -14,8 +14,8 @@
</label> </label>
<input <input
:id="id" :id="id"
v-model="inputData"
:list="idDataList" :list="idDataList"
:value="value"
class="ft-input" class="ft-input"
type="text" type="text"
:placeholder="placeholder" :placeholder="placeholder"

View File

@ -190,3 +190,58 @@
height: 35px; height: 35px;
overflow: hidden; overflow: hidden;
} }
.videoRecommendation.list {
height: 110px;
}
.videoRecommendation.list .videoThumbnail {
width: 180px;
height: 100px;
}
.videoRecommendation.list .videoThumbnail img {
height: 100px;
}
.videoRecommendation.list .videoTitle {
font-size: 12px;
margin-left: 185px;
}
.videoRecommendation.list .channelName {
margin-left: 185px;
}
.videoRecommendation.list .viewCount {
margin-left: 5px;
}
.playlistItem .list {
height: 60px;
width: calc(100% - 30px);
}
.playlistItem .list .videoThumbnail {
width: 100px;
height: 60px;
}
.playlistItem .list .videoThumbnail img {
height: 60px;
}
.playlistItem .list .videoTitle {
font-size: 12px;
margin-left: 105px;
margin-right: 30px;
}
.playlistItem .list .channelName {
margin-left: 105px;
margin-right: 30px;
}
.playlistItem .list .viewCount {
margin-left: 5px;
}

View File

@ -111,6 +111,7 @@ export default Vue.extend({
if (typeof (this.data.descriptionHtml) !== 'undefined' || if (typeof (this.data.descriptionHtml) !== 'undefined' ||
typeof (this.data.index) !== 'undefined' || typeof (this.data.index) !== 'undefined' ||
typeof (this.data.authorId) !== 'undefined' ||
typeof (this.data.publishedText) !== 'undefined' || typeof (this.data.publishedText) !== 'undefined' ||
typeof (this.data.authorThumbnails) === 'object' typeof (this.data.authorThumbnails) === 'object'
) { ) {

View File

@ -42,7 +42,7 @@
title="More Options" title="More Options"
theme="base" theme="base"
:use-shadow="false" :use-shadow="false"
dropdown-position="left" dropdown-position-x="left"
:dropdown-names="optionsNames" :dropdown-names="optionsNames"
:dropdown-values="optionsValues" :dropdown-values="optionsValues"
@click="handleOptionsClick" @click="handleOptionsClick"

View File

@ -0,0 +1,96 @@
import Vue from 'vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
import FtButton from '../ft-button/ft-button.vue'
export default Vue.extend({
name: 'FtShareButton',
components: {
'ft-icon-button': FtIconButton,
'ft-button': FtButton
},
props: {
id: {
type: String,
required: true
}
},
computed: {
invidiousInstance: function () {
return this.$store.getters.getInvidiousInstance
},
usingElectron: function () {
return this.$store.getters.getUsingElectron
},
invidiousURL() {
return `${this.invidiousInstance}/watch?v=${this.id}`
},
invidiousEmbedURL() {
return `${this.invidiousInstance}/embed/${this.id}`
},
youtubeURL() {
return `https://www.youtube.com/watch?v=${this.id}`
},
youtubeEmbedURL() {
return `https://www.youtube-nocookie.com/embed/${this.id}`
},
},
methods: {
copy(text) {
navigator.clipboard.writeText(text)
},
open(url) {
if (this.usingElectron) {
const shell = require('electron').shell
shell.openExternal(url)
}
},
openInvidious() {
this.open(this.invidiousURL)
this.$refs.iconButton.toggleDropdown()
},
copyInvidious() {
this.copy(this.invidiousURL)
this.$refs.iconButton.toggleDropdown()
},
openYoutube() {
this.open(this.youtubeURL)
this.$refs.iconButton.toggleDropdown()
},
copyYoutube() {
this.copy(this.youtubeURL)
this.$refs.iconButton.toggleDropdown()
},
openYoutubeEmbed() {
this.open(this.youtubeEmbedURL)
this.$refs.iconButton.toggleDropdown()
},
copyYoutubeEmbed() {
this.copy(this.youtubeEmbedURL)
this.$refs.iconButton.toggleDropdown()
},
openInvidiousEmbed() {
this.open(this.invidiousEmbedURL)
this.$refs.iconButton.toggleDropdown()
},
copyInvidiousEmbed() {
this.copy(this.invidiousEmbedURL)
this.$refs.iconButton.toggleDropdown()
},
}
})

View File

@ -0,0 +1,55 @@
.shareLinks
display: grid
grid-template-rows: auto auto
grid-auto-flow: column
padding: 12px
width: max-content
.header
font-size: 18px
font-weight: bold
margin: 4px 0px 8px
color: var(--primary-text-color)
.buttons
display: flex
flex-direction: column
.action
padding: 6px
.divider
grid-row: span 3
margin: 0px 12px
width: 1px
background: var(--tertiary-text-color)
.youtubeLogo
height: 18px
width: auto
@at-root
.dark &
filter: brightness(0.868)
.light &
filter: invert(0.87)
.invidious
display: flex
justify-content: center
letter-spacing: -0.4px
.invidiousLogo
display: inline-block
width: 20px
height: 20px
background-size: cover
margin-right: 2px
@at-root
.dark &
background-image: url(~../../assets/img/invidious-logo-dark.svg)
.light &
background-image: url(~../../assets/img/invidious-logo-light.svg)

View File

@ -0,0 +1,97 @@
<template>
<ft-icon-button
ref="iconButton"
title="Share Video"
theme="secondary"
icon="share-alt"
dropdown-position-x="left"
:force-dropdown="true"
>
<div class="shareLinks">
<div class="header">
<img
class="youtubeLogo"
src="~../../assets/img/yt_logo_mono_dark.png"
alt="YouTube"
width="794"
height="178"
>
</div>
<div class="buttons">
<ft-button
class="action"
@click="copyYoutube()"
>
<font-awesome-icon icon="copy" />
Copy link
</ft-button>
<ft-button
class="action"
@click="openYoutube()"
>
<font-awesome-icon icon="globe" />
Open link
</ft-button>
<ft-button
class="action"
background-color="var(--accent-color-active)"
@click="copyYoutubeEmbed()"
>
<font-awesome-icon icon="copy" />
Copy embed
</ft-button>
<ft-button
class="action"
background-color="var(--accent-color-active)"
@click="openYoutubeEmbed()"
>
<font-awesome-icon icon="globe" />
Open embed
</ft-button>
</div>
<div class="divider" />
<div class="header invidious">
<span class="invidiousLogo" />Invidious
</div>
<div class="buttons">
<ft-button
class="action"
@click="copyInvidious()"
>
<font-awesome-icon icon="copy" />
Copy link
</ft-button>
<ft-button
class="action"
@click="openInvidious()"
>
<font-awesome-icon icon="globe" />
Open link
</ft-button>
<ft-button
class="action"
background-color="var(--accent-color-active)"
@click="copyInvidiousEmbed()"
>
<font-awesome-icon icon="copy" />
Copy embed
</ft-button>
<ft-button
class="action"
background-color="var(--accent-color-active)"
@click="openInvidiousEmbed()"
>
<font-awesome-icon icon="globe" />
Open embed
</ft-button>
</div>
</div>
</ft-icon-button>
</template>
<script src="./ft-share-button.js" />
<style scoped lang="sass" src="./ft-share-button.sass" />

View File

@ -4,11 +4,11 @@
> >
<input <input
:id="id" :id="id"
v-model.number="currentValue"
type="range" type="range"
:min="minValue" :min="minValue"
:max="maxValue" :max="maxValue"
:step="step" :step="step"
v-model.number="currentValue"
@change="$emit('change', $event.target.value)" @change="$emit('change', $event.target.value)"
> >
<span> <span>

View File

@ -1,12 +1,12 @@
<template> <template>
<div> <div>
<input <input
type="checkbox"
:id="id" :id="id"
v-model="currentValue"
type="checkbox"
name="set-name" name="set-name"
class="switch-input" class="switch-input"
:checked='currentValue' :checked="currentValue"
v-model="currentValue"
@change="$emit('change', currentValue)" @change="$emit('change', currentValue)"
> >
<label <label

View File

@ -1,9 +1,7 @@
.relative { .relative {
position: relative; position: relative;
width: 85%;
} }
.ftVideoPlayer { .ftVideoPlayer {
width: 85%;
max-height: 50vh; max-height: 50vh;
} }

View File

@ -221,7 +221,11 @@ export default Vue.extend({
qualitySelector(videojs, { showQualitySelectionLabelInControlBar: true }) qualitySelector(videojs, { showQualitySelectionLabelInControlBar: true })
} }
this.player = videojs(videoPlayer) this.player = videojs(videoPlayer, {
userActions: {
hotkeys: this.keyboardShortcutHandler
}
})
this.player.volume(this.volume) this.player.volume(this.volume)
this.player.playbackRate(this.defaultPlayback) this.player.playbackRate(this.defaultPlayback)
@ -245,7 +249,7 @@ export default Vue.extend({
}, 200) }, 200)
} }
$(document).on('keydown', this.keyboardShortcutHandler) // $(document).on('keydown', this.keyboardShortcutHandler)
this.player.on('mousemove', this.hideMouseTimeout) this.player.on('mousemove', this.hideMouseTimeout)
this.player.on('mouseleave', this.removeMouseTimeout) this.player.on('mouseleave', this.removeMouseTimeout)

View File

@ -15,7 +15,7 @@
:type="source.type || source.mimeType" :type="source.type || source.mimeType"
:label="source.qualityLabel" :label="source.qualityLabel"
:selected="source.qualityLabel === selectedDefaultQuality" :selected="source.qualityLabel === selectedDefaultQuality"
/> >
<track <track
v-for="(caption, index) in captionList" v-for="(caption, index) in captionList"
:key="index + '_caption'" :key="index + '_caption'"

View File

@ -552,6 +552,9 @@ export default Vue.extend({
invidiousInstance: function () { invidiousInstance: function () {
return this.$store.getters.getInvidiousInstance return this.$store.getters.getInvidiousInstance
}, },
enableSearchSuggestions: function () {
return this.$store.getters.getEnableSearchSuggestions
},
backendFallback: function () { backendFallback: function () {
return this.$store.getters.getBackendFallback return this.$store.getters.getBackendFallback
}, },
@ -616,6 +619,7 @@ export default Vue.extend({
}, },
...mapActions([ ...mapActions([
'updateEnableSearchSuggestions',
'updateBackendFallback', 'updateBackendFallback',
'updateCheckForUpdates', 'updateCheckForUpdates',
'updateBarColor', 'updateBarColor',

View File

@ -1,6 +1,7 @@
<template> <template>
<ft-card <ft-card
class="card"> class="card"
>
<h3 <h3
class="videoTitle" class="videoTitle"
> >
@ -12,6 +13,11 @@
:default-value="backendFallback" :default-value="backendFallback"
@change="updateBackendFallback" @change="updateBackendFallback"
/> />
<ft-toggle-switch
label="Enable Search Suggestions"
:default-value="enableSearchSuggestions"
@change="updateEnableSearchSuggestions"
/>
<ft-toggle-switch <ft-toggle-switch
v-if="false" v-if="false"
label="Check for Updates" label="Check for Updates"

View File

@ -1,6 +1,7 @@
<template> <template>
<ft-card <ft-card
class="relative card"> class="relative card"
>
<h3 <h3
class="videoTitle" class="videoTitle"
> >

View File

@ -2,19 +2,30 @@
display: block; display: block;
height: calc(100vh - 60px); height: calc(100vh - 60px);
width: 200px; width: 200px;
overflow-y: auto; overflow-x: hidden;
position: fixed; position: fixed;
left: 0px; left: 0px;
top: 0px; top: 0px;
z-index: 1; z-index: 1;
margin-top: 60px; margin-top: 60px;
-webkit-box-shadow: 1px -1px 1px -1px var(--primary-shadow-color); box-shadow: 1px -1px 1px -1px var(--primary-shadow-color);
background-color: var(--side-nav-color); background-color: var(--side-nav-color);
transition-property: width; transition-property: width;
transition-duration: 150ms; transition-duration: 150ms;
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
} }
.inner {
height: 100%;
width: 200px;
overflow-y: auto;
overflow-x: hidden;
}
.closed .inner {
width: 80px;
}
.topNavOption { .topNavOption {
margin-top: 10px; margin-top: 10px;
} }
@ -97,6 +108,10 @@
} }
@media only screen and (max-width: 680px) { @media only screen and (max-width: 680px) {
.inner {
display: contents; /* sunglasses emoji */
}
hr, .mobileHidden, .refreshIcon { hr, .mobileHidden, .refreshIcon {
display: none; display: none;
} }
@ -117,7 +132,7 @@
width: 100%; width: 100%;
bottom: 0px; bottom: 0px;
top: auto; top: auto;
overflow-y: inherit; overflow-y: hidden;
} }
.navOption, .closed .navOption { .navOption, .closed .navOption {

View File

@ -4,6 +4,7 @@
class="sideNav" class="sideNav"
:class="{closed: !isOpen}" :class="{closed: !isOpen}"
> >
<div class="inner">
<div <div
class="navOption topNavOption mobileShow" class="navOption topNavOption mobileShow"
@click="navigate('subscriptions')" @click="navigate('subscriptions')"
@ -97,6 +98,7 @@
</p> </p>
</div> </div>
<hr> <hr>
</div>
</ft-flex-box> </ft-flex-box>
</template> </template>

View File

@ -1,6 +1,7 @@
<template> <template>
<ft-card <ft-card
class="relative card"> class="relative card"
>
<h3 <h3
class="videoTitle" class="videoTitle"
> >

View File

@ -21,11 +21,13 @@ export default Vue.extend({
currentSecColor: '', currentSecColor: '',
baseThemeNames: [ baseThemeNames: [
'Light', 'Light',
'Dark' 'Dark',
'Black'
], ],
baseThemeValues: [ baseThemeValues: [
'light', 'light',
'dark' 'dark',
'black'
], ],
colorNames: [ colorNames: [
'Red', 'Red',

View File

@ -1,6 +1,7 @@
<template> <template>
<ft-card <ft-card
class="relative card"> class="relative card"
>
<h3> <h3>
{{ title }} {{ title }}
</h3> </h3>

View File

@ -17,11 +17,14 @@ export default Vue.extend({
component: this, component: this,
windowWidth: 0, windowWidth: 0,
showFilters: false, showFilters: false,
searchValue: '',
searchSuggestionsDataList: [] searchSuggestionsDataList: []
} }
}, },
computed: { computed: {
enableSearchSuggestions: function () {
return this.$store.getters.getEnableSearchSuggestions
},
searchSettings: function () { searchSettings: function () {
return this.$store.getters.getSearchSettings return this.$store.getters.getSearchSettings
}, },
@ -103,7 +106,9 @@ export default Vue.extend({
}, },
getSearchSuggestionsDebounce: function (query) { getSearchSuggestionsDebounce: function (query) {
if (this.enableSearchSuggestions) {
this.debounceSearchResults(query) this.debounceSearchResults(query)
}
}, },
getSearchSuggestions: function (query) { getSearchSuggestions: function (query) {
@ -120,20 +125,17 @@ export default Vue.extend({
getSearchSuggestionsLocal: function (query) { getSearchSuggestionsLocal: function (query) {
if (query === '') { if (query === '') {
this.searchSuggestionsDataList = [] this.searchSuggestionsDataList = []
this.searchValue = ''
return return
} }
ytSuggest(query).then((results) => { ytSuggest(query).then((results) => {
this.searchSuggestionsDataList = results this.searchSuggestionsDataList = results
this.searchValue = query
}) })
}, },
getSearchSuggestionsInvidious: function (query) { getSearchSuggestionsInvidious: function (query) {
if (query === '') { if (query === '') {
this.searchSuggestionsDataList = [] this.searchSuggestionsDataList = []
this.searchValue = ''
return return
} }
@ -147,7 +149,6 @@ export default Vue.extend({
this.$store.dispatch('invidiousAPICall', searchPayload).then((results) => { this.$store.dispatch('invidiousAPICall', searchPayload).then((results) => {
this.searchSuggestionsDataList = results.suggestions this.searchSuggestionsDataList = results.suggestions
this.searchValue = query
}).error((err) => { }).error((err) => {
console.log(err) console.log(err)
if (this.backendFallback) { if (this.backendFallback) {

View File

@ -40,7 +40,6 @@
class="searchInput" class="searchInput"
:is-search="true" :is-search="true"
:data-list="searchSuggestionsDataList" :data-list="searchSuggestionsDataList"
:value="searchValue"
@input="getSearchSuggestionsDebounce" @input="getSearchSuggestionsDebounce"
@click="goToSearch" @click="goToSearch"
/> />

View File

@ -60,7 +60,9 @@
class="commentMoreReplies" class="commentMoreReplies"
@click="getCommentReplies(index)" @click="getCommentReplies(index)"
> >
View {{ comment.numReplies }} replies <span v-if="!comment.showReplies">View</span>
<span v-else>Hide</span>
{{ comment.numReplies }} replies
</p> </p>
<div <div
v-if="comment.showReplies" v-if="comment.showReplies"

View File

@ -1,7 +1,10 @@
.videoDescription {
overflow-y: auto;
max-height: 300px;
}
.description { .description {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
font-size: 17px; font-size: 17px;
white-space: pre-wrap; white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
} }

View File

@ -23,15 +23,10 @@ export default Vue.extend({
}, },
data: function () { data: function () {
return { return {
dateString: '',
shownDescription: '' shownDescription: ''
} }
}, },
mounted: function () { mounted: function () {
const date = new Date(this.published)
const dateSplit = date.toDateString().split(' ')
this.dateString = `${dateSplit[0]} ${dateSplit[1]} ${dateSplit[2]}, ${dateSplit[3]}`
if (this.descriptionHtml !== '') { if (this.descriptionHtml !== '') {
this.shownDescription = this.parseDescriptionHtml(this.descriptionHtml) this.shownDescription = this.parseDescriptionHtml(this.descriptionHtml)
} else { } else {

View File

@ -1,6 +1,5 @@
<template> <template>
<ft-card class="videoDescription"> <ft-card class="videoDescription">
<h4>Published on {{ dateString }}</h4>
<p <p
class="description" class="description"
v-html="shownDescription" v-html="shownDescription"

View File

@ -1,117 +0,0 @@
.relative {
position: relative;
}
.watchVideoInfo {
min-height: 130px;
}
.videoTitle {
font-size: 22px;
max-width: 45%;
}
.channelInformation {
position: absolute;
bottom: 10px;
width: 350px;
}
.channelThumbnail {
cursor: pointer;
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
}
.channelName {
position: absolute;
top: 0px;
left: 55px;
font-weight: bold;
font-size: 15px;
cursor: pointer;
}
.subscribeButton {
height: 20px;
position: absolute;
top: 20px;
left: 50px;
line-height: 1px;
font-size: 0.8rem;
}
.viewCount {
position: absolute;
right: 15px;
bottom: 30px;
}
.likeBarContainer {
position: absolute;
right: 15px;
bottom: 35px;
width: 300px;
height: 5px;
}
.likeBar {
background-color: var(--accent-color);
height: 100%;
position: absolute;
top: 0px;
left: 0px;
z-index: 1;
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
}
.dislikeBar {
background-color: #9E9E9E;
height: 100%;
width: 100%;
position: absolute;
top: 0px;
left: 0px;
border-radius: 200px 200px 200px 200px;
-webkit-border-radius: 200px 200px 200px 200px;
}
.likeCountContainer {
position: absolute;
right: 15px;
bottom: 0px;
font-size: 12px;
color: var(--tertiary-text-color);
}
.videoOptions {
position: absolute;
right: 15px;
top: 20px;
width: 175px;
}
@media only screen and (max-width: 1500px) {
.videoOptions {
width: 175px;
}
.watchVideoInfo {
min-height: 150px;
}
}
@media only screen and (max-width: 1350px) {
.theatreModeButton {
display: none;
}
.watchVideoInfo {
min-height: 130px;
}
.videoOptions {
width: 120px;
}
}

View File

@ -4,7 +4,7 @@ import FtButton from '../ft-button/ft-button.vue'
import FtListDropdown from '../ft-list-dropdown/ft-list-dropdown.vue' import FtListDropdown from '../ft-list-dropdown/ft-list-dropdown.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue' import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue' import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
import FtToastEvents from '../ft-toast/ft-toast-events' import FtShareButton from '../ft-share-button/ft-share-button.vue'
// import { shell } from 'electron' // import { shell } from 'electron'
export default Vue.extend({ export default Vue.extend({
@ -14,7 +14,8 @@ export default Vue.extend({
'ft-button': FtButton, 'ft-button': FtButton,
'ft-list-dropdown': FtListDropdown, 'ft-list-dropdown': FtListDropdown,
'ft-flex-box': FtFlexBox, 'ft-flex-box': FtFlexBox,
'ft-icon-button': FtIconButton 'ft-icon-button': FtIconButton,
'ft-share-button': FtShareButton
}, },
props: { props: {
id: { id: {
@ -37,6 +38,10 @@ export default Vue.extend({
type: String, type: String,
required: true required: true
}, },
published: {
type: Number,
required: true
},
viewCount: { viewCount: {
type: Number, type: Number,
required: true required: true
@ -66,19 +71,6 @@ export default Vue.extend({
'dash', 'dash',
'legacy', 'legacy',
'audio' 'audio'
],
shareLabel: 'SHARE VIDEO',
shareNames: [
'COPY INVIDIOUS LINK',
'OPEN INVIDIOUS LINK',
'COPY YOUTUBE LINK',
'OPEN YOUTUBE LINK'
],
shareValues: [
'copyInvidious',
'openInvidious',
'copyYoutube',
'openYoutube'
] ]
} }
}, },
@ -91,18 +83,6 @@ export default Vue.extend({
return this.$store.getters.getUsingElectron return this.$store.getters.getUsingElectron
}, },
invidiousUrl: function () {
return `${this.invidiousInstance}/watch?v=${this.id}`
},
youtubeUrl: function () {
return `https://www.youtube.com/watch?v=${this.id}`
},
youtubeEmbedUrl: function () {
return `https://www.youtube-nocookie.com/embed/${this.id}`
},
totalLikeCount: function () { totalLikeCount: function () {
return this.likeCount + this.dislikeCount return this.likeCount + this.dislikeCount
}, },
@ -117,6 +97,12 @@ export default Vue.extend({
subscribedText: function () { subscribedText: function () {
return `SUBSCRIBE ${this.subscriptionCountText}` return `SUBSCRIBE ${this.subscriptionCountText}`
},
dateString() {
const date = new Date(this.published)
const dateSplit = date.toDateString().split(' ')
return `${dateSplit[1]} ${dateSplit[2]}, ${dateSplit[3]}`
} }
}, },
methods: { methods: {
@ -140,43 +126,6 @@ export default Vue.extend({
this.$parent.enableAudioFormat() this.$parent.enableAudioFormat()
break break
} }
},
handleShare: function (method) {
console.log('Handling share')
switch (method) {
case 'copyYoutube':
FtToastEvents.$emit('toast.open', "YouTube URL copied to clipboard")
navigator.clipboard.writeText(this.youtubeUrl)
break
case 'openYoutube':
if (this.usingElectron) {
const shell = require('electron').shell
shell.openExternal(this.youtubeUrl)
}
break
case 'copyYoutubeEmbed':
FtToastEvents.$emit('toast.open', "YouTube Embed URL copied to clipboard")
navigator.clipboard.writeText(this.youtubeEmbedUrl)
break
case 'openYoutubeEmbed':
if (this.usingElectron) {
const shell = require('electron').shell
shell.openExternal(this.youtubeEmbedUrl)
}
break
case 'copyInvidious':
FtToastEvents.$emit('toast.open', "Invidious URL copied to clipboard")
navigator.clipboard.writeText(this.invidiousUrl)
break
case 'openInvidious':
if (this.usingElectron) {
const shell = require('electron').shell
shell.openExternal(this.invidiousUrl)
}
break
}
} }
} }
}) })

View File

@ -0,0 +1,91 @@
.watchVideoInfo
display: grid
grid-template-columns: 2fr 1fr
padding: 16px
@media screen and (max-width: 680px)
grid-template-columns: auto
.videoTitle
font-size: 22px
margin: 0 0 24px
.channelInformation
.profileRow
display: flex
.channelThumbnail
border-radius: 50%
margin-right: 10px
cursor: pointer
width: 56px
.channelName
margin-left: 6px
cursor: pointer
position: relative
top: -2px
.subscribeButton
margin-top: 6px
margin-left: 6px
padding: 6px
font-size: 14px
.viewCount, .datePublished
color: var(--secondary-text-color)
text-align: right
font-size: 15px
@media screen and (max-width: 680px)
text-align: left
.viewCount
margin: 18px 0px 0px
.datePublished
margin: 4px 0px 0px
@media screen and (max-width: 680px)
margin-top: 16px
.likeSection
margin-top: 4px
font-size: 12px
color: var(--tertiary-text-color)
display: flex
flex-direction: column
margin-left: auto
text-align: right
max-width: 210px
@media screen and (max-width: 680px)
margin-left: 0
text-align: left
.likeBar
height: 8px
border-radius: 4px
margin-bottom: 4px
.likeCount
margin-right: 6px
.videoOptions
margin-top: 16px
display: flex
justify-content: flex-end
.option:not(:first-child)
margin-left: 4px
@media screen and (max-width: 680px)
justify-content: flex-start
::v-deep .iconDropdown
left: calc(50% - 20px)
right: auto
@media only screen and (max-width: 1350px)
.theatreModeButton
display: none

View File

@ -1,5 +1,6 @@
<template> <template>
<ft-card class="relative watchVideoInfo"> <ft-card class="watchVideoInfo">
<div>
<p <p
class="videoTitle" class="videoTitle"
> >
@ -8,17 +9,23 @@
<div <div
class="channelInformation" class="channelInformation"
> >
<div
class="profileRow"
>
<div>
<img <img
:src="channelThumbnail" :src="channelThumbnail"
class="channelThumbnail" class="channelThumbnail"
@click="goToChannel" @click="goToChannel"
> >
<span </div>
<div>
<div
class="channelName" class="channelName"
@click="goToChannel" @click="goToChannel"
> >
{{ channelName }} {{ channelName }}
</span> </div>
<ft-button <ft-button
:label="subscribedText" :label="subscribedText"
class="subscribeButton" class="subscribeButton"
@ -26,54 +33,55 @@
@click="handleSubscription" @click="handleSubscription"
/> />
</div> </div>
<ft-flex-box class="videoOptions"> </div>
</div>
</div>
<div>
<div class="datePublished">
Published {{ dateString }}
</div>
<div class="viewCount">
{{ parsedViewCount }}
</div>
<div class="likeBarContainer">
<div
class="likeSection"
>
<div
class="likeBar"
:style="{ background: `linear-gradient(to right, var(--accent-color) ${likePercentageRatio}%, #9E9E9E ${likePercentageRatio}%` }"
/>
<div>
<span class="likeCount"><font-awesome-icon icon="thumbs-up" /> {{ likeCount }}</span>
<span class="dislikeCount"><font-awesome-icon icon="thumbs-down" /> {{ dislikeCount }}</span>
</div>
</div>
</div>
<div class="videoOptions">
<ft-icon-button <ft-icon-button
title="Toggle Theatre Mode" title="Toggle Theatre Mode"
class="theatreModeButton" class="theatreModeButton option"
icon="expand-alt" icon="expand-alt"
theme="secondary" theme="secondary"
@click="$emit('theatreMode')" @click="$emit('theatreMode')"
/> />
<ft-icon-button <ft-icon-button
title="Change Video Formats" title="Change Video Formats"
class="option"
theme="secondary" theme="secondary"
icon="file-video" icon="file-video"
:dropdown-names="formatTypeNames" :dropdown-names="formatTypeNames"
:dropdown-values="formatTypeValues" :dropdown-values="formatTypeValues"
@click="handleFormatChange" @click="handleFormatChange"
/> />
<ft-icon-button <ft-share-button
title="Share Video" :id="id"
theme="secondary" class="option"
icon="share-alt"
:dropdown-names="shareNames"
:dropdown-values="shareValues"
@click="handleShare"
/> />
</ft-flex-box>
<p class="viewCount">
{{ parsedViewCount }}
</p>
<div class="likeBarContainer">
<div
class="likeBar"
:style="{ width: likePercentageRatio + '%' }"
/>
<div class="dislikeBar" />
</div> </div>
<p class="likeCountContainer"> </div>
<font-awesome-icon
icon="thumbs-up"
/>
{{ likeCount }}
&nbsp;
<font-awesome-icon
icon="thumbs-down"
/>
{{ dislikeCount }}
</p>
</ft-card> </ft-card>
</template> </template>
<script src="./watch-video-info.js" /> <script src="./watch-video-info.js" />
<style scoped src="./watch-video-info.css" /> <style scoped src="./watch-video-info.sass" lang="sass" />

View File

@ -53,7 +53,7 @@
<img <img
:src="comment.author.thumbnail.url" :src="comment.author.thumbnail.url"
class="channelThumbnail" class="channelThumbnail"
/> >
<p <p
class="superChatContent" class="superChatContent"
:style="{ color: 'var(--text-with-main-color)' }" :style="{ color: 'var(--text-with-main-color)' }"
@ -67,8 +67,8 @@
</div> </div>
</div> </div>
<div <div
class="openedSuperChat"
v-if="showSuperChat" v-if="showSuperChat"
class="openedSuperChat"
:class="superChat.superchat.colorClass" :class="superChat.superchat.colorClass"
@click="showSuperChat = false" @click="showSuperChat = false"
> >
@ -82,7 +82,7 @@
<img <img
:src="superChat.author.thumbnail.url" :src="superChat.author.thumbnail.url"
class="channelThumbnail" class="channelThumbnail"
/> >
<p <p
class="channelName" class="channelName"
> >
@ -95,11 +95,10 @@
</p> </p>
</div> </div>
<p <p
class="chatMessage"
v-if="superChat.message.length > 0" v-if="superChat.message.length > 0"
class="chatMessage"
v-html="superChat.messageHtml" v-html="superChat.messageHtml"
> />
</p>
</div> </div>
</div> </div>
<div <div
@ -107,9 +106,11 @@
:style="{ height: chatHeight }" :style="{ height: chatHeight }"
@mousewheel="e => onScroll(e)" @mousewheel="e => onScroll(e)"
> >
<div v-for="(comment, index) in comments" <div
v-for="(comment, index) in comments"
:key="index" :key="index"
class="comment"> class="comment"
>
<div <div
v-if="typeof (comment.superchat) !== 'undefined'" v-if="typeof (comment.superchat) !== 'undefined'"
class="superChatMessage" class="superChatMessage"
@ -121,7 +122,7 @@
<img <img
:src="comment.author.thumbnail.url" :src="comment.author.thumbnail.url"
class="channelThumbnail" class="channelThumbnail"
/> >
<p <p
class="channelName" class="channelName"
> >
@ -134,11 +135,10 @@
</p> </p>
</div> </div>
<p <p
class="chatMessage"
v-if="comment.message.length > 0" v-if="comment.message.length > 0"
class="chatMessage"
v-html="comment.messageHtml" v-html="comment.messageHtml"
> />
</p>
</div> </div>
<div <div
v-else v-else
@ -146,7 +146,7 @@
<img <img
:src="comment.author.thumbnail.url" :src="comment.author.thumbnail.url"
class="channelThumbnail" class="channelThumbnail"
/> >
<p <p
class="chatContent" class="chatContent"
> >
@ -169,14 +169,13 @@
:alt="comment.author.badge.thumbnail.alt" :alt="comment.author.badge.thumbnail.alt"
:title="comment.author.badge.thumbnail.alt" :title="comment.author.badge.thumbnail.alt"
class="badgeImage" class="badgeImage"
/>
</span>
<span
class="chatMessage"
v-if="comment.message.length > 0"
v-html="comment.messageHtml"
> >
</span> </span>
<span
v-if="comment.message.length > 0"
class="chatMessage"
v-html="comment.messageHtml"
/>
</p> </p>
</div> </div>
</div> </div>

View File

@ -75,32 +75,3 @@
position: relative; position: relative;
bottom: 7px; bottom: 7px;
} }
/deep/ .list {
height: 60px;
width: calc(100% - 30px);
}
/deep/ .list .videoThumbnail {
width: 100px;
height: 60px;
}
/deep/ .list .videoThumbnail img {
height: 60px;
}
/deep/ .list .videoTitle {
font-size: 12px;
margin-left: 105px;
margin-right: 30px;
}
/deep/ .list .channelName {
margin-left: 105px;
margin-right: 30px;
}
/deep/ .list .viewCount {
margin-left: 5px;
}

View File

@ -5,29 +5,3 @@
.videoRecommendation { .videoRecommendation {
margin-bottom: -15px; margin-bottom: -15px;
} }
/deep/ .list {
height: 110px;
}
/deep/ .list .videoThumbnail {
width: 180px;
height: 100px;
}
/deep/ .list .videoThumbnail img {
height: 100px;
}
/deep/ .list .videoTitle {
font-size: 12px;
margin-left: 185px;
}
/deep/ .list .channelName {
margin-left: 185px;
}
/deep/ .list .viewCount {
margin-left: 5px;
}

View File

@ -37,6 +37,7 @@ const state = {
thumbnailPreference: '', thumbnailPreference: '',
invidiousInstance: 'https://invidio.us', invidiousInstance: 'https://invidio.us',
barColor: false, barColor: false,
enableSearchSuggestions: true,
rememberHistory: true, rememberHistory: true,
autoplayVideos: true, autoplayVideos: true,
autoplayPlaylists: true, autoplayPlaylists: true,
@ -72,6 +73,10 @@ const getters = {
return state.barColor return state.barColor
}, },
getEnableSearchSuggestions: () => {
return state.enableSearchSuggestions
},
getBackendPreference: () => { getBackendPreference: () => {
return state.backendPreference return state.backendPreference
}, },
@ -169,6 +174,9 @@ const actions = {
case 'checkForUpdates': case 'checkForUpdates':
commit('setCheckForUpdates', result.value) commit('setCheckForUpdates', result.value)
break break
case 'enableSearchSuggestions':
commit('setEnableSearchSuggestions', result.value)
break
case 'backendPreference': case 'backendPreference':
commit('setBackendPreference', result.value) commit('setBackendPreference', result.value)
break break
@ -255,6 +263,14 @@ const actions = {
}) })
}, },
updateEnableSearchSuggestions ({ commit }, enableSearchSuggestions) {
settingsDb.update({ _id: 'enableSearchSuggestions' }, { _id: 'enableSearchSuggestions', value: enableSearchSuggestions }, { upsert: true }, (err, numReplaced) => {
if (!err) {
commit('setEnableSearchSuggestions', enableSearchSuggestions)
}
})
},
updateBackendPreference ({ commit }, backendPreference) { updateBackendPreference ({ commit }, backendPreference) {
settingsDb.update({ _id: 'backendPreference' }, { _id: 'backendPreference', value: backendPreference }, { upsert: true }, (err, numReplaced) => { settingsDb.update({ _id: 'backendPreference' }, { _id: 'backendPreference', value: backendPreference }, { upsert: true }, (err, numReplaced) => {
if (!err) { if (!err) {
@ -440,6 +456,9 @@ const mutations = {
setBarColor (state, barColor) { setBarColor (state, barColor) {
state.barColor = barColor state.barColor = barColor
}, },
setEnableSearchSuggestions (state, enableSearchSuggestions) {
state.enableSearchSuggestions = enableSearchSuggestions
},
setRememberHistory (state, rememberHistory) { setRememberHistory (state, rememberHistory) {
state.rememberHistory = rememberHistory state.rememberHistory = rememberHistory
}, },

View File

@ -53,18 +53,37 @@ const actions = {
return state.colorClasses[randomInt] return state.colorClasses[randomInt]
}, },
getVideoIdFromUrl ({ state }, url) { getVideoIdFromUrl (_, url) {
console.log('checking for id') /** @type {URL} */
console.log(url) let urlObject
const rx = /^.*(?:(?:(you|hook)tu\.?be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/ try {
urlObject = new URL(url)
const match = url.match(rx) } catch (e) {
if (match) {
return match[2]
} else {
return false return false
} }
const extractors = [
// anything with /watch?v=
function() {
if (urlObject.pathname === '/watch' && urlObject.searchParams.has('v')) {
return urlObject.searchParams.get('v')
}
},
// youtu.be
function() {
if (urlObject.host === 'youtu.be' && urlObject.pathname.match(/^\/[A-Za-z0-9_-]+$/)) {
return urlObject.pathname.slice(1)
}
},
// cloudtube
function() {
if (urlObject.host.match(/^cadence\.(gq|moe)$/) && urlObject.pathname.match(/^\/cloudtube\/video\/[A-Za-z0-9_-]+$/)) {
return urlObject.pathname.slice('/cloudtube/video/'.length)
}
}
]
return extractors.reduce((a, c) => a || c(), null) || false
} }
} }

View File

@ -6,12 +6,15 @@
--primary-shadow-color: rgba(232, 232, 232, 1); --primary-shadow-color: rgba(232, 232, 232, 1);
--title-color: #3f7ac6; --title-color: #3f7ac6;
--bg-color: #f1f1f1; --bg-color: #f1f1f1;
--link-color: var(--accent-color);
--link-visited-color: var(--accent-color-visited);
--card-bg-color: #FFFFFF; --card-bg-color: #FFFFFF;
--secondary-card-bg-color: #eeeeee; --secondary-card-bg-color: #eeeeee;
--side-nav-color: #FFFFFF; --side-nav-color: #FFFFFF;
--side-nav-hover-color: #e0e0e0; --side-nav-hover-color: #e0e0e0;
--side-nav-active-color: #757575; --side-nav-active-color: #757575;
--search-bar-color: #f5f5f5; --search-bar-color: #f5f5f5;
--instance-menu-color: var(--search-bar-color);
--logo-icon: url("~../../_icons/iconColorSmall.png"); --logo-icon: url("~../../_icons/iconColorSmall.png");
--logo-text: url("~../../_icons/textColorSmall.png"); --logo-text: url("~../../_icons/textColorSmall.png");
} }
@ -20,21 +23,44 @@
.dark { .dark {
--primary-text-color: #EEEEEE; --primary-text-color: #EEEEEE;
--secondary-text-color: #ddd; --secondary-text-color: #ddd;
--tertiary-text-color: #888; --tertiary-text-color: #999;
--primary-input-color: rgba(0, 0, 0, 0.50); --primary-input-color: rgba(0, 0, 0, 0.50);
--primary-shadow-color: rgba(0, 0, 0, 0.75); --primary-shadow-color: rgba(0, 0, 0, 0.75);
--title-color: #EEEEEE; --title-color: #EEEEEE;
--bg-color: #212121; --bg-color: #212121;
--link-color: var(--accent-color);
--link-visited-color: var(--accent-color-visited);
--card-bg-color: #303030; --card-bg-color: #303030;
--secondary-card-bg-color: rgba(0, 0, 0, 0.75); --secondary-card-bg-color: rgba(0, 0, 0, 0.75);
--side-nav-color: #262626; --side-nav-color: #262626;
--side-nav-hover-color: #212121; --side-nav-hover-color: #212121;
--side-nav-active-color: #303030; --side-nav-active-color: #303030;
--search-bar-color: #262626; --search-bar-color: #262626;
--instance-menu-color: var(--search-bar-color);
--logo-icon: url("~../../_icons/iconColorSmall.png"); --logo-icon: url("~../../_icons/iconColorSmall.png");
--logo-text: url("~../../_icons/textColorSmall.png"); --logo-text: url("~../../_icons/textColorSmall.png");
} }
.black {
--primary-text-color: #EEEEEE;
--secondary-text-color: #ddd;
--tertiary-text-color: #EEEEEE;
--primary-input-color: rgba(0, 0, 0, 0.50);
--primary-shadow-color: rgba(0, 0, 0, 0.75);
--title-color: #EEEEEEE;
--bg-color: #000000;
--link-color: var(--accent-color);
--link-visited-color: var(--accent-color-visited);
--card-bg-color: #000000;
--secondary-card-bg-color: rgba(0, 0, 0, 0.75);
--side-nav-color: #000000;
--side-nav-hover-color: #212121;
--side-nav-active-color: #303030;
--search-bar-color: #262626;
--instance-menu-color: var(--search-bar-color);
--logo-icon: url("~../../_icons/iconColorSmall.png");
--logo-text: url("~../../_icons/textColorSmall.png");
}
.gray { .gray {
--primary-text-color: #EEEEEE; --primary-text-color: #EEEEEE;
@ -203,6 +229,7 @@
--accent-color-hover: #e53935; --accent-color-hover: #e53935;
--accent-color-active: #c62828; --accent-color-active: #c62828;
--accent-color-light: #ef9a9a; --accent-color-light: #ef9a9a;
--accent-color-visited: #b71c1c;
--text-with-accent-color: #FFFFFF; --text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(244,67,54,0.04); --accent-color-opacity1: rgba(244,67,54,0.04);
--accent-color-opacity2: rgba(244,67,54,0.12); --accent-color-opacity2: rgba(244,67,54,0.12);
@ -215,6 +242,7 @@
--accent-color-hover: #D81B60; --accent-color-hover: #D81B60;
--accent-color-active: #AD1457; --accent-color-active: #AD1457;
--accent-color-light: #F48FB1; --accent-color-light: #F48FB1;
--accent-color-visited: #880E4F;
--text-with-accent-color: #FFFFFF; --text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(233,30,99,0.04); --accent-color-opacity1: rgba(233,30,99,0.04);
--accent-color-opacity2: rgba(233,30,99,0.12); --accent-color-opacity2: rgba(233,30,99,0.12);
@ -227,6 +255,7 @@
--accent-color-hover: #8E24AA; --accent-color-hover: #8E24AA;
--accent-color-active: #6A1B9A; --accent-color-active: #6A1B9A;
--accent-color-light: #CE93D8; --accent-color-light: #CE93D8;
--accent-color-visited: #4A148C;
--text-with-accent-color: #FFFFFF; --text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(156,39,176,0.04); --accent-color-opacity1: rgba(156,39,176,0.04);
--accent-color-opacity2: rgba(156,39,176,0.12); --accent-color-opacity2: rgba(156,39,176,0.12);
@ -239,6 +268,7 @@
--accent-color-hover: #5E35B1; --accent-color-hover: #5E35B1;
--accent-color-active: #4527A0; --accent-color-active: #4527A0;
--accent-color-light: #B39DDB; --accent-color-light: #B39DDB;
--accent-color-visited: #311B92;
--text-with-accent-color: #FFFFFF; --text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(103,58,183,0.04); --accent-color-opacity1: rgba(103,58,183,0.04);
--accent-color-opacity2: rgba(103,58,183,0.12); --accent-color-opacity2: rgba(103,58,183,0.12);
@ -251,6 +281,7 @@
--accent-color-hover: #3949AB; --accent-color-hover: #3949AB;
--accent-color-active: #283593; --accent-color-active: #283593;
--accent-color-light: #9FA8DA; --accent-color-light: #9FA8DA;
--accent-color-visited: #1A237E;
--text-with-accent-color: #FFFFFF; --text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(63,81,181,0.04); --accent-color-opacity1: rgba(63,81,181,0.04);
--accent-color-opacity2: rgba(63,81,181,0.12); --accent-color-opacity2: rgba(63,81,181,0.12);
@ -263,6 +294,7 @@
--accent-color-hover: #1E88E5; --accent-color-hover: #1E88E5;
--accent-color-active: #1565C0; --accent-color-active: #1565C0;
--accent-color-light: #90CAF9; --accent-color-light: #90CAF9;
--accent-color-visited: #0D47A1;
--text-with-accent-color: #FFFFFF; --text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(33,150,243,0.04); --accent-color-opacity1: rgba(33,150,243,0.04);
--accent-color-opacity2: rgba(33,150,243,0.12); --accent-color-opacity2: rgba(33,150,243,0.12);
@ -275,6 +307,7 @@
--accent-color-hover: #039BE5; --accent-color-hover: #039BE5;
--accent-color-active: #0277BD; --accent-color-active: #0277BD;
--accent-color-light: #81D4FA; --accent-color-light: #81D4FA;
--accent-color-visited: #01579B;
--text-with-accent-color: #FFFFFF; --text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(3,169,244,0.04); --accent-color-opacity1: rgba(3,169,244,0.04);
--accent-color-opacity2: rgba(3,169,244,0.12); --accent-color-opacity2: rgba(3,169,244,0.12);
@ -287,6 +320,7 @@
--accent-color-hover: #00ACC1; --accent-color-hover: #00ACC1;
--accent-color-active: #00838F; --accent-color-active: #00838F;
--accent-color-light: #80DEEA; --accent-color-light: #80DEEA;
--accent-color-visited: #006064;
--text-with-accent-color: #FFFFFF; --text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(0,188,212,0.04); --accent-color-opacity1: rgba(0,188,212,0.04);
--accent-color-opacity2: rgba(0,188,212,0.12); --accent-color-opacity2: rgba(0,188,212,0.12);
@ -299,6 +333,7 @@
--accent-color-hover: #00897B; --accent-color-hover: #00897B;
--accent-color-active: #00695C; --accent-color-active: #00695C;
--accent-color-light: #80CBC4; --accent-color-light: #80CBC4;
--accent-color-visited: #004D40;
--text-with-accent-color: #FFFFFF; --text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(0,150,136,0.04); --accent-color-opacity1: rgba(0,150,136,0.04);
--accent-color-opacity2: rgba(0,150,136,0.12); --accent-color-opacity2: rgba(0,150,136,0.12);
@ -311,6 +346,7 @@
--accent-color-hover: #43A047; --accent-color-hover: #43A047;
--accent-color-active: #2E7D32; --accent-color-active: #2E7D32;
--accent-color-light: #A5D6A7; --accent-color-light: #A5D6A7;
--accent-color-visited: #1B5E20;
--text-with-accent-color: #FFFFFF; --text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(76,175,80,0.04); --accent-color-opacity1: rgba(76,175,80,0.04);
--accent-color-opacity2: rgba(76,175,80,0.12); --accent-color-opacity2: rgba(76,175,80,0.12);
@ -323,6 +359,7 @@
--accent-color-hover: #7CB342; --accent-color-hover: #7CB342;
--accent-color-active: #558B2F; --accent-color-active: #558B2F;
--accent-color-light: #C5E1A5; --accent-color-light: #C5E1A5;
--accent-color-visited: #33691E;
--text-with-accent-color: #000000; --text-with-accent-color: #000000;
--accent-color-opacity1: rgba(139,195,74,0.04); --accent-color-opacity1: rgba(139,195,74,0.04);
--accent-color-opacity2: rgba(139,195,74,0.12); --accent-color-opacity2: rgba(139,195,74,0.12);
@ -335,6 +372,7 @@
--accent-color-hover: #C0CA33; --accent-color-hover: #C0CA33;
--accent-color-active: #9E9D24; --accent-color-active: #9E9D24;
--accent-color-light: #E6EE9C; --accent-color-light: #E6EE9C;
--accent-color-visited: #827717;
--text-with-accent-color: #000000; --text-with-accent-color: #000000;
--accent-color-opacity1: rgba(205,220,57,0.04); --accent-color-opacity1: rgba(205,220,57,0.04);
--accent-color-opacity2: rgba(205,220,57,0.12); --accent-color-opacity2: rgba(205,220,57,0.12);
@ -347,6 +385,7 @@
--accent-color-hover: #FDD835; --accent-color-hover: #FDD835;
--accent-color-active: #F9A825; --accent-color-active: #F9A825;
--accent-color-light: #FFF59D; --accent-color-light: #FFF59D;
--accent-color-visited: #F57F17;
--text-with-accent-color: #000000; --text-with-accent-color: #000000;
--accent-color-opacity1: rgba(255,235,59,0.04); --accent-color-opacity1: rgba(255,235,59,0.04);
--accent-color-opacity2: rgba(255,235,59,0.12); --accent-color-opacity2: rgba(255,235,59,0.12);
@ -359,6 +398,7 @@
--accent-color-hover: #FFB300; --accent-color-hover: #FFB300;
--accent-color-active: #FF8F00; --accent-color-active: #FF8F00;
--accent-color-light: #FFE082; --accent-color-light: #FFE082;
--accent-color-visited: #FF6F00;
--text-with-accent-color: #000000; --text-with-accent-color: #000000;
--accent-color-opacity1: rgba(255,193,7,0.04); --accent-color-opacity1: rgba(255,193,7,0.04);
--accent-color-opacity2: rgba(255,193,7,0.12); --accent-color-opacity2: rgba(255,193,7,0.12);
@ -371,6 +411,7 @@
--accent-color-hover: #FB8C00; --accent-color-hover: #FB8C00;
--accent-color-active: #EF6C00; --accent-color-active: #EF6C00;
--accent-color-light: #FFCC80; --accent-color-light: #FFCC80;
--accent-color-visited: #E65100;
--text-with-accent-color: #000000; --text-with-accent-color: #000000;
--accent-color-opacity1: rgba(255,152,0,0.04); --accent-color-opacity1: rgba(255,152,0,0.04);
--accent-color-opacity2: rgba(255,152,0,0.12); --accent-color-opacity2: rgba(255,152,0,0.12);
@ -383,6 +424,7 @@
--accent-color-hover: #F4511E; --accent-color-hover: #F4511E;
--accent-color-active: #D84315; --accent-color-active: #D84315;
--accent-color-light: #FFAB91; --accent-color-light: #FFAB91;
--accent-color-visited: #BF360C;
--text-with-accent-color: #000000; --text-with-accent-color: #000000;
--accent-color-opacity1: rgba(255,87,34,0.04); --accent-color-opacity1: rgba(255,87,34,0.04);
--accent-color-opacity2: rgba(255,87,34,0.12); --accent-color-opacity2: rgba(255,87,34,0.12);
@ -400,3 +442,9 @@ body {
color: var(--primary-text-color); color: var(--primary-text-color);
background-color: var(--bg-color); background-color: var(--bg-color);
} }
a:link {
color: var(--link-color);
}
a:visited {
color: var(--link-visited-color);
}

View File

@ -166,10 +166,15 @@
font-family: VideoJS; font-family: VideoJS;
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
position: relative;
top: -3px;
} }
.vjs-icon-cog:before { .vjs-icon-cog:before {
content: "\f110"; content: "\f110";
} }
.video-js .vjs-icon-cog {
font-size: 2em;
}
.vjs-icon-circle, .vjs-seek-to-live-control .vjs-icon-placeholder, .video-js .vjs-volume-level, .video-js .vjs-play-progress { .vjs-icon-circle, .vjs-seek-to-live-control .vjs-icon-placeholder, .video-js .vjs-volume-level, .video-js .vjs-play-progress {
font-family: VideoJS; font-family: VideoJS;
@ -335,7 +340,6 @@
.video-js { .video-js {
display: block; display: block;
vertical-align: top;
box-sizing: border-box; box-sizing: border-box;
color: #fff; color: #fff;
background-color: #000; background-color: #000;
@ -774,7 +778,7 @@ body.vjs-full-window {
} }
.vjs-button > .vjs-icon-placeholder:before { .vjs-button > .vjs-icon-placeholder:before {
font-size: 1.8em; font-size: 2em;
line-height: 1.67; line-height: 1.67;
} }
@ -810,6 +814,7 @@ body.vjs-full-window {
align-items: center; align-items: center;
min-width: 4em; min-width: 4em;
touch-action: none; touch-action: none;
z-index: 1;
} }
.video-js .vjs-progress-control.disabled { .video-js .vjs-progress-control.disabled {
@ -1012,7 +1017,7 @@ body.vjs-full-window {
transition: left 0s; transition: left 0s;
} }
.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover, .video-js .vjs-volume-panel.vjs-volume-panel-horizontal:active, .video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active { .video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover, .video-js .vjs-volume-panel.vjs-volume-panel-horizontal:active, .video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active {
width: 10em; width: 8em;
transition: width 0.1s; transition: width 0.1s;
} }
.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-mute-toggle-only { .video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-mute-toggle-only {
@ -1057,6 +1062,8 @@ body.vjs-full-window {
.vjs-volume-bar.vjs-slider-horizontal { .vjs-volume-bar.vjs-slider-horizontal {
width: 5em; width: 5em;
height: 0.3em; height: 0.3em;
position: relative;
top: -2px;
} }
.vjs-volume-bar.vjs-slider-vertical { .vjs-volume-bar.vjs-slider-vertical {
@ -1237,6 +1244,8 @@ body.vjs-full-window {
.video-js .vjs-play-control { .video-js .vjs-play-control {
cursor: pointer; cursor: pointer;
position: relative;
top: -1px;
} }
.video-js .vjs-play-control .vjs-icon-placeholder { .video-js .vjs-play-control .vjs-icon-placeholder {
@ -1285,15 +1294,19 @@ video::-webkit-media-text-track-display {
.video-js .vjs-picture-in-picture-control { .video-js .vjs-picture-in-picture-control {
cursor: pointer; cursor: pointer;
flex: none; flex: none;
position: relative;
top: -1px;
} }
.video-js .vjs-fullscreen-control { .video-js .vjs-fullscreen-control {
cursor: pointer; cursor: pointer;
flex: none; flex: none;
position: relative;
top: -1px;
} }
.vjs-playback-rate > .vjs-menu-button, .vjs-playback-rate > .vjs-menu-button,
.vjs-playback-rate .vjs-playback-rate-value { .vjs-playback-rate .vjs-playback-rate-value {
position: absolute; position: absolute;
top: 16px; top: 12px;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -1760,7 +1773,10 @@ video::-webkit-media-text-track-display {
-webkit-flex: 0 1 auto; -webkit-flex: 0 1 auto;
-ms-flex: 0 1 auto; -ms-flex: 0 1 auto;
flex: 0 1 auto; flex: 0 1 auto;
width: auto width: auto;
font-size: 14px;
position: relative;
top: -6px;
} }
.video-js .vjs-time-control.vjs-time-divider { .video-js .vjs-time-control.vjs-time-divider {
@ -1815,8 +1831,8 @@ video::-webkit-media-text-track-display {
} }
.video-js .vjs-progress-control:hover { .video-js .vjs-progress-control:hover {
height: 1em; height: 1.25em;
top: -1em top: -0.95em
} }
.video-js .vjs-control-bar { .video-js .vjs-control-bar {
@ -1831,6 +1847,7 @@ video::-webkit-media-text-track-display {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
-webkit-backface-visibility: hidden; -webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: translateY(3em); -webkit-transform: translateY(3em);
-moz-transform: translateY(3em); -moz-transform: translateY(3em);
-ms-transform: translateY(3em); -ms-transform: translateY(3em);
@ -1896,6 +1913,12 @@ video::-webkit-media-text-track-display {
width: 5.5em; width: 5.5em;
left: 1.5em; left: 1.5em;
padding-bottom: .5em; padding-bottom: .5em;
z-index: 1;
bottom: 1.15em;
}
.video-js .vjs-menu-button-popup.vjs-http-source-selector .vjs-menu .vjs-menu-content {
bottom: 1.5em;
} }
.video-js .vjs-menu-button-popup .vjs-menu .vjs-menu-item,.video-js .vjs-menu-button-popup .vjs-menu .vjs-menu-title { .video-js .vjs-menu-button-popup .vjs-menu .vjs-menu-item,.video-js .vjs-menu-button-popup .vjs-menu .vjs-menu-title {
@ -1988,3 +2011,13 @@ video::-webkit-media-text-track-display {
.video-js .vjs-http-source-selector { .video-js .vjs-http-source-selector {
top: 4px; top: 4px;
} }
.vjs-subs-caps-button.vjs-control {
position: relative;
top: -1px;
}
.vjs-volume-panel .vjs-mute-control {
position: relative;
top: -1px;
}

View File

@ -8,6 +8,8 @@ import FtChannelBubble from '../../components/ft-channel-bubble/ft-channel-bubbl
import FtLoader from '../../components/ft-loader/ft-loader.vue' import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.vue' import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import ytch from 'yt-channel-info'
export default Vue.extend({ export default Vue.extend({
name: 'Search', name: 'Search',
components: { components: {
@ -32,7 +34,9 @@ export default Vue.extend({
subCount: 0, subCount: 0,
latestVideosPage: 2, latestVideosPage: 2,
searchPage: 2, searchPage: 2,
videoContinuationString: '',
playlistContinuationString: '', playlistContinuationString: '',
searchContinuationString: '',
channelDescription: '', channelDescription: '',
videoSortBy: 'newest', videoSortBy: 'newest',
playlistSortBy: 'last', playlistSortBy: 'last',
@ -42,6 +46,7 @@ export default Vue.extend({
latestPlaylists: [], latestPlaylists: [],
searchResults: [], searchResults: [],
shownElementList: [], shownElementList: [],
apiUsed: '',
videoSelectNames: [ videoSelectNames: [
'Newest', 'Newest',
'Oldest', 'Oldest',
@ -65,6 +70,18 @@ export default Vue.extend({
} }
}, },
computed: { computed: {
usingElectron: function () {
return this.$store.getters.getUsingElectron
},
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
sessionSearchHistory: function () { sessionSearchHistory: function () {
return this.$store.getters.getSessionSearchHistory return this.$store.getters.getSessionSearchHistory
}, },
@ -77,25 +94,92 @@ export default Vue.extend({
videoSortBy () { videoSortBy () {
this.isElementListLoading = true this.isElementListLoading = true
this.latestVideos = [] this.latestVideos = []
switch (this.apiUsed) {
case 'local':
this.getChannelVideosLocal()
break
case 'invidious':
this.latestVideosPage = 1 this.latestVideosPage = 1
this.channelNextPage() this.channelInvidiousNextPage()
break
default:
this.getChannelVideosLocal()
}
}, },
playlistSortBy () { playlistSortBy () {
this.isElementListLoading = true this.isElementListLoading = true
this.latestPlaylists = [] this.latestPlaylists = []
this.playlistContinuationString = '' this.playlistContinuationString = ''
this.getPlaylists() switch (this.apiUsed) {
case 'local':
this.getPlaylistsLocal()
break
case 'invidious':
this.channelInvidiousNextPage()
break
default:
this.getPlaylistsLocal()
}
} }
}, },
mounted: function () { mounted: function () {
this.id = this.$route.params.id this.id = this.$route.params.id
this.isLoading = true
this.getChannelInfo() if (!this.usingElectron) {
this.getPlaylists() this.getVideoInformationInvidious()
} else {
switch (this.backendPreference) {
case 'local':
this.apiUsed = 'local'
this.getChannelInfoLocal()
this.getChannelVideosLocal()
this.getPlaylistsLocal()
break
case 'invidious':
this.apiUsed = 'invidious'
this.getChannelInfoInvidious()
this.getPlaylistsInvidious()
break
}
}
}, },
methods: { methods: {
getChannelInfo: function () { getChannelInfoLocal: function () {
ytch.getChannelInfo(this.id).then((response) => {
this.id = response.authorId
this.channelName = response.author
this.subCount = response.subscriberCount
this.thumbnailUrl = response.authorThumbnails[2].url
this.bannerUrl = `https://${response.authorBanners[response.authorBanners.length - 1].url}`
this.channelDescription = response.description
this.relatedChannels = response.relatedChannels
this.isLoading = false
}).catch((err) => {
console.log(err)
})
},
getChannelVideosLocal: function () {
this.isElementListLoading = true
ytch.getChannelVideos(this.id, this.videoSortBy).then((response) => {
this.latestVideos = response.items
this.videoContinuationString = response.continuation
this.isElementListLoading = false
})
},
channelLocalNextPage: function () {
console.log(this.videoContinuationString)
ytch.getChannelVideosMore(this.id, this.videoContinuationString).then((response) => {
this.latestVideos = this.latestVideos.concat(response.items)
this.videoContinuationString = response.continuation
console.log(this.videoContinuationString)
})
},
getChannelInfoInvidious: function () {
this.isLoading = true this.isLoading = true
this.$store.dispatch('invidiousGetChannelInfo', this.id).then((response) => { this.$store.dispatch('invidiousGetChannelInfo', this.id).then((response) => {
@ -115,7 +199,7 @@ export default Vue.extend({
}) })
}, },
channelNextPage: function () { channelInvidiousNextPage: function () {
const payload = { const payload = {
resource: 'channels/videos', resource: 'channels/videos',
id: this.id, id: this.id,
@ -132,7 +216,24 @@ export default Vue.extend({
}) })
}, },
getPlaylists: function () { getPlaylistsLocal: function () {
ytch.getChannelPlaylistInfo(this.id, this.playlistSortBy).then((response) => {
console.log(response)
this.latestPlaylists = response.items
this.playlistContinuationString = response.continuation
this.isElementListLoading = false
})
},
getPlaylistsLocalMore: function () {
ytch.getChannelPlaylistsMore(this.id, this.playlistContinuationString).then((response) => {
console.log(response)
this.latestPlaylists = this.latestPlaylists.concat(response.items)
this.playlistContinuationString = response.continuation
})
},
getPlaylistsInvidious: function () {
if (this.playlistContinuationString === null) { if (this.playlistContinuationString === null) {
console.log('There are no more playlists available for this channel') console.log('There are no more playlists available for this channel')
return return
@ -163,13 +264,34 @@ export default Vue.extend({
handleFetchMore: function () { handleFetchMore: function () {
switch (this.currentTab) { switch (this.currentTab) {
case 'videos': case 'videos':
this.channelNextPage() switch (this.apiUsed) {
case 'local':
this.channelLocalNextPage()
break
case 'invidious':
this.channelInvidiousNextPage()
break
}
break break
case 'playlists': case 'playlists':
this.getPlaylists() switch (this.apiUsed) {
case 'local':
this.getPlaylistsLocalMore()
break
case 'invidious':
this.getPlaylistsInvidious()
break
}
break break
case 'search': case 'search':
this.searchChannel() switch (this.apiUsed) {
case 'local':
this.searchChannelLocal()
break
case 'invidious':
this.searchChannelInvidious()
break
}
break break
} }
}, },
@ -180,14 +302,40 @@ export default Vue.extend({
newSearch: function (query) { newSearch: function (query) {
this.lastSearchQuery = query this.lastSearchQuery = query
this.searchContinuationString = ''
this.isElementListLoading = true this.isElementListLoading = true
this.searchPage = 1 this.searchPage = 1
this.searchResults = [] this.searchResults = []
this.changeTab('search') this.changeTab('search')
this.searchChannel() switch (this.apiUsed) {
case 'local':
this.searchChannelLocal()
break
case 'invidious':
this.searchChannelInvidious()
break
}
}, },
searchChannel: function () { searchChannelLocal: function () {
if (this.searchContinuationString === '') {
ytch.searchChannel(this.id, this.lastSearchQuery).then((response) => {
console.log(response)
this.searchResults = response.items
this.isElementListLoading = false
this.searchContinuationString = response.continuation
})
} else {
ytch.searchChannelMore(this.id, this.searchContinuationString).then((response) => {
console.log(response)
this.searchResults = this.searchResults.concat(response.items)
this.isElementListLoading = false
this.searchContinuationString = response.continuation
})
}
},
searchChannelInvidious: function () {
const payload = { const payload = {
resource: 'channels/search', resource: 'channels/search',
id: this.id, id: this.id,

View File

@ -109,7 +109,7 @@
:key="index" :key="index"
:channel-name="channel.author" :channel-name="channel.author"
:channel-id="channel.authorId" :channel-id="channel.authorId"
:channel-thumbnail="channel.authorThumbnails[3].url" :channel-thumbnail="channel.authorThumbnails[channel.authorThumbnails.length - 1].url"
/> />
</ft-flex-box> </ft-flex-box>
</div> </div>

View File

@ -1,123 +0,0 @@
.watchVideo {
width: 65%;
float: left;
margin-top: 0px;
margin-bottom: 10px;
}
.theatreWatchVideo {
float: none;
margin: 0 auto;
width: 85%;
margin-bottom: 10px;
}
.videoPlayer {
width: calc(65% + 30px);
float: left;
margin-top: 0px;
margin-left: 10px;
margin-bottom: 10px;
}
.theatrePlayer {
width: 100%;
float: none;
margin: 0 auto;
margin-bottom: 10px;
}
.watchVideoSideBar {
width: 27%;
max-width: 425px;
float: right;
margin-bottom: 10px;
position: absolute;
}
.watchVideoPlaylist {
right: 10px;
top: 70px;
height: 500px;
}
.theatrePlaylist {
float: none;
margin: 0 auto;
width: 85%;
height: 500px;
margin-bottom: 10px;
max-width: none;
position: static;
}
.watchVideoRecommendations {
right: 10px;
}
.watchVideoRecommendationsNoCard {
top: 70px;
}
.watchVideoRecommendationsLowerCard {
top: 600px;
}
.theatreRecommendations {
float: none;
margin: 0 auto;
width: 85%;
max-width: none;
position: static;
}
@media only screen and (max-width: 1500px) {
.watchVideo {
width: 63%;
}
.videoPlayer {
width: calc(63% + 30px);
}
.theatreWatchVideo {
width: 85%;
}
.theatrePlayer {
width: calc(85% + 30px);
}
}
@media only screen and (max-width: 1350px) {
.watchVideo {
float: none;
margin: 0 auto;
width: 85%;
margin-bottom: 10px;
}
.videoPlayer {
float: none;
margin: 0 auto;
width: calc(85% + 30px);
margin-bottom: 10px;
}
.watchVideoPlaylist {
float: none;
margin: 0 auto;
margin-bottom: 10px;
width: 85%;
max-width: none;
position: static;
}
.watchVideoRecommendations {
float: none;
margin: 0 auto;
width: 85%;
max-width: none;
position: static;
}
}

View File

@ -0,0 +1,57 @@
=dual-column-template
grid-template: "video video sidebar" 0fr "info info sidebar" auto "info info sidebar" auto / 1fr 1fr 1fr
=theatre-mode-template
grid-template: "video video video" auto "info info sidebar" auto "info info sidebar" auto / 1fr 1fr 1fr
=single-column-template
grid-template: "video" auto "info" auto "sidebar" auto / auto
.videoLayout
display: grid
align-items: start
+dual-column-template
@media only screen and (max-width: 1350px)
+theatre-mode-template
@media only screen and (min-width: 901px)
&.useTheatreMode
+theatre-mode-template
@media only screen and (max-width: 900px)
+single-column-template
&.isLoading
+single-column-template
.videoArea
grid-area: video
.videoAreaMargin
margin: 0px 8px 16px
.videoPlayer
grid-column: 1
max-width: calc(80vh * 1.78)
margin: 0 auto
.watchVideo
margin: 0px 8px 16px
grid-column: 1
.infoArea
grid-area: info
.sidebarArea
grid-area: sidebar
@media only screen and (min-width: 901px)
min-width: 380px
.watchVideoPlaylist, .watchVideoSidebar, .theatrePlaylist
height: 500px
margin: 0 8px 16px
.watchVideoRecommendations, .theatreRecommendations
margin: 0 8px 16px

View File

@ -1,11 +1,20 @@
<template> <template>
<div> <div
class="videoLayout"
:class="{
isLoading,
useTheatreMode
}"
>
<ft-loader <ft-loader
v-if="isLoading" v-if="isLoading"
:fullscreen="true" :fullscreen="true"
/> />
<div class="videoArea">
<div class="videoAreaMargin">
<ft-video-player <ft-video-player
v-if="!isLoading && !hidePlayer" v-if="!isLoading && !hidePlayer"
ref="videoPlayer"
:dash-src="dashSrc" :dash-src="dashSrc"
:source-list="activeSourceList" :source-list="activeSourceList"
:caption-list="captionSourceList" :caption-list="captionSourceList"
@ -14,10 +23,12 @@
:thumbnail="thumbnail" :thumbnail="thumbnail"
class="videoPlayer" class="videoPlayer"
:class="{ theatrePlayer: useTheatreMode }" :class="{ theatrePlayer: useTheatreMode }"
ref="videoPlayer"
@ended="handleVideoEnded" @ended="handleVideoEnded"
@error="handleVideoError" @error="handleVideoError"
/> />
</div>
</div>
<div class="infoArea">
<watch-video-info <watch-video-info
v-if="!isLoading" v-if="!isLoading"
:id="videoId" :id="videoId"
@ -25,13 +36,14 @@
:channel-id="channelId" :channel-id="channelId"
:channel-name="channelName" :channel-name="channelName"
:channel-thumbnail="channelThumbnail" :channel-thumbnail="channelThumbnail"
:published="videoPublished"
:subscription-count-text="channelSubscriptionCountText" :subscription-count-text="channelSubscriptionCountText"
:like-count="videoLikeCount" :like-count="videoLikeCount"
:dislike-count="videoDislikeCount" :dislike-count="videoDislikeCount"
:view-count="videoViewCount" :view-count="videoViewCount"
@theatreMode="toggleTheatreMode"
class="watchVideo" class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }" :class="{ theatreWatchVideo: useTheatreMode }"
@theatreMode="toggleTheatreMode"
/> />
<watch-video-description <watch-video-description
v-if="!isLoading" v-if="!isLoading"
@ -47,6 +59,8 @@
class="watchVideo" class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }" :class="{ theatreWatchVideo: useTheatreMode }"
/> />
</div>
<div class="sidebarArea">
<watch-video-live-chat <watch-video-live-chat
v-if="!isLoading && isLive" v-if="!isLoading && isLive"
:video-id="videoId" :video-id="videoId"
@ -57,9 +71,9 @@
<watch-video-playlist <watch-video-playlist
v-if="watchingPlaylist" v-if="watchingPlaylist"
v-show="!isLoading" v-show="!isLoading"
ref="watchVideoPlaylist"
:playlist-id="playlistId" :playlist-id="playlistId"
:video-id="videoId" :video-id="videoId"
ref="watchVideoPlaylist"
class="watchVideoSideBar watchVideoPlaylist" class="watchVideoSideBar watchVideoPlaylist"
:class="{ theatrePlaylist: useTheatreMode }" :class="{ theatrePlaylist: useTheatreMode }"
/> />
@ -74,7 +88,8 @@
}" }"
/> />
</div> </div>
</div>
</template> </template>
<script src="./Watch.js" /> <script src="./Watch.js" />
<style scoped src="./Watch.css" /> <style scoped src="./Watch.sass" lang="sass" />

93
static/locales/en-US.json Normal file
View File

@ -0,0 +1,93 @@
{
"File": "File",
"Quit": "Quit",
"Edit": "Edit",
"Undo": "Undo",
"Redo": "Redo",
"Cut": "Cut",
"Copy": "Copy",
"Paste": "Paste",
"Delete": "Delete",
"Select all": "Select all",
"View": "View",
"Reload": "Reload",
"Force Reload": "Force Reload",
"Toggle Developer Tools": "Toggle Developer Tools",
"Actual size": "Actual size",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Toggle fullscreen": "Toggle fullscreen",
"Window": "Window",
"Minimize": "Minimize",
"Close": "Close",
"FreeTube": "FreeTube",
"Subscriptions": "Subscriptions",
"Featured": "Featured",
"Most Popular": "Most Popular",
"Saved": "Saved",
"Playlists": "Playlists",
"History": "History",
"Settings": "Settings",
"About": "About",
"Search / Go to URL": "Search / Go to URL",
"Search Results": "Search Results",
"Subscriber": "Subscriber",
"Subscribers": "Subscribers",
"Video": "Video",
"Videos": "Videos",
"View Full Playlist": "View Full Playlist",
"Live Now": "Live Now",
"Fetch more results": "Fetch more results",
"Fetching results. Please wait": "Fetching results. Please wait",
"Latest Subscriptions": "Latest Subscriptions",
"Save Video": "Save Video",
"Remove Saved Video": "Remove Saved Video",
"Open in YouTube": "Open in YouTube",
"Copy YouTube Link": "Copy YouTube Link",
"Open in Invidious": "Open in Invidious",
"Copy Invidious Link": "Copy Invidious Link",
"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",
"View": "View",
"Views": "Views",
"Subscribe": "Subscribe",
"Unsubscribe": "Unsubscribe",
"Published on": "Published on",
"Jan": "Jan",
"Feb": "Feb",
"Mar": "Mar",
"Apr": "Apr",
"May": "May",
"Jun": "Jun",
"Jul": "Jul",
"Aug": "Aug",
"Sep": "Sep",
"Oct": "Oct",
"Nov": "Nov",
"Dec": "Dec",
"Show Comments": "Show Comments",
"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.": "Add subscriptions to see them here.",
"Saved Videos": "Saved Videos",
"Watch History": "Watch History",
"Use Dark Theme": "Use Dark Theme",
"Import Subscriptions": "Import Subscriptions",
"Export Subscriptions": "Export Subscriptions",
"Clear History": "Clear History",
"Are you sure you want to delete your history?": "Are you sure you want to delete your history?",
"Clear Saved Videos": "Clear Favorited Videos",
"Are you sure you want to remove all saved videos?": "Are you sure you want to remove all saved videos?",
"Clear Subscriptions": "Clear Subscriptions",
"Are you sure you want to remove all subscriptions?": "Are you sure you want to remove all subscriptions?",
"Save Settings": "Save Settings",
"Yes": "Yes",
"No": "No",
"Beta": "Beta",
"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."
}

View File

@ -143,7 +143,7 @@ function fromCache(request) {
return caches.open(CACHE).then(function (cache) { return caches.open(CACHE).then(function (cache) {
return cache.match(request).then(function (matching) { return cache.match(request).then(function (matching) {
if (!matching || matching.status === 404) { if (!matching || matching.status === 404) {
return Promise.reject('no-match') return Promise.reject(new Error('no-match'))
} }
return matching return matching