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",
|
||||
{
|
||||
"targets": {
|
||||
"chrome": "73",
|
||||
"node": 12
|
||||
"chrome": "96",
|
||||
"node": 16
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/typescript"
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/proposal-class-properties",
|
||||
"@babel/proposal-object-rest-spread"
|
||||
"@babel/proposal-class-properties"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ body:
|
|||
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.
|
||||
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
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
|
@ -113,4 +115,4 @@ body:
|
|||
description: Please ensure you've completed the following, if applicable.
|
||||
options:
|
||||
- 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:
|
||||
- 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
|
||||
- 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
|
||||
attributes:
|
||||
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:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-version: [16.x]
|
||||
runtime: [ linux-x64, linux-arm64, win-x64, osx-x64 ]
|
||||
include:
|
||||
- runtime: linux-x64
|
||||
os: ubuntu-latest
|
||||
os: ubuntu-18.04
|
||||
|
||||
- runtime: linux-arm64
|
||||
os: ubuntu-latest
|
||||
|
|
|
@ -6,7 +6,7 @@ name: Linter
|
|||
# events but only for the master branch
|
||||
on:
|
||||
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
|
||||
jobs:
|
||||
|
@ -18,10 +18,10 @@ jobs:
|
|||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 14.x
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 16.x
|
||||
cache: "yarn"
|
||||
- run: npm run ci
|
||||
- run: npm run lint
|
||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-version: [16.x]
|
||||
runtime: [ linux-x64, linux-arm64, win-x64, osx-x64 ]
|
||||
include:
|
||||
- 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).
|
||||
|
||||
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.
|
||||
|
||||
## 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 Package Manager (winget): [Usage](https://docs.microsoft.com/en-us/windows/package-manager/winget/)
|
||||
|
||||
### 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).
|
||||
|
|
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
|
||||
|
||||
let targets
|
||||
var platform = os.platform()
|
||||
const platform = os.platform()
|
||||
const cpus = os.cpus()
|
||||
|
||||
if (platform == 'darwin') {
|
||||
targets = Platform.MAC.createTarget()
|
||||
} else if (platform == 'win32') {
|
||||
if (platform === 'darwin') {
|
||||
let arch = Arch.x64
|
||||
|
||||
// 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()
|
||||
} else if (platform == 'linux') {
|
||||
} else if (platform === 'linux') {
|
||||
let arch = Arch.x64
|
||||
|
||||
if (args[2] === 'arm64') {
|
||||
|
|
|
@ -115,17 +115,20 @@ function startRenderer(callback) {
|
|||
console.log(`\nWatching file changes for ${name} script...`)
|
||||
})
|
||||
|
||||
const server = new WebpackDevServer(compiler, {
|
||||
const server = new WebpackDevServer({
|
||||
static: {
|
||||
directory: path.join(process.cwd(), 'static'),
|
||||
watch: {
|
||||
ignored: /(dashFiles|storyboards)\/*/
|
||||
ignored: [
|
||||
/(dashFiles|storyboards)\/*/,
|
||||
'/**/.DS_Store',
|
||||
]
|
||||
}
|
||||
},
|
||||
port
|
||||
})
|
||||
}, compiler)
|
||||
|
||||
server.listen(port, '', err => {
|
||||
server.startCallback(err => {
|
||||
if (err) console.error(err)
|
||||
|
||||
callback()
|
||||
|
|
|
@ -49,7 +49,7 @@ const config = {
|
|||
path: path.join(__dirname, '../dist'),
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
extensions: ['.js', '.json'],
|
||||
alias: {
|
||||
'@': path.join(__dirname, '../src/'),
|
||||
src: path.join(__dirname, '../src/'),
|
||||
|
|
|
@ -138,7 +138,7 @@ const config = {
|
|||
images: path.join(__dirname, '../src/renderer/assets/img/'),
|
||||
static: path.join(__dirname, '../static/'),
|
||||
},
|
||||
extensions: ['.ts', '.js', '.vue', '.json'],
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
},
|
||||
target: 'electron-renderer',
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ const config = {
|
|||
mode: process.env.NODE_ENV,
|
||||
devtool: isDevMode ? 'eval-cheap-module-source-map' : false,
|
||||
entry: {
|
||||
workerSample: path.join(__dirname, '../src/utilities/workerSample.ts'),
|
||||
workerSample: path.join(__dirname, '../src/utilities/workerSample.js'),
|
||||
},
|
||||
output: {
|
||||
libraryTarget: 'commonjs2',
|
||||
|
@ -52,7 +52,7 @@ const config = {
|
|||
'@': path.join(__dirname, '../src/'),
|
||||
src: path.join(__dirname, '../src/'),
|
||||
},
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
extensions: ['.js', '.json'],
|
||||
},
|
||||
target: 'node',
|
||||
}
|
||||
|
|
55
package.json
55
package.json
|
@ -2,7 +2,7 @@
|
|||
"name": "freetube",
|
||||
"productName": "FreeTube",
|
||||
"description": "A private YouTube client",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "./dist/main.js",
|
||||
"private": true,
|
||||
|
@ -30,23 +30,18 @@
|
|||
"debug-runner": "node _scripts/dev-runner.js --remote-debug",
|
||||
"dev": "run-s rebuild:electron dev-runner",
|
||||
"dev-runner": "node _scripts/dev-runner.js",
|
||||
"jest": "jest",
|
||||
"jest:coverage": "jest --collect-coverage",
|
||||
"jest:watch": "jest --watch",
|
||||
"lint-fix": "eslint --fix --ext .js,.ts,.vue ./",
|
||||
"lint": "eslint --ext .js,.ts,.vue ./",
|
||||
"lint-fix": "eslint --fix --ext .js,.vue ./",
|
||||
"lint": "eslint --ext .js,.vue ./",
|
||||
"pack": "run-p pack:main pack:renderer pack:workers",
|
||||
"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: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",
|
||||
"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:node": "npm rebuild",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -55,60 +50,48 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.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",
|
||||
"autolinker": "^3.14.3",
|
||||
"bulma-pro": "^0.2.0",
|
||||
"dateformat": "^4.5.1",
|
||||
"electron-context-menu": "^3.1.1",
|
||||
"autolinker": "^3.15.0",
|
||||
"electron-context-menu": "^3.1.2",
|
||||
"http-proxy-agent": "^4.0.1",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"jquery": "^3.6.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.uniqwith": "^4.5.0",
|
||||
"marked": "^4.0.10",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"marked": "^4.0.15",
|
||||
"nedb-promises": "^5.0.1",
|
||||
"node-forge": "^1.0.0",
|
||||
"opml-to-json": "^1.0.1",
|
||||
"rss-parser": "^3.12.0",
|
||||
"socks-proxy-agent": "^6.0.0",
|
||||
"video.js": "7.14.3",
|
||||
"videojs-abloop": "^1.2.0",
|
||||
"video.js": "7.18.1",
|
||||
"videojs-contrib-quality-levels": "^2.1.0",
|
||||
"videojs-http-source-selector": "^1.1.6",
|
||||
"videojs-overlay": "^2.1.4",
|
||||
"videojs-replay": "^1.1.0",
|
||||
"videojs-vtt-thumbnails-freetube": "0.0.15",
|
||||
"vue": "^2.6.14",
|
||||
"vue-electron": "^1.0.6",
|
||||
"vue-i18n": "^8.25.0",
|
||||
"vue-observe-visibility": "^1.0.0",
|
||||
"vue-router": "^3.5.2",
|
||||
"vuex": "^3.6.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-trending-scraper": "^2.0.1",
|
||||
"ytdl-core": "^4.10.1",
|
||||
"ytpl": "^2.2.3",
|
||||
"ytsr": "^3.5.3"
|
||||
"ytdl-core": "^4.11.0",
|
||||
"ytpl": "^2.3.0",
|
||||
"ytsr": "^3.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.14.7",
|
||||
"@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/core": "^7.17.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/preset-env": "^7.17.10",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-loader": "^8.2.5",
|
||||
"copy-webpack-plugin": "^9.0.1",
|
||||
"css-loader": "5.2.6",
|
||||
"electron": "^16.0.8",
|
||||
"electron": "^16.2.7",
|
||||
"electron-builder": "^22.11.7",
|
||||
"electron-builder-squirrel-windows": "^22.13.1",
|
||||
"electron-debug": "^3.2.0",
|
||||
|
@ -124,7 +107,6 @@
|
|||
"fast-glob": "^3.2.7",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jest": "^27.1.0",
|
||||
"mini-css-extract-plugin": "^2.2.2",
|
||||
"node-abi": "^2.30.1",
|
||||
"node-loader": "^2.0.0",
|
||||
|
@ -135,7 +117,6 @@
|
|||
"sass-loader": "^12.1.0",
|
||||
"style-loader": "^3.2.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"typescript": "^4.4.2",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-devtools": "^5.1.4",
|
||||
"vue-eslint-parser": "^7.10.0",
|
||||
|
|
|
@ -6,12 +6,14 @@ const IpcChannels = {
|
|||
GET_SYSTEM_LOCALE: 'get-system-locale',
|
||||
GET_USER_DATA_PATH: 'get-user-data-path',
|
||||
GET_USER_DATA_PATH_SYNC: 'get-user-data-path-sync',
|
||||
GET_PICTURES_PATH: 'get-pictures-path',
|
||||
SHOW_OPEN_DIALOG: 'show-open-dialog',
|
||||
SHOW_SAVE_DIALOG: 'show-save-dialog',
|
||||
STOP_POWER_SAVE_BLOCKER: 'stop-power-save-blocker',
|
||||
START_POWER_SAVE_BLOCKER: 'start-power-save-blocker',
|
||||
CREATE_NEW_WINDOW: 'create-new-window',
|
||||
OPEN_IN_EXTERNAL_PLAYER: 'open-in-external-player',
|
||||
NATIVE_THEME_UPDATE: 'native-theme-update',
|
||||
|
||||
DB_SETTINGS: 'db-settings',
|
||||
DB_HISTORY: 'db-history',
|
||||
|
@ -36,8 +38,7 @@ const DBActions = {
|
|||
},
|
||||
|
||||
HISTORY: {
|
||||
UPDATE_WATCH_PROGRESS: 'db-action-history-update-watch-progress',
|
||||
SEARCH: 'db-action-history-search'
|
||||
UPDATE_WATCH_PROGRESS: 'db-action-history-update-watch-progress'
|
||||
},
|
||||
|
||||
PLAYLISTS: {
|
||||
|
|
|
@ -27,6 +27,10 @@ class Settings {
|
|||
return db.settings.findOne({ _id: 'bounds' })
|
||||
}
|
||||
|
||||
static _findTheme() {
|
||||
return db.settings.findOne({ _id: 'baseTheme' })
|
||||
}
|
||||
|
||||
static _updateBounds(value) {
|
||||
return db.settings.update({ _id: 'bounds' }, { _id: 'bounds', value }, { upsert: true })
|
||||
}
|
||||
|
@ -38,11 +42,6 @@ class History {
|
|||
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) {
|
||||
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) {
|
||||
return ipcRenderer.invoke(
|
||||
IpcChannels.DB_HISTORY,
|
||||
|
|
|
@ -25,10 +25,6 @@ class History {
|
|||
return baseHandlers.history.find()
|
||||
}
|
||||
|
||||
static search(query) {
|
||||
return baseHandlers.history.search(query)
|
||||
}
|
||||
|
||||
static 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.history = Datastore.create({ filename: dbPath('history'), autoload: true })
|
||||
|
||||
db.history.ensureIndex({ fieldName: 'author' })
|
||||
db.history.ensureIndex({ fieldName: 'title' })
|
||||
|
||||
export default db
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<% } %>
|
||||
</head>
|
||||
|
||||
<body class="dark mainRed secBlue">
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- Set `__static` path to static files in production -->
|
||||
<script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
app, BrowserWindow, dialog, Menu, ipcMain,
|
||||
powerSaveBlocker, screen, session, shell
|
||||
powerSaveBlocker, screen, session, shell, nativeTheme
|
||||
} from 'electron'
|
||||
import path from 'path'
|
||||
import cp from 'child_process'
|
||||
|
@ -25,7 +25,7 @@ function runApp() {
|
|||
label: 'Show Video Statistics',
|
||||
visible: parameters.mediaType === 'video',
|
||||
click: () => {
|
||||
browserWindow.webContents.send('showVideoStatistics', 'show')
|
||||
browserWindow.webContents.send('showVideoStatistics')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -35,6 +35,7 @@ function runApp() {
|
|||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const isDebug = process.argv.includes('--debug')
|
||||
|
||||
let mainWindow
|
||||
let startupUrl
|
||||
|
||||
|
@ -146,7 +147,8 @@ function runApp() {
|
|||
session.defaultSession.cookies.set({
|
||||
url: url,
|
||||
name: 'CONSENT',
|
||||
value: 'YES+'
|
||||
value: 'YES+',
|
||||
sameSite: 'no_restriction'
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -172,11 +174,33 @@ function runApp() {
|
|||
}
|
||||
|
||||
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
|
||||
*/
|
||||
const commonBrowserWindowOptions = {
|
||||
backgroundColor: '#212121',
|
||||
backgroundColor: windowBackground,
|
||||
darkTheme: nativeTheme.shouldUseDarkColors,
|
||||
icon: isDev
|
||||
? path.join(__dirname, '../../_icons/iconColor.png')
|
||||
/* eslint-disable-next-line */
|
||||
|
@ -191,6 +215,7 @@ function runApp() {
|
|||
contextIsolation: false
|
||||
}
|
||||
}
|
||||
|
||||
const newWindow = new BrowserWindow(
|
||||
Object.assign(
|
||||
{
|
||||
|
@ -241,6 +266,7 @@ function runApp() {
|
|||
height: bounds.height
|
||||
})
|
||||
}
|
||||
|
||||
if (maximized) {
|
||||
newWindow.maximize()
|
||||
}
|
||||
|
@ -345,6 +371,14 @@ function runApp() {
|
|||
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) => {
|
||||
console.log(url)
|
||||
session.defaultSession.setProxy({
|
||||
|
@ -372,11 +406,23 @@ function runApp() {
|
|||
event.returnValue = app.getPath('userData')
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannels.GET_PICTURES_PATH, () => {
|
||||
return app.getPath('pictures')
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannels.SHOW_OPEN_DIALOG, async (_, 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)
|
||||
})
|
||||
|
||||
|
@ -388,10 +434,11 @@ function runApp() {
|
|||
return powerSaveBlocker.start('prevent-display-sleep')
|
||||
})
|
||||
|
||||
ipcMain.on(IpcChannels.CREATE_NEW_WINDOW, () => {
|
||||
ipcMain.on(IpcChannels.CREATE_NEW_WINDOW, (_e, { windowStartupUrl = null } = { }) => {
|
||||
createWindow({
|
||||
replaceMainWindow: false,
|
||||
showWindowNow: true
|
||||
showWindowNow: true,
|
||||
windowStartupUrl: windowStartupUrl
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -456,9 +503,6 @@ function runApp() {
|
|||
)
|
||||
return null
|
||||
|
||||
case DBActions.HISTORY.SEARCH:
|
||||
return await baseHandlers.history.search(data)
|
||||
|
||||
case DBActions.GENERAL.DELETE:
|
||||
await baseHandlers.history.delete(data)
|
||||
syncOtherWindows(
|
||||
|
@ -726,7 +770,21 @@ function runApp() {
|
|||
const template = [
|
||||
{
|
||||
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',
|
||||
|
|
|
@ -3,33 +3,29 @@
|
|||
src: url(assets/font/Roboto-Regular.ttf);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#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 {
|
||||
margin-left: 200px;
|
||||
margin-top: 80px;
|
||||
transition-property: margin;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.expand {
|
||||
margin-left: 80px;
|
||||
flex: 1 1 0%;
|
||||
margin: 18px 10px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
width: 85%;
|
||||
margin: 20px auto 0;
|
||||
}
|
||||
|
||||
.banner-wrapper {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.flexBox {
|
||||
margin-top: 60px;
|
||||
margin-bottom: -75px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#changeLogText {
|
||||
|
@ -46,13 +42,13 @@ body {
|
|||
}
|
||||
|
||||
@media only screen and (max-width: 680px) {
|
||||
.expand, .routerView {
|
||||
margin-left: 0px;
|
||||
margin-bottom: 80px;
|
||||
.routerView {
|
||||
margin: 68px 8px 68px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
width: 90%;
|
||||
width: 80%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.flexBox {
|
||||
|
|
|
@ -12,6 +12,7 @@ import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue'
|
|||
import $ from 'jquery'
|
||||
import { marked } from 'marked'
|
||||
import Parser from 'rss-parser'
|
||||
import { IpcChannels } from '../constants'
|
||||
|
||||
let ipcRenderer = null
|
||||
|
||||
|
@ -101,6 +102,22 @@ export default Vue.extend({
|
|||
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 () {
|
||||
return [
|
||||
this.$t('Yes'),
|
||||
|
@ -114,6 +131,13 @@ export default Vue.extend({
|
|||
},
|
||||
watch: {
|
||||
windowTitle: 'setWindowTitle',
|
||||
|
||||
baseTheme: 'checkThemeSettings',
|
||||
|
||||
mainColor: 'checkThemeSettings',
|
||||
|
||||
secColor: 'checkThemeSettings',
|
||||
|
||||
$route () {
|
||||
// react to route changes...
|
||||
// Hide top nav filter panel on page change
|
||||
|
@ -126,6 +150,8 @@ export default Vue.extend({
|
|||
},
|
||||
mounted: function () {
|
||||
this.grabUserSettings().then(async () => {
|
||||
this.checkThemeSettings()
|
||||
|
||||
await this.fetchInvidiousInstances({ isDev: this.isDev })
|
||||
if (this.defaultInvidiousInstance === '') {
|
||||
await this.setRandomCurrentInvidiousInstance()
|
||||
|
@ -142,6 +168,7 @@ export default Vue.extend({
|
|||
this.activateKeyboardShortcuts()
|
||||
this.openAllLinksExternally()
|
||||
this.enableOpenUrl()
|
||||
this.watchSystemTheme()
|
||||
await this.checkExternalPlayer()
|
||||
}
|
||||
|
||||
|
@ -160,45 +187,27 @@ export default Vue.extend({
|
|||
},
|
||||
methods: {
|
||||
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 = {
|
||||
baseTheme: baseTheme,
|
||||
mainColor: mainColor,
|
||||
secColor: secColor
|
||||
baseTheme: this.baseTheme || 'dark',
|
||||
mainColor: this.mainColor || 'mainRed',
|
||||
secColor: this.secColor || 'secBlue'
|
||||
}
|
||||
|
||||
this.updateTheme(theme)
|
||||
},
|
||||
|
||||
updateTheme: function (theme) {
|
||||
console.log(theme)
|
||||
const className = `${theme.baseTheme} ${theme.mainColor} ${theme.secColor}`
|
||||
const body = document.getElementsByTagName('body')[0]
|
||||
body.className = className
|
||||
localStorage.setItem('baseTheme', theme.baseTheme)
|
||||
localStorage.setItem('mainColor', theme.mainColor)
|
||||
localStorage.setItem('secColor', theme.secColor)
|
||||
console.group('updateTheme')
|
||||
console.log('Theme: ', theme)
|
||||
document.body.className = `${theme.baseTheme} main${theme.mainColor} sec${theme.secColor}`
|
||||
document.body.dataset.systemTheme = this.systemTheme
|
||||
console.groupEnd()
|
||||
},
|
||||
|
||||
checkForNewUpdates: function () {
|
||||
if (this.checkForUpdates) {
|
||||
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) => {
|
||||
const tagName = response[0].tag_name
|
||||
|
@ -301,12 +310,21 @@ export default Vue.extend({
|
|||
case 'ArrowLeft':
|
||||
this.$refs.topNav.historyBack()
|
||||
break
|
||||
case 'KeyD':
|
||||
this.$refs.topNav.focusSearch()
|
||||
break
|
||||
}
|
||||
}
|
||||
switch (event.code) {
|
||||
case 'Tab':
|
||||
this.hideOutlines = false
|
||||
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) => {
|
||||
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) {
|
||||
const el = event.currentTarget
|
||||
console.log(this.usingElectron)
|
||||
console.log(el)
|
||||
event.preventDefault()
|
||||
|
||||
// Check if it's a YouTube link
|
||||
|
@ -331,7 +352,11 @@ export default Vue.extend({
|
|||
const isYoutubeLink = youtubeUrlPattern.test(el.href)
|
||||
|
||||
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') {
|
||||
// Let user know opening external link is disabled via setting
|
||||
this.showToast({
|
||||
|
@ -348,7 +373,7 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
handleYoutubeLink: function (href) {
|
||||
handleYoutubeLink: function (href, { doCreateNewWindow = false } = { }) {
|
||||
this.getYoutubeUrlInfo(href).then((result) => {
|
||||
switch (result.urlType) {
|
||||
case 'video': {
|
||||
|
@ -361,9 +386,11 @@ export default Vue.extend({
|
|||
if (playlistId && playlistId.length > 0) {
|
||||
query.playlistId = playlistId
|
||||
}
|
||||
this.$router.push({
|
||||
path: `/watch/${videoId}`,
|
||||
query: query
|
||||
const path = `/watch/${videoId}`
|
||||
this.openInternalPath({
|
||||
path,
|
||||
query,
|
||||
doCreateNewWindow
|
||||
})
|
||||
break
|
||||
}
|
||||
|
@ -371,9 +398,11 @@ export default Vue.extend({
|
|||
case 'playlist': {
|
||||
const { playlistId, query } = result
|
||||
|
||||
this.$router.push({
|
||||
path: `/playlist/${playlistId}`,
|
||||
query
|
||||
const path = `/playlist/${playlistId}`
|
||||
this.openInternalPath({
|
||||
path,
|
||||
query,
|
||||
doCreateNewWindow
|
||||
})
|
||||
break
|
||||
}
|
||||
|
@ -381,9 +410,11 @@ export default Vue.extend({
|
|||
case 'search': {
|
||||
const { searchQuery, query } = result
|
||||
|
||||
this.$router.push({
|
||||
path: `/search/${encodeURIComponent(searchQuery)}`,
|
||||
query
|
||||
const path = `/search/${encodeURIComponent(searchQuery)}`
|
||||
this.openInternalPath({
|
||||
path,
|
||||
query,
|
||||
doCreateNewWindow
|
||||
})
|
||||
break
|
||||
}
|
||||
|
@ -404,8 +435,10 @@ export default Vue.extend({
|
|||
case 'channel': {
|
||||
const { channelId, subPath } = result
|
||||
|
||||
this.$router.push({
|
||||
path: `/channel/${channelId}/${subPath}`
|
||||
const path = `/channel/${channelId}/${subPath}`
|
||||
this.openInternalPath({
|
||||
path,
|
||||
doCreateNewWindow
|
||||
})
|
||||
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 () {
|
||||
ipcRenderer.on('openUrl', (event, url) => {
|
||||
if (url) {
|
||||
|
@ -473,7 +537,10 @@ export default Vue.extend({
|
|||
'getExternalPlayerCmdArgumentsData',
|
||||
'fetchInvidiousInstances',
|
||||
'setRandomCurrentInvidiousInstance',
|
||||
'setupListenersToSyncWindows'
|
||||
'setupListenersToSyncWindows',
|
||||
'updateBaseTheme',
|
||||
'updateMainColor',
|
||||
'updateSecColor'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="dataReady"
|
||||
id="app"
|
||||
:class="{
|
||||
hideOutlines: hideOutlines,
|
||||
|
@ -9,36 +10,39 @@
|
|||
<top-nav ref="topNav" />
|
||||
<side-nav ref="sideNav" />
|
||||
<ft-flex-box
|
||||
v-if="showUpdatesBanner || showBlogBanner"
|
||||
class="flexBox routerView"
|
||||
:class="{ expand: !isOpen }"
|
||||
>
|
||||
<ft-notification-banner
|
||||
v-if="showUpdatesBanner"
|
||||
class="banner"
|
||||
:message="updateBannerMessage"
|
||||
@click="handleUpdateBannerClick"
|
||||
/>
|
||||
<ft-notification-banner
|
||||
v-if="showBlogBanner"
|
||||
class="banner"
|
||||
:message="blogBannerMessage"
|
||||
@click="handleNewBlogBannerClick"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
<transition
|
||||
v-if="dataReady"
|
||||
mode="out-in"
|
||||
name="fade"
|
||||
>
|
||||
<!-- <keep-alive> -->
|
||||
<RouterView
|
||||
ref="router"
|
||||
class="routerView"
|
||||
:class="{ expand: !isOpen }"
|
||||
/>
|
||||
<div
|
||||
v-if="showUpdatesBanner || showBlogBanner"
|
||||
class="banner-wrapper"
|
||||
>
|
||||
<ft-notification-banner
|
||||
v-if="showUpdatesBanner"
|
||||
class="banner"
|
||||
:message="updateBannerMessage"
|
||||
@click="handleUpdateBannerClick"
|
||||
/>
|
||||
<ft-notification-banner
|
||||
v-if="showBlogBanner"
|
||||
class="banner"
|
||||
:message="blogBannerMessage"
|
||||
@click="handleNewBlogBannerClick"
|
||||
/>
|
||||
</div>
|
||||
<transition
|
||||
v-if="dataReady"
|
||||
mode="out-in"
|
||||
name="fade"
|
||||
>
|
||||
<!-- <keep-alive> -->
|
||||
<RouterView
|
||||
ref="router"
|
||||
class="routerView"
|
||||
/>
|
||||
<!-- </keep-alive> -->
|
||||
</transition>
|
||||
</transition>
|
||||
</ft-flex-box>
|
||||
|
||||
<ft-prompt
|
||||
v-if="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
|
||||
}
|
||||
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 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 === '') {
|
||||
// User canceled the save dialog
|
||||
return
|
||||
|
@ -764,7 +766,7 @@ export default Vue.extend({
|
|||
return object
|
||||
})
|
||||
|
||||
const response = await this.showSaveDialog(options)
|
||||
const response = await this.showSaveDialog({ options })
|
||||
if (response.canceled || response.filePath === '') {
|
||||
// User canceled the save dialog
|
||||
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 === '') {
|
||||
// User canceled the save dialog
|
||||
return
|
||||
|
@ -855,10 +857,14 @@ export default Vue.extend({
|
|||
let exportText = 'Channel ID,Channel URL,Channel title\n'
|
||||
this.profileList[0].subscriptions.forEach((channel) => {
|
||||
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'
|
||||
const response = await this.showSaveDialog(options)
|
||||
const response = await this.showSaveDialog({ options })
|
||||
if (response.canceled || response.filePath === '') {
|
||||
// User canceled the save dialog
|
||||
return
|
||||
|
@ -911,7 +917,7 @@ export default Vue.extend({
|
|||
newPipeObject.subscriptions.push(subscription)
|
||||
})
|
||||
|
||||
const response = await this.showSaveDialog(options)
|
||||
const response = await this.showSaveDialog({ options })
|
||||
if (response.canceled || response.filePath === '') {
|
||||
// User canceled the save dialog
|
||||
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 === '') {
|
||||
// User canceled the save dialog
|
||||
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 === '') {
|
||||
// User canceled the save dialog
|
||||
return
|
||||
|
@ -1303,7 +1309,7 @@ export default Vue.extend({
|
|||
|
||||
getChannelInfoLocal: function (channelId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ytch.getChannelInfo(channelId, 'latest').then(async (response) => {
|
||||
ytch.getChannelInfo({ channelId: channelId }).then(async (response) => {
|
||||
resolve(response)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue'
|
||||
import FtFlexBox from '../ft-flex-box/ft-flex-box.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 FtInput from '../ft-input/ft-input.vue'
|
||||
import { mapActions } from 'vuex'
|
||||
|
@ -12,19 +13,36 @@ export default Vue.extend({
|
|||
components: {
|
||||
'ft-toggle-switch': FtToggleSwitch,
|
||||
'ft-flex-box': FtFlexBox,
|
||||
'ft-select': FtSelect,
|
||||
'ft-button': FtButton,
|
||||
'ft-input': FtInput
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
askForDownloadPath: this.$store.getters.getDownloadFolderPath === ''
|
||||
askForDownloadPath: false,
|
||||
downloadBehaviorValues: [
|
||||
'download',
|
||||
'open'
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
downloadPath: function() {
|
||||
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: {
|
||||
handleDownloadingSettingChange: function (value) {
|
||||
this.askForDownloadPath = value
|
||||
|
@ -42,7 +60,8 @@ export default Vue.extend({
|
|||
this.updateDownloadFolderPath(folder.filePaths[0])
|
||||
},
|
||||
...mapActions([
|
||||
'updateDownloadFolderPath'
|
||||
'updateDownloadFolderPath',
|
||||
'updateDownloadBehavior'
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,19 @@
|
|||
</h3>
|
||||
</summary>
|
||||
<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
|
||||
:label="$t('Settings.Download Settings.Ask Download Path')"
|
||||
:default-value="askForDownloadPath"
|
||||
|
@ -14,7 +26,7 @@
|
|||
/>
|
||||
</ft-flex-box>
|
||||
<ft-flex-box
|
||||
v-if="!askForDownloadPath"
|
||||
v-if="!askForDownloadPath && downloadBehavior === 'download'"
|
||||
>
|
||||
<ft-input
|
||||
class="folderDisplay"
|
||||
|
@ -25,7 +37,7 @@
|
|||
/>
|
||||
</ft-flex-box>
|
||||
<ft-flex-box
|
||||
v-if="!askForDownloadPath"
|
||||
v-if="!askForDownloadPath && downloadBehavior === 'download'"
|
||||
>
|
||||
<ft-button
|
||||
:label="$t('Settings.Download Settings.Choose Path')"
|
||||
|
|
|
@ -16,11 +16,6 @@ export default Vue.extend({
|
|||
required: true
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
test: 'hello'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
listType: function () {
|
||||
return this.$store.getters.getListType
|
||||
|
|
|
@ -44,11 +44,11 @@ export default Vue.extend({
|
|||
type: String,
|
||||
default: 'bottom'
|
||||
},
|
||||
dropdownNames: {
|
||||
type: Array,
|
||||
default: () => { return [] }
|
||||
},
|
||||
dropdownValues: {
|
||||
dropdownOptions: {
|
||||
// Array of objects with these properties
|
||||
// - type: ('labelValue'|'divider', default to 'labelValue' for less typing)
|
||||
// - label: String (if type == 'labelValue')
|
||||
// - value: String (if type == 'labelValue')
|
||||
type: Array,
|
||||
default: () => { return [] }
|
||||
}
|
||||
|
@ -107,18 +107,18 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
handleIconClick: function () {
|
||||
if (this.forceDropdown || (this.dropdownNames.length > 0 && this.dropdownValues.length > 0)) {
|
||||
if (this.forceDropdown || (this.dropdownOptions.length > 0)) {
|
||||
this.toggleDropdown()
|
||||
} else {
|
||||
this.$emit('click')
|
||||
}
|
||||
},
|
||||
|
||||
handleDropdownClick: function (index) {
|
||||
handleDropdownClick: function ({ url, index }) {
|
||||
if (this.returnIndex) {
|
||||
this.$emit('click', index)
|
||||
} else {
|
||||
this.$emit('click', this.dropdownValues[index])
|
||||
this.$emit('click', url)
|
||||
}
|
||||
|
||||
this.focusOut()
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
list-style-type: none
|
||||
|
||||
.listItem
|
||||
padding: 10px
|
||||
padding: 8px 10px
|
||||
margin: 0
|
||||
white-space: nowrap
|
||||
cursor: pointer
|
||||
|
@ -96,3 +96,11 @@
|
|||
&:active
|
||||
background-color: var(--side-nav-active-color)
|
||||
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>
|
||||
<ul
|
||||
v-if="dropdownNames.length > 0"
|
||||
v-if="dropdownOptions.length > 0"
|
||||
class="list"
|
||||
>
|
||||
<li
|
||||
v-for="(label, index) in dropdownNames"
|
||||
v-for="(option, index) in dropdownOptions"
|
||||
:key="index"
|
||||
class="listItem"
|
||||
@click="handleDropdownClick(index)"
|
||||
:class="option.type === 'divider' ? 'listItemDivider' : 'listItem'"
|
||||
@click="handleDropdownClick({url: option.value, index: index})"
|
||||
>
|
||||
{{ label }}
|
||||
{{ option.type === 'divider' ? '' : option.label }}
|
||||
</li>
|
||||
</ul>
|
||||
</slot>
|
||||
|
|
|
@ -2,6 +2,35 @@
|
|||
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{
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
|
@ -9,33 +38,27 @@
|
|||
|
||||
.clearInputTextButton {
|
||||
position: absolute;
|
||||
/* horizontal intentionally reduced to keep "I-beam pointer" visible */
|
||||
padding: 10px 8px;
|
||||
margin: 0 3px;
|
||||
padding: 10px;
|
||||
top: 5px;
|
||||
left: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
border-radius: 100%;
|
||||
color: var(--primary-text-color);
|
||||
|
||||
opacity: 0;
|
||||
|
||||
-moz-transition: background 0.2s ease-in, opacity 0.2s ease-in;
|
||||
-o-transition: background 0.2s ease-in, opacity 0.2s ease-in;
|
||||
transition: background 0.2s ease-in, opacity 0.2s ease-in;
|
||||
-moz-transition: background 0.2s ease-in;
|
||||
-o-transition: background 0.2s ease-in;
|
||||
transition: background 0.2s ease-in;
|
||||
}
|
||||
|
||||
.clearInputTextButton:hover {
|
||||
.clearInputTextButton.visible:hover {
|
||||
background-color: var(--side-nav-hover-color);
|
||||
}
|
||||
.clearInputTextButton.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.forceTextColor .clearInputTextButton:hover {
|
||||
background-color: var(--primary-color-hover);
|
||||
}
|
||||
|
||||
.clearInputTextButton:active {
|
||||
.clearInputTextButton.visible:active {
|
||||
background-color: var(--tertiary-text-color);
|
||||
-moz-transition: background 0.2s ease-in;
|
||||
-o-transition: background 0.2s ease-in;
|
||||
|
@ -55,20 +78,20 @@
|
|||
}
|
||||
|
||||
.ft-input {
|
||||
box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
height: 45px;
|
||||
color: var(--secondary-text-color);
|
||||
border-radius: 5px;
|
||||
background-color: var(--search-bar-color);
|
||||
box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
height: 45px;
|
||||
color: var(--secondary-text-color);
|
||||
border-radius: 5px;
|
||||
background-color: var(--search-bar-color);
|
||||
}
|
||||
|
||||
.ft-input-component ::-webkit-input-placeholder {
|
||||
|
@ -93,10 +116,11 @@
|
|||
|
||||
.inputAction {
|
||||
position: absolute;
|
||||
padding: 10px 8px;
|
||||
margin: 0 3px;
|
||||
padding: 10px;
|
||||
top: 5px;
|
||||
right: 0;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
border-radius: 100%;
|
||||
color: var(--primary-text-color);
|
||||
/* this should look disabled by default */
|
||||
opacity: 50%;
|
||||
|
@ -125,7 +149,7 @@
|
|||
With arrow present means
|
||||
the text might get under the arrow with normal padding
|
||||
*/
|
||||
padding-right: 2em;
|
||||
padding-right: calc(36px + 6px);
|
||||
}
|
||||
|
||||
.inputAction.enabled:hover {
|
||||
|
@ -173,7 +197,3 @@
|
|||
background-color: var(--scrollbar-color-hover);
|
||||
/* color: white; */
|
||||
}
|
||||
|
||||
.showClearTextButton .ft-input {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
|
|
@ -87,28 +87,6 @@ export default Vue.extend({
|
|||
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 () {
|
||||
this.id = this._uid
|
||||
this.inputData = this.value
|
||||
|
@ -136,14 +114,20 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
handleClearTextClick: function () {
|
||||
// No action if no input text
|
||||
if (!this.inputDataPresent) { return }
|
||||
|
||||
this.inputData = ''
|
||||
this.handleActionIconChange()
|
||||
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
|
||||
const inputElement = document.getElementById(this.id)
|
||||
inputElement.focus()
|
||||
|
||||
this.$emit('clear')
|
||||
},
|
||||
|
||||
handleActionIconChange: function() {
|
||||
|
@ -200,7 +184,7 @@ export default Vue.extend({
|
|||
|
||||
if (inputElement !== null) {
|
||||
inputElement.addEventListener('keydown', (event) => {
|
||||
if (event.keyCode === 13) {
|
||||
if (event.key === 'Enter') {
|
||||
this.handleClick()
|
||||
}
|
||||
})
|
||||
|
@ -214,14 +198,14 @@ export default Vue.extend({
|
|||
this.handleClick()
|
||||
},
|
||||
|
||||
handleKeyDown: function (keyCode) {
|
||||
if (this.dataList.length === 0) { return }
|
||||
handleKeyDown: function (event) {
|
||||
if (this.visibleDataList.length === 0) { return }
|
||||
// Update selectedOption based on arrow key pressed
|
||||
if (keyCode === 40) {
|
||||
this.searchState.selectedOption = (this.searchState.selectedOption + 1) % this.dataList.length
|
||||
} else if (keyCode === 38) {
|
||||
if (this.searchState.selectedOption === -1) {
|
||||
this.searchState.selectedOption = this.dataList.length - 1
|
||||
if (event.key === 'ArrowDown') {
|
||||
this.searchState.selectedOption = (this.searchState.selectedOption + 1) % this.visibleDataList.length
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
if (this.searchState.selectedOption < 1) {
|
||||
this.searchState.selectedOption = this.visibleDataList.length - 1
|
||||
} else {
|
||||
this.searchState.selectedOption--
|
||||
}
|
||||
|
@ -230,14 +214,13 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
// Key pressed isn't enter
|
||||
if (keyCode !== 13) {
|
||||
if (event.key !== 'Enter') {
|
||||
this.searchState.showOptions = true
|
||||
}
|
||||
// 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]
|
||||
} else {
|
||||
this.updateVisibleDataList()
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
forceTextColor: forceTextColor,
|
||||
showActionButton: showActionButton,
|
||||
showClearTextButton: showClearTextButton,
|
||||
clearTextButtonVisible: inputDataPresent,
|
||||
disabled: disabled
|
||||
}"
|
||||
>
|
||||
|
@ -22,11 +23,11 @@
|
|||
/>
|
||||
</label>
|
||||
<font-awesome-icon
|
||||
v-if="showClearTextButton && clearTextButtonExisting"
|
||||
v-if="showClearTextButton"
|
||||
icon="times-circle"
|
||||
class="clearInputTextButton"
|
||||
:class="{
|
||||
visible: clearTextButtonVisible
|
||||
visible: inputDataPresent
|
||||
}"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
|
@ -37,6 +38,7 @@
|
|||
/>
|
||||
<input
|
||||
:id="id"
|
||||
ref="input"
|
||||
v-model="inputData"
|
||||
:list="idDataList"
|
||||
class="ft-input"
|
||||
|
@ -47,7 +49,7 @@
|
|||
@input="e => handleInput(e.target.value)"
|
||||
@focus="handleFocus"
|
||||
@blur="handleInputBlur"
|
||||
@keydown="e => handleKeyDown(e.keyCode)"
|
||||
@keydown="handleKeyDown"
|
||||
>
|
||||
<font-awesome-icon
|
||||
v-if="showActionButton"
|
||||
|
|
|
@ -52,8 +52,8 @@
|
|||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
cursor: pointer;
|
||||
color: var(--tertiary-text-color);
|
||||
background-color: var(--secondary-card-bg-color);
|
||||
color: var(--secondary-text-color);
|
||||
background-color: var(--side-nav-color);
|
||||
-webkit-transition: background 0.2s ease-out;
|
||||
-moz-transition: background 0.2s ease-out;
|
||||
-o-transition: background 0.2s ease-out;
|
||||
|
@ -61,7 +61,7 @@
|
|||
}
|
||||
|
||||
.buttonOption:hover {
|
||||
background-color: var(--search-bar-color);
|
||||
background-color: var(--side-nav-hover-color);
|
||||
-moz-transition: background 0.2s ease-in;
|
||||
-o-transition: background 0.2s ease-in;
|
||||
transition: background 0.2s ease-in;
|
||||
|
|
|
@ -59,20 +59,7 @@ export default Vue.extend({
|
|||
isFavorited: false,
|
||||
isUpcoming: false,
|
||||
isPremium: false,
|
||||
hideViews: false,
|
||||
optionsValues: [
|
||||
'history',
|
||||
'openYoutube',
|
||||
'copyYoutube',
|
||||
'openYoutubeEmbed',
|
||||
'copyYoutubeEmbed',
|
||||
'openInvidious',
|
||||
'copyInvidious',
|
||||
'openYoutubeChannel',
|
||||
'copyYoutubeChannel',
|
||||
'openInvidiousChannel',
|
||||
'copyInvidiousChannel'
|
||||
]
|
||||
hideViews: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -130,27 +117,71 @@ export default Vue.extend({
|
|||
return (this.watchProgress / this.data.lengthSeconds) * 100
|
||||
},
|
||||
|
||||
optionsNames: function () {
|
||||
const names = [
|
||||
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')
|
||||
]
|
||||
dropdownOptions: function () {
|
||||
const options = []
|
||||
|
||||
if (this.watched) {
|
||||
names.unshift(this.$t('Video.Remove From History'))
|
||||
} else {
|
||||
names.unshift(this.$t('Video.Mark As Watched'))
|
||||
}
|
||||
options.push(
|
||||
{
|
||||
label: this.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 () {
|
||||
|
@ -208,12 +239,6 @@ export default Vue.extend({
|
|||
return this.$store.getters.getSaveWatchedProgress
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: function () {
|
||||
this.parseVideoData()
|
||||
this.checkIfWatched()
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.parseVideoData()
|
||||
this.checkIfWatched()
|
||||
|
@ -227,6 +252,7 @@ export default Vue.extend({
|
|||
watchProgress: this.watchProgress,
|
||||
playbackRate: this.defaultPlayback,
|
||||
videoId: this.id,
|
||||
videoLength: this.data.lengthSeconds,
|
||||
playlistId: this.playlistId,
|
||||
playlistIndex: this.playlistIndex,
|
||||
playlistReverse: this.playlistReverse,
|
||||
|
@ -445,6 +471,7 @@ export default Vue.extend({
|
|||
})
|
||||
|
||||
this.watched = false
|
||||
this.watchProgress = 0
|
||||
},
|
||||
|
||||
addToPlaylist: function () {
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
@use "../../sass-partials/_ft-list-item"
|
||||
|
||||
.thumbnailLink:hover
|
||||
outline: 3px solid var(--side-nav-hover-color)
|
||||
|
|
|
@ -66,13 +66,13 @@
|
|||
<div class="info">
|
||||
<ft-icon-button
|
||||
class="optionsButton"
|
||||
icon="ellipsis-v"
|
||||
title="More Options"
|
||||
theme="base-no-default"
|
||||
:size="16"
|
||||
:use-shadow="false"
|
||||
dropdown-position-x="left"
|
||||
:dropdown-names="optionsNames"
|
||||
:dropdown-values="optionsValues"
|
||||
:dropdown-options="dropdownOptions"
|
||||
@click="handleOptionsClick"
|
||||
/>
|
||||
<router-link
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
.colorOption {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
-webkit-border-radius: 50%;
|
||||
}
|
||||
|
||||
.colorOption:hover {
|
||||
box-shadow: 0 0 0 2px var(--side-nav-hover-color);
|
||||
}
|
||||
|
||||
.initial {
|
||||
font-size: 20px;
|
||||
line-height: 1em;
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
#profileList {
|
||||
|
@ -57,6 +62,7 @@
|
|||
float: left;
|
||||
position: relative;
|
||||
bottom: 5px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.profileName {
|
||||
|
|
|
@ -29,10 +29,15 @@
|
|||
width: auto
|
||||
|
||||
@at-root
|
||||
.dark &
|
||||
.dark &, .system[data-system-theme*='dark'] &
|
||||
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)
|
||||
|
||||
.invidious
|
||||
|
@ -48,8 +53,11 @@
|
|||
margin-right: 2px
|
||||
|
||||
@at-root
|
||||
.dark &
|
||||
.dark &,
|
||||
.black &,
|
||||
.dracula &,
|
||||
.system[data-system-theme*='dark'] &
|
||||
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)
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
}
|
||||
|
||||
.pure-material-slider > input:disabled + span {
|
||||
color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
opacity: 0.38;
|
||||
}
|
||||
|
||||
/* Webkit | Track */
|
||||
|
|
|
@ -26,6 +26,10 @@ export default Vue.extend({
|
|||
valueExtension: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
<input
|
||||
:id="id"
|
||||
v-model.number="currentValue"
|
||||
:disabled="disabled"
|
||||
type="range"
|
||||
:min="minValue"
|
||||
:max="maxValue"
|
||||
:step="step"
|
||||
@change="$emit('change', $event.target.value)"
|
||||
@change="$emit('change', currentValue)"
|
||||
>
|
||||
<span>
|
||||
{{ 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 qualitySelector from '@silvermine/videojs-quality-selector'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import 'videojs-overlay/dist/videojs-overlay'
|
||||
import 'videojs-overlay/dist/videojs-overlay.css'
|
||||
import 'videojs-vtt-thumbnails-freetube'
|
||||
import 'videojs-contrib-quality-levels'
|
||||
import 'videojs-http-source-selector'
|
||||
import { ipcRenderer } from 'electron'
|
||||
|
||||
import { IpcChannels } from '../../../constants'
|
||||
|
||||
|
@ -85,6 +85,10 @@ export default Vue.extend({
|
|||
useHls: false,
|
||||
selectedDefaultQuality: '',
|
||||
selectedQuality: '',
|
||||
selectedResolution: '',
|
||||
selectedBitrate: '',
|
||||
selectedMimeType: '',
|
||||
selectedFPS: 0,
|
||||
using60Fps: false,
|
||||
maxFramerate: 0,
|
||||
activeSourceList: [],
|
||||
|
@ -92,6 +96,10 @@ export default Vue.extend({
|
|||
mouseTimeout: null,
|
||||
touchTimeout: null,
|
||||
lastTouchTime: null,
|
||||
playerStats: null,
|
||||
statsModal: null,
|
||||
showStatsModal: false,
|
||||
statsModalEventName: 'updateStats',
|
||||
dataSetup: {
|
||||
fluid: true,
|
||||
nativeTextTracks: false,
|
||||
|
@ -108,6 +116,7 @@ export default Vue.extend({
|
|||
'seekToLive',
|
||||
'remainingTimeDisplay',
|
||||
'customControlSpacer',
|
||||
'screenshotButton',
|
||||
'playbackRateMenuButton',
|
||||
'loopButton',
|
||||
'chaptersButton',
|
||||
|
@ -119,56 +128,6 @@ export default Vue.extend({
|
|||
'qualitySelector',
|
||||
'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() {
|
||||
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";>'
|
||||
for (const stat of stats) {
|
||||
formattedStats += `<li style="font-size: 75%">${stat[0]}: ${stat[1]}</li>`
|
||||
sponsorSkips: function () {
|
||||
const sponsorCats = ['sponsor',
|
||||
'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: {
|
||||
selectedQuality: function() {
|
||||
this.currentFps()
|
||||
showStatsModal: function() {
|
||||
this.player.trigger(this.statsModalEventName)
|
||||
},
|
||||
|
||||
enableScreenshot: function() {
|
||||
this.toggleScreenshotButton()
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
|
@ -270,11 +305,19 @@ export default Vue.extend({
|
|||
this.volume = volume
|
||||
}
|
||||
|
||||
this.dataSetup.playbackRates = this.playbackRates
|
||||
|
||||
this.createFullWindowButton()
|
||||
this.createLoopButton()
|
||||
this.createToggleTheatreModeButton()
|
||||
this.createScreenshotButton()
|
||||
this.determineFormatType()
|
||||
this.determineMaxFramerate()
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler('play', () => this.player.play())
|
||||
navigator.mediaSession.setActionHandler('pause', () => this.player.pause())
|
||||
}
|
||||
},
|
||||
beforeDestroy: function () {
|
||||
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) {
|
||||
const { ipcRenderer } = require('electron')
|
||||
ipcRenderer.send(IpcChannels.STOP_POWER_SAVE_BLOCKER, this.powerSaveBlocker)
|
||||
|
@ -411,20 +460,34 @@ export default Vue.extend({
|
|||
this.player.on('ready', () => {
|
||||
this.$emit('ready')
|
||||
this.checkAspectRatio()
|
||||
this.createStatsModal()
|
||||
if (this.captionHybridList.length !== 0) {
|
||||
this.transformAndInsertCaptions()
|
||||
}
|
||||
this.toggleScreenshotButton()
|
||||
})
|
||||
|
||||
this.player.on('ended', () => {
|
||||
this.$emit('ended')
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.playbackState = 'none'
|
||||
}
|
||||
})
|
||||
|
||||
this.player.on('error', (error, message) => {
|
||||
this.$emit('error', error.target.player.error_)
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.playbackState = 'none'
|
||||
}
|
||||
})
|
||||
|
||||
this.player.on('play', async function () {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.playbackState = 'playing'
|
||||
}
|
||||
|
||||
if (this.usingElectron) {
|
||||
const { ipcRenderer } = require('electron')
|
||||
this.powerSaveBlocker =
|
||||
|
@ -433,6 +496,10 @@ export default Vue.extend({
|
|||
})
|
||||
|
||||
this.player.on('pause', function () {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.playbackState = 'paused'
|
||||
}
|
||||
|
||||
if (this.usingElectron && this.powerSaveBlocker !== null) {
|
||||
const { ipcRenderer } = require('electron')
|
||||
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', (_) => {
|
||||
const settings = this.player.textTrackSettings.getValues()
|
||||
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() {
|
||||
this.sponsorBlockSkipSegments({
|
||||
videoId: this.videoId,
|
||||
categories: ['sponsor']
|
||||
categories: this.sponsorSkips.seekBar
|
||||
}).then((skipSegments) => {
|
||||
if (skipSegments.length === 0) {
|
||||
return
|
||||
|
@ -469,7 +561,8 @@ export default Vue.extend({
|
|||
this.addSponsorBlockMarker({
|
||||
time: 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 (this.sponsorBlockShowSkippedToast) {
|
||||
this.showSkippedSponsorSegmentInformation(skippedCategory)
|
||||
if (this.sponsorSkips.autoSkip[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')
|
||||
case 'music_offtopic':
|
||||
return this.$t('Video.Sponsor Block category.music offtopic')
|
||||
case 'filler':
|
||||
return this.$t('Video.Sponsor Block category.filler')
|
||||
default:
|
||||
console.error(`Unknown translation for SponsorBlock category ${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) {
|
||||
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.position = 'absolute'
|
||||
markerDiv.style.opacity = '0.6'
|
||||
markerDiv.style['background-color'] = marker.color
|
||||
markerDiv.style.width = (marker.duration / 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)
|
||||
},
|
||||
|
@ -851,6 +929,18 @@ export default Vue.extend({
|
|||
qualityElement.innerText = 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()
|
||||
|
||||
$('.quality-item').removeClass('quality-selected')
|
||||
|
@ -999,7 +1089,7 @@ export default Vue.extend({
|
|||
changePlayBackRate: function (rate) {
|
||||
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)
|
||||
}
|
||||
},
|
||||
|
@ -1012,7 +1102,7 @@ export default Vue.extend({
|
|||
if (this.maxFramerate === 60 && quality.height >= 480) {
|
||||
for (let i = 0; i < this.adaptiveFormats.length; i++) {
|
||||
if (this.adaptiveFormats[i].bitrate === quality.bitrate) {
|
||||
fps = this.adaptiveFormats[i].fps
|
||||
fps = this.adaptiveFormats[i].fps ? this.adaptiveFormats[i].fps : 30
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -1077,8 +1167,8 @@ export default Vue.extend({
|
|||
|
||||
toggleVideoLoop: async function () {
|
||||
if (!this.player.loop()) {
|
||||
const currentTheme = localStorage.getItem('mainColor')
|
||||
const colorNames = this.$store.state.utils.colorClasses
|
||||
const currentTheme = this.$store.state.settings.mainColor
|
||||
const colorNames = this.$store.state.utils.colorNames
|
||||
const colorValues = this.$store.state.utils.colorValues
|
||||
|
||||
const nameIndex = colorNames.findIndex((color) => {
|
||||
|
@ -1162,6 +1252,168 @@ export default Vue.extend({
|
|||
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) {
|
||||
if (levels.levels_.length === 0) {
|
||||
setTimeout(() => {
|
||||
|
@ -1209,6 +1461,10 @@ export default Vue.extend({
|
|||
return format.bitrate === quality.bitrate
|
||||
})
|
||||
|
||||
if (typeof adaptiveFormat === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
this.activeAdaptiveFormats.push(adaptiveFormat)
|
||||
|
||||
fps = adaptiveFormat.fps ? adaptiveFormat.fps : 30
|
||||
|
@ -1426,7 +1682,71 @@ export default Vue.extend({
|
|||
handleTouchEnd: function (event) {
|
||||
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) {
|
||||
const activeInputs = $('.ft-input')
|
||||
|
||||
|
@ -1452,7 +1772,7 @@ export default Vue.extend({
|
|||
// J Key
|
||||
// Rewind by 2x the time-skip interval (in seconds)
|
||||
event.preventDefault()
|
||||
this.changeDurationBySeconds(-this.defaultSkipInterval * 2)
|
||||
this.changeDurationBySeconds(-this.defaultSkipInterval * this.player.playbackRate() * 2)
|
||||
break
|
||||
case 75:
|
||||
// K Key
|
||||
|
@ -1464,19 +1784,19 @@ export default Vue.extend({
|
|||
// L Key
|
||||
// Fast-Forward by 2x the time-skip interval (in seconds)
|
||||
event.preventDefault()
|
||||
this.changeDurationBySeconds(this.defaultSkipInterval * 2)
|
||||
this.changeDurationBySeconds(this.defaultSkipInterval * this.player.playbackRate() * 2)
|
||||
break
|
||||
case 79:
|
||||
// O Key
|
||||
// Decrease playback rate by 0.25x
|
||||
event.preventDefault()
|
||||
this.changePlayBackRate(-0.25)
|
||||
this.changePlayBackRate(-this.videoPlaybackRateInterval)
|
||||
break
|
||||
case 80:
|
||||
// P Key
|
||||
// Increase playback rate by 0.25x
|
||||
event.preventDefault()
|
||||
this.changePlayBackRate(0.25)
|
||||
this.changePlayBackRate(this.videoPlaybackRateInterval)
|
||||
break
|
||||
case 70:
|
||||
// F Key
|
||||
|
@ -1512,13 +1832,23 @@ export default Vue.extend({
|
|||
// Left Arrow Key
|
||||
// Rewind by the time-skip interval (in seconds)
|
||||
event.preventDefault()
|
||||
this.changeDurationBySeconds(-this.defaultSkipInterval * 1)
|
||||
this.changeDurationBySeconds(-this.defaultSkipInterval * this.player.playbackRate())
|
||||
break
|
||||
case 39:
|
||||
// Right Arrow Key
|
||||
// Fast-Forward by the time-skip interval (in seconds)
|
||||
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
|
||||
case 49:
|
||||
// 1 Key
|
||||
|
@ -1592,12 +1922,8 @@ export default Vue.extend({
|
|||
break
|
||||
case 68:
|
||||
// D Key
|
||||
// Toggle Picture in Picture Mode
|
||||
if (!this.player.isInPictureInPicture()) {
|
||||
this.player.requestPictureInPicture()
|
||||
} else if (this.player.isInPictureInPicture()) {
|
||||
this.player.exitPictureInPicture()
|
||||
}
|
||||
event.preventDefault()
|
||||
this.toggleShowStatsModal()
|
||||
break
|
||||
case 27:
|
||||
// esc Key
|
||||
|
@ -1615,86 +1941,11 @@ export default Vue.extend({
|
|||
// Toggle Theatre Mode
|
||||
this.toggleTheatreMode()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addPlayerStatsEvent: function() {
|
||||
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
|
||||
case 85:
|
||||
// U Key
|
||||
// Take screenshot
|
||||
this.takeScreenshot()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1703,7 +1954,11 @@ export default Vue.extend({
|
|||
'calculateColorLuminance',
|
||||
'updateDefaultCaptionSettings',
|
||||
'showToast',
|
||||
'sponsorBlockSkipSegments'
|
||||
'sponsorBlockSkipSegments',
|
||||
'parseScreenshotCustomFileName',
|
||||
'updateScreenshotFolderPath',
|
||||
'getPicturesPath',
|
||||
'showSaveDialog'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
controls
|
||||
preload="auto"
|
||||
:data-setup="JSON.stringify(dataSetup)"
|
||||
crossorigin="anonymous"
|
||||
@touchstart="handleTouchStart"
|
||||
@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 FtSlider from '../ft-slider/ft-slider.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({
|
||||
name: 'PlayerSettings',
|
||||
|
@ -13,7 +19,10 @@ export default Vue.extend({
|
|||
'ft-select': FtSelect,
|
||||
'ft-toggle-switch': FtToggleSwitch,
|
||||
'ft-slider': FtSlider,
|
||||
'ft-flex-box': FtFlexBox
|
||||
'ft-flex-box': FtFlexBox,
|
||||
'ft-button': FtButton,
|
||||
'ft-input': FtInput,
|
||||
'ft-tooltip': FtTooltip
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
|
@ -30,7 +39,24 @@ export default Vue.extend({
|
|||
480,
|
||||
720,
|
||||
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: {
|
||||
|
@ -106,6 +132,14 @@ export default Vue.extend({
|
|||
return this.$store.getters.getDisplayVideoPlayButton
|
||||
},
|
||||
|
||||
maxVideoPlaybackRate: function () {
|
||||
return parseInt(this.$store.getters.getMaxVideoPlaybackRate)
|
||||
},
|
||||
|
||||
videoPlaybackRateInterval: function () {
|
||||
return this.$store.getters.getVideoPlaybackRateInterval
|
||||
},
|
||||
|
||||
formatNames: function () {
|
||||
return [
|
||||
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.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: {
|
||||
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([
|
||||
'updateAutoplayVideos',
|
||||
'updateAutoplayPlaylists',
|
||||
|
@ -143,7 +269,17 @@ export default Vue.extend({
|
|||
'updateDefaultQuality',
|
||||
'updateVideoVolumeMouseScroll',
|
||||
'updateVideoPlaybackRateMouseScroll',
|
||||
'updateDisplayVideoPlayButton'
|
||||
'updateDisplayVideoPlayButton',
|
||||
'updateMaxVideoPlaybackRate',
|
||||
'updateVideoPlaybackRateInterval',
|
||||
'updateEnableScreenshot',
|
||||
'updateScreenshotFormat',
|
||||
'updateScreenshotQuality',
|
||||
'updateScreenshotAskPath',
|
||||
'updateScreenshotFolderPath',
|
||||
'updateScreenshotFilenamePattern',
|
||||
'parseScreenshotCustomFileName',
|
||||
'getPicturesPath'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1 +1,14 @@
|
|||
@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="×"
|
||||
@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-select
|
||||
|
@ -133,6 +149,88 @@
|
|||
@change="updateDefaultQuality"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -3,32 +3,44 @@
|
|||
|
||||
.playlistThumbnail img
|
||||
width: 100%
|
||||
|
||||
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
|
||||
max-height: 20vh
|
||||
overflow-y: auto
|
||||
white-space: break-spaces
|
||||
|
||||
@media only screen and (max-width: 500px)
|
||||
max-height: 10vh
|
||||
|
||||
.playlistChannel
|
||||
height: 70px
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 8px
|
||||
height: 40px
|
||||
|
||||
/* Indicates the box can be clicked to navigate */
|
||||
cursor: pointer
|
||||
|
||||
.channelThumbnail
|
||||
width: 70px
|
||||
width: 40px
|
||||
float: left
|
||||
border-radius: 200px 200px 200px 200px
|
||||
-webkit-border-radius: 200px 200px 200px 200px
|
||||
|
||||
.channelName
|
||||
float: left
|
||||
position: relative
|
||||
width: 200px
|
||||
margin-left: 10px
|
||||
top: 5px
|
||||
margin: 0
|
||||
font-size: 15px
|
||||
|
|
|
@ -8,22 +8,27 @@
|
|||
@click="playFirstVideo"
|
||||
>
|
||||
</div>
|
||||
<h2>
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ videoCount }} {{ $t("Playlist.Videos") }} - <span v-if="!hideViews">{{ viewCount }} {{ $t("Playlist.Views") }} -</span>
|
||||
<span v-if="infoSource !== 'local'">
|
||||
{{ $t("Playlist.Last Updated On") }}
|
||||
</span>
|
||||
{{ lastUpdated }}
|
||||
</p>
|
||||
|
||||
<div class="playlistStats">
|
||||
<h2 class="playlistTitle">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ videoCount }} {{ $t("Playlist.Videos") }} - <span v-if="!hideViews">{{ viewCount }} {{ $t("Playlist.Views") }} -</span>
|
||||
<span v-if="infoSource !== 'local'">
|
||||
{{ $t("Playlist.Last Updated On") }}
|
||||
</span>
|
||||
{{ lastUpdated }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="playlistDescription"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
v-text="description"
|
||||
/>
|
||||
|
||||
<hr>
|
||||
|
||||
<div
|
||||
class="playlistChannel"
|
||||
@click="goToChannel"
|
||||
|
@ -38,7 +43,9 @@
|
|||
{{ channelName }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<ft-list-dropdown
|
||||
:title="$t('Playlist.Share Playlist.Share Playlist')"
|
||||
:label-names="shareHeaders"
|
||||
|
|
|
@ -31,8 +31,7 @@ export default Vue.extend({
|
|||
return {
|
||||
isLoading: false,
|
||||
dataAvailable: false,
|
||||
proxyTestUrl: 'https://api.ipify.org?format=json',
|
||||
proxyTestUrl1: 'https://freegeoip.app/json/',
|
||||
proxyTestUrl: 'https://ipwho.is/',
|
||||
proxyId: '',
|
||||
proxyCountry: '',
|
||||
proxyRegion: '',
|
||||
|
@ -125,11 +124,11 @@ export default Vue.extend({
|
|||
if (!this.useProxy) {
|
||||
this.enableProxy()
|
||||
}
|
||||
$.getJSON(this.proxyTestUrl1, (response) => {
|
||||
$.getJSON(this.proxyTestUrl, (response) => {
|
||||
console.log(response)
|
||||
this.proxyIp = response.ip
|
||||
this.proxyCountry = response.country_name
|
||||
this.proxyRegion = response.region_name
|
||||
this.proxyCountry = response.country
|
||||
this.proxyRegion = response.region
|
||||
this.proxyCity = response.city
|
||||
this.dataAvailable = true
|
||||
}).fail((xhr, textStatus, error) => {
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
class="center"
|
||||
: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>
|
||||
<ft-flex-box>
|
||||
<ft-button
|
||||
|
|
|
@ -3,11 +3,10 @@
|
|||
height: calc(100vh - 60px);
|
||||
width: 200px;
|
||||
overflow-x: hidden;
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
z-index: 4;
|
||||
margin-top: 60px;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
top: 60px;
|
||||
z-index: 3;
|
||||
box-shadow: 1px -1px 1px -1px var(--primary-shadow-color);
|
||||
background-color: var(--side-nav-color);
|
||||
transition-property: width;
|
||||
|
@ -168,6 +167,10 @@
|
|||
}
|
||||
|
||||
.sideNav {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
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 FtInput from '../ft-input/ft-input.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({
|
||||
name: 'SponsorBlockSettings',
|
||||
|
@ -11,7 +12,22 @@ export default Vue.extend({
|
|||
'ft-card': FtCard,
|
||||
'ft-toggle-switch': FtToggleSwitch,
|
||||
'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: {
|
||||
useSponsorBlock: function () {
|
||||
|
|
|
@ -32,6 +32,13 @@
|
|||
@input="handleUpdateSponsorBlockUrl"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
<ft-flex-box>
|
||||
<ft-sponsor-block-category
|
||||
v-for="category in categories"
|
||||
:key="category"
|
||||
:category-name="category"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
</div>
|
||||
</details>
|
||||
</template>
|
||||
|
|
|
@ -19,10 +19,6 @@ export default Vue.extend({
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
currentBaseTheme: '',
|
||||
currentMainColor: '',
|
||||
currentSecColor: '',
|
||||
expandSideBar: false,
|
||||
minUiScale: 50,
|
||||
maxUiScale: 300,
|
||||
uiScaleStep: 5,
|
||||
|
@ -33,35 +29,11 @@ export default Vue.extend({
|
|||
'no'
|
||||
],
|
||||
baseThemeValues: [
|
||||
'system',
|
||||
'light',
|
||||
'dark',
|
||||
'black',
|
||||
'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
|
||||
},
|
||||
|
||||
baseTheme: function () {
|
||||
return this.$store.getters.getBaseTheme
|
||||
},
|
||||
|
||||
mainColor: function () {
|
||||
return this.$store.getters.getMainColor
|
||||
},
|
||||
|
||||
secColor: function () {
|
||||
return this.$store.getters.getSecColor
|
||||
},
|
||||
|
||||
isSideNavOpen: function () {
|
||||
return this.$store.getters.getIsSideNavOpen
|
||||
},
|
||||
|
@ -81,9 +65,15 @@ export default Vue.extend({
|
|||
disableSmoothScrolling: function () {
|
||||
return this.$store.getters.getDisableSmoothScrolling
|
||||
},
|
||||
|
||||
expandSideBar: function () {
|
||||
return this.$store.getters.getExpandSideBar
|
||||
},
|
||||
|
||||
hideLabelsSideBar: function () {
|
||||
return this.$store.getters.getHideLabelsSideBar
|
||||
},
|
||||
|
||||
restartPromptMessage: function () {
|
||||
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 () {
|
||||
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.Dark'),
|
||||
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 () {
|
||||
return [
|
||||
this.$t('Settings.Theme Settings.Main Color Theme.Red'),
|
||||
this.$t('Settings.Theme Settings.Main Color Theme.Pink'),
|
||||
this.$t('Settings.Theme Settings.Main Color Theme.Purple'),
|
||||
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')
|
||||
]
|
||||
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}`)
|
||||
})
|
||||
}
|
||||
},
|
||||
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
|
||||
},
|
||||
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) {
|
||||
if (this.isSideNavOpen !== value) {
|
||||
this.$store.commit('toggleSideNav')
|
||||
}
|
||||
|
||||
this.expandSideBar = value
|
||||
localStorage.setItem('expandSideBar', value)
|
||||
this.updateExpandSideBar(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([
|
||||
'updateBarColor',
|
||||
'updateBaseTheme',
|
||||
'updateMainColor',
|
||||
'updateSecColor',
|
||||
'updateExpandSideBar',
|
||||
'updateUiScale',
|
||||
'updateDisableSmoothScrolling',
|
||||
'updateHideLabelsSideBar'
|
||||
|
|
|
@ -43,21 +43,21 @@
|
|||
<ft-flex-box>
|
||||
<ft-select
|
||||
:placeholder="$t('Settings.Theme Settings.Base Theme.Base Theme')"
|
||||
:value="currentBaseTheme"
|
||||
:value="baseTheme"
|
||||
:select-names="baseThemeNames"
|
||||
:select-values="baseThemeValues"
|
||||
@change="updateBaseTheme"
|
||||
/>
|
||||
<ft-select
|
||||
:placeholder="$t('Settings.Theme Settings.Main Color Theme.Main Color Theme')"
|
||||
:value="currentMainColor"
|
||||
:value="mainColor"
|
||||
:select-names="colorNames"
|
||||
:select-values="colorValues"
|
||||
@change="updateMainColor"
|
||||
/>
|
||||
<ft-select
|
||||
:placeholder="$t('Settings.Theme Settings.Secondary Color Theme')"
|
||||
:value="currentSecColor"
|
||||
:value="secColor"
|
||||
:select-names="colorNames"
|
||||
:select-values="colorValues"
|
||||
@change="updateSecColor"
|
||||
|
|
|
@ -36,12 +36,12 @@ export default Vue.extend({
|
|||
return this.$store.getters.getEnableSearchSuggestions
|
||||
},
|
||||
|
||||
searchSettings: function () {
|
||||
return this.$store.getters.getSearchSettings
|
||||
searchInput: function () {
|
||||
return this.$refs.searchInput.$refs.input
|
||||
},
|
||||
|
||||
isSideNavOpen: function () {
|
||||
return this.$store.getters.getIsSideNavOpen
|
||||
searchSettings: function () {
|
||||
return this.$store.getters.getSearchSettings
|
||||
},
|
||||
|
||||
barColor: function () {
|
||||
|
@ -60,12 +60,16 @@ export default Vue.extend({
|
|||
return this.$store.getters.getBackendPreference
|
||||
},
|
||||
|
||||
expandSideBar: function () {
|
||||
return this.$store.getters.getExpandSideBar
|
||||
},
|
||||
|
||||
forwardText: function () {
|
||||
return this.$t('Forward')
|
||||
},
|
||||
|
||||
backwardText: function () {
|
||||
return this.$t('Backward')
|
||||
return this.$t('Back')
|
||||
},
|
||||
|
||||
newWindowText: function () {
|
||||
|
@ -80,9 +84,12 @@ export default Vue.extend({
|
|||
searchContainer.style.display = 'none'
|
||||
}
|
||||
|
||||
if (localStorage.getItem('expandSideBar') === 'true') {
|
||||
this.toggleSideNav()
|
||||
}
|
||||
// Store is not up-to-date when the component mounts, so we use timeout.
|
||||
setTimeout(() => {
|
||||
if (this.expandSideBar) {
|
||||
this.toggleSideNav()
|
||||
}
|
||||
}, 0)
|
||||
|
||||
window.addEventListener('resize', function (event) {
|
||||
const width = event.srcElement.innerWidth
|
||||
|
@ -190,6 +197,10 @@ export default Vue.extend({
|
|||
this.showFilters = false
|
||||
},
|
||||
|
||||
focusSearch: function () {
|
||||
this.searchInput.focus()
|
||||
},
|
||||
|
||||
getSearchSuggestionsDebounce: function (query) {
|
||||
if (this.enableSearchSuggestions) {
|
||||
this.debounceSearchResults(query)
|
||||
|
|
|
@ -4,12 +4,13 @@
|
|||
@content
|
||||
|
||||
.topNav
|
||||
position: fixed
|
||||
position: sticky
|
||||
z-index: 4
|
||||
left: 0
|
||||
right: 0
|
||||
top: 0
|
||||
height: 60px
|
||||
width: 100%
|
||||
line-height: 60px
|
||||
background-color: var(--card-bg-color)
|
||||
-webkit-box-shadow: 0px 2px 1px 0px var(--primary-shadow-color)
|
||||
|
@ -24,6 +25,9 @@
|
|||
@include top-nav-is-colored
|
||||
background-color: var(--primary-color)
|
||||
|
||||
@media only screen and (max-width: 680px)
|
||||
position: fixed
|
||||
|
||||
.menuIcon // the hamburger button
|
||||
@media only screen and (max-width: 680px)
|
||||
display: none
|
||||
|
@ -74,6 +78,8 @@
|
|||
|
||||
.side // parts of the top nav either side of the search bar
|
||||
display: flex
|
||||
gap: 3px
|
||||
margin: 0 6px
|
||||
align-items: center
|
||||
|
||||
&.profiles
|
||||
|
@ -158,7 +164,3 @@
|
|||
left: 0
|
||||
right: 0
|
||||
margin: 95px 10px 0px
|
||||
|
||||
@media only screen and (min-width: 681px)
|
||||
&.expand
|
||||
margin-left: 100px
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
icon="arrow-left"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:title="forwardText"
|
||||
:title="backwardText"
|
||||
@click="historyBack"
|
||||
@keypress="historyBack"
|
||||
/>
|
||||
|
@ -66,6 +66,7 @@
|
|||
<div class="middle">
|
||||
<div class="searchContainer">
|
||||
<ft-input
|
||||
ref="searchInput"
|
||||
:placeholder="$t('Search / Go to URL')"
|
||||
class="searchInput"
|
||||
:is-search="true"
|
||||
|
@ -89,7 +90,6 @@
|
|||
<ft-search-filters
|
||||
v-show="showFilters"
|
||||
class="searchFilters"
|
||||
:class="{ expand: !isSideNavOpen }"
|
||||
@filterValueUpdated="handleSearchFilterValueChanged"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -78,6 +78,12 @@
|
|||
font-size: 12px;
|
||||
}
|
||||
|
||||
.commentMemberIcon {
|
||||
margin-left: 5px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.commentLikeCount {
|
||||
font-size: 11px;
|
||||
margin-left: 70px;
|
||||
|
|
|
@ -269,6 +269,11 @@ export default Vue.extend({
|
|||
comment.likes = null
|
||||
}
|
||||
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
|
||||
})
|
||||
|
|
|
@ -68,6 +68,14 @@
|
|||
>
|
||||
{{ comment.author }}
|
||||
</span>
|
||||
<img
|
||||
v-if="comment.isMember"
|
||||
:src="comment.memberIconUrl"
|
||||
:title="$t('Comments.Member')"
|
||||
:aria-label="$t('Comments.Member')"
|
||||
class="commentMemberIcon"
|
||||
alt=""
|
||||
>
|
||||
<span class="commentDate">
|
||||
{{ comment.time }}
|
||||
</span>
|
||||
|
@ -137,6 +145,12 @@
|
|||
>
|
||||
{{ reply.author }}
|
||||
</span>
|
||||
<img
|
||||
v-if="reply.isMember"
|
||||
:src="reply.memberIconUrl"
|
||||
class="commentMemberIcon"
|
||||
alt=""
|
||||
>
|
||||
<span class="commentDate">
|
||||
{{ reply.time }}
|
||||
</span>
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
}
|
||||
|
||||
.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;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
|
|
@ -118,12 +118,7 @@ export default Vue.extend({
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
formatTypeLabel: 'VIDEO FORMATS',
|
||||
formatTypeValues: [
|
||||
'dash',
|
||||
'legacy',
|
||||
'audio'
|
||||
]
|
||||
formatTypeLabel: 'VIDEO FORMATS'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -175,23 +170,33 @@ export default Vue.extend({
|
|||
return this.inFavoritesPlaylist ? 'base favorite' : 'base'
|
||||
},
|
||||
|
||||
downloadLinkNames: function () {
|
||||
downloadLinkOptions: function () {
|
||||
return this.downloadLinks.map((download) => {
|
||||
return download.label
|
||||
return {
|
||||
label: download.label,
|
||||
value: download.url
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
downloadLinkValues: function () {
|
||||
return this.downloadLinks.map((download) => {
|
||||
return download.url
|
||||
})
|
||||
downloadBehavior: function () {
|
||||
return this.$store.getters.getDownloadBehavior
|
||||
},
|
||||
|
||||
formatTypeNames: function () {
|
||||
formatTypeOptions: function () {
|
||||
return [
|
||||
this.$t('Change Format.Use Dash Formats').toUpperCase(),
|
||||
this.$t('Change Format.Use Legacy Formats').toUpperCase(),
|
||||
this.$t('Change Format.Use Audio Formats').toUpperCase()
|
||||
{
|
||||
label: this.$t('Change Format.Use Dash 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: {
|
||||
|
@ -298,6 +315,7 @@ export default Vue.extend({
|
|||
watchProgress: this.getTimestamp(),
|
||||
playbackRate: this.defaultPlayback,
|
||||
videoId: this.id,
|
||||
videoLength: this.lengthSeconds,
|
||||
playlistId: this.playlistId,
|
||||
playlistIndex: this.getPlaylistIndex(),
|
||||
playlistReverse: this.getPlaylistReverse(),
|
||||
|
@ -409,15 +427,20 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
handleDownload: function (index) {
|
||||
const url = this.downloadLinkValues[index]
|
||||
const linkName = this.downloadLinkNames[index]
|
||||
const selectedDownloadLinkOption = this.downloadLinkOptions[index]
|
||||
const url = selectedDownloadLinkOption.value
|
||||
const linkName = selectedDownloadLinkOption.label
|
||||
const extension = this.grabExtensionFromUrl(linkName)
|
||||
|
||||
this.downloadMedia({
|
||||
url: url,
|
||||
title: this.title,
|
||||
extension: extension
|
||||
})
|
||||
if (this.downloadBehavior === 'open') {
|
||||
this.openExternalLink(url)
|
||||
} else {
|
||||
this.downloadMedia({
|
||||
url: url,
|
||||
title: this.title,
|
||||
extension: extension
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
grabExtensionFromUrl: function (url) {
|
||||
|
|
|
@ -95,13 +95,13 @@
|
|||
/>
|
||||
<ft-icon-button
|
||||
v-if="!isUpcoming && downloadLinks.length > 0"
|
||||
ref="downloadButton"
|
||||
:title="$t('Video.Download Video')"
|
||||
class="option"
|
||||
theme="secondary"
|
||||
icon="download"
|
||||
:return-index="true"
|
||||
:dropdown-names="downloadLinkNames"
|
||||
:dropdown-values="downloadLinkValues"
|
||||
:dropdown-options="downloadLinkOptions"
|
||||
@click="handleDownload"
|
||||
/>
|
||||
<ft-icon-button
|
||||
|
@ -110,8 +110,7 @@
|
|||
class="option"
|
||||
theme="secondary"
|
||||
icon="file-video"
|
||||
:dropdown-names="formatTypeNames"
|
||||
:dropdown-values="formatTypeValues"
|
||||
:dropdown-options="formatTypeOptions"
|
||||
@click="handleFormatChange"
|
||||
/>
|
||||
<ft-share-button
|
||||
|
|
|
@ -98,6 +98,17 @@ export default Vue.extend({
|
|||
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: {
|
||||
goToPlaylist: function () {
|
||||
|
|
|
@ -4,7 +4,6 @@ import App from './App.vue'
|
|||
import router from './router/index'
|
||||
import store from './store/index'
|
||||
import i18n from './i18n/index'
|
||||
// import 'material-design-icons/iconfont/material-icons.css'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { fas } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons/faGithub'
|
||||
|
@ -32,7 +31,7 @@ new Vue({
|
|||
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') {
|
||||
const { ipcRenderer } = require('electron')
|
||||
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { DBHistoryHandlers } from '../../../datastores/handlers/index'
|
||||
|
||||
const state = {
|
||||
historyCache: [],
|
||||
searchHistoryCache: []
|
||||
historyCache: []
|
||||
}
|
||||
|
||||
const getters = {
|
||||
getHistoryCache: () => {
|
||||
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 }) {
|
||||
try {
|
||||
await DBHistoryHandlers.updateWatchProgress(videoId, watchProgress)
|
||||
|
@ -79,10 +66,6 @@ const mutations = {
|
|||
state.historyCache = historyCache
|
||||
},
|
||||
|
||||
setSearchHistoryCache(state, result) {
|
||||
state.searchHistoryCache = result
|
||||
},
|
||||
|
||||
hoistEntryToTopOfHistoryCache(state, { currentIndex, updatedEntry }) {
|
||||
state.historyCache.splice(currentIndex, 1)
|
||||
state.historyCache.unshift(updatedEntry)
|
||||
|
|
|
@ -13,20 +13,14 @@ const state = {
|
|||
removeOnWatched: true,
|
||||
videos: []
|
||||
}
|
||||
],
|
||||
searchPlaylistCache: {
|
||||
videos: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const getters = {
|
||||
getAllPlaylists: () => state.playlists,
|
||||
getFavorites: () => state.playlists[0],
|
||||
getPlaylist: (playlistId) => state.playlists.find(playlist => playlist._id === playlistId),
|
||||
getWatchLater: () => state.playlists[1],
|
||||
getSearchPlaylistCache: () => {
|
||||
return state.searchPlaylistCache
|
||||
}
|
||||
getWatchLater: () => state.playlists[1]
|
||||
}
|
||||
|
||||
const actions = {
|
||||
|
@ -136,25 +130,10 @@ const actions = {
|
|||
} catch (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 = {
|
||||
setPlaylistCache(state, result) {
|
||||
state.searchPlaylistCache = {
|
||||
videos: result
|
||||
}
|
||||
},
|
||||
addPlaylist(state, payload) {
|
||||
state.playlists.push(payload)
|
||||
},
|
||||
|
|
|
@ -89,6 +89,31 @@ const actions = {
|
|||
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) {
|
||||
try {
|
||||
const newProfile = await DBProfileHandlers.create(profile)
|
||||
|
|
|
@ -167,7 +167,9 @@ const state = {
|
|||
barColor: false,
|
||||
checkForBlogPosts: true,
|
||||
checkForUpdates: true,
|
||||
// currentTheme: 'lightRed',
|
||||
baseTheme: 'system',
|
||||
mainColor: 'Red',
|
||||
secColor: 'Blue',
|
||||
defaultCaptionSettings: '{}',
|
||||
defaultInterval: 5,
|
||||
defaultPlayback: 1,
|
||||
|
@ -185,6 +187,7 @@ const state = {
|
|||
externalPlayerExecutable: '',
|
||||
externalPlayerIgnoreWarnings: false,
|
||||
externalPlayerCustomArgs: '',
|
||||
expandSideBar: false,
|
||||
forceLocalBackendForLegacy: false,
|
||||
hideActiveSubscriptions: false,
|
||||
hideChannelSubscriptions: false,
|
||||
|
@ -200,6 +203,7 @@ const state = {
|
|||
hideLabelsSideBar: false,
|
||||
landingPage: 'subscriptions',
|
||||
listType: 'grid',
|
||||
maxVideoPlaybackRate: 3,
|
||||
playNextVideo: false,
|
||||
proxyHostname: '127.0.0.1',
|
||||
proxyPort: '9050',
|
||||
|
@ -211,13 +215,53 @@ const state = {
|
|||
saveWatchedProgress: true,
|
||||
sponsorBlockShowSkippedToast: true,
|
||||
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: '',
|
||||
useProxy: false,
|
||||
useRssFeeds: false,
|
||||
useSponsorBlock: false,
|
||||
videoVolumeMouseScroll: 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 = {
|
||||
|
@ -228,12 +272,31 @@ const stateWithSideEffects = {
|
|||
|
||||
let targetLocale = value
|
||||
if (value === 'system') {
|
||||
const systemLocale = await dispatch('getSystemLocale')
|
||||
|
||||
targetLocale = Object.keys(i18n.messages).find((locale) => {
|
||||
const localeName = locale.replace('-', '_')
|
||||
return localeName.includes(systemLocale.replace('-', '_'))
|
||||
const systemLocaleName = (await dispatch('getSystemLocale')).replace('-', '_') // ex: en_US
|
||||
const systemLocaleLang = systemLocaleName.split('_')[0] // ex: en
|
||||
const targetLocaleOptions = Object.keys(i18n.messages).filter((locale) => { // filter out other languages
|
||||
const localeLang = locale.replace('-', '_').split('_')[0]
|
||||
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
|
||||
if (!targetLocale) {
|
||||
|
@ -322,7 +385,9 @@ const customActions = {
|
|||
dispatch(defaultSideEffectsTriggerId(_id), value)
|
||||
}
|
||||
|
||||
commit(defaultMutationId(_id), value)
|
||||
if (Object.keys(mutations).includes(defaultMutationId(_id))) {
|
||||
commit(defaultMutationId(_id), value)
|
||||
}
|
||||
}
|
||||
} catch (errMessage) {
|
||||
console.error(errMessage)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import $ from 'jquery'
|
||||
import forge from 'node-forge'
|
||||
|
||||
const state = {}
|
||||
const getters = {}
|
||||
|
@ -7,22 +6,30 @@ const getters = {}
|
|||
const actions = {
|
||||
sponsorBlockSkipSegments ({ rootState }, { videoId, categories }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageDigestSha256 = forge.md.sha256.create()
|
||||
messageDigestSha256.update(videoId)
|
||||
const videoIdHashPrefix = messageDigestSha256.digest().toHex().substring(0, 4)
|
||||
const requestUrl = `${rootState.settings.sponsorBlockUrl}/api/skipSegments/${videoIdHashPrefix}?categories=${JSON.stringify(categories)}`
|
||||
const videoIdBuffer = new TextEncoder().encode(videoId)
|
||||
|
||||
$.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)
|
||||
crypto.subtle.digest('SHA-256', videoIdBuffer).then((hashBuffer) => {
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
|
||||
const videoIdHashPrefix = hashArray
|
||||
.map(byte => byte.toString(16).padStart(2, '0'))
|
||||
.slice(0, 4)
|
||||
.join('')
|
||||
|
||||
const requestUrl = `${rootState.settings.sponsorBlockUrl}/api/skipSegments/${videoIdHashPrefix}?categories=${JSON.stringify(categories)}`
|
||||
|
||||
$.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: [],
|
||||
profileSubscriptions: {
|
||||
activeProfile: MAIN_PROFILE_ID,
|
||||
videoList: []
|
||||
videoList: [],
|
||||
errorChannels: []
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import IsEqual from 'lodash.isequal'
|
||||
import FtToastEvents from '../../components/ft-toast/ft-toast-events'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import i18n from '../../i18n/index'
|
||||
|
||||
import { IpcChannels } from '../../../constants'
|
||||
import { ipcRenderer } from 'electron'
|
||||
|
||||
const state = {
|
||||
isSideNavOpen: false,
|
||||
|
@ -27,30 +27,30 @@ const state = {
|
|||
type: 'all',
|
||||
duration: ''
|
||||
},
|
||||
colorClasses: [
|
||||
'mainRed',
|
||||
'mainPink',
|
||||
'mainPurple',
|
||||
'mainDeepPurple',
|
||||
'mainIndigo',
|
||||
'mainBlue',
|
||||
'mainLightBlue',
|
||||
'mainCyan',
|
||||
'mainTeal',
|
||||
'mainGreen',
|
||||
'mainLightGreen',
|
||||
'mainLime',
|
||||
'mainYellow',
|
||||
'mainAmber',
|
||||
'mainOrange',
|
||||
'mainDeepOrange',
|
||||
'mainDraculaCyan',
|
||||
'mainDraculaGreen',
|
||||
'mainDraculaOrange',
|
||||
'mainDraculaPink',
|
||||
'mainDraculaPurple',
|
||||
'mainDraculaRed',
|
||||
'mainDraculaYellow'
|
||||
colorNames: [
|
||||
'Red',
|
||||
'Pink',
|
||||
'Purple',
|
||||
'DeepPurple',
|
||||
'Indigo',
|
||||
'Blue',
|
||||
'LightBlue',
|
||||
'Cyan',
|
||||
'Teal',
|
||||
'Green',
|
||||
'LightGreen',
|
||||
'Lime',
|
||||
'Yellow',
|
||||
'Amber',
|
||||
'Orange',
|
||||
'DeepOrange',
|
||||
'DraculaCyan',
|
||||
'DraculaGreen',
|
||||
'DraculaOrange',
|
||||
'DraculaPink',
|
||||
'DraculaPurple',
|
||||
'DraculaRed',
|
||||
'DraculaYellow'
|
||||
],
|
||||
colorValues: [
|
||||
'#d50000',
|
||||
|
@ -107,6 +107,10 @@ const getters = {
|
|||
return state.searchSettings
|
||||
},
|
||||
|
||||
getColorNames () {
|
||||
return state.colorNames
|
||||
},
|
||||
|
||||
getColorValues () {
|
||||
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 }) {
|
||||
const fileName = `${title}.${extension}`
|
||||
const fileName = `${await dispatch('replaceFilenameForbiddenChars', title)}.${extension}`
|
||||
const usingElectron = rootState.settings.usingElectron
|
||||
const locale = i18n._vm.locale
|
||||
const translations = i18n._vm.messages[locale]
|
||||
|
@ -197,11 +234,12 @@ const actions = {
|
|||
defaultPath: fileName,
|
||||
filters: [
|
||||
{
|
||||
name: extension.toUpperCase(),
|
||||
extensions: [extension]
|
||||
}
|
||||
]
|
||||
}
|
||||
const response = await dispatch('showSaveDialog', options)
|
||||
const response = await dispatch('showSaveDialog', { options })
|
||||
|
||||
if (response.canceled || response.filePath === '') {
|
||||
// User canceled the save dialog
|
||||
|
@ -209,6 +247,19 @@ const actions = {
|
|||
}
|
||||
|
||||
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', {
|
||||
|
@ -223,8 +274,6 @@ const actions = {
|
|||
})
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const contentLength = response.headers.get('Content-Length')
|
||||
let receivedLength = 0
|
||||
const chunks = []
|
||||
|
||||
const handleError = (err) => {
|
||||
|
@ -240,9 +289,10 @@ const actions = {
|
|||
}
|
||||
|
||||
chunks.push(value)
|
||||
receivedLength += value.length
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
@ -281,10 +331,10 @@ const actions = {
|
|||
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
|
||||
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) {
|
||||
|
@ -293,13 +343,73 @@ const actions = {
|
|||
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) {
|
||||
commit('setShowProgressBar', value)
|
||||
},
|
||||
|
||||
getRandomColorClass () {
|
||||
const randomInt = Math.floor(Math.random() * state.colorClasses.length)
|
||||
return state.colorClasses[randomInt]
|
||||
const randomInt = Math.floor(Math.random() * state.colorNames.length)
|
||||
return 'main' + state.colorNames[randomInt]
|
||||
},
|
||||
|
||||
getRandomColor () {
|
||||
|
@ -832,7 +942,7 @@ const actions = {
|
|||
args.push(...defaultCustomArguments)
|
||||
}
|
||||
|
||||
if (payload.watchProgress > 0) {
|
||||
if (payload.watchProgress > 0 && payload.watchProgress < payload.videoLength - 10) {
|
||||
if (typeof cmdArgs.startOffset === 'string') {
|
||||
args.push(`${cmdArgs.startOffset}${payload.watchProgress}`)
|
||||
} else {
|
||||
|
@ -973,7 +1083,7 @@ const mutations = {
|
|||
})
|
||||
|
||||
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
|
||||
} else {
|
||||
state.sessionSearchHistory.push(payload)
|
||||
|
@ -984,7 +1094,7 @@ const mutations = {
|
|||
state.popularCache = value
|
||||
},
|
||||
|
||||
setTrendingCache (state, value, page) {
|
||||
setTrendingCache (state, { value, page }) {
|
||||
state.trendingCache[page] = value
|
||||
},
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ import { SocksProxyAgent } from 'socks-proxy-agent'
|
|||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||
|
||||
import i18n from '../../i18n/index'
|
||||
|
||||
const state = {
|
||||
isYtSearchRunning: false
|
||||
}
|
||||
|
@ -288,10 +290,15 @@ const actions = {
|
|||
break
|
||||
}
|
||||
}
|
||||
const locale = settings.currentLocale.replace('-', '_')
|
||||
let locale = i18n.locale.replace('_', '-')
|
||||
|
||||
if (locale === 'nn') {
|
||||
locale = 'no'
|
||||
}
|
||||
|
||||
ytpl(playlistId, {
|
||||
hl: locale,
|
||||
limit: 'Infinity',
|
||||
limit: Infinity,
|
||||
requestOptions: { agent }
|
||||
}).then((result) => {
|
||||
resolve(result)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.light {
|
||||
.system[data-system-theme*='light'], .light {
|
||||
--primary-text-color: #212121;
|
||||
--secondary-text-color: #424242;
|
||||
--tertiary-text-color: #757575;
|
||||
|
@ -22,7 +22,7 @@
|
|||
--logo-text: url("~../../_icons/textColorSmall.png");
|
||||
}
|
||||
|
||||
.dark {
|
||||
.system[data-system-theme*='dark'], .dark {
|
||||
--primary-text-color: #EEEEEE;
|
||||
--secondary-text-color: #ddd;
|
||||
--tertiary-text-color: #999;
|
||||
|
@ -61,7 +61,7 @@
|
|||
--secondary-card-bg-color: rgba(0, 0, 0, 0.75);
|
||||
--scrollbar-color: #515151;
|
||||
--scrollbar-color-hover: #424242;
|
||||
--side-nav-color: #000000;
|
||||
--side-nav-color: #0f0f0f;
|
||||
--side-nav-hover-color: #212121;
|
||||
--side-nav-active-color: #303030;
|
||||
--search-bar-color: #262626;
|
||||
|
@ -619,6 +619,8 @@
|
|||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--bg-color);
|
||||
--red-500: #f44336;
|
||||
|
|
|
@ -350,7 +350,7 @@
|
|||
line-height: 1;
|
||||
font-weight: 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;
|
||||
}
|
||||
.video-js:-moz-full-screen {
|
||||
|
@ -490,6 +490,16 @@ body.vjs-full-window {
|
|||
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) {
|
||||
.videoPlayer .vjs-button-theatre {
|
||||
display: none
|
||||
|
@ -1023,7 +1033,7 @@ body.vjs-full-window {
|
|||
padding: 6px 8px 8px 8px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -3.4em;
|
||||
top: -2.7em;
|
||||
visibility: hidden;
|
||||
z-index: 2;
|
||||
}
|
||||
|
@ -2136,7 +2146,8 @@ video::-webkit-media-text-track-display {
|
|||
.video-js .vjs-vtt-thumbnail-display {
|
||||
position: absolute;
|
||||
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;
|
||||
box-shadow: 0 0 7px rgba(0,0,0,.6);
|
||||
z-index: 3;
|
||||
|
@ -2160,3 +2171,24 @@ video::-webkit-media-text-track-display {
|
|||
font-size: xx-large;
|
||||
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;
|
||||
width: 85%;
|
||||
margin: 0 auto 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.channelBanner {
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
.channelDetails {
|
||||
padding: 0 0 16px;
|
||||
}
|
||||
|
||||
.defaultChannelBanner {
|
||||
.channelBannerContainer {
|
||||
background: center / cover no-repeat var(--banner-url, transparent);
|
||||
height: 13vw;
|
||||
min-height: 110px;
|
||||
max-height: 32vh;
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
height:200px;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.channelBannerContainer.default {
|
||||
background-image: url("images/defaultBanner.png");
|
||||
background-repeat: repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.channelInfoContainer {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background-color: var(--card-bg-color);
|
||||
margin-top: 10px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.channelInfo {
|
||||
|
@ -102,7 +108,7 @@
|
|||
}
|
||||
|
||||
.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;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
@ -143,3 +149,8 @@
|
|||
flex-direction: column;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.channelLineContainer h1,
|
||||
.channelLineContainer p {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ export default Vue.extend({
|
|||
searchResults: [],
|
||||
shownElementList: [],
|
||||
apiUsed: '',
|
||||
errorMessage: '',
|
||||
videoSelectValues: [
|
||||
'newest',
|
||||
'oldest',
|
||||
|
@ -90,16 +91,14 @@ export default Vue.extend({
|
|||
return this.$store.getters.getActiveProfile
|
||||
},
|
||||
|
||||
isSubscribed: function () {
|
||||
const subIndex = this.activeProfile.subscriptions.findIndex((channel) => {
|
||||
subscriptionInfo: function () {
|
||||
return this.activeProfile.subscriptions.find((channel) => {
|
||||
return channel.id === this.id
|
||||
})
|
||||
}) ?? null
|
||||
},
|
||||
|
||||
if (subIndex === -1) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
isSubscribed: function () {
|
||||
return this.subscriptionInfo !== null
|
||||
},
|
||||
|
||||
subscribedText: function () {
|
||||
|
@ -249,25 +248,40 @@ export default Vue.extend({
|
|||
|
||||
getChannelInfoLocal: function () {
|
||||
this.apiUsed = 'local'
|
||||
ytch.getChannelInfo(this.id).then((response) => {
|
||||
this.id = response.authorId
|
||||
this.channelName = response.author
|
||||
const expectedId = this.id
|
||||
ytch.getChannelInfo({ channelId: expectedId }).then((response) => {
|
||||
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}`
|
||||
if (this.hideChannelSubscriptions || response.subscriberCount === 0) {
|
||||
this.subCount = null
|
||||
} else {
|
||||
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.relatedChannels = response.relatedChannels.items
|
||||
this.relatedChannels.forEach(relatedChannel => {
|
||||
relatedChannel.authorThumbnails.map(thumbnail => {
|
||||
relatedChannel.thumbnail.map(thumbnail => {
|
||||
if (!thumbnail.url.includes('https')) {
|
||||
thumbnail.url = `https:${thumbnail.url}`
|
||||
}
|
||||
return thumbnail
|
||||
})
|
||||
relatedChannel.authorThumbnails = relatedChannel.thumbnail
|
||||
})
|
||||
|
||||
if (response.authorBanners !== null) {
|
||||
|
@ -306,7 +320,12 @@ export default Vue.extend({
|
|||
|
||||
getChannelVideosLocal: function () {
|
||||
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.videoContinuationString = response.continuation
|
||||
this.isElementListLoading = false
|
||||
|
@ -332,7 +351,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
channelLocalNextPage: function () {
|
||||
ytch.getChannelVideosMore(this.videoContinuationString).then((response) => {
|
||||
ytch.getChannelVideosMore({ continuation: this.videoContinuationString }).then((response) => {
|
||||
this.latestVideos = this.latestVideos.concat(response.items)
|
||||
this.videoContinuationString = response.continuation
|
||||
}).catch((err) => {
|
||||
|
@ -352,17 +371,26 @@ export default Vue.extend({
|
|||
this.isLoading = true
|
||||
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)
|
||||
this.channelName = response.author
|
||||
const channelName = response.author
|
||||
const channelId = response.authorId
|
||||
this.channelName = channelName
|
||||
document.title = `${this.channelName} - ${process.env.PRODUCT_NAME}`
|
||||
this.id = response.authorId
|
||||
this.id = channelId
|
||||
if (this.hideChannelSubscriptions) {
|
||||
this.subCount = null
|
||||
} else {
|
||||
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.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/`)
|
||||
|
@ -371,12 +399,16 @@ export default Vue.extend({
|
|||
})
|
||||
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/`)
|
||||
} else {
|
||||
this.bannerUrl = null
|
||||
}
|
||||
|
||||
this.errorMessage = ''
|
||||
this.isLoading = false
|
||||
}).catch((err) => {
|
||||
this.setErrorMessage(err.responseJSON.error)
|
||||
console.log(err)
|
||||
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
||||
this.showToast({
|
||||
|
@ -418,7 +450,12 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
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)
|
||||
this.latestPlaylists = response.items.map((item) => {
|
||||
item.proxyThumbnail = false
|
||||
|
@ -448,7 +485,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
getPlaylistsLocalMore: function () {
|
||||
ytch.getChannelPlaylistsMore(this.playlistContinuationString).then((response) => {
|
||||
ytch.getChannelPlaylistsMore({ continuation: this.playlistContinuationString }).then((response) => {
|
||||
console.log(response)
|
||||
this.latestPlaylists = this.latestPlaylists.concat(response.items)
|
||||
this.playlistContinuationString = response.continuation
|
||||
|
@ -466,6 +503,40 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
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) {
|
||||
console.log('There are no more playlists available for this channel')
|
||||
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 () {
|
||||
switch (this.currentTab) {
|
||||
case 'videos':
|
||||
|
@ -598,7 +679,7 @@ export default Vue.extend({
|
|||
this.getPlaylistsLocalMore()
|
||||
break
|
||||
case 'invidious':
|
||||
this.getPlaylistsInvidious()
|
||||
this.getPlaylistsInvidiousMore()
|
||||
break
|
||||
}
|
||||
break
|
||||
|
@ -638,7 +719,7 @@ export default Vue.extend({
|
|||
|
||||
searchChannelLocal: function () {
|
||||
if (this.searchContinuationString === '') {
|
||||
ytch.searchChannel(this.id, this.lastSearchQuery).then((response) => {
|
||||
ytch.searchChannel({ channelId: this.id, query: this.lastSearchQuery }).then((response) => {
|
||||
console.log(response)
|
||||
this.searchResults = response.items
|
||||
this.isElementListLoading = false
|
||||
|
@ -663,7 +744,7 @@ export default Vue.extend({
|
|||
}
|
||||
})
|
||||
} else {
|
||||
ytch.searchChannelMore(this.searchContinuationString).then((response) => {
|
||||
ytch.searchChannelMore({ continuation: this.searchContinuationString }).then((response) => {
|
||||
console.log(response)
|
||||
this.searchResults = this.searchResults.concat(response.items)
|
||||
this.isElementListLoading = false
|
||||
|
@ -721,7 +802,8 @@ export default Vue.extend({
|
|||
'showToast',
|
||||
'updateProfile',
|
||||
'invidiousGetChannelInfo',
|
||||
'invidiousAPICall'
|
||||
'invidiousAPICall',
|
||||
'updateSubscriptionDetails'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -3,22 +3,21 @@
|
|||
ref="search"
|
||||
>
|
||||
<ft-loader
|
||||
v-if="isLoading"
|
||||
v-if="isLoading && !errorMessage"
|
||||
:fullscreen="true"
|
||||
/>
|
||||
<ft-card
|
||||
v-else
|
||||
class="card"
|
||||
class="card channelDetails"
|
||||
>
|
||||
<img
|
||||
v-if="bannerUrl !== null"
|
||||
class="channelBanner"
|
||||
:src="bannerUrl"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
class="defaultChannelBanner"
|
||||
>
|
||||
<div
|
||||
class="channelBannerContainer"
|
||||
:class="{
|
||||
default: !bannerUrl
|
||||
}"
|
||||
:style="{ '--banner-url': `url('${bannerUrl}')` }"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="channelInfoContainer"
|
||||
>
|
||||
|
@ -35,19 +34,20 @@
|
|||
<div
|
||||
class="channelLineContainer"
|
||||
>
|
||||
<span
|
||||
<h1
|
||||
class="channelName"
|
||||
>
|
||||
{{ channelName }}
|
||||
</span>
|
||||
<span
|
||||
</h1>
|
||||
|
||||
<p
|
||||
v-if="subCount !== null"
|
||||
class="channelSubCount"
|
||||
>
|
||||
{{ formattedSubCount }}
|
||||
<span v-if="subCount === 1">{{ $t("Channel.Subscriber") }}</span>
|
||||
<span v-else>{{ $t("Channel.Subscribers") }}</span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -61,6 +61,7 @@
|
|||
</div>
|
||||
|
||||
<ft-flex-box
|
||||
v-if="!errorMessage"
|
||||
class="channelInfoTabs"
|
||||
>
|
||||
<div
|
||||
|
@ -112,7 +113,7 @@
|
|||
</div>
|
||||
</ft-card>
|
||||
<ft-card
|
||||
v-if="!isLoading"
|
||||
v-if="!isLoading && !errorMessage"
|
||||
class="card"
|
||||
>
|
||||
<div
|
||||
|
@ -138,10 +139,10 @@
|
|||
<ft-channel-bubble
|
||||
v-for="(channel, index) in relatedChannels"
|
||||
:key="index"
|
||||
:channel-name="channel.author"
|
||||
:channel-id="channel.authorId"
|
||||
:channel-name="channel.author || channel.channelName"
|
||||
:channel-id="channel.channelId"
|
||||
:channel-thumbnail="channel.authorThumbnails[channel.authorThumbnails.length - 1].url"
|
||||
@click="goToChannel(channel.authorId)"
|
||||
@click="goToChannel(channel.channelId)"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
</div>
|
||||
|
@ -194,6 +195,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</ft-card>
|
||||
<ft-card
|
||||
v-if="errorMessage"
|
||||
class="card"
|
||||
>
|
||||
<p>
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
</ft-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -20,19 +20,19 @@ export default Vue.extend({
|
|||
return {
|
||||
isLoading: false,
|
||||
dataLimit: 100,
|
||||
hasQuery: false
|
||||
searchDataLimit: 100,
|
||||
showLoadMoreButton: false,
|
||||
hasQuery: false,
|
||||
query: '',
|
||||
activeData: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
historyCache: function () {
|
||||
if (!this.hasQuery) {
|
||||
return this.$store.getters.getHistoryCache
|
||||
} else {
|
||||
return this.$store.getters.getSearchHistoryCache
|
||||
}
|
||||
return this.$store.getters.getHistoryCache
|
||||
},
|
||||
|
||||
activeData: function () {
|
||||
fullData: function () {
|
||||
if (this.historyCache.length < this.dataLimit) {
|
||||
return this.historyCache
|
||||
} 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 () {
|
||||
console.log(this.historyCache)
|
||||
|
||||
const limit = sessionStorage.getItem('historyLimit')
|
||||
|
||||
if (limit !== null) {
|
||||
this.dataLimit = limit
|
||||
}
|
||||
|
||||
this.activeData = this.fullData
|
||||
|
||||
if (this.activeData.length < this.historyCache.length) {
|
||||
this.showLoadMoreButton = true
|
||||
} else {
|
||||
this.showLoadMoreButton = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
increaseLimit: function () {
|
||||
this.dataLimit += 100
|
||||
sessionStorage.setItem('historyLimit', this.dataLimit)
|
||||
if (this.query !== '') {
|
||||
this.searchDataLimit += 100
|
||||
this.filterHistory()
|
||||
} else {
|
||||
this.dataLimit += 100
|
||||
sessionStorage.setItem('historyLimit', this.dataLimit)
|
||||
}
|
||||
},
|
||||
filterHistory: function(query) {
|
||||
this.hasQuery = query !== ''
|
||||
this.$store.dispatch('searchHistory', query)
|
||||
if (this.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
|
||||
setTimeout(() => {
|
||||
Vue.nextTick(() => {
|
||||
this.isLoading = false
|
||||
}, 100)
|
||||
Vue.nextTick(() => {
|
||||
window.scrollTo(0, scrollPos)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,34 +1,43 @@
|
|||
<template>
|
||||
<div>
|
||||
<ft-loader
|
||||
v-if="isLoading"
|
||||
v-show="isLoading"
|
||||
:fullscreen="true"
|
||||
/>
|
||||
<ft-card
|
||||
v-else
|
||||
v-show="!isLoading"
|
||||
class="card"
|
||||
>
|
||||
<h3>{{ $t("History.History") }}</h3>
|
||||
<ft-input
|
||||
v-show="fullData.length > 0"
|
||||
ref="searchBar"
|
||||
:placeholder="$t('History.Search bar placeholder')"
|
||||
:show-clear-text-button="true"
|
||||
:show-action-button="false"
|
||||
@input="filterHistory"
|
||||
@input="(input) => query = input"
|
||||
@clear="query = ''"
|
||||
/>
|
||||
<ft-flex-box
|
||||
v-if="activeData.length === 0"
|
||||
v-show="fullData.length === 0"
|
||||
>
|
||||
<p class="message">
|
||||
{{ $t("History['Your history list is currently empty.']") }}
|
||||
</p>
|
||||
</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
|
||||
v-else
|
||||
v-if="activeData.length > 0 && !isLoading"
|
||||
:data="activeData"
|
||||
/>
|
||||
<ft-flex-box
|
||||
v-if="activeData.length < historyCache.length"
|
||||
v-if="showLoadMoreButton"
|
||||
>
|
||||
<ft-button
|
||||
label="Load More"
|
||||
|
|
|
@ -1,18 +1,42 @@
|
|||
.routerView {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.playlistInfo {
|
||||
background-color: var(--card-bg-color);
|
||||
padding: 10px;
|
||||
float: left;
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
height: calc(100vh - 96px);
|
||||
margin-right: 1em;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
position: sticky;
|
||||
top: 78px;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.playlistItems {
|
||||
float: right;
|
||||
width: 60%;
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
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 { mapActions } from 'vuex'
|
||||
import dateFormat from 'dateformat'
|
||||
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||
import PlaylistInfo from '../../components/playlist-info/playlist-info.vue'
|
||||
import FtListVideo from '../../components/ft-list-video/ft-list-video.vue'
|
||||
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||
import i18n from '../../i18n/index'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'Playlist',
|
||||
|
@ -36,6 +36,9 @@ export default Vue.extend({
|
|||
},
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
currentLocale: function () {
|
||||
return i18n.locale.replace('_', '-')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -81,6 +84,12 @@ export default Vue.extend({
|
|||
infoSource: 'local'
|
||||
}
|
||||
|
||||
this.updateSubscriptionDetails({
|
||||
channelThumbnailUrl: this.infoData.channelThumbnail,
|
||||
channelName: this.infoData.channelName,
|
||||
channelId: this.infoData.channelId
|
||||
})
|
||||
|
||||
this.playlistItems = result.items.map((video) => {
|
||||
if (typeof video.author !== 'undefined') {
|
||||
const channelName = video.author.name
|
||||
|
@ -133,9 +142,14 @@ export default Vue.extend({
|
|||
infoSource: 'invidious'
|
||||
}
|
||||
|
||||
this.updateSubscriptionDetails({
|
||||
channelThumbnailUrl: result.authorThumbnails[2].url,
|
||||
channelName: this.infoData.channelName,
|
||||
channelId: this.infoData.channelId
|
||||
})
|
||||
|
||||
const dateString = new Date(result.updated * 1000)
|
||||
dateString.setDate(dateString.getDate() + 1)
|
||||
this.infoData.lastUpdated = dateFormat(dateString, 'mmm dS, yyyy')
|
||||
this.infoData.lastUpdated = dateString.toLocaleDateString(this.currentLocale, { year: 'numeric', month: 'short', day: 'numeric' })
|
||||
|
||||
this.playlistItems = this.playlistItems.concat(result.videos)
|
||||
|
||||
|
@ -172,7 +186,8 @@ export default Vue.extend({
|
|||
|
||||
...mapActions([
|
||||
'ytGetPlaylistInfo',
|
||||
'invidiousGetPlaylistInfo'
|
||||
'invidiousGetPlaylistInfo',
|
||||
'updateSubscriptionDetails'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
v-if="isLoading"
|
||||
:fullscreen="true"
|
||||
/>
|
||||
|
||||
<playlist-info
|
||||
v-if="!isLoading"
|
||||
:data="infoData"
|
||||
class="playlistInfo"
|
||||
/>
|
||||
|
||||
<ft-card
|
||||
v-if="!isLoading"
|
||||
class="playlistItems"
|
||||
|
|
|
@ -14,6 +14,10 @@
|
|||
right: 10px;
|
||||
}
|
||||
|
||||
.channelBubble {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 350px) {
|
||||
.floatingTopButton {
|
||||
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 FtFlexBox from '../../components/ft-flex-box/ft-flex-box.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 Parser from 'rss-parser'
|
||||
|
@ -19,13 +20,15 @@ export default Vue.extend({
|
|||
'ft-button': FtButton,
|
||||
'ft-icon-button': FtIconButton,
|
||||
'ft-flex-box': FtFlexBox,
|
||||
'ft-element-list': FtElementList
|
||||
'ft-element-list': FtElementList,
|
||||
'ft-channel-bubble': FtChannelBubble
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
isLoading: false,
|
||||
dataLimit: 100,
|
||||
videoList: []
|
||||
videoList: [],
|
||||
errorChannels: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -110,6 +113,7 @@ export default Vue.extend({
|
|||
}))
|
||||
} else {
|
||||
this.videoList = subscriptionList.videoList
|
||||
this.errorChannels = subscriptionList.errorChannels
|
||||
}
|
||||
} else {
|
||||
this.getProfileSubscriptions()
|
||||
|
@ -123,6 +127,10 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
goToChannel: function (id) {
|
||||
this.$router.push({ path: `/channel/${id}` })
|
||||
},
|
||||
|
||||
getSubscriptions: function () {
|
||||
if (this.activeSubscriptionList.length === 0) {
|
||||
this.isLoading = false
|
||||
|
@ -144,10 +152,9 @@ export default Vue.extend({
|
|||
|
||||
let videoList = []
|
||||
let channelCount = 0
|
||||
|
||||
this.errorChannels = []
|
||||
this.activeSubscriptionList.forEach(async (channel) => {
|
||||
let videos = []
|
||||
|
||||
if (!this.usingElectron || this.backendPreference === 'invidious') {
|
||||
if (useRss) {
|
||||
videos = await this.getChannelVideosInvidiousRSS(channel)
|
||||
|
@ -174,7 +181,8 @@ export default Vue.extend({
|
|||
|
||||
const profileSubscriptions = {
|
||||
activeProfile: this.activeProfile._id,
|
||||
videoList: videoList
|
||||
videoList: videoList,
|
||||
errorChannels: this.errorChannels
|
||||
}
|
||||
|
||||
this.videoList = await Promise.all(videoList.filter((video) => {
|
||||
|
@ -225,7 +233,12 @@ export default Vue.extend({
|
|||
|
||||
getChannelVideosLocalScraper: function (channel, failedAttempts = 0) {
|
||||
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) => {
|
||||
if (video.liveNow) {
|
||||
video.publishedDate = new Date().getTime()
|
||||
|
@ -297,33 +310,38 @@ export default Vue.extend({
|
|||
resolve(items)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
this.showToast({
|
||||
message: `${errorMessage}: ${err}`,
|
||||
time: 10000,
|
||||
action: () => {
|
||||
navigator.clipboard.writeText(err)
|
||||
}
|
||||
})
|
||||
switch (failedAttempts) {
|
||||
case 0:
|
||||
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([])
|
||||
if (err.toString().match(/404/)) {
|
||||
this.errorChannels.push(channel)
|
||||
resolve([])
|
||||
} else {
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
this.showToast({
|
||||
message: `${errorMessage}: ${err}`,
|
||||
time: 10000,
|
||||
action: () => {
|
||||
navigator.clipboard.writeText(err)
|
||||
}
|
||||
break
|
||||
case 2:
|
||||
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
|
||||
break
|
||||
default:
|
||||
resolve([])
|
||||
})
|
||||
switch (failedAttempts) {
|
||||
case 0:
|
||||
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:
|
||||
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
|
||||
break
|
||||
default:
|
||||
resolve([])
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -403,25 +421,30 @@ export default Vue.extend({
|
|||
navigator.clipboard.writeText(err)
|
||||
}
|
||||
})
|
||||
switch (failedAttempts) {
|
||||
case 0:
|
||||
resolve(this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1))
|
||||
break
|
||||
case 1:
|
||||
if (this.backendFallback) {
|
||||
this.showToast({
|
||||
message: this.$t('Falling back to the local API')
|
||||
})
|
||||
resolve(this.getChannelVideosLocalRSS(channel, failedAttempts + 1))
|
||||
} else {
|
||||
if (err.toString().match(/500/)) {
|
||||
this.errorChannels.push(channel)
|
||||
resolve([])
|
||||
} else {
|
||||
switch (failedAttempts) {
|
||||
case 0:
|
||||
resolve(this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1))
|
||||
break
|
||||
case 1:
|
||||
if (this.backendFallback) {
|
||||
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([])
|
||||
}
|
||||
break
|
||||
case 2:
|
||||
resolve(this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1))
|
||||
break
|
||||
default:
|
||||
resolve([])
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -8,6 +8,22 @@
|
|||
v-else
|
||||
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>
|
||||
<ft-flex-box
|
||||
v-if="activeVideoList.length === 0"
|
||||
|
|
|
@ -53,7 +53,7 @@ export default Vue.extend({
|
|||
},
|
||||
mounted: function () {
|
||||
if (this.trendingCache[this.currentTab] && this.trendingCache[this.currentTab].length > 0) {
|
||||
this.shownResults = this.trendingCache
|
||||
this.getTrendingInfoCache()
|
||||
} else {
|
||||
this.getTrendingInfo()
|
||||
}
|
||||
|
@ -92,7 +92,11 @@ export default Vue.extend({
|
|||
currentTabNode.attr('aria-selected', 'false')
|
||||
newTabNode.attr('aria-selected', 'true')
|
||||
this.currentTab = tab
|
||||
this.getTrendingInfo()
|
||||
if (this.trendingCache[this.currentTab] && this.trendingCache[this.currentTab].length > 0) {
|
||||
this.getTrendingInfoCache()
|
||||
} else {
|
||||
this.getTrendingInfo()
|
||||
}
|
||||
},
|
||||
|
||||
getTrendingInfo () {
|
||||
|
@ -127,7 +131,8 @@ export default Vue.extend({
|
|||
|
||||
this.shownResults = returnData
|
||||
this.isLoading = false
|
||||
this.$store.commit('setTrendingCache', this.shownResults, this.currentTab)
|
||||
const currentTab = this.currentTab
|
||||
this.$store.commit('setTrendingCache', { value: returnData, page: currentTab })
|
||||
}).then(() => {
|
||||
document.querySelector(`#${this.currentTab}Tab`).focus()
|
||||
}).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 () {
|
||||
this.isLoading = true
|
||||
|
||||
|
@ -177,7 +190,8 @@ export default Vue.extend({
|
|||
|
||||
this.shownResults = returnData
|
||||
this.isLoading = false
|
||||
this.$store.commit('setTrendingCache', this.shownResults, this.trendingCache)
|
||||
const currentTab = this.currentTab
|
||||
this.$store.commit('setTrendingCache', { value: returnData, page: currentTab })
|
||||
}).then(() => {
|
||||
document.querySelector(`#${this.currentTab}Tab`).focus()
|
||||
}).catch((err) => {
|
||||
|
|
|
@ -22,19 +22,19 @@ export default Vue.extend({
|
|||
return {
|
||||
isLoading: false,
|
||||
dataLimit: 100,
|
||||
hasQuery: false
|
||||
searchDataLimit: 100,
|
||||
showLoadMoreButton: false,
|
||||
query: '',
|
||||
hasQuery: false,
|
||||
activeData: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
favoritesPlaylist: function () {
|
||||
if (!this.hasQuery) {
|
||||
return this.$store.getters.getFavorites
|
||||
} else {
|
||||
return this.$store.getters.getSearchPlaylistCache
|
||||
}
|
||||
return this.$store.getters.getFavorites
|
||||
},
|
||||
|
||||
activeData: function () {
|
||||
fullData: function () {
|
||||
const data = [].concat(this.favoritesPlaylist.videos).reverse()
|
||||
if (this.favoritesPlaylist.videos.length < this.dataLimit) {
|
||||
return data
|
||||
|
@ -44,16 +44,16 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
query() {
|
||||
this.searchDataLimit = 100
|
||||
this.filterPlaylist()
|
||||
},
|
||||
activeData() {
|
||||
const scrollPos = window.scrollY || window.scrollTop || document.getElementsByTagName('html')[0].scrollTop
|
||||
this.isLoading = true
|
||||
setTimeout(() => {
|
||||
this.isLoading = false
|
||||
// This is kinda ugly, but should fix a few existing issues
|
||||
setTimeout(() => {
|
||||
window.scrollTo(0, scrollPos)
|
||||
}, 100)
|
||||
}, 100)
|
||||
this.refreshPage()
|
||||
},
|
||||
fullData() {
|
||||
this.activeData = this.fullData
|
||||
this.filterPlaylist()
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
|
@ -62,15 +62,60 @@ export default Vue.extend({
|
|||
if (limit !== null) {
|
||||
this.dataLimit = limit
|
||||
}
|
||||
|
||||
if (this.activeData.length < this.favoritesPlaylist.videos.length) {
|
||||
this.showLoadMoreButton = true
|
||||
} else {
|
||||
this.showLoadMoreButton = false
|
||||
}
|
||||
|
||||
this.activeData = this.fullData
|
||||
},
|
||||
methods: {
|
||||
increaseLimit: function () {
|
||||
this.dataLimit += 100
|
||||
sessionStorage.setItem('favoritesLimit', this.dataLimit)
|
||||
if (this.query !== '') {
|
||||
this.searchDataLimit += 100
|
||||
this.filterPlaylist()
|
||||
} else {
|
||||
this.dataLimit += 100
|
||||
sessionStorage.setItem('favoritesLimit', this.dataLimit)
|
||||
}
|
||||
},
|
||||
filterPlaylist: function(query) {
|
||||
this.hasQuery = query !== ''
|
||||
this.$store.dispatch('searchFavoritePlaylist', query)
|
||||
filterPlaylist: function() {
|
||||
if (this.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>
|
||||
<div>
|
||||
<ft-loader
|
||||
v-if="isLoading"
|
||||
v-show="isLoading"
|
||||
:fullscreen="true"
|
||||
/>
|
||||
<ft-card
|
||||
v-else
|
||||
v-show="!isLoading"
|
||||
class="card"
|
||||
>
|
||||
<h3>
|
||||
|
@ -17,25 +17,34 @@
|
|||
/>
|
||||
</h3>
|
||||
<ft-input
|
||||
v-show="fullData.length > 0"
|
||||
ref="searchBar"
|
||||
:placeholder="$t('User Playlists.Search bar placeholder')"
|
||||
:show-clear-text-button="true"
|
||||
:show-action-button="false"
|
||||
@input="filterPlaylist"
|
||||
@input="(input) => query = input"
|
||||
@clear="query = ''"
|
||||
/>
|
||||
<ft-flex-box
|
||||
v-if="activeData.length === 0"
|
||||
v-show="fullData.length === 0"
|
||||
>
|
||||
<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']") }}
|
||||
</p>
|
||||
</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
|
||||
v-else
|
||||
v-if="activeData.length > 0 && !isLoading"
|
||||
:data="activeData"
|
||||
/>
|
||||
<ft-flex-box
|
||||
v-if="activeData.length < favoritesPlaylist.videos.length"
|
||||
v-if="showLoadMoreButton"
|
||||
>
|
||||
<ft-button
|
||||
label="Load More"
|
||||
|
|
|
@ -29,11 +29,11 @@ export default Vue.extend({
|
|||
'watch-video-recommendations': WatchVideoRecommendations
|
||||
},
|
||||
beforeRouteLeave: function (to, from, next) {
|
||||
this.handleRouteChange()
|
||||
this.handleRouteChange(this.videoId)
|
||||
window.removeEventListener('beforeunload', this.handleWatchProgress)
|
||||
next()
|
||||
},
|
||||
data: function() {
|
||||
data: function () {
|
||||
return {
|
||||
isLoading: false,
|
||||
firstLoad: true,
|
||||
|
@ -75,7 +75,9 @@ export default Vue.extend({
|
|||
playlistId: '',
|
||||
timestamp: null,
|
||||
playNextTimeout: null,
|
||||
playNextCountDownIntervalId: null
|
||||
playNextCountDownIntervalId: null,
|
||||
pictureInPictureButtonInverval: null,
|
||||
infoAreaSticky: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -127,6 +129,9 @@ export default Vue.extend({
|
|||
playNextVideo: function () {
|
||||
return this.$store.getters.getPlayNextVideo
|
||||
},
|
||||
autoplayPlaylists: function () {
|
||||
return this.$store.getters.getAutoplayPlaylists
|
||||
},
|
||||
hideRecommendedVideos: function () {
|
||||
return this.$store.getters.getHideRecommendedVideos
|
||||
},
|
||||
|
@ -143,13 +148,13 @@ export default Vue.extend({
|
|||
hideVideoLikesAndDislikes: function () {
|
||||
return this.$store.getters.getHideVideoLikesAndDislikes
|
||||
},
|
||||
theatrePossible: function() {
|
||||
theatrePossible: function () {
|
||||
return !this.hideRecommendedVideos || (!this.hideLiveChat && this.isLive) || this.watchingPlaylist
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.handleRouteChange()
|
||||
this.handleRouteChange(this.videoId)
|
||||
// react to route changes...
|
||||
this.videoId = this.$route.params.id
|
||||
|
||||
|
@ -174,6 +179,25 @@ export default Vue.extend({
|
|||
}
|
||||
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 () {
|
||||
|
@ -200,14 +224,14 @@ export default Vue.extend({
|
|||
window.addEventListener('beforeunload', this.handleWatchProgress)
|
||||
},
|
||||
methods: {
|
||||
changeTimestamp: function(timestamp) {
|
||||
changeTimestamp: function (timestamp) {
|
||||
this.$refs.videoPlayer.player.currentTime(timestamp)
|
||||
},
|
||||
toggleTheatreMode: function() {
|
||||
toggleTheatreMode: function () {
|
||||
this.useTheatreMode = !this.useTheatreMode
|
||||
},
|
||||
|
||||
getVideoInformationLocal: function() {
|
||||
getVideoInformationLocal: function () {
|
||||
if (this.firstLoad) {
|
||||
this.isLoading = true
|
||||
}
|
||||
|
@ -256,6 +280,12 @@ export default Vue.extend({
|
|||
this.channelName = result.player_response.videoDetails.author
|
||||
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.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.videoSourceList = result.formats.filter((format) => {
|
||||
|
@ -402,8 +432,9 @@ export default Vue.extend({
|
|||
)
|
||||
|
||||
if (!standardLocale.startsWith('en') && noLocaleCaption) {
|
||||
const baseUrl = result.player_response.captions.playerCaptionsRenderer.baseUrl
|
||||
this.tryAddingTranslatedLocaleCaption(captionTracks, standardLocale, baseUrl)
|
||||
captionTracks.forEach((caption) => {
|
||||
this.tryAddingTranslatedLocaleCaption(captionTracks, standardLocale, caption.baseUrl)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -508,7 +539,7 @@ export default Vue.extend({
|
|||
})
|
||||
},
|
||||
|
||||
getVideoInformationInvidious: function() {
|
||||
getVideoInformationInvidious: function () {
|
||||
if (this.firstLoad) {
|
||||
this.isLoading = true
|
||||
}
|
||||
|
@ -540,7 +571,14 @@ export default Vue.extend({
|
|||
}
|
||||
this.channelId = result.authorId
|
||||
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.videoDescriptionHtml = result.descriptionHtml
|
||||
this.recommendedVideos = result.recommendedVideos
|
||||
|
@ -880,7 +918,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
handleVideoEnded: function () {
|
||||
if (!this.watchingPlaylist && !this.playNextVideo) {
|
||||
if ((!this.watchingPlaylist || !this.autoplayPlaylists) && !this.playNextVideo) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -933,7 +971,11 @@ export default Vue.extend({
|
|||
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)
|
||||
clearInterval(this.playNextCountDownIntervalId)
|
||||
|
||||
|
@ -943,14 +985,13 @@ export default Vue.extend({
|
|||
const player = this.$refs.videoPlayer.player
|
||||
|
||||
if (player !== null && !player.paused() && player.isInPictureInPicture()) {
|
||||
const playerId = this.videoId
|
||||
setTimeout(() => {
|
||||
player.play()
|
||||
player.on('leavepictureinpicture', (event) => {
|
||||
const watchTime = player.currentTime()
|
||||
if (this.$route.fullPath.includes('/watch')) {
|
||||
const routeId = this.$route.params.id
|
||||
if (routeId === playerId) {
|
||||
if (routeId === videoId) {
|
||||
const activePlayer = $('.ftVideoPlayer video').get(0)
|
||||
activePlayer.currentTime = watchTime
|
||||
}
|
||||
|
@ -966,23 +1007,23 @@ export default Vue.extend({
|
|||
if (this.removeVideoMetaFiles) {
|
||||
const userData = await this.getUserDataPath()
|
||||
if (this.isDev) {
|
||||
const dashFileLocation = `static/dashFiles/${this.videoId}.xml`
|
||||
const vttFileLocation = `static/storyboards/${this.videoId}.vtt`
|
||||
const dashFileLocation = `static/dashFiles/${videoId}.xml`
|
||||
const vttFileLocation = `static/storyboards/${videoId}.vtt`
|
||||
// only delete the file it actually exists
|
||||
if (fs.existsSync('static/dashFiles/') && fs.existsSync(dashFileLocation)) {
|
||||
if (fs.existsSync(dashFileLocation)) {
|
||||
fs.rmSync(dashFileLocation)
|
||||
}
|
||||
if (fs.existsSync('static/storyboards/') && fs.existsSync(vttFileLocation)) {
|
||||
if (fs.existsSync(vttFileLocation)) {
|
||||
fs.rmSync(vttFileLocation)
|
||||
}
|
||||
} else {
|
||||
const dashFileLocation = `${userData}/dashFiles/${this.videoId}.xml`
|
||||
const vttFileLocation = `${userData}/storyboards/${this.videoId}.vtt`
|
||||
const dashFileLocation = `${userData}/dashFiles/${videoId}.xml`
|
||||
const vttFileLocation = `${userData}/storyboards/${videoId}.vtt`
|
||||
|
||||
if (fs.existsSync(`${userData}/dashFiles/`) && fs.existsSync(dashFileLocation)) {
|
||||
if (fs.existsSync(dashFileLocation)) {
|
||||
fs.rmSync(dashFileLocation)
|
||||
}
|
||||
if (fs.existsSync(`${userData}/storyboards/`) && fs.existsSync(vttFileLocation)) {
|
||||
if (fs.existsSync(vttFileLocation)) {
|
||||
fs.rmSync(vttFileLocation)
|
||||
}
|
||||
}
|
||||
|
@ -1136,6 +1177,13 @@ export default Vue.extend({
|
|||
label = `${this.$t('Locale Name')} (translated from English)`
|
||||
}
|
||||
|
||||
const indexTranslated = captionTracks.findIndex((item) => {
|
||||
return item.name.simpleText === label
|
||||
})
|
||||
if (indexTranslated !== -1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (enCaptionExists) {
|
||||
url = new URL(captionTracks[enCaptionIdx].baseUrl)
|
||||
} else {
|
||||
|
@ -1240,7 +1288,8 @@ export default Vue.extend({
|
|||
'updateWatchProgress',
|
||||
'getUserDataPath',
|
||||
'ytGetVideoInformation',
|
||||
'invidiousGetVideoInformation'
|
||||
'invidiousGetVideoInformation',
|
||||
'updateSubscriptionDetails'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
=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
|
||||
grid-template: "video video video" auto "info info sidebar" auto "info info sidebar" auto / 1fr 1fr 1fr
|
||||
|
@ -29,7 +29,7 @@
|
|||
grid-area: video
|
||||
|
||||
.videoAreaMargin
|
||||
margin: 0px 8px 16px
|
||||
margin: 0 0 16px
|
||||
|
||||
.videoPlayer
|
||||
grid-column: 1
|
||||
|
@ -61,11 +61,20 @@
|
|||
margin-top: 10px
|
||||
|
||||
.watchVideo
|
||||
margin: 0px 8px 16px
|
||||
margin: 0 0 16px
|
||||
grid-column: 1
|
||||
|
||||
.infoArea
|
||||
grid-area: info
|
||||
position: relative
|
||||
|
||||
@media only screen and (min-width: 901px)
|
||||
.infoArea
|
||||
scroll-margin-top: 76px
|
||||
|
||||
.infoAreaSticky
|
||||
position: sticky
|
||||
top: 76px
|
||||
|
||||
.sidebarArea
|
||||
grid-area: sidebar
|
||||
|
@ -83,4 +92,7 @@
|
|||
height: 500px
|
||||
|
||||
.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