mirror of https://github.com/FreeTubeApp/FreeTube
Merge branch 'development'
Conflicts: package-lock.json package.json
This commit is contained in:
commit
263fad0591
|
@ -25,6 +25,9 @@ Please add all steps to reproduce the behavior:
|
|||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Link to video**
|
||||
If you refer to a problem which occurs within a specific video, or a range of videos, please link at least one here.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, please add screenshots to help explain your problem. Especially console logs from the development tools are very helpful.
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
minApprovals:
|
||||
COLLABORATOR: 2
|
||||
maxRequestedChanges:
|
||||
COLLABORATOR: 0
|
||||
mergeMethod: squash
|
||||
requiredBaseBranches:
|
||||
- development
|
||||
- test
|
||||
reportStatus: true
|
|
@ -0,0 +1,17 @@
|
|||
name: Auto Merge PR
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, auto_merge_disabled]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Auto Merge PR
|
||||
if: contains(${{ github.event.pull_request.base.ref }}, 'development')
|
||||
run: |
|
||||
echo ${{ secrets.PUSH_TOKEN }} >> auth.txt
|
||||
gh auth login --with-token < auth.txt
|
||||
rm auth.txt
|
||||
gh pr merge https://github.com/FreeTubeApp/FreeTube/pull/${{ github.event.pull_request.number }} --auto --squash
|
|
@ -13,14 +13,14 @@ jobs:
|
|||
# This workflow contains a single job called "build"
|
||||
lint:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.X
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.X
|
||||
node-version: 14.x
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
dist/electron/*
|
||||
storyboards/*
|
||||
dashFiles/*
|
||||
static/dashFiles
|
||||
static/storyboards
|
||||
static/dashFiles/*
|
||||
static/storyboards/*
|
||||
dist/web/*
|
||||
build/*
|
||||
!build/icons
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
whitelist:
|
||||
blacklist:
|
||||
- wip
|
||||
- do-not-merge
|
||||
method: squash
|
|
@ -24,4 +24,4 @@ Please follow these guidelines before sending your pull request and making contr
|
|||
|
||||
# Setting up Your Environment
|
||||
|
||||
Check out the [wiki](https://github.com/FreeTubeApp/FreeTube/wiki/Environment-Setup-and-Packaging) page to learn how to set up your environment and create packages.
|
||||
Check out the [wiki](https://docs.freetubeapp.io/development/getting-started/) page to learn how to set up your environment and get started.
|
||||
|
|
|
@ -29,7 +29,7 @@ if (platform == 'darwin') {
|
|||
|
||||
const config = {
|
||||
appId: `io.freetubeapp.${name}`,
|
||||
copyright: 'Copyleft © 2020 freetubeapp@protonmail.com',
|
||||
copyright: 'Copyleft © 2020-2021 freetubeapp@protonmail.com',
|
||||
// asar: false,
|
||||
// compression: 'store',
|
||||
productName,
|
||||
|
|
|
@ -23,6 +23,12 @@ if (remoteDebugging) {
|
|||
process.env.RENDERER_REMOTE_DEBUGGING = true
|
||||
}
|
||||
|
||||
// Define exit code for relaunch and set it in the environment
|
||||
const relaunchExitCode = 69
|
||||
process.env.FREETUBE_RELAUNCH_EXIT_CODE = relaunchExitCode
|
||||
|
||||
const port = 9080
|
||||
|
||||
async function killElectron(pid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (pid) {
|
||||
|
@ -50,7 +56,13 @@ async function restartElectron() {
|
|||
remoteDebugging ? '--remote-debugging-port=9223' : '',
|
||||
])
|
||||
|
||||
electronProcess.on('exit', (code, signal) => {
|
||||
electronProcess.on('exit', (code, _) => {
|
||||
if (code === relaunchExitCode) {
|
||||
electronProcess = null
|
||||
restartElectron()
|
||||
return
|
||||
}
|
||||
|
||||
if (!manualRestart) process.exit(0)
|
||||
})
|
||||
}
|
||||
|
@ -104,13 +116,11 @@ function startRenderer(callback) {
|
|||
})
|
||||
|
||||
const server = new WebpackDevServer(compiler, {
|
||||
contentBase: path.join(__dirname, '../'),
|
||||
hot: true,
|
||||
overlay: true,
|
||||
clientLogLevel: 'warning'
|
||||
static: path.join(process.cwd(), 'static'),
|
||||
port
|
||||
})
|
||||
|
||||
server.listen(9080, '', err => {
|
||||
server.listen(port, '', err => {
|
||||
if (err) console.error(err)
|
||||
|
||||
callback()
|
||||
|
|
|
@ -22,6 +22,12 @@ const config = {
|
|||
entry: {
|
||||
renderer: path.join(__dirname, '../src/renderer/main.js'),
|
||||
},
|
||||
infrastructureLogging: {
|
||||
// Only warnings and errors
|
||||
// level: 'none' disable logging
|
||||
// Please read https://webpack.js.org/configuration/other-options/#infrastructurelogginglevel
|
||||
level: isDevMode ? 'info' : 'none'
|
||||
},
|
||||
output: {
|
||||
libraryTarget: 'commonjs2',
|
||||
path: path.join(__dirname, '../dist'),
|
||||
|
|
File diff suppressed because it is too large
Load Diff
224
package.json
224
package.json
|
@ -1,122 +1,23 @@
|
|||
{
|
||||
"name": "freetube",
|
||||
"productName": "FreeTube",
|
||||
"description": "A private YouTube client",
|
||||
"version": "0.14.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "./dist/main.js",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "PrestonN",
|
||||
"email": "FreeTubeApp@protonmail.com",
|
||||
"url": "https://github.com/FreeTubeApp/FreeTube"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/FreeTubeApp/FreeTube/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^1.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.2",
|
||||
"@silvermine/videojs-quality-selector": "^1.2.5",
|
||||
"autolinker": "^3.14.3",
|
||||
"bulma-pro": "^0.2.0",
|
||||
"dateformat": "^4.5.1",
|
||||
"electron-context-menu": "^3.0.0",
|
||||
"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",
|
||||
"markdown": "^0.5.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"nedb": "^1.8.0",
|
||||
"node-forge": "^0.10.0",
|
||||
"opml-to-json": "^1.0.1",
|
||||
"rss-parser": "^3.12.0",
|
||||
"socks-proxy-agent": "^5.0.0",
|
||||
"video.js": "^7.10.2",
|
||||
"videojs-abloop": "^1.2.0",
|
||||
"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.12",
|
||||
"vue-electron": "^1.0.6",
|
||||
"vue-i18n": "^8.24.4",
|
||||
"vue-observe-visibility": "^1.0.0",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuex": "^3.6.2",
|
||||
"xml2json": "^0.12.0",
|
||||
"youtube-chat": "git+https://github.com/IcedCoffeee/youtube-chat.git",
|
||||
"youtube-suggest": "^1.1.2",
|
||||
"yt-channel-info": "^2.2.0",
|
||||
"yt-comment-scraper": "^4.0.3",
|
||||
"yt-dash-manifest-generator": "1.1.0",
|
||||
"yt-trending-scraper": "^2.0.0",
|
||||
"ytdl-core": "^4.8.3",
|
||||
"ytpl": "^2.2.1",
|
||||
"ytsr": "^3.5.0"
|
||||
},
|
||||
"description": "A private YouTube client",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.14.2",
|
||||
"@babel/preset-env": "^7.14.2",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||
"@typescript-eslint/parser": "^4.23.0",
|
||||
"acorn": "^8.2.4",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"copy-webpack-plugin": "8.1.1",
|
||||
"css-loader": "^5.2.4",
|
||||
"electron": "^12.0.7",
|
||||
"electron-builder": "^22.10.5",
|
||||
"electron-builder-squirrel-windows": "^22.11.4",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-rebuild": "^2.3.5",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-import": "^2.23.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^7.9.0",
|
||||
"fast-glob": "^3.2.5",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.3.1",
|
||||
"jest": "^26.6.3",
|
||||
"mini-css-extract-plugin": "^1.6.0",
|
||||
"node-abi": "^2.26.0",
|
||||
"node-loader": "^2.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.3.0",
|
||||
"sass": "^1.32.13",
|
||||
"sass-loader": "^11.1.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"tree-kill": "1.2.2",
|
||||
"typescript": "^4.2.4",
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-devtools": "^5.1.4",
|
||||
"vue-eslint-parser": "^7.6.0",
|
||||
"vue-loader": "^15.9.7",
|
||||
"vue-style-loader": "^4.1.3",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"webpack": "^5.37.0",
|
||||
"webpack-cli": "^4.7.0",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "./dist/main.js",
|
||||
"name": "freetube",
|
||||
"private": true,
|
||||
"productName": "FreeTube",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/FreeTubeApp/FreeTube.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/FreeTubeApp/FreeTube/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "run-s rebuild:electron pack build-release",
|
||||
"build:arm64": "run-s rebuild:electron pack build-release:arm64",
|
||||
|
@ -124,12 +25,11 @@
|
|||
"build-release": "node _scripts/build.js",
|
||||
"build-release:arm64": "node _scripts/build.js arm64",
|
||||
"build-release:arm32": "node _scripts/build.js arm32",
|
||||
"clean": "rimraf build/ dashFiles/ dist/ storyboards/",
|
||||
"debug": "run-s rebuild:electron debug-runner",
|
||||
"debug-runner": "node _scripts/dev-runner.js --remote-debug",
|
||||
"dev": "run-s rebuild:electron dev-runner",
|
||||
"dev-runner": "node _scripts/dev-runner.js",
|
||||
"electron-builder-install": "electron-builder install-app-deps",
|
||||
"electron-rebuild": "electron-rebuild",
|
||||
"jest": "jest",
|
||||
"jest:coverage": "jest --collect-coverage",
|
||||
"jest:watch": "jest --watch",
|
||||
|
@ -140,13 +40,109 @@
|
|||
"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": "electron-rebuild",
|
||||
"postinstall": "npm run rebuild:electron",
|
||||
"prettier": "prettier --write \"{src,_scripts}/**/*.{js,ts,vue}\"",
|
||||
"rebuild:electron": "run-s electron-builder-install electron-rebuild",
|
||||
"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"
|
||||
},
|
||||
"version": "0.13.2"
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "^2.0.2",
|
||||
"@freetube/youtube-chat": "^1.1.1",
|
||||
"@freetube/yt-comment-scraper": "^6.0.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",
|
||||
"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": "^3.0.2",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"nedb-promises": "^5.0.0",
|
||||
"node-forge": "^0.10.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",
|
||||
"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-dash-manifest-generator": "1.1.0",
|
||||
"yt-trending-scraper": "^2.0.1",
|
||||
"ytdl-core": "^4.9.1",
|
||||
"ytpl": "^2.2.3",
|
||||
"ytsr": "^3.5.3"
|
||||
},
|
||||
"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-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"copy-webpack-plugin": "^9.0.1",
|
||||
"css-loader": "5.2.6",
|
||||
"electron": "^14.0.0",
|
||||
"electron-builder": "^22.11.7",
|
||||
"electron-builder-squirrel-windows": "^22.13.1",
|
||||
"electron-debug": "^3.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^7.17.0",
|
||||
"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",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.38.2",
|
||||
"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",
|
||||
"vue-loader": "^15.9.8",
|
||||
"vue-style-loader": "^4.1.3",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack": "^5.51.1",
|
||||
"webpack-cli": "^4.8.0",
|
||||
"webpack-dev-server": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import { app, BrowserWindow, Menu, ipcMain, screen } from 'electron'
|
||||
import Datastore from 'nedb'
|
||||
import {
|
||||
app, BrowserWindow, dialog, Menu, ipcMain,
|
||||
powerSaveBlocker, screen, session, shell
|
||||
} from 'electron'
|
||||
import Datastore from 'nedb-promises'
|
||||
import path from 'path'
|
||||
import cp from 'child_process'
|
||||
|
||||
if (process.argv.includes('--version')) {
|
||||
console.log(`v${app.getVersion()}`)
|
||||
app.exit(0)
|
||||
app.exit()
|
||||
} else {
|
||||
runApp()
|
||||
}
|
||||
|
||||
function runApp() {
|
||||
require('@electron/remote/main').initialize()
|
||||
|
||||
require('electron-context-menu')({
|
||||
showSearchWithGoogle: false,
|
||||
showSaveImageAs: true,
|
||||
|
@ -20,18 +23,16 @@ function runApp() {
|
|||
|
||||
const localDataStorage = app.getPath('userData') // Grabs the userdata directory based on the user's OS
|
||||
|
||||
const settingsDb = new Datastore({
|
||||
const settingsDb = Datastore.create({
|
||||
filename: localDataStorage + '/settings.db',
|
||||
autoload: true
|
||||
})
|
||||
|
||||
// disable electron warning
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true'
|
||||
const path = require('path')
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const isDebug = process.argv.includes('--debug')
|
||||
let mainWindow
|
||||
let openedWindows = []
|
||||
let startupUrl
|
||||
|
||||
// CORS somehow gets re-enabled in Electron v9.0.4
|
||||
|
@ -56,97 +57,35 @@ function runApp() {
|
|||
app.setAsDefaultProtocolClient('freetube')
|
||||
}
|
||||
|
||||
// TODO: Uncomment if needed
|
||||
// only allow single instance of application
|
||||
if (!isDev) {
|
||||
// Only allow single instance of the application
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
|
||||
if (gotTheLock) {
|
||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (mainWindow && typeof (commandLine) !== 'undefined') {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
||||
mainWindow.focus()
|
||||
|
||||
const url = getLinkUrl(commandLine)
|
||||
if (url) {
|
||||
mainWindow.webContents.send('openUrl', url)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.on('ready', (event, commandLine, workingDirectory) => {
|
||||
settingsDb.find({
|
||||
$or: [
|
||||
{ _id: 'disableSmoothScrolling' },
|
||||
{ _id: 'useProxy' },
|
||||
{ _id: 'proxyProtocol' },
|
||||
{ _id: 'proxyHostname' },
|
||||
{ _id: 'proxyPort' }
|
||||
]
|
||||
}, function (err, doc) {
|
||||
if (err) {
|
||||
app.exit(0)
|
||||
return
|
||||
}
|
||||
|
||||
let disableSmoothScrolling = false
|
||||
let useProxy = false
|
||||
let proxyProtocol = 'socks5'
|
||||
let proxyHostname = '127.0.0.1'
|
||||
let proxyPort = '9050'
|
||||
|
||||
if (typeof doc === 'object' && doc.length > 0) {
|
||||
doc.forEach((dbItem) => {
|
||||
switch (dbItem._id) {
|
||||
case 'disableSmoothScrolling':
|
||||
disableSmoothScrolling = dbItem.value
|
||||
break
|
||||
case 'useProxy':
|
||||
useProxy = dbItem.value
|
||||
break
|
||||
case 'proxyProtocol':
|
||||
proxyProtocol = dbItem.value
|
||||
break
|
||||
case 'proxyHostname':
|
||||
proxyHostname = dbItem.value
|
||||
break
|
||||
case 'proxyPort':
|
||||
proxyPort = dbItem.value
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (disableSmoothScrolling) {
|
||||
app.commandLine.appendSwitch('disable-smooth-scrolling')
|
||||
} else {
|
||||
app.commandLine.appendSwitch('enable-smooth-scrolling')
|
||||
}
|
||||
|
||||
const proxyUrl = `${proxyProtocol}://${proxyHostname}:${proxyPort}`
|
||||
|
||||
createWindow(useProxy, proxyUrl)
|
||||
|
||||
if (isDev) {
|
||||
installDevTools()
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
mainWindow.webContents.openDevTools()
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
if (!gotTheLock) {
|
||||
app.quit()
|
||||
}
|
||||
|
||||
app.on('second-instance', (_, commandLine, __) => {
|
||||
// Someone tried to run a second instance, we should focus our window
|
||||
if (mainWindow && typeof commandLine !== 'undefined') {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
||||
mainWindow.focus()
|
||||
|
||||
const url = getLinkUrl(commandLine)
|
||||
if (url) {
|
||||
mainWindow.webContents.send('openUrl', url)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
require('electron-debug')({
|
||||
showDevTools: !(process.env.RENDERER_REMOTE_DEBUGGING === 'true')
|
||||
})
|
||||
}
|
||||
|
||||
app.on('ready', () => {
|
||||
settingsDb.find({
|
||||
app.on('ready', async (_, __) => {
|
||||
let docArray
|
||||
try {
|
||||
docArray = await settingsDb.find({
|
||||
$or: [
|
||||
{ _id: 'disableSmoothScrolling' },
|
||||
{ _id: 'useProxy' },
|
||||
|
@ -154,62 +93,80 @@ function runApp() {
|
|||
{ _id: 'proxyHostname' },
|
||||
{ _id: 'proxyPort' }
|
||||
]
|
||||
}, function (err, doc) {
|
||||
if (err) {
|
||||
app.exit(0)
|
||||
return
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
app.exit()
|
||||
return
|
||||
}
|
||||
|
||||
let disableSmoothScrolling = false
|
||||
let useProxy = false
|
||||
let proxyProtocol = 'socks5'
|
||||
let proxyHostname = '127.0.0.1'
|
||||
let proxyPort = '9050'
|
||||
let disableSmoothScrolling = false
|
||||
let useProxy = false
|
||||
let proxyProtocol = 'socks5'
|
||||
let proxyHostname = '127.0.0.1'
|
||||
let proxyPort = '9050'
|
||||
|
||||
if (typeof doc === 'object' && doc.length > 0) {
|
||||
doc.forEach((dbItem) => {
|
||||
switch (dbItem._id) {
|
||||
case 'disableSmoothScrolling':
|
||||
disableSmoothScrolling = dbItem.value
|
||||
break
|
||||
case 'useProxy':
|
||||
useProxy = dbItem.value
|
||||
break
|
||||
case 'proxyProtocol':
|
||||
proxyProtocol = dbItem.value
|
||||
break
|
||||
case 'proxyHostname':
|
||||
proxyHostname = dbItem.value
|
||||
break
|
||||
case 'proxyPort':
|
||||
proxyPort = dbItem.value
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (disableSmoothScrolling) {
|
||||
app.commandLine.appendSwitch('disable-smooth-scrolling')
|
||||
} else {
|
||||
app.commandLine.appendSwitch('enable-smooth-scrolling')
|
||||
}
|
||||
|
||||
const proxyUrl = `${proxyProtocol}://${proxyHostname}:${proxyPort}`
|
||||
|
||||
createWindow(useProxy, proxyUrl)
|
||||
|
||||
if (isDev) {
|
||||
installDevTools()
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
mainWindow.webContents.openDevTools()
|
||||
if (docArray?.length > 0) {
|
||||
docArray.forEach((doc) => {
|
||||
switch (doc._id) {
|
||||
case 'disableSmoothScrolling':
|
||||
disableSmoothScrolling = doc.value
|
||||
break
|
||||
case 'useProxy':
|
||||
useProxy = doc.value
|
||||
break
|
||||
case 'proxyProtocol':
|
||||
proxyProtocol = doc.value
|
||||
break
|
||||
case 'proxyHostname':
|
||||
proxyHostname = doc.value
|
||||
break
|
||||
case 'proxyPort':
|
||||
proxyPort = doc.value
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function installDevTools () {
|
||||
if (disableSmoothScrolling) {
|
||||
app.commandLine.appendSwitch('disable-smooth-scrolling')
|
||||
} else {
|
||||
app.commandLine.appendSwitch('enable-smooth-scrolling')
|
||||
}
|
||||
|
||||
if (useProxy) {
|
||||
session.defaultSession.setProxy({
|
||||
proxyRules: `${proxyProtocol}://${proxyHostname}:${proxyPort}`
|
||||
})
|
||||
}
|
||||
|
||||
// Set CONSENT cookie on reasonable domains
|
||||
const consentCookieDomains = [
|
||||
'http://www.youtube.com',
|
||||
'https://www.youtube.com',
|
||||
'http://youtube.com',
|
||||
'https://youtube.com'
|
||||
]
|
||||
consentCookieDomains.forEach(url => {
|
||||
session.defaultSession.cookies.set({
|
||||
url: url,
|
||||
name: 'CONSENT',
|
||||
value: 'YES+'
|
||||
})
|
||||
})
|
||||
|
||||
await createWindow()
|
||||
|
||||
if (isDev) {
|
||||
installDevTools()
|
||||
}
|
||||
|
||||
if (isDebug) {
|
||||
mainWindow.webContents.openDevTools()
|
||||
}
|
||||
})
|
||||
|
||||
async function installDevTools() {
|
||||
try {
|
||||
/* eslint-disable */
|
||||
require('vue-devtools').install()
|
||||
|
@ -219,7 +176,7 @@ function runApp() {
|
|||
}
|
||||
}
|
||||
|
||||
function createWindow (useProxy = false, proxyUrl = '', replaceMainWindow = true) {
|
||||
async function createWindow(replaceMainWindow = true) {
|
||||
/**
|
||||
* Initial window options
|
||||
*/
|
||||
|
@ -236,12 +193,11 @@ function runApp() {
|
|||
nodeIntegrationInWorker: false,
|
||||
webSecurity: false,
|
||||
backgroundThrottling: false,
|
||||
enableRemoteModule: true,
|
||||
contextIsolation: false
|
||||
},
|
||||
show: false
|
||||
})
|
||||
openedWindows.push(newWindow)
|
||||
|
||||
if (replaceMainWindow) {
|
||||
mainWindow = newWindow
|
||||
}
|
||||
|
@ -251,38 +207,9 @@ function runApp() {
|
|||
height: 800
|
||||
})
|
||||
|
||||
if (useProxy) {
|
||||
newWindow.webContents.session.setProxy({
|
||||
proxyRules: proxyUrl
|
||||
})
|
||||
}
|
||||
|
||||
// Set CONSENT cookie on reasonable domains
|
||||
[
|
||||
'http://www.youtube.com',
|
||||
'https://www.youtube.com',
|
||||
'http://youtube.com',
|
||||
'https://youtube.com'
|
||||
].forEach(url => {
|
||||
newWindow.webContents.session.cookies.set({
|
||||
url: url,
|
||||
name: 'CONSENT',
|
||||
value: 'YES+'
|
||||
})
|
||||
})
|
||||
|
||||
settingsDb.findOne({
|
||||
_id: 'bounds'
|
||||
}, function (err, doc) {
|
||||
if (doc === null || err) {
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof doc !== 'object' || typeof doc.value !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
const { maximized, ...bounds } = doc.value
|
||||
const boundsDoc = await settingsDb.findOne({ _id: 'bounds' })
|
||||
if (typeof boundsDoc?.value === 'object') {
|
||||
const { maximized, ...bounds } = boundsDoc.value
|
||||
const allDisplaysSummaryWidth = screen
|
||||
.getAllDisplays()
|
||||
.reduce((accumulator, { size: { width } }) => accumulator + width, 0)
|
||||
|
@ -298,7 +225,7 @@ function runApp() {
|
|||
if (maximized) {
|
||||
newWindow.maximize()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// If called multiple times
|
||||
// Duplicate menu items will be added
|
||||
|
@ -320,114 +247,165 @@ function runApp() {
|
|||
}
|
||||
|
||||
// Show when loaded
|
||||
newWindow.on('ready-to-show', () => {
|
||||
newWindow.once('ready-to-show', () => {
|
||||
newWindow.show()
|
||||
newWindow.focus()
|
||||
})
|
||||
|
||||
newWindow.on('close', () => {
|
||||
// Clear cache and storage if it's the last window
|
||||
if (openedWindows.length === 1) {
|
||||
newWindow.webContents.session.clearCache()
|
||||
newWindow.webContents.session.clearStorageData({
|
||||
storages: [
|
||||
'appcache',
|
||||
'cookies',
|
||||
'filesystem',
|
||||
'indexdb',
|
||||
'shadercache',
|
||||
'websql',
|
||||
'serviceworkers',
|
||||
'cachestorage'
|
||||
]
|
||||
})
|
||||
newWindow.once('close', async () => {
|
||||
if (BrowserWindow.getAllWindows().length !== 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = {
|
||||
...newWindow.getNormalBounds(),
|
||||
maximized: newWindow.isMaximized()
|
||||
}
|
||||
|
||||
await settingsDb.update(
|
||||
{ _id: 'bounds' },
|
||||
{ _id: 'bounds', value },
|
||||
{ upsert: true }
|
||||
)
|
||||
})
|
||||
|
||||
newWindow.on('closed', () => {
|
||||
// Remove closed window
|
||||
openedWindows = openedWindows.filter((window) => window !== newWindow)
|
||||
if (newWindow === mainWindow) {
|
||||
newWindow.once('closed', () => {
|
||||
const allWindows = BrowserWindow.getAllWindows()
|
||||
if (allWindows.length !== 0 && newWindow === mainWindow) {
|
||||
// Replace mainWindow to avoid accessing `mainWindow.webContents`
|
||||
// Which raises "Object has been destroyed" error
|
||||
mainWindow = openedWindows[0]
|
||||
mainWindow = allWindows[0]
|
||||
}
|
||||
|
||||
console.log('closed')
|
||||
})
|
||||
}
|
||||
|
||||
// Save closing window bounds & maximized status
|
||||
ipcMain.on('setBounds', (_e, data) => {
|
||||
const value = {
|
||||
...mainWindow.getNormalBounds(),
|
||||
maximized: mainWindow.isMaximized()
|
||||
}
|
||||
|
||||
settingsDb.findOne({
|
||||
_id: 'bounds'
|
||||
}, function (err, doc) {
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
if (doc !== null) {
|
||||
settingsDb.update({
|
||||
_id: 'bounds'
|
||||
}, {
|
||||
$set: {
|
||||
value
|
||||
}
|
||||
}, {})
|
||||
} else {
|
||||
settingsDb.insert({
|
||||
_id: 'bounds',
|
||||
value
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('appReady', () => {
|
||||
ipcMain.once('appReady', () => {
|
||||
if (startupUrl) {
|
||||
mainWindow.webContents.send('openUrl', startupUrl)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('disableSmoothScrolling', () => {
|
||||
app.commandLine.appendSwitch('disable-smooth-scrolling')
|
||||
mainWindow.close()
|
||||
createWindow()
|
||||
ipcMain.once('relaunchRequest', () => {
|
||||
if (isDev) {
|
||||
app.exit(parseInt(process.env.FREETUBE_RELAUNCH_EXIT_CODE))
|
||||
return
|
||||
}
|
||||
|
||||
// The AppImage and Windows portable formats must be accounted for
|
||||
// because `process.execPath` points at the temporarily extracted
|
||||
// executables, not the executables themselves
|
||||
//
|
||||
// It's possible to detect these formats and identify their
|
||||
// executables' paths by checking the environmental variables
|
||||
const { env: { APPIMAGE, PORTABLE_EXECUTABLE_FILE } } = process
|
||||
|
||||
if (!APPIMAGE) {
|
||||
// If it's a Windows portable, PORTABLE_EXECUTABLE_FILE will
|
||||
// hold a value.
|
||||
// Otherwise, `process.execPath` should be used instead.
|
||||
app.relaunch({
|
||||
args: process.argv.slice(1),
|
||||
execPath: PORTABLE_EXECUTABLE_FILE || process.execPath
|
||||
})
|
||||
} else {
|
||||
// If it's an AppImage, things must be done the "hard way"
|
||||
// `app.relaunch` doesn't work because of FUSE limitations
|
||||
// Spawn a new process using the APPIMAGE env variable
|
||||
cp.spawn(APPIMAGE, { detached: true, stdio: 'ignore' })
|
||||
}
|
||||
|
||||
app.quit()
|
||||
})
|
||||
|
||||
ipcMain.on('enableSmoothScrolling', () => {
|
||||
app.commandLine.appendSwitch('enable-smooth-scrolling')
|
||||
mainWindow.close()
|
||||
createWindow()
|
||||
})
|
||||
|
||||
ipcMain.on('enableProxy', (event, url) => {
|
||||
ipcMain.on('enableProxy', (_, url) => {
|
||||
console.log(url)
|
||||
mainWindow.webContents.session.setProxy({
|
||||
session.defaultSession.setProxy({
|
||||
proxyRules: url
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.on('disableProxy', () => {
|
||||
mainWindow.webContents.session.setProxy({})
|
||||
session.defaultSession.setProxy({})
|
||||
})
|
||||
|
||||
ipcMain.on('openExternalLink', (_, url) => {
|
||||
if (typeof url === 'string') shell.openExternal(url)
|
||||
})
|
||||
|
||||
ipcMain.handle('getSystemLocale', () => {
|
||||
return app.getLocale()
|
||||
})
|
||||
|
||||
ipcMain.handle('getUserDataPath', () => {
|
||||
return app.getPath('userData')
|
||||
})
|
||||
|
||||
ipcMain.on('getUserDataPathSync', (event) => {
|
||||
event.returnValue = app.getPath('userData')
|
||||
})
|
||||
|
||||
ipcMain.handle('showOpenDialog', async (_, options) => {
|
||||
return await dialog.showOpenDialog(options)
|
||||
})
|
||||
|
||||
ipcMain.handle('showSaveDialog', async (_, options) => {
|
||||
return await dialog.showSaveDialog(options)
|
||||
})
|
||||
|
||||
ipcMain.on('stopPowerSaveBlocker', (_, id) => {
|
||||
powerSaveBlocker.stop(id)
|
||||
})
|
||||
|
||||
ipcMain.handle('startPowerSaveBlocker', (_, type) => {
|
||||
return powerSaveBlocker.start(type)
|
||||
})
|
||||
|
||||
ipcMain.on('createNewWindow', () => {
|
||||
createWindow(false, '', false)
|
||||
createWindow(false)
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
ipcMain.on('syncWindows', (event, payload) => {
|
||||
const otherWindows = BrowserWindow.getAllWindows().filter(
|
||||
(window) => {
|
||||
return window.webContents.id !== event.sender.id
|
||||
}
|
||||
)
|
||||
|
||||
for (const window of otherWindows) {
|
||||
window.webContents.send('syncWindows', payload)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('openInExternalPlayer', (_, payload) => {
|
||||
const child = cp.spawn(payload.executable, payload.args, { detached: true, stdio: 'ignore' })
|
||||
child.unref()
|
||||
})
|
||||
|
||||
app.once('window-all-closed', () => {
|
||||
// Clear cache and storage if it's the last window
|
||||
session.defaultSession.clearCache()
|
||||
session.defaultSession.clearStorageData({
|
||||
storages: [
|
||||
'appcache',
|
||||
'cookies',
|
||||
'filesystem',
|
||||
'indexdb',
|
||||
'shadercache',
|
||||
'websql',
|
||||
'serviceworkers',
|
||||
'cachestorage'
|
||||
]
|
||||
})
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow === null || mainWindow === undefined) {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
|
@ -490,85 +468,57 @@ function runApp() {
|
|||
mainWindow.webContents.send('change-view', data)
|
||||
}
|
||||
|
||||
const template = [{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
function setMenu() {
|
||||
const template = [
|
||||
{
|
||||
role: 'quit'
|
||||
label: 'File',
|
||||
submenu: [{ role: 'quit' }]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'cut' },
|
||||
{
|
||||
role: 'copy',
|
||||
accelerator: 'CmdOrCtrl+C',
|
||||
selector: 'copy:'
|
||||
},
|
||||
{
|
||||
role: 'paste',
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
selector: 'paste:'
|
||||
},
|
||||
{ role: 'pasteandmatchstyle' },
|
||||
{ role: 'delete' },
|
||||
{ role: 'selectall' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'reload' },
|
||||
{
|
||||
role: 'forcereload',
|
||||
accelerator: 'CmdOrCtrl+Shift+R'
|
||||
},
|
||||
{ role: 'toggledevtools' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetzoom' },
|
||||
{ role: 'zoomin' },
|
||||
{ role: 'zoomout' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [
|
||||
{ role: 'minimize' },
|
||||
{ role: 'close' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
role: 'cut'
|
||||
},
|
||||
{
|
||||
role: 'copy',
|
||||
accelerator: 'CmdOrCtrl+C',
|
||||
selector: 'copy:'
|
||||
},
|
||||
{
|
||||
role: 'paste',
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
selector: 'paste:'
|
||||
},
|
||||
{
|
||||
role: 'pasteandmatchstyle'
|
||||
},
|
||||
{
|
||||
role: 'delete'
|
||||
},
|
||||
{
|
||||
role: 'selectall'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [{
|
||||
role: 'reload'
|
||||
},
|
||||
{
|
||||
role: 'forcereload',
|
||||
accelerator: 'CmdOrCtrl+Shift+R'
|
||||
},
|
||||
{
|
||||
role: 'toggledevtools'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'resetzoom'
|
||||
},
|
||||
{
|
||||
role: 'zoomin'
|
||||
},
|
||||
{
|
||||
role: 'zoomout'
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
role: 'togglefullscreen'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [{
|
||||
role: 'minimize'
|
||||
},
|
||||
{
|
||||
role: 'close'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
function setMenu () {
|
||||
if (process.platform === 'darwin') {
|
||||
template.unshift({
|
||||
label: app.getName(),
|
||||
|
@ -585,15 +535,11 @@ function runApp() {
|
|||
]
|
||||
})
|
||||
|
||||
template.push({
|
||||
role: 'window'
|
||||
})
|
||||
|
||||
template.push({
|
||||
role: 'help'
|
||||
})
|
||||
|
||||
template.push({ role: 'services' })
|
||||
template.push(
|
||||
{ role: 'window' },
|
||||
{ role: 'help' },
|
||||
{ role: 'services' }
|
||||
)
|
||||
}
|
||||
|
||||
const menu = Menu.buildFromTemplate(template)
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
const ModuleScraper = require('yt-comment-scraper')
|
||||
let scraper = null
|
||||
let currentSort = null
|
||||
let currentVideoId = null
|
||||
|
||||
process.on('message', (msg) => {
|
||||
if (msg === 'end') {
|
||||
process.exit(0)
|
||||
}
|
||||
if (msg.id !== currentVideoId || msg.sortNewest !== currentSort) {
|
||||
if (scraper !== null) {
|
||||
scraper.cleanupStatics()
|
||||
}
|
||||
currentSort = msg.sortNewest
|
||||
currentVideoId = msg.id
|
||||
scraper = new ModuleScraper(true, currentSort)
|
||||
}
|
||||
scraper.scrape_next_page_youtube_comments(currentVideoId).then((comments) => {
|
||||
process.send({ comments: JSON.stringify(comments), error: null })
|
||||
}).catch((error) => {
|
||||
process.send({ comments: null, error: error })
|
||||
})
|
||||
})
|
|
@ -32,6 +32,12 @@ body {
|
|||
margin-bottom: -75px;
|
||||
}
|
||||
|
||||
#changeLogText {
|
||||
overflow-y: scroll;
|
||||
height: 40vh;
|
||||
display: block
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .15s;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Vue from 'vue'
|
||||
import { mapActions } from 'vuex'
|
||||
import { mapActions, mapMutations } from 'vuex'
|
||||
import { ObserveVisibility } from 'vue-observe-visibility'
|
||||
import FtFlexBox from './components/ft-flex-box/ft-flex-box.vue'
|
||||
import TopNav from './components/top-nav/top-nav.vue'
|
||||
|
@ -10,25 +10,13 @@ import FtButton from './components/ft-button/ft-button.vue'
|
|||
import FtToast from './components/ft-toast/ft-toast.vue'
|
||||
import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue'
|
||||
import $ from 'jquery'
|
||||
import { app } from '@electron/remote'
|
||||
import { markdown } from 'markdown'
|
||||
import marked from 'marked'
|
||||
import Parser from 'rss-parser'
|
||||
|
||||
let useElectron
|
||||
let shell
|
||||
let electron
|
||||
let ipcRenderer = null
|
||||
|
||||
Vue.directive('observe-visibility', ObserveVisibility)
|
||||
|
||||
if (window && window.process && window.process.type === 'renderer') {
|
||||
/* eslint-disable-next-line */
|
||||
electron = require('electron')
|
||||
shell = electron.shell
|
||||
useElectron = true
|
||||
} else {
|
||||
useElectron = false
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'App',
|
||||
components: {
|
||||
|
@ -62,6 +50,9 @@ export default Vue.extend({
|
|||
isOpen: function () {
|
||||
return this.$store.getters.getIsSideNavOpen
|
||||
},
|
||||
usingElectron: function() {
|
||||
return this.$store.getters.getUsingElectron
|
||||
},
|
||||
showProgressBar: function () {
|
||||
return this.$store.getters.getShowProgressBar
|
||||
},
|
||||
|
@ -80,33 +71,63 @@ export default Vue.extend({
|
|||
profileList: function () {
|
||||
return this.$store.getters.getProfileList
|
||||
},
|
||||
windowTitle: function () {
|
||||
if (this.$route.meta.title !== 'Channel' && this.$route.meta.title !== 'Watch') {
|
||||
let title =
|
||||
this.$route.meta.path === '/home'
|
||||
? process.env.PRODUCT_NAME
|
||||
: `${this.$t(this.$route.meta.title)} - ${process.env.PRODUCT_NAME}`
|
||||
if (!title) {
|
||||
title = process.env.PRODUCT_NAME
|
||||
}
|
||||
return title
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
activeProfile: function () {
|
||||
return this.$store.getters.getActiveProfile
|
||||
},
|
||||
defaultProfile: function () {
|
||||
return this.$store.getters.getDefaultProfile
|
||||
},
|
||||
externalPlayer: function () {
|
||||
return this.$store.getters.getExternalPlayer
|
||||
},
|
||||
defaultInvidiousInstance: function () {
|
||||
return this.$store.getters.getDefaultInvidiousInstance
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
windowTitle: 'setWindowTitle'
|
||||
},
|
||||
created () {
|
||||
this.setWindowTitle()
|
||||
},
|
||||
mounted: function () {
|
||||
const v = this
|
||||
this.$store.dispatch('grabUserSettings').then(() => {
|
||||
this.$store.dispatch('grabAllProfiles', this.$t('Profile.All Channels')).then(() => {
|
||||
this.$store.dispatch('grabHistory')
|
||||
this.$store.dispatch('grabAllPlaylists')
|
||||
this.$store.commit('setUsingElectron', useElectron)
|
||||
this.grabUserSettings().then(async () => {
|
||||
await this.fetchInvidiousInstances()
|
||||
if (this.defaultInvidiousInstance === '') {
|
||||
await this.setRandomCurrentInvidiousInstance()
|
||||
}
|
||||
|
||||
this.grabAllProfiles(this.$t('Profile.All Channels')).then(async () => {
|
||||
this.grabHistory()
|
||||
this.grabAllPlaylists()
|
||||
this.checkThemeSettings()
|
||||
this.checkLocale()
|
||||
|
||||
v.dataReady = true
|
||||
|
||||
if (useElectron) {
|
||||
if (this.usingElectron) {
|
||||
console.log('User is using Electron')
|
||||
ipcRenderer = require('electron').ipcRenderer
|
||||
this.setupListenerToSyncWindows()
|
||||
this.activateKeyboardShortcuts()
|
||||
this.openAllLinksExternally()
|
||||
this.enableOpenUrl()
|
||||
this.setBoundsOnClose()
|
||||
await this.checkExternalPlayer()
|
||||
}
|
||||
|
||||
this.dataReady = true
|
||||
|
||||
setTimeout(() => {
|
||||
this.checkForNewUpdates()
|
||||
this.checkForNewBlogPosts()
|
||||
|
@ -115,34 +136,6 @@ export default Vue.extend({
|
|||
})
|
||||
},
|
||||
methods: {
|
||||
checkLocale: function () {
|
||||
const locale = localStorage.getItem('locale')
|
||||
|
||||
if (locale === null || locale === 'system') {
|
||||
const systemLocale = app.getLocale().replace(/-|_/, '_')
|
||||
const findLocale = Object.keys(this.$i18n.messages).find((locale) => {
|
||||
const localeName = locale.replace(/-|_/, '_')
|
||||
return localeName.includes(systemLocale)
|
||||
})
|
||||
|
||||
if (typeof findLocale !== 'undefined') {
|
||||
this.$i18n.locale = findLocale
|
||||
localStorage.setItem('locale', 'system')
|
||||
} else {
|
||||
this.$i18n.locale = 'en-US'
|
||||
localStorage.setItem('locale', 'en-US')
|
||||
}
|
||||
} else {
|
||||
this.$i18n.locale = locale
|
||||
}
|
||||
const payload = {
|
||||
isDev: this.isDev,
|
||||
locale: this.$i18n.locale
|
||||
}
|
||||
|
||||
this.$store.dispatch('getRegionData', payload)
|
||||
},
|
||||
|
||||
checkThemeSettings: function () {
|
||||
let baseTheme = localStorage.getItem('baseTheme')
|
||||
let mainColor = localStorage.getItem('mainColor')
|
||||
|
@ -187,7 +180,7 @@ export default Vue.extend({
|
|||
$.getJSON(requestUrl, (response) => {
|
||||
const tagName = response[0].tag_name
|
||||
const versionNumber = tagName.replace('v', '').replace('-beta', '')
|
||||
this.updateChangelog = markdown.toHTML(response[0].body)
|
||||
this.updateChangelog = marked(response[0].body)
|
||||
this.changeLogTitle = response[0].name
|
||||
|
||||
const message = this.$t('Version $ is now available! Click for more details')
|
||||
|
@ -200,7 +193,7 @@ export default Vue.extend({
|
|||
this.showUpdatesBanner = true
|
||||
} else if (parseInt(appVersion[1]) < parseInt(latestVersion[1])) {
|
||||
this.showUpdatesBanner = true
|
||||
} else if (parseInt(appVersion[2]) < parseInt(latestVersion[2])) {
|
||||
} else if (parseInt(appVersion[2]) < parseInt(latestVersion[2]) && parseInt(appVersion[1]) <= parseInt(latestVersion[1])) {
|
||||
this.showUpdatesBanner = true
|
||||
}
|
||||
}).fail((xhr, textStatus, error) => {
|
||||
|
@ -238,6 +231,14 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
checkExternalPlayer: async function () {
|
||||
const payload = {
|
||||
isDev: this.isDev,
|
||||
externalPlayer: this.externalPlayer
|
||||
}
|
||||
this.getExternalPlayerCmdArgumentsData(payload)
|
||||
},
|
||||
|
||||
handleUpdateBannerClick: function (response) {
|
||||
if (response !== false) {
|
||||
this.showReleaseNotes = true
|
||||
|
@ -248,7 +249,7 @@ export default Vue.extend({
|
|||
|
||||
handleNewBlogBannerClick: function (response) {
|
||||
if (response) {
|
||||
shell.openExternal(this.latestBlogUrl)
|
||||
this.openExternalLink(this.latestBlogUrl)
|
||||
}
|
||||
|
||||
this.showBlogBanner = false
|
||||
|
@ -256,7 +257,7 @@ export default Vue.extend({
|
|||
|
||||
openDownloadsPage: function () {
|
||||
const url = 'https://freetubeapp.io#download'
|
||||
shell.openExternal(url)
|
||||
this.openExternalLink(url)
|
||||
this.showReleaseNotes = false
|
||||
this.showUpdatesBanner = false
|
||||
},
|
||||
|
@ -289,7 +290,7 @@ export default Vue.extend({
|
|||
openAllLinksExternally: function () {
|
||||
$(document).on('click', 'a[href^="http"]', (event) => {
|
||||
const el = event.currentTarget
|
||||
console.log(useElectron)
|
||||
console.log(this.usingElectron)
|
||||
console.log(el)
|
||||
event.preventDefault()
|
||||
|
||||
|
@ -301,22 +302,27 @@ export default Vue.extend({
|
|||
this.handleYoutubeLink(el.href)
|
||||
} else {
|
||||
// Open links externally by default
|
||||
if (typeof (shell) !== 'undefined') {
|
||||
shell.openExternal(el.href)
|
||||
}
|
||||
this.openExternalLink(el.href)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
handleYoutubeLink: function (href) {
|
||||
this.$store.dispatch('getYoutubeUrlInfo', href).then((result) => {
|
||||
this.getYoutubeUrlInfo(href).then((result) => {
|
||||
switch (result.urlType) {
|
||||
case 'video': {
|
||||
const { videoId, timestamp } = result
|
||||
const { videoId, timestamp, playlistId } = result
|
||||
|
||||
const query = {}
|
||||
if (timestamp) {
|
||||
query.timestamp = timestamp
|
||||
}
|
||||
if (playlistId && playlistId.length > 0) {
|
||||
query.playlistId = playlistId
|
||||
}
|
||||
this.$router.push({
|
||||
path: `/watch/${videoId}`,
|
||||
query: timestamp ? { timestamp } : {}
|
||||
query: query
|
||||
})
|
||||
break
|
||||
}
|
||||
|
@ -384,24 +390,36 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
enableOpenUrl: function () {
|
||||
const v = this
|
||||
electron.ipcRenderer.on('openUrl', function (event, url) {
|
||||
ipcRenderer.on('openUrl', (event, url) => {
|
||||
if (url) {
|
||||
v.handleYoutubeLink(url)
|
||||
this.handleYoutubeLink(url)
|
||||
}
|
||||
})
|
||||
|
||||
electron.ipcRenderer.send('appReady')
|
||||
ipcRenderer.send('appReady')
|
||||
},
|
||||
|
||||
setBoundsOnClose: function () {
|
||||
window.onbeforeunload = (e) => {
|
||||
electron.ipcRenderer.send('setBounds')
|
||||
...mapMutations([
|
||||
'setInvidiousInstancesList'
|
||||
]),
|
||||
|
||||
setWindowTitle: function() {
|
||||
if (this.windowTitle !== null) {
|
||||
document.title = this.windowTitle
|
||||
}
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'showToast'
|
||||
'showToast',
|
||||
'openExternalLink',
|
||||
'grabUserSettings',
|
||||
'grabAllProfiles',
|
||||
'grabHistory',
|
||||
'grabAllPlaylists',
|
||||
'getYoutubeUrlInfo',
|
||||
'getExternalPlayerCmdArgumentsData',
|
||||
'fetchInvidiousInstances',
|
||||
'setRandomCurrentInvidiousInstance',
|
||||
'setupListenerToSyncWindows'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -46,7 +46,10 @@
|
|||
<h2>
|
||||
{{ changeLogTitle }}
|
||||
</h2>
|
||||
<span v-html="updateChangelog" />
|
||||
<span
|
||||
id="changeLogText"
|
||||
v-html="updateChangelog"
|
||||
/>
|
||||
<ft-flex-box>
|
||||
<ft-button
|
||||
:label="$t('Download From Site')"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg aria-hidden="true" focusable="false" fill="white" width="20px" height="19px" data-prefix="fas" data-icon="desktop" class="svg-inline--fa fa-desktop fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M528 0H48C21.5 0 0 21.5 0 48v320c0 26.5 21.5 48 48 48h192l-16 48h-72c-13.3 0-24 10.7-24 24s10.7 24 24 24h272c13.3 0 24-10.7 24-24s-10.7-24-24-24h-72l-16-48h192c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zm-16 352H64V64h448v288z"></path></svg>
|
After Width: | Height: | Size: 483 B |
|
@ -0,0 +1 @@
|
|||
<svg aria-hidden="true" focusable="false" data-prefix="fas" fill="white" width="20px" height="19px" data-icon="tv" class="svg-inline--fa fa-tv fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M592 0H48A48 48 0 0 0 0 48v320a48 48 0 0 0 48 48h240v32H112a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16H352v-32h240a48 48 0 0 0 48-48V48a48 48 0 0 0-48-48zm-16 352H64V64h512z"></path></svg>
|
After Width: | Height: | Size: 458 B |
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,53 @@
|
|||
import Vue from 'vue'
|
||||
import { mapActions } from 'vuex'
|
||||
import FtCard from '../ft-card/ft-card.vue'
|
||||
import FtSelect from '../ft-select/ft-select.vue'
|
||||
import FtInput from '../ft-input/ft-input.vue'
|
||||
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
||||
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ExternalPlayerSettings',
|
||||
components: {
|
||||
'ft-card': FtCard,
|
||||
'ft-select': FtSelect,
|
||||
'ft-input': FtInput,
|
||||
'ft-toggle-switch': FtToggleSwitch,
|
||||
'ft-flex-box': FtFlexBox
|
||||
},
|
||||
data: function () {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
isDev: function () {
|
||||
return process.env.NODE_ENV === 'development'
|
||||
},
|
||||
|
||||
externalPlayerNames: function () {
|
||||
return this.$store.getters.getExternalPlayerNames
|
||||
},
|
||||
externalPlayerValues: function () {
|
||||
return this.$store.getters.getExternalPlayerValues
|
||||
},
|
||||
externalPlayer: function () {
|
||||
return this.$store.getters.getExternalPlayer
|
||||
},
|
||||
externalPlayerExecutable: function () {
|
||||
return this.$store.getters.getExternalPlayerExecutable
|
||||
},
|
||||
externalPlayerIgnoreWarnings: function () {
|
||||
return this.$store.getters.getExternalPlayerIgnoreWarnings
|
||||
},
|
||||
externalPlayerCustomArgs: function () {
|
||||
return this.$store.getters.getExternalPlayerCustomArgs
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'updateExternalPlayer',
|
||||
'updateExternalPlayerExecutable',
|
||||
'updateExternalPlayerIgnoreWarnings',
|
||||
'updateExternalPlayerCustomArgs'
|
||||
])
|
||||
}
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
@use "../../sass-partials/settings"
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<ft-card
|
||||
class="relative card"
|
||||
>
|
||||
<h3
|
||||
class="videoTitle"
|
||||
>
|
||||
{{ $t("Settings.External Player Settings.External Player Settings") }}
|
||||
</h3>
|
||||
<div class="switchColumnGrid">
|
||||
<div class="switchColumn">
|
||||
<ft-select
|
||||
:placeholder="$t('Settings.External Player Settings.External Player')"
|
||||
:value="externalPlayer"
|
||||
:select-names="externalPlayerNames"
|
||||
:select-values="externalPlayerValues"
|
||||
:tooltip="$t('Tooltips.External Player Settings.External Player')"
|
||||
@change="updateExternalPlayer"
|
||||
/>
|
||||
</div>
|
||||
<div class="switchColumn">
|
||||
<ft-toggle-switch
|
||||
:label="$t('Settings.External Player Settings.Ignore Unsupported Action Warnings')"
|
||||
:default-value="externalPlayerIgnoreWarnings"
|
||||
:compact="true"
|
||||
:tooltip="$t('Tooltips.External Player Settings.Ignore Warnings')"
|
||||
@change="updateExternalPlayerIgnoreWarnings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ft-flex-box
|
||||
v-if="externalPlayer !== ''"
|
||||
class="externalPlayerSettingsFlexBox"
|
||||
>
|
||||
<ft-input
|
||||
:placeholder="$t('Settings.External Player Settings.Custom External Player Executable')"
|
||||
:show-arrow="false"
|
||||
:show-label="true"
|
||||
:value="externalPlayerExecutable"
|
||||
:tooltip="$t('Tooltips.External Player Settings.Custom External Player Executable')"
|
||||
@input="updateExternalPlayerExecutable"
|
||||
/>
|
||||
<ft-input
|
||||
:placeholder="$t('Settings.External Player Settings.Custom External Player Arguments')"
|
||||
:show-arrow="false"
|
||||
:show-label="true"
|
||||
:value="externalPlayerCustomArgs"
|
||||
:tooltip="$t('Tooltips.External Player Settings.Custom External Player Arguments')"
|
||||
@input="updateExternalPlayerCustomArgs"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
</ft-card>
|
||||
</template>
|
||||
|
||||
<script src="./external-player-settings.js" />
|
||||
<style scoped lang="sass" src="./external-player-settings.sass" />
|
|
@ -51,7 +51,7 @@ export default Vue.extend({
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
showDropdown: false,
|
||||
dropdownShown: false,
|
||||
id: ''
|
||||
}
|
||||
},
|
||||
|
@ -60,25 +60,46 @@ export default Vue.extend({
|
|||
},
|
||||
methods: {
|
||||
toggleDropdown: function () {
|
||||
$(`#${this.id}`)[0].style.display = 'inline'
|
||||
$(`#${this.id}`).focus()
|
||||
const dropdownBox = $(`#${this.id}`)
|
||||
|
||||
$(`#${this.id}`).focusout(() => {
|
||||
const shareLinks = $(`#${this.id}`).find('.shareLinks')
|
||||
if (this.dropdownShown) {
|
||||
dropdownBox.get(0).style.display = 'none'
|
||||
this.dropdownShown = false
|
||||
} else {
|
||||
dropdownBox.get(0).style.display = 'inline'
|
||||
dropdownBox.get(0).focus()
|
||||
this.dropdownShown = true
|
||||
|
||||
if (shareLinks.length > 0) {
|
||||
if (!shareLinks[0].parentNode.matches(':hover')) {
|
||||
$(`#${this.id}`)[0].style.display = 'none'
|
||||
dropdownBox.focusout(() => {
|
||||
const shareLinks = dropdownBox.find('.shareLinks')
|
||||
|
||||
if (shareLinks.length > 0) {
|
||||
if (!shareLinks[0].parentNode.matches(':hover')) {
|
||||
dropdownBox.get(0).style.display = 'none'
|
||||
// When pressing the profile button
|
||||
// It will make the menu reappear if we set `dropdownShown` immediately
|
||||
setTimeout(() => {
|
||||
this.dropdownShown = false
|
||||
}, 100)
|
||||
}
|
||||
} else {
|
||||
dropdownBox.get(0).style.display = 'none'
|
||||
// When pressing the profile button
|
||||
// It will make the menu reappear if we set `dropdownShown` immediately
|
||||
setTimeout(() => {
|
||||
this.dropdownShown = false
|
||||
}, 100)
|
||||
}
|
||||
} else {
|
||||
$(`#${this.id}`)[0].style.display = 'none'
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
focusOut: function () {
|
||||
$(`#${this.id}`).focusout()
|
||||
$(`#${this.id}`)[0].style.display = 'none'
|
||||
const dropdownBox = $(`#${this.id}`)
|
||||
|
||||
dropdownBox.focusout()
|
||||
dropdownBox.get(0).style.display = 'none'
|
||||
this.dropdownShown = false
|
||||
},
|
||||
|
||||
handleIconClick: function () {
|
||||
|
|
|
@ -35,6 +35,10 @@ export default Vue.extend({
|
|||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
spellcheck: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
dataList: {
|
||||
type: Array,
|
||||
default: () => { return [] }
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
type="text"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:spellcheck="spellcheck"
|
||||
@input="e => handleInput(e.target.value)"
|
||||
@focus="handleFocus"
|
||||
@blur="handleInputBlur"
|
||||
|
|
|
@ -24,8 +24,8 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
listType: function () {
|
||||
return this.$store.getters.getListType
|
||||
|
@ -66,7 +66,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
parseInvidiousData: function () {
|
||||
this.thumbnail = this.data.authorThumbnails[2].url.replace('https://yt3.ggpht.com', `${this.invidiousInstance}/ggpht/`)
|
||||
this.thumbnail = this.data.authorThumbnails[2].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||
this.channelName = this.data.author
|
||||
this.id = this.data.authorId
|
||||
if (this.hideChannelSubscriptions) {
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import Vue from 'vue'
|
||||
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FtListVideo',
|
||||
name: 'FtListPlaylist',
|
||||
components: {
|
||||
'ft-icon-button': FtIconButton
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
|
@ -24,8 +29,8 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
|
||||
listType: function () {
|
||||
|
@ -40,6 +45,14 @@ export default Vue.extend({
|
|||
let id = this.channelLink.replace('https://www.youtube.com/user/', '')
|
||||
id = id.replace('https://www.youtube.com/channel/', '')
|
||||
return id
|
||||
},
|
||||
|
||||
externalPlayer: function () {
|
||||
return this.$store.getters.getExternalPlayer
|
||||
},
|
||||
|
||||
defaultPlayback: function () {
|
||||
return this.$store.getters.getDefaultPlayback
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
|
@ -50,9 +63,23 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
handleExternalPlayer: function () {
|
||||
this.openInExternalPlayer({
|
||||
strings: this.$t('Video.External Player'),
|
||||
watchProgress: 0,
|
||||
playbackRate: this.defaultPlayback,
|
||||
videoId: null,
|
||||
playlistId: this.playlistId,
|
||||
playlistIndex: null,
|
||||
playlistReverse: null,
|
||||
playlistShuffle: null,
|
||||
playlistLoop: null
|
||||
})
|
||||
},
|
||||
|
||||
parseInvidiousData: function () {
|
||||
this.title = this.data.title
|
||||
this.thumbnail = this.data.playlistThumbnail.replace('https://i.ytimg.com', this.invidiousInstance).replace('hqdefault', 'mqdefault')
|
||||
this.thumbnail = this.data.playlistThumbnail.replace('https://i.ytimg.com', this.currentInvidiousInstance).replace('hqdefault', 'mqdefault')
|
||||
this.channelName = this.data.author
|
||||
this.channelLink = this.data.authorUrl
|
||||
this.playlistLink = this.data.playlistId
|
||||
|
@ -70,6 +97,10 @@ export default Vue.extend({
|
|||
this.channelLink = this.data.owner.url
|
||||
this.playlistLink = this.data.url
|
||||
this.videoCount = this.data.length
|
||||
}
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'openInExternalPlayer'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -23,6 +23,16 @@
|
|||
</div>
|
||||
</router-link>
|
||||
<div class="info">
|
||||
<ft-icon-button
|
||||
v-if="externalPlayer !== ''"
|
||||
:title="$t('Video.External Player.OpenInTemplate').replace('$', externalPlayer)"
|
||||
icon="external-link-alt"
|
||||
class="externalPlayerButton"
|
||||
theme="base-no-default"
|
||||
:size="16"
|
||||
:use-shadow="false"
|
||||
@click="handleExternalPlayer"
|
||||
/>
|
||||
<router-link
|
||||
class="title"
|
||||
:to="`/playlist/${playlistId}`"
|
||||
|
|
|
@ -16,6 +16,22 @@ export default Vue.extend({
|
|||
type: String,
|
||||
default: null
|
||||
},
|
||||
playlistIndex: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
playlistReverse: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
playlistShuffle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
playlistLoop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
forceListType: {
|
||||
type: String,
|
||||
default: null
|
||||
|
@ -60,10 +76,6 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
usingElectron: function () {
|
||||
return this.$store.getters.getUsingElectron
|
||||
},
|
||||
|
||||
historyCache: function () {
|
||||
return this.$store.getters.getHistoryCache
|
||||
},
|
||||
|
@ -80,8 +92,8 @@ export default Vue.extend({
|
|||
return this.$store.getters.getBackendPreference
|
||||
},
|
||||
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
|
||||
inHistory: function () {
|
||||
|
@ -91,11 +103,11 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
invidiousUrl: function () {
|
||||
return `${this.invidiousInstance}/watch?v=${this.id}`
|
||||
return `${this.currentInvidiousInstance}/watch?v=${this.id}`
|
||||
},
|
||||
|
||||
invidiousChannelUrl: function () {
|
||||
return `${this.invidiousInstance}/channel/${this.channelId}`
|
||||
return `${this.currentInvidiousInstance}/channel/${this.channelId}`
|
||||
},
|
||||
|
||||
youtubeUrl: function () {
|
||||
|
@ -144,7 +156,7 @@ export default Vue.extend({
|
|||
thumbnail: function () {
|
||||
let baseUrl
|
||||
if (this.backendPreference === 'invidious') {
|
||||
baseUrl = this.invidiousInstance
|
||||
baseUrl = this.currentInvidiousInstance
|
||||
} else {
|
||||
baseUrl = 'https://i.ytimg.com'
|
||||
}
|
||||
|
@ -182,6 +194,18 @@ export default Vue.extend({
|
|||
|
||||
favoriteIconTheme: function () {
|
||||
return this.inFavoritesPlaylist ? 'base favorite' : 'base'
|
||||
},
|
||||
|
||||
externalPlayer: function () {
|
||||
return this.$store.getters.getExternalPlayer
|
||||
},
|
||||
|
||||
defaultPlayback: function () {
|
||||
return this.$store.getters.getDefaultPlayback
|
||||
},
|
||||
|
||||
saveWatchedProgress: function () {
|
||||
return this.$store.getters.getSaveWatchedProgress
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
|
@ -189,6 +213,26 @@ export default Vue.extend({
|
|||
this.checkIfWatched()
|
||||
},
|
||||
methods: {
|
||||
handleExternalPlayer: function () {
|
||||
this.$emit('pause-player')
|
||||
|
||||
this.openInExternalPlayer({
|
||||
strings: this.$t('Video.External Player'),
|
||||
watchProgress: this.watchProgress,
|
||||
playbackRate: this.defaultPlayback,
|
||||
videoId: this.id,
|
||||
playlistId: this.playlistId,
|
||||
playlistIndex: this.playlistIndex,
|
||||
playlistReverse: this.playlistReverse,
|
||||
playlistShuffle: this.playlistShuffle,
|
||||
playlistLoop: this.playlistLoop
|
||||
})
|
||||
|
||||
if (this.saveWatchedProgress && !this.watched) {
|
||||
this.markAsWatched()
|
||||
}
|
||||
},
|
||||
|
||||
toggleSave: function () {
|
||||
if (this.inFavoritesPlaylist) {
|
||||
this.removeFromPlaylist()
|
||||
|
@ -216,10 +260,7 @@ export default Vue.extend({
|
|||
})
|
||||
break
|
||||
case 'openYoutube':
|
||||
if (this.usingElectron) {
|
||||
const shell = require('electron').shell
|
||||
shell.openExternal(this.youtubeUrl)
|
||||
}
|
||||
this.openExternalLink(this.youtubeUrl)
|
||||
break
|
||||
case 'copyYoutubeEmbed':
|
||||
navigator.clipboard.writeText(this.youtubeEmbedUrl)
|
||||
|
@ -228,10 +269,7 @@ export default Vue.extend({
|
|||
})
|
||||
break
|
||||
case 'openYoutubeEmbed':
|
||||
if (this.usingElectron) {
|
||||
const shell = require('electron').shell
|
||||
shell.openExternal(this.youtubeEmbedUrl)
|
||||
}
|
||||
this.openExternalLink(this.youtubeEmbedUrl)
|
||||
break
|
||||
case 'copyInvidious':
|
||||
navigator.clipboard.writeText(this.invidiousUrl)
|
||||
|
@ -240,11 +278,7 @@ export default Vue.extend({
|
|||
})
|
||||
break
|
||||
case 'openInvidious':
|
||||
if (this.usingElectron) {
|
||||
console.log('using electron')
|
||||
const shell = require('electron').shell
|
||||
shell.openExternal(this.invidiousUrl)
|
||||
}
|
||||
this.openExternalLink(this.invidiousUrl)
|
||||
break
|
||||
case 'copyYoutubeChannel':
|
||||
navigator.clipboard.writeText(this.youtubeChannelUrl)
|
||||
|
@ -253,10 +287,7 @@ export default Vue.extend({
|
|||
})
|
||||
break
|
||||
case 'openYoutubeChannel':
|
||||
if (this.usingElectron) {
|
||||
const shell = require('electron').shell
|
||||
shell.openExternal(this.youtubeChannelUrl)
|
||||
}
|
||||
this.openExternalLink(this.youtubeChannelUrl)
|
||||
break
|
||||
case 'copyInvidiousChannel':
|
||||
navigator.clipboard.writeText(this.invidiousChannelUrl)
|
||||
|
@ -265,10 +296,7 @@ export default Vue.extend({
|
|||
})
|
||||
break
|
||||
case 'openInvidiousChannel':
|
||||
if (this.usingElectron) {
|
||||
const shell = require('electron').shell
|
||||
shell.openExternal(this.invidiousChannelUrl)
|
||||
}
|
||||
this.openExternalLink(this.invidiousChannelUrl)
|
||||
break
|
||||
}
|
||||
},
|
||||
|
@ -385,7 +413,7 @@ export default Vue.extend({
|
|||
title: this.title,
|
||||
author: this.channelName,
|
||||
authorId: this.channelId,
|
||||
published: this.publishedText.split(',')[0],
|
||||
published: this.publishedText ? this.publishedText.split(',')[0] : this.publishedText,
|
||||
description: this.description,
|
||||
viewCount: this.viewCount,
|
||||
lengthSeconds: this.data.lengthSeconds,
|
||||
|
@ -457,10 +485,12 @@ export default Vue.extend({
|
|||
...mapActions([
|
||||
'showToast',
|
||||
'toLocalePublicationString',
|
||||
'openInExternalPlayer',
|
||||
'updateHistory',
|
||||
'removeFromHistory',
|
||||
'addVideo',
|
||||
'removeVideo'
|
||||
'removeVideo',
|
||||
'openExternalLink'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -31,6 +31,16 @@
|
|||
>
|
||||
{{ isLive ? $t("Video.Live") : duration }}
|
||||
</div>
|
||||
<ft-icon-button
|
||||
v-if="externalPlayer !== ''"
|
||||
:title="$t('Video.External Player.OpenInTemplate').replace('$', externalPlayer)"
|
||||
icon="external-link-alt"
|
||||
class="externalPlayerIcon"
|
||||
theme="base"
|
||||
:padding="appearance === `watchPlaylistItem` ? 6 : 7"
|
||||
:size="appearance === `watchPlaylistItem` ? 12 : 16"
|
||||
@click="handleExternalPlayer"
|
||||
/>
|
||||
<ft-icon-button
|
||||
v-if="!isLive"
|
||||
:title="$t('Video.Save Video')"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.bubblePadding {
|
||||
width: 100px;
|
||||
height: 115px;
|
||||
padding: 10px;
|
||||
padding: 10px 10px 30px 10px;
|
||||
cursor: pointer;
|
||||
-webkit-transition: background 0.2s ease-out;
|
||||
-moz-transition: background 0.2s ease-out;
|
||||
|
@ -17,25 +17,23 @@
|
|||
}
|
||||
|
||||
.bubble {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 25px;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
-webkit-border-radius: 200px 200px 200px 200px;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin: 20px auto 5px auto;
|
||||
border-radius: 50%;
|
||||
-webkit-border-radius: 50%;
|
||||
}
|
||||
|
||||
.initial {
|
||||
font-size: 25px;
|
||||
font-size: 35px;
|
||||
line-height: 1em;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: 12px;
|
||||
padding: 17.5px 0;
|
||||
}
|
||||
|
||||
.profileName {
|
||||
font-size: 13px;
|
||||
height: 60px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5em;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ export default Vue.extend({
|
|||
},
|
||||
computed: {
|
||||
profileInitial: function () {
|
||||
return this.profileName.slice(0, 1).toUpperCase()
|
||||
return this?.profileName?.length > 0 ? Array.from(this.profileName)[0].toUpperCase() : ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
class="bubble"
|
||||
:style="{ background: backgroundColor, color: textColor }"
|
||||
>
|
||||
<p class="initial">
|
||||
<div class="initial">
|
||||
{{ profileInitial }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profileName">
|
||||
{{ profileName }}
|
||||
|
|
|
@ -42,8 +42,8 @@ export default Vue.extend({
|
|||
backendPreference: function () {
|
||||
return this.$store.getters.getBackendPreference
|
||||
},
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
profileList: function () {
|
||||
return this.$store.getters.getProfileList
|
||||
|
@ -80,7 +80,7 @@ export default Vue.extend({
|
|||
return 0
|
||||
}).map((channel) => {
|
||||
if (this.backendPreference === 'invidious') {
|
||||
channel.thumbnail = channel.thumbnail.replace('https://yt3.ggpht.com', `${this.invidiousInstance}/ggpht/`)
|
||||
channel.thumbnail = channel.thumbnail.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||
}
|
||||
channel.selected = false
|
||||
return channel
|
||||
|
@ -101,7 +101,7 @@ export default Vue.extend({
|
|||
return 0
|
||||
}).map((channel) => {
|
||||
if (this.backendPreference === 'invidious') {
|
||||
channel.thumbnail = channel.thumbnail.replace('https://yt3.ggpht.com', `${this.invidiousInstance}/ggpht/`)
|
||||
channel.thumbnail = channel.thumbnail.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||
}
|
||||
channel.selected = false
|
||||
return channel
|
||||
|
|
|
@ -27,15 +27,15 @@
|
|||
height: 100px;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
-webkit-border-radius: 200px 200px 200px 200px;
|
||||
border-radius: 50%;
|
||||
-webkit-border-radius: 50%;
|
||||
}
|
||||
|
||||
.initial {
|
||||
font-size: 50px;
|
||||
line-height: 1em;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
bottom: 27px;
|
||||
padding: 25px 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 680px) {
|
||||
|
|
|
@ -44,7 +44,7 @@ export default Vue.extend({
|
|||
return this.$store.getters.getColorValues
|
||||
},
|
||||
profileInitial: function () {
|
||||
return this.profileName.slice(0, 1).toUpperCase()
|
||||
return this?.profileName?.length > 0 ? Array.from(this.profileName)[0].toUpperCase() : ''
|
||||
},
|
||||
profileList: function () {
|
||||
return this.$store.getters.getProfileList
|
||||
|
|
|
@ -53,11 +53,11 @@
|
|||
:style="{ background: profileBgColor, color: profileTextColor }"
|
||||
style="cursor: default"
|
||||
>
|
||||
<p
|
||||
<div
|
||||
class="initial"
|
||||
>
|
||||
{{ profileInitial }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ft-flex-box>
|
||||
<ft-flex-box>
|
||||
|
|
|
@ -35,8 +35,8 @@ export default Vue.extend({
|
|||
backendPreference: function () {
|
||||
return this.$store.getters.getBackendPreference
|
||||
},
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
profileList: function () {
|
||||
return this.$store.getters.getProfileList
|
||||
|
@ -73,7 +73,7 @@ export default Vue.extend({
|
|||
return index === -1
|
||||
}).map((channel) => {
|
||||
if (this.backendPreference === 'invidious') {
|
||||
channel.thumbnail = channel.thumbnail.replace('https://yt3.ggpht.com', `${this.invidiousInstance}/ggpht/`)
|
||||
channel.thumbnail = channel.thumbnail.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||
}
|
||||
channel.selected = false
|
||||
return channel
|
||||
|
@ -100,7 +100,7 @@ export default Vue.extend({
|
|||
return index === -1
|
||||
}).map((channel) => {
|
||||
if (this.backendPreference === 'invidious') {
|
||||
channel.thumbnail = channel.thumbnail.replace('https://yt3.ggpht.com', `${this.invidiousInstance}/ggpht/`)
|
||||
channel.thumbnail = channel.thumbnail.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||
}
|
||||
channel.selected = false
|
||||
return channel
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
height: 40px;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
-webkit-border-radius: 200px 200px 200px 200px;
|
||||
border-radius: 50%;
|
||||
-webkit-border-radius: 50%;
|
||||
}
|
||||
|
||||
.initial {
|
||||
font-size: 20px;
|
||||
line-height: 1em;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
bottom: 30px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
#profileList {
|
||||
|
|
|
@ -13,7 +13,7 @@ export default Vue.extend({
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
showProfileList: false
|
||||
profileListShown: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -27,23 +27,36 @@ export default Vue.extend({
|
|||
return this.$store.getters.getDefaultProfile
|
||||
},
|
||||
activeProfileInitial: function () {
|
||||
return this.activeProfile.name.slice(0, 1).toUpperCase()
|
||||
return this?.activeProfile?.name?.length > 0 ? Array.from(this.activeProfile.name)[0].toUpperCase() : ''
|
||||
},
|
||||
profileInitials: function () {
|
||||
return this.profileList.map((profile) => {
|
||||
return profile.name.slice(0, 1).toUpperCase()
|
||||
return profile?.name?.length > 0 ? Array.from(profile.name)[0].toUpperCase() : ''
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
$('#profileList').focusout(() => {
|
||||
$('#profileList')[0].style.display = 'none'
|
||||
// When pressing the profile button
|
||||
// It will make the menu reappear if we set `profileListShown` immediately
|
||||
setTimeout(() => {
|
||||
this.profileListShown = false
|
||||
}, 100)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
toggleProfileList: function () {
|
||||
$('#profileList')[0].style.display = 'inline'
|
||||
$('#profileList').focus()
|
||||
const profileList = $('#profileList')
|
||||
|
||||
if (this.profileListShown) {
|
||||
profileList.get(0).style.display = 'none'
|
||||
this.profileListShown = false
|
||||
} else {
|
||||
profileList.get(0).style.display = 'inline'
|
||||
profileList.get(0).focus()
|
||||
this.profileListShown = true
|
||||
}
|
||||
},
|
||||
|
||||
openProfileSettings: function () {
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
:style="{ background: profileList[activeProfile].bgColor, color: profileList[activeProfile].textColor }"
|
||||
@click="toggleProfileList"
|
||||
>
|
||||
<p
|
||||
<div
|
||||
class="initial"
|
||||
>
|
||||
{{ profileInitials[activeProfile] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ft-card
|
||||
id="profileList"
|
||||
|
@ -38,11 +38,11 @@
|
|||
class="colorOption"
|
||||
:style="{ background: profile.bgColor, color: profile.textColor }"
|
||||
>
|
||||
<p
|
||||
<div
|
||||
class="initial"
|
||||
>
|
||||
{{ profileInitials[index] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
class="profileName"
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
.searchFilter {
|
||||
background-color: var(--card-bg-color);
|
||||
padding: 20px;
|
||||
padding-bottom: 70px;
|
||||
.searchFilterInner {
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
padding: 20px 20px 70px 20px;
|
||||
max-height: 410px;
|
||||
overflow-y: auto;
|
||||
|
||||
background-color: var(--card-bg-color);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.1);
|
||||
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.center {
|
||||
|
@ -22,6 +28,6 @@
|
|||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.searchRadio {
|
||||
border-right: 0px;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,15 @@ export default Vue.extend({
|
|||
return this.$store.getters.getSearchSettings
|
||||
},
|
||||
|
||||
filterValueChanged: function() {
|
||||
return [
|
||||
this.$refs.sortByRadio.selectedValue !== this.sortByValues[0],
|
||||
this.$refs.timeRadio.selectedValue !== this.timeValues[0],
|
||||
this.$refs.typeRadio.selectedValue !== this.typeValues[0],
|
||||
this.$refs.durationRadio.selectedValue !== this.durationValues[0]
|
||||
].some((bool) => bool === true)
|
||||
},
|
||||
|
||||
sortByLabels: function () {
|
||||
return [
|
||||
this.$t('Search Filters.Sort By.Most Relevant'),
|
||||
|
@ -82,6 +91,7 @@ export default Vue.extend({
|
|||
methods: {
|
||||
updateSortBy: function (value) {
|
||||
this.$store.commit('setSearchSortBy', value)
|
||||
this.$emit('filterValueUpdated', this.filterValueChanged)
|
||||
},
|
||||
|
||||
updateTime: function (value) {
|
||||
|
@ -91,6 +101,7 @@ export default Vue.extend({
|
|||
this.$store.commit('setSearchType', 'all')
|
||||
}
|
||||
this.$store.commit('setSearchTime', value)
|
||||
this.$emit('filterValueUpdated', this.filterValueChanged)
|
||||
},
|
||||
|
||||
updateType: function (value) {
|
||||
|
@ -103,6 +114,7 @@ export default Vue.extend({
|
|||
this.$store.commit('setSearchDuration', '')
|
||||
}
|
||||
this.$store.commit('setSearchType', value)
|
||||
this.$emit('filterValueUpdated', this.filterValueChanged)
|
||||
},
|
||||
|
||||
updateDuration: function (value) {
|
||||
|
@ -112,6 +124,7 @@ export default Vue.extend({
|
|||
this.updateType('all')
|
||||
}
|
||||
this.$store.commit('setSearchDuration', value)
|
||||
this.$emit('filterValueUpdated', this.filterValueChanged)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,42 +1,44 @@
|
|||
<template>
|
||||
<div class="searchFilter">
|
||||
<h2 class="center">
|
||||
{{ $t("Search Filters.Search Filters") }}
|
||||
</h2>
|
||||
<ft-flex-box class="radioFlexBox">
|
||||
<ft-radio-button
|
||||
ref="sortByRadio"
|
||||
:title="$t('Search Filters.Sort By.Sort By')"
|
||||
:labels="sortByLabels"
|
||||
:values="sortByValues"
|
||||
class="searchRadio"
|
||||
@change="updateSortBy"
|
||||
/>
|
||||
<ft-radio-button
|
||||
ref="timeRadio"
|
||||
:title="$t('Search Filters.Time.Time')"
|
||||
:labels="timeLabels"
|
||||
:values="timeValues"
|
||||
class="searchRadio"
|
||||
@change="updateTime"
|
||||
/>
|
||||
<ft-radio-button
|
||||
ref="typeRadio"
|
||||
:title="$t('Search Filters.Type.Type')"
|
||||
:labels="typeLabels"
|
||||
:values="typeValues"
|
||||
class="searchRadio"
|
||||
@change="updateType"
|
||||
/>
|
||||
<ft-radio-button
|
||||
ref="durationRadio"
|
||||
:title="$t('Search Filters.Duration.Duration')"
|
||||
:labels="durationLabels"
|
||||
:values="durationValues"
|
||||
class="searchRadio"
|
||||
@change="updateDuration"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
<div>
|
||||
<div class="searchFilterInner">
|
||||
<h2 class="center">
|
||||
{{ $t("Search Filters.Search Filters") }}
|
||||
</h2>
|
||||
<ft-flex-box class="radioFlexBox">
|
||||
<ft-radio-button
|
||||
ref="sortByRadio"
|
||||
:title="$t('Search Filters.Sort By.Sort By')"
|
||||
:labels="sortByLabels"
|
||||
:values="sortByValues"
|
||||
class="searchRadio"
|
||||
@change="updateSortBy"
|
||||
/>
|
||||
<ft-radio-button
|
||||
ref="timeRadio"
|
||||
:title="$t('Search Filters.Time.Time')"
|
||||
:labels="timeLabels"
|
||||
:values="timeValues"
|
||||
class="searchRadio"
|
||||
@change="updateTime"
|
||||
/>
|
||||
<ft-radio-button
|
||||
ref="typeRadio"
|
||||
:title="$t('Search Filters.Type.Type')"
|
||||
:labels="typeLabels"
|
||||
:values="typeValues"
|
||||
class="searchRadio"
|
||||
@change="updateType"
|
||||
/>
|
||||
<ft-radio-button
|
||||
ref="durationRadio"
|
||||
:title="$t('Search Filters.Duration.Duration')"
|
||||
:labels="durationLabels"
|
||||
:values="durationValues"
|
||||
class="searchRadio"
|
||||
@change="updateDuration"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -19,6 +19,10 @@ export default Vue.extend({
|
|||
type: String,
|
||||
required: true
|
||||
},
|
||||
playlistId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
getTimestamp: {
|
||||
type: Function,
|
||||
required: true
|
||||
|
@ -30,27 +34,40 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
},
|
||||
|
||||
usingElectron: function () {
|
||||
return this.$store.getters.getUsingElectron
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
|
||||
invidiousURL() {
|
||||
return `${this.invidiousInstance}/watch?v=${this.id}`
|
||||
let videoUrl = `${this.currentInvidiousInstance}/watch?v=${this.id}`
|
||||
// `playlistId` can be undefined
|
||||
if (this.playlistId && this.playlistId.length !== 0) {
|
||||
// `index` seems can be ignored
|
||||
videoUrl += `&list=${this.playlistId}`
|
||||
}
|
||||
return videoUrl
|
||||
},
|
||||
|
||||
invidiousEmbedURL() {
|
||||
return `${this.invidiousInstance}/embed/${this.id}`
|
||||
return `${this.currentInvidiousInstance}/embed/${this.id}`
|
||||
},
|
||||
|
||||
youtubeURL() {
|
||||
return `https://www.youtube.com/watch?v=${this.id}`
|
||||
let videoUrl = `https://www.youtube.com/watch?v=${this.id}`
|
||||
// `playlistId` can be undefined
|
||||
if (this.playlistId && this.playlistId.length !== 0) {
|
||||
// `index` seems can be ignored
|
||||
videoUrl += `&list=${this.playlistId}`
|
||||
}
|
||||
return videoUrl
|
||||
},
|
||||
|
||||
youtubeShareURL() {
|
||||
// `playlistId` can be undefined
|
||||
if (this.playlistId && this.playlistId.length !== 0) {
|
||||
// `index` seems can be ignored
|
||||
return `https://www.youtube.com/watch?v=${this.id}&list=${this.playlistId}`
|
||||
}
|
||||
return `https://youtu.be/${this.id}`
|
||||
},
|
||||
|
||||
|
@ -63,15 +80,8 @@ export default Vue.extend({
|
|||
navigator.clipboard.writeText(text)
|
||||
},
|
||||
|
||||
open(url) {
|
||||
if (this.usingElectron) {
|
||||
const shell = require('electron').shell
|
||||
shell.openExternal(url)
|
||||
}
|
||||
},
|
||||
|
||||
openInvidious() {
|
||||
this.open(this.getFinalUrl(this.invidiousURL))
|
||||
this.openExternalLink(this.getFinalUrl(this.invidiousURL))
|
||||
this.$refs.iconButton.focusOut()
|
||||
},
|
||||
|
||||
|
@ -84,7 +94,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
openYoutube() {
|
||||
this.open(this.getFinalUrl(this.youtubeURL))
|
||||
this.openExternalLink(this.getFinalUrl(this.youtubeURL))
|
||||
this.$refs.iconButton.focusOut()
|
||||
},
|
||||
|
||||
|
@ -97,7 +107,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
openYoutubeEmbed() {
|
||||
this.open(this.getFinalUrl(this.youtubeEmbedURL))
|
||||
this.openExternalLink(this.getFinalUrl(this.youtubeEmbedURL))
|
||||
this.$refs.iconButton.focusOut()
|
||||
},
|
||||
|
||||
|
@ -110,7 +120,7 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
openInvidiousEmbed() {
|
||||
this.open(this.getFinalUrl(this.invidiousEmbedURL))
|
||||
this.openExternalLink(this.getFinalUrl(this.invidiousEmbedURL))
|
||||
this.$refs.iconButton.focusOut()
|
||||
},
|
||||
|
||||
|
@ -122,7 +132,7 @@ export default Vue.extend({
|
|||
this.$refs.iconButton.focusOut()
|
||||
},
|
||||
|
||||
updateincludeTimestamp() {
|
||||
updateIncludeTimestamp() {
|
||||
this.includeTimestamp = !this.includeTimestamp
|
||||
},
|
||||
|
||||
|
@ -134,7 +144,8 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
...mapActions([
|
||||
'showToast'
|
||||
'showToast',
|
||||
'openExternalLink'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
:label="$t('Share.Include Timestamp')"
|
||||
:compact="true"
|
||||
:default-value="includeTimestamp"
|
||||
@change="updateincludeTimestamp"
|
||||
@change="updateIncludeTimestamp"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
<div class="shareLinks">
|
||||
|
|
|
@ -30,8 +30,8 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
if (this.usingElectron && this.powerSaveBlocker !== null) {
|
||||
const { powerSaveBlocker } = require('electron')
|
||||
powerSaveBlocker.stop(this.powerSaveBlocker)
|
||||
const { ipcRenderer } = require('electron')
|
||||
ipcRenderer.send('stopPowerSaveBlocker', this.powerSaveBlocker)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
@ -112,6 +112,7 @@ export default Vue.extend({
|
|||
'subsCapsButton',
|
||||
'audioTrackButton',
|
||||
'pictureInPictureToggle',
|
||||
'toggleTheatreModeButton',
|
||||
'fullWindowButton',
|
||||
'qualitySelector',
|
||||
'fullscreenToggle'
|
||||
|
@ -143,10 +144,23 @@ export default Vue.extend({
|
|||
return this.$store.getters.getDefaultPlayback
|
||||
},
|
||||
|
||||
defaultSkipInterval: function () {
|
||||
return this.$store.getters.getDefaultSkipInterval
|
||||
},
|
||||
|
||||
defaultQuality: function () {
|
||||
return parseInt(this.$store.getters.getDefaultQuality)
|
||||
},
|
||||
|
||||
defaultCaptionSettings: function () {
|
||||
try {
|
||||
return JSON.parse(this.$store.getters.getDefaultCaptionSettings)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return {}
|
||||
}
|
||||
},
|
||||
|
||||
defaultVideoFormat: function () {
|
||||
return this.$store.getters.getDefaultVideoFormat
|
||||
},
|
||||
|
@ -155,12 +169,20 @@ export default Vue.extend({
|
|||
return this.$store.getters.getAutoplayVideos
|
||||
},
|
||||
|
||||
videoVolumeMouseScroll: function () {
|
||||
return this.$store.getters.getVideoVolumeMouseScroll
|
||||
},
|
||||
|
||||
useSponsorBlock: function () {
|
||||
return this.$store.getters.getUseSponsorBlock
|
||||
},
|
||||
|
||||
sponsorBlockShowSkippedToast: function () {
|
||||
return this.$store.getters.getSponsorBlockShowSkippedToast
|
||||
},
|
||||
|
||||
displayVideoPlayButton: function() {
|
||||
return this.$store.getters.getDisplayVideoPlayButton
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
|
@ -174,6 +196,7 @@ export default Vue.extend({
|
|||
|
||||
this.createFullWindowButton()
|
||||
this.createLoopButton()
|
||||
this.createToggleTheatreModeButton()
|
||||
this.determineFormatType()
|
||||
this.determineMaxFramerate()
|
||||
},
|
||||
|
@ -189,8 +212,8 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
if (this.usingElectron && this.powerSaveBlocker !== null) {
|
||||
const { powerSaveBlocker } = require('electron')
|
||||
powerSaveBlocker.stop(this.powerSaveBlocker)
|
||||
const { ipcRenderer } = require('electron')
|
||||
ipcRenderer.send('stopPowerSaveBlocker', this.powerSaveBlocker)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -217,6 +240,12 @@ export default Vue.extend({
|
|||
|
||||
this.player.volume(this.volume)
|
||||
this.player.playbackRate(this.defaultPlayback)
|
||||
this.player.textTrackSettings.setValues(this.defaultCaptionSettings)
|
||||
// Remove big play button
|
||||
// https://github.com/videojs/video.js/blob/v7.12.1/docs/guides/components.md#basic-example
|
||||
if (!this.displayVideoPlayButton) {
|
||||
this.player.removeChild('BigPlayButton')
|
||||
}
|
||||
|
||||
if (this.storyboardSrc !== '') {
|
||||
this.player.vttThumbnails({
|
||||
|
@ -241,6 +270,11 @@ export default Vue.extend({
|
|||
}, 200)
|
||||
}
|
||||
|
||||
// Remove built-in progress bar mouse over current time display
|
||||
// `MouseTimeDisplay` in
|
||||
// https://github.com/videojs/video.js/blob/v7.13.3/docs/guides/components.md#default-component-tree
|
||||
this.player.controlBar.progressControl.seekBar.playProgressBar.removeChild('timeTooltip')
|
||||
|
||||
if (this.useSponsorBlock) {
|
||||
this.initializeSponsorBlock()
|
||||
}
|
||||
|
@ -251,49 +285,56 @@ export default Vue.extend({
|
|||
this.player.on('mouseleave', this.removeMouseTimeout)
|
||||
|
||||
this.player.on('volumechange', this.updateVolume)
|
||||
this.player.controlBar.getChild('volumePanel').on('mousewheel', this.mouseScrollVolume)
|
||||
if (this.videoVolumeMouseScroll) {
|
||||
this.player.on('wheel', this.mouseScrollVolume)
|
||||
} else {
|
||||
this.player.controlBar.getChild('volumePanel').on('wheel', this.mouseScrollVolume)
|
||||
}
|
||||
|
||||
this.player.on('fullscreenchange', this.fullscreenOverlay)
|
||||
this.player.on('fullscreenchange', this.toggleFullscreenClass)
|
||||
|
||||
const v = this
|
||||
|
||||
this.player.on('ready', function () {
|
||||
v.$emit('ready')
|
||||
v.checkAspectRatio()
|
||||
if (v.captionHybridList.length !== 0) {
|
||||
v.transformAndInsertCaptions()
|
||||
this.player.on('ready', () => {
|
||||
this.$emit('ready')
|
||||
this.checkAspectRatio()
|
||||
if (this.captionHybridList.length !== 0) {
|
||||
this.transformAndInsertCaptions()
|
||||
}
|
||||
})
|
||||
|
||||
this.player.on('ended', function () {
|
||||
v.$emit('ended')
|
||||
this.player.on('ended', () => {
|
||||
this.$emit('ended')
|
||||
})
|
||||
|
||||
this.player.on('error', function (error, message) {
|
||||
v.$emit('error', error.target.player.error_)
|
||||
this.player.on('error', (error, message) => {
|
||||
this.$emit('error', error.target.player.error_)
|
||||
})
|
||||
|
||||
this.player.on('play', function () {
|
||||
this.player.on('play', async function () {
|
||||
if (this.usingElectron) {
|
||||
const { powerSaveBlocker } = require('electron')
|
||||
|
||||
this.powerSaveBlocker = powerSaveBlocker.start('prevent-display-sleep')
|
||||
const { ipcRenderer } = require('electron')
|
||||
this.powerSaveBlocker =
|
||||
await ipcRenderer.invoke('startPowerSaveBlocker', 'prevent-display-sleep')
|
||||
}
|
||||
})
|
||||
|
||||
this.player.on('pause', function () {
|
||||
if (this.usingElectron && this.powerSaveBlocker !== null) {
|
||||
const { powerSaveBlocker } = require('electron')
|
||||
powerSaveBlocker.stop(this.powerSaveBlocker)
|
||||
const { ipcRenderer } = require('electron')
|
||||
ipcRenderer.send('stopPowerSaveBlocker', this.powerSaveBlocker)
|
||||
this.powerSaveBlocker = null
|
||||
}
|
||||
})
|
||||
|
||||
this.player.textTrackSettings.on('modalclose', (_) => {
|
||||
const settings = this.player.textTrackSettings.getValues()
|
||||
this.updateDefaultCaptionSettings(JSON.stringify(settings))
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
initializeSponsorBlock() {
|
||||
this.$store.dispatch('sponsorBlockSkipSegments', {
|
||||
this.sponsorBlockSkipSegments({
|
||||
videoId: this.videoId,
|
||||
categories: ['sponsor']
|
||||
}).then((skipSegments) => {
|
||||
|
@ -417,8 +458,10 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
updateVolume: function (event) {
|
||||
const volume = this.player.volume()
|
||||
updateVolume: function (_event) {
|
||||
// 0 means muted
|
||||
// https://docs.videojs.com/html5#volume
|
||||
const volume = this.player.muted() ? 0 : this.player.volume()
|
||||
sessionStorage.setItem('volume', volume)
|
||||
},
|
||||
|
||||
|
@ -866,14 +909,13 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
createLoopButton: function () {
|
||||
const v = this
|
||||
const VjsButton = videojs.getComponent('Button')
|
||||
const loopButton = videojs.extend(VjsButton, {
|
||||
constructor: function(player, options) {
|
||||
VjsButton.call(this, player, options)
|
||||
},
|
||||
handleClick: function() {
|
||||
v.toggleVideoLoop()
|
||||
handleClick: () => {
|
||||
this.toggleVideoLoop()
|
||||
},
|
||||
createControlTextEl: function (button) {
|
||||
return $(button).html($('<div id="loopButton" class="vjs-icon-loop loop-white vjs-button loopWhite"></div>')
|
||||
|
@ -912,14 +954,13 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
createFullWindowButton: function () {
|
||||
const v = this
|
||||
const VjsButton = videojs.getComponent('Button')
|
||||
const fullWindowButton = videojs.extend(VjsButton, {
|
||||
constructor: function(player, options) {
|
||||
VjsButton.call(this, player, options)
|
||||
},
|
||||
handleClick: function() {
|
||||
v.toggleFullWindow()
|
||||
handleClick: () => {
|
||||
this.toggleFullWindow()
|
||||
},
|
||||
createControlTextEl: function (button) {
|
||||
// Add class name to button to be able to target it with CSS selector
|
||||
|
@ -932,8 +973,46 @@ export default Vue.extend({
|
|||
videojs.registerComponent('fullWindowButton', fullWindowButton)
|
||||
},
|
||||
|
||||
createToggleTheatreModeButton: function() {
|
||||
if (!this.$parent.theatrePossible) {
|
||||
return
|
||||
}
|
||||
|
||||
const theatreModeActive = this.$parent.useTheatreMode ? ' vjs-icon-theatre-active' : ''
|
||||
|
||||
const VjsButton = videojs.getComponent('Button')
|
||||
const toggleTheatreModeButton = videojs.extend(VjsButton, {
|
||||
constructor: function(player, options) {
|
||||
VjsButton.call(this, player, options)
|
||||
},
|
||||
handleClick: () => {
|
||||
this.toggleTheatreMode()
|
||||
},
|
||||
createControlTextEl: function (button) {
|
||||
return $(button)
|
||||
.addClass('vjs-button-theatre')
|
||||
.html($(`<div id="toggleTheatreModeButton" class="vjs-icon-theatre-inactive${theatreModeActive} vjs-button"></div>`))
|
||||
.attr('title', 'Toggle Theatre Mode')
|
||||
}
|
||||
})
|
||||
|
||||
videojs.registerComponent('toggleTheatreModeButton', toggleTheatreModeButton)
|
||||
},
|
||||
|
||||
toggleTheatreMode: function() {
|
||||
if (!this.player.isFullscreen_) {
|
||||
const toggleTheatreModeButton = $('#toggleTheatreModeButton')
|
||||
if (!this.$parent.useTheatreMode) {
|
||||
toggleTheatreModeButton.addClass('vjs-icon-theatre-active')
|
||||
} else {
|
||||
toggleTheatreModeButton.removeClass('vjs-icon-theatre-active')
|
||||
}
|
||||
}
|
||||
|
||||
this.$parent.toggleTheatreMode()
|
||||
},
|
||||
|
||||
createDashQualitySelector: function (levels) {
|
||||
const v = this
|
||||
if (levels.levels_.length === 0) {
|
||||
setTimeout(() => {
|
||||
this.createDashQualitySelector(this.player.qualityLevels())
|
||||
|
@ -945,13 +1024,13 @@ export default Vue.extend({
|
|||
constructor: function(player, options) {
|
||||
VjsButton.call(this, player, options)
|
||||
},
|
||||
handleClick: function(event) {
|
||||
handleClick: (event) => {
|
||||
console.log(event)
|
||||
const selectedQuality = event.target.innerText
|
||||
const bitrate = selectedQuality === 'auto' ? 'auto' : parseInt(event.target.attributes.bitrate.value)
|
||||
v.setDashQualityLevel(bitrate)
|
||||
this.setDashQualityLevel(bitrate)
|
||||
},
|
||||
createControlTextEl: function (button) {
|
||||
createControlTextEl: (button) => {
|
||||
const beginningHtml = `<div class="vjs-quality-level-value">
|
||||
<span id="vjs-current-quality">1080p</span>
|
||||
</div>
|
||||
|
@ -975,12 +1054,12 @@ export default Vue.extend({
|
|||
let qualityLabel
|
||||
let bitrate
|
||||
|
||||
if (typeof v.adaptiveFormats !== 'undefined' && v.adaptiveFormats.length > 0) {
|
||||
const adaptiveFormat = v.adaptiveFormats.find((format) => {
|
||||
if (typeof this.adaptiveFormats !== 'undefined' && this.adaptiveFormats.length > 0) {
|
||||
const adaptiveFormat = this.adaptiveFormats.find((format) => {
|
||||
return format.bitrate === quality.bitrate
|
||||
})
|
||||
|
||||
v.activeAdaptiveFormats.push(adaptiveFormat)
|
||||
this.activeAdaptiveFormats.push(adaptiveFormat)
|
||||
|
||||
fps = adaptiveFormat.fps
|
||||
qualityLabel = adaptiveFormat.qualityLabel ? adaptiveFormat.qualityLabel : quality.height + 'p'
|
||||
|
@ -1104,12 +1183,11 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
fullscreenOverlay: function () {
|
||||
const v = this
|
||||
const title = document.title.replace('- FreeTube', '')
|
||||
|
||||
if (this.player.isFullscreen()) {
|
||||
this.player.ready(function () {
|
||||
v.player.overlay({
|
||||
this.player.ready(() => {
|
||||
this.player.overlay({
|
||||
overlays: [{
|
||||
showBackground: false,
|
||||
content: title,
|
||||
|
@ -1119,8 +1197,8 @@ export default Vue.extend({
|
|||
})
|
||||
})
|
||||
} else {
|
||||
this.player.ready(function () {
|
||||
v.player.overlay({
|
||||
this.player.ready(() => {
|
||||
this.player.overlay({
|
||||
overlays: [{
|
||||
showBackground: false,
|
||||
content: ' ',
|
||||
|
@ -1141,9 +1219,8 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
handleTouchStart: function (event) {
|
||||
const v = this
|
||||
this.touchPauseTimeout = setTimeout(() => {
|
||||
v.togglePlayPause()
|
||||
this.togglePlayPause()
|
||||
}, 1000)
|
||||
|
||||
const touchTime = new Date()
|
||||
|
@ -1182,9 +1259,9 @@ export default Vue.extend({
|
|||
break
|
||||
case 74:
|
||||
// J Key
|
||||
// Rewind by 10 seconds
|
||||
// Rewind by 2x the time-skip interval (in seconds)
|
||||
event.preventDefault()
|
||||
this.changeDurationBySeconds(-10)
|
||||
this.changeDurationBySeconds(-this.defaultSkipInterval * 2)
|
||||
break
|
||||
case 75:
|
||||
// K Key
|
||||
|
@ -1194,9 +1271,9 @@ export default Vue.extend({
|
|||
break
|
||||
case 76:
|
||||
// L Key
|
||||
// Fast Forward by 10 seconds
|
||||
// Fast-Forward by 2x the time-skip interval (in seconds)
|
||||
event.preventDefault()
|
||||
this.changeDurationBySeconds(10)
|
||||
this.changeDurationBySeconds(this.defaultSkipInterval * 2)
|
||||
break
|
||||
case 79:
|
||||
// O Key
|
||||
|
@ -1242,15 +1319,15 @@ export default Vue.extend({
|
|||
break
|
||||
case 37:
|
||||
// Left Arrow Key
|
||||
// Rewind by 5 seconds
|
||||
// Rewind by the time-skip interval (in seconds)
|
||||
event.preventDefault()
|
||||
this.changeDurationBySeconds(-5)
|
||||
this.changeDurationBySeconds(-this.defaultSkipInterval * 1)
|
||||
break
|
||||
case 39:
|
||||
// Right Arrow Key
|
||||
// Fast Forward by 5 seconds
|
||||
// Fast-Forward by the time-skip interval (in seconds)
|
||||
event.preventDefault()
|
||||
this.changeDurationBySeconds(5)
|
||||
this.changeDurationBySeconds(this.defaultSkipInterval * 1)
|
||||
break
|
||||
case 49:
|
||||
// 1 Key
|
||||
|
@ -1347,8 +1424,10 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
...mapActions([
|
||||
'calculateColorLuminance',
|
||||
'updateDefaultCaptionSettings',
|
||||
'showToast',
|
||||
'calculateColorLuminance'
|
||||
'sponsorBlockSkipSegments'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import Vue from 'vue'
|
||||
import $ from 'jquery'
|
||||
import { mapActions } from 'vuex'
|
||||
import { app } from '@electron/remote'
|
||||
import { mapActions, mapMutations } from 'vuex'
|
||||
import FtCard from '../ft-card/ft-card.vue'
|
||||
import FtSelect from '../ft-select/ft-select.vue'
|
||||
import FtInput from '../ft-input/ft-input.vue'
|
||||
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
||||
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||
import FtButton from '../ft-button/ft-button.vue'
|
||||
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
|
@ -17,14 +16,13 @@ export default Vue.extend({
|
|||
'ft-select': FtSelect,
|
||||
'ft-input': FtInput,
|
||||
'ft-toggle-switch': FtToggleSwitch,
|
||||
'ft-flex-box': FtFlexBox
|
||||
'ft-flex-box': FtFlexBox,
|
||||
'ft-button': FtButton
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
showInvidiousInstances: false,
|
||||
instanceNames: [],
|
||||
instanceValues: [],
|
||||
currentLocale: '',
|
||||
backendValues: [
|
||||
'invidious',
|
||||
'local'
|
||||
|
@ -59,8 +57,13 @@ export default Vue.extend({
|
|||
isDev: function () {
|
||||
return process.env.NODE_ENV === 'development'
|
||||
},
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
|
||||
usingElectron: function () {
|
||||
return this.$store.getters.getUsingElectron
|
||||
},
|
||||
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
enableSearchSuggestions: function () {
|
||||
return this.$store.getters.getEnableSearchSuggestions
|
||||
|
@ -89,12 +92,21 @@ export default Vue.extend({
|
|||
thumbnailPreference: function () {
|
||||
return this.$store.getters.getThumbnailPreference
|
||||
},
|
||||
currentLocale: function () {
|
||||
return this.$store.getters.getCurrentLocale
|
||||
},
|
||||
regionNames: function () {
|
||||
return this.$store.getters.getRegionNames
|
||||
},
|
||||
regionValues: function () {
|
||||
return this.$store.getters.getRegionValues
|
||||
},
|
||||
invidiousInstancesList: function () {
|
||||
return this.$store.getters.getInvidiousInstancesList
|
||||
},
|
||||
defaultInvidiousInstance: function () {
|
||||
return this.$store.getters.getDefaultInvidiousInstance
|
||||
},
|
||||
|
||||
localeOptions: function () {
|
||||
return ['system'].concat(Object.keys(this.$i18n.messages))
|
||||
|
@ -141,46 +153,42 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
mounted: function () {
|
||||
const requestUrl = 'https://api.invidious.io/instances.json'
|
||||
$.getJSON(requestUrl, (response) => {
|
||||
console.log(response)
|
||||
const instances = response.filter((instance) => {
|
||||
if (instance[0].includes('.onion') || instance[0].includes('.i2p') || instance[0].includes('yewtu.be')) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
this.instanceNames = instances.map((instance) => {
|
||||
return instance[0]
|
||||
})
|
||||
|
||||
this.instanceValues = instances.map((instance) => {
|
||||
return instance[1].uri.replace(/\/$/, '')
|
||||
})
|
||||
|
||||
this.showInvidiousInstances = true
|
||||
}).fail((xhr, textStatus, error) => {
|
||||
console.log(xhr)
|
||||
console.log(textStatus)
|
||||
console.log(requestUrl)
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
this.updateInvidiousInstanceBounce = debounce(this.updateInvidiousInstance, 500)
|
||||
|
||||
this.currentLocale = localStorage.getItem('locale')
|
||||
this.setCurrentInvidiousInstanceBounce =
|
||||
debounce(this.setCurrentInvidiousInstance, 500)
|
||||
},
|
||||
beforeDestroy: function () {
|
||||
if (this.invidiousInstance === '') {
|
||||
this.updateInvidiousInstance('https://invidious.snopyta.org')
|
||||
if (this.currentInvidiousInstance === '') {
|
||||
// FIXME: If we call an action from here, there's no guarantee it will finish
|
||||
// before the component is destroyed, which could bring up some problems
|
||||
// Since I can't see any way to await it (because lifecycle hooks must be
|
||||
// synchronous), unfortunately, we have to copy/paste the logic
|
||||
// from the `setRandomCurrentInvidiousInstance` action onto here
|
||||
const instanceList = this.invidiousInstancesList
|
||||
const randomIndex = Math.floor(Math.random() * instanceList.length)
|
||||
this.setCurrentInvidiousInstance(instanceList[randomIndex])
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInvidiousInstanceInput: function (input) {
|
||||
const invidiousInstance = input.replace(/\/$/, '')
|
||||
this.updateInvidiousInstanceBounce(invidiousInstance)
|
||||
const instance = input.replace(/\/$/, '')
|
||||
this.setCurrentInvidiousInstanceBounce(instance)
|
||||
},
|
||||
|
||||
handleSetDefaultInstanceClick: function () {
|
||||
const instance = this.currentInvidiousInstance
|
||||
this.updateDefaultInvidiousInstance(instance)
|
||||
|
||||
const message = this.$t('Default Invidious instance has been set to $')
|
||||
this.showToast({
|
||||
message: message.replace('$', instance)
|
||||
})
|
||||
},
|
||||
|
||||
handleClearDefaultInstanceClick: function () {
|
||||
this.updateDefaultInvidiousInstance('')
|
||||
this.showToast({
|
||||
message: this.$t('Default Invidious instance has been cleared')
|
||||
})
|
||||
},
|
||||
|
||||
handlePreferredApiBackend: function (backend) {
|
||||
|
@ -192,39 +200,9 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
updateLocale: function (locale) {
|
||||
if (locale === 'system') {
|
||||
const systemLocale = app.getLocale().replace(/-|_/, '_')
|
||||
const findLocale = Object.keys(this.$i18n.messages).find((locale) => {
|
||||
const localeName = locale.replace(/-|_/, '_')
|
||||
return localeName.includes(systemLocale)
|
||||
})
|
||||
|
||||
if (typeof findLocale !== 'undefined') {
|
||||
this.$i18n.locale = findLocale
|
||||
this.currentLocale = 'system'
|
||||
localStorage.setItem('locale', 'system')
|
||||
} else {
|
||||
// Translating this string isn't needed because the user will always see it in English
|
||||
this.showToast({
|
||||
message: 'Locale not found, defaulting to English (US)'
|
||||
})
|
||||
this.$i18n.locale = 'en-US'
|
||||
this.currentLocale = 'en-US'
|
||||
localStorage.setItem('locale', 'en-US')
|
||||
}
|
||||
} else {
|
||||
this.$i18n.locale = locale
|
||||
this.currentLocale = locale
|
||||
localStorage.setItem('locale', locale)
|
||||
}
|
||||
|
||||
const payload = {
|
||||
isDev: this.isDev,
|
||||
locale: this.currentLocale
|
||||
}
|
||||
this.getRegionData(payload)
|
||||
},
|
||||
...mapMutations([
|
||||
'setCurrentInvidiousInstance'
|
||||
]),
|
||||
|
||||
...mapActions([
|
||||
'showToast',
|
||||
|
@ -234,13 +212,13 @@ export default Vue.extend({
|
|||
'updateCheckForBlogPosts',
|
||||
'updateBarColor',
|
||||
'updateBackendPreference',
|
||||
'updateDefaultInvidiousInstance',
|
||||
'updateLandingPage',
|
||||
'updateRegion',
|
||||
'updateListType',
|
||||
'updateThumbnailPreference',
|
||||
'updateInvidiousInstance',
|
||||
'updateForceLocalBackendForLegacy',
|
||||
'getRegionData'
|
||||
'updateCurrentLocale'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
:value="currentLocale"
|
||||
:select-names="localeNames"
|
||||
:select-values="localeOptions"
|
||||
@change="updateLocale"
|
||||
@change="updateCurrentLocale"
|
||||
/>
|
||||
<ft-select
|
||||
:placeholder="$t('Settings.General Settings.Region for Trending')"
|
||||
|
@ -88,21 +88,47 @@
|
|||
</div>
|
||||
<ft-flex-box class="generalSettingsFlexBox">
|
||||
<ft-input
|
||||
:placeholder="$t('Settings.General Settings[\'Invidious Instance (Default is https://invidious.snopyta.org)\']')"
|
||||
:placeholder="$t('Settings.General Settings.Current Invidious Instance')"
|
||||
:show-arrow="false"
|
||||
:show-label="true"
|
||||
:value="invidiousInstance"
|
||||
:data-list="instanceValues"
|
||||
:value="currentInvidiousInstance"
|
||||
:data-list="invidiousInstancesList"
|
||||
:tooltip="$t('Tooltips.General Settings.Invidious Instance')"
|
||||
@input="handleInvidiousInstanceInput"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
<ft-flex-box>
|
||||
<a
|
||||
href="https://api.invidious.io"
|
||||
>
|
||||
{{ $t('Settings.General Settings.View all Invidious instance information') }}
|
||||
</a>
|
||||
<div>
|
||||
<a
|
||||
href="https://api.invidious.io"
|
||||
>
|
||||
{{ $t('Settings.General Settings.View all Invidious instance information') }}
|
||||
</a>
|
||||
</div>
|
||||
</ft-flex-box>
|
||||
<p
|
||||
v-if="defaultInvidiousInstance !== ''"
|
||||
class="center"
|
||||
>
|
||||
{{ $t('Settings.General Settings.The currently set default instance is $').replace('$', defaultInvidiousInstance) }}
|
||||
</p>
|
||||
<template v-else>
|
||||
<p class="center">
|
||||
{{ $t('Settings.General Settings.No default instance has been set') }}
|
||||
</p>
|
||||
<p class="center">
|
||||
{{ $t('Settings.General Settings.Current instance will be randomized on startup') }}
|
||||
</p>
|
||||
</template>
|
||||
<ft-flex-box>
|
||||
<ft-button
|
||||
:label="$t('Settings.General Settings.Set Current Instance as Default')"
|
||||
@click="handleSetDefaultInstanceClick"
|
||||
/>
|
||||
<ft-button
|
||||
:label="$t('Settings.General Settings.Clear Default Instance')"
|
||||
@click="handleClearDefaultInstanceClick"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
</ft-card>
|
||||
</template>
|
||||
|
|
|
@ -62,12 +62,16 @@ export default Vue.extend({
|
|||
return this.$store.getters.getProxyVideos
|
||||
},
|
||||
|
||||
defaultSkipInterval: function () {
|
||||
return parseInt(this.$store.getters.getDefaultSkipInterval)
|
||||
},
|
||||
|
||||
defaultInterval: function () {
|
||||
return parseInt(this.$store.getters.getDefaultInterval)
|
||||
},
|
||||
|
||||
defaultVolume: function () {
|
||||
return parseFloat(this.$store.getters.getDefaultVolume) * 100
|
||||
return Math.round(parseFloat(this.$store.getters.getDefaultVolume) * 100)
|
||||
},
|
||||
|
||||
defaultPlayback: function () {
|
||||
|
@ -90,6 +94,14 @@ export default Vue.extend({
|
|||
return this.$store.getters.getHideRecommendedVideos
|
||||
},
|
||||
|
||||
videoVolumeMouseScroll: function () {
|
||||
return this.$store.getters.getVideoVolumeMouseScroll
|
||||
},
|
||||
|
||||
displayVideoPlayButton: function () {
|
||||
return this.$store.getters.getDisplayVideoPlayButton
|
||||
},
|
||||
|
||||
formatNames: function () {
|
||||
return [
|
||||
this.$t('Settings.Player Settings.Default Video Format.Dash Formats'),
|
||||
|
@ -111,10 +123,6 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
parseVolumeBeforeUpdate: function (volume) {
|
||||
this.updateDefaultVolume(volume / 100)
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'updateAutoplayVideos',
|
||||
'updateAutoplayPlaylists',
|
||||
|
@ -123,11 +131,14 @@ export default Vue.extend({
|
|||
'updateForceLocalBackendForLegacy',
|
||||
'updateProxyVideos',
|
||||
'updateDefaultTheatreMode',
|
||||
'updateDefaultSkipInterval',
|
||||
'updateDefaultInterval',
|
||||
'updateDefaultVolume',
|
||||
'updateDefaultPlayback',
|
||||
'updateDefaultVideoFormat',
|
||||
'updateDefaultQuality'
|
||||
'updateDefaultQuality',
|
||||
'updateVideoVolumeMouseScroll',
|
||||
'updateDisplayVideoPlayButton'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -37,6 +37,18 @@
|
|||
:default-value="defaultTheatreMode"
|
||||
@change="updateDefaultTheatreMode"
|
||||
/>
|
||||
<ft-toggle-switch
|
||||
:label="$t('Settings.Player Settings.Scroll Volume Over Video Player')"
|
||||
:compact="true"
|
||||
:default-value="videoVolumeMouseScroll"
|
||||
@change="updateVideoVolumeMouseScroll"
|
||||
/>
|
||||
<ft-toggle-switch
|
||||
:label="$t('Settings.Player Settings.Display Play Button In Video Player')"
|
||||
:compact="true"
|
||||
:default-value="displayVideoPlayButton"
|
||||
@change="updateDisplayVideoPlayButton"
|
||||
/>
|
||||
</div>
|
||||
<div class="switchColumn">
|
||||
<ft-toggle-switch
|
||||
|
@ -61,6 +73,15 @@
|
|||
</div>
|
||||
</div>
|
||||
<ft-flex-box>
|
||||
<ft-slider
|
||||
:label="$t('Settings.Player Settings.Fast-Forward / Rewind Interval')"
|
||||
:default-value="defaultSkipInterval"
|
||||
:min-value="1"
|
||||
:max-value="70"
|
||||
:step="1"
|
||||
value-extension="s"
|
||||
@change="updateDefaultSkipInterval"
|
||||
/>
|
||||
<ft-slider
|
||||
:label="$t('Settings.Player Settings.Next Video Interval')"
|
||||
:default-value="defaultInterval"
|
||||
|
@ -77,7 +98,7 @@
|
|||
:max-value="100"
|
||||
:step="1"
|
||||
value-extension="%"
|
||||
@change="parseVolumeBeforeUpdate"
|
||||
@change="updateDefaultVolume($event / 100)"
|
||||
/>
|
||||
<ft-slider
|
||||
:label="$t('Settings.Player Settings.Default Playback Rate')"
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
.playListThumbnail {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.playlistThumbnail img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.playlistChannel {
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.playlistChannel img {
|
||||
width: 70px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
border-radius: 200px 200px 200px 200px;
|
||||
-webkit-border-radius: 200px 200px 200px 200px;
|
||||
}
|
||||
|
||||
.playlistChannel h3 {
|
||||
float: left;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
width: 200px;
|
||||
margin-left: 10px;
|
||||
top: 5px;
|
||||
font-size: 15px;
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import Vue from 'vue'
|
||||
import { mapActions } from 'vuex'
|
||||
import FtListDropdown from '../ft-list-dropdown/ft-list-dropdown.vue'
|
||||
import { shell } from 'electron'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FtElementList',
|
||||
name: 'PlaylistInfo',
|
||||
components: {
|
||||
'ft-list-dropdown': FtListDropdown
|
||||
},
|
||||
|
@ -16,7 +16,7 @@ export default Vue.extend({
|
|||
data: function () {
|
||||
return {
|
||||
id: '',
|
||||
randomVideoId: '',
|
||||
firstVideoId: '',
|
||||
title: '',
|
||||
channelThumbnail: '',
|
||||
channelName: '',
|
||||
|
@ -35,8 +35,8 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
|
||||
listType: function () {
|
||||
|
@ -59,23 +59,24 @@ export default Vue.extend({
|
|||
thumbnail: function () {
|
||||
switch (this.thumbnailPreference) {
|
||||
case 'start':
|
||||
return `https://i.ytimg.com/vi/${this.randomVideoId}/mq1.jpg`
|
||||
return `https://i.ytimg.com/vi/${this.firstVideoId}/mq1.jpg`
|
||||
case 'middle':
|
||||
return `https://i.ytimg.com/vi/${this.randomVideoId}/mq2.jpg`
|
||||
return `https://i.ytimg.com/vi/${this.firstVideoId}/mq2.jpg`
|
||||
case 'end':
|
||||
return `https://i.ytimg.com/vi/${this.randomVideoId}/mq3.jpg`
|
||||
return `https://i.ytimg.com/vi/${this.firstVideoId}/mq3.jpg`
|
||||
default:
|
||||
return `https://i.ytimg.com/vi/${this.randomVideoId}/mqdefault.jpg`
|
||||
return `https://i.ytimg.com/vi/${this.firstVideoId}/mqdefault.jpg`
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
console.log(this.data)
|
||||
this.id = this.data.id
|
||||
this.randomVideoId = this.data.randomVideoId
|
||||
this.firstVideoId = this.data.firstVideoId
|
||||
this.title = this.data.title
|
||||
this.channelName = this.data.channelName
|
||||
this.channelThumbnail = this.data.channelThumbnail
|
||||
this.channelId = this.data.channelId
|
||||
this.uploadedTime = this.data.uploaded_at
|
||||
this.description = this.data.description
|
||||
this.infoSource = this.data.infoSource
|
||||
|
@ -94,22 +95,43 @@ export default Vue.extend({
|
|||
methods: {
|
||||
sharePlaylist: function (method) {
|
||||
const youtubeUrl = `https://youtube.com/playlist?list=${this.id}`
|
||||
const invidiousUrl = `${this.invidiousInstance}/playlist?list=${this.id}`
|
||||
const invidiousUrl = `${this.currentInvidiousInstance}/playlist?list=${this.id}`
|
||||
|
||||
switch (method) {
|
||||
case 'copyYoutube':
|
||||
navigator.clipboard.writeText(youtubeUrl)
|
||||
break
|
||||
case 'openYoutube':
|
||||
shell.openExternal(youtubeUrl)
|
||||
this.openExternalLink(youtubeUrl)
|
||||
break
|
||||
case 'copyInvidious':
|
||||
navigator.clipboard.writeText(invidiousUrl)
|
||||
break
|
||||
case 'openInvidious':
|
||||
shell.openExternal(invidiousUrl)
|
||||
this.openExternalLink(invidiousUrl)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
playFirstVideo() {
|
||||
const playlistInfo = {
|
||||
playlistId: this.id
|
||||
}
|
||||
|
||||
this.$router.push(
|
||||
{
|
||||
path: `/watch/${this.firstVideoId}`,
|
||||
query: playlistInfo
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
goToChannel: function () {
|
||||
this.$router.push({ path: `/channel/${this.channelId}` })
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'openExternalLink'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
.playListThumbnail
|
||||
width: 100%
|
||||
|
||||
.playlistThumbnail img
|
||||
width: 100%
|
||||
|
||||
cursor: pointer
|
||||
|
||||
.playlistDescription
|
||||
max-height: 20vh
|
||||
overflow-y: auto
|
||||
|
||||
@media only screen and (max-width: 500px)
|
||||
max-height: 10vh
|
||||
|
||||
.playlistChannel
|
||||
height: 70px
|
||||
|
||||
/* Indicates the box can be clicked to navigate */
|
||||
cursor: pointer
|
||||
|
||||
.channelThumbnail
|
||||
width: 70px
|
||||
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
|
||||
font-size: 15px
|
|
@ -5,6 +5,7 @@
|
|||
>
|
||||
<img
|
||||
:src="thumbnail"
|
||||
@click="playFirstVideo"
|
||||
>
|
||||
</div>
|
||||
<h2>
|
||||
|
@ -17,15 +18,23 @@
|
|||
</span>
|
||||
{{ lastUpdated }}
|
||||
</p>
|
||||
<p>
|
||||
<p
|
||||
class="playlistDescription"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
<hr>
|
||||
<div
|
||||
class="playlistChannel"
|
||||
@click="goToChannel"
|
||||
>
|
||||
<img :src="channelThumbnail">
|
||||
<h3>
|
||||
<img
|
||||
class="channelThumbnail"
|
||||
:src="channelThumbnail"
|
||||
>
|
||||
<h3
|
||||
class="channelName"
|
||||
>
|
||||
{{ channelName }}
|
||||
</h3>
|
||||
</div>
|
||||
|
@ -40,4 +49,4 @@
|
|||
</template>
|
||||
|
||||
<script src="./playlist-info.js" />
|
||||
<style scoped src="./playlist-info.css" />
|
||||
<style scoped lang="sass" src="./playlist-info.sass" />
|
||||
|
|
|
@ -9,7 +9,9 @@ import FtInput from '../ft-input/ft-input.vue'
|
|||
import FtLoader from '../ft-loader/ft-loader.vue'
|
||||
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||
|
||||
import electron from 'electron'
|
||||
// FIXME: Missing web logic branching
|
||||
|
||||
import { ipcRenderer } from 'electron'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
export default Vue.extend({
|
||||
|
@ -109,11 +111,11 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
enableProxy: function () {
|
||||
electron.ipcRenderer.send('enableProxy', this.proxyUrl)
|
||||
ipcRenderer.send('enableProxy', this.proxyUrl)
|
||||
},
|
||||
|
||||
disableProxy: function () {
|
||||
electron.ipcRenderer.send('disableProxy')
|
||||
ipcRenderer.send('disableProxy')
|
||||
},
|
||||
|
||||
testProxy: function () {
|
||||
|
|
|
@ -7,6 +7,14 @@ export default Vue.extend({
|
|||
openMoreOptions: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hidePopularVideos: function () {
|
||||
return this.$store.getters.getHidePopularVideos
|
||||
},
|
||||
hideTrendingVideos: function () {
|
||||
return this.$store.getters.getHideTrendingVideos
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
navigate: function (route) {
|
||||
this.openMoreOptions = false
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
class="moreOptionContainer"
|
||||
>
|
||||
<div
|
||||
v-if="!hideTrendingVideos"
|
||||
class="navOption"
|
||||
@click="navigate('trending')"
|
||||
>
|
||||
|
@ -25,10 +26,11 @@
|
|||
class="navIcon"
|
||||
/>
|
||||
<p class="navLabel">
|
||||
{{ $t("Trending") }}
|
||||
{{ $t("Trending.Trending") }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hidePopularVideos"
|
||||
class="navOption"
|
||||
@click="navigate('popular')"
|
||||
>
|
||||
|
|
|
@ -15,8 +15,8 @@ export default Vue.extend({
|
|||
backendPreference: function () {
|
||||
return this.$store.getters.getBackendPreference
|
||||
},
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
profileList: function () {
|
||||
return this.$store.getters.getProfileList
|
||||
|
@ -38,7 +38,7 @@ export default Vue.extend({
|
|||
return 0
|
||||
}).map((channel) => {
|
||||
if (this.backendPreference === 'invidious') {
|
||||
channel.thumbnail = channel.thumbnail.replace('https://yt3.ggpht.com', `${this.invidiousInstance}/ggpht/`)
|
||||
channel.thumbnail = channel.thumbnail.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||
}
|
||||
|
||||
return channel
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
fixed-width
|
||||
/>
|
||||
<p class="navLabel">
|
||||
{{ $t("Trending") }}
|
||||
{{ $t("Trending.Trending") }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
@ -145,13 +145,6 @@ export default Vue.extend({
|
|||
localStorage.setItem('expandSideBar', value)
|
||||
},
|
||||
|
||||
handleUiScale: function (value) {
|
||||
const { webFrame } = require('electron')
|
||||
const zoomFactor = value / 100
|
||||
webFrame.setZoomFactor(zoomFactor)
|
||||
this.updateUiScale(parseInt(value))
|
||||
},
|
||||
|
||||
handleRestartPrompt: function (value) {
|
||||
this.disableSmoothScrollingToggleValue = value
|
||||
this.showRestartPrompt = true
|
||||
|
@ -165,15 +158,14 @@ export default Vue.extend({
|
|||
return
|
||||
}
|
||||
|
||||
this.updateDisableSmoothScrolling(this.disableSmoothScrollingToggleValue)
|
||||
this.updateDisableSmoothScrolling(
|
||||
this.disableSmoothScrollingToggleValue
|
||||
).then(() => {
|
||||
// FIXME: No electron safeguard
|
||||
const { ipcRenderer } = require('electron')
|
||||
|
||||
const electron = require('electron')
|
||||
|
||||
if (this.disableSmoothScrollingToggleValue) {
|
||||
electron.ipcRenderer.send('disableSmoothScrolling')
|
||||
} else {
|
||||
electron.ipcRenderer.send('enableSmoothScrolling')
|
||||
}
|
||||
ipcRenderer.send('relaunchRequest')
|
||||
})
|
||||
},
|
||||
|
||||
updateMainColor: function (color) {
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
:max-value="maxUiScale"
|
||||
:step="uiScaleStep"
|
||||
value-extension="%"
|
||||
@change="handleUiScale"
|
||||
@change="updateUiScale(parseInt($event))"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
<br>
|
||||
|
|
|
@ -6,7 +6,6 @@ import FtProfileSelector from '../ft-profile-selector/ft-profile-selector.vue'
|
|||
import $ from 'jquery'
|
||||
import debounce from 'lodash.debounce'
|
||||
import ytSuggest from 'youtube-suggest'
|
||||
const { ipcRenderer } = require('electron')
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'TopNav',
|
||||
|
@ -20,10 +19,15 @@ export default Vue.extend({
|
|||
component: this,
|
||||
windowWidth: 0,
|
||||
showFilters: false,
|
||||
searchFilterValueChanged: false,
|
||||
searchSuggestionsDataList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
usingElectron: function () {
|
||||
return this.$store.getters.getUsingElectron
|
||||
},
|
||||
|
||||
enableSearchSuggestions: function () {
|
||||
return this.$store.getters.getEnableSearchSuggestions
|
||||
},
|
||||
|
@ -40,8 +44,8 @@ export default Vue.extend({
|
|||
return this.$store.getters.getBarColor
|
||||
},
|
||||
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
|
||||
backendFallback: function () {
|
||||
|
@ -102,14 +106,21 @@ export default Vue.extend({
|
|||
searchInput.blur()
|
||||
}
|
||||
|
||||
this.$store.dispatch('getYoutubeUrlInfo', query).then((result) => {
|
||||
this.getYoutubeUrlInfo(query).then((result) => {
|
||||
switch (result.urlType) {
|
||||
case 'video': {
|
||||
const { videoId, timestamp } = result
|
||||
const { videoId, timestamp, playlistId } = result
|
||||
|
||||
const query = {}
|
||||
if (timestamp) {
|
||||
query.timestamp = timestamp
|
||||
}
|
||||
if (playlistId && playlistId.length > 0) {
|
||||
query.playlistId = playlistId
|
||||
}
|
||||
this.$router.push({
|
||||
path: `/watch/${videoId}`,
|
||||
query: timestamp ? { timestamp } : {}
|
||||
query: query
|
||||
})
|
||||
break
|
||||
}
|
||||
|
@ -217,7 +228,7 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
|
||||
this.$store.dispatch('invidiousAPICall', searchPayload).then((results) => {
|
||||
this.invidiousAPICall(searchPayload).then((results) => {
|
||||
this.searchSuggestionsDataList = results.suggestions
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
|
@ -242,6 +253,10 @@ export default Vue.extend({
|
|||
this.showFilters = false
|
||||
},
|
||||
|
||||
handleSearchFilterValueChanged: function(filterValueChanged) {
|
||||
this.searchFilterValueChanged = filterValueChanged
|
||||
},
|
||||
|
||||
historyBack: function () {
|
||||
window.history.back()
|
||||
},
|
||||
|
@ -255,11 +270,20 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
createNewWindow: function () {
|
||||
ipcRenderer.send('createNewWindow')
|
||||
if (this.usingElectron) {
|
||||
const { ipcRenderer } = require('electron')
|
||||
ipcRenderer.send('createNewWindow')
|
||||
} else {
|
||||
// Web placeholder
|
||||
}
|
||||
},
|
||||
navigate: function (route) {
|
||||
this.$router.push('/' + route)
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'showToast'
|
||||
'showToast',
|
||||
'getYoutubeUrlInfo',
|
||||
'invidiousAPICall'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -55,6 +55,17 @@
|
|||
@include top-nav-is-colored
|
||||
background-color: var(--primary-color-active)
|
||||
|
||||
.navFilterIcon // Filter icon
|
||||
$effect-distance: 10px
|
||||
|
||||
margin-left: $effect-distance
|
||||
|
||||
&.filterChanged // When filter value changed from default
|
||||
box-shadow: 0 0 $effect-distance var(--primary-color)
|
||||
|
||||
@include top-nav-is-colored
|
||||
box-shadow: 0 0 $effect-distance var(--text-with-main-color)
|
||||
|
||||
.side // parts of the top nav either side of the search bar
|
||||
display: flex
|
||||
align-items: center
|
||||
|
@ -74,7 +85,13 @@
|
|||
display: flex
|
||||
align-items: center
|
||||
padding: 0px 25px 0px 10px
|
||||
cursor: pointer
|
||||
|
||||
&:active
|
||||
background-color: var(--tertiary-text-color)
|
||||
transition: background 0.2s ease-in
|
||||
@include top-nav-is-colored
|
||||
background-color: var(--primary-color-active)
|
||||
.logoIcon
|
||||
background-image: var(--logo-icon)
|
||||
background-repeat: no-repeat
|
||||
|
|
|
@ -44,7 +44,15 @@
|
|||
:title="newWindowText"
|
||||
@click="createNewWindow"
|
||||
/>
|
||||
<div class="logo">
|
||||
<div
|
||||
class="logo"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
:title="$t('Subscriptions.Subscriptions')"
|
||||
@click="navigate('subscriptions')"
|
||||
@keydown.space.prevent="navigate('subscriptions')"
|
||||
@keydown.enter.prevent="navigate('subscriptions')"
|
||||
>
|
||||
<div
|
||||
class="logoIcon"
|
||||
/>
|
||||
|
@ -61,11 +69,13 @@
|
|||
:is-search="true"
|
||||
:select-on-focus="true"
|
||||
:data-list="searchSuggestionsDataList"
|
||||
:spellcheck="false"
|
||||
@input="getSearchSuggestionsDebounce"
|
||||
@click="goToSearch"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
class="navFilterIcon navIcon"
|
||||
:class="{ filterChanged: searchFilterValueChanged }"
|
||||
icon="filter"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
|
@ -77,6 +87,7 @@
|
|||
v-show="showFilters"
|
||||
class="searchFilters"
|
||||
:class="{ expand: !isSideNavOpen }"
|
||||
@filterValueUpdated="handleSearchFilterValueChanged"
|
||||
/>
|
||||
</div>
|
||||
<ft-profile-selector class="side profiles" />
|
||||
|
|
|
@ -5,7 +5,7 @@ import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
|||
import FtSelect from '../../components/ft-select/ft-select.vue'
|
||||
import FtTimestampCatcher from '../../components/ft-timestamp-catcher/ft-timestamp-catcher.vue'
|
||||
import autolinker from 'autolinker'
|
||||
import ytcm from 'yt-comment-scraper'
|
||||
import ytcm from '@freetube/yt-comment-scraper'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'WatchVideoComments',
|
||||
|
@ -50,8 +50,8 @@ export default Vue.extend({
|
|||
return this.$store.getters.getBackendFallback
|
||||
},
|
||||
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
hideCommentLikes: function () {
|
||||
return this.$store.getters.getHideCommentLikes
|
||||
|
@ -164,7 +164,7 @@ export default Vue.extend({
|
|||
videoId: this.id,
|
||||
setCookie: false,
|
||||
sortByNewest: this.sortNewest,
|
||||
continuation: this.commentData[index].replyToken,
|
||||
replyToken: this.commentData[index].replyToken,
|
||||
index: index
|
||||
})
|
||||
break
|
||||
|
@ -191,7 +191,14 @@ export default Vue.extend({
|
|||
this.showToast({
|
||||
message: this.$t('Falling back to Invidious API')
|
||||
})
|
||||
this.getCommentDataInvidious()
|
||||
this.getCommentDataInvidious({
|
||||
resource: 'comments',
|
||||
id: this.id,
|
||||
params: {
|
||||
continuation: this.nextPageToken,
|
||||
sort_by: this.sortNewest ? 'new' : 'top'
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.isLoading = false
|
||||
}
|
||||
|
@ -202,7 +209,8 @@ export default Vue.extend({
|
|||
this.showToast({
|
||||
message: this.$t('Comments.Getting comment replies, please wait')
|
||||
})
|
||||
ytcm.getCommentReplies(payload.videoId, payload.continuation).then((response) => {
|
||||
|
||||
ytcm.getCommentReplies(payload).then((response) => {
|
||||
this.parseLocalCommentData(response, payload.index)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
|
@ -218,7 +226,14 @@ export default Vue.extend({
|
|||
this.showToast({
|
||||
message: this.$t('Falling back to Invidious API')
|
||||
})
|
||||
this.getCommentDataInvidious()
|
||||
this.getCommentDataInvidious({
|
||||
resource: 'comments',
|
||||
id: this.id,
|
||||
params: {
|
||||
continuation: this.nextPageToken,
|
||||
sort_by: this.sortNewest ? 'new' : 'top'
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.isLoading = false
|
||||
}
|
||||
|
@ -249,7 +264,7 @@ export default Vue.extend({
|
|||
if (this.hideCommentLikes) {
|
||||
comment.likes = null
|
||||
}
|
||||
comment.text = autolinker.link(comment.text)
|
||||
comment.text = autolinker.link(comment.text.replace(/(<([^>]+)>)/ig, ''))
|
||||
|
||||
return comment
|
||||
})
|
||||
|
@ -275,13 +290,13 @@ export default Vue.extend({
|
|||
const commentData = response.comments.map((comment) => {
|
||||
comment.showReplies = false
|
||||
comment.authorLink = comment.authorId
|
||||
comment.authorThumb = comment.authorThumbnails[1].url.replace('https://yt3.ggpht.com', `${this.invidiousInstance}/ggpht/`)
|
||||
comment.authorThumb = comment.authorThumbnails[1].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||
if (this.hideCommentLikes) {
|
||||
comment.likes = null
|
||||
} else {
|
||||
comment.likes = comment.likeCount
|
||||
}
|
||||
comment.text = autolinker.link(comment.content)
|
||||
comment.text = autolinker.link(comment.content.replace(/(<([^>]+)>)/ig, ''))
|
||||
comment.dataType = 'invidious'
|
||||
|
||||
if (typeof (comment.replies) !== 'undefined' && typeof (comment.replies.replyCount) !== 'undefined') {
|
||||
|
@ -337,17 +352,17 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
|
||||
this.$store.dispatch('invidiousAPICall', payload).then((response) => {
|
||||
this.invidiousAPICall(payload).then((response) => {
|
||||
const commentData = response.comments.map((comment) => {
|
||||
comment.showReplies = false
|
||||
comment.authorLink = comment.authorId
|
||||
comment.authorThumb = comment.authorThumbnails[1].url.replace('https://yt3.ggpht.com', `${this.invidiousInstance}/ggpht/`)
|
||||
comment.authorThumb = comment.authorThumbnails[1].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||
if (this.hideCommentLikes) {
|
||||
comment.likes = null
|
||||
} else {
|
||||
comment.likes = comment.likeCount
|
||||
}
|
||||
comment.text = autolinker.link(comment.content)
|
||||
comment.text = autolinker.link(comment.content.replace(/(<([^>]+)>)/ig, ''))
|
||||
comment.time = comment.publishedText
|
||||
comment.dataType = 'invidious'
|
||||
comment.numReplies = 0
|
||||
|
|
|
@ -146,7 +146,7 @@
|
|||
class="showMoreReplies"
|
||||
@click="getCommentReplies(index)"
|
||||
>
|
||||
<span>Show More Replies</span>
|
||||
<span>{{ $t("Comments.Show More Replies") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@ import Vue from 'vue'
|
|||
import FtCard from '../ft-card/ft-card.vue'
|
||||
import FtTimestampCatcher from '../ft-timestamp-catcher/ft-timestamp-catcher.vue'
|
||||
import autolinker from 'autolinker'
|
||||
import $ from 'jquery'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'WatchVideoDescription',
|
||||
|
@ -34,6 +35,10 @@ export default Vue.extend({
|
|||
} else {
|
||||
this.shownDescription = autolinker.link(this.description)
|
||||
}
|
||||
|
||||
if (/^\s*$/.test(this.shownDescription)) {
|
||||
$('.videoDescription')[0].style.display = 'none'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTimestamp: function(timestamp) {
|
||||
|
|
|
@ -6,7 +6,6 @@ import FtListDropdown from '../ft-list-dropdown/ft-list-dropdown.vue'
|
|||
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
|
||||
import FtShareButton from '../ft-share-button/ft-share-button.vue'
|
||||
// import { shell } from 'electron'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'WatchVideoInfo',
|
||||
|
@ -83,6 +82,26 @@ export default Vue.extend({
|
|||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
playlistId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
getPlaylistIndex: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getPlaylistReverse: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getPlaylistShuffle: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getPlaylistLoop: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
theatrePossible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
|
@ -107,12 +126,8 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
},
|
||||
|
||||
usingElectron: function () {
|
||||
return this.$store.getters.getUsingElectron
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
|
||||
profileList: function () {
|
||||
|
@ -225,6 +240,14 @@ export default Vue.extend({
|
|||
} else {
|
||||
return this.$t('Video.Published on')
|
||||
}
|
||||
},
|
||||
|
||||
externalPlayer: function () {
|
||||
return this.$store.getters.getExternalPlayer
|
||||
},
|
||||
|
||||
defaultPlayback: function () {
|
||||
return this.$store.getters.getDefaultPlayback
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
|
@ -244,6 +267,22 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
handleExternalPlayer: function () {
|
||||
this.$emit('pause-player')
|
||||
|
||||
this.openInExternalPlayer({
|
||||
strings: this.$t('Video.External Player'),
|
||||
watchProgress: this.getTimestamp(),
|
||||
playbackRate: this.defaultPlayback,
|
||||
videoId: this.id,
|
||||
playlistId: this.playlistId,
|
||||
playlistIndex: this.getPlaylistIndex(),
|
||||
playlistReverse: this.getPlaylistReverse(),
|
||||
playlistShuffle: this.getPlaylistShuffle(),
|
||||
playlistLoop: this.getPlaylistLoop()
|
||||
})
|
||||
},
|
||||
|
||||
goToChannel: function () {
|
||||
this.$router.push({ path: `/channel/${this.channelId}` })
|
||||
},
|
||||
|
@ -346,11 +385,6 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
handleDownloadLink: function (url) {
|
||||
const shell = require('electron').shell
|
||||
shell.openExternal(url)
|
||||
},
|
||||
|
||||
addToPlaylist: function () {
|
||||
const videoData = {
|
||||
videoId: this.id,
|
||||
|
@ -394,9 +428,11 @@ export default Vue.extend({
|
|||
|
||||
...mapActions([
|
||||
'showToast',
|
||||
'openInExternalPlayer',
|
||||
'updateProfile',
|
||||
'addVideo',
|
||||
'removeVideo'
|
||||
'removeVideo',
|
||||
'openExternalLink'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -86,7 +86,3 @@
|
|||
::v-deep .iconDropdown
|
||||
left: calc(50% - 20px)
|
||||
right: auto
|
||||
|
||||
@media only screen and (max-width: 1350px)
|
||||
.theatreModeButton
|
||||
display: none
|
||||
|
|
|
@ -71,12 +71,12 @@
|
|||
@click="toggleSave"
|
||||
/>
|
||||
<ft-icon-button
|
||||
v-if="theatrePossible"
|
||||
:title="$t('Toggle Theatre Mode')"
|
||||
class="theatreModeButton option"
|
||||
icon="tv"
|
||||
v-if="externalPlayer !== ''"
|
||||
:title="$t('Video.External Player.OpenInTemplate').replace('$', externalPlayer)"
|
||||
icon="external-link-alt"
|
||||
class="option"
|
||||
theme="secondary"
|
||||
@click="$emit('theatre-mode')"
|
||||
@click="handleExternalPlayer"
|
||||
/>
|
||||
<ft-icon-button
|
||||
v-if="!isUpcoming && downloadLinks.length > 0"
|
||||
|
@ -86,7 +86,7 @@
|
|||
icon="download"
|
||||
:dropdown-names="downloadLinkNames"
|
||||
:dropdown-values="downloadLinkValues"
|
||||
@click="handleDownloadLink"
|
||||
@click="openExternalLink"
|
||||
/>
|
||||
<ft-icon-button
|
||||
v-if="!isUpcoming"
|
||||
|
@ -101,6 +101,7 @@
|
|||
<ft-share-button
|
||||
:id="id"
|
||||
:get-timestamp="getTimestamp"
|
||||
:playlist-id="playlistId"
|
||||
class="option"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Vue from 'vue'
|
||||
import { mapActions } from 'vuex'
|
||||
import FtLoader from '../ft-loader/ft-loader.vue'
|
||||
import FtCard from '../ft-card/ft-card.vue'
|
||||
import FtButton from '../ft-button/ft-button.vue'
|
||||
|
@ -6,7 +7,7 @@ import FtListVideo from '../ft-list-video/ft-list-video.vue'
|
|||
|
||||
import $ from 'jquery'
|
||||
import autolinker from 'autolinker'
|
||||
import { LiveChat } from 'youtube-chat'
|
||||
import { LiveChat } from '@freetube/youtube-chat'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'WatchVideoLiveChat',
|
||||
|
@ -156,15 +157,15 @@ export default Vue.extend({
|
|||
if (typeof (text.navigationEndpoint) !== 'undefined') {
|
||||
if (typeof (text.navigationEndpoint.watchEndpoint) !== 'undefined') {
|
||||
const htmlRef = `<a href="https://www.youtube.com/watch?v=${text.navigationEndpoint.watchEndpoint.videoId}">${text.text}</a>`
|
||||
comment.messageHtml = comment.messageHtml + htmlRef
|
||||
comment.messageHtml = comment.messageHtml.replace(/(<([^>]+)>)/ig, '') + htmlRef
|
||||
} else {
|
||||
comment.messageHtml = comment.messageHtml + text.text
|
||||
comment.messageHtml = (comment.messageHtml + text.text).replace(/(<([^>]+)>)/ig, '')
|
||||
}
|
||||
} else if (typeof (text.alt) !== 'undefined') {
|
||||
const htmlImg = `<img src="${text.url}" alt="${text.alt}" height="24" width="24" />`
|
||||
comment.messageHtml = comment.messageHtml + htmlImg
|
||||
comment.messageHtml = comment.messageHtml.replace(/(<([^>]+)>)/ig, '') + htmlImg
|
||||
} else {
|
||||
comment.messageHtml = comment.messageHtml + text.text
|
||||
comment.messageHtml = (comment.messageHtml + text.text).replace(/(<([^>]+)>)/ig, '')
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -183,7 +184,7 @@ export default Vue.extend({
|
|||
console.log(this.comments.length)
|
||||
|
||||
if (typeof (comment.superchat) !== 'undefined') {
|
||||
this.$store.dispatch('getRandomColorClass').then((data) => {
|
||||
this.getRandomColorClass().then((data) => {
|
||||
comment.superchat.colorClass = data
|
||||
|
||||
this.superChatComments.unshift(comment)
|
||||
|
@ -195,7 +196,7 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
if (comment.author.name[0] === 'Ge' || comment.author.name[0] === 'Ne') {
|
||||
this.$store.dispatch('getRandomColorClass').then((data) => {
|
||||
this.getRandomColorClass().then((data) => {
|
||||
comment.superChat = {
|
||||
amount: '$5.00',
|
||||
colorClass: data
|
||||
|
@ -260,6 +261,10 @@ export default Vue.extend({
|
|||
preventDefault: function (event) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'getRandomColorClass'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -190,14 +190,14 @@ export default Vue.extend({
|
|||
}
|
||||
} else {
|
||||
const videoIndex = this.playlistItems.findIndex((item) => {
|
||||
return item.id === this.videoId
|
||||
return (item.id ?? item.videoId) === this.videoId
|
||||
})
|
||||
|
||||
if (videoIndex === this.playlistItems.length - 1) {
|
||||
if (this.loopEnabled) {
|
||||
this.$router.push(
|
||||
{
|
||||
path: `/watch/${this.playlistItems[0].id}`,
|
||||
path: `/watch/${this.playlistItems[0].id ?? this.playlistItems[0].videoId}`,
|
||||
query: playlistInfo
|
||||
}
|
||||
)
|
||||
|
@ -211,7 +211,7 @@ export default Vue.extend({
|
|||
} else {
|
||||
this.$router.push(
|
||||
{
|
||||
path: `/watch/${this.playlistItems[videoIndex + 1].id}`,
|
||||
path: `/watch/${this.playlistItems[videoIndex + 1].id ?? this.playlistItems[videoIndex + 1].videoId}`,
|
||||
query: playlistInfo
|
||||
}
|
||||
)
|
||||
|
@ -253,20 +253,20 @@ export default Vue.extend({
|
|||
}
|
||||
} else {
|
||||
const videoIndex = this.playlistItems.findIndex((item) => {
|
||||
return item.id === this.videoId
|
||||
return (item.id ?? item.videoId) === this.videoId
|
||||
})
|
||||
|
||||
if (videoIndex === 0) {
|
||||
this.$router.push(
|
||||
{
|
||||
path: `/watch/${this.playlistItems[this.randomizedPlaylistItems.length - 1].id}`,
|
||||
path: `/watch/${this.playlistItems[this.randomizedPlaylistItems.length - 1].id ?? this.playlistItems[this.randomizedPlaylistItems.length - 1].videoId}`,
|
||||
query: playlistInfo
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.$router.push(
|
||||
{
|
||||
path: `/watch/${this.playlistItems[videoIndex - 1].id}`,
|
||||
path: `/watch/${this.playlistItems[videoIndex - 1].id ?? this.playlistItems[videoIndex - 1].videoId}`,
|
||||
query: playlistInfo
|
||||
}
|
||||
)
|
||||
|
@ -277,7 +277,7 @@ export default Vue.extend({
|
|||
getPlaylistInformationLocal: function () {
|
||||
this.isLoading = true
|
||||
|
||||
this.$store.dispatch('ytGetPlaylistInfo', this.playlistId).then((result) => {
|
||||
this.ytGetPlaylistInfo(this.playlistId).then((result) => {
|
||||
console.log('done')
|
||||
console.log(result)
|
||||
|
||||
|
@ -338,7 +338,7 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
|
||||
this.$store.dispatch('invidiousGetPlaylistInfo', payload).then((result) => {
|
||||
this.invidiousGetPlaylistInfo(payload).then((result) => {
|
||||
console.log('done')
|
||||
console.log(result)
|
||||
|
||||
|
@ -388,8 +388,8 @@ export default Vue.extend({
|
|||
this.playlistItems.forEach((item) => {
|
||||
const randomInt = Math.floor(Math.random() * remainingItems.length)
|
||||
|
||||
if (remainingItems[randomInt].id !== this.videoId) {
|
||||
items.push(remainingItems[randomInt].id)
|
||||
if ((remainingItems[randomInt].id ?? remainingItems[randomInt].videoId) !== this.videoId) {
|
||||
items.push(remainingItems[randomInt].id ?? remainingItems[randomInt].videoId)
|
||||
}
|
||||
|
||||
remainingItems.splice(randomInt, 1)
|
||||
|
@ -399,7 +399,9 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
...mapActions([
|
||||
'showToast'
|
||||
'showToast',
|
||||
'ytGetPlaylistInfo',
|
||||
'invidiousGetPlaylistInfo'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -83,8 +83,13 @@
|
|||
<ft-list-video
|
||||
:data="item"
|
||||
:playlist-id="playlistId"
|
||||
:playlist-index="reversePlaylist ? playlistItems.length - index - 1 : index"
|
||||
:playlist-reverse="reversePlaylist"
|
||||
:playlist-shuffle="shuffleEnabled"
|
||||
:playlist-loop="loopEnabled"
|
||||
appearance="watchPlaylistItem"
|
||||
force-list-type="list"
|
||||
@pause-player="$emit('pause-player')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import yaml from 'js-yaml'
|
||||
import fs from 'fs'
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
Vue.use(VueI18n)
|
||||
|
||||
// List of locales approved for use
|
||||
const activeLocales = ['en-US', 'en_GB', 'ar', 'bg', 'cs', 'da', 'de-DE', 'el', 'es', 'es-MX', 'et', 'fi', 'fr-FR', 'gl', 'he', 'hu', 'hr', 'id', 'is', 'it', 'ja', 'nb_NO', 'nl', 'nn', 'pl', 'pt', 'pt-BR', 'pt-PT', 'ru', 'sk', 'sl', 'sr', 'sv', 'tr', 'uk', 'vi', 'zh-CN', 'zh-TW']
|
||||
const messages = {}
|
||||
/* eslint-disable-next-line */
|
||||
const fileLocation = isDev ? 'static/locales/' : `${__dirname}/static/locales/`
|
||||
|
||||
// Take active locales and load respective YAML file
|
||||
activeLocales.forEach((locale) => {
|
||||
try {
|
||||
// File location when running in dev
|
||||
const doc = yaml.load(fs.readFileSync(`${fileLocation}${locale}.yaml`))
|
||||
messages[locale] = doc
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
|
||||
export default new VueI18n({
|
||||
locale: 'en-US',
|
||||
fallbackLocale: { default: 'en-US' },
|
||||
messages: messages
|
||||
})
|
|
@ -3,6 +3,7 @@ import Vue from 'vue'
|
|||
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'
|
||||
|
@ -11,9 +12,6 @@ import { faBitcoin } from '@fortawesome/free-brands-svg-icons/faBitcoin'
|
|||
import { faMonero } from '@fortawesome/free-brands-svg-icons/faMonero'
|
||||
import { faMastodon } from '@fortawesome/free-brands-svg-icons/faMastodon'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import yaml from 'js-yaml'
|
||||
import fs from 'fs'
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
|
@ -24,32 +22,6 @@ Vue.config.productionTip = isDev
|
|||
library.add(fas, faGithub, faBitcoin, faMonero, faMastodon)
|
||||
|
||||
Vue.component('FontAwesomeIcon', FontAwesomeIcon)
|
||||
Vue.use(VueI18n)
|
||||
|
||||
// List of locales approved for use
|
||||
const activeLocales = ['en-US', 'en_GB', 'ar', 'bg', 'cs', 'da', 'de-DE', 'el', 'es', 'es-MX', 'fi', 'fr-FR', 'gl', 'he', 'hu', 'hr', 'id', 'is', 'it', 'ja', 'nb_NO', 'nl', 'nn', 'pl', 'pt', 'pt-BR', 'pt-PT', 'ru', 'sk', 'sl', 'sv', 'tr', 'uk', 'vi', 'zh-CN', 'zh-TW']
|
||||
const messages = {}
|
||||
/* eslint-disable-next-line */
|
||||
const fileLocation = isDev ? 'static/locales/' : `${__dirname}/static/locales/`
|
||||
|
||||
// Take active locales and load respective YAML file
|
||||
activeLocales.forEach((locale) => {
|
||||
try {
|
||||
// File location when running in dev
|
||||
const doc = yaml.load(fs.readFileSync(`${fileLocation}${locale}.yaml`))
|
||||
messages[locale] = doc
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
|
||||
const i18n = new VueI18n({
|
||||
locale: 'en-US', // set locale
|
||||
fallbackLocale: {
|
||||
default: 'en-US' // for the case systems locale has no corresponding .yaml file en-US gets set
|
||||
},
|
||||
messages // set locale messages
|
||||
})
|
||||
|
||||
/* eslint-disable-next-line */
|
||||
new Vue({
|
||||
|
|
|
@ -21,7 +21,7 @@ const router = new Router({
|
|||
{
|
||||
path: '/',
|
||||
meta: {
|
||||
title: 'Subscriptions',
|
||||
title: 'Subscriptions.Subscriptions',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: Subscriptions
|
||||
|
@ -29,7 +29,7 @@ const router = new Router({
|
|||
{
|
||||
path: '/subscriptions',
|
||||
meta: {
|
||||
title: 'Subscriptions',
|
||||
title: 'Subscriptions.Subscriptions',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: Subscriptions
|
||||
|
@ -37,7 +37,7 @@ const router = new Router({
|
|||
{
|
||||
path: '/settings/profile',
|
||||
meta: {
|
||||
title: 'Profile Settings',
|
||||
title: 'Profile.Profile Settings',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: ProfileSettings
|
||||
|
@ -46,7 +46,7 @@ const router = new Router({
|
|||
path: '/settings/profile/new',
|
||||
name: 'newProfile',
|
||||
meta: {
|
||||
title: 'New Profile',
|
||||
title: 'Profile.Create New Profile',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: ProfileEdit
|
||||
|
@ -55,7 +55,7 @@ const router = new Router({
|
|||
path: '/settings/profile/edit/:id',
|
||||
name: 'editProfile',
|
||||
meta: {
|
||||
title: 'Edit Profile',
|
||||
title: 'Profile.Edit Profile',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: ProfileEdit
|
||||
|
@ -63,7 +63,7 @@ const router = new Router({
|
|||
{
|
||||
path: '/trending',
|
||||
meta: {
|
||||
title: 'Trending',
|
||||
title: 'Trending.Trending',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: Trending
|
||||
|
@ -79,7 +79,7 @@ const router = new Router({
|
|||
{
|
||||
path: '/userplaylists',
|
||||
meta: {
|
||||
title: 'User Playlists',
|
||||
title: 'User Playlists.Your Playlists',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: UserPlaylists
|
||||
|
@ -88,7 +88,7 @@ const router = new Router({
|
|||
path: '/history',
|
||||
name: 'history',
|
||||
meta: {
|
||||
title: 'History',
|
||||
title: 'History.History',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: History
|
||||
|
@ -96,7 +96,7 @@ const router = new Router({
|
|||
{
|
||||
path: '/settings',
|
||||
meta: {
|
||||
title: 'Settings',
|
||||
title: 'Settings.Settings',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: Settings
|
||||
|
@ -104,7 +104,7 @@ const router = new Router({
|
|||
{
|
||||
path: '/about',
|
||||
meta: {
|
||||
title: 'About',
|
||||
title: 'About.About',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: About
|
||||
|
@ -112,7 +112,7 @@ const router = new Router({
|
|||
{
|
||||
path: '/search/:query',
|
||||
meta: {
|
||||
title: 'Search',
|
||||
title: 'Search Filters.Search Results',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: Search
|
||||
|
@ -120,7 +120,7 @@ const router = new Router({
|
|||
{
|
||||
path: '/playlist/:id',
|
||||
meta: {
|
||||
title: 'Playlist',
|
||||
title: 'Playlist.Playlist',
|
||||
icon: 'fa-home'
|
||||
},
|
||||
component: Playlist
|
||||
|
@ -155,18 +155,4 @@ const router = new Router({
|
|||
}
|
||||
})
|
||||
|
||||
// dynamically set application title to current view
|
||||
router.afterEach(to => {
|
||||
let title =
|
||||
to.path === '/home'
|
||||
? process.env.PRODUCT_NAME
|
||||
: `${to.meta.title} - ${process.env.PRODUCT_NAME}`
|
||||
|
||||
if (!title) {
|
||||
title = 'Home'
|
||||
}
|
||||
|
||||
document.title = title
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
|
@ -89,6 +89,13 @@ $thumbnail-overlay-opacity: 0.85
|
|||
@include is-watch-playlist-item
|
||||
font-size: 12px
|
||||
|
||||
.externalPlayerIcon
|
||||
position: absolute
|
||||
bottom: 4px
|
||||
left: 4px
|
||||
font-size: 17px
|
||||
opacity: $thumbnail-overlay-opacity
|
||||
|
||||
.favoritesIcon
|
||||
position: absolute
|
||||
top: 3px
|
||||
|
@ -144,6 +151,9 @@ $thumbnail-overlay-opacity: 0.85
|
|||
.optionsButton
|
||||
float: right // ohhhh man, float was finally the right choice for something
|
||||
|
||||
.externalPlayerButton
|
||||
float: right
|
||||
|
||||
.title
|
||||
font-size: 20px
|
||||
@include low-contrast-when-watched(var(--primary-text-color))
|
||||
|
|
|
@ -24,6 +24,9 @@
|
|||
@media only screen and (max-width: 680px)
|
||||
width: 90%
|
||||
|
||||
.center
|
||||
text-align: center
|
||||
|
||||
@media only screen and (max-width: 460px)
|
||||
.generalSettingsFlexBox, .playerSettingsFlexBox
|
||||
.generalSettingsFlexBox, .playerSettingsFlexBox, .externalPlayerSettingsFlexBox
|
||||
justify-content: flex-start
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import Datastore from 'nedb-promises'
|
||||
|
||||
// Initialize all datastores and export their references
|
||||
// Current dbs:
|
||||
// `settings.db`
|
||||
// `profiles.db`
|
||||
// `playlists.db`
|
||||
// `history.db`
|
||||
|
||||
let buildFileName = null
|
||||
|
||||
// Check if using Electron
|
||||
const usingElectron = window?.process?.type === 'renderer'
|
||||
if (usingElectron) {
|
||||
const { ipcRenderer } = require('electron')
|
||||
const userDataPath = ipcRenderer.sendSync('getUserDataPathSync')
|
||||
buildFileName = (dbName) => userDataPath + '/' + dbName + '.db'
|
||||
} else {
|
||||
buildFileName = (dbName) => dbName + '.db'
|
||||
}
|
||||
|
||||
const settingsDb = Datastore.create({
|
||||
filename: buildFileName('settings'),
|
||||
autoload: true
|
||||
})
|
||||
|
||||
const playlistsDb = Datastore.create({
|
||||
filename: buildFileName('playlists'),
|
||||
autoload: true
|
||||
})
|
||||
|
||||
const profilesDb = Datastore.create({
|
||||
filename: buildFileName('profiles'),
|
||||
autoload: true
|
||||
})
|
||||
|
||||
const historyDb = Datastore.create({
|
||||
filename: buildFileName('history'),
|
||||
autoload: true
|
||||
})
|
||||
|
||||
export {
|
||||
settingsDb,
|
||||
profilesDb,
|
||||
playlistsDb,
|
||||
historyDb
|
||||
}
|
|
@ -1,27 +1,4 @@
|
|||
import Datastore from 'nedb'
|
||||
|
||||
let dbLocation
|
||||
|
||||
if (window && window.process && window.process.type === 'renderer') {
|
||||
// Electron is being used
|
||||
/* let dbLocation = localStorage.getItem('dbLocation')
|
||||
|
||||
if (dbLocation === null) {
|
||||
const electron = require('electron')
|
||||
dbLocation = electron.remote.app.getPath('userData')
|
||||
} */
|
||||
|
||||
const remote = require('@electron/remote')
|
||||
dbLocation = remote.app.getPath('userData')
|
||||
dbLocation = dbLocation + '/history.db'
|
||||
} else {
|
||||
dbLocation = 'history.db'
|
||||
}
|
||||
|
||||
const historyDb = new Datastore({
|
||||
filename: dbLocation,
|
||||
autoload: true
|
||||
})
|
||||
import { historyDb } from '../datastores'
|
||||
|
||||
const state = {
|
||||
historyCache: []
|
||||
|
@ -34,58 +11,100 @@ const getters = {
|
|||
}
|
||||
|
||||
const actions = {
|
||||
grabHistory ({ commit }) {
|
||||
historyDb.find({}).sort({
|
||||
timeWatched: -1
|
||||
}).exec((err, results) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
return
|
||||
}
|
||||
commit('setHistoryCache', results)
|
||||
})
|
||||
async grabHistory({ commit }) {
|
||||
const results = await historyDb.find({}).sort({ timeWatched: -1 })
|
||||
commit('setHistoryCache', results)
|
||||
},
|
||||
|
||||
updateHistory ({ dispatch }, videoData) {
|
||||
historyDb.update({ videoId: videoData.videoId }, videoData, { upsert: true }, (err, numReplaced) => {
|
||||
if (!err) {
|
||||
dispatch('grabHistory')
|
||||
}
|
||||
async updateHistory({ commit, dispatch, state }, entry) {
|
||||
await historyDb.update(
|
||||
{ videoId: entry.videoId },
|
||||
entry,
|
||||
{ upsert: true }
|
||||
)
|
||||
|
||||
const entryIndex = state.historyCache.findIndex((currentEntry) => {
|
||||
return entry.videoId === currentEntry.videoId
|
||||
})
|
||||
|
||||
entryIndex === -1
|
||||
? commit('insertNewEntryToHistoryCache', entry)
|
||||
: commit('hoistEntryToTopOfHistoryCache', {
|
||||
currentIndex: entryIndex,
|
||||
updatedEntry: entry
|
||||
})
|
||||
|
||||
dispatch('propagateHistory')
|
||||
},
|
||||
|
||||
removeFromHistory ({ dispatch }, videoId) {
|
||||
historyDb.remove({ videoId: videoId }, (err, numReplaced) => {
|
||||
if (!err) {
|
||||
dispatch('grabHistory')
|
||||
}
|
||||
async removeFromHistory({ commit, dispatch }, videoId) {
|
||||
await historyDb.remove({ videoId: videoId })
|
||||
|
||||
const updatedCache = state.historyCache.filter((entry) => {
|
||||
return entry.videoId !== videoId
|
||||
})
|
||||
|
||||
commit('setHistoryCache', updatedCache)
|
||||
|
||||
dispatch('propagateHistory')
|
||||
},
|
||||
|
||||
removeAllHistory ({ dispatch }) {
|
||||
historyDb.remove({}, { multi: true }, (err, numReplaced) => {
|
||||
if (!err) {
|
||||
dispatch('grabHistory')
|
||||
}
|
||||
})
|
||||
async removeAllHistory({ commit, dispatch }) {
|
||||
await historyDb.remove({}, { multi: true })
|
||||
commit('setHistoryCache', [])
|
||||
dispatch('propagateHistory')
|
||||
},
|
||||
|
||||
updateWatchProgress ({ dispatch }, videoData) {
|
||||
historyDb.update({ videoId: videoData.videoId }, { $set: { watchProgress: videoData.watchProgress } }, { upsert: true }, (err, numReplaced) => {
|
||||
if (!err) {
|
||||
dispatch('grabHistory')
|
||||
}
|
||||
async updateWatchProgress({ commit, dispatch }, entry) {
|
||||
await historyDb.update(
|
||||
{ videoId: entry.videoId },
|
||||
{ $set: { watchProgress: entry.watchProgress } },
|
||||
{ upsert: true }
|
||||
)
|
||||
|
||||
const entryIndex = state.historyCache.findIndex((currentEntry) => {
|
||||
return entry.videoId === currentEntry.videoId
|
||||
})
|
||||
|
||||
commit('updateEntryWatchProgressInHistoryCache', {
|
||||
index: entryIndex,
|
||||
value: entry.watchProgress
|
||||
})
|
||||
|
||||
dispatch('propagateHistory')
|
||||
},
|
||||
|
||||
compactHistory (_) {
|
||||
propagateHistory({ getters: { getUsingElectron: usingElectron } }) {
|
||||
if (usingElectron) {
|
||||
const { ipcRenderer } = require('electron')
|
||||
ipcRenderer.send('syncWindows', {
|
||||
type: 'history',
|
||||
data: state.historyCache
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
compactHistory(_) {
|
||||
historyDb.persistence.compactDatafile()
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
setHistoryCache (state, historyCache) {
|
||||
setHistoryCache(state, historyCache) {
|
||||
state.historyCache = historyCache
|
||||
},
|
||||
|
||||
insertNewEntryToHistoryCache(state, entry) {
|
||||
state.historyCache.unshift(entry)
|
||||
},
|
||||
|
||||
hoistEntryToTopOfHistoryCache(state, { currentIndex, updatedEntry }) {
|
||||
state.historyCache.splice(currentIndex, 1)
|
||||
state.historyCache.unshift(updatedEntry)
|
||||
},
|
||||
|
||||
updateEntryWatchProgressInHistoryCache(state, { index, value }) {
|
||||
state.historyCache[index].watchProgress = value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,58 @@
|
|||
import $ from 'jquery'
|
||||
|
||||
const state = {
|
||||
main: 0,
|
||||
currentInvidiousInstance: '',
|
||||
invidiousInstancesList: null,
|
||||
isGetChannelInfoRunning: false
|
||||
}
|
||||
|
||||
const getters = {
|
||||
getIsGetChannelInfoRunning ({ state }) {
|
||||
getIsGetChannelInfoRunning(state) {
|
||||
return state.isGetChannelInfoRunning
|
||||
},
|
||||
|
||||
getCurrentInvidiousInstance(state) {
|
||||
return state.currentInvidiousInstance
|
||||
},
|
||||
|
||||
getInvidiousInstancesList(state) {
|
||||
return state.invidiousInstancesList
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
invidiousAPICall ({ rootState }, payload) {
|
||||
async fetchInvidiousInstances({ commit }) {
|
||||
const requestUrl = 'https://api.invidious.io/instances.json'
|
||||
|
||||
let response
|
||||
try {
|
||||
response = await $.getJSON(requestUrl)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
const instances = response.filter((instance) => {
|
||||
if (instance[0].includes('.onion') || instance[0].includes('.i2p')) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}).map((instance) => {
|
||||
return instance[1].uri.replace(/\/$/, '')
|
||||
})
|
||||
|
||||
commit('setInvidiousInstancesList', instances)
|
||||
},
|
||||
|
||||
setRandomCurrentInvidiousInstance({ commit, state }) {
|
||||
const instanceList = state.invidiousInstancesList
|
||||
const randomIndex = Math.floor(Math.random() * instanceList.length)
|
||||
commit('setCurrentInvidiousInstance', instanceList[randomIndex])
|
||||
},
|
||||
|
||||
invidiousAPICall({ state }, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestUrl = rootState.settings.invidiousInstance + '/api/v1/' + payload.resource + '/' + payload.id + '?' + $.param(payload.params)
|
||||
const requestUrl = state.currentInvidiousInstance + '/api/v1/' + payload.resource + '/' + payload.id + '?' + $.param(payload.params)
|
||||
|
||||
$.getJSON(requestUrl, (response) => {
|
||||
resolve(response)
|
||||
|
@ -28,7 +66,7 @@ const actions = {
|
|||
})
|
||||
},
|
||||
|
||||
invidiousGetChannelInfo ({ commit, dispatch }, channelId) {
|
||||
invidiousGetChannelInfo({ commit, dispatch }, channelId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
commit('toggleIsGetChannelInfoRunning')
|
||||
|
||||
|
@ -49,7 +87,7 @@ const actions = {
|
|||
})
|
||||
},
|
||||
|
||||
invidiousGetPlaylistInfo ({ commit, dispatch }, payload) {
|
||||
invidiousGetPlaylistInfo({ commit, dispatch }, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dispatch('invidiousAPICall', payload).then((response) => {
|
||||
resolve(response)
|
||||
|
@ -62,7 +100,7 @@ const actions = {
|
|||
})
|
||||
},
|
||||
|
||||
invidiousGetVideoInformation ({ dispatch }, videoId) {
|
||||
invidiousGetVideoInformation({ dispatch }, videoId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = {
|
||||
resource: 'videos',
|
||||
|
@ -82,8 +120,16 @@ const actions = {
|
|||
}
|
||||
|
||||
const mutations = {
|
||||
toggleIsGetChannelInfoRunning (state) {
|
||||
toggleIsGetChannelInfoRunning(state) {
|
||||
state.isGetChannelInfoRunning = !state.isGetChannelInfoRunning
|
||||
},
|
||||
|
||||
setCurrentInvidiousInstance(state, value) {
|
||||
state.currentInvidiousInstance = value
|
||||
},
|
||||
|
||||
setInvidiousInstancesList(state, value) {
|
||||
state.invidiousInstancesList = value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,212 +0,0 @@
|
|||
import Datastore from 'nedb'
|
||||
|
||||
let dbLocation
|
||||
|
||||
if (window && window.process && window.process.type === 'renderer') {
|
||||
// Electron is being used
|
||||
// let dbLocation = localStorage.getItem('dbLocation')
|
||||
//
|
||||
// if (dbLocation === null) {
|
||||
// const electron = require('electron')
|
||||
// dbLocation = electron.remote.app.getPath('userData')
|
||||
// }
|
||||
//
|
||||
// dbLocation += '/playlists.db'
|
||||
|
||||
const remote = require('@electron/remote')
|
||||
dbLocation = remote.app.getPath('userData')
|
||||
|
||||
dbLocation = dbLocation + '/playlists.db'
|
||||
} else {
|
||||
dbLocation = 'playlists.db'
|
||||
}
|
||||
|
||||
const playlistDb = new Datastore({
|
||||
filename: dbLocation,
|
||||
autoload: true
|
||||
})
|
||||
|
||||
const state = {
|
||||
playlists: [
|
||||
{
|
||||
playlistName: 'Favorites',
|
||||
protected: true,
|
||||
videos: []
|
||||
},
|
||||
{
|
||||
playlistName: 'WatchLater',
|
||||
protected: true,
|
||||
removeOnWatched: true,
|
||||
videos: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const getters = {
|
||||
getAllPlaylists: () => state.playlists,
|
||||
getFavorites: () => state.playlists[0],
|
||||
getPlaylist: (playlistId) => state.playlists.find(playlist => playlist._id === playlistId),
|
||||
getWatchLater: () => state.playlists[1]
|
||||
}
|
||||
|
||||
const actions = {
|
||||
addPlaylist ({ commit }, payload) {
|
||||
playlistDb.insert(payload, (err, payload) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
} else {
|
||||
commit('addPlaylist', payload)
|
||||
}
|
||||
})
|
||||
},
|
||||
addPlaylists ({ commit }, payload) {
|
||||
playlistDb.insert(payload, (err, payload) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
} else {
|
||||
commit('addPlaylists', payload)
|
||||
}
|
||||
})
|
||||
},
|
||||
addVideo ({ commit }, payload) {
|
||||
playlistDb.update({ playlistName: payload.playlistName }, { $push: { videos: payload.videoData } }, { upsert: true }, err => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
} else {
|
||||
commit('addVideo', payload)
|
||||
}
|
||||
})
|
||||
},
|
||||
addVideos ({ commit }, payload) {
|
||||
playlistDb.update({ _id: payload.playlistId }, { $push: { videos: { $each: payload.videosIds } } }, { upsert: true }, err => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
} else {
|
||||
commit('addVideos', payload)
|
||||
}
|
||||
})
|
||||
},
|
||||
grabAllPlaylists({ commit, dispatch }) {
|
||||
playlistDb.find({}, (err, payload) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
} else {
|
||||
if (payload.length === 0) {
|
||||
commit('setAllPlaylists', state.playlists)
|
||||
dispatch('addPlaylists', payload)
|
||||
} else {
|
||||
commit('setAllPlaylists', payload)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
removeAllPlaylists ({ commit }) {
|
||||
playlistDb.remove({ protected: { $ne: true } }, err => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
} else {
|
||||
commit('removeAllPlaylists')
|
||||
}
|
||||
})
|
||||
},
|
||||
removeAllVideos ({ commit }, playlistName) {
|
||||
playlistDb.update({ playlistName: playlistName }, { $set: { videos: [] } }, { upsert: true }, err => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
} else {
|
||||
commit('removeAllVideos', playlistName)
|
||||
}
|
||||
})
|
||||
},
|
||||
removePlaylist ({ commit }, playlistId) {
|
||||
playlistDb.remove({ _id: playlistId, protected: { $ne: true } }, (err, playlistId) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
} else {
|
||||
commit('removePlaylist', playlistId)
|
||||
}
|
||||
})
|
||||
},
|
||||
removePlaylists ({ commit }, playlistIds) {
|
||||
playlistDb.remove({ _id: { $in: playlistIds }, protected: { $ne: true } }, (err, playlistIds) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
} else {
|
||||
commit('removePlaylists', playlistIds)
|
||||
}
|
||||
})
|
||||
},
|
||||
removeVideo ({ commit }, payload) {
|
||||
playlistDb.update({ playlistName: payload.playlistName }, { $pull: { videos: { videoId: payload.videoId } } }, { upsert: true }, (err, numRemoved) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
} else {
|
||||
commit('removeVideo', payload)
|
||||
}
|
||||
})
|
||||
},
|
||||
removeVideos ({ commit }, payload) {
|
||||
playlistDb.update({ _id: payload.playlistName }, { $pull: { videos: { $in: payload.videoId } } }, { upsert: true }, err => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
} else {
|
||||
commit('removeVideos', payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
addPlaylist (state, payload) {
|
||||
state.playlists.push(payload)
|
||||
},
|
||||
addPlaylists (state, payload) {
|
||||
state.playlists = state.playlists.concat(payload)
|
||||
},
|
||||
addVideo (state, payload) {
|
||||
const playlist = state.playlists.find(playlist => playlist.playlistName === payload.playlistName)
|
||||
if (playlist) {
|
||||
playlist.videos.push(payload.videoData)
|
||||
}
|
||||
},
|
||||
addVideos (state, payload) {
|
||||
const playlist = state.playlists.find(playlist => playlist._id === payload.playlistId)
|
||||
if (playlist) {
|
||||
playlist.videos = playlist.videos.concat(payload.playlistIds)
|
||||
}
|
||||
},
|
||||
removeAllPlaylists (state) {
|
||||
state.playlists = state.playlists.filter(playlist => playlist.protected !== true)
|
||||
},
|
||||
removeAllVideos (state, playlistName) {
|
||||
const playlist = state.playlists.find(playlist => playlist.playlistName === playlistName)
|
||||
if (playlist) {
|
||||
playlist.videos = []
|
||||
}
|
||||
},
|
||||
removeVideo (state, payload) {
|
||||
const playlist = state.playlists.findIndex(playlist => playlist.playlistName === payload.playlistName)
|
||||
if (playlist !== -1) {
|
||||
state.playlists[playlist].videos = state.playlists[playlist].videos.filter(video => video.videoId !== payload.videoId)
|
||||
}
|
||||
},
|
||||
removeVideos (state, payload) {
|
||||
const playlist = state.playlists.findIndex(playlist => playlist._id === payload.playlistId)
|
||||
if (playlist !== -1) {
|
||||
playlist.videos = playlist.videos.filter(video => payload.videoId.indexOf(video) === -1)
|
||||
}
|
||||
},
|
||||
removePlaylist (state, playlistId) {
|
||||
state.playlists = state.playlists.filter(playlist => playlist._id !== playlistId || playlist.protected)
|
||||
},
|
||||
setAllPlaylists (state, payload) {
|
||||
state.playlists = payload
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
import { playlistsDb } from '../datastores'
|
||||
|
||||
const state = {
|
||||
playlists: [
|
||||
{
|
||||
playlistName: 'Favorites',
|
||||
protected: true,
|
||||
videos: []
|
||||
},
|
||||
{
|
||||
playlistName: 'WatchLater',
|
||||
protected: true,
|
||||
removeOnWatched: true,
|
||||
videos: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const getters = {
|
||||
getAllPlaylists: () => state.playlists,
|
||||
getFavorites: () => state.playlists[0],
|
||||
getPlaylist: (playlistId) => state.playlists.find(playlist => playlist._id === playlistId),
|
||||
getWatchLater: () => state.playlists[1]
|
||||
}
|
||||
|
||||
const actions = {
|
||||
async addPlaylist({ commit }, payload) {
|
||||
await playlistsDb.insert(payload)
|
||||
commit('addPlaylist', payload)
|
||||
},
|
||||
|
||||
async addPlaylists({ commit }, payload) {
|
||||
await playlistsDb.insert(payload)
|
||||
commit('addPlaylists', payload)
|
||||
},
|
||||
|
||||
async addVideo({ commit }, payload) {
|
||||
await playlistsDb.update(
|
||||
{ playlistName: payload.playlistName },
|
||||
{ $push: { videos: payload.videoData } },
|
||||
{ upsert: true }
|
||||
)
|
||||
commit('addVideo', payload)
|
||||
},
|
||||
|
||||
async addVideos({ commit }, payload) {
|
||||
await playlistsDb.update(
|
||||
{ _id: payload.playlistId },
|
||||
{ $push: { videos: { $each: payload.videosIds } } },
|
||||
{ upsert: true }
|
||||
)
|
||||
commit('addVideos', payload)
|
||||
},
|
||||
|
||||
async grabAllPlaylists({ commit, dispatch }) {
|
||||
const payload = await playlistsDb.find({})
|
||||
if (payload.length === 0) {
|
||||
commit('setAllPlaylists', state.playlists)
|
||||
dispatch('addPlaylists', payload)
|
||||
} else {
|
||||
commit('setAllPlaylists', payload)
|
||||
}
|
||||
},
|
||||
|
||||
async removeAllPlaylists({ commit }) {
|
||||
await playlistsDb.remove({ protected: { $ne: true } })
|
||||
commit('removeAllPlaylists')
|
||||
},
|
||||
|
||||
async removeAllVideos({ commit }, playlistName) {
|
||||
await playlistsDb.update(
|
||||
{ playlistName: playlistName },
|
||||
{ $set: { videos: [] } },
|
||||
{ upsert: true }
|
||||
)
|
||||
commit('removeAllVideos', playlistName)
|
||||
},
|
||||
|
||||
async removePlaylist({ commit }, playlistId) {
|
||||
await playlistsDb.remove({
|
||||
_id: playlistId,
|
||||
protected: { $ne: true }
|
||||
})
|
||||
commit('removePlaylist', playlistId)
|
||||
},
|
||||
|
||||
async removePlaylists({ commit }, playlistIds) {
|
||||
await playlistsDb.remove({
|
||||
_id: { $in: playlistIds },
|
||||
protected: { $ne: true }
|
||||
})
|
||||
commit('removePlaylists', playlistIds)
|
||||
},
|
||||
|
||||
async removeVideo({ commit }, payload) {
|
||||
await playlistsDb.update(
|
||||
{ playlistName: payload.playlistName },
|
||||
{ $pull: { videos: { videoId: payload.videoId } } },
|
||||
{ upsert: true }
|
||||
)
|
||||
commit('removeVideo', payload)
|
||||
},
|
||||
|
||||
async removeVideos({ commit }, payload) {
|
||||
await playlistsDb.update(
|
||||
{ _id: payload.playlistName },
|
||||
{ $pull: { videos: { $in: payload.videoId } } },
|
||||
{ upsert: true }
|
||||
)
|
||||
commit('removeVideos', payload)
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
addPlaylist(state, payload) {
|
||||
state.playlists.push(payload)
|
||||
},
|
||||
|
||||
addPlaylists(state, payload) {
|
||||
state.playlists = state.playlists.concat(payload)
|
||||
},
|
||||
|
||||
addVideo(state, payload) {
|
||||
const playlist = state.playlists.find(playlist => playlist.playlistName === payload.playlistName)
|
||||
if (playlist) {
|
||||
playlist.videos.push(payload.videoData)
|
||||
}
|
||||
},
|
||||
|
||||
addVideos(state, payload) {
|
||||
const playlist = state.playlists.find(playlist => playlist._id === payload.playlistId)
|
||||
if (playlist) {
|
||||
playlist.videos = playlist.videos.concat(payload.playlistIds)
|
||||
}
|
||||
},
|
||||
|
||||
removeAllPlaylists(state) {
|
||||
state.playlists = state.playlists.filter(playlist => playlist.protected !== true)
|
||||
},
|
||||
|
||||
removeAllVideos(state, playlistName) {
|
||||
const playlist = state.playlists.find(playlist => playlist.playlistName === playlistName)
|
||||
if (playlist) {
|
||||
playlist.videos = []
|
||||
}
|
||||
},
|
||||
|
||||
removeVideo(state, payload) {
|
||||
const playlist = state.playlists.findIndex(playlist => playlist.playlistName === payload.playlistName)
|
||||
if (playlist !== -1) {
|
||||
state.playlists[playlist].videos = state.playlists[playlist].videos.filter(video => video.videoId !== payload.videoId)
|
||||
}
|
||||
},
|
||||
|
||||
removeVideos(state, payload) {
|
||||
const playlist = state.playlists.findIndex(playlist => playlist._id === payload.playlistId)
|
||||
if (playlist !== -1) {
|
||||
playlist.videos = playlist.videos.filter(video => payload.videoId.indexOf(video) === -1)
|
||||
}
|
||||
},
|
||||
|
||||
removePlaylist(state, playlistId) {
|
||||
state.playlists = state.playlists.filter(playlist => playlist._id !== playlistId || playlist.protected)
|
||||
},
|
||||
|
||||
setAllPlaylists(state, payload) {
|
||||
state.playlists = payload
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
import Datastore from 'nedb'
|
||||
|
||||
let dbLocation
|
||||
|
||||
if (window && window.process && window.process.type === 'renderer') {
|
||||
// Electron is being used
|
||||
/* let dbLocation = localStorage.getItem('dbLocation')
|
||||
|
||||
if (dbLocation === null) {
|
||||
const electron = require('electron')
|
||||
dbLocation = electron.remote.app.getPath('userData')
|
||||
} */
|
||||
|
||||
const remote = require('@electron/remote')
|
||||
dbLocation = remote.app.getPath('userData')
|
||||
|
||||
dbLocation = dbLocation + '/profiles.db'
|
||||
} else {
|
||||
dbLocation = 'profiles.db'
|
||||
}
|
||||
|
||||
const profileDb = new Datastore({
|
||||
filename: dbLocation,
|
||||
autoload: true
|
||||
})
|
||||
|
||||
const state = {
|
||||
profileList: [{
|
||||
_id: 'allChannels',
|
||||
name: 'All Channels',
|
||||
bgColor: '#000000',
|
||||
textColor: '#FFFFFF',
|
||||
subscriptions: []
|
||||
}],
|
||||
activeProfile: 0
|
||||
}
|
||||
|
||||
const getters = {
|
||||
getProfileList: () => {
|
||||
return state.profileList
|
||||
},
|
||||
|
||||
getActiveProfile: () => {
|
||||
return state.activeProfile
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
grabAllProfiles ({ rootState, dispatch, commit }, defaultName = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
profileDb.find({}, (err, results) => {
|
||||
if (!err) {
|
||||
if (results.length === 0) {
|
||||
dispatch('createDefaultProfile', defaultName)
|
||||
} else {
|
||||
// We want the primary profile to always be first
|
||||
// So sort with that then sort alphabetically by profile name
|
||||
const profiles = results.sort((a, b) => {
|
||||
if (a._id === 'allChannels') {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (b._id === 'allChannels') {
|
||||
return 1
|
||||
}
|
||||
|
||||
return b.name - a.name
|
||||
})
|
||||
|
||||
if (state.profileList.length < profiles.length) {
|
||||
const profileIndex = profiles.findIndex((profile) => {
|
||||
return profile._id === rootState.settings.defaultProfile
|
||||
})
|
||||
|
||||
if (profileIndex !== -1) {
|
||||
commit('setActiveProfile', profileIndex)
|
||||
}
|
||||
}
|
||||
|
||||
commit('setProfileList', profiles)
|
||||
}
|
||||
|
||||
resolve()
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
grabProfileInfo (_, profileId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(profileId)
|
||||
profileDb.findOne({ _id: profileId }, (err, results) => {
|
||||
if (!err) {
|
||||
resolve(results)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async createDefaultProfile ({ dispatch }, defaultName) {
|
||||
const randomColor = await dispatch('getRandomColor')
|
||||
const textColor = await dispatch('calculateColorLuminance', randomColor)
|
||||
const defaultProfile = {
|
||||
_id: 'allChannels',
|
||||
name: defaultName,
|
||||
bgColor: randomColor,
|
||||
textColor: textColor,
|
||||
subscriptions: []
|
||||
}
|
||||
|
||||
profileDb.update({ _id: 'allChannels' }, defaultProfile, { upsert: true }, (err, numReplaced) => {
|
||||
if (!err) {
|
||||
dispatch('grabAllProfiles')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
updateProfile ({ dispatch }, profile) {
|
||||
profileDb.update({ _id: profile._id }, profile, { upsert: true }, (err, numReplaced) => {
|
||||
if (!err) {
|
||||
dispatch('grabAllProfiles')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
insertProfile ({ dispatch }, profile) {
|
||||
profileDb.insert(profile, (err, newDocs) => {
|
||||
if (!err) {
|
||||
dispatch('grabAllProfiles')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
removeProfile ({ dispatch }, profileId) {
|
||||
profileDb.remove({ _id: profileId }, (err, numReplaced) => {
|
||||
if (!err) {
|
||||
dispatch('grabAllProfiles')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
compactProfiles (_) {
|
||||
profileDb.persistence.compactDatafile()
|
||||
},
|
||||
|
||||
updateActiveProfile ({ commit }, index) {
|
||||
commit('setActiveProfile', index)
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
setProfileList (state, profileList) {
|
||||
state.profileList = profileList
|
||||
},
|
||||
setActiveProfile (state, activeProfile) {
|
||||
state.activeProfile = activeProfile
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import { profilesDb } from '../datastores'
|
||||
|
||||
const state = {
|
||||
profileList: [{
|
||||
_id: 'allChannels',
|
||||
name: 'All Channels',
|
||||
bgColor: '#000000',
|
||||
textColor: '#FFFFFF',
|
||||
subscriptions: []
|
||||
}],
|
||||
activeProfile: 0
|
||||
}
|
||||
|
||||
const getters = {
|
||||
getProfileList: () => {
|
||||
return state.profileList
|
||||
},
|
||||
|
||||
getActiveProfile: () => {
|
||||
return state.activeProfile
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
async grabAllProfiles({ rootState, dispatch, commit }, defaultName = null) {
|
||||
let profiles = await profilesDb.find({})
|
||||
if (profiles.length === 0) {
|
||||
dispatch('createDefaultProfile', defaultName)
|
||||
return
|
||||
}
|
||||
// We want the primary profile to always be first
|
||||
// So sort with that then sort alphabetically by profile name
|
||||
profiles = profiles.sort((a, b) => {
|
||||
if (a._id === 'allChannels') {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (b._id === 'allChannels') {
|
||||
return 1
|
||||
}
|
||||
|
||||
return b.name - a.name
|
||||
})
|
||||
|
||||
if (state.profileList.length < profiles.length) {
|
||||
const profileIndex = profiles.findIndex((profile) => {
|
||||
return profile._id === rootState.settings.defaultProfile
|
||||
})
|
||||
|
||||
if (profileIndex !== -1) {
|
||||
commit('setActiveProfile', profileIndex)
|
||||
}
|
||||
}
|
||||
|
||||
commit('setProfileList', profiles)
|
||||
},
|
||||
|
||||
async grabProfileInfo(_, profileId) {
|
||||
console.log(profileId)
|
||||
return await profilesDb.findOne({ _id: profileId })
|
||||
},
|
||||
|
||||
async createDefaultProfile({ dispatch }, defaultName) {
|
||||
const randomColor = await dispatch('getRandomColor')
|
||||
const textColor = await dispatch('calculateColorLuminance', randomColor)
|
||||
const defaultProfile = {
|
||||
_id: 'allChannels',
|
||||
name: defaultName,
|
||||
bgColor: randomColor,
|
||||
textColor: textColor,
|
||||
subscriptions: []
|
||||
}
|
||||
|
||||
await profilesDb.update(
|
||||
{ _id: 'allChannels' },
|
||||
defaultProfile,
|
||||
{ upsert: true }
|
||||
)
|
||||
dispatch('grabAllProfiles')
|
||||
},
|
||||
|
||||
async updateProfile({ dispatch }, profile) {
|
||||
await profilesDb.update(
|
||||
{ _id: profile._id },
|
||||
profile,
|
||||
{ upsert: true }
|
||||
)
|
||||
dispatch('grabAllProfiles')
|
||||
},
|
||||
|
||||
async insertProfile({ dispatch }, profile) {
|
||||
await profilesDb.insert(profile)
|
||||
dispatch('grabAllProfiles')
|
||||
},
|
||||
|
||||
async removeProfile({ dispatch }, profileId) {
|
||||
await profilesDb.remove({ _id: profileId })
|
||||
dispatch('grabAllProfiles')
|
||||
},
|
||||
|
||||
compactProfiles(_) {
|
||||
profilesDb.persistence.compactDatafile()
|
||||
},
|
||||
|
||||
updateActiveProfile({ commit }, index) {
|
||||
commit('setActiveProfile', index)
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
setProfileList(state, profileList) {
|
||||
state.profileList = profileList
|
||||
},
|
||||
setActiveProfile(state, activeProfile) {
|
||||
state.activeProfile = activeProfile
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -5,7 +5,12 @@ const state = {
|
|||
isSideNavOpen: false,
|
||||
sessionSearchHistory: [],
|
||||
popularCache: null,
|
||||
trendingCache: null,
|
||||
trendingCache: {
|
||||
default: null,
|
||||
music: null,
|
||||
gaming: null,
|
||||
movies: null
|
||||
},
|
||||
showProgressBar: false,
|
||||
progressBarPercentage: 0,
|
||||
regionNames: [],
|
||||
|
@ -52,7 +57,10 @@ const state = {
|
|||
'#FFAB00',
|
||||
'#FF6D00',
|
||||
'#DD2C00'
|
||||
]
|
||||
],
|
||||
externalPlayerNames: [],
|
||||
externalPlayerValues: [],
|
||||
externalPlayerCmdArguments: {}
|
||||
}
|
||||
|
||||
const getters = {
|
||||
|
@ -102,10 +110,82 @@ const getters = {
|
|||
|
||||
getRecentBlogPosts () {
|
||||
return state.recentBlogPosts
|
||||
},
|
||||
|
||||
getExternalPlayerNames () {
|
||||
return state.externalPlayerNames
|
||||
},
|
||||
|
||||
getExternalPlayerValues () {
|
||||
return state.externalPlayerValues
|
||||
},
|
||||
|
||||
getExternalPlayerCmdArguments () {
|
||||
return state.externalPlayerCmdArguments
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function that calls `ipcRenderer.invoke(IRCtype, payload)` if the user is
|
||||
* using Electron or a provided custom callback otherwise.
|
||||
* @param {Object} context Object
|
||||
* @param {String} IRCtype String
|
||||
* @param {Function} webCbk Function
|
||||
* @param {Object} payload any (default: null)
|
||||
*/
|
||||
|
||||
async function invokeIRC(context, IRCtype, webCbk, payload = null) {
|
||||
let response = null
|
||||
const usingElectron = context.rootState.settings.usingElectron
|
||||
if (usingElectron) {
|
||||
const { ipcRenderer } = require('electron')
|
||||
response = await ipcRenderer.invoke(IRCtype, payload)
|
||||
} else if (webCbk) {
|
||||
response = await webCbk()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
const actions = {
|
||||
openExternalLink ({ rootState }, url) {
|
||||
const usingElectron = rootState.settings.usingElectron
|
||||
if (usingElectron) {
|
||||
const ipcRenderer = require('electron').ipcRenderer
|
||||
ipcRenderer.send('openExternalLink', url)
|
||||
} else {
|
||||
// Web placeholder
|
||||
}
|
||||
},
|
||||
|
||||
async getSystemLocale (context) {
|
||||
const webCbk = () => {
|
||||
if (navigator && navigator.language) {
|
||||
return navigator.language
|
||||
}
|
||||
}
|
||||
|
||||
return (await invokeIRC(context, 'getSystemLocale', webCbk)) || 'en-US'
|
||||
},
|
||||
|
||||
async showOpenDialog (context, options) {
|
||||
// TODO: implement showOpenDialog web compatible callback
|
||||
const webCbk = () => null
|
||||
return await invokeIRC(context, 'showOpenDialog', webCbk, options)
|
||||
},
|
||||
|
||||
async showSaveDialog (context, options) {
|
||||
// TODO: implement showSaveDialog web compatible callback
|
||||
const webCbk = () => null
|
||||
return await invokeIRC(context, 'showSaveDialog', webCbk, options)
|
||||
},
|
||||
|
||||
async getUserDataPath (context) {
|
||||
// TODO: implement getUserDataPath web compatible callback
|
||||
const webCbk = () => null
|
||||
return await invokeIRC(context, 'getUserDataPath', webCbk)
|
||||
},
|
||||
|
||||
updateShowProgressBar ({ commit }, value) {
|
||||
commit('setShowProgressBar', value)
|
||||
},
|
||||
|
@ -193,7 +273,7 @@ const actions = {
|
|||
getVideoParamsFromUrl (_, url) {
|
||||
/** @type {URL} */
|
||||
let urlObject
|
||||
const paramsObject = { videoId: null, timestamp: null }
|
||||
const paramsObject = { videoId: null, timestamp: null, playlistId: null }
|
||||
try {
|
||||
urlObject = new URL(url)
|
||||
} catch (e) {
|
||||
|
@ -210,6 +290,7 @@ const actions = {
|
|||
function() {
|
||||
if (urlObject.pathname === '/watch' && urlObject.searchParams.has('v')) {
|
||||
extractParams(urlObject.searchParams.get('v'))
|
||||
paramsObject.playlistId = urlObject.searchParams.get('list')
|
||||
return paramsObject
|
||||
}
|
||||
},
|
||||
|
@ -227,6 +308,13 @@ const actions = {
|
|||
return paramsObject
|
||||
}
|
||||
},
|
||||
// youtube.com/shorts
|
||||
function() {
|
||||
if (urlObject.pathname.match(/^\/shorts\/[A-Za-z0-9_-]+$/)) {
|
||||
extractParams(urlObject.pathname.replace('/shorts/', ''))
|
||||
return paramsObject
|
||||
}
|
||||
},
|
||||
// cloudtube
|
||||
function() {
|
||||
if (urlObject.host.match(/^cadence\.(gq|moe)$/) && urlObject.pathname.match(/^\/cloudtube\/video\/[A-Za-z0-9_-]+$/)) {
|
||||
|
@ -266,11 +354,12 @@ const actions = {
|
|||
//
|
||||
// If `urlType` is "invalid_url"
|
||||
// Nothing else
|
||||
const { videoId, timestamp } = actions.getVideoParamsFromUrl(null, urlStr)
|
||||
const { videoId, timestamp, playlistId } = actions.getVideoParamsFromUrl(null, urlStr)
|
||||
if (videoId) {
|
||||
return {
|
||||
urlType: 'video',
|
||||
videoId,
|
||||
playlistId,
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
|
@ -534,6 +623,181 @@ const actions = {
|
|||
|
||||
showToast (_, payload) {
|
||||
FtToastEvents.$emit('toast-open', payload.message, payload.action, payload.time)
|
||||
},
|
||||
|
||||
showExternalPlayerUnsupportedActionToast: function ({ dispatch }, payload) {
|
||||
if (!payload.ignoreWarnings) {
|
||||
const toastMessage = payload.template
|
||||
.replace('$', payload.externalPlayer)
|
||||
.replace('%', payload.action)
|
||||
dispatch('showToast', {
|
||||
message: toastMessage
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
getExternalPlayerCmdArgumentsData ({ commit }, payload) {
|
||||
const fileName = 'external-player-map.json'
|
||||
let fileData
|
||||
/* eslint-disable-next-line */
|
||||
const fileLocation = payload.isDev ? './static/' : `${__dirname}/static/`
|
||||
|
||||
if (fs.existsSync(`${fileLocation}${fileName}`)) {
|
||||
fileData = fs.readFileSync(`${fileLocation}${fileName}`)
|
||||
} else {
|
||||
fileData = '[{"name":"None","value":"","cmdArguments":null}]'
|
||||
}
|
||||
|
||||
const externalPlayerMap = JSON.parse(fileData).map((entry) => {
|
||||
return { name: entry.name, value: entry.value, cmdArguments: entry.cmdArguments }
|
||||
})
|
||||
|
||||
const externalPlayerNames = externalPlayerMap.map((entry) => { return entry.name })
|
||||
const externalPlayerValues = externalPlayerMap.map((entry) => { return entry.value })
|
||||
const externalPlayerCmdArguments = externalPlayerMap.reduce((result, item) => {
|
||||
result[item.value] = item.cmdArguments
|
||||
return result
|
||||
}, {})
|
||||
|
||||
commit('setExternalPlayerNames', externalPlayerNames)
|
||||
commit('setExternalPlayerValues', externalPlayerValues)
|
||||
commit('setExternalPlayerCmdArguments', externalPlayerCmdArguments)
|
||||
},
|
||||
|
||||
openInExternalPlayer ({ dispatch, state, rootState }, payload) {
|
||||
const args = []
|
||||
const externalPlayer = rootState.settings.externalPlayer
|
||||
const cmdArgs = state.externalPlayerCmdArguments[externalPlayer]
|
||||
const executable = rootState.settings.externalPlayerExecutable !== ''
|
||||
? rootState.settings.externalPlayerExecutable
|
||||
: cmdArgs.defaultExecutable
|
||||
const ignoreWarnings = rootState.settings.externalPlayerIgnoreWarnings
|
||||
const customArgs = rootState.settings.externalPlayerCustomArgs
|
||||
|
||||
if (payload.watchProgress > 0) {
|
||||
if (typeof cmdArgs.startOffset === 'string') {
|
||||
args.push(`${cmdArgs.startOffset}${payload.watchProgress}`)
|
||||
} else {
|
||||
dispatch('showExternalPlayerUnsupportedActionToast', {
|
||||
ignoreWarnings,
|
||||
externalPlayer,
|
||||
template: payload.strings.UnsupportedActionTemplate,
|
||||
action: payload.strings['Unsupported Actions']['starting video at offset']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.playbackRate !== null) {
|
||||
if (typeof cmdArgs.playbackRate === 'string') {
|
||||
args.push(`${cmdArgs.playbackRate}${payload.playbackRate}`)
|
||||
} else {
|
||||
dispatch('showExternalPlayerUnsupportedActionToast', {
|
||||
ignoreWarnings,
|
||||
externalPlayer,
|
||||
template: payload.strings.UnsupportedActionTemplate,
|
||||
action: payload.strings['Unsupported Actions']['setting a playback rate']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the video is in a playlist
|
||||
if (typeof cmdArgs.playlistUrl === 'string' && payload.playlistId !== null && payload.playlistId !== '') {
|
||||
if (payload.playlistIndex !== null) {
|
||||
if (typeof cmdArgs.playlistIndex === 'string') {
|
||||
args.push(`${cmdArgs.playlistIndex}${payload.playlistIndex}`)
|
||||
} else {
|
||||
dispatch('showExternalPlayerUnsupportedActionToast', {
|
||||
ignoreWarnings,
|
||||
externalPlayer,
|
||||
template: payload.strings.UnsupportedActionTemplate,
|
||||
action: payload.strings['Unsupported Actions']['opening specific video in a playlist (falling back to opening the video)']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.playlistReverse) {
|
||||
if (typeof cmdArgs.playlistReverse === 'string') {
|
||||
args.push(cmdArgs.playlistReverse)
|
||||
} else {
|
||||
dispatch('showExternalPlayerUnsupportedActionToast', {
|
||||
ignoreWarnings,
|
||||
externalPlayer,
|
||||
template: payload.strings.UnsupportedActionTemplate,
|
||||
action: payload.strings['Unsupported Actions']['reversing playlists']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.playlistShuffle) {
|
||||
if (typeof cmdArgs.playlistShuffle === 'string') {
|
||||
args.push(cmdArgs.playlistShuffle)
|
||||
} else {
|
||||
dispatch('showExternalPlayerUnsupportedActionToast', {
|
||||
ignoreWarnings,
|
||||
externalPlayer,
|
||||
template: payload.strings.UnsupportedActionTemplate,
|
||||
action: payload.strings['Unsupported Actions']['shuffling playlists']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.playlistLoop) {
|
||||
if (typeof cmdArgs.playlistLoop === 'string') {
|
||||
args.push(cmdArgs.playlistLoop)
|
||||
} else {
|
||||
dispatch('showExternalPlayerUnsupportedActionToast', {
|
||||
ignoreWarnings,
|
||||
externalPlayer,
|
||||
template: payload.strings.UnsupportedActionTemplate,
|
||||
action: payload.strings['Unsupported Actions']['looping playlists']
|
||||
})
|
||||
}
|
||||
}
|
||||
if (cmdArgs.supportsYtdlProtocol) {
|
||||
args.push(`${cmdArgs.playlistUrl}ytdl://${payload.playlistId}`)
|
||||
} else {
|
||||
args.push(`${cmdArgs.playlistUrl}https://youtube.com/playlist?list=${payload.playlistId}`)
|
||||
}
|
||||
} else {
|
||||
if (payload.playlistId !== null && payload.playlistId !== '') {
|
||||
dispatch('showExternalPlayerUnsupportedActionToast', {
|
||||
ignoreWarnings,
|
||||
externalPlayer,
|
||||
template: payload.strings.UnsupportedActionTemplate,
|
||||
action: payload.strings['Unsupported Actions']['opening playlists']
|
||||
})
|
||||
}
|
||||
if (payload.videoId !== null) {
|
||||
if (cmdArgs.supportsYtdlProtocol) {
|
||||
args.push(`${cmdArgs.videoUrl}ytdl://${payload.videoId}`)
|
||||
} else {
|
||||
args.push(`${cmdArgs.videoUrl}https://www.youtube.com/watch?v=${payload.videoId}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append custom user-defined arguments
|
||||
if (customArgs !== null) {
|
||||
const custom = customArgs.split(';')
|
||||
args.push(...custom)
|
||||
}
|
||||
|
||||
const openingToast = payload.strings.OpeningTemplate
|
||||
.replace('$', payload.playlistId === null || payload.playlistId === ''
|
||||
? payload.strings.video
|
||||
: payload.strings.playlist)
|
||||
.replace('%', externalPlayer)
|
||||
dispatch('showToast', {
|
||||
message: openingToast
|
||||
})
|
||||
|
||||
console.log(executable, args)
|
||||
|
||||
const { ipcRenderer } = require('electron')
|
||||
ipcRenderer.send('openInExternalPlayer', {
|
||||
executable,
|
||||
args
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -571,8 +835,8 @@ const mutations = {
|
|||
state.popularCache = value
|
||||
},
|
||||
|
||||
setTrendingCache (state, value) {
|
||||
state.trendingCache = value
|
||||
setTrendingCache (state, value, page) {
|
||||
state.trendingCache[page] = value
|
||||
},
|
||||
|
||||
setSearchSortBy (state, value) {
|
||||
|
@ -601,6 +865,18 @@ const mutations = {
|
|||
|
||||
setRecentBlogPosts (state, value) {
|
||||
state.recentBlogPosts = value
|
||||
},
|
||||
|
||||
setExternalPlayerNames (state, value) {
|
||||
state.externalPlayerNames = value
|
||||
},
|
||||
|
||||
setExternalPlayerValues (state, value) {
|
||||
state.externalPlayerValues = value
|
||||
},
|
||||
|
||||
setExternalPlayerCmdArguments (state, value) {
|
||||
state.externalPlayerCmdArguments = value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,15 +8,10 @@ import { HttpsProxyAgent } from 'https-proxy-agent'
|
|||
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||
|
||||
const state = {
|
||||
main: 0,
|
||||
isYtSearchRunning: false
|
||||
}
|
||||
|
||||
const getters = {
|
||||
getMain ({ state }) {
|
||||
return state.main
|
||||
}
|
||||
}
|
||||
const getters = {}
|
||||
|
||||
const actions = {
|
||||
ytSearch ({ commit, dispatch, rootState }, payload) {
|
||||
|
|
|
@ -437,7 +437,7 @@ body.vjs-full-window {
|
|||
cursor: none;
|
||||
}
|
||||
|
||||
.vjs-icon-fullwindow-enter, .video-js .vjs-fullwindow-control .vjs-icon-placeholder {
|
||||
.vjs-icon-fullwindow-enter, .vjs-icon-theatre-inactive, .video-js .vjs-fullwindow-control .vjs-icon-placeholder {
|
||||
color: white !important;
|
||||
margin-top: 10px !important;
|
||||
cursor:pointer;
|
||||
|
@ -449,11 +449,11 @@ body.vjs-full-window {
|
|||
content: url(assets/img/open_fullwindow.svg);
|
||||
}
|
||||
/* Hide button in full screen mode */
|
||||
.vjs--full-screen-enabled .vjs-button-fullwindow {
|
||||
.vjs--full-screen-enabled .vjs-button-fullwindow, .vjs--full-screen-enabled .vjs-button-theatre, .vjs-full-window .vjs-button-theatre {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vjs-icon-fullwindow-exit, .video-js.vjs-fullwindow .vjs-fullwindow-control .vjs-icon-placeholder {
|
||||
.vjs-icon-fullwindow-exit, .vjs-icon-theatre-active, .video-js.vjs-fullwindow .vjs-fullwindow-control .vjs-icon-placeholder {
|
||||
font-family: VideoJS;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
@ -469,6 +469,10 @@ body.vjs-full-window {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vjs-icon-theatre-inactive, .vjs-icon-theatre-active {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
|
||||
.vjs-icon-loop-active {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
@ -478,6 +482,20 @@ body.vjs-full-window {
|
|||
/* filter: invert(1) drop-shadow(1px 0px 0px var(--primary-color)); */
|
||||
}
|
||||
|
||||
.vjs-icon-theatre-inactive:before {
|
||||
content: url(assets/img/open_theatre.svg)
|
||||
}
|
||||
|
||||
.vjs-icon-theatre-active:before {
|
||||
content: url(assets/img/close_theatre.svg)
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1350px) {
|
||||
.videoPlayer .vjs-button-theatre {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
.loop-black:before {
|
||||
filter: brightness(0%);
|
||||
}
|
||||
|
|
|
@ -88,18 +88,5 @@ export default Vue.extend({
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
usingElectron: function () {
|
||||
return this.$store.getters.getUsingElectron
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openUrl: function (url) {
|
||||
if (this.usingElectron) {
|
||||
const shell = require('electron').shell
|
||||
shell.openExternal(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,42 +1,33 @@
|
|||
.card {
|
||||
position: relative;
|
||||
width: 85%;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 20px;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.channelBanner {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
max-height: 300px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.defaultChannelBanner {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 200px;
|
||||
max-height: 200px;
|
||||
height:200px;
|
||||
background-color: black;
|
||||
background-image: url("~images/defaultBanner.png");
|
||||
background-image: url("images/defaultBanner.png");
|
||||
}
|
||||
|
||||
.channelInfoContainer {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
margin-top: 300px;
|
||||
position: relative;
|
||||
background-color: var(--card-bg-color);
|
||||
}
|
||||
|
||||
.channelInfo {
|
||||
height: 100px;
|
||||
width: 85%;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 30px;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.channelThumbnail {
|
||||
|
@ -50,24 +41,18 @@
|
|||
font-weight: bold;
|
||||
width: 100%;
|
||||
font-size: 25px;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 120px;
|
||||
}
|
||||
|
||||
.channelSubCount {
|
||||
position: absolute;
|
||||
color: var(--tertiary-text-color);
|
||||
top: 50px;
|
||||
left: 120px;
|
||||
}
|
||||
|
||||
.subscribeButton {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 20px;
|
||||
height: 50px;
|
||||
min-width: 150px;
|
||||
align-self: center
|
||||
}
|
||||
|
||||
.channelSearch {
|
||||
|
@ -80,8 +65,7 @@
|
|||
}
|
||||
|
||||
.channelInfoTabs {
|
||||
position: absolute;
|
||||
bottom: -16px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
@ -89,7 +73,6 @@
|
|||
padding: 15px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
align-self: flex-end;
|
||||
-webkit-transition: background 0.2s ease-out;
|
||||
-moz-transition: background 0.2s ease-out;
|
||||
|
@ -97,6 +80,10 @@
|
|||
transition: background 0.2s ease-out;
|
||||
}
|
||||
|
||||
.selectedTab {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background-color: var(--side-nav-hover-color);
|
||||
-moz-transition: background 0.2s ease-in;
|
||||
|
@ -139,3 +126,14 @@
|
|||
cursor: pointer;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.thumbnailContainer {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.channelLineContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
|
|
@ -73,8 +73,8 @@ export default Vue.extend({
|
|||
return this.$store.getters.getBackendFallback
|
||||
},
|
||||
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
|
||||
sessionSearchHistory: function () {
|
||||
|
@ -250,6 +250,7 @@ export default Vue.extend({
|
|||
ytch.getChannelInfo(this.id).then((response) => {
|
||||
this.id = response.authorId
|
||||
this.channelName = response.author
|
||||
document.title = `${this.channelName} - ${process.env.PRODUCT_NAME}`
|
||||
if (this.hideChannelSubscriptions || response.subscriberCount === 0) {
|
||||
this.subCount = null
|
||||
} else {
|
||||
|
@ -258,6 +259,14 @@ export default Vue.extend({
|
|||
this.thumbnailUrl = response.authorThumbnails[2].url
|
||||
this.channelDescription = autolinker.link(response.description)
|
||||
this.relatedChannels = response.relatedChannels.items
|
||||
this.relatedChannels.forEach(relatedChannel => {
|
||||
relatedChannel.authorThumbnails.map(thumbnail => {
|
||||
if (!thumbnail.url.includes('https')) {
|
||||
thumbnail.url = `https:${thumbnail.url}`
|
||||
}
|
||||
return thumbnail
|
||||
})
|
||||
})
|
||||
|
||||
if (response.authorBanners !== null) {
|
||||
const bannerUrl = response.authorBanners[response.authorBanners.length - 1].url
|
||||
|
@ -341,26 +350,27 @@ export default Vue.extend({
|
|||
this.isLoading = true
|
||||
this.apiUsed = 'invidious'
|
||||
|
||||
this.$store.dispatch('invidiousGetChannelInfo', this.id).then((response) => {
|
||||
this.invidiousGetChannelInfo(this.id).then((response) => {
|
||||
console.log(response)
|
||||
this.channelName = response.author
|
||||
document.title = `${this.channelName} - ${process.env.PRODUCT_NAME}`
|
||||
this.id = response.authorId
|
||||
if (this.hideChannelSubscriptions) {
|
||||
this.subCount = null
|
||||
} else {
|
||||
this.subCount = response.subCount
|
||||
}
|
||||
this.thumbnailUrl = response.authorThumbnails[3].url.replace('https://yt3.ggpht.com', `${this.invidiousInstance}/ggpht/`)
|
||||
this.thumbnailUrl = response.authorThumbnails[3].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||
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.invidiousInstance}/ggpht/`)
|
||||
channel.authorThumbnails[channel.authorThumbnails.length - 1].url = channel.authorThumbnails[channel.authorThumbnails.length - 1].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||
|
||||
return channel
|
||||
})
|
||||
this.latestVideos = response.latestVideos
|
||||
|
||||
if (typeof (response.authorBanners) !== 'undefined') {
|
||||
this.bannerUrl = response.authorBanners[0].url.replace('https://yt3.ggpht.com', `${this.invidiousInstance}/ggpht/`)
|
||||
this.bannerUrl = response.authorBanners[0].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`)
|
||||
}
|
||||
|
||||
this.isLoading = false
|
||||
|
@ -388,7 +398,7 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
|
||||
this.$store.dispatch('invidiousAPICall', payload).then((response) => {
|
||||
this.invidiousAPICall(payload).then((response) => {
|
||||
this.latestVideos = this.latestVideos.concat(response)
|
||||
this.latestVideosPage++
|
||||
this.isElementListLoading = false
|
||||
|
@ -471,7 +481,7 @@ export default Vue.extend({
|
|||
payload.params.continuation = this.playlistContinuationString
|
||||
}
|
||||
|
||||
this.$store.dispatch('invidiousAPICall', payload).then((response) => {
|
||||
this.invidiousAPICall(payload).then((response) => {
|
||||
this.playlistContinuationString = response.continuation
|
||||
this.latestPlaylists = this.latestPlaylists.concat(response.playlists)
|
||||
this.isElementListLoading = false
|
||||
|
@ -680,7 +690,7 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
|
||||
this.$store.dispatch('invidiousAPICall', payload).then((response) => {
|
||||
this.invidiousAPICall(payload).then((response) => {
|
||||
this.searchResults = this.searchResults.concat(response)
|
||||
this.isElementListLoading = false
|
||||
this.searchPage++
|
||||
|
@ -707,7 +717,9 @@ export default Vue.extend({
|
|||
|
||||
...mapActions([
|
||||
'showToast',
|
||||
'updateProfile'
|
||||
'updateProfile',
|
||||
'invidiousGetChannelInfo',
|
||||
'invidiousAPICall'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -19,51 +19,67 @@
|
|||
v-else
|
||||
class="defaultChannelBanner"
|
||||
>
|
||||
<div class="channelInfoContainer">
|
||||
<div class="channelInfo">
|
||||
<img
|
||||
class="channelThumbnail"
|
||||
:src="thumbnailUrl"
|
||||
<div
|
||||
class="channelInfoContainer"
|
||||
>
|
||||
<div
|
||||
class="channelInfo"
|
||||
>
|
||||
<div
|
||||
class="thumbnailContainer"
|
||||
>
|
||||
<span
|
||||
class="channelName"
|
||||
>
|
||||
{{ channelName }}
|
||||
</span>
|
||||
<br>
|
||||
<span
|
||||
v-if="subCount !== null"
|
||||
class="channelSubCount"
|
||||
>
|
||||
{{ formattedSubCount }}
|
||||
<span v-if="subCount === 1">{{ $t("Channel.Subscriber") }}</span>
|
||||
<span v-else>{{ $t("Channel.Subscribers") }}</span>
|
||||
</span>
|
||||
<img
|
||||
class="channelThumbnail"
|
||||
:src="thumbnailUrl"
|
||||
>
|
||||
<div
|
||||
class="channelLineContainer"
|
||||
>
|
||||
<span
|
||||
class="channelName"
|
||||
>
|
||||
{{ channelName }}
|
||||
</span>
|
||||
<span
|
||||
v-if="subCount !== null"
|
||||
class="channelSubCount"
|
||||
>
|
||||
{{ formattedSubCount }}
|
||||
<span v-if="subCount === 1">{{ $t("Channel.Subscriber") }}</span>
|
||||
<span v-else>{{ $t("Channel.Subscribers") }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ft-button
|
||||
:label="subscribedText"
|
||||
background-color="var(--primary-color)"
|
||||
text-color="var(--text-with-main-color)"
|
||||
class="subscribeButton"
|
||||
@click="handleSubscription"
|
||||
/>
|
||||
</div>
|
||||
<ft-button
|
||||
:label="subscribedText"
|
||||
background-color="var(--primary-color)"
|
||||
text-color="var(--text-with-main-color)"
|
||||
class="subscribeButton"
|
||||
@click="handleSubscription"
|
||||
/>
|
||||
|
||||
<ft-flex-box
|
||||
class="channelInfoTabs"
|
||||
>
|
||||
<div
|
||||
class="tab"
|
||||
:class="(currentTab==='videos')?'selectedTab':''"
|
||||
@click="changeTab('videos')"
|
||||
>
|
||||
{{ $t("Channel.Videos.Videos").toUpperCase() }}
|
||||
</div>
|
||||
<div
|
||||
class="tab"
|
||||
:class="(currentTab==='playlists')?'selectedTab':''"
|
||||
@click="changeTab('playlists')"
|
||||
>
|
||||
{{ $t("Channel.Playlists.Playlists").toUpperCase() }}
|
||||
</div>
|
||||
<div
|
||||
class="tab"
|
||||
:class="(currentTab==='about')?'selectedTab':''"
|
||||
@click="changeTab('about')"
|
||||
>
|
||||
{{ $t("Channel.About.About").toUpperCase() }}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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'
|
||||
|
@ -33,8 +34,8 @@ export default Vue.extend({
|
|||
backendFallback: function () {
|
||||
return this.$store.getters.getBackendFallback
|
||||
},
|
||||
invidiousInstance: function () {
|
||||
return this.$store.getters.getInvidiousInstance
|
||||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -62,17 +63,15 @@ export default Vue.extend({
|
|||
getPlaylistLocal: function () {
|
||||
this.isLoading = true
|
||||
|
||||
this.$store.dispatch('ytGetPlaylistInfo', this.playlistId).then((result) => {
|
||||
this.ytGetPlaylistInfo(this.playlistId).then((result) => {
|
||||
console.log('done')
|
||||
console.log(result)
|
||||
|
||||
const randomVideoIndex = Math.floor((Math.random() * result.items.length))
|
||||
|
||||
this.infoData = {
|
||||
id: result.id,
|
||||
title: result.title,
|
||||
description: result.description ? result.description : '',
|
||||
randomVideoId: result.items[randomVideoIndex].id,
|
||||
firstVideoId: result.items[0].id,
|
||||
viewCount: result.views,
|
||||
videoCount: result.estimatedItemCount,
|
||||
lastUpdated: result.lastUpdated ? result.lastUpdated : '',
|
||||
|
@ -120,21 +119,19 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
|
||||
this.$store.dispatch('invidiousGetPlaylistInfo', payload).then((result) => {
|
||||
this.invidiousGetPlaylistInfo(payload).then((result) => {
|
||||
console.log('done')
|
||||
console.log(result)
|
||||
|
||||
const randomVideoIndex = Math.floor((Math.random() * result.videos.length) + 1)
|
||||
|
||||
this.infoData = {
|
||||
id: result.playlistId,
|
||||
title: result.title,
|
||||
description: result.description,
|
||||
randomVideoId: result.videos[randomVideoIndex].videoId,
|
||||
firstVideoId: result.videos[0].videoId,
|
||||
viewCount: result.viewCount,
|
||||
videoCount: result.videoCount,
|
||||
channelName: result.author,
|
||||
channelThumbnail: result.authorThumbnails[2].url.replace('https://yt3.ggpht.com', `${this.invidiousInstance}/ggpht/`),
|
||||
channelThumbnail: result.authorThumbnails[2].url.replace('https://yt3.ggpht.com', `${this.currentInvidiousInstance}/ggpht/`),
|
||||
channelId: result.authorId,
|
||||
infoSource: 'invidious'
|
||||
}
|
||||
|
@ -180,6 +177,11 @@ export default Vue.extend({
|
|||
this.shownResults = history.data
|
||||
this.nextPageRef = history.nextPageRef
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'ytGetPlaylistInfo',
|
||||
'invidiousGetPlaylistInfo'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
:key="index"
|
||||
:data="item"
|
||||
:playlist-id="playlistId"
|
||||
:playlist-index="index"
|
||||
appearance="result"
|
||||
force-list-type="list"
|
||||
/>
|
||||
|
|
|
@ -5,11 +5,17 @@
|
|||
}
|
||||
|
||||
.floatingTopButton {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 70px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 350px) {
|
||||
.floatingTopButton {
|
||||
position: absolute
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 680px) {
|
||||
.card {
|
||||
width: 90%;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Vue from 'vue'
|
||||
import { mapActions } from 'vuex'
|
||||
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
|
||||
|
@ -38,9 +39,10 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
this.isLoading = true
|
||||
const result = await this.$store.dispatch('invidiousAPICall', searchPayload).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
const result = await this.invidiousAPICall(searchPayload)
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
this.isLoading = false
|
||||
|
@ -54,6 +56,10 @@ export default Vue.extend({
|
|||
})
|
||||
this.isLoading = false
|
||||
this.$store.commit('setPopularCache', this.shownResults)
|
||||
}
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'invidiousAPICall'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -118,7 +118,7 @@ export default Vue.extend({
|
|||
payload.options.pages = 1
|
||||
}
|
||||
|
||||
this.$store.dispatch('ytSearch', payload).then((result) => {
|
||||
this.ytSearch(payload).then((result) => {
|
||||
console.log(result)
|
||||
if (!result) {
|
||||
return
|
||||
|
@ -230,7 +230,7 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
|
||||
this.$store.dispatch('invidiousAPICall', searchPayload).then((result) => {
|
||||
this.invidiousAPICall(searchPayload).then((result) => {
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
@ -333,7 +333,9 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
...mapActions([
|
||||
'showToast'
|
||||
'showToast',
|
||||
'ytSearch',
|
||||
'invidiousAPICall'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue