Merge branch 'development'

Conflicts:
	package-lock.json
	package.json
This commit is contained in:
PrestonN 2021-09-04 16:35:32 -04:00
commit 263fad0591
164 changed files with 11149 additions and 11888 deletions

View File

@ -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.

9
.github/auto-merge.yml vendored Normal file
View File

@ -0,0 +1,9 @@
minApprovals:
COLLABORATOR: 2
maxRequestedChanges:
COLLABORATOR: 0
mergeMethod: squash
requiredBaseBranches:
- development
- test
reportStatus: true

17
.github/workflows/autoMerge.yml vendored Normal file
View File

@ -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

View File

@ -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

4
.gitignore vendored
View File

@ -2,6 +2,10 @@
dist/electron/*
storyboards/*
dashFiles/*
static/dashFiles
static/storyboards
static/dashFiles/*
static/storyboards/*
dist/web/*
build/*
!build/icons

5
.mergepal.yml Normal file
View File

@ -0,0 +1,5 @@
whitelist:
blacklist:
- wip
- do-not-merge
method: squash

View File

@ -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.

View File

@ -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,

View File

@ -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()

View File

@ -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'),

11816
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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)

View File

@ -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 })
})
})

View File

@ -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;
}

View File

@ -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'
])
}
})

View File

@ -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')"

View File

@ -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

View File

@ -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

View File

@ -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'
])
}
})

View File

@ -0,0 +1 @@
@use "../../sass-partials/settings"

View File

@ -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" />

View File

@ -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 () {

View File

@ -35,6 +35,10 @@ export default Vue.extend({
type: Boolean,
default: false
},
spellcheck: {
type: Boolean,
default: true
},
dataList: {
type: Array,
default: () => { return [] }

View File

@ -26,6 +26,7 @@
type="text"
:placeholder="placeholder"
:disabled="disabled"
:spellcheck="spellcheck"
@input="e => handleInput(e.target.value)"
@focus="handleFocus"
@blur="handleInputBlur"

View File

@ -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) {

View File

@ -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'
])
}
})

View File

@ -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}`"

View File

@ -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'
])
}
})

View File

@ -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')"

View File

@ -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;
}

View File

@ -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: {

View File

@ -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 }}

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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 {

View File

@ -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 () {

View File

@ -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"

View File

@ -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;
}
}

View File

@ -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)
}
}
})

View File

@ -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>

View File

@ -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'
])
}
})

View File

@ -12,7 +12,7 @@
:label="$t('Share.Include Timestamp')"
:compact="true"
:default-value="includeTimestamp"
@change="updateincludeTimestamp"
@change="updateIncludeTimestamp"
/>
</ft-flex-box>
<div class="shareLinks">

View File

@ -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'
])
}
})

View File

@ -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'
])
}
})

View File

@ -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>

View File

@ -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'
])
}
})

View File

@ -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')"

View File

@ -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;
}

View File

@ -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'
])
}
})

View File

@ -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

View File

@ -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" />

View File

@ -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 () {

View File

@ -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

View File

@ -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')"
>

View File

@ -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

View File

@ -34,7 +34,7 @@
fixed-width
/>
<p class="navLabel">
{{ $t("Trending") }}
{{ $t("Trending.Trending") }}
</p>
</div>
<div

View File

@ -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) {

View File

@ -30,7 +30,7 @@
:max-value="maxUiScale"
:step="uiScaleStep"
value-extension="%"
@change="handleUiScale"
@change="updateUiScale(parseInt($event))"
/>
</ft-flex-box>
<br>

View File

@ -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'
])
}
})

View File

@ -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

View File

@ -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" />

View File

@ -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

View File

@ -146,7 +146,7 @@
class="showMoreReplies"
@click="getCommentReplies(index)"
>
<span>Show More Replies</span>
<span>{{ $t("Comments.Show More Replies") }}</span>
</div>
</div>
</div>

View File

@ -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) {

View File

@ -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'
])
}
})

View File

@ -86,7 +86,3 @@
::v-deep .iconDropdown
left: calc(50% - 20px)
right: auto
@media only screen and (max-width: 1350px)
.theatreModeButton
display: none

View File

@ -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>

View File

@ -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'
])
}
})

View File

@ -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'
])
}
})

View File

@ -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>

View File

@ -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
})

View File

@ -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({

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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%);
}

View File

@ -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)
}
}
}
})

View File

@ -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;
}

View File

@ -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'
])
}
})

View File

@ -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() }}

View File

@ -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'
])
}
})

View File

@ -18,6 +18,7 @@
:key="index"
:data="item"
:playlist-id="playlistId"
:playlist-index="index"
appearance="result"
force-list-type="list"
/>

View File

@ -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%;

View File

@ -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'
])
}
})

View File

@ -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