mirror of https://github.com/FreeTubeApp/FreeTube
Merge branch 'v0.17.0-RC'
This commit is contained in:
commit
0bcf2d1ece
10
.babelrc
10
.babelrc
|
@ -4,15 +4,13 @@
|
||||||
"@babel/env",
|
"@babel/env",
|
||||||
{
|
{
|
||||||
"targets": {
|
"targets": {
|
||||||
"chrome": "73",
|
"chrome": "96",
|
||||||
"node": 12
|
"node": 16
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"@babel/typescript"
|
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@babel/proposal-class-properties",
|
"@babel/proposal-class-properties"
|
||||||
"@babel/proposal-object-rest-spread"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ body:
|
||||||
required: true
|
required: true
|
||||||
- label: I have searched the [issue tracker](https://github.com/FreeTubeApp/FreeTube/issues) for a bug report that matches the one I want to file, without success.
|
- label: I have searched the [issue tracker](https://github.com/FreeTubeApp/FreeTube/issues) for a bug report that matches the one I want to file, without success.
|
||||||
required: true
|
required: true
|
||||||
|
- label: I have searched the [documentation](https://docs.freetubeapp.io/) for information that matches the description of the bug I want to file, without success.
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe the bug
|
label: Describe the bug
|
||||||
|
@ -113,4 +115,4 @@ body:
|
||||||
description: Please ensure you've completed the following, if applicable.
|
description: Please ensure you've completed the following, if applicable.
|
||||||
options:
|
options:
|
||||||
- label: I have encountered this bug in the latest [nightly build](https://docs.freetubeapp.io/development/nightly-builds).
|
- label: I have encountered this bug in the latest [nightly build](https://docs.freetubeapp.io/development/nightly-builds).
|
||||||
required: false
|
required: false
|
||||||
|
|
|
@ -15,6 +15,8 @@ body:
|
||||||
options:
|
options:
|
||||||
- label: I have searched the [issue tracker](https://github.com/FreeTubeApp/FreeTube/issues) for a feature request that matches the one I want to file, without success.
|
- label: I have searched the [issue tracker](https://github.com/FreeTubeApp/FreeTube/issues) for a feature request that matches the one I want to file, without success.
|
||||||
required: true
|
required: true
|
||||||
|
- label: I have searched the [documentation](https://docs.freetubeapp.io/) for information that matches the description of the feature request I want to file, without success.
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Problem Description
|
label: Problem Description
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
name: "Label Duplicate"
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: Amwam/issue-comment-action@v1.3.1
|
||||||
|
with:
|
||||||
|
keywords: '["duplicate of"]'
|
||||||
|
labels: '["U: duplicate"]'
|
||||||
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
@ -11,11 +11,11 @@ jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [14.x]
|
node-version: [16.x]
|
||||||
runtime: [ linux-x64, linux-arm64, win-x64, osx-x64 ]
|
runtime: [ linux-x64, linux-arm64, win-x64, osx-x64 ]
|
||||||
include:
|
include:
|
||||||
- runtime: linux-x64
|
- runtime: linux-x64
|
||||||
os: ubuntu-latest
|
os: ubuntu-18.04
|
||||||
|
|
||||||
- runtime: linux-arm64
|
- runtime: linux-arm64
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
|
|
@ -6,7 +6,7 @@ name: Linter
|
||||||
# events but only for the master branch
|
# events but only for the master branch
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master, development ]
|
branches: [ master, development, '**-RC' ]
|
||||||
|
|
||||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -18,10 +18,10 @@ jobs:
|
||||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 16.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 16.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: npm run ci
|
- run: npm run ci
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [14.x]
|
node-version: [16.x]
|
||||||
runtime: [ linux-x64, linux-arm64, win-x64, osx-x64 ]
|
runtime: [ linux-x64, linux-arm64, win-x64, osx-x64 ]
|
||||||
include:
|
include:
|
||||||
- runtime: linux-x64
|
- runtime: linux-x64
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
name: Remove outdated labels
|
||||||
|
on:
|
||||||
|
# https://github.community/t/github-actions-are-severely-limited-on-prs/18179/15
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
jobs:
|
||||||
|
remove-merged-pr-labels:
|
||||||
|
name: Remove merged pull request labels
|
||||||
|
if: github.event.pull_request.merged
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: mondeja/remove-labels-gh-action@v1.1.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
labels: |
|
||||||
|
PR: waiting for review
|
||||||
|
PR: WIP
|
||||||
|
PR: changes requested
|
||||||
|
PR: merge conflicts / rebase needed
|
||||||
|
PR/Issue: dependent
|
||||||
|
|
||||||
|
remove-closed-pr-labels:
|
||||||
|
name: Remove closed pull request labels
|
||||||
|
if: github.event_name == 'pull_request_target' && (! github.event.pull_request.merged)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: mondeja/remove-labels-gh-action@v1.1.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
labels: |
|
||||||
|
PR: waiting for review
|
||||||
|
PR: WIP
|
||||||
|
PR: changes requested
|
||||||
|
PR: merge conflicts / rebase needed
|
||||||
|
PR/Issue: dependent
|
|
@ -18,6 +18,8 @@ FreeTube is supported by the [Privacy Redirect](https://github.com/SimonBrazell/
|
||||||
|
|
||||||
Download Privacy Redirect for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/privacy-redirect/) or [Google Chrome](https://chrome.google.com/webstore/detail/privacy-redirect/pmcmeagblkinmogikoikkdjiligflglb).
|
Download Privacy Redirect for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/privacy-redirect/) or [Google Chrome](https://chrome.google.com/webstore/detail/privacy-redirect/pmcmeagblkinmogikoikkdjiligflglb).
|
||||||
|
|
||||||
|
Disclaimer: Learn more about why a browser extension is bad for your [privacy](https://www.privacyguides.org/browsers/#extensions).
|
||||||
|
|
||||||
If you have issues with the extension working with FreeTube, please create an issue in this repository instead of the extension repository.
|
If you have issues with the extension working with FreeTube, please create an issue in this repository instead of the extension repository.
|
||||||
|
|
||||||
## How does it work?
|
## How does it work?
|
||||||
|
@ -62,6 +64,8 @@ Chocolatey: [Download](https://chocolatey.org/packages/freetube/)
|
||||||
|
|
||||||
Windows Portable: [Download](https://github.com/rddim/FreeTubePortable/releases) [Source](https://github.com/rddim/FreeTubePortable)
|
Windows Portable: [Download](https://github.com/rddim/FreeTubePortable/releases) [Source](https://github.com/rddim/FreeTubePortable)
|
||||||
|
|
||||||
|
Windows Package Manager (winget): [Usage](https://docs.microsoft.com/en-us/windows/package-manager/winget/)
|
||||||
|
|
||||||
### Automated Builds (Nightly / Weekly)
|
### Automated Builds (Nightly / Weekly)
|
||||||
|
|
||||||
Builds are automatically created from changes to our development branch via [GitHub Actions](https://github.com/FreeTubeApp/FreeTube/actions?query=workflow%3ABuild).
|
Builds are automatically created from changes to our development branch via [GitHub Actions](https://github.com/FreeTubeApp/FreeTube/actions?query=workflow%3ABuild).
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 7.3 KiB |
|
@ -7,13 +7,22 @@ const { name, productName } = require('../package.json')
|
||||||
const args = process.argv
|
const args = process.argv
|
||||||
|
|
||||||
let targets
|
let targets
|
||||||
var platform = os.platform()
|
const platform = os.platform()
|
||||||
|
const cpus = os.cpus()
|
||||||
|
|
||||||
if (platform == 'darwin') {
|
if (platform === 'darwin') {
|
||||||
targets = Platform.MAC.createTarget()
|
let arch = Arch.x64
|
||||||
} else if (platform == 'win32') {
|
|
||||||
|
// Macbook Air 2020 with M1 = 'Apple M1'
|
||||||
|
// Macbook Pro 2021 with M1 Pro = 'Apple M1 Pro'
|
||||||
|
if (cpus[0].model.startsWith('Apple')) {
|
||||||
|
arch = Arch.arm64
|
||||||
|
}
|
||||||
|
|
||||||
|
targets = Platform.MAC.createTarget(['dmg'], arch)
|
||||||
|
} else if (platform === 'win32') {
|
||||||
targets = Platform.WINDOWS.createTarget()
|
targets = Platform.WINDOWS.createTarget()
|
||||||
} else if (platform == 'linux') {
|
} else if (platform === 'linux') {
|
||||||
let arch = Arch.x64
|
let arch = Arch.x64
|
||||||
|
|
||||||
if (args[2] === 'arm64') {
|
if (args[2] === 'arm64') {
|
||||||
|
|
|
@ -115,17 +115,20 @@ function startRenderer(callback) {
|
||||||
console.log(`\nWatching file changes for ${name} script...`)
|
console.log(`\nWatching file changes for ${name} script...`)
|
||||||
})
|
})
|
||||||
|
|
||||||
const server = new WebpackDevServer(compiler, {
|
const server = new WebpackDevServer({
|
||||||
static: {
|
static: {
|
||||||
directory: path.join(process.cwd(), 'static'),
|
directory: path.join(process.cwd(), 'static'),
|
||||||
watch: {
|
watch: {
|
||||||
ignored: /(dashFiles|storyboards)\/*/
|
ignored: [
|
||||||
|
/(dashFiles|storyboards)\/*/,
|
||||||
|
'/**/.DS_Store',
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
port
|
port
|
||||||
})
|
}, compiler)
|
||||||
|
|
||||||
server.listen(port, '', err => {
|
server.startCallback(err => {
|
||||||
if (err) console.error(err)
|
if (err) console.error(err)
|
||||||
|
|
||||||
callback()
|
callback()
|
||||||
|
|
|
@ -49,7 +49,7 @@ const config = {
|
||||||
path: path.join(__dirname, '../dist'),
|
path: path.join(__dirname, '../dist'),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.ts', '.js', '.json'],
|
extensions: ['.js', '.json'],
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.join(__dirname, '../src/'),
|
'@': path.join(__dirname, '../src/'),
|
||||||
src: path.join(__dirname, '../src/'),
|
src: path.join(__dirname, '../src/'),
|
||||||
|
|
|
@ -138,7 +138,7 @@ const config = {
|
||||||
images: path.join(__dirname, '../src/renderer/assets/img/'),
|
images: path.join(__dirname, '../src/renderer/assets/img/'),
|
||||||
static: path.join(__dirname, '../static/'),
|
static: path.join(__dirname, '../static/'),
|
||||||
},
|
},
|
||||||
extensions: ['.ts', '.js', '.vue', '.json'],
|
extensions: ['.js', '.vue', '.json'],
|
||||||
},
|
},
|
||||||
target: 'electron-renderer',
|
target: 'electron-renderer',
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ const config = {
|
||||||
mode: process.env.NODE_ENV,
|
mode: process.env.NODE_ENV,
|
||||||
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
|
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
|
||||||
entry: {
|
entry: {
|
||||||
workerSample: path.join(__dirname, '../src/utilities/workerSample.ts'),
|
workerSample: path.join(__dirname, '../src/utilities/workerSample.js'),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
libraryTarget: 'commonjs2',
|
libraryTarget: 'commonjs2',
|
||||||
|
@ -52,7 +52,7 @@ const config = {
|
||||||
'@': path.join(__dirname, '../src/'),
|
'@': path.join(__dirname, '../src/'),
|
||||||
src: path.join(__dirname, '../src/'),
|
src: path.join(__dirname, '../src/'),
|
||||||
},
|
},
|
||||||
extensions: ['.ts', '.js', '.json'],
|
extensions: ['.js', '.json'],
|
||||||
},
|
},
|
||||||
target: 'node',
|
target: 'node',
|
||||||
}
|
}
|
||||||
|
|
55
package.json
55
package.json
|
@ -2,7 +2,7 @@
|
||||||
"name": "freetube",
|
"name": "freetube",
|
||||||
"productName": "FreeTube",
|
"productName": "FreeTube",
|
||||||
"description": "A private YouTube client",
|
"description": "A private YouTube client",
|
||||||
"version": "0.16.0",
|
"version": "0.17.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"main": "./dist/main.js",
|
"main": "./dist/main.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -30,23 +30,18 @@
|
||||||
"debug-runner": "node _scripts/dev-runner.js --remote-debug",
|
"debug-runner": "node _scripts/dev-runner.js --remote-debug",
|
||||||
"dev": "run-s rebuild:electron dev-runner",
|
"dev": "run-s rebuild:electron dev-runner",
|
||||||
"dev-runner": "node _scripts/dev-runner.js",
|
"dev-runner": "node _scripts/dev-runner.js",
|
||||||
"jest": "jest",
|
"lint-fix": "eslint --fix --ext .js,.vue ./",
|
||||||
"jest:coverage": "jest --collect-coverage",
|
"lint": "eslint --ext .js,.vue ./",
|
||||||
"jest:watch": "jest --watch",
|
|
||||||
"lint-fix": "eslint --fix --ext .js,.ts,.vue ./",
|
|
||||||
"lint": "eslint --ext .js,.ts,.vue ./",
|
|
||||||
"pack": "run-p pack:main pack:renderer pack:workers",
|
"pack": "run-p pack:main pack:renderer pack:workers",
|
||||||
"pack:main": "webpack --mode=production --node-env=production --config _scripts/webpack.main.config.js",
|
"pack:main": "webpack --mode=production --node-env=production --config _scripts/webpack.main.config.js",
|
||||||
"pack:renderer": "webpack --mode=production --node-env=production --config _scripts/webpack.renderer.config.js",
|
"pack:renderer": "webpack --mode=production --node-env=production --config _scripts/webpack.renderer.config.js",
|
||||||
"pack:web": "webpack --mode=production --node-env=production --config _scripts/webpack.web.config.js",
|
"pack:web": "webpack --mode=production --node-env=production --config _scripts/webpack.web.config.js",
|
||||||
"pack:workers": "webpack --mode=production --node-env=production --config _scripts/webpack.workers.config.js",
|
"pack:workers": "webpack --mode=production --node-env=production --config _scripts/webpack.workers.config.js",
|
||||||
"postinstall": "npm run rebuild:electron",
|
"postinstall": "npm run rebuild:electron",
|
||||||
"prettier": "prettier --write \"{src,_scripts}/**/*.{js,ts,vue}\"",
|
"prettier": "prettier --write \"{src,_scripts}/**/*.{js,vue}\"",
|
||||||
"rebuild:electron": "electron-builder install-app-deps",
|
"rebuild:electron": "electron-builder install-app-deps",
|
||||||
"rebuild:node": "npm rebuild",
|
"rebuild:node": "npm rebuild",
|
||||||
"release": "run-s test build",
|
"release": "run-s test build",
|
||||||
"test": "run-s rebuild:node pack:workers jest",
|
|
||||||
"test:watch": "run-s rebuild:node pack:workers jest:watch",
|
|
||||||
"ci": "yarn install --frozen-lockfile"
|
"ci": "yarn install --frozen-lockfile"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -55,60 +50,48 @@
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.2",
|
"@fortawesome/vue-fontawesome": "^2.0.2",
|
||||||
"@freetube/youtube-chat": "^1.1.2",
|
"@freetube/youtube-chat": "^1.1.2",
|
||||||
"@freetube/yt-comment-scraper": "^6.0.0",
|
"@freetube/yt-comment-scraper": "^6.1.0",
|
||||||
"@silvermine/videojs-quality-selector": "^1.2.5",
|
"@silvermine/videojs-quality-selector": "^1.2.5",
|
||||||
"autolinker": "^3.14.3",
|
"autolinker": "^3.15.0",
|
||||||
"bulma-pro": "^0.2.0",
|
"electron-context-menu": "^3.1.2",
|
||||||
"dateformat": "^4.5.1",
|
|
||||||
"electron-context-menu": "^3.1.1",
|
|
||||||
"http-proxy-agent": "^4.0.1",
|
"http-proxy-agent": "^4.0.1",
|
||||||
"https-proxy-agent": "^5.0.0",
|
"https-proxy-agent": "^5.0.0",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.uniqwith": "^4.5.0",
|
"marked": "^4.0.15",
|
||||||
"marked": "^4.0.10",
|
|
||||||
"material-design-icons": "^3.0.1",
|
|
||||||
"nedb-promises": "^5.0.1",
|
"nedb-promises": "^5.0.1",
|
||||||
"node-forge": "^1.0.0",
|
|
||||||
"opml-to-json": "^1.0.1",
|
"opml-to-json": "^1.0.1",
|
||||||
"rss-parser": "^3.12.0",
|
"rss-parser": "^3.12.0",
|
||||||
"socks-proxy-agent": "^6.0.0",
|
"socks-proxy-agent": "^6.0.0",
|
||||||
"video.js": "7.14.3",
|
"video.js": "7.18.1",
|
||||||
"videojs-abloop": "^1.2.0",
|
|
||||||
"videojs-contrib-quality-levels": "^2.1.0",
|
"videojs-contrib-quality-levels": "^2.1.0",
|
||||||
"videojs-http-source-selector": "^1.1.6",
|
"videojs-http-source-selector": "^1.1.6",
|
||||||
"videojs-overlay": "^2.1.4",
|
"videojs-overlay": "^2.1.4",
|
||||||
"videojs-replay": "^1.1.0",
|
|
||||||
"videojs-vtt-thumbnails-freetube": "0.0.15",
|
"videojs-vtt-thumbnails-freetube": "0.0.15",
|
||||||
"vue": "^2.6.14",
|
"vue": "^2.6.14",
|
||||||
"vue-electron": "^1.0.6",
|
|
||||||
"vue-i18n": "^8.25.0",
|
"vue-i18n": "^8.25.0",
|
||||||
"vue-observe-visibility": "^1.0.0",
|
"vue-observe-visibility": "^1.0.0",
|
||||||
"vue-router": "^3.5.2",
|
"vue-router": "^3.5.2",
|
||||||
"vuex": "^3.6.2",
|
"vuex": "^3.6.2",
|
||||||
"youtube-suggest": "^1.1.2",
|
"youtube-suggest": "^1.1.2",
|
||||||
"yt-channel-info": "^2.2.0",
|
"yt-channel-info": "^3.0.4",
|
||||||
"yt-dash-manifest-generator": "1.1.0",
|
"yt-dash-manifest-generator": "1.1.0",
|
||||||
"yt-trending-scraper": "^2.0.1",
|
"yt-trending-scraper": "^2.0.1",
|
||||||
"ytdl-core": "^4.10.1",
|
"ytdl-core": "^4.11.0",
|
||||||
"ytpl": "^2.2.3",
|
"ytpl": "^2.3.0",
|
||||||
"ytsr": "^3.5.3"
|
"ytsr": "^3.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.15.0",
|
"@babel/core": "^7.17.10",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.14.7",
|
"@babel/preset-env": "^7.17.10",
|
||||||
"@babel/preset-env": "^7.15.0",
|
|
||||||
"@babel/preset-typescript": "^7.15.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^4.30.0",
|
|
||||||
"@typescript-eslint/parser": "^4.30.0",
|
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.5",
|
||||||
"copy-webpack-plugin": "^9.0.1",
|
"copy-webpack-plugin": "^9.0.1",
|
||||||
"css-loader": "5.2.6",
|
"css-loader": "5.2.6",
|
||||||
"electron": "^16.0.8",
|
"electron": "^16.2.7",
|
||||||
"electron-builder": "^22.11.7",
|
"electron-builder": "^22.11.7",
|
||||||
"electron-builder-squirrel-windows": "^22.13.1",
|
"electron-builder-squirrel-windows": "^22.13.1",
|
||||||
"electron-debug": "^3.2.0",
|
"electron-debug": "^3.2.0",
|
||||||
|
@ -124,7 +107,6 @@
|
||||||
"fast-glob": "^3.2.7",
|
"fast-glob": "^3.2.7",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"html-webpack-plugin": "^5.3.2",
|
"html-webpack-plugin": "^5.3.2",
|
||||||
"jest": "^27.1.0",
|
|
||||||
"mini-css-extract-plugin": "^2.2.2",
|
"mini-css-extract-plugin": "^2.2.2",
|
||||||
"node-abi": "^2.30.1",
|
"node-abi": "^2.30.1",
|
||||||
"node-loader": "^2.0.0",
|
"node-loader": "^2.0.0",
|
||||||
|
@ -135,7 +117,6 @@
|
||||||
"sass-loader": "^12.1.0",
|
"sass-loader": "^12.1.0",
|
||||||
"style-loader": "^3.2.1",
|
"style-loader": "^3.2.1",
|
||||||
"tree-kill": "1.2.2",
|
"tree-kill": "1.2.2",
|
||||||
"typescript": "^4.4.2",
|
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"vue-devtools": "^5.1.4",
|
"vue-devtools": "^5.1.4",
|
||||||
"vue-eslint-parser": "^7.10.0",
|
"vue-eslint-parser": "^7.10.0",
|
||||||
|
|
|
@ -6,12 +6,14 @@ const IpcChannels = {
|
||||||
GET_SYSTEM_LOCALE: 'get-system-locale',
|
GET_SYSTEM_LOCALE: 'get-system-locale',
|
||||||
GET_USER_DATA_PATH: 'get-user-data-path',
|
GET_USER_DATA_PATH: 'get-user-data-path',
|
||||||
GET_USER_DATA_PATH_SYNC: 'get-user-data-path-sync',
|
GET_USER_DATA_PATH_SYNC: 'get-user-data-path-sync',
|
||||||
|
GET_PICTURES_PATH: 'get-pictures-path',
|
||||||
SHOW_OPEN_DIALOG: 'show-open-dialog',
|
SHOW_OPEN_DIALOG: 'show-open-dialog',
|
||||||
SHOW_SAVE_DIALOG: 'show-save-dialog',
|
SHOW_SAVE_DIALOG: 'show-save-dialog',
|
||||||
STOP_POWER_SAVE_BLOCKER: 'stop-power-save-blocker',
|
STOP_POWER_SAVE_BLOCKER: 'stop-power-save-blocker',
|
||||||
START_POWER_SAVE_BLOCKER: 'start-power-save-blocker',
|
START_POWER_SAVE_BLOCKER: 'start-power-save-blocker',
|
||||||
CREATE_NEW_WINDOW: 'create-new-window',
|
CREATE_NEW_WINDOW: 'create-new-window',
|
||||||
OPEN_IN_EXTERNAL_PLAYER: 'open-in-external-player',
|
OPEN_IN_EXTERNAL_PLAYER: 'open-in-external-player',
|
||||||
|
NATIVE_THEME_UPDATE: 'native-theme-update',
|
||||||
|
|
||||||
DB_SETTINGS: 'db-settings',
|
DB_SETTINGS: 'db-settings',
|
||||||
DB_HISTORY: 'db-history',
|
DB_HISTORY: 'db-history',
|
||||||
|
@ -36,8 +38,7 @@ const DBActions = {
|
||||||
},
|
},
|
||||||
|
|
||||||
HISTORY: {
|
HISTORY: {
|
||||||
UPDATE_WATCH_PROGRESS: 'db-action-history-update-watch-progress',
|
UPDATE_WATCH_PROGRESS: 'db-action-history-update-watch-progress'
|
||||||
SEARCH: 'db-action-history-search'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
PLAYLISTS: {
|
PLAYLISTS: {
|
||||||
|
|
|
@ -27,6 +27,10 @@ class Settings {
|
||||||
return db.settings.findOne({ _id: 'bounds' })
|
return db.settings.findOne({ _id: 'bounds' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static _findTheme() {
|
||||||
|
return db.settings.findOne({ _id: 'baseTheme' })
|
||||||
|
}
|
||||||
|
|
||||||
static _updateBounds(value) {
|
static _updateBounds(value) {
|
||||||
return db.settings.update({ _id: 'bounds' }, { _id: 'bounds', value }, { upsert: true })
|
return db.settings.update({ _id: 'bounds' }, { _id: 'bounds', value }, { upsert: true })
|
||||||
}
|
}
|
||||||
|
@ -38,11 +42,6 @@ class History {
|
||||||
return db.history.find({}).sort({ timeWatched: -1 })
|
return db.history.find({}).sort({ timeWatched: -1 })
|
||||||
}
|
}
|
||||||
|
|
||||||
static search(query) {
|
|
||||||
const re = new RegExp(query, 'i')
|
|
||||||
return db.history.find({ $or: [{ author: { $regex: re } }, { title: { $regex: re } }] }).sort({ timeWatched: -1 })
|
|
||||||
}
|
|
||||||
|
|
||||||
static upsert(record) {
|
static upsert(record) {
|
||||||
return db.history.update({ videoId: record.videoId }, record, { upsert: true })
|
return db.history.update({ videoId: record.videoId }, record, { upsert: true })
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,6 @@ class History {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
static search(query) {
|
|
||||||
return ipcRenderer.invoke(
|
|
||||||
IpcChannels.DB_HISTORY,
|
|
||||||
{ action: DBActions.HISTORY.SEARCH, data: query }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static upsert(record) {
|
static upsert(record) {
|
||||||
return ipcRenderer.invoke(
|
return ipcRenderer.invoke(
|
||||||
IpcChannels.DB_HISTORY,
|
IpcChannels.DB_HISTORY,
|
||||||
|
|
|
@ -25,10 +25,6 @@ class History {
|
||||||
return baseHandlers.history.find()
|
return baseHandlers.history.find()
|
||||||
}
|
}
|
||||||
|
|
||||||
static search(query) {
|
|
||||||
return baseHandlers.history.search(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
static upsert(record) {
|
static upsert(record) {
|
||||||
return baseHandlers.history.upsert(record)
|
return baseHandlers.history.upsert(record)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,4 @@ db.profiles = Datastore.create({ filename: dbPath('profiles'), autoload: true })
|
||||||
db.playlists = Datastore.create({ filename: dbPath('playlists'), autoload: true })
|
db.playlists = Datastore.create({ filename: dbPath('playlists'), autoload: true })
|
||||||
db.history = Datastore.create({ filename: dbPath('history'), autoload: true })
|
db.history = Datastore.create({ filename: dbPath('history'), autoload: true })
|
||||||
|
|
||||||
db.history.ensureIndex({ fieldName: 'author' })
|
|
||||||
db.history.ensureIndex({ fieldName: 'title' })
|
|
||||||
|
|
||||||
export default db
|
export default db
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<% } %>
|
<% } %>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="dark mainRed secBlue">
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- Set `__static` path to static files in production -->
|
<!-- Set `__static` path to static files in production -->
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
app, BrowserWindow, dialog, Menu, ipcMain,
|
app, BrowserWindow, dialog, Menu, ipcMain,
|
||||||
powerSaveBlocker, screen, session, shell
|
powerSaveBlocker, screen, session, shell, nativeTheme
|
||||||
} from 'electron'
|
} from 'electron'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import cp from 'child_process'
|
import cp from 'child_process'
|
||||||
|
@ -25,7 +25,7 @@ function runApp() {
|
||||||
label: 'Show Video Statistics',
|
label: 'Show Video Statistics',
|
||||||
visible: parameters.mediaType === 'video',
|
visible: parameters.mediaType === 'video',
|
||||||
click: () => {
|
click: () => {
|
||||||
browserWindow.webContents.send('showVideoStatistics', 'show')
|
browserWindow.webContents.send('showVideoStatistics')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -35,6 +35,7 @@ function runApp() {
|
||||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
|
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
|
||||||
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
|
||||||
let startupUrl
|
let startupUrl
|
||||||
|
|
||||||
|
@ -146,7 +147,8 @@ function runApp() {
|
||||||
session.defaultSession.cookies.set({
|
session.defaultSession.cookies.set({
|
||||||
url: url,
|
url: url,
|
||||||
name: 'CONSENT',
|
name: 'CONSENT',
|
||||||
value: 'YES+'
|
value: 'YES+',
|
||||||
|
sameSite: 'no_restriction'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -172,11 +174,33 @@ function runApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createWindow({ replaceMainWindow = true, windowStartupUrl = null, showWindowNow = false } = { }) {
|
async function createWindow({ replaceMainWindow = true, windowStartupUrl = null, showWindowNow = false } = { }) {
|
||||||
|
// Syncing new window background to theme choice.
|
||||||
|
const windowBackground = await baseHandlers.settings._findTheme().then(({ value }) => {
|
||||||
|
switch (value) {
|
||||||
|
case 'dark':
|
||||||
|
return '#212121'
|
||||||
|
case 'light':
|
||||||
|
return '#f1f1f1'
|
||||||
|
case 'black':
|
||||||
|
return '#000000'
|
||||||
|
case 'dracula':
|
||||||
|
return '#282a36'
|
||||||
|
case 'system':
|
||||||
|
default:
|
||||||
|
return nativeTheme.shouldUseDarkColors ? '#212121' : '#f1f1f1'
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log(error)
|
||||||
|
// Default to nativeTheme settings if nothing is found.
|
||||||
|
return nativeTheme.shouldUseDarkColors ? '#212121' : '#f1f1f1'
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial window options
|
* Initial window options
|
||||||
*/
|
*/
|
||||||
const commonBrowserWindowOptions = {
|
const commonBrowserWindowOptions = {
|
||||||
backgroundColor: '#212121',
|
backgroundColor: windowBackground,
|
||||||
|
darkTheme: nativeTheme.shouldUseDarkColors,
|
||||||
icon: isDev
|
icon: isDev
|
||||||
? path.join(__dirname, '../../_icons/iconColor.png')
|
? path.join(__dirname, '../../_icons/iconColor.png')
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
|
@ -191,6 +215,7 @@ function runApp() {
|
||||||
contextIsolation: false
|
contextIsolation: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newWindow = new BrowserWindow(
|
const newWindow = new BrowserWindow(
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
|
@ -241,6 +266,7 @@ function runApp() {
|
||||||
height: bounds.height
|
height: bounds.height
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maximized) {
|
if (maximized) {
|
||||||
newWindow.maximize()
|
newWindow.maximize()
|
||||||
}
|
}
|
||||||
|
@ -345,6 +371,14 @@ function runApp() {
|
||||||
app.quit()
|
app.quit()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
nativeTheme.on('updated', () => {
|
||||||
|
const allWindows = BrowserWindow.getAllWindows()
|
||||||
|
|
||||||
|
allWindows.forEach((window) => {
|
||||||
|
window.webContents.send(IpcChannels.NATIVE_THEME_UPDATE, nativeTheme.shouldUseDarkColors)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.on(IpcChannels.ENABLE_PROXY, (_, url) => {
|
ipcMain.on(IpcChannels.ENABLE_PROXY, (_, url) => {
|
||||||
console.log(url)
|
console.log(url)
|
||||||
session.defaultSession.setProxy({
|
session.defaultSession.setProxy({
|
||||||
|
@ -372,11 +406,23 @@ function runApp() {
|
||||||
event.returnValue = app.getPath('userData')
|
event.returnValue = app.getPath('userData')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle(IpcChannels.GET_PICTURES_PATH, () => {
|
||||||
|
return app.getPath('pictures')
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle(IpcChannels.SHOW_OPEN_DIALOG, async (_, options) => {
|
ipcMain.handle(IpcChannels.SHOW_OPEN_DIALOG, async (_, options) => {
|
||||||
return await dialog.showOpenDialog(options)
|
return await dialog.showOpenDialog(options)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle(IpcChannels.SHOW_SAVE_DIALOG, async (_, options) => {
|
ipcMain.handle(IpcChannels.SHOW_SAVE_DIALOG, async (event, { options, useModal }) => {
|
||||||
|
if (useModal) {
|
||||||
|
const senderWindow = BrowserWindow.getAllWindows().find((window) => {
|
||||||
|
return window.webContents.id === event.sender.id
|
||||||
|
})
|
||||||
|
if (senderWindow) {
|
||||||
|
return await dialog.showSaveDialog(senderWindow, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
return await dialog.showSaveDialog(options)
|
return await dialog.showSaveDialog(options)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -388,10 +434,11 @@ function runApp() {
|
||||||
return powerSaveBlocker.start('prevent-display-sleep')
|
return powerSaveBlocker.start('prevent-display-sleep')
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on(IpcChannels.CREATE_NEW_WINDOW, () => {
|
ipcMain.on(IpcChannels.CREATE_NEW_WINDOW, (_e, { windowStartupUrl = null } = { }) => {
|
||||||
createWindow({
|
createWindow({
|
||||||
replaceMainWindow: false,
|
replaceMainWindow: false,
|
||||||
showWindowNow: true
|
showWindowNow: true,
|
||||||
|
windowStartupUrl: windowStartupUrl
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -456,9 +503,6 @@ function runApp() {
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
case DBActions.HISTORY.SEARCH:
|
|
||||||
return await baseHandlers.history.search(data)
|
|
||||||
|
|
||||||
case DBActions.GENERAL.DELETE:
|
case DBActions.GENERAL.DELETE:
|
||||||
await baseHandlers.history.delete(data)
|
await baseHandlers.history.delete(data)
|
||||||
syncOtherWindows(
|
syncOtherWindows(
|
||||||
|
@ -726,7 +770,21 @@ function runApp() {
|
||||||
const template = [
|
const template = [
|
||||||
{
|
{
|
||||||
label: 'File',
|
label: 'File',
|
||||||
submenu: [{ role: 'quit' }]
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'New Window',
|
||||||
|
accelerator: 'CmdOrCtrl+N',
|
||||||
|
click: (_menuItem, _browserWindow, _event) => {
|
||||||
|
createWindow({
|
||||||
|
replaceMainWindow: false,
|
||||||
|
showWindowNow: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
type: 'normal'
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ role: 'quit' }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
|
|
|
@ -3,33 +3,29 @@
|
||||||
src: url(assets/font/Roboto-Regular.ttf);
|
src: url(assets/font/Roboto-Regular.ttf);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
font-family: 'Roboto', sans-serif;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-family: 'Roboto', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.routerView {
|
.routerView {
|
||||||
margin-left: 200px;
|
flex: 1 1 0%;
|
||||||
margin-top: 80px;
|
margin: 18px 10px;
|
||||||
transition-property: margin;
|
|
||||||
transition-duration: 150ms;
|
|
||||||
transition-timing-function: ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand {
|
|
||||||
margin-left: 80px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
width: 85%;
|
width: 85%;
|
||||||
|
margin: 20px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-wrapper {
|
||||||
|
margin: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flexBox {
|
.flexBox {
|
||||||
margin-top: 60px;
|
display: block;
|
||||||
margin-bottom: -75px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#changeLogText {
|
#changeLogText {
|
||||||
|
@ -46,13 +42,13 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 680px) {
|
@media only screen and (max-width: 680px) {
|
||||||
.expand, .routerView {
|
.routerView {
|
||||||
margin-left: 0px;
|
margin: 68px 8px 68px;
|
||||||
margin-bottom: 80px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
width: 90%;
|
width: 80%;
|
||||||
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flexBox {
|
.flexBox {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue'
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import Parser from 'rss-parser'
|
import Parser from 'rss-parser'
|
||||||
|
import { IpcChannels } from '../constants'
|
||||||
|
|
||||||
let ipcRenderer = null
|
let ipcRenderer = null
|
||||||
|
|
||||||
|
@ -101,6 +102,22 @@ export default Vue.extend({
|
||||||
return this.$store.getters.getDefaultInvidiousInstance
|
return this.$store.getters.getDefaultInvidiousInstance
|
||||||
},
|
},
|
||||||
|
|
||||||
|
baseTheme: function () {
|
||||||
|
return this.$store.getters.getBaseTheme
|
||||||
|
},
|
||||||
|
|
||||||
|
mainColor: function () {
|
||||||
|
return this.$store.getters.getMainColor
|
||||||
|
},
|
||||||
|
|
||||||
|
secColor: function () {
|
||||||
|
return this.$store.getters.getSecColor
|
||||||
|
},
|
||||||
|
|
||||||
|
systemTheme: function () {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
},
|
||||||
|
|
||||||
externalLinkOpeningPromptNames: function () {
|
externalLinkOpeningPromptNames: function () {
|
||||||
return [
|
return [
|
||||||
this.$t('Yes'),
|
this.$t('Yes'),
|
||||||
|
@ -114,6 +131,13 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
windowTitle: 'setWindowTitle',
|
windowTitle: 'setWindowTitle',
|
||||||
|
|
||||||
|
baseTheme: 'checkThemeSettings',
|
||||||
|
|
||||||
|
mainColor: 'checkThemeSettings',
|
||||||
|
|
||||||
|
secColor: 'checkThemeSettings',
|
||||||
|
|
||||||
$route () {
|
$route () {
|
||||||
// react to route changes...
|
// react to route changes...
|
||||||
// Hide top nav filter panel on page change
|
// Hide top nav filter panel on page change
|
||||||
|
@ -126,6 +150,8 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
this.grabUserSettings().then(async () => {
|
this.grabUserSettings().then(async () => {
|
||||||
|
this.checkThemeSettings()
|
||||||
|
|
||||||
await this.fetchInvidiousInstances({ isDev: this.isDev })
|
await this.fetchInvidiousInstances({ isDev: this.isDev })
|
||||||
if (this.defaultInvidiousInstance === '') {
|
if (this.defaultInvidiousInstance === '') {
|
||||||
await this.setRandomCurrentInvidiousInstance()
|
await this.setRandomCurrentInvidiousInstance()
|
||||||
|
@ -142,6 +168,7 @@ export default Vue.extend({
|
||||||
this.activateKeyboardShortcuts()
|
this.activateKeyboardShortcuts()
|
||||||
this.openAllLinksExternally()
|
this.openAllLinksExternally()
|
||||||
this.enableOpenUrl()
|
this.enableOpenUrl()
|
||||||
|
this.watchSystemTheme()
|
||||||
await this.checkExternalPlayer()
|
await this.checkExternalPlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,45 +187,27 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
checkThemeSettings: function () {
|
checkThemeSettings: function () {
|
||||||
let baseTheme = localStorage.getItem('baseTheme')
|
|
||||||
let mainColor = localStorage.getItem('mainColor')
|
|
||||||
let secColor = localStorage.getItem('secColor')
|
|
||||||
|
|
||||||
if (baseTheme === null) {
|
|
||||||
baseTheme = 'dark'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mainColor === null) {
|
|
||||||
mainColor = 'mainRed'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secColor === null) {
|
|
||||||
secColor = 'secBlue'
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
baseTheme: baseTheme,
|
baseTheme: this.baseTheme || 'dark',
|
||||||
mainColor: mainColor,
|
mainColor: this.mainColor || 'mainRed',
|
||||||
secColor: secColor
|
secColor: this.secColor || 'secBlue'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateTheme(theme)
|
this.updateTheme(theme)
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTheme: function (theme) {
|
updateTheme: function (theme) {
|
||||||
console.log(theme)
|
console.group('updateTheme')
|
||||||
const className = `${theme.baseTheme} ${theme.mainColor} ${theme.secColor}`
|
console.log('Theme: ', theme)
|
||||||
const body = document.getElementsByTagName('body')[0]
|
document.body.className = `${theme.baseTheme} main${theme.mainColor} sec${theme.secColor}`
|
||||||
body.className = className
|
document.body.dataset.systemTheme = this.systemTheme
|
||||||
localStorage.setItem('baseTheme', theme.baseTheme)
|
console.groupEnd()
|
||||||
localStorage.setItem('mainColor', theme.mainColor)
|
|
||||||
localStorage.setItem('secColor', theme.secColor)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
checkForNewUpdates: function () {
|
checkForNewUpdates: function () {
|
||||||
if (this.checkForUpdates) {
|
if (this.checkForUpdates) {
|
||||||
const { version } = require('../../package.json')
|
const { version } = require('../../package.json')
|
||||||
const requestUrl = 'https://api.github.com/repos/freetubeapp/freetube/releases'
|
const requestUrl = 'https://api.github.com/repos/freetubeapp/freetube/releases?per_page=1'
|
||||||
|
|
||||||
$.getJSON(requestUrl, (response) => {
|
$.getJSON(requestUrl, (response) => {
|
||||||
const tagName = response[0].tag_name
|
const tagName = response[0].tag_name
|
||||||
|
@ -301,12 +310,21 @@ export default Vue.extend({
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
this.$refs.topNav.historyBack()
|
this.$refs.topNav.historyBack()
|
||||||
break
|
break
|
||||||
|
case 'KeyD':
|
||||||
|
this.$refs.topNav.focusSearch()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (event.code) {
|
switch (event.code) {
|
||||||
case 'Tab':
|
case 'Tab':
|
||||||
this.hideOutlines = false
|
this.hideOutlines = false
|
||||||
break
|
break
|
||||||
|
case 'KeyL':
|
||||||
|
if ((process.platform !== 'darwin' && event.ctrlKey) ||
|
||||||
|
(process.platform === 'darwin' && event.metaKey)) {
|
||||||
|
this.$refs.topNav.focusSearch()
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -316,14 +334,17 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
|
|
||||||
$(document).on('auxclick', 'a[href^="http"]', (event) => {
|
$(document).on('auxclick', 'a[href^="http"]', (event) => {
|
||||||
this.handleLinkClick(event)
|
// auxclick fires for all clicks not performed with the primary button
|
||||||
|
// only handle the link click if it was the middle button,
|
||||||
|
// otherwise the context menu breaks
|
||||||
|
if (event.button === 1) {
|
||||||
|
this.handleLinkClick(event)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
handleLinkClick: function (event) {
|
handleLinkClick: function (event) {
|
||||||
const el = event.currentTarget
|
const el = event.currentTarget
|
||||||
console.log(this.usingElectron)
|
|
||||||
console.log(el)
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
// Check if it's a YouTube link
|
// Check if it's a YouTube link
|
||||||
|
@ -331,7 +352,11 @@ export default Vue.extend({
|
||||||
const isYoutubeLink = youtubeUrlPattern.test(el.href)
|
const isYoutubeLink = youtubeUrlPattern.test(el.href)
|
||||||
|
|
||||||
if (isYoutubeLink) {
|
if (isYoutubeLink) {
|
||||||
this.handleYoutubeLink(el.href)
|
// `auxclick` is the event type for non-left click
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event
|
||||||
|
this.handleYoutubeLink(el.href, {
|
||||||
|
doCreateNewWindow: event.type === 'auxclick'
|
||||||
|
})
|
||||||
} else if (this.externalLinkHandling === 'doNothing') {
|
} else if (this.externalLinkHandling === 'doNothing') {
|
||||||
// Let user know opening external link is disabled via setting
|
// Let user know opening external link is disabled via setting
|
||||||
this.showToast({
|
this.showToast({
|
||||||
|
@ -348,7 +373,7 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleYoutubeLink: function (href) {
|
handleYoutubeLink: function (href, { doCreateNewWindow = false } = { }) {
|
||||||
this.getYoutubeUrlInfo(href).then((result) => {
|
this.getYoutubeUrlInfo(href).then((result) => {
|
||||||
switch (result.urlType) {
|
switch (result.urlType) {
|
||||||
case 'video': {
|
case 'video': {
|
||||||
|
@ -361,9 +386,11 @@ export default Vue.extend({
|
||||||
if (playlistId && playlistId.length > 0) {
|
if (playlistId && playlistId.length > 0) {
|
||||||
query.playlistId = playlistId
|
query.playlistId = playlistId
|
||||||
}
|
}
|
||||||
this.$router.push({
|
const path = `/watch/${videoId}`
|
||||||
path: `/watch/${videoId}`,
|
this.openInternalPath({
|
||||||
query: query
|
path,
|
||||||
|
query,
|
||||||
|
doCreateNewWindow
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -371,9 +398,11 @@ export default Vue.extend({
|
||||||
case 'playlist': {
|
case 'playlist': {
|
||||||
const { playlistId, query } = result
|
const { playlistId, query } = result
|
||||||
|
|
||||||
this.$router.push({
|
const path = `/playlist/${playlistId}`
|
||||||
path: `/playlist/${playlistId}`,
|
this.openInternalPath({
|
||||||
query
|
path,
|
||||||
|
query,
|
||||||
|
doCreateNewWindow
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -381,9 +410,11 @@ export default Vue.extend({
|
||||||
case 'search': {
|
case 'search': {
|
||||||
const { searchQuery, query } = result
|
const { searchQuery, query } = result
|
||||||
|
|
||||||
this.$router.push({
|
const path = `/search/${encodeURIComponent(searchQuery)}`
|
||||||
path: `/search/${encodeURIComponent(searchQuery)}`,
|
this.openInternalPath({
|
||||||
query
|
path,
|
||||||
|
query,
|
||||||
|
doCreateNewWindow
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -404,8 +435,10 @@ export default Vue.extend({
|
||||||
case 'channel': {
|
case 'channel': {
|
||||||
const { channelId, subPath } = result
|
const { channelId, subPath } = result
|
||||||
|
|
||||||
this.$router.push({
|
const path = `/channel/${channelId}/${subPath}`
|
||||||
path: `/channel/${channelId}/${subPath}`
|
this.openInternalPath({
|
||||||
|
path,
|
||||||
|
doCreateNewWindow
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -430,6 +463,37 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linux fix for dynamically updating theme preference, this works on
|
||||||
|
* all systems running the electron app.
|
||||||
|
*/
|
||||||
|
watchSystemTheme: function () {
|
||||||
|
ipcRenderer.on(IpcChannels.NATIVE_THEME_UPDATE, (event, shouldUseDarkColors) => {
|
||||||
|
document.body.dataset.systemTheme = shouldUseDarkColors ? 'dark' : 'light'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
openInternalPath: function({ path, doCreateNewWindow, query = {} }) {
|
||||||
|
if (this.usingElectron && doCreateNewWindow) {
|
||||||
|
const { ipcRenderer } = require('electron')
|
||||||
|
|
||||||
|
// Combine current document path and new "hash" as new window startup URL
|
||||||
|
const newWindowStartupURL = [
|
||||||
|
window.location.href.split('#')[0],
|
||||||
|
`#${path}?${(new URLSearchParams(query)).toString()}`
|
||||||
|
].join('')
|
||||||
|
ipcRenderer.send(IpcChannels.CREATE_NEW_WINDOW, {
|
||||||
|
windowStartupUrl: newWindowStartupURL
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Web
|
||||||
|
this.$router.push({
|
||||||
|
path,
|
||||||
|
query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
enableOpenUrl: function () {
|
enableOpenUrl: function () {
|
||||||
ipcRenderer.on('openUrl', (event, url) => {
|
ipcRenderer.on('openUrl', (event, url) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
|
@ -473,7 +537,10 @@ export default Vue.extend({
|
||||||
'getExternalPlayerCmdArgumentsData',
|
'getExternalPlayerCmdArgumentsData',
|
||||||
'fetchInvidiousInstances',
|
'fetchInvidiousInstances',
|
||||||
'setRandomCurrentInvidiousInstance',
|
'setRandomCurrentInvidiousInstance',
|
||||||
'setupListenersToSyncWindows'
|
'setupListenersToSyncWindows',
|
||||||
|
'updateBaseTheme',
|
||||||
|
'updateMainColor',
|
||||||
|
'updateSecColor'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
v-if="dataReady"
|
||||||
id="app"
|
id="app"
|
||||||
:class="{
|
:class="{
|
||||||
hideOutlines: hideOutlines,
|
hideOutlines: hideOutlines,
|
||||||
|
@ -9,36 +10,39 @@
|
||||||
<top-nav ref="topNav" />
|
<top-nav ref="topNav" />
|
||||||
<side-nav ref="sideNav" />
|
<side-nav ref="sideNav" />
|
||||||
<ft-flex-box
|
<ft-flex-box
|
||||||
v-if="showUpdatesBanner || showBlogBanner"
|
|
||||||
class="flexBox routerView"
|
class="flexBox routerView"
|
||||||
:class="{ expand: !isOpen }"
|
|
||||||
>
|
>
|
||||||
<ft-notification-banner
|
<div
|
||||||
v-if="showUpdatesBanner"
|
v-if="showUpdatesBanner || showBlogBanner"
|
||||||
class="banner"
|
class="banner-wrapper"
|
||||||
:message="updateBannerMessage"
|
>
|
||||||
@click="handleUpdateBannerClick"
|
<ft-notification-banner
|
||||||
/>
|
v-if="showUpdatesBanner"
|
||||||
<ft-notification-banner
|
class="banner"
|
||||||
v-if="showBlogBanner"
|
:message="updateBannerMessage"
|
||||||
class="banner"
|
@click="handleUpdateBannerClick"
|
||||||
:message="blogBannerMessage"
|
/>
|
||||||
@click="handleNewBlogBannerClick"
|
<ft-notification-banner
|
||||||
/>
|
v-if="showBlogBanner"
|
||||||
</ft-flex-box>
|
class="banner"
|
||||||
<transition
|
:message="blogBannerMessage"
|
||||||
v-if="dataReady"
|
@click="handleNewBlogBannerClick"
|
||||||
mode="out-in"
|
/>
|
||||||
name="fade"
|
</div>
|
||||||
>
|
<transition
|
||||||
<!-- <keep-alive> -->
|
v-if="dataReady"
|
||||||
<RouterView
|
mode="out-in"
|
||||||
ref="router"
|
name="fade"
|
||||||
class="routerView"
|
>
|
||||||
:class="{ expand: !isOpen }"
|
<!-- <keep-alive> -->
|
||||||
/>
|
<RouterView
|
||||||
|
ref="router"
|
||||||
|
class="routerView"
|
||||||
|
/>
|
||||||
<!-- </keep-alive> -->
|
<!-- </keep-alive> -->
|
||||||
</transition>
|
</transition>
|
||||||
|
</ft-flex-box>
|
||||||
|
|
||||||
<ft-prompt
|
<ft-prompt
|
||||||
v-if="showReleaseNotes"
|
v-if="showReleaseNotes"
|
||||||
@click="showReleaseNotes = !showReleaseNotes"
|
@click="showReleaseNotes = !showReleaseNotes"
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="26" height="26" viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="#ffffff"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M5 7h1a2 2 0 0 0 2 -2a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1a2 2 0 0 0 2 2h1a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-9a2 2 0 0 1 2 -2" />
|
||||||
|
<circle cx="12" cy="13" r="3" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 443 B |
|
@ -248,7 +248,9 @@ export default Vue.extend({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const textDecode = new TextDecoder('utf-8').decode(data)
|
const textDecode = new TextDecoder('utf-8').decode(data)
|
||||||
const youtubeSubscriptions = textDecode.split('\n')
|
const youtubeSubscriptions = textDecode.split('\n').filter(sub => {
|
||||||
|
return sub !== ''
|
||||||
|
})
|
||||||
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
const primaryProfile = JSON.parse(JSON.stringify(this.profileList[0]))
|
||||||
const subscriptions = []
|
const subscriptions = []
|
||||||
|
|
||||||
|
@ -681,7 +683,7 @@ export default Vue.extend({
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.showSaveDialog(options)
|
const response = await this.showSaveDialog({ options })
|
||||||
if (response.canceled || response.filePath === '') {
|
if (response.canceled || response.filePath === '') {
|
||||||
// User canceled the save dialog
|
// User canceled the save dialog
|
||||||
return
|
return
|
||||||
|
@ -764,7 +766,7 @@ export default Vue.extend({
|
||||||
return object
|
return object
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await this.showSaveDialog(options)
|
const response = await this.showSaveDialog({ options })
|
||||||
if (response.canceled || response.filePath === '') {
|
if (response.canceled || response.filePath === '') {
|
||||||
// User canceled the save dialog
|
// User canceled the save dialog
|
||||||
return
|
return
|
||||||
|
@ -816,7 +818,7 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await this.showSaveDialog(options)
|
const response = await this.showSaveDialog({ options })
|
||||||
if (response.canceled || response.filePath === '') {
|
if (response.canceled || response.filePath === '') {
|
||||||
// User canceled the save dialog
|
// User canceled the save dialog
|
||||||
return
|
return
|
||||||
|
@ -855,10 +857,14 @@ export default Vue.extend({
|
||||||
let exportText = 'Channel ID,Channel URL,Channel title\n'
|
let exportText = 'Channel ID,Channel URL,Channel title\n'
|
||||||
this.profileList[0].subscriptions.forEach((channel) => {
|
this.profileList[0].subscriptions.forEach((channel) => {
|
||||||
const channelUrl = `https://www.youtube.com/channel/${channel.id}`
|
const channelUrl = `https://www.youtube.com/channel/${channel.id}`
|
||||||
exportText += `${channel.id},${channelUrl},${channel.name}\n`
|
let channelName = channel.name
|
||||||
|
if (channelName.search(',') !== -1) { // add quotations if channel has comma in name
|
||||||
|
channelName = `"${channelName}"`
|
||||||
|
}
|
||||||
|
exportText += `${channel.id},${channelUrl},${channelName}\n`
|
||||||
})
|
})
|
||||||
exportText += '\n'
|
exportText += '\n'
|
||||||
const response = await this.showSaveDialog(options)
|
const response = await this.showSaveDialog({ options })
|
||||||
if (response.canceled || response.filePath === '') {
|
if (response.canceled || response.filePath === '') {
|
||||||
// User canceled the save dialog
|
// User canceled the save dialog
|
||||||
return
|
return
|
||||||
|
@ -911,7 +917,7 @@ export default Vue.extend({
|
||||||
newPipeObject.subscriptions.push(subscription)
|
newPipeObject.subscriptions.push(subscription)
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await this.showSaveDialog(options)
|
const response = await this.showSaveDialog({ options })
|
||||||
if (response.canceled || response.filePath === '') {
|
if (response.canceled || response.filePath === '') {
|
||||||
// User canceled the save dialog
|
// User canceled the save dialog
|
||||||
return
|
return
|
||||||
|
@ -1042,7 +1048,7 @@ export default Vue.extend({
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.showSaveDialog(options)
|
const response = await this.showSaveDialog({ options })
|
||||||
if (response.canceled || response.filePath === '') {
|
if (response.canceled || response.filePath === '') {
|
||||||
// User canceled the save dialog
|
// User canceled the save dialog
|
||||||
return
|
return
|
||||||
|
@ -1214,7 +1220,7 @@ export default Vue.extend({
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.showSaveDialog(options)
|
const response = await this.showSaveDialog({ options })
|
||||||
if (response.canceled || response.filePath === '') {
|
if (response.canceled || response.filePath === '') {
|
||||||
// User canceled the save dialog
|
// User canceled the save dialog
|
||||||
return
|
return
|
||||||
|
@ -1303,7 +1309,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
getChannelInfoLocal: function (channelId) {
|
getChannelInfoLocal: function (channelId) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ytch.getChannelInfo(channelId, 'latest').then(async (response) => {
|
ytch.getChannelInfo({ channelId: channelId }).then(async (response) => {
|
||||||
resolve(response)
|
resolve(response)
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||||
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
||||||
|
import FtSelect from '../ft-select/ft-select.vue'
|
||||||
import FtButton from '../ft-button/ft-button.vue'
|
import FtButton from '../ft-button/ft-button.vue'
|
||||||
import FtInput from '../ft-input/ft-input.vue'
|
import FtInput from '../ft-input/ft-input.vue'
|
||||||
import { mapActions } from 'vuex'
|
import { mapActions } from 'vuex'
|
||||||
|
@ -12,19 +13,36 @@ export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
'ft-toggle-switch': FtToggleSwitch,
|
'ft-toggle-switch': FtToggleSwitch,
|
||||||
'ft-flex-box': FtFlexBox,
|
'ft-flex-box': FtFlexBox,
|
||||||
|
'ft-select': FtSelect,
|
||||||
'ft-button': FtButton,
|
'ft-button': FtButton,
|
||||||
'ft-input': FtInput
|
'ft-input': FtInput
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
askForDownloadPath: this.$store.getters.getDownloadFolderPath === ''
|
askForDownloadPath: false,
|
||||||
|
downloadBehaviorValues: [
|
||||||
|
'download',
|
||||||
|
'open'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
downloadPath: function() {
|
downloadPath: function() {
|
||||||
return this.$store.getters.getDownloadFolderPath
|
return this.$store.getters.getDownloadFolderPath
|
||||||
|
},
|
||||||
|
downloadBehaviorNames: function () {
|
||||||
|
return [
|
||||||
|
this.$t('Settings.Download Settings.Download in app'),
|
||||||
|
this.$t('Settings.Download Settings.Open in web browser')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
downloadBehavior: function () {
|
||||||
|
return this.$store.getters.getDownloadBehavior
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted: function () {
|
||||||
|
this.askForDownloadPath = this.downloadPath === ''
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleDownloadingSettingChange: function (value) {
|
handleDownloadingSettingChange: function (value) {
|
||||||
this.askForDownloadPath = value
|
this.askForDownloadPath = value
|
||||||
|
@ -42,7 +60,8 @@ export default Vue.extend({
|
||||||
this.updateDownloadFolderPath(folder.filePaths[0])
|
this.updateDownloadFolderPath(folder.filePaths[0])
|
||||||
},
|
},
|
||||||
...mapActions([
|
...mapActions([
|
||||||
'updateDownloadFolderPath'
|
'updateDownloadFolderPath',
|
||||||
|
'updateDownloadBehavior'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,19 @@
|
||||||
</h3>
|
</h3>
|
||||||
</summary>
|
</summary>
|
||||||
<hr>
|
<hr>
|
||||||
<ft-flex-box class="downloadSettingsFlexBox">
|
<ft-flex-box>
|
||||||
|
<ft-select
|
||||||
|
:placeholder="$t('Settings.Download Settings.Download Behavior')"
|
||||||
|
:value="downloadBehavior"
|
||||||
|
:select-names="downloadBehaviorNames"
|
||||||
|
:select-values="downloadBehaviorValues"
|
||||||
|
@change="updateDownloadBehavior"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
<ft-flex-box
|
||||||
|
v-if="downloadBehavior === 'download'"
|
||||||
|
class="downloadSettingsFlexBox"
|
||||||
|
>
|
||||||
<ft-toggle-switch
|
<ft-toggle-switch
|
||||||
:label="$t('Settings.Download Settings.Ask Download Path')"
|
:label="$t('Settings.Download Settings.Ask Download Path')"
|
||||||
:default-value="askForDownloadPath"
|
:default-value="askForDownloadPath"
|
||||||
|
@ -14,7 +26,7 @@
|
||||||
/>
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
<ft-flex-box
|
<ft-flex-box
|
||||||
v-if="!askForDownloadPath"
|
v-if="!askForDownloadPath && downloadBehavior === 'download'"
|
||||||
>
|
>
|
||||||
<ft-input
|
<ft-input
|
||||||
class="folderDisplay"
|
class="folderDisplay"
|
||||||
|
@ -25,7 +37,7 @@
|
||||||
/>
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
<ft-flex-box
|
<ft-flex-box
|
||||||
v-if="!askForDownloadPath"
|
v-if="!askForDownloadPath && downloadBehavior === 'download'"
|
||||||
>
|
>
|
||||||
<ft-button
|
<ft-button
|
||||||
:label="$t('Settings.Download Settings.Choose Path')"
|
:label="$t('Settings.Download Settings.Choose Path')"
|
||||||
|
|
|
@ -16,11 +16,6 @@ export default Vue.extend({
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
test: 'hello'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
listType: function () {
|
listType: function () {
|
||||||
return this.$store.getters.getListType
|
return this.$store.getters.getListType
|
||||||
|
|
|
@ -44,11 +44,11 @@ export default Vue.extend({
|
||||||
type: String,
|
type: String,
|
||||||
default: 'bottom'
|
default: 'bottom'
|
||||||
},
|
},
|
||||||
dropdownNames: {
|
dropdownOptions: {
|
||||||
type: Array,
|
// Array of objects with these properties
|
||||||
default: () => { return [] }
|
// - type: ('labelValue'|'divider', default to 'labelValue' for less typing)
|
||||||
},
|
// - label: String (if type == 'labelValue')
|
||||||
dropdownValues: {
|
// - value: String (if type == 'labelValue')
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => { return [] }
|
default: () => { return [] }
|
||||||
}
|
}
|
||||||
|
@ -107,18 +107,18 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleIconClick: function () {
|
handleIconClick: function () {
|
||||||
if (this.forceDropdown || (this.dropdownNames.length > 0 && this.dropdownValues.length > 0)) {
|
if (this.forceDropdown || (this.dropdownOptions.length > 0)) {
|
||||||
this.toggleDropdown()
|
this.toggleDropdown()
|
||||||
} else {
|
} else {
|
||||||
this.$emit('click')
|
this.$emit('click')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDropdownClick: function (index) {
|
handleDropdownClick: function ({ url, index }) {
|
||||||
if (this.returnIndex) {
|
if (this.returnIndex) {
|
||||||
this.$emit('click', index)
|
this.$emit('click', index)
|
||||||
} else {
|
} else {
|
||||||
this.$emit('click', this.dropdownValues[index])
|
this.$emit('click', url)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.focusOut()
|
this.focusOut()
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
list-style-type: none
|
list-style-type: none
|
||||||
|
|
||||||
.listItem
|
.listItem
|
||||||
padding: 10px
|
padding: 8px 10px
|
||||||
margin: 0
|
margin: 0
|
||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
@ -96,3 +96,11 @@
|
||||||
&:active
|
&:active
|
||||||
background-color: var(--side-nav-active-color)
|
background-color: var(--side-nav-active-color)
|
||||||
transition: background 0.1s ease-in
|
transition: background 0.1s ease-in
|
||||||
|
|
||||||
|
.listItemDivider
|
||||||
|
width: 95%
|
||||||
|
margin: 1px auto
|
||||||
|
border-top: 1px solid var(--tertiary-text-color)
|
||||||
|
// Too "visible" with current color
|
||||||
|
opacity: 50%
|
||||||
|
|
||||||
|
|
|
@ -28,16 +28,16 @@
|
||||||
>
|
>
|
||||||
<slot>
|
<slot>
|
||||||
<ul
|
<ul
|
||||||
v-if="dropdownNames.length > 0"
|
v-if="dropdownOptions.length > 0"
|
||||||
class="list"
|
class="list"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
v-for="(label, index) in dropdownNames"
|
v-for="(option, index) in dropdownOptions"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="listItem"
|
:class="option.type === 'divider' ? 'listItemDivider' : 'listItem'"
|
||||||
@click="handleDropdownClick(index)"
|
@click="handleDropdownClick({url: option.value, index: index})"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ option.type === 'divider' ? '' : option.label }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
|
@ -2,6 +2,35 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ft-input-component.search.showClearTextButton {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ft-input-component.search.clearTextButtonVisible,
|
||||||
|
.ft-input-component.search.showClearTextButton:focus-within {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ft-input-component.showClearTextButton:not(.search) .ft-input {
|
||||||
|
padding-left: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main search input */
|
||||||
|
.clearTextButtonVisible.search .ft-input,
|
||||||
|
.ft-input-component.search.showClearTextButton:focus-within .ft-input {
|
||||||
|
padding-left: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ft-input-component:focus-within .clearInputTextButton {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearTextButtonVisible .clearInputTextButton.visible,
|
||||||
|
.ft-input-component:focus-within .clearInputTextButton.visible {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.disabled label, .disabled .ft-input{
|
.disabled label, .disabled .ft-input{
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
@ -9,33 +38,27 @@
|
||||||
|
|
||||||
.clearInputTextButton {
|
.clearInputTextButton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
/* horizontal intentionally reduced to keep "I-beam pointer" visible */
|
margin: 0 3px;
|
||||||
padding: 10px 8px;
|
padding: 10px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
left: 0;
|
left: 0;
|
||||||
cursor: pointer;
|
border-radius: 100%;
|
||||||
border-radius: 200px 200px 200px 200px;
|
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
-moz-transition: background 0.2s ease-in;
|
||||||
-moz-transition: background 0.2s ease-in, opacity 0.2s ease-in;
|
-o-transition: background 0.2s ease-in;
|
||||||
-o-transition: background 0.2s ease-in, opacity 0.2s ease-in;
|
transition: background 0.2s ease-in;
|
||||||
transition: background 0.2s ease-in, opacity 0.2s ease-in;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.clearInputTextButton:hover {
|
.clearInputTextButton.visible:hover {
|
||||||
background-color: var(--side-nav-hover-color);
|
background-color: var(--side-nav-hover-color);
|
||||||
}
|
}
|
||||||
.clearInputTextButton.visible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.forceTextColor .clearInputTextButton:hover {
|
.forceTextColor .clearInputTextButton:hover {
|
||||||
background-color: var(--primary-color-hover);
|
background-color: var(--primary-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.clearInputTextButton:active {
|
.clearInputTextButton.visible:active {
|
||||||
background-color: var(--tertiary-text-color);
|
background-color: var(--tertiary-text-color);
|
||||||
-moz-transition: background 0.2s ease-in;
|
-moz-transition: background 0.2s ease-in;
|
||||||
-o-transition: background 0.2s ease-in;
|
-o-transition: background 0.2s ease-in;
|
||||||
|
@ -55,20 +78,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ft-input {
|
.ft-input {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
outline: none;
|
outline: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: var(--search-bar-color);
|
background-color: var(--search-bar-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ft-input-component ::-webkit-input-placeholder {
|
.ft-input-component ::-webkit-input-placeholder {
|
||||||
|
@ -93,10 +116,11 @@
|
||||||
|
|
||||||
.inputAction {
|
.inputAction {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 10px 8px;
|
margin: 0 3px;
|
||||||
|
padding: 10px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-radius: 200px 200px 200px 200px;
|
border-radius: 100%;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
/* this should look disabled by default */
|
/* this should look disabled by default */
|
||||||
opacity: 50%;
|
opacity: 50%;
|
||||||
|
@ -125,7 +149,7 @@
|
||||||
With arrow present means
|
With arrow present means
|
||||||
the text might get under the arrow with normal padding
|
the text might get under the arrow with normal padding
|
||||||
*/
|
*/
|
||||||
padding-right: 2em;
|
padding-right: calc(36px + 6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputAction.enabled:hover {
|
.inputAction.enabled:hover {
|
||||||
|
@ -173,7 +197,3 @@
|
||||||
background-color: var(--scrollbar-color-hover);
|
background-color: var(--scrollbar-color-hover);
|
||||||
/* color: white; */
|
/* color: white; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.showClearTextButton .ft-input {
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
|
|
|
@ -87,28 +87,6 @@ export default Vue.extend({
|
||||||
return this.inputData.length > 0
|
return this.inputData.length > 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
inputDataPresent: function (newVal, oldVal) {
|
|
||||||
if (newVal) {
|
|
||||||
// The button needs to be visible **immediately**
|
|
||||||
// To allow user to see the transition
|
|
||||||
this.clearTextButtonExisting = true
|
|
||||||
// The transition is not rendered if this property is set right after
|
|
||||||
// It's visible
|
|
||||||
setTimeout(() => {
|
|
||||||
this.clearTextButtonVisible = true
|
|
||||||
}, 0)
|
|
||||||
} else {
|
|
||||||
// Hide the button with transition
|
|
||||||
this.clearTextButtonVisible = false
|
|
||||||
// Remove the button after the transition
|
|
||||||
// 0.2s in CSS = 200ms in JS
|
|
||||||
setTimeout(() => {
|
|
||||||
this.clearTextButtonExisting = false
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
this.id = this._uid
|
this.id = this._uid
|
||||||
this.inputData = this.value
|
this.inputData = this.value
|
||||||
|
@ -136,14 +114,20 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleClearTextClick: function () {
|
handleClearTextClick: function () {
|
||||||
|
// No action if no input text
|
||||||
|
if (!this.inputDataPresent) { return }
|
||||||
|
|
||||||
this.inputData = ''
|
this.inputData = ''
|
||||||
this.handleActionIconChange()
|
this.handleActionIconChange()
|
||||||
this.updateVisibleDataList()
|
this.updateVisibleDataList()
|
||||||
this.$emit('input', this.inputData)
|
|
||||||
|
const inputElement = document.getElementById(this.id)
|
||||||
|
inputElement.value = ''
|
||||||
|
|
||||||
// Focus on input element after text is clear for better UX
|
// Focus on input element after text is clear for better UX
|
||||||
const inputElement = document.getElementById(this.id)
|
|
||||||
inputElement.focus()
|
inputElement.focus()
|
||||||
|
|
||||||
|
this.$emit('clear')
|
||||||
},
|
},
|
||||||
|
|
||||||
handleActionIconChange: function() {
|
handleActionIconChange: function() {
|
||||||
|
@ -200,7 +184,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
if (inputElement !== null) {
|
if (inputElement !== null) {
|
||||||
inputElement.addEventListener('keydown', (event) => {
|
inputElement.addEventListener('keydown', (event) => {
|
||||||
if (event.keyCode === 13) {
|
if (event.key === 'Enter') {
|
||||||
this.handleClick()
|
this.handleClick()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -214,14 +198,14 @@ export default Vue.extend({
|
||||||
this.handleClick()
|
this.handleClick()
|
||||||
},
|
},
|
||||||
|
|
||||||
handleKeyDown: function (keyCode) {
|
handleKeyDown: function (event) {
|
||||||
if (this.dataList.length === 0) { return }
|
if (this.visibleDataList.length === 0) { return }
|
||||||
// Update selectedOption based on arrow key pressed
|
// Update selectedOption based on arrow key pressed
|
||||||
if (keyCode === 40) {
|
if (event.key === 'ArrowDown') {
|
||||||
this.searchState.selectedOption = (this.searchState.selectedOption + 1) % this.dataList.length
|
this.searchState.selectedOption = (this.searchState.selectedOption + 1) % this.visibleDataList.length
|
||||||
} else if (keyCode === 38) {
|
} else if (event.key === 'ArrowUp') {
|
||||||
if (this.searchState.selectedOption === -1) {
|
if (this.searchState.selectedOption < 1) {
|
||||||
this.searchState.selectedOption = this.dataList.length - 1
|
this.searchState.selectedOption = this.visibleDataList.length - 1
|
||||||
} else {
|
} else {
|
||||||
this.searchState.selectedOption--
|
this.searchState.selectedOption--
|
||||||
}
|
}
|
||||||
|
@ -230,14 +214,13 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key pressed isn't enter
|
// Key pressed isn't enter
|
||||||
if (keyCode !== 13) {
|
if (event.key !== 'Enter') {
|
||||||
this.searchState.showOptions = true
|
this.searchState.showOptions = true
|
||||||
}
|
}
|
||||||
// Update Input box value if arrow keys were pressed
|
// Update Input box value if arrow keys were pressed
|
||||||
if ((keyCode === 40 || keyCode === 38) && this.searchState.selectedOption !== -1) {
|
if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && this.searchState.selectedOption !== -1) {
|
||||||
|
event.preventDefault()
|
||||||
this.inputData = this.visibleDataList[this.searchState.selectedOption]
|
this.inputData = this.visibleDataList[this.searchState.selectedOption]
|
||||||
} else {
|
|
||||||
this.updateVisibleDataList()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
forceTextColor: forceTextColor,
|
forceTextColor: forceTextColor,
|
||||||
showActionButton: showActionButton,
|
showActionButton: showActionButton,
|
||||||
showClearTextButton: showClearTextButton,
|
showClearTextButton: showClearTextButton,
|
||||||
|
clearTextButtonVisible: inputDataPresent,
|
||||||
disabled: disabled
|
disabled: disabled
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
@ -22,11 +23,11 @@
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
v-if="showClearTextButton && clearTextButtonExisting"
|
v-if="showClearTextButton"
|
||||||
icon="times-circle"
|
icon="times-circle"
|
||||||
class="clearInputTextButton"
|
class="clearInputTextButton"
|
||||||
:class="{
|
:class="{
|
||||||
visible: clearTextButtonVisible
|
visible: inputDataPresent
|
||||||
}"
|
}"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
:id="id"
|
:id="id"
|
||||||
|
ref="input"
|
||||||
v-model="inputData"
|
v-model="inputData"
|
||||||
:list="idDataList"
|
:list="idDataList"
|
||||||
class="ft-input"
|
class="ft-input"
|
||||||
|
@ -47,7 +49,7 @@
|
||||||
@input="e => handleInput(e.target.value)"
|
@input="e => handleInput(e.target.value)"
|
||||||
@focus="handleFocus"
|
@focus="handleFocus"
|
||||||
@blur="handleInputBlur"
|
@blur="handleInputBlur"
|
||||||
@keydown="e => handleKeyDown(e.keyCode)"
|
@keydown="handleKeyDown"
|
||||||
>
|
>
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
v-if="showActionButton"
|
v-if="showActionButton"
|
||||||
|
|
|
@ -52,8 +52,8 @@
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--tertiary-text-color);
|
color: var(--secondary-text-color);
|
||||||
background-color: var(--secondary-card-bg-color);
|
background-color: var(--side-nav-color);
|
||||||
-webkit-transition: background 0.2s ease-out;
|
-webkit-transition: background 0.2s ease-out;
|
||||||
-moz-transition: background 0.2s ease-out;
|
-moz-transition: background 0.2s ease-out;
|
||||||
-o-transition: background 0.2s ease-out;
|
-o-transition: background 0.2s ease-out;
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonOption:hover {
|
.buttonOption:hover {
|
||||||
background-color: var(--search-bar-color);
|
background-color: var(--side-nav-hover-color);
|
||||||
-moz-transition: background 0.2s ease-in;
|
-moz-transition: background 0.2s ease-in;
|
||||||
-o-transition: background 0.2s ease-in;
|
-o-transition: background 0.2s ease-in;
|
||||||
transition: background 0.2s ease-in;
|
transition: background 0.2s ease-in;
|
||||||
|
|
|
@ -59,20 +59,7 @@ export default Vue.extend({
|
||||||
isFavorited: false,
|
isFavorited: false,
|
||||||
isUpcoming: false,
|
isUpcoming: false,
|
||||||
isPremium: false,
|
isPremium: false,
|
||||||
hideViews: false,
|
hideViews: false
|
||||||
optionsValues: [
|
|
||||||
'history',
|
|
||||||
'openYoutube',
|
|
||||||
'copyYoutube',
|
|
||||||
'openYoutubeEmbed',
|
|
||||||
'copyYoutubeEmbed',
|
|
||||||
'openInvidious',
|
|
||||||
'copyInvidious',
|
|
||||||
'openYoutubeChannel',
|
|
||||||
'copyYoutubeChannel',
|
|
||||||
'openInvidiousChannel',
|
|
||||||
'copyInvidiousChannel'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -130,27 +117,71 @@ export default Vue.extend({
|
||||||
return (this.watchProgress / this.data.lengthSeconds) * 100
|
return (this.watchProgress / this.data.lengthSeconds) * 100
|
||||||
},
|
},
|
||||||
|
|
||||||
optionsNames: function () {
|
dropdownOptions: function () {
|
||||||
const names = [
|
const options = []
|
||||||
this.$t('Video.Open in YouTube'),
|
|
||||||
this.$t('Video.Copy YouTube Link'),
|
|
||||||
this.$t('Video.Open YouTube Embedded Player'),
|
|
||||||
this.$t('Video.Copy YouTube Embedded Player Link'),
|
|
||||||
this.$t('Video.Open in Invidious'),
|
|
||||||
this.$t('Video.Copy Invidious Link'),
|
|
||||||
this.$t('Video.Open Channel in YouTube'),
|
|
||||||
this.$t('Video.Copy YouTube Channel Link'),
|
|
||||||
this.$t('Video.Open Channel in Invidious'),
|
|
||||||
this.$t('Video.Copy Invidious Channel Link')
|
|
||||||
]
|
|
||||||
|
|
||||||
if (this.watched) {
|
options.push(
|
||||||
names.unshift(this.$t('Video.Remove From History'))
|
{
|
||||||
} else {
|
label: this.watched
|
||||||
names.unshift(this.$t('Video.Mark As Watched'))
|
? this.$t('Video.Remove From History')
|
||||||
}
|
: this.$t('Video.Mark As Watched'),
|
||||||
|
value: 'history'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Video.Copy YouTube Link'),
|
||||||
|
value: 'copyYoutube'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Video.Copy YouTube Embedded Player Link'),
|
||||||
|
value: 'copyYoutubeEmbed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Video.Copy Invidious Link'),
|
||||||
|
value: 'copyInvidious'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Video.Open in YouTube'),
|
||||||
|
value: 'openYoutube'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Video.Open YouTube Embedded Player'),
|
||||||
|
value: 'openYoutubeEmbed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Video.Open in Invidious'),
|
||||||
|
value: 'openInvidious'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Video.Copy YouTube Channel Link'),
|
||||||
|
value: 'copyYoutubeChannel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Video.Copy Invidious Channel Link'),
|
||||||
|
value: 'copyInvidiousChannel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Video.Open Channel in YouTube'),
|
||||||
|
value: 'openYoutubeChannel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Video.Open Channel in Invidious'),
|
||||||
|
value: 'openInvidiousChannel'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return names
|
return options
|
||||||
},
|
},
|
||||||
|
|
||||||
thumbnail: function () {
|
thumbnail: function () {
|
||||||
|
@ -208,12 +239,6 @@ export default Vue.extend({
|
||||||
return this.$store.getters.getSaveWatchedProgress
|
return this.$store.getters.getSaveWatchedProgress
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
data: function () {
|
|
||||||
this.parseVideoData()
|
|
||||||
this.checkIfWatched()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
this.parseVideoData()
|
this.parseVideoData()
|
||||||
this.checkIfWatched()
|
this.checkIfWatched()
|
||||||
|
@ -227,6 +252,7 @@ export default Vue.extend({
|
||||||
watchProgress: this.watchProgress,
|
watchProgress: this.watchProgress,
|
||||||
playbackRate: this.defaultPlayback,
|
playbackRate: this.defaultPlayback,
|
||||||
videoId: this.id,
|
videoId: this.id,
|
||||||
|
videoLength: this.data.lengthSeconds,
|
||||||
playlistId: this.playlistId,
|
playlistId: this.playlistId,
|
||||||
playlistIndex: this.playlistIndex,
|
playlistIndex: this.playlistIndex,
|
||||||
playlistReverse: this.playlistReverse,
|
playlistReverse: this.playlistReverse,
|
||||||
|
@ -445,6 +471,7 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
|
|
||||||
this.watched = false
|
this.watched = false
|
||||||
|
this.watchProgress = 0
|
||||||
},
|
},
|
||||||
|
|
||||||
addToPlaylist: function () {
|
addToPlaylist: function () {
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
@use "../../sass-partials/_ft-list-item"
|
@use "../../sass-partials/_ft-list-item"
|
||||||
|
|
||||||
|
.thumbnailLink:hover
|
||||||
|
outline: 3px solid var(--side-nav-hover-color)
|
||||||
|
|
|
@ -66,13 +66,13 @@
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<ft-icon-button
|
<ft-icon-button
|
||||||
class="optionsButton"
|
class="optionsButton"
|
||||||
|
icon="ellipsis-v"
|
||||||
title="More Options"
|
title="More Options"
|
||||||
theme="base-no-default"
|
theme="base-no-default"
|
||||||
:size="16"
|
:size="16"
|
||||||
:use-shadow="false"
|
:use-shadow="false"
|
||||||
dropdown-position-x="left"
|
dropdown-position-x="left"
|
||||||
:dropdown-names="optionsNames"
|
:dropdown-options="dropdownOptions"
|
||||||
:dropdown-values="optionsValues"
|
|
||||||
@click="handleOptionsClick"
|
@click="handleOptionsClick"
|
||||||
/>
|
/>
|
||||||
<router-link
|
<router-link
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
.colorOption {
|
.colorOption {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
margin: 10px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
-webkit-border-radius: 50%;
|
-webkit-border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.colorOption:hover {
|
||||||
|
box-shadow: 0 0 0 2px var(--side-nav-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
.initial {
|
.initial {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 10px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#profileList {
|
#profileList {
|
||||||
|
@ -57,6 +62,7 @@
|
||||||
float: left;
|
float: left;
|
||||||
position: relative;
|
position: relative;
|
||||||
bottom: 5px;
|
bottom: 5px;
|
||||||
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profileName {
|
.profileName {
|
||||||
|
|
|
@ -29,10 +29,15 @@
|
||||||
width: auto
|
width: auto
|
||||||
|
|
||||||
@at-root
|
@at-root
|
||||||
.dark &
|
.dark &, .system[data-system-theme*='dark'] &
|
||||||
filter: brightness(0.868)
|
filter: brightness(0.868)
|
||||||
|
|
||||||
.light &
|
.black &
|
||||||
|
filter: brightness(0.933)
|
||||||
|
|
||||||
|
/* no changes for the dracula theme */
|
||||||
|
|
||||||
|
.light &, .system[data-system-theme*='light'] &
|
||||||
filter: invert(0.87)
|
filter: invert(0.87)
|
||||||
|
|
||||||
.invidious
|
.invidious
|
||||||
|
@ -48,8 +53,11 @@
|
||||||
margin-right: 2px
|
margin-right: 2px
|
||||||
|
|
||||||
@at-root
|
@at-root
|
||||||
.dark &
|
.dark &,
|
||||||
|
.black &,
|
||||||
|
.dracula &,
|
||||||
|
.system[data-system-theme*='dark'] &
|
||||||
background-image: url(~../../assets/img/invidious-logo-dark.svg)
|
background-image: url(~../../assets/img/invidious-logo-dark.svg)
|
||||||
|
|
||||||
.light &
|
.light &, .system[data-system-theme*='light'] &
|
||||||
background-image: url(~../../assets/img/invidious-logo-light.svg)
|
background-image: url(~../../assets/img/invidious-logo-light.svg)
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.pure-material-slider > input:disabled + span {
|
.pure-material-slider > input:disabled + span {
|
||||||
color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
|
opacity: 0.38;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Webkit | Track */
|
/* Webkit | Track */
|
||||||
|
|
|
@ -26,6 +26,10 @@ export default Vue.extend({
|
||||||
valueExtension: {
|
valueExtension: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
<input
|
<input
|
||||||
:id="id"
|
:id="id"
|
||||||
v-model.number="currentValue"
|
v-model.number="currentValue"
|
||||||
|
:disabled="disabled"
|
||||||
type="range"
|
type="range"
|
||||||
:min="minValue"
|
:min="minValue"
|
||||||
:max="maxValue"
|
:max="maxValue"
|
||||||
:step="step"
|
:step="step"
|
||||||
@change="$emit('change', $event.target.value)"
|
@change="$emit('change', currentValue)"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ label }}:
|
{{ label }}:
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import { mapActions } from 'vuex'
|
||||||
|
import FtSelect from '../ft-select/ft-select.vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'FtSponsorBlockCategory',
|
||||||
|
components: {
|
||||||
|
'ft-select': FtSelect
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
categoryName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
categoryColor: '',
|
||||||
|
skipOption: '',
|
||||||
|
skipValues: [
|
||||||
|
'autoSkip',
|
||||||
|
// 'promptToSkip',
|
||||||
|
'showInSeekBar',
|
||||||
|
'doNothing'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
colorValues: function () {
|
||||||
|
return this.$store.getters.getColorNames
|
||||||
|
},
|
||||||
|
|
||||||
|
colorNames: function () {
|
||||||
|
return this.colorValues.map(colorVal => {
|
||||||
|
// add spaces before capital letters
|
||||||
|
const colorName = colorVal.replace(/([A-Z])/g, ' $1').trim()
|
||||||
|
return this.$t(`Settings.Theme Settings.Main Color Theme.${colorName}`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
sponsorBlockValues: function() {
|
||||||
|
let sponsorVal = ''
|
||||||
|
switch (this.categoryName.toLowerCase()) {
|
||||||
|
case 'sponsor':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockSponsor
|
||||||
|
break
|
||||||
|
case 'self-promotion':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockSelfPromo
|
||||||
|
break
|
||||||
|
case 'interaction':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockInteraction
|
||||||
|
break
|
||||||
|
case 'intro':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockIntro
|
||||||
|
break
|
||||||
|
case 'outro':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockOutro
|
||||||
|
break
|
||||||
|
case 'recap':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockRecap
|
||||||
|
break
|
||||||
|
case 'music offtopic':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockMusicOffTopic
|
||||||
|
break
|
||||||
|
case 'filler':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockFiller
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return sponsorVal
|
||||||
|
},
|
||||||
|
|
||||||
|
skipNames: function() {
|
||||||
|
return [
|
||||||
|
this.$t('Settings.SponsorBlock Settings.Skip Options.Auto Skip'),
|
||||||
|
// this.$t('Settings.SponsorBlock Settings.Skip Options.Prompt To Skip'),
|
||||||
|
this.$t('Settings.SponsorBlock Settings.Skip Options.Show In Seek Bar'),
|
||||||
|
this.$t('Settings.SponsorBlock Settings.Skip Options.Do Nothing')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateColor: function (color) {
|
||||||
|
const payload = {
|
||||||
|
color: color,
|
||||||
|
skip: this.sponsorBlockValues.skip
|
||||||
|
}
|
||||||
|
this.updateSponsorCategory(payload)
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSkipOption: function (skipOption) {
|
||||||
|
const payload = {
|
||||||
|
color: this.sponsorBlockValues.color,
|
||||||
|
skip: skipOption
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateSponsorCategory(payload)
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSponsorCategory: function (payload) {
|
||||||
|
switch (this.categoryName.toLowerCase()) {
|
||||||
|
case 'sponsor':
|
||||||
|
this.updateSponsorBlockSponsor(payload)
|
||||||
|
break
|
||||||
|
case 'self-promotion':
|
||||||
|
this.updateSponsorBlockSelfPromo(payload)
|
||||||
|
break
|
||||||
|
case 'interaction':
|
||||||
|
this.updateSponsorBlockInteraction(payload)
|
||||||
|
break
|
||||||
|
case 'intro':
|
||||||
|
this.updateSponsorBlockIntro(payload)
|
||||||
|
break
|
||||||
|
case 'outro':
|
||||||
|
this.updateSponsorBlockOutro(payload)
|
||||||
|
break
|
||||||
|
case 'recap':
|
||||||
|
this.updateSponsorBlockRecap(payload)
|
||||||
|
break
|
||||||
|
case 'music offtopic':
|
||||||
|
this.updateSponsorBlockMusicOffTopic(payload)
|
||||||
|
break
|
||||||
|
case 'filler':
|
||||||
|
this.updateSponsorBlockFiller(payload)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
...mapActions([
|
||||||
|
'showToast',
|
||||||
|
'openExternalLink',
|
||||||
|
'updateSponsorBlockSponsor',
|
||||||
|
'updateSponsorBlockSelfPromo',
|
||||||
|
'updateSponsorBlockInteraction',
|
||||||
|
'updateSponsorBlockIntro',
|
||||||
|
'updateSponsorBlockOutro',
|
||||||
|
'updateSponsorBlockRecap',
|
||||||
|
'updateSponsorBlockMusicOffTopic',
|
||||||
|
'updateSponsorBlockFiller'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,6 @@
|
||||||
|
@use "../../sass-partials/settings"
|
||||||
|
.sponsorBlockCategory
|
||||||
|
margin-top: 30px
|
||||||
|
padding: 0 10px
|
||||||
|
.sponsorTitle
|
||||||
|
font-size: x-large
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="sponsorBlockCategory">
|
||||||
|
<div class="sponsorTitle">
|
||||||
|
{{ $t("Video.Sponsor Block category."+categoryName) }}
|
||||||
|
</div>
|
||||||
|
<ft-select
|
||||||
|
:placeholder="$t('Settings.SponsorBlock Settings.Category Color')"
|
||||||
|
:value="sponsorBlockValues.color"
|
||||||
|
:select-names="colorNames"
|
||||||
|
:select-values="colorValues"
|
||||||
|
@change="updateColor"
|
||||||
|
/>
|
||||||
|
<ft-select
|
||||||
|
:placeholder="$t('Settings.SponsorBlock Settings.Skip Options.Skip Option')"
|
||||||
|
:value="sponsorBlockValues.skip"
|
||||||
|
:select-names="skipNames"
|
||||||
|
:select-values="skipValues"
|
||||||
|
@change="updateSkipOption"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script src="./ft-sponsor-block-category.js" />
|
||||||
|
<style scoped lang="sass" src="./ft-sponsor-block-category.sass" />
|
|
@ -6,12 +6,12 @@ import $ from 'jquery'
|
||||||
import videojs from 'video.js'
|
import videojs from 'video.js'
|
||||||
import qualitySelector from '@silvermine/videojs-quality-selector'
|
import qualitySelector from '@silvermine/videojs-quality-selector'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
import 'videojs-overlay/dist/videojs-overlay'
|
import 'videojs-overlay/dist/videojs-overlay'
|
||||||
import 'videojs-overlay/dist/videojs-overlay.css'
|
import 'videojs-overlay/dist/videojs-overlay.css'
|
||||||
import 'videojs-vtt-thumbnails-freetube'
|
import 'videojs-vtt-thumbnails-freetube'
|
||||||
import 'videojs-contrib-quality-levels'
|
import 'videojs-contrib-quality-levels'
|
||||||
import 'videojs-http-source-selector'
|
import 'videojs-http-source-selector'
|
||||||
import { ipcRenderer } from 'electron'
|
|
||||||
|
|
||||||
import { IpcChannels } from '../../../constants'
|
import { IpcChannels } from '../../../constants'
|
||||||
|
|
||||||
|
@ -85,6 +85,10 @@ export default Vue.extend({
|
||||||
useHls: false,
|
useHls: false,
|
||||||
selectedDefaultQuality: '',
|
selectedDefaultQuality: '',
|
||||||
selectedQuality: '',
|
selectedQuality: '',
|
||||||
|
selectedResolution: '',
|
||||||
|
selectedBitrate: '',
|
||||||
|
selectedMimeType: '',
|
||||||
|
selectedFPS: 0,
|
||||||
using60Fps: false,
|
using60Fps: false,
|
||||||
maxFramerate: 0,
|
maxFramerate: 0,
|
||||||
activeSourceList: [],
|
activeSourceList: [],
|
||||||
|
@ -92,6 +96,10 @@ export default Vue.extend({
|
||||||
mouseTimeout: null,
|
mouseTimeout: null,
|
||||||
touchTimeout: null,
|
touchTimeout: null,
|
||||||
lastTouchTime: null,
|
lastTouchTime: null,
|
||||||
|
playerStats: null,
|
||||||
|
statsModal: null,
|
||||||
|
showStatsModal: false,
|
||||||
|
statsModalEventName: 'updateStats',
|
||||||
dataSetup: {
|
dataSetup: {
|
||||||
fluid: true,
|
fluid: true,
|
||||||
nativeTextTracks: false,
|
nativeTextTracks: false,
|
||||||
|
@ -108,6 +116,7 @@ export default Vue.extend({
|
||||||
'seekToLive',
|
'seekToLive',
|
||||||
'remainingTimeDisplay',
|
'remainingTimeDisplay',
|
||||||
'customControlSpacer',
|
'customControlSpacer',
|
||||||
|
'screenshotButton',
|
||||||
'playbackRateMenuButton',
|
'playbackRateMenuButton',
|
||||||
'loopButton',
|
'loopButton',
|
||||||
'chaptersButton',
|
'chaptersButton',
|
||||||
|
@ -119,56 +128,6 @@ export default Vue.extend({
|
||||||
'qualitySelector',
|
'qualitySelector',
|
||||||
'fullscreenToggle'
|
'fullscreenToggle'
|
||||||
]
|
]
|
||||||
},
|
|
||||||
playbackRates: [
|
|
||||||
0.25,
|
|
||||||
0.5,
|
|
||||||
0.75,
|
|
||||||
1,
|
|
||||||
1.25,
|
|
||||||
1.5,
|
|
||||||
1.75,
|
|
||||||
2,
|
|
||||||
2.25,
|
|
||||||
2.5,
|
|
||||||
2.75,
|
|
||||||
3,
|
|
||||||
3.25,
|
|
||||||
3.5,
|
|
||||||
3.75,
|
|
||||||
4,
|
|
||||||
4.25,
|
|
||||||
4.5,
|
|
||||||
4.75,
|
|
||||||
5,
|
|
||||||
5.25,
|
|
||||||
5.5,
|
|
||||||
5.75,
|
|
||||||
6,
|
|
||||||
6.25,
|
|
||||||
6.5,
|
|
||||||
6.75,
|
|
||||||
7,
|
|
||||||
7.25,
|
|
||||||
7.5,
|
|
||||||
7.75,
|
|
||||||
8
|
|
||||||
]
|
|
||||||
},
|
|
||||||
stats: {
|
|
||||||
videoId: '',
|
|
||||||
playerResolution: null,
|
|
||||||
frameInfo: null,
|
|
||||||
volume: 0,
|
|
||||||
bandwidth: null,
|
|
||||||
bufferPercent: 0,
|
|
||||||
fps: null,
|
|
||||||
display: {
|
|
||||||
modal: null,
|
|
||||||
event: 'statsUpdated',
|
|
||||||
keyboardShortcut: 'KeyI',
|
|
||||||
rightClickEvent: 'showVideoStatistics',
|
|
||||||
activated: false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,35 +189,111 @@ export default Vue.extend({
|
||||||
displayVideoPlayButton: function() {
|
displayVideoPlayButton: function() {
|
||||||
return this.$store.getters.getDisplayVideoPlayButton
|
return this.$store.getters.getDisplayVideoPlayButton
|
||||||
},
|
},
|
||||||
formatted_stats: function() {
|
|
||||||
let resolution = ''
|
|
||||||
let dropFrame = ''
|
|
||||||
if (this.stats.playerResolution != null) {
|
|
||||||
resolution = `(${this.stats.playerResolution.height}X${this.stats.playerResolution.width}) @ ${this.stats.fps} ${this.$t('Video.Stats.fps')}`
|
|
||||||
}
|
|
||||||
if (this.stats.frameInfo != null) {
|
|
||||||
dropFrame = `${this.stats.frameInfo.droppedVideoFrames} ${this.$t('Video.Stats.out of')} ${this.stats.frameInfo.totalVideoFrames}`
|
|
||||||
}
|
|
||||||
const stats = [
|
|
||||||
[this.$t('Video.Stats.video id'), this.stats.videoId],
|
|
||||||
[this.$t('Video.Stats.frame drop'), dropFrame],
|
|
||||||
[this.$t('Video.Stats.player resolution'), resolution],
|
|
||||||
[this.$t('Video.Stats.volume'), `${(this.stats.volume * 100).toFixed(0)} %`],
|
|
||||||
[this.$t('Video.Stats.bandwidth'), `${(this.stats.bandwidth / 1000).toFixed(2)} Kbps`],
|
|
||||||
[this.$t('Video.Stats.buffered'), `${(this.stats.bufferPercent * 100).toFixed(0)} %`]
|
|
||||||
]
|
|
||||||
|
|
||||||
let formattedStats = '<ul style="list-style-type: none;text-align:left; padding-left:0px";>'
|
sponsorSkips: function () {
|
||||||
for (const stat of stats) {
|
const sponsorCats = ['sponsor',
|
||||||
formattedStats += `<li style="font-size: 75%">${stat[0]}: ${stat[1]}</li>`
|
'selfpromo',
|
||||||
|
'interaction',
|
||||||
|
'intro',
|
||||||
|
'outro',
|
||||||
|
'preview',
|
||||||
|
'music_offtopic',
|
||||||
|
'filler'
|
||||||
|
]
|
||||||
|
const autoSkip = {}
|
||||||
|
const seekBar = []
|
||||||
|
const promptSkip = {}
|
||||||
|
const categoryData = {}
|
||||||
|
sponsorCats.forEach(x => {
|
||||||
|
let sponsorVal = {}
|
||||||
|
switch (x) {
|
||||||
|
case 'sponsor':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockSponsor
|
||||||
|
break
|
||||||
|
case 'selfpromo':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockSelfPromo
|
||||||
|
break
|
||||||
|
case 'interaction':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockInteraction
|
||||||
|
break
|
||||||
|
case 'intro':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockIntro
|
||||||
|
break
|
||||||
|
case 'outro':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockOutro
|
||||||
|
break
|
||||||
|
case 'preview':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockRecap
|
||||||
|
break
|
||||||
|
case 'music_offtopic':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockMusicOffTopic
|
||||||
|
break
|
||||||
|
case 'filler':
|
||||||
|
sponsorVal = this.$store.getters.getSponsorBlockFiller
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (sponsorVal.skip !== 'doNothing') {
|
||||||
|
seekBar.push(x)
|
||||||
|
}
|
||||||
|
if (sponsorVal.skip === 'autoSkip') {
|
||||||
|
autoSkip[x] = true
|
||||||
|
}
|
||||||
|
if (sponsorVal.skip === 'promptToSkip') {
|
||||||
|
promptSkip[x] = true
|
||||||
|
}
|
||||||
|
categoryData[x] = sponsorVal
|
||||||
|
})
|
||||||
|
return { autoSkip, seekBar, promptSkip, categoryData }
|
||||||
|
},
|
||||||
|
|
||||||
|
maxVideoPlaybackRate: function () {
|
||||||
|
return parseInt(this.$store.getters.getMaxVideoPlaybackRate)
|
||||||
|
},
|
||||||
|
|
||||||
|
videoPlaybackRateInterval: function () {
|
||||||
|
return parseFloat(this.$store.getters.getVideoPlaybackRateInterval)
|
||||||
|
},
|
||||||
|
|
||||||
|
playbackRates: function () {
|
||||||
|
const playbackRates = []
|
||||||
|
let i = this.videoPlaybackRateInterval
|
||||||
|
|
||||||
|
while (i <= this.maxVideoPlaybackRate) {
|
||||||
|
playbackRates.push(i)
|
||||||
|
i = i + this.videoPlaybackRateInterval
|
||||||
|
i = parseFloat(i.toFixed(2))
|
||||||
}
|
}
|
||||||
formattedStats += '</ul>'
|
|
||||||
return formattedStats
|
return playbackRates
|
||||||
|
},
|
||||||
|
|
||||||
|
enableScreenshot: function() {
|
||||||
|
return this.$store.getters.getEnableScreenshot
|
||||||
|
},
|
||||||
|
|
||||||
|
screenshotFormat: function() {
|
||||||
|
return this.$store.getters.getScreenshotFormat
|
||||||
|
},
|
||||||
|
|
||||||
|
screenshotQuality: function() {
|
||||||
|
return this.$store.getters.getScreenshotQuality
|
||||||
|
},
|
||||||
|
|
||||||
|
screenshotAskPath: function() {
|
||||||
|
return this.$store.getters.getScreenshotAskPath
|
||||||
|
},
|
||||||
|
|
||||||
|
screenshotFolder: function() {
|
||||||
|
return this.$store.getters.getScreenshotFolderPath
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
selectedQuality: function() {
|
showStatsModal: function() {
|
||||||
this.currentFps()
|
this.player.trigger(this.statsModalEventName)
|
||||||
|
},
|
||||||
|
|
||||||
|
enableScreenshot: function() {
|
||||||
|
this.toggleScreenshotButton()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
|
@ -270,11 +305,19 @@ export default Vue.extend({
|
||||||
this.volume = volume
|
this.volume = volume
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.dataSetup.playbackRates = this.playbackRates
|
||||||
|
|
||||||
this.createFullWindowButton()
|
this.createFullWindowButton()
|
||||||
this.createLoopButton()
|
this.createLoopButton()
|
||||||
this.createToggleTheatreModeButton()
|
this.createToggleTheatreModeButton()
|
||||||
|
this.createScreenshotButton()
|
||||||
this.determineFormatType()
|
this.determineFormatType()
|
||||||
this.determineMaxFramerate()
|
this.determineMaxFramerate()
|
||||||
|
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.setActionHandler('play', () => this.player.play())
|
||||||
|
navigator.mediaSession.setActionHandler('pause', () => this.player.pause())
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy: function () {
|
beforeDestroy: function () {
|
||||||
if (this.player !== null) {
|
if (this.player !== null) {
|
||||||
|
@ -287,6 +330,12 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.setActionHandler('play', null)
|
||||||
|
navigator.mediaSession.setActionHandler('pause', null)
|
||||||
|
navigator.mediaSession.playbackState = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
if (this.usingElectron && this.powerSaveBlocker !== null) {
|
if (this.usingElectron && this.powerSaveBlocker !== null) {
|
||||||
const { ipcRenderer } = require('electron')
|
const { ipcRenderer } = require('electron')
|
||||||
ipcRenderer.send(IpcChannels.STOP_POWER_SAVE_BLOCKER, this.powerSaveBlocker)
|
ipcRenderer.send(IpcChannels.STOP_POWER_SAVE_BLOCKER, this.powerSaveBlocker)
|
||||||
|
@ -411,20 +460,34 @@ export default Vue.extend({
|
||||||
this.player.on('ready', () => {
|
this.player.on('ready', () => {
|
||||||
this.$emit('ready')
|
this.$emit('ready')
|
||||||
this.checkAspectRatio()
|
this.checkAspectRatio()
|
||||||
|
this.createStatsModal()
|
||||||
if (this.captionHybridList.length !== 0) {
|
if (this.captionHybridList.length !== 0) {
|
||||||
this.transformAndInsertCaptions()
|
this.transformAndInsertCaptions()
|
||||||
}
|
}
|
||||||
|
this.toggleScreenshotButton()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.player.on('ended', () => {
|
this.player.on('ended', () => {
|
||||||
this.$emit('ended')
|
this.$emit('ended')
|
||||||
|
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.playbackState = 'none'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.player.on('error', (error, message) => {
|
this.player.on('error', (error, message) => {
|
||||||
this.$emit('error', error.target.player.error_)
|
this.$emit('error', error.target.player.error_)
|
||||||
|
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.playbackState = 'none'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.player.on('play', async function () {
|
this.player.on('play', async function () {
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.playbackState = 'playing'
|
||||||
|
}
|
||||||
|
|
||||||
if (this.usingElectron) {
|
if (this.usingElectron) {
|
||||||
const { ipcRenderer } = require('electron')
|
const { ipcRenderer } = require('electron')
|
||||||
this.powerSaveBlocker =
|
this.powerSaveBlocker =
|
||||||
|
@ -433,6 +496,10 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
|
|
||||||
this.player.on('pause', function () {
|
this.player.on('pause', function () {
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.playbackState = 'paused'
|
||||||
|
}
|
||||||
|
|
||||||
if (this.usingElectron && this.powerSaveBlocker !== null) {
|
if (this.usingElectron && this.powerSaveBlocker !== null) {
|
||||||
const { ipcRenderer } = require('electron')
|
const { ipcRenderer } = require('electron')
|
||||||
ipcRenderer.send(IpcChannels.STOP_POWER_SAVE_BLOCKER, this.powerSaveBlocker)
|
ipcRenderer.send(IpcChannels.STOP_POWER_SAVE_BLOCKER, this.powerSaveBlocker)
|
||||||
|
@ -440,18 +507,43 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.player.on(this.statsModalEventName, () => {
|
||||||
|
if (this.showStatsModal) {
|
||||||
|
this.statsModal.open()
|
||||||
|
this.player.controls(true)
|
||||||
|
this.statsModal.contentEl().innerHTML = this.getFormattedStats()
|
||||||
|
} else {
|
||||||
|
this.statsModal.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.player.on('timeupdate', () => {
|
||||||
|
if (this.format === 'dash') {
|
||||||
|
this.playerStats = this.player.tech({ IWillNotUseThisInPlugins: true }).vhs.stats
|
||||||
|
this.updateStatsContent()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.player.textTrackSettings.on('modalclose', (_) => {
|
this.player.textTrackSettings.on('modalclose', (_) => {
|
||||||
const settings = this.player.textTrackSettings.getValues()
|
const settings = this.player.textTrackSettings.getValues()
|
||||||
this.updateDefaultCaptionSettings(JSON.stringify(settings))
|
this.updateDefaultCaptionSettings(JSON.stringify(settings))
|
||||||
})
|
})
|
||||||
this.addPlayerStatsEvent()
|
|
||||||
|
// right click menu
|
||||||
|
if (this.usingElectron) {
|
||||||
|
const { ipcRenderer } = require('electron')
|
||||||
|
ipcRenderer.removeAllListeners('showVideoStatistics')
|
||||||
|
ipcRenderer.on('showVideoStatistics', (event) => {
|
||||||
|
this.toggleShowStatsModal()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
initializeSponsorBlock() {
|
initializeSponsorBlock() {
|
||||||
this.sponsorBlockSkipSegments({
|
this.sponsorBlockSkipSegments({
|
||||||
videoId: this.videoId,
|
videoId: this.videoId,
|
||||||
categories: ['sponsor']
|
categories: this.sponsorSkips.seekBar
|
||||||
}).then((skipSegments) => {
|
}).then((skipSegments) => {
|
||||||
if (skipSegments.length === 0) {
|
if (skipSegments.length === 0) {
|
||||||
return
|
return
|
||||||
|
@ -469,7 +561,8 @@ export default Vue.extend({
|
||||||
this.addSponsorBlockMarker({
|
this.addSponsorBlockMarker({
|
||||||
time: startTime,
|
time: startTime,
|
||||||
duration: endTime - startTime,
|
duration: endTime - startTime,
|
||||||
color: this.sponsorBlockCategoryColor(category)
|
color: 'var(--primary-color)',
|
||||||
|
category: category
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -488,10 +581,12 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (newTime !== null && Math.abs(duration - currentTime) > 0.500) {
|
if (newTime !== null && Math.abs(duration - currentTime) > 0.500) {
|
||||||
if (this.sponsorBlockShowSkippedToast) {
|
if (this.sponsorSkips.autoSkip[skippedCategory]) {
|
||||||
this.showSkippedSponsorSegmentInformation(skippedCategory)
|
if (this.sponsorBlockShowSkippedToast) {
|
||||||
|
this.showSkippedSponsorSegmentInformation(skippedCategory)
|
||||||
|
}
|
||||||
|
this.player.currentTime(newTime)
|
||||||
}
|
}
|
||||||
this.player.currentTime(newTime)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -516,42 +611,25 @@ export default Vue.extend({
|
||||||
return this.$t('Video.Sponsor Block category.interaction')
|
return this.$t('Video.Sponsor Block category.interaction')
|
||||||
case 'music_offtopic':
|
case 'music_offtopic':
|
||||||
return this.$t('Video.Sponsor Block category.music offtopic')
|
return this.$t('Video.Sponsor Block category.music offtopic')
|
||||||
|
case 'filler':
|
||||||
|
return this.$t('Video.Sponsor Block category.filler')
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown translation for SponsorBlock category ${category}`)
|
console.error(`Unknown translation for SponsorBlock category ${category}`)
|
||||||
return category
|
return category
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
sponsorBlockCategoryColor(category) {
|
|
||||||
// TODO: allow to set these colors in settings
|
|
||||||
switch (category) {
|
|
||||||
case 'sponsor':
|
|
||||||
return 'var(--accent-color)'
|
|
||||||
case 'intro':
|
|
||||||
return 'var(--accent-color)'
|
|
||||||
case 'outro':
|
|
||||||
return 'var(--accent-color)'
|
|
||||||
case 'selfpromo':
|
|
||||||
return 'var(--accent-color)'
|
|
||||||
case 'interaction':
|
|
||||||
return 'var(--accent-color)'
|
|
||||||
case 'music_offtopic':
|
|
||||||
return 'var(--accent-color)'
|
|
||||||
default:
|
|
||||||
console.error(`Unknown SponsorBlock category ${category}`)
|
|
||||||
return 'var(--accent-color)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addSponsorBlockMarker(marker) {
|
addSponsorBlockMarker(marker) {
|
||||||
const markerDiv = videojs.dom.createEl('div', {}, {})
|
const markerDiv = videojs.dom.createEl('div', {}, {})
|
||||||
|
|
||||||
markerDiv.className = 'sponsorBlockMarker'
|
markerDiv.className = `sponsorBlockMarker main${this.sponsorSkips.categoryData[marker.category].color}`
|
||||||
markerDiv.style.height = '100%'
|
markerDiv.style.height = '100%'
|
||||||
markerDiv.style.position = 'absolute'
|
markerDiv.style.position = 'absolute'
|
||||||
|
markerDiv.style.opacity = '0.6'
|
||||||
markerDiv.style['background-color'] = marker.color
|
markerDiv.style['background-color'] = marker.color
|
||||||
markerDiv.style.width = (marker.duration / this.player.duration()) * 100 + '%'
|
markerDiv.style.width = (marker.duration / this.player.duration()) * 100 + '%'
|
||||||
markerDiv.style.marginLeft = (marker.time / this.player.duration()) * 100 + '%'
|
markerDiv.style.marginLeft = (marker.time / this.player.duration()) * 100 + '%'
|
||||||
|
markerDiv.title = this.sponsorBlockTranslatedCategory(marker.category)
|
||||||
|
|
||||||
this.player.el().querySelector('.vjs-progress-holder').appendChild(markerDiv)
|
this.player.el().querySelector('.vjs-progress-holder').appendChild(markerDiv)
|
||||||
},
|
},
|
||||||
|
@ -851,6 +929,18 @@ export default Vue.extend({
|
||||||
qualityElement.innerText = selectedQuality
|
qualityElement.innerText = selectedQuality
|
||||||
this.selectedQuality = selectedQuality
|
this.selectedQuality = selectedQuality
|
||||||
|
|
||||||
|
if (selectedQuality !== 'auto') {
|
||||||
|
this.selectedResolution = `${adaptiveFormat.width}x${adaptiveFormat.height}`
|
||||||
|
this.selectedFPS = adaptiveFormat.fps
|
||||||
|
this.selectedBitrate = adaptiveFormat.bitrate
|
||||||
|
this.selectedMimeType = adaptiveFormat.mimeType
|
||||||
|
} else {
|
||||||
|
this.selectedResolution = 'auto'
|
||||||
|
this.selectedFPS = 'auto'
|
||||||
|
this.selectedBitrate = 'auto'
|
||||||
|
this.selectedMimeType = 'auto'
|
||||||
|
}
|
||||||
|
|
||||||
const qualityItems = $('.quality-item').get()
|
const qualityItems = $('.quality-item').get()
|
||||||
|
|
||||||
$('.quality-item').removeClass('quality-selected')
|
$('.quality-item').removeClass('quality-selected')
|
||||||
|
@ -999,7 +1089,7 @@ export default Vue.extend({
|
||||||
changePlayBackRate: function (rate) {
|
changePlayBackRate: function (rate) {
|
||||||
const newPlaybackRate = (this.player.playbackRate() + rate).toFixed(2)
|
const newPlaybackRate = (this.player.playbackRate() + rate).toFixed(2)
|
||||||
|
|
||||||
if (newPlaybackRate >= 0.25 && newPlaybackRate <= 8) {
|
if (newPlaybackRate >= this.videoPlaybackRateInterval && newPlaybackRate <= this.maxVideoPlaybackRate) {
|
||||||
this.player.playbackRate(newPlaybackRate)
|
this.player.playbackRate(newPlaybackRate)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1012,7 +1102,7 @@ export default Vue.extend({
|
||||||
if (this.maxFramerate === 60 && quality.height >= 480) {
|
if (this.maxFramerate === 60 && quality.height >= 480) {
|
||||||
for (let i = 0; i < this.adaptiveFormats.length; i++) {
|
for (let i = 0; i < this.adaptiveFormats.length; i++) {
|
||||||
if (this.adaptiveFormats[i].bitrate === quality.bitrate) {
|
if (this.adaptiveFormats[i].bitrate === quality.bitrate) {
|
||||||
fps = this.adaptiveFormats[i].fps
|
fps = this.adaptiveFormats[i].fps ? this.adaptiveFormats[i].fps : 30
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1077,8 +1167,8 @@ export default Vue.extend({
|
||||||
|
|
||||||
toggleVideoLoop: async function () {
|
toggleVideoLoop: async function () {
|
||||||
if (!this.player.loop()) {
|
if (!this.player.loop()) {
|
||||||
const currentTheme = localStorage.getItem('mainColor')
|
const currentTheme = this.$store.state.settings.mainColor
|
||||||
const colorNames = this.$store.state.utils.colorClasses
|
const colorNames = this.$store.state.utils.colorNames
|
||||||
const colorValues = this.$store.state.utils.colorValues
|
const colorValues = this.$store.state.utils.colorValues
|
||||||
|
|
||||||
const nameIndex = colorNames.findIndex((color) => {
|
const nameIndex = colorNames.findIndex((color) => {
|
||||||
|
@ -1162,6 +1252,168 @@ export default Vue.extend({
|
||||||
this.$parent.toggleTheatreMode()
|
this.$parent.toggleTheatreMode()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createScreenshotButton: function() {
|
||||||
|
const VjsButton = videojs.getComponent('Button')
|
||||||
|
const screenshotButton = videojs.extend(VjsButton, {
|
||||||
|
constructor: function(player, options) {
|
||||||
|
VjsButton.call(this, player, options)
|
||||||
|
},
|
||||||
|
handleClick: () => {
|
||||||
|
this.takeScreenshot()
|
||||||
|
const video = document.getElementsByTagName('video')[0]
|
||||||
|
video.focus()
|
||||||
|
video.blur()
|
||||||
|
},
|
||||||
|
createControlTextEl: function (button) {
|
||||||
|
return $(button)
|
||||||
|
.html('<div id="screenshotButton" class="vjs-icon-screenshot vjs-button vjs-hidden"></div>')
|
||||||
|
.attr('title', 'Screenshot')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
videojs.registerComponent('screenshotButton', screenshotButton)
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleScreenshotButton: function() {
|
||||||
|
const button = document.getElementById('screenshotButton')
|
||||||
|
if (this.enableScreenshot && this.format !== 'audio') {
|
||||||
|
button.classList.remove('vjs-hidden')
|
||||||
|
} else {
|
||||||
|
button.classList.add('vjs-hidden')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
takeScreenshot: async function() {
|
||||||
|
if (!this.enableScreenshot || this.format === 'audio') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = this.player.videoWidth()
|
||||||
|
const height = this.player.videoHeight()
|
||||||
|
if (width <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to set crossorigin="anonymous" for LegacyFormat on Invidious
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
|
||||||
|
const video = document.querySelector('video')
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = width
|
||||||
|
canvas.height = height
|
||||||
|
canvas.getContext('2d').drawImage(video, 0, 0)
|
||||||
|
|
||||||
|
const format = this.screenshotFormat
|
||||||
|
const mimeType = `image/${format === 'jpg' ? 'jpeg' : format}`
|
||||||
|
const imageQuality = format === 'jpg' ? this.screenshotQuality / 100 : 1
|
||||||
|
|
||||||
|
let filename
|
||||||
|
try {
|
||||||
|
filename = await this.parseScreenshotCustomFileName({
|
||||||
|
date: new Date(Date.now()),
|
||||||
|
playerTime: this.player.currentTime(),
|
||||||
|
videoId: this.videoId
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Parse failed: ${err.message}`)
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Screenshot Error').replace('$', err.message)
|
||||||
|
})
|
||||||
|
canvas.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirChar = process.platform === 'win32' ? '\\' : '/'
|
||||||
|
let subDir = ''
|
||||||
|
if (filename.indexOf(dirChar) !== -1) {
|
||||||
|
const lastIndex = filename.lastIndexOf(dirChar)
|
||||||
|
subDir = filename.substring(0, lastIndex)
|
||||||
|
filename = filename.substring(lastIndex + 1)
|
||||||
|
}
|
||||||
|
const filenameWithExtension = `${filename}.${format}`
|
||||||
|
|
||||||
|
let dirPath
|
||||||
|
let filePath
|
||||||
|
if (this.screenshotAskPath) {
|
||||||
|
const wasPlaying = !this.player.paused()
|
||||||
|
if (wasPlaying) {
|
||||||
|
this.player.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.screenshotFolder === '' || !fs.existsSync(this.screenshotFolder)) {
|
||||||
|
dirPath = await this.getPicturesPath()
|
||||||
|
} else {
|
||||||
|
dirPath = this.screenshotFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
defaultPath: path.join(dirPath, filenameWithExtension),
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: format.toUpperCase(),
|
||||||
|
extensions: [format]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.showSaveDialog({ options, useModal: true })
|
||||||
|
if (wasPlaying) {
|
||||||
|
this.player.play()
|
||||||
|
}
|
||||||
|
if (response.canceled || response.filePath === '') {
|
||||||
|
canvas.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath = response.filePath
|
||||||
|
if (!filePath.endsWith(`.${format}`)) {
|
||||||
|
filePath = `${filePath}.${format}`
|
||||||
|
}
|
||||||
|
|
||||||
|
dirPath = path.dirname(filePath)
|
||||||
|
this.updateScreenshotFolderPath(dirPath)
|
||||||
|
} else {
|
||||||
|
if (this.screenshotFolder === '') {
|
||||||
|
dirPath = path.join(await this.getPicturesPath(), 'Freetube', subDir)
|
||||||
|
} else {
|
||||||
|
dirPath = path.join(this.screenshotFolder, subDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true })
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Screenshot Error').replace('$', err)
|
||||||
|
})
|
||||||
|
canvas.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filePath = path.join(dirPath, filenameWithExtension)
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.toBlob((result) => {
|
||||||
|
result.arrayBuffer().then(ab => {
|
||||||
|
const arr = new Uint8Array(ab)
|
||||||
|
|
||||||
|
fs.writeFile(filePath, arr, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err)
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Screenshot Error').replace('$', err)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Screenshot Success').replace('$', filePath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, mimeType, imageQuality)
|
||||||
|
canvas.remove()
|
||||||
|
},
|
||||||
|
|
||||||
createDashQualitySelector: function (levels) {
|
createDashQualitySelector: function (levels) {
|
||||||
if (levels.levels_.length === 0) {
|
if (levels.levels_.length === 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -1209,6 +1461,10 @@ export default Vue.extend({
|
||||||
return format.bitrate === quality.bitrate
|
return format.bitrate === quality.bitrate
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (typeof adaptiveFormat === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.activeAdaptiveFormats.push(adaptiveFormat)
|
this.activeAdaptiveFormats.push(adaptiveFormat)
|
||||||
|
|
||||||
fps = adaptiveFormat.fps ? adaptiveFormat.fps : 30
|
fps = adaptiveFormat.fps ? adaptiveFormat.fps : 30
|
||||||
|
@ -1426,7 +1682,71 @@ export default Vue.extend({
|
||||||
handleTouchEnd: function (event) {
|
handleTouchEnd: function (event) {
|
||||||
clearTimeout(this.touchPauseTimeout)
|
clearTimeout(this.touchPauseTimeout)
|
||||||
},
|
},
|
||||||
|
toggleShowStatsModal: function() {
|
||||||
|
console.log(this.format)
|
||||||
|
if (this.format !== 'dash') {
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Video.Stats.Video statistics are not available for legacy videos')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.showStatsModal = !this.showStatsModal
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createStatsModal: function() {
|
||||||
|
const ModalDialog = videojs.getComponent('ModalDialog')
|
||||||
|
this.statsModal = new ModalDialog(this.player, {
|
||||||
|
temporary: false,
|
||||||
|
pauseOnOpen: false
|
||||||
|
})
|
||||||
|
this.statsModal.handleKeyDown_ = (event) => {
|
||||||
|
// the default handler prevents keyboard events propagating beyond the modal
|
||||||
|
// the modal should only handle the escape and tab key, all others should be handled by the player
|
||||||
|
if (event.key === 'Escape' || event.key === 'Tab') {
|
||||||
|
this.statsModal.handleKeyDown(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.player.addChild(this.statsModal)
|
||||||
|
this.statsModal.el_.classList.add('statsModal')
|
||||||
|
this.statsModal.on('modalclose', () => {
|
||||||
|
this.showStatsModal = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateStatsContent: function() {
|
||||||
|
if (this.showStatsModal) {
|
||||||
|
this.statsModal.contentEl().innerHTML = this.getFormattedStats()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getFormattedStats: function() {
|
||||||
|
const currentVolume = this.player.muted() ? 0 : this.player.volume()
|
||||||
|
const volume = `${(currentVolume * 100).toFixed(0)}%`
|
||||||
|
const bandwidth = `${(this.playerStats.bandwidth / 1000).toFixed(2)}kbps`
|
||||||
|
const buffered = `${(this.player.bufferedPercent() * 100).toFixed(0)}%`
|
||||||
|
const droppedFrames = this.playerStats.videoPlaybackQuality.droppedVideoFrames
|
||||||
|
const totalFrames = this.playerStats.videoPlaybackQuality.totalVideoFrames
|
||||||
|
const frames = `${droppedFrames} / ${totalFrames}`
|
||||||
|
const resolution = this.selectedResolution === 'auto' ? 'auto' : `${this.selectedResolution}@${this.selectedFPS}fps`
|
||||||
|
const playerDimensions = `${this.playerStats.playerDimensions.width}x${this.playerStats.playerDimensions.height}`
|
||||||
|
const statsArray = [
|
||||||
|
[this.$t('Video.Stats.Video ID'), this.videoId],
|
||||||
|
[this.$t('Video.Stats.Resolution'), resolution],
|
||||||
|
[this.$t('Video.Stats.Player Dimensions'), playerDimensions],
|
||||||
|
[this.$t('Video.Stats.Bitrate'), this.selectedBitrate],
|
||||||
|
[this.$t('Video.Stats.Volume'), volume],
|
||||||
|
[this.$t('Video.Stats.Bandwidth'), bandwidth],
|
||||||
|
[this.$t('Video.Stats.Buffered'), buffered],
|
||||||
|
[this.$t('Video.Stats.Dropped / Total Frames'), frames],
|
||||||
|
[this.$t('Video.Stats.Mimetype'), this.selectedMimeType]
|
||||||
|
]
|
||||||
|
let listContentHTML = ''
|
||||||
|
|
||||||
|
statsArray.forEach((stat) => {
|
||||||
|
const content = `<p>${stat[0]}: ${stat[1]}</p>`
|
||||||
|
listContentHTML += content
|
||||||
|
})
|
||||||
|
return listContentHTML
|
||||||
|
},
|
||||||
|
|
||||||
|
// This function should always be at the bottom of this file
|
||||||
keyboardShortcutHandler: function (event) {
|
keyboardShortcutHandler: function (event) {
|
||||||
const activeInputs = $('.ft-input')
|
const activeInputs = $('.ft-input')
|
||||||
|
|
||||||
|
@ -1452,7 +1772,7 @@ export default Vue.extend({
|
||||||
// J Key
|
// J Key
|
||||||
// Rewind by 2x the time-skip interval (in seconds)
|
// Rewind by 2x the time-skip interval (in seconds)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.changeDurationBySeconds(-this.defaultSkipInterval * 2)
|
this.changeDurationBySeconds(-this.defaultSkipInterval * this.player.playbackRate() * 2)
|
||||||
break
|
break
|
||||||
case 75:
|
case 75:
|
||||||
// K Key
|
// K Key
|
||||||
|
@ -1464,19 +1784,19 @@ export default Vue.extend({
|
||||||
// L Key
|
// L Key
|
||||||
// Fast-Forward by 2x the time-skip interval (in seconds)
|
// Fast-Forward by 2x the time-skip interval (in seconds)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.changeDurationBySeconds(this.defaultSkipInterval * 2)
|
this.changeDurationBySeconds(this.defaultSkipInterval * this.player.playbackRate() * 2)
|
||||||
break
|
break
|
||||||
case 79:
|
case 79:
|
||||||
// O Key
|
// O Key
|
||||||
// Decrease playback rate by 0.25x
|
// Decrease playback rate by 0.25x
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.changePlayBackRate(-0.25)
|
this.changePlayBackRate(-this.videoPlaybackRateInterval)
|
||||||
break
|
break
|
||||||
case 80:
|
case 80:
|
||||||
// P Key
|
// P Key
|
||||||
// Increase playback rate by 0.25x
|
// Increase playback rate by 0.25x
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.changePlayBackRate(0.25)
|
this.changePlayBackRate(this.videoPlaybackRateInterval)
|
||||||
break
|
break
|
||||||
case 70:
|
case 70:
|
||||||
// F Key
|
// F Key
|
||||||
|
@ -1512,13 +1832,23 @@ export default Vue.extend({
|
||||||
// Left Arrow Key
|
// Left Arrow Key
|
||||||
// Rewind by the time-skip interval (in seconds)
|
// Rewind by the time-skip interval (in seconds)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.changeDurationBySeconds(-this.defaultSkipInterval * 1)
|
this.changeDurationBySeconds(-this.defaultSkipInterval * this.player.playbackRate())
|
||||||
break
|
break
|
||||||
case 39:
|
case 39:
|
||||||
// Right Arrow Key
|
// Right Arrow Key
|
||||||
// Fast-Forward by the time-skip interval (in seconds)
|
// Fast-Forward by the time-skip interval (in seconds)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.changeDurationBySeconds(this.defaultSkipInterval * 1)
|
this.changeDurationBySeconds(this.defaultSkipInterval * this.player.playbackRate())
|
||||||
|
break
|
||||||
|
case 73:
|
||||||
|
// I Key
|
||||||
|
event.preventDefault()
|
||||||
|
// Toggle Picture in Picture Mode
|
||||||
|
if (this.format !== 'audio' && !this.player.isInPictureInPicture()) {
|
||||||
|
this.player.requestPictureInPicture()
|
||||||
|
} else if (this.player.isInPictureInPicture()) {
|
||||||
|
this.player.exitPictureInPicture()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 49:
|
case 49:
|
||||||
// 1 Key
|
// 1 Key
|
||||||
|
@ -1592,12 +1922,8 @@ export default Vue.extend({
|
||||||
break
|
break
|
||||||
case 68:
|
case 68:
|
||||||
// D Key
|
// D Key
|
||||||
// Toggle Picture in Picture Mode
|
event.preventDefault()
|
||||||
if (!this.player.isInPictureInPicture()) {
|
this.toggleShowStatsModal()
|
||||||
this.player.requestPictureInPicture()
|
|
||||||
} else if (this.player.isInPictureInPicture()) {
|
|
||||||
this.player.exitPictureInPicture()
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
case 27:
|
case 27:
|
||||||
// esc Key
|
// esc Key
|
||||||
|
@ -1615,86 +1941,11 @@ export default Vue.extend({
|
||||||
// Toggle Theatre Mode
|
// Toggle Theatre Mode
|
||||||
this.toggleTheatreMode()
|
this.toggleTheatreMode()
|
||||||
break
|
break
|
||||||
}
|
case 85:
|
||||||
}
|
// U Key
|
||||||
},
|
// Take screenshot
|
||||||
|
this.takeScreenshot()
|
||||||
addPlayerStatsEvent: function() {
|
break
|
||||||
this.stats.videoId = this.videoId
|
|
||||||
this.player.on('volumechange', () => {
|
|
||||||
this.stats.volume = this.player.volume()
|
|
||||||
this.player.trigger(this.stats.display.event)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.player.on('timeupdate', () => {
|
|
||||||
const stats = this.player.tech({ IWillNotUseThisInPlugins: true }).vhs.stats
|
|
||||||
this.stats.frameInfo = stats.videoPlaybackQuality
|
|
||||||
this.player.trigger(this.stats.display.event)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.player.on('progress', () => {
|
|
||||||
const stats = this.player.tech({ IWillNotUseThisInPlugins: true }).vhs.stats
|
|
||||||
|
|
||||||
this.stats.bandwidth = stats.bandwidth
|
|
||||||
this.stats.bufferPercent = this.player.bufferedPercent()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.player.on('playerresize', () => {
|
|
||||||
this.stats.playerResolution = this.player.currentDimensions()
|
|
||||||
this.player.trigger(this.stats.display.event)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.createStatsModal()
|
|
||||||
|
|
||||||
this.player.on(this.stats.display.event, () => {
|
|
||||||
if (this.stats.display.activated) {
|
|
||||||
this.stats.display.modal.open()
|
|
||||||
this.player.controls(true)
|
|
||||||
this.stats.display.modal.contentEl().innerHTML = this.formatted_stats
|
|
||||||
} else {
|
|
||||||
this.stats.display.modal.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// keyboard shortcut
|
|
||||||
window.addEventListener('keyup', (event) => {
|
|
||||||
if (event.code === this.stats.display.keyboardShortcut) {
|
|
||||||
if (this.stats.display.activated) {
|
|
||||||
this.deactivateStatsDisplay()
|
|
||||||
} else {
|
|
||||||
this.activateStatsDisplay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, true)
|
|
||||||
// right click menu
|
|
||||||
ipcRenderer.on(this.stats.display.rightClickEvent, () => {
|
|
||||||
this.activateStatsDisplay()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
createStatsModal: function() {
|
|
||||||
const ModalDialog = videojs.getComponent('ModalDialog')
|
|
||||||
this.stats.display.modal = new ModalDialog(this.player, {
|
|
||||||
temporary: false,
|
|
||||||
pauseOnOpen: false
|
|
||||||
})
|
|
||||||
this.player.addChild(this.stats.display.modal)
|
|
||||||
this.stats.display.modal.height('35%')
|
|
||||||
this.stats.display.modal.width('50%')
|
|
||||||
this.stats.display.modal.contentEl().style.backgroundColor = 'rgba(0, 0, 0, 0.55)'
|
|
||||||
this.stats.display.modal.on('modalclose', () => {
|
|
||||||
this.deactivateStatsDisplay()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
activateStatsDisplay: function() {
|
|
||||||
this.stats.display.activated = true
|
|
||||||
},
|
|
||||||
deactivateStatsDisplay: function() {
|
|
||||||
this.stats.display.activated = false
|
|
||||||
},
|
|
||||||
currentFps: function() {
|
|
||||||
for (const el of this.activeAdaptiveFormats) {
|
|
||||||
if (el.qualityLabel === this.selectedQuality) {
|
|
||||||
this.stats.fps = el.fps
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1703,7 +1954,11 @@ export default Vue.extend({
|
||||||
'calculateColorLuminance',
|
'calculateColorLuminance',
|
||||||
'updateDefaultCaptionSettings',
|
'updateDefaultCaptionSettings',
|
||||||
'showToast',
|
'showToast',
|
||||||
'sponsorBlockSkipSegments'
|
'sponsorBlockSkipSegments',
|
||||||
|
'parseScreenshotCustomFileName',
|
||||||
|
'updateScreenshotFolderPath',
|
||||||
|
'getPicturesPath',
|
||||||
|
'showSaveDialog'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
controls
|
controls
|
||||||
preload="auto"
|
preload="auto"
|
||||||
:data-setup="JSON.stringify(dataSetup)"
|
:data-setup="JSON.stringify(dataSetup)"
|
||||||
|
crossorigin="anonymous"
|
||||||
@touchstart="handleTouchStart"
|
@touchstart="handleTouchStart"
|
||||||
@touchend="handleTouchEnd"
|
@touchend="handleTouchEnd"
|
||||||
>
|
>
|
||||||
|
|
|
@ -5,6 +5,12 @@ import FtSelect from '../ft-select/ft-select.vue'
|
||||||
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
||||||
import FtSlider from '../ft-slider/ft-slider.vue'
|
import FtSlider from '../ft-slider/ft-slider.vue'
|
||||||
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||||
|
import FtButton from '../ft-button/ft-button.vue'
|
||||||
|
import FtInput from '../ft-input/ft-input.vue'
|
||||||
|
import FtTooltip from '../ft-tooltip/ft-tooltip.vue'
|
||||||
|
import { ipcRenderer } from 'electron'
|
||||||
|
import { IpcChannels } from '../../../constants'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'PlayerSettings',
|
name: 'PlayerSettings',
|
||||||
|
@ -13,7 +19,10 @@ export default Vue.extend({
|
||||||
'ft-select': FtSelect,
|
'ft-select': FtSelect,
|
||||||
'ft-toggle-switch': FtToggleSwitch,
|
'ft-toggle-switch': FtToggleSwitch,
|
||||||
'ft-slider': FtSlider,
|
'ft-slider': FtSlider,
|
||||||
'ft-flex-box': FtFlexBox
|
'ft-flex-box': FtFlexBox,
|
||||||
|
'ft-button': FtButton,
|
||||||
|
'ft-input': FtInput,
|
||||||
|
'ft-tooltip': FtTooltip
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
@ -30,7 +39,24 @@ export default Vue.extend({
|
||||||
480,
|
480,
|
||||||
720,
|
720,
|
||||||
1080
|
1080
|
||||||
]
|
],
|
||||||
|
playbackRateIntervalValues: [
|
||||||
|
0.1,
|
||||||
|
0.25,
|
||||||
|
0.5,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
screenshotFormatNames: [
|
||||||
|
'PNG',
|
||||||
|
'JPEG'
|
||||||
|
],
|
||||||
|
screenshotFormatValues: [
|
||||||
|
'png',
|
||||||
|
'jpg'
|
||||||
|
],
|
||||||
|
screenshotFolderPlaceholder: '',
|
||||||
|
screenshotFilenameExample: '',
|
||||||
|
screenshotDefaultPattern: '%Y%M%D-%H%N%S'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -106,6 +132,14 @@ export default Vue.extend({
|
||||||
return this.$store.getters.getDisplayVideoPlayButton
|
return this.$store.getters.getDisplayVideoPlayButton
|
||||||
},
|
},
|
||||||
|
|
||||||
|
maxVideoPlaybackRate: function () {
|
||||||
|
return parseInt(this.$store.getters.getMaxVideoPlaybackRate)
|
||||||
|
},
|
||||||
|
|
||||||
|
videoPlaybackRateInterval: function () {
|
||||||
|
return this.$store.getters.getVideoPlaybackRateInterval
|
||||||
|
},
|
||||||
|
|
||||||
formatNames: function () {
|
formatNames: function () {
|
||||||
return [
|
return [
|
||||||
this.$t('Settings.Player Settings.Default Video Format.Dash Formats'),
|
this.$t('Settings.Player Settings.Default Video Format.Dash Formats'),
|
||||||
|
@ -124,9 +158,101 @@ export default Vue.extend({
|
||||||
this.$t('Settings.Player Settings.Default Quality.720p'),
|
this.$t('Settings.Player Settings.Default Quality.720p'),
|
||||||
this.$t('Settings.Player Settings.Default Quality.1080p')
|
this.$t('Settings.Player Settings.Default Quality.1080p')
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
enableScreenshot: function() {
|
||||||
|
return this.$store.getters.getEnableScreenshot
|
||||||
|
},
|
||||||
|
|
||||||
|
screenshotFormat: function() {
|
||||||
|
return this.$store.getters.getScreenshotFormat
|
||||||
|
},
|
||||||
|
|
||||||
|
screenshotQuality: function() {
|
||||||
|
return this.$store.getters.getScreenshotQuality
|
||||||
|
},
|
||||||
|
|
||||||
|
screenshotAskPath: function() {
|
||||||
|
return this.$store.getters.getScreenshotAskPath
|
||||||
|
},
|
||||||
|
|
||||||
|
screenshotFolder: function() {
|
||||||
|
return this.$store.getters.getScreenshotFolderPath
|
||||||
|
},
|
||||||
|
|
||||||
|
screenshotFilenamePattern: function() {
|
||||||
|
return this.$store.getters.getScreenshotFilenamePattern
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
screenshotFolder: function() {
|
||||||
|
this.getScreenshotFolderPlaceholder()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted: function() {
|
||||||
|
this.getScreenshotFolderPlaceholder()
|
||||||
|
this.getScreenshotFilenameExample(this.screenshotFilenamePattern)
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleUpdateScreenshotFormat: async function(format) {
|
||||||
|
await this.updateScreenshotFormat(format)
|
||||||
|
this.getScreenshotFilenameExample(this.screenshotFilenamePattern)
|
||||||
|
},
|
||||||
|
|
||||||
|
getScreenshotEmptyFolderPlaceholder: async function() {
|
||||||
|
return path.join(await this.getPicturesPath(), 'Freetube')
|
||||||
|
},
|
||||||
|
|
||||||
|
getScreenshotFolderPlaceholder: function() {
|
||||||
|
if (this.screenshotFolder !== '') {
|
||||||
|
this.screenshotFolderPlaceholder = this.screenshotFolder
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.getScreenshotEmptyFolderPlaceholder().then((res) => {
|
||||||
|
this.screenshotFolderPlaceholder = res
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
chooseScreenshotFolder: async function() {
|
||||||
|
// only use with electron
|
||||||
|
const folder = await ipcRenderer.invoke(
|
||||||
|
IpcChannels.SHOW_OPEN_DIALOG,
|
||||||
|
{ properties: ['openDirectory'] }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!folder.canceled) {
|
||||||
|
await this.updateScreenshotFolderPath(folder.filePaths[0])
|
||||||
|
this.getScreenshotFolderPlaceholder()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleScreenshotFilenamePatternChanged: async function(input) {
|
||||||
|
const pattern = input.trim()
|
||||||
|
if (!await this.getScreenshotFilenameExample(pattern)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (pattern) {
|
||||||
|
this.updateScreenshotFilenamePattern(pattern)
|
||||||
|
} else {
|
||||||
|
this.updateScreenshotFilenamePattern(this.screenshotDefaultPattern)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getScreenshotFilenameExample: function(pattern) {
|
||||||
|
return this.parseScreenshotCustomFileName({
|
||||||
|
pattern: pattern || this.screenshotDefaultPattern,
|
||||||
|
date: new Date(Date.now()),
|
||||||
|
playerTime: 123.456,
|
||||||
|
videoId: 'dQw4w9WgXcQ'
|
||||||
|
}).then(res => {
|
||||||
|
this.screenshotFilenameExample = `${res}.${this.screenshotFormat}`
|
||||||
|
return true
|
||||||
|
}).catch(err => {
|
||||||
|
this.screenshotFilenameExample = `❗ ${this.$t(`Settings.Player Settings.Screenshot.Error.${err.message}`)}`
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
...mapActions([
|
...mapActions([
|
||||||
'updateAutoplayVideos',
|
'updateAutoplayVideos',
|
||||||
'updateAutoplayPlaylists',
|
'updateAutoplayPlaylists',
|
||||||
|
@ -143,7 +269,17 @@ export default Vue.extend({
|
||||||
'updateDefaultQuality',
|
'updateDefaultQuality',
|
||||||
'updateVideoVolumeMouseScroll',
|
'updateVideoVolumeMouseScroll',
|
||||||
'updateVideoPlaybackRateMouseScroll',
|
'updateVideoPlaybackRateMouseScroll',
|
||||||
'updateDisplayVideoPlayButton'
|
'updateDisplayVideoPlayButton',
|
||||||
|
'updateMaxVideoPlaybackRate',
|
||||||
|
'updateVideoPlaybackRateInterval',
|
||||||
|
'updateEnableScreenshot',
|
||||||
|
'updateScreenshotFormat',
|
||||||
|
'updateScreenshotQuality',
|
||||||
|
'updateScreenshotAskPath',
|
||||||
|
'updateScreenshotFolderPath',
|
||||||
|
'updateScreenshotFilenamePattern',
|
||||||
|
'parseScreenshotCustomFileName',
|
||||||
|
'getPicturesPath'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1 +1,14 @@
|
||||||
@use "../../sass-partials/settings"
|
@use "../../sass-partials/settings"
|
||||||
|
|
||||||
|
.screenshotFolderContainer
|
||||||
|
width: 95%
|
||||||
|
margin: 0 auto
|
||||||
|
align-items: center
|
||||||
|
column-gap: 1rem
|
||||||
|
|
||||||
|
.screenshotFolderLabel, .screenshotFolderButton, .screenshotFilenamePatternTitle
|
||||||
|
flex-grow: 0
|
||||||
|
|
||||||
|
.screenshotFolderPath, .screenshotFilenamePatternInput, .screenshotFilenamePatternExample
|
||||||
|
flex-grow: 1
|
||||||
|
margin-top: 10px
|
||||||
|
|
|
@ -115,6 +115,22 @@
|
||||||
value-extension="×"
|
value-extension="×"
|
||||||
@change="updateDefaultPlayback"
|
@change="updateDefaultPlayback"
|
||||||
/>
|
/>
|
||||||
|
<ft-slider
|
||||||
|
:label="$t('Settings.Player Settings.Max Video Playback Rate')"
|
||||||
|
:default-value="maxVideoPlaybackRate"
|
||||||
|
:min-value="2"
|
||||||
|
:max-value="10"
|
||||||
|
:step="1"
|
||||||
|
value-extension="x"
|
||||||
|
@change="updateMaxVideoPlaybackRate"
|
||||||
|
/>
|
||||||
|
<ft-select
|
||||||
|
:placeholder="$t('Settings.Player Settings.Video Playback Rate Interval')"
|
||||||
|
:value="videoPlaybackRateInterval"
|
||||||
|
:select-names="playbackRateIntervalValues"
|
||||||
|
:select-values="playbackRateIntervalValues"
|
||||||
|
@change="updateVideoPlaybackRateInterval"
|
||||||
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
<ft-flex-box>
|
<ft-flex-box>
|
||||||
<ft-select
|
<ft-select
|
||||||
|
@ -133,6 +149,88 @@
|
||||||
@change="updateDefaultQuality"
|
@change="updateDefaultQuality"
|
||||||
/>
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
|
<br>
|
||||||
|
<ft-flex-box>
|
||||||
|
<ft-toggle-switch
|
||||||
|
:label="$t('Settings.Player Settings.Screenshot.Enable')"
|
||||||
|
:default-value="enableScreenshot"
|
||||||
|
@change="updateEnableScreenshot"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
<div v-if="enableScreenshot">
|
||||||
|
<ft-flex-box>
|
||||||
|
<ft-select
|
||||||
|
:placeholder="$t('Settings.Player Settings.Screenshot.Format Label')"
|
||||||
|
:value="screenshotFormat"
|
||||||
|
:select-names="screenshotFormatNames"
|
||||||
|
:select-values="screenshotFormatValues"
|
||||||
|
@change="handleUpdateScreenshotFormat"
|
||||||
|
/>
|
||||||
|
<ft-slider
|
||||||
|
:label="$t('Settings.Player Settings.Screenshot.Quality Label')"
|
||||||
|
:default-value="screenshotQuality"
|
||||||
|
:min-value="0"
|
||||||
|
:max-value="100"
|
||||||
|
:step="1"
|
||||||
|
value-extension="%"
|
||||||
|
:disabled="screenshotFormat !== 'jpg'"
|
||||||
|
@change="updateScreenshotQuality"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
<ft-flex-box>
|
||||||
|
<ft-toggle-switch
|
||||||
|
:label="$t('Settings.Player Settings.Screenshot.Ask Path')"
|
||||||
|
:default-value="screenshotAskPath"
|
||||||
|
@change="updateScreenshotAskPath"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
<ft-flex-box
|
||||||
|
v-if="!screenshotAskPath"
|
||||||
|
class="screenshotFolderContainer"
|
||||||
|
>
|
||||||
|
<p class="screenshotFolderLabel">
|
||||||
|
{{ $t('Settings.Player Settings.Screenshot.Folder Label') }}
|
||||||
|
</p>
|
||||||
|
<ft-input
|
||||||
|
class="screenshotFolderPath"
|
||||||
|
:placeholder="screenshotFolderPlaceholder"
|
||||||
|
:show-action-button="false"
|
||||||
|
:show-label="false"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
<ft-button
|
||||||
|
:label="$t('Settings.Player Settings.Screenshot.Folder Button')"
|
||||||
|
class="screenshotFolderButton"
|
||||||
|
@click="chooseScreenshotFolder"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
<ft-flex-box class="screenshotFolderContainer">
|
||||||
|
<p class="screenshotFilenamePatternTitle">
|
||||||
|
{{ $t('Settings.Player Settings.Screenshot.File Name Label') }}
|
||||||
|
<ft-tooltip
|
||||||
|
class="selectTooltip"
|
||||||
|
position="bottom"
|
||||||
|
:tooltip="$t('Settings.Player Settings.Screenshot.File Name Tooltip')"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<ft-input
|
||||||
|
class="screenshotFilenamePatternInput"
|
||||||
|
placeholder=""
|
||||||
|
:value="screenshotFilenamePattern"
|
||||||
|
:spellcheck="false"
|
||||||
|
:show-action-button="false"
|
||||||
|
:show-label="false"
|
||||||
|
@input="handleScreenshotFilenamePatternChanged"
|
||||||
|
/>
|
||||||
|
<ft-input
|
||||||
|
class="screenshotFilenamePatternExample"
|
||||||
|
:placeholder="`${screenshotFilenameExample}`"
|
||||||
|
:show-action-button="false"
|
||||||
|
:show-label="false"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -3,32 +3,44 @@
|
||||||
|
|
||||||
.playlistThumbnail img
|
.playlistThumbnail img
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
|
@media only screen and (max-width: 800px)
|
||||||
|
display: none
|
||||||
|
|
||||||
|
.playlistStats
|
||||||
|
font-size: 15px
|
||||||
|
|
||||||
|
.playlistStats p
|
||||||
|
color: var(--secondary-text-color)
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
.playlistTitle
|
||||||
|
margin-bottom: 0.1em
|
||||||
|
|
||||||
.playlistDescription
|
.playlistDescription
|
||||||
max-height: 20vh
|
max-height: 20vh
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
|
white-space: break-spaces
|
||||||
|
|
||||||
@media only screen and (max-width: 500px)
|
@media only screen and (max-width: 500px)
|
||||||
max-height: 10vh
|
max-height: 10vh
|
||||||
|
|
||||||
.playlistChannel
|
.playlistChannel
|
||||||
height: 70px
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 8px
|
||||||
|
height: 40px
|
||||||
|
|
||||||
/* Indicates the box can be clicked to navigate */
|
/* Indicates the box can be clicked to navigate */
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
.channelThumbnail
|
.channelThumbnail
|
||||||
width: 70px
|
width: 40px
|
||||||
float: left
|
float: left
|
||||||
border-radius: 200px 200px 200px 200px
|
border-radius: 200px 200px 200px 200px
|
||||||
-webkit-border-radius: 200px 200px 200px 200px
|
-webkit-border-radius: 200px 200px 200px 200px
|
||||||
|
|
||||||
.channelName
|
.channelName
|
||||||
float: left
|
margin: 0
|
||||||
position: relative
|
|
||||||
width: 200px
|
|
||||||
margin-left: 10px
|
|
||||||
top: 5px
|
|
||||||
font-size: 15px
|
font-size: 15px
|
||||||
|
|
|
@ -8,22 +8,27 @@
|
||||||
@click="playFirstVideo"
|
@click="playFirstVideo"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<h2>
|
|
||||||
{{ title }}
|
<div class="playlistStats">
|
||||||
</h2>
|
<h2 class="playlistTitle">
|
||||||
<p>
|
{{ title }}
|
||||||
{{ videoCount }} {{ $t("Playlist.Videos") }} - <span v-if="!hideViews">{{ viewCount }} {{ $t("Playlist.Views") }} -</span>
|
</h2>
|
||||||
<span v-if="infoSource !== 'local'">
|
<p>
|
||||||
{{ $t("Playlist.Last Updated On") }}
|
{{ videoCount }} {{ $t("Playlist.Videos") }} - <span v-if="!hideViews">{{ viewCount }} {{ $t("Playlist.Views") }} -</span>
|
||||||
</span>
|
<span v-if="infoSource !== 'local'">
|
||||||
{{ lastUpdated }}
|
{{ $t("Playlist.Last Updated On") }}
|
||||||
</p>
|
</span>
|
||||||
|
{{ lastUpdated }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
class="playlistDescription"
|
class="playlistDescription"
|
||||||
>
|
v-text="description"
|
||||||
{{ description }}
|
/>
|
||||||
</p>
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="playlistChannel"
|
class="playlistChannel"
|
||||||
@click="goToChannel"
|
@click="goToChannel"
|
||||||
|
@ -38,7 +43,9 @@
|
||||||
{{ channelName }}
|
{{ channelName }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<ft-list-dropdown
|
<ft-list-dropdown
|
||||||
:title="$t('Playlist.Share Playlist.Share Playlist')"
|
:title="$t('Playlist.Share Playlist.Share Playlist')"
|
||||||
:label-names="shareHeaders"
|
:label-names="shareHeaders"
|
||||||
|
|
|
@ -31,8 +31,7 @@ export default Vue.extend({
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
dataAvailable: false,
|
dataAvailable: false,
|
||||||
proxyTestUrl: 'https://api.ipify.org?format=json',
|
proxyTestUrl: 'https://ipwho.is/',
|
||||||
proxyTestUrl1: 'https://freegeoip.app/json/',
|
|
||||||
proxyId: '',
|
proxyId: '',
|
||||||
proxyCountry: '',
|
proxyCountry: '',
|
||||||
proxyRegion: '',
|
proxyRegion: '',
|
||||||
|
@ -125,11 +124,11 @@ export default Vue.extend({
|
||||||
if (!this.useProxy) {
|
if (!this.useProxy) {
|
||||||
this.enableProxy()
|
this.enableProxy()
|
||||||
}
|
}
|
||||||
$.getJSON(this.proxyTestUrl1, (response) => {
|
$.getJSON(this.proxyTestUrl, (response) => {
|
||||||
console.log(response)
|
console.log(response)
|
||||||
this.proxyIp = response.ip
|
this.proxyIp = response.ip
|
||||||
this.proxyCountry = response.country_name
|
this.proxyCountry = response.country
|
||||||
this.proxyRegion = response.region_name
|
this.proxyRegion = response.region
|
||||||
this.proxyCity = response.city
|
this.proxyCity = response.city
|
||||||
this.dataAvailable = true
|
this.dataAvailable = true
|
||||||
}).fail((xhr, textStatus, error) => {
|
}).fail((xhr, textStatus, error) => {
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
class="center"
|
class="center"
|
||||||
:style="{opacity: useProxy ? 1 : 0.4}"
|
:style="{opacity: useProxy ? 1 : 0.4}"
|
||||||
>
|
>
|
||||||
{{ $t('Settings.Proxy Settings.Clicking on Test Proxy will send a request to') }} https://freegeoip.app/json/
|
{{ $t('Settings.Proxy Settings.Clicking on Test Proxy will send a request to') }} {{ proxyTestUrl }}
|
||||||
</p>
|
</p>
|
||||||
<ft-flex-box>
|
<ft-flex-box>
|
||||||
<ft-button
|
<ft-button
|
||||||
|
|
|
@ -3,11 +3,10 @@
|
||||||
height: calc(100vh - 60px);
|
height: calc(100vh - 60px);
|
||||||
width: 200px;
|
width: 200px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
position: fixed;
|
position: sticky;
|
||||||
left: 0px;
|
left: 0;
|
||||||
top: 0px;
|
top: 60px;
|
||||||
z-index: 4;
|
z-index: 3;
|
||||||
margin-top: 60px;
|
|
||||||
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;
|
||||||
|
@ -168,6 +167,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sideNav {
|
.sideNav {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import FtCard from '../ft-card/ft-card.vue'
|
||||||
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
||||||
import FtInput from '../ft-input/ft-input.vue'
|
import FtInput from '../ft-input/ft-input.vue'
|
||||||
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||||
|
import FtSponsorBlockCategory from '../ft-sponsor-block-category/ft-sponsor-block-category.vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'SponsorBlockSettings',
|
name: 'SponsorBlockSettings',
|
||||||
|
@ -11,7 +12,22 @@ export default Vue.extend({
|
||||||
'ft-card': FtCard,
|
'ft-card': FtCard,
|
||||||
'ft-toggle-switch': FtToggleSwitch,
|
'ft-toggle-switch': FtToggleSwitch,
|
||||||
'ft-input': FtInput,
|
'ft-input': FtInput,
|
||||||
'ft-flex-box': FtFlexBox
|
'ft-flex-box': FtFlexBox,
|
||||||
|
'ft-sponsor-block-category': FtSponsorBlockCategory
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
categories: [
|
||||||
|
'sponsor',
|
||||||
|
'self-promotion',
|
||||||
|
'interaction',
|
||||||
|
'intro',
|
||||||
|
'outro',
|
||||||
|
'recap',
|
||||||
|
'music offtopic',
|
||||||
|
'filler'
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
useSponsorBlock: function () {
|
useSponsorBlock: function () {
|
||||||
|
|
|
@ -32,6 +32,13 @@
|
||||||
@input="handleUpdateSponsorBlockUrl"
|
@input="handleUpdateSponsorBlockUrl"
|
||||||
/>
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
|
<ft-flex-box>
|
||||||
|
<ft-sponsor-block-category
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category"
|
||||||
|
:category-name="category"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -19,10 +19,6 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
currentBaseTheme: '',
|
|
||||||
currentMainColor: '',
|
|
||||||
currentSecColor: '',
|
|
||||||
expandSideBar: false,
|
|
||||||
minUiScale: 50,
|
minUiScale: 50,
|
||||||
maxUiScale: 300,
|
maxUiScale: 300,
|
||||||
uiScaleStep: 5,
|
uiScaleStep: 5,
|
||||||
|
@ -33,35 +29,11 @@ export default Vue.extend({
|
||||||
'no'
|
'no'
|
||||||
],
|
],
|
||||||
baseThemeValues: [
|
baseThemeValues: [
|
||||||
|
'system',
|
||||||
'light',
|
'light',
|
||||||
'dark',
|
'dark',
|
||||||
'black',
|
'black',
|
||||||
'dracula'
|
'dracula'
|
||||||
],
|
|
||||||
colorValues: [
|
|
||||||
'Red',
|
|
||||||
'Pink',
|
|
||||||
'Purple',
|
|
||||||
'DeepPurple',
|
|
||||||
'Indigo',
|
|
||||||
'Blue',
|
|
||||||
'LightBlue',
|
|
||||||
'Cyan',
|
|
||||||
'Teal',
|
|
||||||
'Green',
|
|
||||||
'LightGreen',
|
|
||||||
'Lime',
|
|
||||||
'Yellow',
|
|
||||||
'Amber',
|
|
||||||
'Orange',
|
|
||||||
'DeepOrange',
|
|
||||||
'DraculaCyan',
|
|
||||||
'DraculaGreen',
|
|
||||||
'DraculaOrange',
|
|
||||||
'DraculaPink',
|
|
||||||
'DraculaPurple',
|
|
||||||
'DraculaRed',
|
|
||||||
'DraculaYellow'
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -70,6 +42,18 @@ export default Vue.extend({
|
||||||
return this.$store.getters.getBarColor
|
return this.$store.getters.getBarColor
|
||||||
},
|
},
|
||||||
|
|
||||||
|
baseTheme: function () {
|
||||||
|
return this.$store.getters.getBaseTheme
|
||||||
|
},
|
||||||
|
|
||||||
|
mainColor: function () {
|
||||||
|
return this.$store.getters.getMainColor
|
||||||
|
},
|
||||||
|
|
||||||
|
secColor: function () {
|
||||||
|
return this.$store.getters.getSecColor
|
||||||
|
},
|
||||||
|
|
||||||
isSideNavOpen: function () {
|
isSideNavOpen: function () {
|
||||||
return this.$store.getters.getIsSideNavOpen
|
return this.$store.getters.getIsSideNavOpen
|
||||||
},
|
},
|
||||||
|
@ -81,9 +65,15 @@ export default Vue.extend({
|
||||||
disableSmoothScrolling: function () {
|
disableSmoothScrolling: function () {
|
||||||
return this.$store.getters.getDisableSmoothScrolling
|
return this.$store.getters.getDisableSmoothScrolling
|
||||||
},
|
},
|
||||||
|
|
||||||
|
expandSideBar: function () {
|
||||||
|
return this.$store.getters.getExpandSideBar
|
||||||
|
},
|
||||||
|
|
||||||
hideLabelsSideBar: function () {
|
hideLabelsSideBar: function () {
|
||||||
return this.$store.getters.getHideLabelsSideBar
|
return this.$store.getters.getHideLabelsSideBar
|
||||||
},
|
},
|
||||||
|
|
||||||
restartPromptMessage: function () {
|
restartPromptMessage: function () {
|
||||||
return this.$t('Settings["The app needs to restart for changes to take effect. Restart and apply change?"]')
|
return this.$t('Settings["The app needs to restart for changes to take effect. Restart and apply change?"]')
|
||||||
},
|
},
|
||||||
|
@ -97,6 +87,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
baseThemeNames: function () {
|
baseThemeNames: function () {
|
||||||
return [
|
return [
|
||||||
|
this.$t('Settings.Theme Settings.Base Theme.System Default'),
|
||||||
this.$t('Settings.Theme Settings.Base Theme.Light'),
|
this.$t('Settings.Theme Settings.Base Theme.Light'),
|
||||||
this.$t('Settings.Theme Settings.Base Theme.Dark'),
|
this.$t('Settings.Theme Settings.Base Theme.Dark'),
|
||||||
this.$t('Settings.Theme Settings.Base Theme.Black'),
|
this.$t('Settings.Theme Settings.Base Theme.Black'),
|
||||||
|
@ -104,63 +95,28 @@ export default Vue.extend({
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
colorValues: function () {
|
||||||
|
return this.$store.getters.getColorNames
|
||||||
|
},
|
||||||
|
|
||||||
colorNames: function () {
|
colorNames: function () {
|
||||||
return [
|
return this.colorValues.map(colorVal => {
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Red'),
|
// add spaces before capital letters
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Pink'),
|
const colorName = colorVal.replace(/([A-Z])/g, ' $1').trim()
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Purple'),
|
return this.$t(`Settings.Theme Settings.Main Color Theme.${colorName}`)
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Deep Purple'),
|
})
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Indigo'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Blue'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Light Blue'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Cyan'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Teal'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Green'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Light Green'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Lime'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Yellow'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Amber'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Orange'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Deep Orange'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Cyan'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Green'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Orange'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Pink'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Purple'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Red'),
|
|
||||||
this.$t('Settings.Theme Settings.Main Color Theme.Dracula Yellow')
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
this.currentBaseTheme = localStorage.getItem('baseTheme')
|
|
||||||
this.currentMainColor = localStorage.getItem('mainColor').replace('main', '')
|
|
||||||
this.currentSecColor = localStorage.getItem('secColor').replace('sec', '')
|
|
||||||
this.expandSideBar = localStorage.getItem('expandSideBar') === 'true'
|
|
||||||
this.disableSmoothScrollingToggleValue = this.disableSmoothScrolling
|
this.disableSmoothScrollingToggleValue = this.disableSmoothScrolling
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateBaseTheme: function (theme) {
|
|
||||||
const mainColor = `main${this.currentMainColor}`
|
|
||||||
const secColor = `sec${this.currentSecColor}`
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
baseTheme: theme,
|
|
||||||
mainColor: mainColor,
|
|
||||||
secColor: secColor
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$parent.$parent.updateTheme(payload)
|
|
||||||
this.currentBaseTheme = theme
|
|
||||||
},
|
|
||||||
|
|
||||||
handleExpandSideBar: function (value) {
|
handleExpandSideBar: function (value) {
|
||||||
if (this.isSideNavOpen !== value) {
|
if (this.isSideNavOpen !== value) {
|
||||||
this.$store.commit('toggleSideNav')
|
this.$store.commit('toggleSideNav')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.expandSideBar = value
|
this.updateExpandSideBar(value)
|
||||||
localStorage.setItem('expandSideBar', value)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleRestartPrompt: function (value) {
|
handleRestartPrompt: function (value) {
|
||||||
|
@ -186,36 +142,12 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
updateMainColor: function (color) {
|
|
||||||
const mainColor = `main${color}`
|
|
||||||
const secColor = `sec${this.currentSecColor}`
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
baseTheme: this.currentBaseTheme,
|
|
||||||
mainColor: mainColor,
|
|
||||||
secColor: secColor
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$parent.$parent.updateTheme(payload)
|
|
||||||
this.currentMainColor = color
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSecColor: function (color) {
|
|
||||||
const mainColor = `main${this.currentMainColor}`
|
|
||||||
const secColor = `sec${color}`
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
baseTheme: this.currentBaseTheme,
|
|
||||||
mainColor: mainColor,
|
|
||||||
secColor: secColor
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$parent.$parent.updateTheme(payload)
|
|
||||||
this.currentSecColor = color
|
|
||||||
},
|
|
||||||
|
|
||||||
...mapActions([
|
...mapActions([
|
||||||
'updateBarColor',
|
'updateBarColor',
|
||||||
|
'updateBaseTheme',
|
||||||
|
'updateMainColor',
|
||||||
|
'updateSecColor',
|
||||||
|
'updateExpandSideBar',
|
||||||
'updateUiScale',
|
'updateUiScale',
|
||||||
'updateDisableSmoothScrolling',
|
'updateDisableSmoothScrolling',
|
||||||
'updateHideLabelsSideBar'
|
'updateHideLabelsSideBar'
|
||||||
|
|
|
@ -43,21 +43,21 @@
|
||||||
<ft-flex-box>
|
<ft-flex-box>
|
||||||
<ft-select
|
<ft-select
|
||||||
:placeholder="$t('Settings.Theme Settings.Base Theme.Base Theme')"
|
:placeholder="$t('Settings.Theme Settings.Base Theme.Base Theme')"
|
||||||
:value="currentBaseTheme"
|
:value="baseTheme"
|
||||||
:select-names="baseThemeNames"
|
:select-names="baseThemeNames"
|
||||||
:select-values="baseThemeValues"
|
:select-values="baseThemeValues"
|
||||||
@change="updateBaseTheme"
|
@change="updateBaseTheme"
|
||||||
/>
|
/>
|
||||||
<ft-select
|
<ft-select
|
||||||
:placeholder="$t('Settings.Theme Settings.Main Color Theme.Main Color Theme')"
|
:placeholder="$t('Settings.Theme Settings.Main Color Theme.Main Color Theme')"
|
||||||
:value="currentMainColor"
|
:value="mainColor"
|
||||||
:select-names="colorNames"
|
:select-names="colorNames"
|
||||||
:select-values="colorValues"
|
:select-values="colorValues"
|
||||||
@change="updateMainColor"
|
@change="updateMainColor"
|
||||||
/>
|
/>
|
||||||
<ft-select
|
<ft-select
|
||||||
:placeholder="$t('Settings.Theme Settings.Secondary Color Theme')"
|
:placeholder="$t('Settings.Theme Settings.Secondary Color Theme')"
|
||||||
:value="currentSecColor"
|
:value="secColor"
|
||||||
:select-names="colorNames"
|
:select-names="colorNames"
|
||||||
:select-values="colorValues"
|
:select-values="colorValues"
|
||||||
@change="updateSecColor"
|
@change="updateSecColor"
|
||||||
|
|
|
@ -36,12 +36,12 @@ export default Vue.extend({
|
||||||
return this.$store.getters.getEnableSearchSuggestions
|
return this.$store.getters.getEnableSearchSuggestions
|
||||||
},
|
},
|
||||||
|
|
||||||
searchSettings: function () {
|
searchInput: function () {
|
||||||
return this.$store.getters.getSearchSettings
|
return this.$refs.searchInput.$refs.input
|
||||||
},
|
},
|
||||||
|
|
||||||
isSideNavOpen: function () {
|
searchSettings: function () {
|
||||||
return this.$store.getters.getIsSideNavOpen
|
return this.$store.getters.getSearchSettings
|
||||||
},
|
},
|
||||||
|
|
||||||
barColor: function () {
|
barColor: function () {
|
||||||
|
@ -60,12 +60,16 @@ export default Vue.extend({
|
||||||
return this.$store.getters.getBackendPreference
|
return this.$store.getters.getBackendPreference
|
||||||
},
|
},
|
||||||
|
|
||||||
|
expandSideBar: function () {
|
||||||
|
return this.$store.getters.getExpandSideBar
|
||||||
|
},
|
||||||
|
|
||||||
forwardText: function () {
|
forwardText: function () {
|
||||||
return this.$t('Forward')
|
return this.$t('Forward')
|
||||||
},
|
},
|
||||||
|
|
||||||
backwardText: function () {
|
backwardText: function () {
|
||||||
return this.$t('Backward')
|
return this.$t('Back')
|
||||||
},
|
},
|
||||||
|
|
||||||
newWindowText: function () {
|
newWindowText: function () {
|
||||||
|
@ -80,9 +84,12 @@ export default Vue.extend({
|
||||||
searchContainer.style.display = 'none'
|
searchContainer.style.display = 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localStorage.getItem('expandSideBar') === 'true') {
|
// Store is not up-to-date when the component mounts, so we use timeout.
|
||||||
this.toggleSideNav()
|
setTimeout(() => {
|
||||||
}
|
if (this.expandSideBar) {
|
||||||
|
this.toggleSideNav()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
|
||||||
window.addEventListener('resize', function (event) {
|
window.addEventListener('resize', function (event) {
|
||||||
const width = event.srcElement.innerWidth
|
const width = event.srcElement.innerWidth
|
||||||
|
@ -190,6 +197,10 @@ export default Vue.extend({
|
||||||
this.showFilters = false
|
this.showFilters = false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
focusSearch: function () {
|
||||||
|
this.searchInput.focus()
|
||||||
|
},
|
||||||
|
|
||||||
getSearchSuggestionsDebounce: function (query) {
|
getSearchSuggestionsDebounce: function (query) {
|
||||||
if (this.enableSearchSuggestions) {
|
if (this.enableSearchSuggestions) {
|
||||||
this.debounceSearchResults(query)
|
this.debounceSearchResults(query)
|
||||||
|
|
|
@ -4,12 +4,13 @@
|
||||||
@content
|
@content
|
||||||
|
|
||||||
.topNav
|
.topNav
|
||||||
position: fixed
|
position: sticky
|
||||||
z-index: 4
|
z-index: 4
|
||||||
left: 0
|
left: 0
|
||||||
right: 0
|
right: 0
|
||||||
top: 0
|
top: 0
|
||||||
height: 60px
|
height: 60px
|
||||||
|
width: 100%
|
||||||
line-height: 60px
|
line-height: 60px
|
||||||
background-color: var(--card-bg-color)
|
background-color: var(--card-bg-color)
|
||||||
-webkit-box-shadow: 0px 2px 1px 0px var(--primary-shadow-color)
|
-webkit-box-shadow: 0px 2px 1px 0px var(--primary-shadow-color)
|
||||||
|
@ -24,6 +25,9 @@
|
||||||
@include top-nav-is-colored
|
@include top-nav-is-colored
|
||||||
background-color: var(--primary-color)
|
background-color: var(--primary-color)
|
||||||
|
|
||||||
|
@media only screen and (max-width: 680px)
|
||||||
|
position: fixed
|
||||||
|
|
||||||
.menuIcon // the hamburger button
|
.menuIcon // the hamburger button
|
||||||
@media only screen and (max-width: 680px)
|
@media only screen and (max-width: 680px)
|
||||||
display: none
|
display: none
|
||||||
|
@ -74,6 +78,8 @@
|
||||||
|
|
||||||
.side // parts of the top nav either side of the search bar
|
.side // parts of the top nav either side of the search bar
|
||||||
display: flex
|
display: flex
|
||||||
|
gap: 3px
|
||||||
|
margin: 0 6px
|
||||||
align-items: center
|
align-items: center
|
||||||
|
|
||||||
&.profiles
|
&.profiles
|
||||||
|
@ -158,7 +164,3 @@
|
||||||
left: 0
|
left: 0
|
||||||
right: 0
|
right: 0
|
||||||
margin: 95px 10px 0px
|
margin: 95px 10px 0px
|
||||||
|
|
||||||
@media only screen and (min-width: 681px)
|
|
||||||
&.expand
|
|
||||||
margin-left: 100px
|
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
icon="arrow-left"
|
icon="arrow-left"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
:title="forwardText"
|
:title="backwardText"
|
||||||
@click="historyBack"
|
@click="historyBack"
|
||||||
@keypress="historyBack"
|
@keypress="historyBack"
|
||||||
/>
|
/>
|
||||||
|
@ -66,6 +66,7 @@
|
||||||
<div class="middle">
|
<div class="middle">
|
||||||
<div class="searchContainer">
|
<div class="searchContainer">
|
||||||
<ft-input
|
<ft-input
|
||||||
|
ref="searchInput"
|
||||||
:placeholder="$t('Search / Go to URL')"
|
:placeholder="$t('Search / Go to URL')"
|
||||||
class="searchInput"
|
class="searchInput"
|
||||||
:is-search="true"
|
:is-search="true"
|
||||||
|
@ -89,7 +90,6 @@
|
||||||
<ft-search-filters
|
<ft-search-filters
|
||||||
v-show="showFilters"
|
v-show="showFilters"
|
||||||
class="searchFilters"
|
class="searchFilters"
|
||||||
:class="{ expand: !isSideNavOpen }"
|
|
||||||
@filterValueUpdated="handleSearchFilterValueChanged"
|
@filterValueUpdated="handleSearchFilterValueChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -78,6 +78,12 @@
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.commentMemberIcon {
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.commentLikeCount {
|
.commentLikeCount {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
margin-left: 70px;
|
margin-left: 70px;
|
||||||
|
|
|
@ -269,6 +269,11 @@ export default Vue.extend({
|
||||||
comment.likes = null
|
comment.likes = null
|
||||||
}
|
}
|
||||||
comment.text = autolinker.link(comment.text.replace(/(<(?!br>)([^>]+)>)/ig, ''))
|
comment.text = autolinker.link(comment.text.replace(/(<(?!br>)([^>]+)>)/ig, ''))
|
||||||
|
if (comment.customEmojis.length > 0) {
|
||||||
|
comment.customEmojis.forEach(emoji => {
|
||||||
|
comment.text = comment.text.replace(emoji.text, `<img width="14" height="14" class="commentCustomEmoji" alt="${emoji.text.substring(2, emoji.text.length - 1)}" src="${emoji.emojiThumbnails[0].url}">`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return comment
|
return comment
|
||||||
})
|
})
|
||||||
|
|
|
@ -68,6 +68,14 @@
|
||||||
>
|
>
|
||||||
{{ comment.author }}
|
{{ comment.author }}
|
||||||
</span>
|
</span>
|
||||||
|
<img
|
||||||
|
v-if="comment.isMember"
|
||||||
|
:src="comment.memberIconUrl"
|
||||||
|
:title="$t('Comments.Member')"
|
||||||
|
:aria-label="$t('Comments.Member')"
|
||||||
|
class="commentMemberIcon"
|
||||||
|
alt=""
|
||||||
|
>
|
||||||
<span class="commentDate">
|
<span class="commentDate">
|
||||||
{{ comment.time }}
|
{{ comment.time }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -137,6 +145,12 @@
|
||||||
>
|
>
|
||||||
{{ reply.author }}
|
{{ reply.author }}
|
||||||
</span>
|
</span>
|
||||||
|
<img
|
||||||
|
v-if="reply.isMember"
|
||||||
|
:src="reply.memberIconUrl"
|
||||||
|
class="commentMemberIcon"
|
||||||
|
alt=""
|
||||||
|
>
|
||||||
<span class="commentDate">
|
<span class="commentDate">
|
||||||
{{ reply.time }}
|
{{ reply.time }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: 'Roboto', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,12 +118,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
formatTypeLabel: 'VIDEO FORMATS',
|
formatTypeLabel: 'VIDEO FORMATS'
|
||||||
formatTypeValues: [
|
|
||||||
'dash',
|
|
||||||
'legacy',
|
|
||||||
'audio'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -175,23 +170,33 @@ export default Vue.extend({
|
||||||
return this.inFavoritesPlaylist ? 'base favorite' : 'base'
|
return this.inFavoritesPlaylist ? 'base favorite' : 'base'
|
||||||
},
|
},
|
||||||
|
|
||||||
downloadLinkNames: function () {
|
downloadLinkOptions: function () {
|
||||||
return this.downloadLinks.map((download) => {
|
return this.downloadLinks.map((download) => {
|
||||||
return download.label
|
return {
|
||||||
|
label: download.label,
|
||||||
|
value: download.url
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
downloadLinkValues: function () {
|
downloadBehavior: function () {
|
||||||
return this.downloadLinks.map((download) => {
|
return this.$store.getters.getDownloadBehavior
|
||||||
return download.url
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
formatTypeNames: function () {
|
formatTypeOptions: function () {
|
||||||
return [
|
return [
|
||||||
this.$t('Change Format.Use Dash Formats').toUpperCase(),
|
{
|
||||||
this.$t('Change Format.Use Legacy Formats').toUpperCase(),
|
label: this.$t('Change Format.Use Dash Formats').toUpperCase(),
|
||||||
this.$t('Change Format.Use Audio Formats').toUpperCase()
|
value: 'dash'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Change Format.Use Legacy Formats').toUpperCase(),
|
||||||
|
value: 'legacy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('Change Format.Use Audio Formats').toUpperCase(),
|
||||||
|
value: 'audio'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -287,6 +292,18 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.$watch('$refs.downloadButton.dropdownShown', (dropdownShown) => {
|
||||||
|
this.$parent.infoAreaSticky = !dropdownShown
|
||||||
|
|
||||||
|
if (dropdownShown && window.innerWidth >= 901) {
|
||||||
|
// adds a slight delay so we know that the dropdown has shown up
|
||||||
|
// and won't mess up our scrolling
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
this.$parent.$refs.infoArea.scrollIntoView()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -298,6 +315,7 @@ export default Vue.extend({
|
||||||
watchProgress: this.getTimestamp(),
|
watchProgress: this.getTimestamp(),
|
||||||
playbackRate: this.defaultPlayback,
|
playbackRate: this.defaultPlayback,
|
||||||
videoId: this.id,
|
videoId: this.id,
|
||||||
|
videoLength: this.lengthSeconds,
|
||||||
playlistId: this.playlistId,
|
playlistId: this.playlistId,
|
||||||
playlistIndex: this.getPlaylistIndex(),
|
playlistIndex: this.getPlaylistIndex(),
|
||||||
playlistReverse: this.getPlaylistReverse(),
|
playlistReverse: this.getPlaylistReverse(),
|
||||||
|
@ -409,15 +427,20 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDownload: function (index) {
|
handleDownload: function (index) {
|
||||||
const url = this.downloadLinkValues[index]
|
const selectedDownloadLinkOption = this.downloadLinkOptions[index]
|
||||||
const linkName = this.downloadLinkNames[index]
|
const url = selectedDownloadLinkOption.value
|
||||||
|
const linkName = selectedDownloadLinkOption.label
|
||||||
const extension = this.grabExtensionFromUrl(linkName)
|
const extension = this.grabExtensionFromUrl(linkName)
|
||||||
|
|
||||||
this.downloadMedia({
|
if (this.downloadBehavior === 'open') {
|
||||||
url: url,
|
this.openExternalLink(url)
|
||||||
title: this.title,
|
} else {
|
||||||
extension: extension
|
this.downloadMedia({
|
||||||
})
|
url: url,
|
||||||
|
title: this.title,
|
||||||
|
extension: extension
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
grabExtensionFromUrl: function (url) {
|
grabExtensionFromUrl: function (url) {
|
||||||
|
|
|
@ -95,13 +95,13 @@
|
||||||
/>
|
/>
|
||||||
<ft-icon-button
|
<ft-icon-button
|
||||||
v-if="!isUpcoming && downloadLinks.length > 0"
|
v-if="!isUpcoming && downloadLinks.length > 0"
|
||||||
|
ref="downloadButton"
|
||||||
:title="$t('Video.Download Video')"
|
:title="$t('Video.Download Video')"
|
||||||
class="option"
|
class="option"
|
||||||
theme="secondary"
|
theme="secondary"
|
||||||
icon="download"
|
icon="download"
|
||||||
:return-index="true"
|
:return-index="true"
|
||||||
:dropdown-names="downloadLinkNames"
|
:dropdown-options="downloadLinkOptions"
|
||||||
:dropdown-values="downloadLinkValues"
|
|
||||||
@click="handleDownload"
|
@click="handleDownload"
|
||||||
/>
|
/>
|
||||||
<ft-icon-button
|
<ft-icon-button
|
||||||
|
@ -110,8 +110,7 @@
|
||||||
class="option"
|
class="option"
|
||||||
theme="secondary"
|
theme="secondary"
|
||||||
icon="file-video"
|
icon="file-video"
|
||||||
:dropdown-names="formatTypeNames"
|
:dropdown-options="formatTypeOptions"
|
||||||
:dropdown-values="formatTypeValues"
|
|
||||||
@click="handleFormatChange"
|
@click="handleFormatChange"
|
||||||
/>
|
/>
|
||||||
<ft-share-button
|
<ft-share-button
|
||||||
|
|
|
@ -98,6 +98,17 @@ export default Vue.extend({
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', this.playPreviousVideo)
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', this.playNextVideo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy: function () {
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', null)
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', null)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
goToPlaylist: function () {
|
goToPlaylist: function () {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import App from './App.vue'
|
||||||
import router from './router/index'
|
import router from './router/index'
|
||||||
import store from './store/index'
|
import store from './store/index'
|
||||||
import i18n from './i18n/index'
|
import i18n from './i18n/index'
|
||||||
// import 'material-design-icons/iconfont/material-icons.css'
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { fas } from '@fortawesome/free-solid-svg-icons'
|
import { fas } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faGithub } from '@fortawesome/free-brands-svg-icons/faGithub'
|
import { faGithub } from '@fortawesome/free-brands-svg-icons/faGithub'
|
||||||
|
@ -32,7 +31,7 @@ new Vue({
|
||||||
render: h => h(App)
|
render: h => h(App)
|
||||||
})
|
})
|
||||||
|
|
||||||
// to avoild accesing electorn api from web app build
|
// to avoid accesing electron api from web app build
|
||||||
if (window && window.process && window.process.type === 'renderer') {
|
if (window && window.process && window.process.type === 'renderer') {
|
||||||
const { ipcRenderer } = require('electron')
|
const { ipcRenderer } = require('electron')
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import { DBHistoryHandlers } from '../../../datastores/handlers/index'
|
import { DBHistoryHandlers } from '../../../datastores/handlers/index'
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
historyCache: [],
|
historyCache: []
|
||||||
searchHistoryCache: []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getters = {
|
const getters = {
|
||||||
getHistoryCache: () => {
|
getHistoryCache: () => {
|
||||||
return state.historyCache
|
return state.historyCache
|
||||||
},
|
|
||||||
getSearchHistoryCache: () => {
|
|
||||||
return state.searchHistoryCache
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,15 +47,6 @@ const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async searchHistory({ commit }, query) {
|
|
||||||
try {
|
|
||||||
const results = await DBHistoryHandlers.search(query)
|
|
||||||
commit('setSearchHistoryCache', results)
|
|
||||||
} catch (errMessage) {
|
|
||||||
console.error(errMessage)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateWatchProgress({ commit }, { videoId, watchProgress }) {
|
async updateWatchProgress({ commit }, { videoId, watchProgress }) {
|
||||||
try {
|
try {
|
||||||
await DBHistoryHandlers.updateWatchProgress(videoId, watchProgress)
|
await DBHistoryHandlers.updateWatchProgress(videoId, watchProgress)
|
||||||
|
@ -79,10 +66,6 @@ const mutations = {
|
||||||
state.historyCache = historyCache
|
state.historyCache = historyCache
|
||||||
},
|
},
|
||||||
|
|
||||||
setSearchHistoryCache(state, result) {
|
|
||||||
state.searchHistoryCache = result
|
|
||||||
},
|
|
||||||
|
|
||||||
hoistEntryToTopOfHistoryCache(state, { currentIndex, updatedEntry }) {
|
hoistEntryToTopOfHistoryCache(state, { currentIndex, updatedEntry }) {
|
||||||
state.historyCache.splice(currentIndex, 1)
|
state.historyCache.splice(currentIndex, 1)
|
||||||
state.historyCache.unshift(updatedEntry)
|
state.historyCache.unshift(updatedEntry)
|
||||||
|
|
|
@ -13,20 +13,14 @@ const state = {
|
||||||
removeOnWatched: true,
|
removeOnWatched: true,
|
||||||
videos: []
|
videos: []
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
searchPlaylistCache: {
|
|
||||||
videos: []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getters = {
|
const getters = {
|
||||||
getAllPlaylists: () => state.playlists,
|
getAllPlaylists: () => state.playlists,
|
||||||
getFavorites: () => state.playlists[0],
|
getFavorites: () => state.playlists[0],
|
||||||
getPlaylist: (playlistId) => state.playlists.find(playlist => playlist._id === playlistId),
|
getPlaylist: (playlistId) => state.playlists.find(playlist => playlist._id === playlistId),
|
||||||
getWatchLater: () => state.playlists[1],
|
getWatchLater: () => state.playlists[1]
|
||||||
getSearchPlaylistCache: () => {
|
|
||||||
return state.searchPlaylistCache
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
@ -136,25 +130,10 @@ const actions = {
|
||||||
} catch (errMessage) {
|
} catch (errMessage) {
|
||||||
console.error(errMessage)
|
console.error(errMessage)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
async searchFavoritePlaylist({ commit }, query) {
|
|
||||||
const re = new RegExp(query, 'i')
|
|
||||||
// filtering in the frontend because the documents are the playlists and not the videos
|
|
||||||
const results = state.playlists[0].videos.slice()
|
|
||||||
.filter((video) => {
|
|
||||||
return video.author.match(re) ||
|
|
||||||
video.title.match(re)
|
|
||||||
})
|
|
||||||
commit('setPlaylistCache', results)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
setPlaylistCache(state, result) {
|
|
||||||
state.searchPlaylistCache = {
|
|
||||||
videos: result
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addPlaylist(state, payload) {
|
addPlaylist(state, payload) {
|
||||||
state.playlists.push(payload)
|
state.playlists.push(payload)
|
||||||
},
|
},
|
||||||
|
|
|
@ -89,6 +89,31 @@ const actions = {
|
||||||
commit('setProfileList', profiles)
|
commit('setProfileList', profiles)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async updateSubscriptionDetails({ getters, dispatch }, { channelThumbnailUrl, channelName, channelId }) {
|
||||||
|
const thumbnail = channelThumbnailUrl?.replace(/=s\d*/, '=s176') ?? null // change thumbnail size if different
|
||||||
|
const profileList = getters.getProfileList
|
||||||
|
for (const profile of profileList) {
|
||||||
|
const currentProfileCopy = JSON.parse(JSON.stringify(profile))
|
||||||
|
const channel = currentProfileCopy.subscriptions.find((channel) => {
|
||||||
|
return channel.id === channelId
|
||||||
|
}) ?? null
|
||||||
|
if (channel === null) { continue }
|
||||||
|
let updated = false
|
||||||
|
if (channel.name !== channelName || (channel.thumbnail !== thumbnail && thumbnail !== null)) {
|
||||||
|
if (thumbnail !== null) {
|
||||||
|
channel.thumbnail = thumbnail
|
||||||
|
}
|
||||||
|
channel.name = channelName
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
await dispatch('updateProfile', currentProfileCopy)
|
||||||
|
} else { // channel has not been updated, stop iterating through profiles
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async createProfile({ commit }, profile) {
|
async createProfile({ commit }, profile) {
|
||||||
try {
|
try {
|
||||||
const newProfile = await DBProfileHandlers.create(profile)
|
const newProfile = await DBProfileHandlers.create(profile)
|
||||||
|
|
|
@ -167,7 +167,9 @@ const state = {
|
||||||
barColor: false,
|
barColor: false,
|
||||||
checkForBlogPosts: true,
|
checkForBlogPosts: true,
|
||||||
checkForUpdates: true,
|
checkForUpdates: true,
|
||||||
// currentTheme: 'lightRed',
|
baseTheme: 'system',
|
||||||
|
mainColor: 'Red',
|
||||||
|
secColor: 'Blue',
|
||||||
defaultCaptionSettings: '{}',
|
defaultCaptionSettings: '{}',
|
||||||
defaultInterval: 5,
|
defaultInterval: 5,
|
||||||
defaultPlayback: 1,
|
defaultPlayback: 1,
|
||||||
|
@ -185,6 +187,7 @@ const state = {
|
||||||
externalPlayerExecutable: '',
|
externalPlayerExecutable: '',
|
||||||
externalPlayerIgnoreWarnings: false,
|
externalPlayerIgnoreWarnings: false,
|
||||||
externalPlayerCustomArgs: '',
|
externalPlayerCustomArgs: '',
|
||||||
|
expandSideBar: false,
|
||||||
forceLocalBackendForLegacy: false,
|
forceLocalBackendForLegacy: false,
|
||||||
hideActiveSubscriptions: false,
|
hideActiveSubscriptions: false,
|
||||||
hideChannelSubscriptions: false,
|
hideChannelSubscriptions: false,
|
||||||
|
@ -200,6 +203,7 @@ const state = {
|
||||||
hideLabelsSideBar: false,
|
hideLabelsSideBar: false,
|
||||||
landingPage: 'subscriptions',
|
landingPage: 'subscriptions',
|
||||||
listType: 'grid',
|
listType: 'grid',
|
||||||
|
maxVideoPlaybackRate: 3,
|
||||||
playNextVideo: false,
|
playNextVideo: false,
|
||||||
proxyHostname: '127.0.0.1',
|
proxyHostname: '127.0.0.1',
|
||||||
proxyPort: '9050',
|
proxyPort: '9050',
|
||||||
|
@ -211,13 +215,53 @@ const state = {
|
||||||
saveWatchedProgress: true,
|
saveWatchedProgress: true,
|
||||||
sponsorBlockShowSkippedToast: true,
|
sponsorBlockShowSkippedToast: true,
|
||||||
sponsorBlockUrl: 'https://sponsor.ajay.app',
|
sponsorBlockUrl: 'https://sponsor.ajay.app',
|
||||||
|
sponsorBlockSponsor: {
|
||||||
|
color: 'Blue',
|
||||||
|
skip: 'autoSkip'
|
||||||
|
},
|
||||||
|
sponsorBlockSelfPromo: {
|
||||||
|
color: 'Yellow',
|
||||||
|
skip: 'showInSeekBar'
|
||||||
|
},
|
||||||
|
sponsorBlockInteraction: {
|
||||||
|
color: 'Green',
|
||||||
|
skip: 'showInSeekBar'
|
||||||
|
},
|
||||||
|
sponsorBlockIntro: {
|
||||||
|
color: 'Orange',
|
||||||
|
skip: 'doNothing'
|
||||||
|
},
|
||||||
|
sponsorBlockOutro: {
|
||||||
|
color: 'Orange',
|
||||||
|
skip: 'doNothing'
|
||||||
|
},
|
||||||
|
sponsorBlockRecap: {
|
||||||
|
color: 'Orange',
|
||||||
|
skip: 'doNothing'
|
||||||
|
},
|
||||||
|
sponsorBlockMusicOffTopic: {
|
||||||
|
color: 'Orange',
|
||||||
|
skip: 'doNothing'
|
||||||
|
},
|
||||||
|
sponsorBlockFiller: {
|
||||||
|
color: 'Orange',
|
||||||
|
skip: 'doNothing'
|
||||||
|
},
|
||||||
thumbnailPreference: '',
|
thumbnailPreference: '',
|
||||||
useProxy: false,
|
useProxy: false,
|
||||||
useRssFeeds: false,
|
useRssFeeds: false,
|
||||||
useSponsorBlock: false,
|
useSponsorBlock: false,
|
||||||
videoVolumeMouseScroll: false,
|
videoVolumeMouseScroll: false,
|
||||||
videoPlaybackRateMouseScroll: false,
|
videoPlaybackRateMouseScroll: false,
|
||||||
downloadFolderPath: ''
|
videoPlaybackRateInterval: 0.25,
|
||||||
|
downloadFolderPath: '',
|
||||||
|
downloadBehavior: 'download',
|
||||||
|
enableScreenshot: false,
|
||||||
|
screenshotFormat: 'png',
|
||||||
|
screenshotQuality: 95,
|
||||||
|
screenshotAskPath: false,
|
||||||
|
screenshotFolderPath: '',
|
||||||
|
screenshotFilenamePattern: '%Y%M%D-%H%N%S'
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateWithSideEffects = {
|
const stateWithSideEffects = {
|
||||||
|
@ -228,12 +272,31 @@ const stateWithSideEffects = {
|
||||||
|
|
||||||
let targetLocale = value
|
let targetLocale = value
|
||||||
if (value === 'system') {
|
if (value === 'system') {
|
||||||
const systemLocale = await dispatch('getSystemLocale')
|
const systemLocaleName = (await dispatch('getSystemLocale')).replace('-', '_') // ex: en_US
|
||||||
|
const systemLocaleLang = systemLocaleName.split('_')[0] // ex: en
|
||||||
targetLocale = Object.keys(i18n.messages).find((locale) => {
|
const targetLocaleOptions = Object.keys(i18n.messages).filter((locale) => { // filter out other languages
|
||||||
const localeName = locale.replace('-', '_')
|
const localeLang = locale.replace('-', '_').split('_')[0]
|
||||||
return localeName.includes(systemLocale.replace('-', '_'))
|
return localeLang.includes(systemLocaleLang)
|
||||||
|
}).sort((a, b) => {
|
||||||
|
const aLocaleName = a.replace('-', '_')
|
||||||
|
const bLocaleName = b.replace('-', '_')
|
||||||
|
const aLocale = aLocaleName.split('_') // ex: [en, US]
|
||||||
|
const bLocale = bLocaleName.split('_')
|
||||||
|
if (aLocale.includes(systemLocaleName)) { // country & language match, prefer a
|
||||||
|
return -1
|
||||||
|
} else if (bLocale.includes(systemLocaleName)) { // country & language match, prefer b
|
||||||
|
return 1
|
||||||
|
} else if (aLocale.length === 1) { // no country code for a, prefer a
|
||||||
|
return -1
|
||||||
|
} else if (bLocale.length === 1) { // no country code for b, prefer b
|
||||||
|
return 1
|
||||||
|
} else { // a & b have different country code from system, sort alphabetically
|
||||||
|
return aLocaleName.localeCompare(bLocaleName)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
if (targetLocaleOptions.length > 0) {
|
||||||
|
targetLocale = targetLocaleOptions[0]
|
||||||
|
}
|
||||||
|
|
||||||
// Go back to default value if locale is unavailable
|
// Go back to default value if locale is unavailable
|
||||||
if (!targetLocale) {
|
if (!targetLocale) {
|
||||||
|
@ -322,7 +385,9 @@ const customActions = {
|
||||||
dispatch(defaultSideEffectsTriggerId(_id), value)
|
dispatch(defaultSideEffectsTriggerId(_id), value)
|
||||||
}
|
}
|
||||||
|
|
||||||
commit(defaultMutationId(_id), value)
|
if (Object.keys(mutations).includes(defaultMutationId(_id))) {
|
||||||
|
commit(defaultMutationId(_id), value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (errMessage) {
|
} catch (errMessage) {
|
||||||
console.error(errMessage)
|
console.error(errMessage)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import forge from 'node-forge'
|
|
||||||
|
|
||||||
const state = {}
|
const state = {}
|
||||||
const getters = {}
|
const getters = {}
|
||||||
|
@ -7,22 +6,30 @@ const getters = {}
|
||||||
const actions = {
|
const actions = {
|
||||||
sponsorBlockSkipSegments ({ rootState }, { videoId, categories }) {
|
sponsorBlockSkipSegments ({ rootState }, { videoId, categories }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const messageDigestSha256 = forge.md.sha256.create()
|
const videoIdBuffer = new TextEncoder().encode(videoId)
|
||||||
messageDigestSha256.update(videoId)
|
|
||||||
const videoIdHashPrefix = messageDigestSha256.digest().toHex().substring(0, 4)
|
|
||||||
const requestUrl = `${rootState.settings.sponsorBlockUrl}/api/skipSegments/${videoIdHashPrefix}?categories=${JSON.stringify(categories)}`
|
|
||||||
|
|
||||||
$.getJSON(requestUrl, (response) => {
|
crypto.subtle.digest('SHA-256', videoIdBuffer).then((hashBuffer) => {
|
||||||
const segments = response
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||||
.filter((result) => result.videoID === videoId)
|
|
||||||
.flatMap((result) => result.segments)
|
const videoIdHashPrefix = hashArray
|
||||||
resolve(segments)
|
.map(byte => byte.toString(16).padStart(2, '0'))
|
||||||
}).fail((xhr, textStatus, error) => {
|
.slice(0, 4)
|
||||||
console.log(xhr)
|
.join('')
|
||||||
console.log(textStatus)
|
|
||||||
console.log(requestUrl)
|
const requestUrl = `${rootState.settings.sponsorBlockUrl}/api/skipSegments/${videoIdHashPrefix}?categories=${JSON.stringify(categories)}`
|
||||||
console.log(error)
|
|
||||||
reject(xhr)
|
$.getJSON(requestUrl, (response) => {
|
||||||
|
const segments = response
|
||||||
|
.filter((result) => result.videoID === videoId)
|
||||||
|
.flatMap((result) => result.segments)
|
||||||
|
resolve(segments)
|
||||||
|
}).fail((xhr, textStatus, error) => {
|
||||||
|
console.log(xhr)
|
||||||
|
console.log(textStatus)
|
||||||
|
console.log(requestUrl)
|
||||||
|
console.log(error)
|
||||||
|
reject(xhr)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@ const state = {
|
||||||
allSubscriptionsList: [],
|
allSubscriptionsList: [],
|
||||||
profileSubscriptions: {
|
profileSubscriptions: {
|
||||||
activeProfile: MAIN_PROFILE_ID,
|
activeProfile: MAIN_PROFILE_ID,
|
||||||
videoList: []
|
videoList: [],
|
||||||
|
errorChannels: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import IsEqual from 'lodash.isequal'
|
import IsEqual from 'lodash.isequal'
|
||||||
import FtToastEvents from '../../components/ft-toast/ft-toast-events'
|
import FtToastEvents from '../../components/ft-toast/ft-toast-events'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
import i18n from '../../i18n/index'
|
import i18n from '../../i18n/index'
|
||||||
|
|
||||||
import { IpcChannels } from '../../../constants'
|
import { IpcChannels } from '../../../constants'
|
||||||
import { ipcRenderer } from 'electron'
|
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
isSideNavOpen: false,
|
isSideNavOpen: false,
|
||||||
|
@ -27,30 +27,30 @@ const state = {
|
||||||
type: 'all',
|
type: 'all',
|
||||||
duration: ''
|
duration: ''
|
||||||
},
|
},
|
||||||
colorClasses: [
|
colorNames: [
|
||||||
'mainRed',
|
'Red',
|
||||||
'mainPink',
|
'Pink',
|
||||||
'mainPurple',
|
'Purple',
|
||||||
'mainDeepPurple',
|
'DeepPurple',
|
||||||
'mainIndigo',
|
'Indigo',
|
||||||
'mainBlue',
|
'Blue',
|
||||||
'mainLightBlue',
|
'LightBlue',
|
||||||
'mainCyan',
|
'Cyan',
|
||||||
'mainTeal',
|
'Teal',
|
||||||
'mainGreen',
|
'Green',
|
||||||
'mainLightGreen',
|
'LightGreen',
|
||||||
'mainLime',
|
'Lime',
|
||||||
'mainYellow',
|
'Yellow',
|
||||||
'mainAmber',
|
'Amber',
|
||||||
'mainOrange',
|
'Orange',
|
||||||
'mainDeepOrange',
|
'DeepOrange',
|
||||||
'mainDraculaCyan',
|
'DraculaCyan',
|
||||||
'mainDraculaGreen',
|
'DraculaGreen',
|
||||||
'mainDraculaOrange',
|
'DraculaOrange',
|
||||||
'mainDraculaPink',
|
'DraculaPink',
|
||||||
'mainDraculaPurple',
|
'DraculaPurple',
|
||||||
'mainDraculaRed',
|
'DraculaRed',
|
||||||
'mainDraculaYellow'
|
'DraculaYellow'
|
||||||
],
|
],
|
||||||
colorValues: [
|
colorValues: [
|
||||||
'#d50000',
|
'#d50000',
|
||||||
|
@ -107,6 +107,10 @@ const getters = {
|
||||||
return state.searchSettings
|
return state.searchSettings
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getColorNames () {
|
||||||
|
return state.colorNames
|
||||||
|
},
|
||||||
|
|
||||||
getColorValues () {
|
getColorValues () {
|
||||||
return state.colorValues
|
return state.colorValues
|
||||||
},
|
},
|
||||||
|
@ -177,8 +181,41 @@ const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
replaceFilenameForbiddenChars(_, filenameOriginal) {
|
||||||
|
let filenameNew = filenameOriginal
|
||||||
|
let forbiddenChars = {}
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'win32':
|
||||||
|
forbiddenChars = {
|
||||||
|
'<': '<', // U+FF1C
|
||||||
|
'>': '>', // U+FF1E
|
||||||
|
':': ':', // U+FF1A
|
||||||
|
'"': '"', // U+FF02
|
||||||
|
'/': '/', // U+FF0F
|
||||||
|
'\\': '\', // U+FF3C
|
||||||
|
'|': '|', // U+FF5C
|
||||||
|
'?': '?', // U+FF1F
|
||||||
|
'*': '*' // U+FF0A
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'darwin':
|
||||||
|
forbiddenChars = { '/': '/', ':': ':' }
|
||||||
|
break
|
||||||
|
case 'linux':
|
||||||
|
forbiddenChars = { '/': '/' }
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const forbiddenChar in forbiddenChars) {
|
||||||
|
filenameNew = filenameNew.replaceAll(forbiddenChar, forbiddenChars[forbiddenChar])
|
||||||
|
}
|
||||||
|
return filenameNew
|
||||||
|
},
|
||||||
|
|
||||||
async downloadMedia({ rootState, dispatch }, { url, title, extension, fallingBackPath }) {
|
async downloadMedia({ rootState, dispatch }, { url, title, extension, fallingBackPath }) {
|
||||||
const fileName = `${title}.${extension}`
|
const fileName = `${await dispatch('replaceFilenameForbiddenChars', title)}.${extension}`
|
||||||
const usingElectron = rootState.settings.usingElectron
|
const usingElectron = rootState.settings.usingElectron
|
||||||
const locale = i18n._vm.locale
|
const locale = i18n._vm.locale
|
||||||
const translations = i18n._vm.messages[locale]
|
const translations = i18n._vm.messages[locale]
|
||||||
|
@ -197,11 +234,12 @@ const actions = {
|
||||||
defaultPath: fileName,
|
defaultPath: fileName,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
|
name: extension.toUpperCase(),
|
||||||
extensions: [extension]
|
extensions: [extension]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const response = await dispatch('showSaveDialog', options)
|
const response = await dispatch('showSaveDialog', { options })
|
||||||
|
|
||||||
if (response.canceled || response.filePath === '') {
|
if (response.canceled || response.filePath === '') {
|
||||||
// User canceled the save dialog
|
// User canceled the save dialog
|
||||||
|
@ -209,6 +247,19 @@ const actions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
folderPath = response.filePath
|
folderPath = response.filePath
|
||||||
|
} else {
|
||||||
|
if (!fs.existsSync(folderPath)) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(folderPath, { recursive: true })
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
this.showToast({
|
||||||
|
message: err
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
folderPath = path.join(folderPath, fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch('showToast', {
|
dispatch('showToast', {
|
||||||
|
@ -223,8 +274,6 @@ const actions = {
|
||||||
})
|
})
|
||||||
|
|
||||||
const reader = response.body.getReader()
|
const reader = response.body.getReader()
|
||||||
const contentLength = response.headers.get('Content-Length')
|
|
||||||
let receivedLength = 0
|
|
||||||
const chunks = []
|
const chunks = []
|
||||||
|
|
||||||
const handleError = (err) => {
|
const handleError = (err) => {
|
||||||
|
@ -240,9 +289,10 @@ const actions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks.push(value)
|
chunks.push(value)
|
||||||
receivedLength += value.length
|
|
||||||
// Can be used in the future to determine download percentage
|
// Can be used in the future to determine download percentage
|
||||||
const percentage = receivedLength / contentLength
|
// const contentLength = response.headers.get('Content-Length')
|
||||||
|
// const receivedLength = value.length
|
||||||
|
// const percentage = receivedLength / contentLength
|
||||||
await reader.read().then(processText).catch(handleError)
|
await reader.read().then(processText).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,10 +331,10 @@ const actions = {
|
||||||
return await invokeIRC(context, IpcChannels.SHOW_OPEN_DIALOG, webCbk, options)
|
return await invokeIRC(context, IpcChannels.SHOW_OPEN_DIALOG, webCbk, options)
|
||||||
},
|
},
|
||||||
|
|
||||||
async showSaveDialog (context, options) {
|
async showSaveDialog (context, { options, useModal = false }) {
|
||||||
// TODO: implement showSaveDialog web compatible callback
|
// TODO: implement showSaveDialog web compatible callback
|
||||||
const webCbk = () => null
|
const webCbk = () => null
|
||||||
return await invokeIRC(context, IpcChannels.SHOW_SAVE_DIALOG, webCbk, options)
|
return await invokeIRC(context, IpcChannels.SHOW_SAVE_DIALOG, webCbk, { options, useModal })
|
||||||
},
|
},
|
||||||
|
|
||||||
async getUserDataPath (context) {
|
async getUserDataPath (context) {
|
||||||
|
@ -293,13 +343,73 @@ const actions = {
|
||||||
return await invokeIRC(context, IpcChannels.GET_USER_DATA_PATH, webCbk)
|
return await invokeIRC(context, IpcChannels.GET_USER_DATA_PATH, webCbk)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getPicturesPath (context) {
|
||||||
|
const webCbk = () => null
|
||||||
|
return await invokeIRC(context, IpcChannels.GET_PICTURES_PATH, webCbk)
|
||||||
|
},
|
||||||
|
|
||||||
|
parseScreenshotCustomFileName: function({ rootState }, payload) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { pattern = rootState.settings.screenshotFilenamePattern, date, playerTime, videoId } = payload
|
||||||
|
const keywords = [
|
||||||
|
['%Y', date.getFullYear()], // year 4 digits
|
||||||
|
['%M', (date.getMonth() + 1).toString().padStart(2, '0')], // month 2 digits
|
||||||
|
['%D', date.getDate().toString().padStart(2, '0')], // day 2 digits
|
||||||
|
['%H', date.getHours().toString().padStart(2, '0')], // hour 2 digits
|
||||||
|
['%N', date.getMinutes().toString().padStart(2, '0')], // minute 2 digits
|
||||||
|
['%S', date.getSeconds().toString().padStart(2, '0')], // second 2 digits
|
||||||
|
['%T', date.getMilliseconds().toString().padStart(3, '0')], // millisecond 3 digits
|
||||||
|
['%s', parseInt(playerTime)], // video position second n digits
|
||||||
|
['%t', (playerTime % 1).toString().slice(2, 5) || '000'], // video position millisecond 3 digits
|
||||||
|
['%i', videoId] // video id
|
||||||
|
]
|
||||||
|
|
||||||
|
let parsedString = pattern
|
||||||
|
for (const [key, value] of keywords) {
|
||||||
|
parsedString = parsedString.replaceAll(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = process.platform
|
||||||
|
if (platform === 'win32') {
|
||||||
|
// https://www.boost.org/doc/libs/1_78_0/libs/filesystem/doc/portability_guide.htm
|
||||||
|
// https://stackoverflow.com/questions/1976007/
|
||||||
|
const noForbiddenChars = ['<', '>', ':', '"', '/', '|', '?', '*'].every(char => {
|
||||||
|
return parsedString.indexOf(char) === -1
|
||||||
|
})
|
||||||
|
if (!noForbiddenChars) {
|
||||||
|
reject(new Error('Forbidden Characters')) // use message as translation key
|
||||||
|
}
|
||||||
|
} else if (platform === 'darwin') {
|
||||||
|
// https://superuser.com/questions/204287/
|
||||||
|
if (parsedString.indexOf(':') !== -1) {
|
||||||
|
reject(new Error('Forbidden Characters'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirChar = platform === 'win32' ? '\\' : '/'
|
||||||
|
let filename
|
||||||
|
if (parsedString.indexOf(dirChar) !== -1) {
|
||||||
|
const lastIndex = parsedString.lastIndexOf(dirChar)
|
||||||
|
filename = parsedString.substring(lastIndex + 1)
|
||||||
|
} else {
|
||||||
|
filename = parsedString
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filename) {
|
||||||
|
reject(new Error('Empty File Name'))
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(parsedString)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
updateShowProgressBar ({ commit }, value) {
|
updateShowProgressBar ({ commit }, value) {
|
||||||
commit('setShowProgressBar', value)
|
commit('setShowProgressBar', value)
|
||||||
},
|
},
|
||||||
|
|
||||||
getRandomColorClass () {
|
getRandomColorClass () {
|
||||||
const randomInt = Math.floor(Math.random() * state.colorClasses.length)
|
const randomInt = Math.floor(Math.random() * state.colorNames.length)
|
||||||
return state.colorClasses[randomInt]
|
return 'main' + state.colorNames[randomInt]
|
||||||
},
|
},
|
||||||
|
|
||||||
getRandomColor () {
|
getRandomColor () {
|
||||||
|
@ -832,7 +942,7 @@ const actions = {
|
||||||
args.push(...defaultCustomArguments)
|
args.push(...defaultCustomArguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.watchProgress > 0) {
|
if (payload.watchProgress > 0 && payload.watchProgress < payload.videoLength - 10) {
|
||||||
if (typeof cmdArgs.startOffset === 'string') {
|
if (typeof cmdArgs.startOffset === 'string') {
|
||||||
args.push(`${cmdArgs.startOffset}${payload.watchProgress}`)
|
args.push(`${cmdArgs.startOffset}${payload.watchProgress}`)
|
||||||
} else {
|
} else {
|
||||||
|
@ -973,7 +1083,7 @@ const mutations = {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (sameSearch !== -1) {
|
if (sameSearch !== -1) {
|
||||||
state.sessionSearchHistory[sameSearch].data = state.sessionSearchHistory[sameSearch].data.concat(payload.data)
|
state.sessionSearchHistory[sameSearch].data = payload.data
|
||||||
state.sessionSearchHistory[sameSearch].nextPageRef = payload.nextPageRef
|
state.sessionSearchHistory[sameSearch].nextPageRef = payload.nextPageRef
|
||||||
} else {
|
} else {
|
||||||
state.sessionSearchHistory.push(payload)
|
state.sessionSearchHistory.push(payload)
|
||||||
|
@ -984,7 +1094,7 @@ const mutations = {
|
||||||
state.popularCache = value
|
state.popularCache = value
|
||||||
},
|
},
|
||||||
|
|
||||||
setTrendingCache (state, value, page) {
|
setTrendingCache (state, { value, page }) {
|
||||||
state.trendingCache[page] = value
|
state.trendingCache[page] = value
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||||
import { HttpProxyAgent } from 'http-proxy-agent'
|
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||||
|
|
||||||
|
import i18n from '../../i18n/index'
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
isYtSearchRunning: false
|
isYtSearchRunning: false
|
||||||
}
|
}
|
||||||
|
@ -288,10 +290,15 @@ const actions = {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const locale = settings.currentLocale.replace('-', '_')
|
let locale = i18n.locale.replace('_', '-')
|
||||||
|
|
||||||
|
if (locale === 'nn') {
|
||||||
|
locale = 'no'
|
||||||
|
}
|
||||||
|
|
||||||
ytpl(playlistId, {
|
ytpl(playlistId, {
|
||||||
hl: locale,
|
hl: locale,
|
||||||
limit: 'Infinity',
|
limit: Infinity,
|
||||||
requestOptions: { agent }
|
requestOptions: { agent }
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
resolve(result)
|
resolve(result)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.light {
|
.system[data-system-theme*='light'], .light {
|
||||||
--primary-text-color: #212121;
|
--primary-text-color: #212121;
|
||||||
--secondary-text-color: #424242;
|
--secondary-text-color: #424242;
|
||||||
--tertiary-text-color: #757575;
|
--tertiary-text-color: #757575;
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
--logo-text: url("~../../_icons/textColorSmall.png");
|
--logo-text: url("~../../_icons/textColorSmall.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.system[data-system-theme*='dark'], .dark {
|
||||||
--primary-text-color: #EEEEEE;
|
--primary-text-color: #EEEEEE;
|
||||||
--secondary-text-color: #ddd;
|
--secondary-text-color: #ddd;
|
||||||
--tertiary-text-color: #999;
|
--tertiary-text-color: #999;
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
--secondary-card-bg-color: rgba(0, 0, 0, 0.75);
|
--secondary-card-bg-color: rgba(0, 0, 0, 0.75);
|
||||||
--scrollbar-color: #515151;
|
--scrollbar-color: #515151;
|
||||||
--scrollbar-color-hover: #424242;
|
--scrollbar-color-hover: #424242;
|
||||||
--side-nav-color: #000000;
|
--side-nav-color: #0f0f0f;
|
||||||
--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;
|
||||||
|
@ -619,6 +619,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
--red-500: #f44336;
|
--red-500: #f44336;
|
||||||
|
|
|
@ -350,7 +350,7 @@
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
word-break: initial;
|
word-break: initial;
|
||||||
}
|
}
|
||||||
.video-js:-moz-full-screen {
|
.video-js:-moz-full-screen {
|
||||||
|
@ -490,6 +490,16 @@ body.vjs-full-window {
|
||||||
content: url(assets/img/close_theatre.svg)
|
content: url(assets/img/close_theatre.svg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vjs-icon-screenshot {
|
||||||
|
margin-top: 3px;
|
||||||
|
padding-top: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-icon-screenshot::before {
|
||||||
|
content: url(assets/img/camera.svg)
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 1350px) {
|
@media only screen and (max-width: 1350px) {
|
||||||
.videoPlayer .vjs-button-theatre {
|
.videoPlayer .vjs-button-theatre {
|
||||||
display: none
|
display: none
|
||||||
|
@ -1023,7 +1033,7 @@ body.vjs-full-window {
|
||||||
padding: 6px 8px 8px 8px;
|
padding: 6px 8px 8px 8px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -3.4em;
|
top: -2.7em;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
@ -2136,7 +2146,8 @@ video::-webkit-media-text-track-display {
|
||||||
.video-js .vjs-vtt-thumbnail-display {
|
.video-js .vjs-vtt-thumbnail-display {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transition: transform .1s, opacity .2s;
|
transition: transform .1s, opacity .2s;
|
||||||
bottom: 20px;
|
/* `bottom` was 20px, to avoid obstruction by time tooltip updated to current value */
|
||||||
|
bottom: 56px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
box-shadow: 0 0 7px rgba(0,0,0,.6);
|
box-shadow: 0 0 7px rgba(0,0,0,.6);
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
@ -2160,3 +2171,24 @@ video::-webkit-media-text-track-display {
|
||||||
font-size: xx-large;
|
font-size: xx-large;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vjs-modal-dialog.statsModal {
|
||||||
|
line-height: 10px;
|
||||||
|
width: 550px;
|
||||||
|
height: 225px;
|
||||||
|
font-size: 10px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-modal-dialog.statsModal p {
|
||||||
|
line-height: 10px;
|
||||||
|
position:relative;
|
||||||
|
bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 775px) {
|
||||||
|
.vjs-modal-dialog.statsModal {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,26 +2,32 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 85%;
|
width: 85%;
|
||||||
margin: 0 auto 20px;
|
margin: 0 auto 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channelBanner {
|
.channelDetails {
|
||||||
width: 100%;
|
padding: 0 0 16px;
|
||||||
max-height: 200px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.defaultChannelBanner {
|
.channelBannerContainer {
|
||||||
|
background: center / cover no-repeat var(--banner-url, transparent);
|
||||||
|
height: 13vw;
|
||||||
|
min-height: 110px;
|
||||||
|
max-height: 32vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 200px;
|
}
|
||||||
height:200px;
|
|
||||||
background-color: black;
|
.channelBannerContainer.default {
|
||||||
background-image: url("images/defaultBanner.png");
|
background-image: url("images/defaultBanner.png");
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channelInfoContainer {
|
.channelInfoContainer {
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: var(--card-bg-color);
|
background-color: var(--card-bg-color);
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
padding: 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channelInfo {
|
.channelInfo {
|
||||||
|
@ -102,7 +108,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.aboutInfo {
|
.aboutInfo {
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: 'Roboto', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
@ -143,3 +149,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channelLineContainer h1,
|
||||||
|
.channelLineContainer p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ export default Vue.extend({
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
shownElementList: [],
|
shownElementList: [],
|
||||||
apiUsed: '',
|
apiUsed: '',
|
||||||
|
errorMessage: '',
|
||||||
videoSelectValues: [
|
videoSelectValues: [
|
||||||
'newest',
|
'newest',
|
||||||
'oldest',
|
'oldest',
|
||||||
|
@ -90,16 +91,14 @@ export default Vue.extend({
|
||||||
return this.$store.getters.getActiveProfile
|
return this.$store.getters.getActiveProfile
|
||||||
},
|
},
|
||||||
|
|
||||||
isSubscribed: function () {
|
subscriptionInfo: function () {
|
||||||
const subIndex = this.activeProfile.subscriptions.findIndex((channel) => {
|
return this.activeProfile.subscriptions.find((channel) => {
|
||||||
return channel.id === this.id
|
return channel.id === this.id
|
||||||
})
|
}) ?? null
|
||||||
|
},
|
||||||
|
|
||||||
if (subIndex === -1) {
|
isSubscribed: function () {
|
||||||
return false
|
return this.subscriptionInfo !== null
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
subscribedText: function () {
|
subscribedText: function () {
|
||||||
|
@ -249,25 +248,40 @@ export default Vue.extend({
|
||||||
|
|
||||||
getChannelInfoLocal: function () {
|
getChannelInfoLocal: function () {
|
||||||
this.apiUsed = 'local'
|
this.apiUsed = 'local'
|
||||||
ytch.getChannelInfo(this.id).then((response) => {
|
const expectedId = this.id
|
||||||
this.id = response.authorId
|
ytch.getChannelInfo({ channelId: expectedId }).then((response) => {
|
||||||
this.channelName = response.author
|
if (response.alertMessage) {
|
||||||
|
this.setErrorMessage(response.alertMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.errorMessage = ''
|
||||||
|
if (expectedId !== this.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelId = response.authorId
|
||||||
|
const channelName = response.author
|
||||||
|
const channelThumbnailUrl = response.authorThumbnails[2].url
|
||||||
|
this.id = channelId
|
||||||
|
this.channelName = channelName
|
||||||
document.title = `${this.channelName} - ${process.env.PRODUCT_NAME}`
|
document.title = `${this.channelName} - ${process.env.PRODUCT_NAME}`
|
||||||
if (this.hideChannelSubscriptions || response.subscriberCount === 0) {
|
if (this.hideChannelSubscriptions || response.subscriberCount === 0) {
|
||||||
this.subCount = null
|
this.subCount = null
|
||||||
} else {
|
} else {
|
||||||
this.subCount = response.subscriberCount.toFixed(0)
|
this.subCount = response.subscriberCount.toFixed(0)
|
||||||
}
|
}
|
||||||
this.thumbnailUrl = response.authorThumbnails[2].url
|
this.thumbnailUrl = channelThumbnailUrl
|
||||||
|
this.updateSubscriptionDetails({ channelThumbnailUrl, channelName, channelId })
|
||||||
this.channelDescription = autolinker.link(response.description)
|
this.channelDescription = autolinker.link(response.description)
|
||||||
this.relatedChannels = response.relatedChannels.items
|
this.relatedChannels = response.relatedChannels.items
|
||||||
this.relatedChannels.forEach(relatedChannel => {
|
this.relatedChannels.forEach(relatedChannel => {
|
||||||
relatedChannel.authorThumbnails.map(thumbnail => {
|
relatedChannel.thumbnail.map(thumbnail => {
|
||||||
if (!thumbnail.url.includes('https')) {
|
if (!thumbnail.url.includes('https')) {
|
||||||
thumbnail.url = `https:${thumbnail.url}`
|
thumbnail.url = `https:${thumbnail.url}`
|
||||||
}
|
}
|
||||||
return thumbnail
|
return thumbnail
|
||||||
})
|
})
|
||||||
|
relatedChannel.authorThumbnails = relatedChannel.thumbnail
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.authorBanners !== null) {
|
if (response.authorBanners !== null) {
|
||||||
|
@ -306,7 +320,12 @@ export default Vue.extend({
|
||||||
|
|
||||||
getChannelVideosLocal: function () {
|
getChannelVideosLocal: function () {
|
||||||
this.isElementListLoading = true
|
this.isElementListLoading = true
|
||||||
ytch.getChannelVideos(this.id, this.videoSortBy).then((response) => {
|
const expectedId = this.id
|
||||||
|
ytch.getChannelVideos({ channelId: expectedId, sortBy: this.videoSortBy }).then((response) => {
|
||||||
|
if (expectedId !== this.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.latestVideos = response.items
|
this.latestVideos = response.items
|
||||||
this.videoContinuationString = response.continuation
|
this.videoContinuationString = response.continuation
|
||||||
this.isElementListLoading = false
|
this.isElementListLoading = false
|
||||||
|
@ -332,7 +351,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
channelLocalNextPage: function () {
|
channelLocalNextPage: function () {
|
||||||
ytch.getChannelVideosMore(this.videoContinuationString).then((response) => {
|
ytch.getChannelVideosMore({ continuation: this.videoContinuationString }).then((response) => {
|
||||||
this.latestVideos = this.latestVideos.concat(response.items)
|
this.latestVideos = this.latestVideos.concat(response.items)
|
||||||
this.videoContinuationString = response.continuation
|
this.videoContinuationString = response.continuation
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
@ -352,17 +371,26 @@ export default Vue.extend({
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.apiUsed = 'invidious'
|
this.apiUsed = 'invidious'
|
||||||
|
|
||||||
this.invidiousGetChannelInfo(this.id).then((response) => {
|
const expectedId = this.id
|
||||||
|
this.invidiousGetChannelInfo(expectedId).then((response) => {
|
||||||
|
if (expectedId !== this.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
console.log(response)
|
console.log(response)
|
||||||
this.channelName = response.author
|
const channelName = response.author
|
||||||
|
const channelId = response.authorId
|
||||||
|
this.channelName = channelName
|
||||||
document.title = `${this.channelName} - ${process.env.PRODUCT_NAME}`
|
document.title = `${this.channelName} - ${process.env.PRODUCT_NAME}`
|
||||||
this.id = response.authorId
|
this.id = channelId
|
||||||
if (this.hideChannelSubscriptions) {
|
if (this.hideChannelSubscriptions) {
|
||||||
this.subCount = null
|
this.subCount = null
|
||||||
} else {
|
} else {
|
||||||
this.subCount = response.subCount
|
this.subCount = response.subCount
|
||||||
}
|
}
|
||||||
this.thumbnailUrl = response.authorThumbnails[3].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
const thumbnail = response.authorThumbnails[3].url
|
||||||
|
this.thumbnailUrl = thumbnail.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||||
|
this.updateSubscriptionDetails({ channelThumbnailUrl: thumbnail, channelName: channelName, channelId: channelId })
|
||||||
this.channelDescription = autolinker.link(response.description)
|
this.channelDescription = autolinker.link(response.description)
|
||||||
this.relatedChannels = response.relatedChannels.map((channel) => {
|
this.relatedChannels = response.relatedChannels.map((channel) => {
|
||||||
channel.authorThumbnails[channel.authorThumbnails.length - 1].url = channel.authorThumbnails[channel.authorThumbnails.length - 1].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
channel.authorThumbnails[channel.authorThumbnails.length - 1].url = channel.authorThumbnails[channel.authorThumbnails.length - 1].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||||
|
@ -371,12 +399,16 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
this.latestVideos = response.latestVideos
|
this.latestVideos = response.latestVideos
|
||||||
|
|
||||||
if (typeof (response.authorBanners) !== 'undefined') {
|
if (response.authorBanners instanceof Array && response.authorBanners.length > 0) {
|
||||||
this.bannerUrl = response.authorBanners[0].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
this.bannerUrl = response.authorBanners[0].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||||
|
} else {
|
||||||
|
this.bannerUrl = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.errorMessage = ''
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
this.setErrorMessage(err.responseJSON.error)
|
||||||
console.log(err)
|
console.log(err)
|
||||||
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
||||||
this.showToast({
|
this.showToast({
|
||||||
|
@ -418,7 +450,12 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
getPlaylistsLocal: function () {
|
getPlaylistsLocal: function () {
|
||||||
ytch.getChannelPlaylistInfo(this.id, this.playlistSortBy).then((response) => {
|
const expectedId = this.id
|
||||||
|
ytch.getChannelPlaylistInfo({ channelId: expectedId, sortBy: this.playlistSortBy }).then((response) => {
|
||||||
|
if (expectedId !== this.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
console.log(response)
|
console.log(response)
|
||||||
this.latestPlaylists = response.items.map((item) => {
|
this.latestPlaylists = response.items.map((item) => {
|
||||||
item.proxyThumbnail = false
|
item.proxyThumbnail = false
|
||||||
|
@ -448,7 +485,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
getPlaylistsLocalMore: function () {
|
getPlaylistsLocalMore: function () {
|
||||||
ytch.getChannelPlaylistsMore(this.playlistContinuationString).then((response) => {
|
ytch.getChannelPlaylistsMore({ continuation: this.playlistContinuationString }).then((response) => {
|
||||||
console.log(response)
|
console.log(response)
|
||||||
this.latestPlaylists = this.latestPlaylists.concat(response.items)
|
this.latestPlaylists = this.latestPlaylists.concat(response.items)
|
||||||
this.playlistContinuationString = response.continuation
|
this.playlistContinuationString = response.continuation
|
||||||
|
@ -466,6 +503,40 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
getPlaylistsInvidious: function () {
|
getPlaylistsInvidious: function () {
|
||||||
|
const payload = {
|
||||||
|
resource: 'channels/playlists',
|
||||||
|
id: this.id,
|
||||||
|
params: {
|
||||||
|
sort_by: this.playlistSortBy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.invidiousAPICall(payload).then((response) => {
|
||||||
|
this.playlistContinuationString = response.continuation
|
||||||
|
this.latestPlaylists = response.playlists
|
||||||
|
this.isElementListLoading = false
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
||||||
|
this.showToast({
|
||||||
|
message: `${errorMessage}: ${err.responseJSON.error}`,
|
||||||
|
time: 10000,
|
||||||
|
action: () => {
|
||||||
|
navigator.clipboard.writeText(err.responseJSON.error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (this.backendPreference === 'invidious' && this.backendFallback) {
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Falling back to Local API')
|
||||||
|
})
|
||||||
|
this.getPlaylistsLocal()
|
||||||
|
} else {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getPlaylistsInvidiousMore: 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
|
||||||
|
@ -580,6 +651,16 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setErrorMessage: function (errorMessage) {
|
||||||
|
this.isLoading = false
|
||||||
|
this.errorMessage = errorMessage
|
||||||
|
this.id = this.subscriptionInfo.id
|
||||||
|
this.channelName = this.subscriptionInfo.name
|
||||||
|
this.thumbnailUrl = this.subscriptionInfo.thumbnail
|
||||||
|
this.bannerUrl = null
|
||||||
|
this.subCount = null
|
||||||
|
},
|
||||||
|
|
||||||
handleFetchMore: function () {
|
handleFetchMore: function () {
|
||||||
switch (this.currentTab) {
|
switch (this.currentTab) {
|
||||||
case 'videos':
|
case 'videos':
|
||||||
|
@ -598,7 +679,7 @@ export default Vue.extend({
|
||||||
this.getPlaylistsLocalMore()
|
this.getPlaylistsLocalMore()
|
||||||
break
|
break
|
||||||
case 'invidious':
|
case 'invidious':
|
||||||
this.getPlaylistsInvidious()
|
this.getPlaylistsInvidiousMore()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
@ -638,7 +719,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
searchChannelLocal: function () {
|
searchChannelLocal: function () {
|
||||||
if (this.searchContinuationString === '') {
|
if (this.searchContinuationString === '') {
|
||||||
ytch.searchChannel(this.id, this.lastSearchQuery).then((response) => {
|
ytch.searchChannel({ channelId: this.id, query: this.lastSearchQuery }).then((response) => {
|
||||||
console.log(response)
|
console.log(response)
|
||||||
this.searchResults = response.items
|
this.searchResults = response.items
|
||||||
this.isElementListLoading = false
|
this.isElementListLoading = false
|
||||||
|
@ -663,7 +744,7 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
ytch.searchChannelMore(this.searchContinuationString).then((response) => {
|
ytch.searchChannelMore({ continuation: this.searchContinuationString }).then((response) => {
|
||||||
console.log(response)
|
console.log(response)
|
||||||
this.searchResults = this.searchResults.concat(response.items)
|
this.searchResults = this.searchResults.concat(response.items)
|
||||||
this.isElementListLoading = false
|
this.isElementListLoading = false
|
||||||
|
@ -721,7 +802,8 @@ export default Vue.extend({
|
||||||
'showToast',
|
'showToast',
|
||||||
'updateProfile',
|
'updateProfile',
|
||||||
'invidiousGetChannelInfo',
|
'invidiousGetChannelInfo',
|
||||||
'invidiousAPICall'
|
'invidiousAPICall',
|
||||||
|
'updateSubscriptionDetails'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,22 +3,21 @@
|
||||||
ref="search"
|
ref="search"
|
||||||
>
|
>
|
||||||
<ft-loader
|
<ft-loader
|
||||||
v-if="isLoading"
|
v-if="isLoading && !errorMessage"
|
||||||
:fullscreen="true"
|
:fullscreen="true"
|
||||||
/>
|
/>
|
||||||
<ft-card
|
<ft-card
|
||||||
v-else
|
v-else
|
||||||
class="card"
|
class="card channelDetails"
|
||||||
>
|
>
|
||||||
<img
|
<div
|
||||||
v-if="bannerUrl !== null"
|
class="channelBannerContainer"
|
||||||
class="channelBanner"
|
:class="{
|
||||||
:src="bannerUrl"
|
default: !bannerUrl
|
||||||
>
|
}"
|
||||||
<img
|
:style="{ '--banner-url': `url('${bannerUrl}')` }"
|
||||||
v-else
|
/>
|
||||||
class="defaultChannelBanner"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="channelInfoContainer"
|
class="channelInfoContainer"
|
||||||
>
|
>
|
||||||
|
@ -35,19 +34,20 @@
|
||||||
<div
|
<div
|
||||||
class="channelLineContainer"
|
class="channelLineContainer"
|
||||||
>
|
>
|
||||||
<span
|
<h1
|
||||||
class="channelName"
|
class="channelName"
|
||||||
>
|
>
|
||||||
{{ channelName }}
|
{{ channelName }}
|
||||||
</span>
|
</h1>
|
||||||
<span
|
|
||||||
|
<p
|
||||||
v-if="subCount !== null"
|
v-if="subCount !== null"
|
||||||
class="channelSubCount"
|
class="channelSubCount"
|
||||||
>
|
>
|
||||||
{{ formattedSubCount }}
|
{{ formattedSubCount }}
|
||||||
<span v-if="subCount === 1">{{ $t("Channel.Subscriber") }}</span>
|
<span v-if="subCount === 1">{{ $t("Channel.Subscriber") }}</span>
|
||||||
<span v-else>{{ $t("Channel.Subscribers") }}</span>
|
<span v-else>{{ $t("Channel.Subscribers") }}</span>
|
||||||
</span>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ft-flex-box
|
<ft-flex-box
|
||||||
|
v-if="!errorMessage"
|
||||||
class="channelInfoTabs"
|
class="channelInfoTabs"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -112,7 +113,7 @@
|
||||||
</div>
|
</div>
|
||||||
</ft-card>
|
</ft-card>
|
||||||
<ft-card
|
<ft-card
|
||||||
v-if="!isLoading"
|
v-if="!isLoading && !errorMessage"
|
||||||
class="card"
|
class="card"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -138,10 +139,10 @@
|
||||||
<ft-channel-bubble
|
<ft-channel-bubble
|
||||||
v-for="(channel, index) in relatedChannels"
|
v-for="(channel, index) in relatedChannels"
|
||||||
:key="index"
|
:key="index"
|
||||||
:channel-name="channel.author"
|
:channel-name="channel.author || channel.channelName"
|
||||||
:channel-id="channel.authorId"
|
:channel-id="channel.channelId"
|
||||||
:channel-thumbnail="channel.authorThumbnails[channel.authorThumbnails.length - 1].url"
|
:channel-thumbnail="channel.authorThumbnails[channel.authorThumbnails.length - 1].url"
|
||||||
@click="goToChannel(channel.authorId)"
|
@click="goToChannel(channel.channelId)"
|
||||||
/>
|
/>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
</div>
|
</div>
|
||||||
|
@ -194,6 +195,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ft-card>
|
</ft-card>
|
||||||
|
<ft-card
|
||||||
|
v-if="errorMessage"
|
||||||
|
class="card"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{{ errorMessage }}
|
||||||
|
</p>
|
||||||
|
</ft-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -20,19 +20,19 @@ export default Vue.extend({
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
dataLimit: 100,
|
dataLimit: 100,
|
||||||
hasQuery: false
|
searchDataLimit: 100,
|
||||||
|
showLoadMoreButton: false,
|
||||||
|
hasQuery: false,
|
||||||
|
query: '',
|
||||||
|
activeData: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
historyCache: function () {
|
historyCache: function () {
|
||||||
if (!this.hasQuery) {
|
return this.$store.getters.getHistoryCache
|
||||||
return this.$store.getters.getHistoryCache
|
|
||||||
} else {
|
|
||||||
return this.$store.getters.getSearchHistoryCache
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
activeData: function () {
|
fullData: function () {
|
||||||
if (this.historyCache.length < this.dataLimit) {
|
if (this.historyCache.length < this.dataLimit) {
|
||||||
return this.historyCache
|
return this.historyCache
|
||||||
} else {
|
} else {
|
||||||
|
@ -40,30 +40,79 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
query() {
|
||||||
|
this.searchDataLimit = 100
|
||||||
|
this.filterHistory()
|
||||||
|
},
|
||||||
|
activeData() {
|
||||||
|
this.refreshPage()
|
||||||
|
},
|
||||||
|
fullData() {
|
||||||
|
this.activeData = this.fullData
|
||||||
|
this.filterHistory()
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
console.log(this.historyCache)
|
|
||||||
|
|
||||||
const limit = sessionStorage.getItem('historyLimit')
|
const limit = sessionStorage.getItem('historyLimit')
|
||||||
|
|
||||||
if (limit !== null) {
|
if (limit !== null) {
|
||||||
this.dataLimit = limit
|
this.dataLimit = limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.activeData = this.fullData
|
||||||
|
|
||||||
|
if (this.activeData.length < this.historyCache.length) {
|
||||||
|
this.showLoadMoreButton = true
|
||||||
|
} else {
|
||||||
|
this.showLoadMoreButton = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
increaseLimit: function () {
|
increaseLimit: function () {
|
||||||
this.dataLimit += 100
|
if (this.query !== '') {
|
||||||
sessionStorage.setItem('historyLimit', this.dataLimit)
|
this.searchDataLimit += 100
|
||||||
|
this.filterHistory()
|
||||||
|
} else {
|
||||||
|
this.dataLimit += 100
|
||||||
|
sessionStorage.setItem('historyLimit', this.dataLimit)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
filterHistory: function(query) {
|
filterHistory: function(query) {
|
||||||
this.hasQuery = query !== ''
|
if (this.query === '') {
|
||||||
this.$store.dispatch('searchHistory', query)
|
this.activeData = this.fullData
|
||||||
|
if (this.activeData.length < this.historyCache.length) {
|
||||||
|
this.showLoadMoreButton = true
|
||||||
|
} else {
|
||||||
|
this.showLoadMoreButton = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const filteredQuery = this.historyCache.filter((video) => {
|
||||||
|
if (typeof (video.title) !== 'string' || typeof (video.author) !== 'string') {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return video.title.toLowerCase().includes(this.query.toLowerCase()) || video.author.toLowerCase().includes(this.query.toLowerCase())
|
||||||
|
}
|
||||||
|
}).sort((a, b) => {
|
||||||
|
return b.timeWatched - a.timeWatched
|
||||||
|
})
|
||||||
|
if (filteredQuery.length <= this.searchDataLimit) {
|
||||||
|
this.showLoadMoreButton = false
|
||||||
|
} else {
|
||||||
|
this.showLoadMoreButton = true
|
||||||
|
}
|
||||||
|
this.activeData = filteredQuery.length < this.searchDataLimit ? filteredQuery : filteredQuery.slice(0, this.searchDataLimit)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
load: function() {
|
refreshPage: function() {
|
||||||
|
const scrollPos = window.scrollY || window.scrollTop || document.getElementsByTagName('html')[0].scrollTop
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
setTimeout(() => {
|
Vue.nextTick(() => {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
}, 100)
|
Vue.nextTick(() => {
|
||||||
|
window.scrollTo(0, scrollPos)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,34 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ft-loader
|
<ft-loader
|
||||||
v-if="isLoading"
|
v-show="isLoading"
|
||||||
:fullscreen="true"
|
:fullscreen="true"
|
||||||
/>
|
/>
|
||||||
<ft-card
|
<ft-card
|
||||||
v-else
|
v-show="!isLoading"
|
||||||
class="card"
|
class="card"
|
||||||
>
|
>
|
||||||
<h3>{{ $t("History.History") }}</h3>
|
<h3>{{ $t("History.History") }}</h3>
|
||||||
<ft-input
|
<ft-input
|
||||||
|
v-show="fullData.length > 0"
|
||||||
ref="searchBar"
|
ref="searchBar"
|
||||||
:placeholder="$t('History.Search bar placeholder')"
|
:placeholder="$t('History.Search bar placeholder')"
|
||||||
:show-clear-text-button="true"
|
:show-clear-text-button="true"
|
||||||
:show-action-button="false"
|
:show-action-button="false"
|
||||||
@input="filterHistory"
|
@input="(input) => query = input"
|
||||||
|
@clear="query = ''"
|
||||||
/>
|
/>
|
||||||
<ft-flex-box
|
<ft-flex-box
|
||||||
v-if="activeData.length === 0"
|
v-show="fullData.length === 0"
|
||||||
>
|
>
|
||||||
<p class="message">
|
<p class="message">
|
||||||
{{ $t("History['Your history list is currently empty.']") }}
|
{{ $t("History['Your history list is currently empty.']") }}
|
||||||
</p>
|
</p>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
|
<ft-flex-box
|
||||||
|
v-show="activeData.length === 0 && fullData.length > 0"
|
||||||
|
>
|
||||||
|
<p class="message">
|
||||||
|
{{ $t("History['Empty Search Message']") }}
|
||||||
|
</p>
|
||||||
|
</ft-flex-box>
|
||||||
<ft-element-list
|
<ft-element-list
|
||||||
v-else
|
v-if="activeData.length > 0 && !isLoading"
|
||||||
:data="activeData"
|
:data="activeData"
|
||||||
/>
|
/>
|
||||||
<ft-flex-box
|
<ft-flex-box
|
||||||
v-if="activeData.length < historyCache.length"
|
v-if="showLoadMoreButton"
|
||||||
>
|
>
|
||||||
<ft-button
|
<ft-button
|
||||||
label="Load More"
|
label="Load More"
|
||||||
|
|
|
@ -1,18 +1,42 @@
|
||||||
|
.routerView {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.playlistInfo {
|
.playlistInfo {
|
||||||
background-color: var(--card-bg-color);
|
background-color: var(--card-bg-color);
|
||||||
padding: 10px;
|
box-sizing: border-box;
|
||||||
float: left;
|
height: calc(100vh - 96px);
|
||||||
position: fixed;
|
margin-right: 1em;
|
||||||
top: 60px;
|
|
||||||
width: 30%;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
position: sticky;
|
||||||
|
top: 78px;
|
||||||
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playlistItems {
|
.playlistItems {
|
||||||
float: right;
|
|
||||||
width: 60%;
|
|
||||||
padding: 10px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 800px) {
|
||||||
|
.routerView {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlistInfo {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlistItems {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import { mapActions } from 'vuex'
|
import { mapActions } from 'vuex'
|
||||||
import dateFormat from 'dateformat'
|
|
||||||
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
||||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||||
import PlaylistInfo from '../../components/playlist-info/playlist-info.vue'
|
import PlaylistInfo from '../../components/playlist-info/playlist-info.vue'
|
||||||
import FtListVideo from '../../components/ft-list-video/ft-list-video.vue'
|
import FtListVideo from '../../components/ft-list-video/ft-list-video.vue'
|
||||||
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||||
|
import i18n from '../../i18n/index'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'Playlist',
|
name: 'Playlist',
|
||||||
|
@ -36,6 +36,9 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
currentInvidiousInstance: function () {
|
currentInvidiousInstance: function () {
|
||||||
return this.$store.getters.getCurrentInvidiousInstance
|
return this.$store.getters.getCurrentInvidiousInstance
|
||||||
|
},
|
||||||
|
currentLocale: function () {
|
||||||
|
return i18n.locale.replace('_', '-')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -81,6 +84,12 @@ export default Vue.extend({
|
||||||
infoSource: 'local'
|
infoSource: 'local'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateSubscriptionDetails({
|
||||||
|
channelThumbnailUrl: this.infoData.channelThumbnail,
|
||||||
|
channelName: this.infoData.channelName,
|
||||||
|
channelId: this.infoData.channelId
|
||||||
|
})
|
||||||
|
|
||||||
this.playlistItems = result.items.map((video) => {
|
this.playlistItems = result.items.map((video) => {
|
||||||
if (typeof video.author !== 'undefined') {
|
if (typeof video.author !== 'undefined') {
|
||||||
const channelName = video.author.name
|
const channelName = video.author.name
|
||||||
|
@ -133,9 +142,14 @@ export default Vue.extend({
|
||||||
infoSource: 'invidious'
|
infoSource: 'invidious'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateSubscriptionDetails({
|
||||||
|
channelThumbnailUrl: result.authorThumbnails[2].url,
|
||||||
|
channelName: this.infoData.channelName,
|
||||||
|
channelId: this.infoData.channelId
|
||||||
|
})
|
||||||
|
|
||||||
const dateString = new Date(result.updated * 1000)
|
const dateString = new Date(result.updated * 1000)
|
||||||
dateString.setDate(dateString.getDate() + 1)
|
this.infoData.lastUpdated = dateString.toLocaleDateString(this.currentLocale, { year: 'numeric', month: 'short', day: 'numeric' })
|
||||||
this.infoData.lastUpdated = dateFormat(dateString, 'mmm dS, yyyy')
|
|
||||||
|
|
||||||
this.playlistItems = this.playlistItems.concat(result.videos)
|
this.playlistItems = this.playlistItems.concat(result.videos)
|
||||||
|
|
||||||
|
@ -172,7 +186,8 @@ export default Vue.extend({
|
||||||
|
|
||||||
...mapActions([
|
...mapActions([
|
||||||
'ytGetPlaylistInfo',
|
'ytGetPlaylistInfo',
|
||||||
'invidiousGetPlaylistInfo'
|
'invidiousGetPlaylistInfo',
|
||||||
|
'updateSubscriptionDetails'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,11 +4,13 @@
|
||||||
v-if="isLoading"
|
v-if="isLoading"
|
||||||
:fullscreen="true"
|
:fullscreen="true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<playlist-info
|
<playlist-info
|
||||||
v-if="!isLoading"
|
v-if="!isLoading"
|
||||||
:data="infoData"
|
:data="infoData"
|
||||||
class="playlistInfo"
|
class="playlistInfo"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ft-card
|
<ft-card
|
||||||
v-if="!isLoading"
|
v-if="!isLoading"
|
||||||
class="playlistItems"
|
class="playlistItems"
|
||||||
|
|
|
@ -14,6 +14,10 @@
|
||||||
right: 10px;
|
right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channelBubble {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 350px) {
|
@media only screen and (max-width: 350px) {
|
||||||
.floatingTopButton {
|
.floatingTopButton {
|
||||||
position: absolute
|
position: absolute
|
||||||
|
|
|
@ -6,6 +6,7 @@ import FtButton from '../../components/ft-button/ft-button.vue'
|
||||||
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
|
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
|
||||||
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||||
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
|
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
|
||||||
|
import FtChannelBubble from '../../components/ft-channel-bubble/ft-channel-bubble.vue'
|
||||||
|
|
||||||
import ytch from 'yt-channel-info'
|
import ytch from 'yt-channel-info'
|
||||||
import Parser from 'rss-parser'
|
import Parser from 'rss-parser'
|
||||||
|
@ -19,13 +20,15 @@ export default Vue.extend({
|
||||||
'ft-button': FtButton,
|
'ft-button': FtButton,
|
||||||
'ft-icon-button': FtIconButton,
|
'ft-icon-button': FtIconButton,
|
||||||
'ft-flex-box': FtFlexBox,
|
'ft-flex-box': FtFlexBox,
|
||||||
'ft-element-list': FtElementList
|
'ft-element-list': FtElementList,
|
||||||
|
'ft-channel-bubble': FtChannelBubble
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
dataLimit: 100,
|
dataLimit: 100,
|
||||||
videoList: []
|
videoList: [],
|
||||||
|
errorChannels: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -110,6 +113,7 @@ export default Vue.extend({
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
this.videoList = subscriptionList.videoList
|
this.videoList = subscriptionList.videoList
|
||||||
|
this.errorChannels = subscriptionList.errorChannels
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.getProfileSubscriptions()
|
this.getProfileSubscriptions()
|
||||||
|
@ -123,6 +127,10 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
goToChannel: function (id) {
|
||||||
|
this.$router.push({ path: `/channel/${id}` })
|
||||||
|
},
|
||||||
|
|
||||||
getSubscriptions: function () {
|
getSubscriptions: function () {
|
||||||
if (this.activeSubscriptionList.length === 0) {
|
if (this.activeSubscriptionList.length === 0) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
|
@ -144,10 +152,9 @@ export default Vue.extend({
|
||||||
|
|
||||||
let videoList = []
|
let videoList = []
|
||||||
let channelCount = 0
|
let channelCount = 0
|
||||||
|
this.errorChannels = []
|
||||||
this.activeSubscriptionList.forEach(async (channel) => {
|
this.activeSubscriptionList.forEach(async (channel) => {
|
||||||
let videos = []
|
let videos = []
|
||||||
|
|
||||||
if (!this.usingElectron || this.backendPreference === 'invidious') {
|
if (!this.usingElectron || this.backendPreference === 'invidious') {
|
||||||
if (useRss) {
|
if (useRss) {
|
||||||
videos = await this.getChannelVideosInvidiousRSS(channel)
|
videos = await this.getChannelVideosInvidiousRSS(channel)
|
||||||
|
@ -174,7 +181,8 @@ export default Vue.extend({
|
||||||
|
|
||||||
const profileSubscriptions = {
|
const profileSubscriptions = {
|
||||||
activeProfile: this.activeProfile._id,
|
activeProfile: this.activeProfile._id,
|
||||||
videoList: videoList
|
videoList: videoList,
|
||||||
|
errorChannels: this.errorChannels
|
||||||
}
|
}
|
||||||
|
|
||||||
this.videoList = await Promise.all(videoList.filter((video) => {
|
this.videoList = await Promise.all(videoList.filter((video) => {
|
||||||
|
@ -225,7 +233,12 @@ export default Vue.extend({
|
||||||
|
|
||||||
getChannelVideosLocalScraper: function (channel, failedAttempts = 0) {
|
getChannelVideosLocalScraper: function (channel, failedAttempts = 0) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
ytch.getChannelVideos(channel.id, 'latest').then(async (response) => {
|
ytch.getChannelVideos({ channelId: channel.id, sortBy: 'latest' }).then(async (response) => {
|
||||||
|
if (response.alertMessage) {
|
||||||
|
this.errorChannels.push(channel)
|
||||||
|
resolve([])
|
||||||
|
return
|
||||||
|
}
|
||||||
const videos = await Promise.all(response.items.map(async (video) => {
|
const videos = await Promise.all(response.items.map(async (video) => {
|
||||||
if (video.liveNow) {
|
if (video.liveNow) {
|
||||||
video.publishedDate = new Date().getTime()
|
video.publishedDate = new Date().getTime()
|
||||||
|
@ -297,33 +310,38 @@ export default Vue.extend({
|
||||||
resolve(items)
|
resolve(items)
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
if (err.toString().match(/404/)) {
|
||||||
this.showToast({
|
this.errorChannels.push(channel)
|
||||||
message: `${errorMessage}: ${err}`,
|
resolve([])
|
||||||
time: 10000,
|
} else {
|
||||||
action: () => {
|
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||||
navigator.clipboard.writeText(err)
|
this.showToast({
|
||||||
}
|
message: `${errorMessage}: ${err}`,
|
||||||
})
|
time: 10000,
|
||||||
switch (failedAttempts) {
|
action: () => {
|
||||||
case 0:
|
navigator.clipboard.writeText(err)
|
||||||
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
|
|
||||||
break
|
|
||||||
case 1:
|
|
||||||
if (this.backendFallback) {
|
|
||||||
this.showToast({
|
|
||||||
message: this.$t('Falling back to Invidious API')
|
|
||||||
})
|
|
||||||
resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1))
|
|
||||||
} else {
|
|
||||||
resolve([])
|
|
||||||
}
|
}
|
||||||
break
|
})
|
||||||
case 2:
|
switch (failedAttempts) {
|
||||||
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
|
case 0:
|
||||||
break
|
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
|
||||||
default:
|
break
|
||||||
resolve([])
|
case 1:
|
||||||
|
if (this.backendFallback) {
|
||||||
|
this.showToast({
|
||||||
|
message: this.$t('Falling back to Invidious API')
|
||||||
|
})
|
||||||
|
resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1))
|
||||||
|
} else {
|
||||||
|
resolve([])
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
resolve([])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -403,25 +421,30 @@ export default Vue.extend({
|
||||||
navigator.clipboard.writeText(err)
|
navigator.clipboard.writeText(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
switch (failedAttempts) {
|
if (err.toString().match(/500/)) {
|
||||||
case 0:
|
this.errorChannels.push(channel)
|
||||||
resolve(this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1))
|
resolve([])
|
||||||
break
|
} else {
|
||||||
case 1:
|
switch (failedAttempts) {
|
||||||
if (this.backendFallback) {
|
case 0:
|
||||||
this.showToast({
|
resolve(this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1))
|
||||||
message: this.$t('Falling back to the local API')
|
break
|
||||||
})
|
case 1:
|
||||||
resolve(this.getChannelVideosLocalRSS(channel, failedAttempts + 1))
|
if (this.backendFallback) {
|
||||||
} else {
|
this.showToast({
|
||||||
|
message: this.$t('Falling back to the local API')
|
||||||
|
})
|
||||||
|
resolve(this.getChannelVideosLocalRSS(channel, failedAttempts + 1))
|
||||||
|
} else {
|
||||||
|
resolve([])
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
resolve(this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1))
|
||||||
|
break
|
||||||
|
default:
|
||||||
resolve([])
|
resolve([])
|
||||||
}
|
}
|
||||||
break
|
|
||||||
case 2:
|
|
||||||
resolve(this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1))
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
resolve([])
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,6 +8,22 @@
|
||||||
v-else
|
v-else
|
||||||
class="card"
|
class="card"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="errorChannels.length !== 0"
|
||||||
|
>
|
||||||
|
<h3> {{ $t("Subscriptions.Error Channels") }}</h3>
|
||||||
|
<div>
|
||||||
|
<ft-channel-bubble
|
||||||
|
v-for="(channel, index) in errorChannels"
|
||||||
|
:key="index"
|
||||||
|
:channel-name="channel.name"
|
||||||
|
:channel-id="channel.id"
|
||||||
|
:channel-thumbnail="channel.thumbnail"
|
||||||
|
class="channelBubble"
|
||||||
|
@click="goToChannel(channel.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<h3>{{ $t("Subscriptions.Subscriptions") }}</h3>
|
<h3>{{ $t("Subscriptions.Subscriptions") }}</h3>
|
||||||
<ft-flex-box
|
<ft-flex-box
|
||||||
v-if="activeVideoList.length === 0"
|
v-if="activeVideoList.length === 0"
|
||||||
|
|
|
@ -53,7 +53,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
if (this.trendingCache[this.currentTab] && this.trendingCache[this.currentTab].length > 0) {
|
if (this.trendingCache[this.currentTab] && this.trendingCache[this.currentTab].length > 0) {
|
||||||
this.shownResults = this.trendingCache
|
this.getTrendingInfoCache()
|
||||||
} else {
|
} else {
|
||||||
this.getTrendingInfo()
|
this.getTrendingInfo()
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,11 @@ export default Vue.extend({
|
||||||
currentTabNode.attr('aria-selected', 'false')
|
currentTabNode.attr('aria-selected', 'false')
|
||||||
newTabNode.attr('aria-selected', 'true')
|
newTabNode.attr('aria-selected', 'true')
|
||||||
this.currentTab = tab
|
this.currentTab = tab
|
||||||
this.getTrendingInfo()
|
if (this.trendingCache[this.currentTab] && this.trendingCache[this.currentTab].length > 0) {
|
||||||
|
this.getTrendingInfoCache()
|
||||||
|
} else {
|
||||||
|
this.getTrendingInfo()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getTrendingInfo () {
|
getTrendingInfo () {
|
||||||
|
@ -127,7 +131,8 @@ export default Vue.extend({
|
||||||
|
|
||||||
this.shownResults = returnData
|
this.shownResults = returnData
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.$store.commit('setTrendingCache', this.shownResults, this.currentTab)
|
const currentTab = this.currentTab
|
||||||
|
this.$store.commit('setTrendingCache', { value: returnData, page: currentTab })
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
document.querySelector(`#${this.currentTab}Tab`).focus()
|
document.querySelector(`#${this.currentTab}Tab`).focus()
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
@ -151,6 +156,14 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getTrendingInfoCache: function() {
|
||||||
|
this.isLoading = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.shownResults = this.trendingCache[this.currentTab]
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
getTrendingInfoInvidious: function () {
|
getTrendingInfoInvidious: function () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
|
@ -177,7 +190,8 @@ export default Vue.extend({
|
||||||
|
|
||||||
this.shownResults = returnData
|
this.shownResults = returnData
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.$store.commit('setTrendingCache', this.shownResults, this.trendingCache)
|
const currentTab = this.currentTab
|
||||||
|
this.$store.commit('setTrendingCache', { value: returnData, page: currentTab })
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
document.querySelector(`#${this.currentTab}Tab`).focus()
|
document.querySelector(`#${this.currentTab}Tab`).focus()
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
|
|
@ -22,19 +22,19 @@ export default Vue.extend({
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
dataLimit: 100,
|
dataLimit: 100,
|
||||||
hasQuery: false
|
searchDataLimit: 100,
|
||||||
|
showLoadMoreButton: false,
|
||||||
|
query: '',
|
||||||
|
hasQuery: false,
|
||||||
|
activeData: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
favoritesPlaylist: function () {
|
favoritesPlaylist: function () {
|
||||||
if (!this.hasQuery) {
|
return this.$store.getters.getFavorites
|
||||||
return this.$store.getters.getFavorites
|
|
||||||
} else {
|
|
||||||
return this.$store.getters.getSearchPlaylistCache
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
activeData: function () {
|
fullData: function () {
|
||||||
const data = [].concat(this.favoritesPlaylist.videos).reverse()
|
const data = [].concat(this.favoritesPlaylist.videos).reverse()
|
||||||
if (this.favoritesPlaylist.videos.length < this.dataLimit) {
|
if (this.favoritesPlaylist.videos.length < this.dataLimit) {
|
||||||
return data
|
return data
|
||||||
|
@ -44,16 +44,16 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
query() {
|
||||||
|
this.searchDataLimit = 100
|
||||||
|
this.filterPlaylist()
|
||||||
|
},
|
||||||
activeData() {
|
activeData() {
|
||||||
const scrollPos = window.scrollY || window.scrollTop || document.getElementsByTagName('html')[0].scrollTop
|
this.refreshPage()
|
||||||
this.isLoading = true
|
},
|
||||||
setTimeout(() => {
|
fullData() {
|
||||||
this.isLoading = false
|
this.activeData = this.fullData
|
||||||
// This is kinda ugly, but should fix a few existing issues
|
this.filterPlaylist()
|
||||||
setTimeout(() => {
|
|
||||||
window.scrollTo(0, scrollPos)
|
|
||||||
}, 100)
|
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
|
@ -62,15 +62,60 @@ export default Vue.extend({
|
||||||
if (limit !== null) {
|
if (limit !== null) {
|
||||||
this.dataLimit = limit
|
this.dataLimit = limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.activeData.length < this.favoritesPlaylist.videos.length) {
|
||||||
|
this.showLoadMoreButton = true
|
||||||
|
} else {
|
||||||
|
this.showLoadMoreButton = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeData = this.fullData
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
increaseLimit: function () {
|
increaseLimit: function () {
|
||||||
this.dataLimit += 100
|
if (this.query !== '') {
|
||||||
sessionStorage.setItem('favoritesLimit', this.dataLimit)
|
this.searchDataLimit += 100
|
||||||
|
this.filterPlaylist()
|
||||||
|
} else {
|
||||||
|
this.dataLimit += 100
|
||||||
|
sessionStorage.setItem('favoritesLimit', this.dataLimit)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
filterPlaylist: function(query) {
|
filterPlaylist: function() {
|
||||||
this.hasQuery = query !== ''
|
if (this.query === '') {
|
||||||
this.$store.dispatch('searchFavoritePlaylist', query)
|
this.activeData = this.fullData
|
||||||
|
if (this.activeData.length < this.favoritesPlaylist.videos.length) {
|
||||||
|
this.showLoadMoreButton = true
|
||||||
|
} else {
|
||||||
|
this.showLoadMoreButton = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const filteredQuery = this.favoritesPlaylist.videos.filter((video) => {
|
||||||
|
if (typeof (video.title) !== 'string' || typeof (video.author) !== 'string') {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return video.title.toLowerCase().includes(this.query.toLowerCase()) || video.author.toLowerCase().includes(this.query.toLowerCase())
|
||||||
|
}
|
||||||
|
}).sort((a, b) => {
|
||||||
|
return b.timeAdded - a.timeAdded
|
||||||
|
})
|
||||||
|
if (filteredQuery.length <= this.searchDataLimit) {
|
||||||
|
this.showLoadMoreButton = false
|
||||||
|
} else {
|
||||||
|
this.showLoadMoreButton = true
|
||||||
|
}
|
||||||
|
this.activeData = filteredQuery.length < this.searchDataLimit ? filteredQuery : filteredQuery.slice(0, this.searchDataLimit)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshPage: function() {
|
||||||
|
const scrollPos = window.scrollY || window.scrollTop || document.getElementsByTagName('html')[0].scrollTop
|
||||||
|
this.isLoading = true
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
window.scrollTo(0, scrollPos)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ft-loader
|
<ft-loader
|
||||||
v-if="isLoading"
|
v-show="isLoading"
|
||||||
:fullscreen="true"
|
:fullscreen="true"
|
||||||
/>
|
/>
|
||||||
<ft-card
|
<ft-card
|
||||||
v-else
|
v-show="!isLoading"
|
||||||
class="card"
|
class="card"
|
||||||
>
|
>
|
||||||
<h3>
|
<h3>
|
||||||
|
@ -17,25 +17,34 @@
|
||||||
/>
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
<ft-input
|
<ft-input
|
||||||
|
v-show="fullData.length > 0"
|
||||||
ref="searchBar"
|
ref="searchBar"
|
||||||
:placeholder="$t('User Playlists.Search bar placeholder')"
|
:placeholder="$t('User Playlists.Search bar placeholder')"
|
||||||
:show-clear-text-button="true"
|
:show-clear-text-button="true"
|
||||||
:show-action-button="false"
|
:show-action-button="false"
|
||||||
@input="filterPlaylist"
|
@input="(input) => query = input"
|
||||||
|
@clear="query = ''"
|
||||||
/>
|
/>
|
||||||
<ft-flex-box
|
<ft-flex-box
|
||||||
v-if="activeData.length === 0"
|
v-show="fullData.length === 0"
|
||||||
>
|
>
|
||||||
<p class="message">
|
<p class="message">
|
||||||
{{ $t("User Playlists['Your saved videos are empty. Click on the save button on the corner of a video to have it listed here']") }}
|
{{ $t("User Playlists['Your saved videos are empty. Click on the save button on the corner of a video to have it listed here']") }}
|
||||||
</p>
|
</p>
|
||||||
</ft-flex-box>
|
</ft-flex-box>
|
||||||
|
<ft-flex-box
|
||||||
|
v-show="activeData.length === 0 && fullData.length > 0"
|
||||||
|
>
|
||||||
|
<p class="message">
|
||||||
|
{{ $t("User Playlists['Empty Search Message']") }}
|
||||||
|
</p>
|
||||||
|
</ft-flex-box>
|
||||||
<ft-element-list
|
<ft-element-list
|
||||||
v-else
|
v-if="activeData.length > 0 && !isLoading"
|
||||||
:data="activeData"
|
:data="activeData"
|
||||||
/>
|
/>
|
||||||
<ft-flex-box
|
<ft-flex-box
|
||||||
v-if="activeData.length < favoritesPlaylist.videos.length"
|
v-if="showLoadMoreButton"
|
||||||
>
|
>
|
||||||
<ft-button
|
<ft-button
|
||||||
label="Load More"
|
label="Load More"
|
||||||
|
|
|
@ -29,11 +29,11 @@ export default Vue.extend({
|
||||||
'watch-video-recommendations': WatchVideoRecommendations
|
'watch-video-recommendations': WatchVideoRecommendations
|
||||||
},
|
},
|
||||||
beforeRouteLeave: function (to, from, next) {
|
beforeRouteLeave: function (to, from, next) {
|
||||||
this.handleRouteChange()
|
this.handleRouteChange(this.videoId)
|
||||||
window.removeEventListener('beforeunload', this.handleWatchProgress)
|
window.removeEventListener('beforeunload', this.handleWatchProgress)
|
||||||
next()
|
next()
|
||||||
},
|
},
|
||||||
data: function() {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
firstLoad: true,
|
firstLoad: true,
|
||||||
|
@ -75,7 +75,9 @@ export default Vue.extend({
|
||||||
playlistId: '',
|
playlistId: '',
|
||||||
timestamp: null,
|
timestamp: null,
|
||||||
playNextTimeout: null,
|
playNextTimeout: null,
|
||||||
playNextCountDownIntervalId: null
|
playNextCountDownIntervalId: null,
|
||||||
|
pictureInPictureButtonInverval: null,
|
||||||
|
infoAreaSticky: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -127,6 +129,9 @@ export default Vue.extend({
|
||||||
playNextVideo: function () {
|
playNextVideo: function () {
|
||||||
return this.$store.getters.getPlayNextVideo
|
return this.$store.getters.getPlayNextVideo
|
||||||
},
|
},
|
||||||
|
autoplayPlaylists: function () {
|
||||||
|
return this.$store.getters.getAutoplayPlaylists
|
||||||
|
},
|
||||||
hideRecommendedVideos: function () {
|
hideRecommendedVideos: function () {
|
||||||
return this.$store.getters.getHideRecommendedVideos
|
return this.$store.getters.getHideRecommendedVideos
|
||||||
},
|
},
|
||||||
|
@ -143,13 +148,13 @@ export default Vue.extend({
|
||||||
hideVideoLikesAndDislikes: function () {
|
hideVideoLikesAndDislikes: function () {
|
||||||
return this.$store.getters.getHideVideoLikesAndDislikes
|
return this.$store.getters.getHideVideoLikesAndDislikes
|
||||||
},
|
},
|
||||||
theatrePossible: function() {
|
theatrePossible: function () {
|
||||||
return !this.hideRecommendedVideos || (!this.hideLiveChat && this.isLive) || this.watchingPlaylist
|
return !this.hideRecommendedVideos || (!this.hideLiveChat && this.isLive) || this.watchingPlaylist
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
$route() {
|
$route() {
|
||||||
this.handleRouteChange()
|
this.handleRouteChange(this.videoId)
|
||||||
// react to route changes...
|
// react to route changes...
|
||||||
this.videoId = this.$route.params.id
|
this.videoId = this.$route.params.id
|
||||||
|
|
||||||
|
@ -174,6 +179,25 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
activeFormat: function (format) {
|
||||||
|
clearInterval(this.pictureInPictureButtonInverval)
|
||||||
|
|
||||||
|
// only hide/show the button once the player is available
|
||||||
|
this.pictureInPictureButtonInverval = setInterval(() => {
|
||||||
|
if (!this.hidePlayer) {
|
||||||
|
const pipButton = document.querySelector('.vjs-picture-in-picture-control')
|
||||||
|
if (pipButton === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (format === 'audio') {
|
||||||
|
pipButton.classList.add('vjs-hidden')
|
||||||
|
} else {
|
||||||
|
pipButton.classList.remove('vjs-hidden')
|
||||||
|
}
|
||||||
|
clearInterval(this.pictureInPictureButtonInverval)
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
|
@ -200,14 +224,14 @@ export default Vue.extend({
|
||||||
window.addEventListener('beforeunload', this.handleWatchProgress)
|
window.addEventListener('beforeunload', this.handleWatchProgress)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changeTimestamp: function(timestamp) {
|
changeTimestamp: function (timestamp) {
|
||||||
this.$refs.videoPlayer.player.currentTime(timestamp)
|
this.$refs.videoPlayer.player.currentTime(timestamp)
|
||||||
},
|
},
|
||||||
toggleTheatreMode: function() {
|
toggleTheatreMode: function () {
|
||||||
this.useTheatreMode = !this.useTheatreMode
|
this.useTheatreMode = !this.useTheatreMode
|
||||||
},
|
},
|
||||||
|
|
||||||
getVideoInformationLocal: function() {
|
getVideoInformationLocal: function () {
|
||||||
if (this.firstLoad) {
|
if (this.firstLoad) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
}
|
}
|
||||||
|
@ -256,6 +280,12 @@ export default Vue.extend({
|
||||||
this.channelName = result.player_response.videoDetails.author
|
this.channelName = result.player_response.videoDetails.author
|
||||||
this.channelThumbnail = result.player_response.embedPreview.thumbnailPreviewRenderer.videoDetails.embeddedPlayerOverlayVideoDetailsRenderer.channelThumbnail.thumbnails[0].url
|
this.channelThumbnail = result.player_response.embedPreview.thumbnailPreviewRenderer.videoDetails.embeddedPlayerOverlayVideoDetailsRenderer.channelThumbnail.thumbnails[0].url
|
||||||
}
|
}
|
||||||
|
this.updateSubscriptionDetails({
|
||||||
|
channelThumbnailUrl: this.channelThumbnail,
|
||||||
|
channelName: this.channelName,
|
||||||
|
channelId: this.channelId
|
||||||
|
})
|
||||||
|
|
||||||
this.videoPublished = new Date(result.videoDetails.publishDate.replace('-', '/')).getTime()
|
this.videoPublished = new Date(result.videoDetails.publishDate.replace('-', '/')).getTime()
|
||||||
this.videoDescription = result.player_response.videoDetails.shortDescription
|
this.videoDescription = result.player_response.videoDetails.shortDescription
|
||||||
|
|
||||||
|
@ -310,7 +340,7 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((this.isLive || this.isLiveContent) && !this.isUpcoming) {
|
if ((this.isLive && this.isLiveContent) && !this.isUpcoming) {
|
||||||
this.enableLegacyFormat()
|
this.enableLegacyFormat()
|
||||||
|
|
||||||
this.videoSourceList = result.formats.filter((format) => {
|
this.videoSourceList = result.formats.filter((format) => {
|
||||||
|
@ -402,8 +432,9 @@ export default Vue.extend({
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!standardLocale.startsWith('en') && noLocaleCaption) {
|
if (!standardLocale.startsWith('en') && noLocaleCaption) {
|
||||||
const baseUrl = result.player_response.captions.playerCaptionsRenderer.baseUrl
|
captionTracks.forEach((caption) => {
|
||||||
this.tryAddingTranslatedLocaleCaption(captionTracks, standardLocale, baseUrl)
|
this.tryAddingTranslatedLocaleCaption(captionTracks, standardLocale, caption.baseUrl)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,7 +539,7 @@ export default Vue.extend({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
getVideoInformationInvidious: function() {
|
getVideoInformationInvidious: function () {
|
||||||
if (this.firstLoad) {
|
if (this.firstLoad) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
}
|
}
|
||||||
|
@ -540,7 +571,14 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
this.channelId = result.authorId
|
this.channelId = result.authorId
|
||||||
this.channelName = result.author
|
this.channelName = result.author
|
||||||
this.channelThumbnail = result.authorThumbnails[1] ? result.authorThumbnails[1].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`) : ''
|
const channelThumb = result.authorThumbnails[1]
|
||||||
|
this.channelThumbnail = channelThumb ? channelThumb.url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`) : ''
|
||||||
|
this.updateSubscriptionDetails({
|
||||||
|
channelThumbnailUrl: channelThumb?.url,
|
||||||
|
channelName: result.author,
|
||||||
|
channelId: result.authorId
|
||||||
|
})
|
||||||
|
|
||||||
this.videoPublished = result.published * 1000
|
this.videoPublished = result.published * 1000
|
||||||
this.videoDescriptionHtml = result.descriptionHtml
|
this.videoDescriptionHtml = result.descriptionHtml
|
||||||
this.recommendedVideos = result.recommendedVideos
|
this.recommendedVideos = result.recommendedVideos
|
||||||
|
@ -880,7 +918,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleVideoEnded: function () {
|
handleVideoEnded: function () {
|
||||||
if (!this.watchingPlaylist && !this.playNextVideo) {
|
if ((!this.watchingPlaylist || !this.autoplayPlaylists) && !this.playNextVideo) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -933,7 +971,11 @@ export default Vue.extend({
|
||||||
this.playNextCountDownIntervalId = setInterval(showCountDownMessage, 1000)
|
this.playNextCountDownIntervalId = setInterval(showCountDownMessage, 1000)
|
||||||
},
|
},
|
||||||
|
|
||||||
handleRouteChange: async function () {
|
handleRouteChange: async function (videoId) {
|
||||||
|
// if the user navigates to another video, the ipc call for the userdata path
|
||||||
|
// takes long enough for the video id to have already changed to the new one
|
||||||
|
// receiving it as an arg instead of accessing it ourselves means we always have the right one
|
||||||
|
|
||||||
clearTimeout(this.playNextTimeout)
|
clearTimeout(this.playNextTimeout)
|
||||||
clearInterval(this.playNextCountDownIntervalId)
|
clearInterval(this.playNextCountDownIntervalId)
|
||||||
|
|
||||||
|
@ -943,14 +985,13 @@ export default Vue.extend({
|
||||||
const player = this.$refs.videoPlayer.player
|
const player = this.$refs.videoPlayer.player
|
||||||
|
|
||||||
if (player !== null && !player.paused() && player.isInPictureInPicture()) {
|
if (player !== null && !player.paused() && player.isInPictureInPicture()) {
|
||||||
const playerId = this.videoId
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
player.play()
|
player.play()
|
||||||
player.on('leavepictureinpicture', (event) => {
|
player.on('leavepictureinpicture', (event) => {
|
||||||
const watchTime = player.currentTime()
|
const watchTime = player.currentTime()
|
||||||
if (this.$route.fullPath.includes('/watch')) {
|
if (this.$route.fullPath.includes('/watch')) {
|
||||||
const routeId = this.$route.params.id
|
const routeId = this.$route.params.id
|
||||||
if (routeId === playerId) {
|
if (routeId === videoId) {
|
||||||
const activePlayer = $('.ftVideoPlayer video').get(0)
|
const activePlayer = $('.ftVideoPlayer video').get(0)
|
||||||
activePlayer.currentTime = watchTime
|
activePlayer.currentTime = watchTime
|
||||||
}
|
}
|
||||||
|
@ -966,23 +1007,23 @@ export default Vue.extend({
|
||||||
if (this.removeVideoMetaFiles) {
|
if (this.removeVideoMetaFiles) {
|
||||||
const userData = await this.getUserDataPath()
|
const userData = await this.getUserDataPath()
|
||||||
if (this.isDev) {
|
if (this.isDev) {
|
||||||
const dashFileLocation = `static/dashFiles/${this.videoId}.xml`
|
const dashFileLocation = `static/dashFiles/${videoId}.xml`
|
||||||
const vttFileLocation = `static/storyboards/${this.videoId}.vtt`
|
const vttFileLocation = `static/storyboards/${videoId}.vtt`
|
||||||
// only delete the file it actually exists
|
// only delete the file it actually exists
|
||||||
if (fs.existsSync('static/dashFiles/') && fs.existsSync(dashFileLocation)) {
|
if (fs.existsSync(dashFileLocation)) {
|
||||||
fs.rmSync(dashFileLocation)
|
fs.rmSync(dashFileLocation)
|
||||||
}
|
}
|
||||||
if (fs.existsSync('static/storyboards/') && fs.existsSync(vttFileLocation)) {
|
if (fs.existsSync(vttFileLocation)) {
|
||||||
fs.rmSync(vttFileLocation)
|
fs.rmSync(vttFileLocation)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const dashFileLocation = `${userData}/dashFiles/${this.videoId}.xml`
|
const dashFileLocation = `${userData}/dashFiles/${videoId}.xml`
|
||||||
const vttFileLocation = `${userData}/storyboards/${this.videoId}.vtt`
|
const vttFileLocation = `${userData}/storyboards/${videoId}.vtt`
|
||||||
|
|
||||||
if (fs.existsSync(`${userData}/dashFiles/`) && fs.existsSync(dashFileLocation)) {
|
if (fs.existsSync(dashFileLocation)) {
|
||||||
fs.rmSync(dashFileLocation)
|
fs.rmSync(dashFileLocation)
|
||||||
}
|
}
|
||||||
if (fs.existsSync(`${userData}/storyboards/`) && fs.existsSync(vttFileLocation)) {
|
if (fs.existsSync(vttFileLocation)) {
|
||||||
fs.rmSync(vttFileLocation)
|
fs.rmSync(vttFileLocation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1136,6 +1177,13 @@ export default Vue.extend({
|
||||||
label = `${this.$t('Locale Name')} (translated from English)`
|
label = `${this.$t('Locale Name')} (translated from English)`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const indexTranslated = captionTracks.findIndex((item) => {
|
||||||
|
return item.name.simpleText === label
|
||||||
|
})
|
||||||
|
if (indexTranslated !== -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (enCaptionExists) {
|
if (enCaptionExists) {
|
||||||
url = new URL(captionTracks[enCaptionIdx].baseUrl)
|
url = new URL(captionTracks[enCaptionIdx].baseUrl)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1240,7 +1288,8 @@ export default Vue.extend({
|
||||||
'updateWatchProgress',
|
'updateWatchProgress',
|
||||||
'getUserDataPath',
|
'getUserDataPath',
|
||||||
'ytGetVideoInformation',
|
'ytGetVideoInformation',
|
||||||
'invidiousGetVideoInformation'
|
'invidiousGetVideoInformation',
|
||||||
|
'updateSubscriptionDetails'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
=dual-column-template
|
=dual-column-template
|
||||||
grid-template: "video video sidebar" 0fr "info info sidebar" auto "info info sidebar" auto / 1fr 1fr 1fr
|
grid-template: "video video sidebar" 0fr "info info sidebar" 1fr "info info sidebar" 1fr / 1fr 1fr 1fr
|
||||||
|
|
||||||
=theatre-mode-template
|
=theatre-mode-template
|
||||||
grid-template: "video video video" auto "info info sidebar" auto "info info sidebar" auto / 1fr 1fr 1fr
|
grid-template: "video video video" auto "info info sidebar" auto "info info sidebar" auto / 1fr 1fr 1fr
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
grid-area: video
|
grid-area: video
|
||||||
|
|
||||||
.videoAreaMargin
|
.videoAreaMargin
|
||||||
margin: 0px 8px 16px
|
margin: 0 0 16px
|
||||||
|
|
||||||
.videoPlayer
|
.videoPlayer
|
||||||
grid-column: 1
|
grid-column: 1
|
||||||
|
@ -61,11 +61,20 @@
|
||||||
margin-top: 10px
|
margin-top: 10px
|
||||||
|
|
||||||
.watchVideo
|
.watchVideo
|
||||||
margin: 0px 8px 16px
|
margin: 0 0 16px
|
||||||
grid-column: 1
|
grid-column: 1
|
||||||
|
|
||||||
.infoArea
|
.infoArea
|
||||||
grid-area: info
|
grid-area: info
|
||||||
|
position: relative
|
||||||
|
|
||||||
|
@media only screen and (min-width: 901px)
|
||||||
|
.infoArea
|
||||||
|
scroll-margin-top: 76px
|
||||||
|
|
||||||
|
.infoAreaSticky
|
||||||
|
position: sticky
|
||||||
|
top: 76px
|
||||||
|
|
||||||
.sidebarArea
|
.sidebarArea
|
||||||
grid-area: sidebar
|
grid-area: sidebar
|
||||||
|
@ -83,4 +92,7 @@
|
||||||
height: 500px
|
height: 500px
|
||||||
|
|
||||||
.watchVideoRecommendations, .theatreRecommendations
|
.watchVideoRecommendations, .theatreRecommendations
|
||||||
margin: 0 8px 16px
|
margin: 0 0 16px
|
||||||
|
|
||||||
|
@media only screen and (min-width: 901px)
|
||||||
|
margin: 0 8px 16px
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue