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:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
@ -25,6 +23,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run lint
- run: npm run build --if-present
- name: Upload .deb Artifact
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"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/vue-fontawesome": "^0.1.9",
"@fortawesome/fontawesome-svg-core": "^1.2.29",
"@fortawesome/free-solid-svg-icons": "^5.13.1",
"@fortawesome/vue-fontawesome": "^0.1.10",
"@silvermine/videojs-quality-selector": "^1.2.4",
"autolinker": "^3.14.1",
"bulma-pro": "^0.2.0",
@ -31,56 +31,58 @@
"videojs-vtt-thumbnails": "0.0.13",
"vue": "^2.6.11",
"vue-electron": "^1.0.6",
"vue-router": "^3.3.2",
"vue-router": "^3.3.4",
"vuex": "^3.4.0",
"xml2json": "^0.12.0",
"youtube-chat": "^1.1.0",
"youtube-comments-fetch": "^1.0.1",
"youtube-comments-task": "^1.3.15",
"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",
"ytpl": "^0.1.21",
"ytsr": "^0.1.15"
},
"description": "A private YouTube client",
"devDependencies": {
"@babel/core": "^7.10.2",
"@babel/core": "^7.10.3",
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-proposal-object-rest-spread": "^7.10.1",
"@babel/preset-env": "^7.10.2",
"@babel/plugin-proposal-object-rest-spread": "^7.10.3",
"@babel/preset-env": "^7.10.3",
"@babel/preset-typescript": "^7.10.1",
"@typescript-eslint/eslint-plugin": "^3.1.0",
"@typescript-eslint/parser": "^3.1.0",
"acorn": "^7.2.0",
"@typescript-eslint/eslint-plugin": "^3.3.0",
"@typescript-eslint/parser": "^3.3.0",
"acorn": "^7.3.1",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"copy-webpack-plugin": "^6.0.2",
"css-loader": "^3.5.3",
"css-loader": "^3.6.0",
"devtron": "^1.4.0",
"electron": "^8.3.0",
"electron": "^9.0.4",
"electron-builder": "^22.7.0",
"electron-builder-squirrel-windows": "^22.7.0",
"electron-debug": "^3.1.0",
"electron-rebuild": "^1.11.0",
"eslint": "^7.1.0",
"eslint": "^7.3.0",
"eslint-config-prettier": "^6.11.0",
"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-prettier": "^3.1.3",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2",
"fast-glob": "^3.2.2",
"fast-glob": "^3.2.4",
"file-loader": "^6.0.0",
"html-webpack-plugin": "^4.3.0",
"jest": "^26.0.1",
"mini-css-extract-plugin": "^0.9.0",
"node-abi": "^2.18.0",
"node-loader": "^0.6.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.5",
"sass": "^1.26.7",
"sass": "^1.26.8",
"sass-loader": "^8.0.2",
"style-loader": "^1.2.1",
"tree-kill": "1.2.2",
@ -92,7 +94,7 @@
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
},
"license": "GPL-3.0-or-later",
@ -116,7 +118,8 @@
"jest": "jest",
"jest:coverage": "jest --collect-coverage",
"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: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",

View File

@ -14,30 +14,36 @@ app.setName(productName)
// disable electron warning
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
const gotTheLock = app.requestSingleInstanceLock()
// const gotTheLock = app.requestSingleInstanceLock()
const isDev = process.env.NODE_ENV === 'development'
const isDebug = process.argv.includes('--debug')
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
if (!isDev) {
if (gotTheLock) {
app.on('second-instance', () => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow && mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.focus()
})
} else {
app.quit()
process.exit(0)
}
} else {
require('electron-debug')({
showDevTools: !(process.env.RENDERER_REMOTE_DEBUGGING === 'true')
})
}
// if (!isDev) {
// if (gotTheLock) {
// app.on('second-instance', () => {
// // Someone tried to run a second instance, we should focus our window.
// if (mainWindow && mainWindow.isMinimized()) {
// mainWindow.restore()
// }
// mainWindow.focus()
// })
// } else {
// app.quit()
// process.exit(0)
// }
// } else {
// require('electron-debug')({
// showDevTools: !(process.env.RENDERER_REMOTE_DEBUGGING === 'true')
// })
// }
async function installDevTools () {
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;
vertical-align: middle;
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 {

View File

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

View File

@ -8,7 +8,9 @@
}"
@click="$emit('click')"
>
{{ label }}
<slot>
{{ label }}
</slot>
</button>
</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,
default: true
},
dropdownPosition: {
forceDropdown: {
type: Boolean,
default: false
},
dropdownPositionX: {
type: String,
default: 'center'
},
dropdownPositionY: {
type: String,
default: 'bottom'
},
dropdownNames: {
type: Array,
default: () => { return [] }
@ -43,7 +51,7 @@ export default Vue.extend({
},
handleIconClick: function () {
if (this.dropdownNames.length > 0 && this.dropdownValues.length > 0) {
if (this.forceDropdown || (this.dropdownNames.length > 0 && this.dropdownValues.length > 0)) {
this.toggleDropdown()
} else {
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"
/>
<div
v-if="dropdownNames.length > 0 && showDropdown"
v-if="showDropdown"
class="iconDropdown"
:class="{
left: dropdownPosition === 'left',
right: dropdownPosition === 'right',
center: dropdownPosition === 'center'
left: dropdownPositionX === 'left',
right: dropdownPositionX === 'right',
center: dropdownPositionX === 'center',
bottom: dropdownPositionY === 'bottom',
top: dropdownPositionY === 'top'
}"
>
<p
v-for="(label, index) in dropdownNames"
:key="index"
@click="handleDropdownClick(index)"
>
{{ label }}
</p>
<slot>
<ul
v-if="dropdownNames.length > 0"
class="list"
>
<li
v-for="(label, index) in dropdownNames"
:key="index"
class="listItem"
@click="handleDropdownClick(index)"
>
{{ label }}
</li>
</ul>
</slot>
</div>
</div>
</template>
<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 () {
this.id = this._uid
this.inputData = this.value
setTimeout(this.addListener, 200)
},
@ -57,9 +58,8 @@ export default Vue.extend({
this.$emit('click', this.inputData)
},
handleInput: function (input) {
this.inputData = input
this.$emit('input', input)
handleInput: function () {
this.$emit('input', this.inputData)
},
addListener: function () {

View File

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

View File

@ -190,3 +190,58 @@
height: 35px;
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' ||
typeof (this.data.index) !== 'undefined' ||
typeof (this.data.authorId) !== 'undefined' ||
typeof (this.data.publishedText) !== 'undefined' ||
typeof (this.data.authorThumbnails) === 'object'
) {

View File

@ -42,7 +42,7 @@
title="More Options"
theme="base"
:use-shadow="false"
dropdown-position="left"
dropdown-position-x="left"
:dropdown-names="optionsNames"
:dropdown-values="optionsValues"
@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,19 +4,19 @@
>
<input
:id="id"
v-model.number="currentValue"
type="range"
:min="minValue"
:max="maxValue"
:step="step"
v-model.number="currentValue"
@change="$emit('change', $event.target.value)"
>
<span>
{{ label }} -
<span>
{{ label }} -
<span>
{{ displayLabel }}
</span>
{{ displayLabel }}
</span>
</span>
</label>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,7 +44,7 @@ export default Vue.extend({
invidiousInstance: function () {
return this.$store.getters.getInvidiousInstance
},
listType: function () {
return this.$store.getters.getListType
},

View File

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

View File

@ -4,99 +4,101 @@
class="sideNav"
:class="{closed: !isOpen}"
>
<div
class="navOption topNavOption mobileShow"
@click="navigate('subscriptions')"
>
<font-awesome-icon
icon="rss"
class="navIcon"
/>
<p class="navLabel">
Subscriptions
</p>
<font-awesome-icon
class="refreshIcon"
icon="sync"
<div class="inner">
<div
class="navOption topNavOption mobileShow"
@click="navigate('subscriptions')"
>
<font-awesome-icon
icon="rss"
class="navIcon"
/>
<p class="navLabel">
Subscriptions
</p>
<font-awesome-icon
class="refreshIcon"
icon="sync"
/>
</div>
<div
class="navOption mobileHidden"
@click="navigate('trending')"
>
<font-awesome-icon
icon="fire"
class="navIcon"
/>
<p class="navLabel">
Trending
</p>
</div>
<div
class="navOption mobileHidden"
@click="navigate('popular')"
>
<font-awesome-icon
icon="users"
class="navIcon"
/>
<p class="navLabel">
Most Popular
</p>
</div>
<div
class="navOption mobileShow"
@click="navigate('userplaylists')"
>
<font-awesome-icon
icon="bookmark"
class="navIcon"
/>
<p class="navLabel">
Playlists
</p>
</div>
<side-nav-more-options
@navigate="navigate"
/>
<div
class="navOption mobileShow"
@click="navigate('history')"
>
<font-awesome-icon
icon="history"
class="navIcon"
/>
<p class="navLabel">
History
</p>
</div>
<hr>
<div
class="navOption mobileShow"
@click="navigate('settings')"
>
<font-awesome-icon
icon="sliders-h"
class="navIcon"
/>
<p class="navLabel">
Settings
</p>
</div>
<div
class="navOption mobileHidden"
@click="navigate('about')"
>
<font-awesome-icon
icon="info-circle"
class="navIcon"
/>
<p class="navLabel">
About
</p>
</div>
<hr>
</div>
<div
class="navOption mobileHidden"
@click="navigate('trending')"
>
<font-awesome-icon
icon="fire"
class="navIcon"
/>
<p class="navLabel">
Trending
</p>
</div>
<div
class="navOption mobileHidden"
@click="navigate('popular')"
>
<font-awesome-icon
icon="users"
class="navIcon"
/>
<p class="navLabel">
Most Popular
</p>
</div>
<div
class="navOption mobileShow"
@click="navigate('userplaylists')"
>
<font-awesome-icon
icon="bookmark"
class="navIcon"
/>
<p class="navLabel">
Playlists
</p>
</div>
<side-nav-more-options
@navigate="navigate"
/>
<div
class="navOption mobileShow"
@click="navigate('history')"
>
<font-awesome-icon
icon="history"
class="navIcon"
/>
<p class="navLabel">
History
</p>
</div>
<hr>
<div
class="navOption mobileShow"
@click="navigate('settings')"
>
<font-awesome-icon
icon="sliders-h"
class="navIcon"
/>
<p class="navLabel">
Settings
</p>
</div>
<div
class="navOption mobileHidden"
@click="navigate('about')"
>
<font-awesome-icon
icon="info-circle"
class="navIcon"
/>
<p class="navLabel">
About
</p>
</div>
<hr>
</ft-flex-box>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,15 +23,10 @@ export default Vue.extend({
},
data: function () {
return {
dateString: '',
shownDescription: ''
}
},
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 !== '') {
this.shownDescription = this.parseDescriptionHtml(this.descriptionHtml)
} else {

View File

@ -1,6 +1,5 @@
<template>
<ft-card class="videoDescription">
<h4>Published on {{ dateString }}</h4>
<p
class="description"
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 FtFlexBox from '../ft-flex-box/ft-flex-box.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'
export default Vue.extend({
@ -14,7 +14,8 @@ export default Vue.extend({
'ft-button': FtButton,
'ft-list-dropdown': FtListDropdown,
'ft-flex-box': FtFlexBox,
'ft-icon-button': FtIconButton
'ft-icon-button': FtIconButton,
'ft-share-button': FtShareButton
},
props: {
id: {
@ -37,6 +38,10 @@ export default Vue.extend({
type: String,
required: true
},
published: {
type: Number,
required: true
},
viewCount: {
type: Number,
required: true
@ -66,19 +71,6 @@ export default Vue.extend({
'dash',
'legacy',
'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
},
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 () {
return this.likeCount + this.dislikeCount
},
@ -117,6 +97,12 @@ export default Vue.extend({
subscribedText: function () {
return `SUBSCRIBE ${this.subscriptionCountText}`
},
dateString() {
const date = new Date(this.published)
const dateSplit = date.toDateString().split(' ')
return `${dateSplit[1]} ${dateSplit[2]}, ${dateSplit[3]}`
}
},
methods: {
@ -140,43 +126,6 @@ export default Vue.extend({
this.$parent.enableAudioFormat()
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,79 +1,87 @@
<template>
<ft-card class="relative watchVideoInfo">
<p
class="videoTitle"
>
{{ title }}
</p>
<div
class="channelInformation"
>
<img
:src="channelThumbnail"
class="channelThumbnail"
@click="goToChannel"
<ft-card class="watchVideoInfo">
<div>
<p
class="videoTitle"
>
<span
class="channelName"
@click="goToChannel"
>
{{ channelName }}
</span>
<ft-button
:label="subscribedText"
class="subscribeButton"
background-color="var(--primary-color)"
@click="handleSubscription"
/>
</div>
<ft-flex-box class="videoOptions">
<ft-icon-button
title="Toggle Theatre Mode"
class="theatreModeButton"
icon="expand-alt"
theme="secondary"
@click="$emit('theatreMode')"
/>
<ft-icon-button
title="Change Video Formats"
theme="secondary"
icon="file-video"
:dropdown-names="formatTypeNames"
:dropdown-values="formatTypeValues"
@click="handleFormatChange"
/>
<ft-icon-button
title="Share Video"
theme="secondary"
icon="share-alt"
:dropdown-names="shareNames"
:dropdown-values="shareValues"
@click="handleShare"
/>
</ft-flex-box>
<p class="viewCount">
{{ parsedViewCount }}
</p>
<div class="likeBarContainer">
{{ title }}
</p>
<div
class="likeBar"
:style="{ width: likePercentageRatio + '%' }"
/>
<div class="dislikeBar" />
class="channelInformation"
>
<div
class="profileRow"
>
<div>
<img
:src="channelThumbnail"
class="channelThumbnail"
@click="goToChannel"
>
</div>
<div>
<div
class="channelName"
@click="goToChannel"
>
{{ channelName }}
</div>
<ft-button
:label="subscribedText"
class="subscribeButton"
background-color="var(--primary-color)"
@click="handleSubscription"
/>
</div>
</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
title="Toggle Theatre Mode"
class="theatreModeButton option"
icon="expand-alt"
theme="secondary"
@click="$emit('theatreMode')"
/>
<ft-icon-button
title="Change Video Formats"
class="option"
theme="secondary"
icon="file-video"
:dropdown-names="formatTypeNames"
:dropdown-values="formatTypeValues"
@click="handleFormatChange"
/>
<ft-share-button
:id="id"
class="option"
/>
</div>
</div>
<p class="likeCountContainer">
<font-awesome-icon
icon="thumbs-up"
/>
{{ likeCount }}
&nbsp;
<font-awesome-icon
icon="thumbs-down"
/>
{{ dislikeCount }}
</p>
</ft-card>
</template>
<script src="./watch-video-info.js" />
<style scoped src="./watch-video-info.css" />
<style scoped src="./watch-video-info.sass" lang="sass" />

View File

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

View File

@ -75,32 +75,3 @@
position: relative;
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 {
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: '',
invidiousInstance: 'https://invidio.us',
barColor: false,
enableSearchSuggestions: true,
rememberHistory: true,
autoplayVideos: true,
autoplayPlaylists: true,
@ -72,6 +73,10 @@ const getters = {
return state.barColor
},
getEnableSearchSuggestions: () => {
return state.enableSearchSuggestions
},
getBackendPreference: () => {
return state.backendPreference
},
@ -169,6 +174,9 @@ const actions = {
case 'checkForUpdates':
commit('setCheckForUpdates', result.value)
break
case 'enableSearchSuggestions':
commit('setEnableSearchSuggestions', result.value)
break
case 'backendPreference':
commit('setBackendPreference', result.value)
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) {
settingsDb.update({ _id: 'backendPreference' }, { _id: 'backendPreference', value: backendPreference }, { upsert: true }, (err, numReplaced) => {
if (!err) {
@ -440,6 +456,9 @@ const mutations = {
setBarColor (state, barColor) {
state.barColor = barColor
},
setEnableSearchSuggestions (state, enableSearchSuggestions) {
state.enableSearchSuggestions = enableSearchSuggestions
},
setRememberHistory (state, rememberHistory) {
state.rememberHistory = rememberHistory
},

View File

@ -53,18 +53,37 @@ const actions = {
return state.colorClasses[randomInt]
},
getVideoIdFromUrl ({ state }, url) {
console.log('checking for id')
console.log(url)
const rx = /^.*(?:(?:(you|hook)tu\.?be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/
const match = url.match(rx)
if (match) {
return match[2]
} else {
getVideoIdFromUrl (_, url) {
/** @type {URL} */
let urlObject
try {
urlObject = new URL(url)
} catch (e) {
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);
--title-color: #3f7ac6;
--bg-color: #f1f1f1;
--link-color: var(--accent-color);
--link-visited-color: var(--accent-color-visited);
--card-bg-color: #FFFFFF;
--secondary-card-bg-color: #eeeeee;
--side-nav-color: #FFFFFF;
--side-nav-hover-color: #e0e0e0;
--side-nav-active-color: #757575;
--search-bar-color: #f5f5f5;
--instance-menu-color: var(--search-bar-color);
--logo-icon: url("~../../_icons/iconColorSmall.png");
--logo-text: url("~../../_icons/textColorSmall.png");
}
@ -20,21 +23,44 @@
.dark {
--primary-text-color: #EEEEEE;
--secondary-text-color: #ddd;
--tertiary-text-color: #888;
--tertiary-text-color: #999;
--primary-input-color: rgba(0, 0, 0, 0.50);
--primary-shadow-color: rgba(0, 0, 0, 0.75);
--title-color: #EEEEEE;
--bg-color: #212121;
--link-color: var(--accent-color);
--link-visited-color: var(--accent-color-visited);
--card-bg-color: #303030;
--secondary-card-bg-color: rgba(0, 0, 0, 0.75);
--side-nav-color: #262626;
--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");
}
.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 {
--primary-text-color: #EEEEEE;
@ -203,6 +229,7 @@
--accent-color-hover: #e53935;
--accent-color-active: #c62828;
--accent-color-light: #ef9a9a;
--accent-color-visited: #b71c1c;
--text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(244,67,54,0.04);
--accent-color-opacity2: rgba(244,67,54,0.12);
@ -215,6 +242,7 @@
--accent-color-hover: #D81B60;
--accent-color-active: #AD1457;
--accent-color-light: #F48FB1;
--accent-color-visited: #880E4F;
--text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(233,30,99,0.04);
--accent-color-opacity2: rgba(233,30,99,0.12);
@ -227,6 +255,7 @@
--accent-color-hover: #8E24AA;
--accent-color-active: #6A1B9A;
--accent-color-light: #CE93D8;
--accent-color-visited: #4A148C;
--text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(156,39,176,0.04);
--accent-color-opacity2: rgba(156,39,176,0.12);
@ -239,6 +268,7 @@
--accent-color-hover: #5E35B1;
--accent-color-active: #4527A0;
--accent-color-light: #B39DDB;
--accent-color-visited: #311B92;
--text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(103,58,183,0.04);
--accent-color-opacity2: rgba(103,58,183,0.12);
@ -251,6 +281,7 @@
--accent-color-hover: #3949AB;
--accent-color-active: #283593;
--accent-color-light: #9FA8DA;
--accent-color-visited: #1A237E;
--text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(63,81,181,0.04);
--accent-color-opacity2: rgba(63,81,181,0.12);
@ -263,6 +294,7 @@
--accent-color-hover: #1E88E5;
--accent-color-active: #1565C0;
--accent-color-light: #90CAF9;
--accent-color-visited: #0D47A1;
--text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(33,150,243,0.04);
--accent-color-opacity2: rgba(33,150,243,0.12);
@ -275,6 +307,7 @@
--accent-color-hover: #039BE5;
--accent-color-active: #0277BD;
--accent-color-light: #81D4FA;
--accent-color-visited: #01579B;
--text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(3,169,244,0.04);
--accent-color-opacity2: rgba(3,169,244,0.12);
@ -287,6 +320,7 @@
--accent-color-hover: #00ACC1;
--accent-color-active: #00838F;
--accent-color-light: #80DEEA;
--accent-color-visited: #006064;
--text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(0,188,212,0.04);
--accent-color-opacity2: rgba(0,188,212,0.12);
@ -299,6 +333,7 @@
--accent-color-hover: #00897B;
--accent-color-active: #00695C;
--accent-color-light: #80CBC4;
--accent-color-visited: #004D40;
--text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(0,150,136,0.04);
--accent-color-opacity2: rgba(0,150,136,0.12);
@ -311,6 +346,7 @@
--accent-color-hover: #43A047;
--accent-color-active: #2E7D32;
--accent-color-light: #A5D6A7;
--accent-color-visited: #1B5E20;
--text-with-accent-color: #FFFFFF;
--accent-color-opacity1: rgba(76,175,80,0.04);
--accent-color-opacity2: rgba(76,175,80,0.12);
@ -323,6 +359,7 @@
--accent-color-hover: #7CB342;
--accent-color-active: #558B2F;
--accent-color-light: #C5E1A5;
--accent-color-visited: #33691E;
--text-with-accent-color: #000000;
--accent-color-opacity1: rgba(139,195,74,0.04);
--accent-color-opacity2: rgba(139,195,74,0.12);
@ -335,6 +372,7 @@
--accent-color-hover: #C0CA33;
--accent-color-active: #9E9D24;
--accent-color-light: #E6EE9C;
--accent-color-visited: #827717;
--text-with-accent-color: #000000;
--accent-color-opacity1: rgba(205,220,57,0.04);
--accent-color-opacity2: rgba(205,220,57,0.12);
@ -347,6 +385,7 @@
--accent-color-hover: #FDD835;
--accent-color-active: #F9A825;
--accent-color-light: #FFF59D;
--accent-color-visited: #F57F17;
--text-with-accent-color: #000000;
--accent-color-opacity1: rgba(255,235,59,0.04);
--accent-color-opacity2: rgba(255,235,59,0.12);
@ -359,6 +398,7 @@
--accent-color-hover: #FFB300;
--accent-color-active: #FF8F00;
--accent-color-light: #FFE082;
--accent-color-visited: #FF6F00;
--text-with-accent-color: #000000;
--accent-color-opacity1: rgba(255,193,7,0.04);
--accent-color-opacity2: rgba(255,193,7,0.12);
@ -371,6 +411,7 @@
--accent-color-hover: #FB8C00;
--accent-color-active: #EF6C00;
--accent-color-light: #FFCC80;
--accent-color-visited: #E65100;
--text-with-accent-color: #000000;
--accent-color-opacity1: rgba(255,152,0,0.04);
--accent-color-opacity2: rgba(255,152,0,0.12);
@ -383,6 +424,7 @@
--accent-color-hover: #F4511E;
--accent-color-active: #D84315;
--accent-color-light: #FFAB91;
--accent-color-visited: #BF360C;
--text-with-accent-color: #000000;
--accent-color-opacity1: rgba(255,87,34,0.04);
--accent-color-opacity2: rgba(255,87,34,0.12);
@ -400,3 +442,9 @@ body {
color: var(--primary-text-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-weight: normal;
font-style: normal;
position: relative;
top: -3px;
}
.vjs-icon-cog:before {
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 {
font-family: VideoJS;
@ -335,7 +340,6 @@
.video-js {
display: block;
vertical-align: top;
box-sizing: border-box;
color: #fff;
background-color: #000;
@ -774,7 +778,7 @@ body.vjs-full-window {
}
.vjs-button > .vjs-icon-placeholder:before {
font-size: 1.8em;
font-size: 2em;
line-height: 1.67;
}
@ -810,6 +814,7 @@ body.vjs-full-window {
align-items: center;
min-width: 4em;
touch-action: none;
z-index: 1;
}
.video-js .vjs-progress-control.disabled {
@ -1012,7 +1017,7 @@ body.vjs-full-window {
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 {
width: 10em;
width: 8em;
transition: width 0.1s;
}
.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 {
width: 5em;
height: 0.3em;
position: relative;
top: -2px;
}
.vjs-volume-bar.vjs-slider-vertical {
@ -1237,6 +1244,8 @@ body.vjs-full-window {
.video-js .vjs-play-control {
cursor: pointer;
position: relative;
top: -1px;
}
.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 {
cursor: pointer;
flex: none;
position: relative;
top: -1px;
}
.video-js .vjs-fullscreen-control {
cursor: pointer;
flex: none;
position: relative;
top: -1px;
}
.vjs-playback-rate > .vjs-menu-button,
.vjs-playback-rate .vjs-playback-rate-value {
position: absolute;
top: 16px;
top: 12px;
left: 0;
width: 100%;
height: 100%;
@ -1760,7 +1773,10 @@ video::-webkit-media-text-track-display {
-webkit-flex: 0 1 auto;
-ms-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 {
@ -1815,8 +1831,8 @@ video::-webkit-media-text-track-display {
}
.video-js .vjs-progress-control:hover {
height: 1em;
top: -1em
height: 1.25em;
top: -0.95em
}
.video-js .vjs-control-bar {
@ -1831,6 +1847,7 @@ video::-webkit-media-text-track-display {
visibility: visible;
opacity: 1;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: translateY(3em);
-moz-transform: translateY(3em);
-ms-transform: translateY(3em);
@ -1896,6 +1913,12 @@ video::-webkit-media-text-track-display {
width: 5.5em;
left: 1.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 {
@ -1988,3 +2011,13 @@ video::-webkit-media-text-track-display {
.video-js .vjs-http-source-selector {
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 FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import ytch from 'yt-channel-info'
export default Vue.extend({
name: 'Search',
components: {
@ -32,7 +34,9 @@ export default Vue.extend({
subCount: 0,
latestVideosPage: 2,
searchPage: 2,
videoContinuationString: '',
playlistContinuationString: '',
searchContinuationString: '',
channelDescription: '',
videoSortBy: 'newest',
playlistSortBy: 'last',
@ -42,6 +46,7 @@ export default Vue.extend({
latestPlaylists: [],
searchResults: [],
shownElementList: [],
apiUsed: '',
videoSelectNames: [
'Newest',
'Oldest',
@ -65,6 +70,18 @@ export default Vue.extend({
}
},
computed: {
usingElectron: function () {
return this.$store.getters.getUsingElectron
},
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
sessionSearchHistory: function () {
return this.$store.getters.getSessionSearchHistory
},
@ -77,25 +94,92 @@ export default Vue.extend({
videoSortBy () {
this.isElementListLoading = true
this.latestVideos = []
this.latestVideosPage = 1
this.channelNextPage()
switch (this.apiUsed) {
case 'local':
this.getChannelVideosLocal()
break
case 'invidious':
this.latestVideosPage = 1
this.channelInvidiousNextPage()
break
default:
this.getChannelVideosLocal()
}
},
playlistSortBy () {
this.isElementListLoading = true
this.latestPlaylists = []
this.playlistContinuationString = ''
this.getPlaylists()
switch (this.apiUsed) {
case 'local':
this.getPlaylistsLocal()
break
case 'invidious':
this.channelInvidiousNextPage()
break
default:
this.getPlaylistsLocal()
}
}
},
mounted: function () {
this.id = this.$route.params.id
this.isLoading = true
this.getChannelInfo()
this.getPlaylists()
if (!this.usingElectron) {
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: {
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.$store.dispatch('invidiousGetChannelInfo', this.id).then((response) => {
@ -115,7 +199,7 @@ export default Vue.extend({
})
},
channelNextPage: function () {
channelInvidiousNextPage: function () {
const payload = {
resource: 'channels/videos',
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) {
console.log('There are no more playlists available for this channel')
return
@ -163,13 +264,34 @@ export default Vue.extend({
handleFetchMore: function () {
switch (this.currentTab) {
case 'videos':
this.channelNextPage()
switch (this.apiUsed) {
case 'local':
this.channelLocalNextPage()
break
case 'invidious':
this.channelInvidiousNextPage()
break
}
break
case 'playlists':
this.getPlaylists()
switch (this.apiUsed) {
case 'local':
this.getPlaylistsLocalMore()
break
case 'invidious':
this.getPlaylistsInvidious()
break
}
break
case 'search':
this.searchChannel()
switch (this.apiUsed) {
case 'local':
this.searchChannelLocal()
break
case 'invidious':
this.searchChannelInvidious()
break
}
break
}
},
@ -180,14 +302,40 @@ export default Vue.extend({
newSearch: function (query) {
this.lastSearchQuery = query
this.searchContinuationString = ''
this.isElementListLoading = true
this.searchPage = 1
this.searchResults = []
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 = {
resource: 'channels/search',
id: this.id,

View File

@ -109,7 +109,7 @@
:key="index"
:channel-name="channel.author"
:channel-id="channel.authorId"
:channel-thumbnail="channel.authorThumbnails[3].url"
:channel-thumbnail="channel.authorThumbnails[channel.authorThumbnails.length - 1].url"
/>
</ft-flex-box>
</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,80 +1,95 @@
<template>
<div>
<div
class="videoLayout"
:class="{
isLoading,
useTheatreMode
}"
>
<ft-loader
v-if="isLoading"
:fullscreen="true"
/>
<ft-video-player
v-if="!isLoading && !hidePlayer"
:dash-src="dashSrc"
:source-list="activeSourceList"
:caption-list="captionSourceList"
:storyboard-src="videoStoryboardSrc"
:format="activeFormat"
:thumbnail="thumbnail"
class="videoPlayer"
:class="{ theatrePlayer: useTheatreMode }"
ref="videoPlayer"
@ended="handleVideoEnded"
@error="handleVideoError"
/>
<watch-video-info
v-if="!isLoading"
:id="videoId"
:title="videoTitle"
:channel-id="channelId"
:channel-name="channelName"
:channel-thumbnail="channelThumbnail"
:subscription-count-text="channelSubscriptionCountText"
:like-count="videoLikeCount"
:dislike-count="videoDislikeCount"
:view-count="videoViewCount"
@theatreMode="toggleTheatreMode"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
/>
<watch-video-description
v-if="!isLoading"
:published="videoPublished"
:description="videoDescription"
:description-html="videoDescriptionHtml"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
/>
<watch-video-comments
v-if="!isLoading && !isLive"
:id="videoId"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
/>
<watch-video-live-chat
v-if="!isLoading && isLive"
:video-id="videoId"
:channel-name="channelName"
class="watchVideoSideBar watchVideoPlaylist"
:class="{ theatrePlaylist: useTheatreMode }"
/>
<watch-video-playlist
v-if="watchingPlaylist"
v-show="!isLoading"
:playlist-id="playlistId"
:video-id="videoId"
ref="watchVideoPlaylist"
class="watchVideoSideBar watchVideoPlaylist"
:class="{ theatrePlaylist: useTheatreMode }"
/>
<watch-video-recommendations
v-if="!isLoading"
:data="recommendedVideos"
class="watchVideoSideBar watchVideoRecommendations"
:class="{
theatreRecommendations: useTheatreMode,
watchVideoRecommendationsLowerCard: watchingPlaylist || isLive,
watchVideoRecommendationsNoCard: !watchingPlaylist || !isLive
}"
/>
<div class="videoArea">
<div class="videoAreaMargin">
<ft-video-player
v-if="!isLoading && !hidePlayer"
ref="videoPlayer"
:dash-src="dashSrc"
:source-list="activeSourceList"
:caption-list="captionSourceList"
:storyboard-src="videoStoryboardSrc"
:format="activeFormat"
:thumbnail="thumbnail"
class="videoPlayer"
:class="{ theatrePlayer: useTheatreMode }"
@ended="handleVideoEnded"
@error="handleVideoError"
/>
</div>
</div>
<div class="infoArea">
<watch-video-info
v-if="!isLoading"
:id="videoId"
:title="videoTitle"
:channel-id="channelId"
:channel-name="channelName"
:channel-thumbnail="channelThumbnail"
:published="videoPublished"
:subscription-count-text="channelSubscriptionCountText"
:like-count="videoLikeCount"
:dislike-count="videoDislikeCount"
:view-count="videoViewCount"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
@theatreMode="toggleTheatreMode"
/>
<watch-video-description
v-if="!isLoading"
:published="videoPublished"
:description="videoDescription"
:description-html="videoDescriptionHtml"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
/>
<watch-video-comments
v-if="!isLoading && !isLive"
:id="videoId"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
/>
</div>
<div class="sidebarArea">
<watch-video-live-chat
v-if="!isLoading && isLive"
:video-id="videoId"
:channel-name="channelName"
class="watchVideoSideBar watchVideoPlaylist"
:class="{ theatrePlaylist: useTheatreMode }"
/>
<watch-video-playlist
v-if="watchingPlaylist"
v-show="!isLoading"
ref="watchVideoPlaylist"
:playlist-id="playlistId"
:video-id="videoId"
class="watchVideoSideBar watchVideoPlaylist"
:class="{ theatrePlaylist: useTheatreMode }"
/>
<watch-video-recommendations
v-if="!isLoading"
:data="recommendedVideos"
class="watchVideoSideBar watchVideoRecommendations"
:class="{
theatreRecommendations: useTheatreMode,
watchVideoRecommendationsLowerCard: watchingPlaylist || isLive,
watchVideoRecommendationsNoCard: !watchingPlaylist || !isLive
}"
/>
</div>
</div>
</template>
<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 cache.match(request).then(function (matching) {
if (!matching || matching.status === 404) {
return Promise.reject('no-match')
return Promise.reject(new Error('no-match'))
}
return matching