FreeTube/src/renderer/store/modules/utils.js

1030 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import IsEqual from 'lodash.isequal'
import FtToastEvents from '../../components/ft-toast/ft-toast-events'
import fs from 'fs'
import path from 'path'
import i18n from '../../i18n/index'
import { IpcChannels } from '../../../constants'
const state = {
isSideNavOpen: false,
sessionSearchHistory: [],
popularCache: null,
trendingCache: {
default: null,
music: null,
gaming: null,
movies: null
},
showProgressBar: false,
progressBarPercentage: 0,
regionNames: [],
regionValues: [],
recentBlogPosts: [],
searchSettings: {
sortBy: 'relevance',
time: '',
type: 'all',
duration: ''
},
externalPlayerNames: [],
externalPlayerNameTranslationKeys: [],
externalPlayerValues: [],
externalPlayerCmdArguments: {}
}
const getters = {
getIsSideNavOpen () {
return state.isSideNavOpen
},
getCurrentVolume () {
return state.currentVolume
},
getSessionSearchHistory () {
return state.sessionSearchHistory
},
getPopularCache () {
return state.popularCache
},
getTrendingCache () {
return state.trendingCache
},
getSearchSettings () {
return state.searchSettings
},
getShowProgressBar () {
return state.showProgressBar
},
getProgressBarPercentage () {
return state.progressBarPercentage
},
getRegionNames () {
return state.regionNames
},
getRegionValues () {
return state.regionValues
},
getRecentBlogPosts () {
return state.recentBlogPosts
},
getExternalPlayerNames () {
return state.externalPlayerNames
},
getExternalPlayerNameTranslationKeys () {
return state.externalPlayerNameTranslationKeys
},
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
if (process.env.IS_ELECTRON) {
const { ipcRenderer } = require('electron')
response = await ipcRenderer.invoke(IRCtype, payload)
} else if (webCbk) {
response = await webCbk()
}
return response
}
const actions = {
openExternalLink (_, url) {
if (process.env.IS_ELECTRON) {
const ipcRenderer = require('electron').ipcRenderer
ipcRenderer.send(IpcChannels.OPEN_EXTERNAL_LINK, url)
} else {
window.open(url, '_blank')
}
},
replaceFilenameForbiddenChars(_, filenameOriginal) {
let filenameNew = filenameOriginal
let forbiddenChars = {}
switch (process.platform) {
case 'win32':
forbiddenChars = {
'<': '', // U+FF1C
'>': '', // U+FF1E
':': '', // U+FF1A
'"': '', // U+FF02
'/': '', // U+FF0F
'\\': '', // U+FF3C
'|': '', // U+FF5C
'?': '', // U+FF1F
'*': '' // U+FF0A
}
break
case 'darwin':
forbiddenChars = { '/': '', ':': '' }
break
case 'linux':
forbiddenChars = { '/': '' }
break
default:
break
}
for (const forbiddenChar in forbiddenChars) {
filenameNew = filenameNew.replaceAll(forbiddenChar, forbiddenChars[forbiddenChar])
}
return filenameNew
},
/**
* This writes to the clipboard. If an error occurs during the copy,
* a toast with the error is shown. If the copy is successful and
* there is a success message, a toast with that message is shown.
* @param {string} content the content to be copied to the clipboard
* @param {string} messageOnSuccess the message to be displayed as a toast when the copy succeeds (optional)
* @param {string} messageOnError the message to be displayed as a toast when the copy fails (optional)
*/
async copyToClipboard ({ dispatch }, { content, messageOnSuccess, messageOnError }) {
if (navigator.clipboard !== undefined && window.isSecureContext) {
try {
await navigator.clipboard.writeText(content)
if (messageOnSuccess !== undefined) {
dispatch('showToast', {
message: messageOnSuccess
})
}
} catch (error) {
console.error(`Failed to copy ${content} to clipboard`, error)
if (messageOnError !== undefined) {
dispatch('showToast', {
message: `${messageOnError}: ${error}`,
time: 5000
})
} else {
dispatch('showToast', {
message: `${i18n.t('Clipboard.Copy failed')}: ${error}`,
time: 5000
})
}
}
} else {
dispatch('showToast', {
message: i18n.t('Clipboard.Cannot access clipboard without a secure connection'),
time: 5000
})
}
},
async downloadMedia({ rootState, dispatch }, { url, title, extension, fallingBackPath }) {
const fileName = `${await dispatch('replaceFilenameForbiddenChars', title)}.${extension}`
const errorMessage = i18n.t('Downloading failed', { videoTitle: title })
let folderPath = rootState.settings.downloadFolderPath
if (!process.env.IS_ELECTRON) {
dispatch('openExternalLink', url)
return
}
if (folderPath === '') {
const options = {
defaultPath: fileName,
filters: [
{
name: extension.toUpperCase(),
extensions: [extension]
}
]
}
const response = await dispatch('showSaveDialog', { options })
if (response.canceled || response.filePath === '') {
// User canceled the save dialog
return
}
folderPath = response.filePath
} else {
if (!fs.existsSync(folderPath)) {
try {
fs.mkdirSync(folderPath, { recursive: true })
} catch (err) {
console.error(err)
dispatch('showToast', {
message: err
})
return
}
}
folderPath = path.join(folderPath, fileName)
}
dispatch('showToast', {
message: i18n.t('Starting download', { videoTitle: title })
})
const response = await fetch(url).catch((error) => {
console.error(error)
dispatch('showToast', {
message: errorMessage
})
})
const reader = response.body.getReader()
const chunks = []
const handleError = (err) => {
console.error(err)
dispatch('showToast', {
message: errorMessage
})
}
const processText = async ({ done, value }) => {
if (done) {
return
}
chunks.push(value)
// Can be used in the future to determine download percentage
// const contentLength = response.headers.get('Content-Length')
// const receivedLength = value.length
// const percentage = receivedLength / contentLength
await reader.read().then(processText).catch(handleError)
}
await reader.read().then(processText).catch(handleError)
const blobFile = new Blob(chunks)
const buffer = await blobFile.arrayBuffer()
fs.writeFile(folderPath, new DataView(buffer), (err) => {
if (err) {
console.error(err)
dispatch('showToast', {
message: errorMessage
})
} else {
dispatch('showToast', {
message: i18n.t('Downloading has completed', { videoTitle: title })
})
}
})
},
async getSystemLocale (context) {
const webCbk = () => {
if (navigator && navigator.language) {
return navigator.language
}
}
return (await invokeIRC(context, IpcChannels.GET_SYSTEM_LOCALE, webCbk)) || 'en-US'
},
/**
* @param {Object} response the response from `showOpenDialog`
* @param {Number} index which file to read (defaults to the first in the response)
* @returns the text contents of the selected file
*/
async readFileFromDialog(context, { response, index = 0 }) {
return await new Promise((resolve, reject) => {
if (process.env.IS_ELECTRON) {
// if this is Electron, use fs
fs.readFile(response.filePaths[index], (err, data) => {
if (err) {
reject(err)
return
}
resolve(new TextDecoder('utf-8').decode(data))
})
} else {
// if this is web, use FileReader
try {
const reader = new FileReader()
reader.onload = function (file) {
resolve(file.currentTarget.result)
}
reader.readAsText(response.files[index])
} catch (exception) {
reject(exception)
}
}
})
},
async showOpenDialog (context, options) {
const webCbk = () => {
return new Promise((resolve) => {
const fileInput = document.createElement('input')
fileInput.setAttribute('type', 'file')
if (options?.filters[0]?.extensions !== undefined) {
// this will map the given extensions from the options to the accept attribute of the input
fileInput.setAttribute('accept', options.filters[0].extensions.map((extension) => { return `.${extension}` }).join(', '))
}
fileInput.onchange = () => {
const files = Array.from(fileInput.files)
resolve({ canceled: false, files })
delete fileInput.onchange
}
const listenForEnd = () => {
window.removeEventListener('focus', listenForEnd)
// 1 second timeout on the response from the file picker to prevent awaiting forever
setTimeout(() => {
if (fileInput.files.length === 0 && typeof fileInput.onchange === 'function') {
// if there are no files and the onchange has not been triggered, the file-picker was canceled
resolve({ canceled: true })
delete fileInput.onchange
}
}, 1000)
}
window.addEventListener('focus', listenForEnd)
fileInput.click()
})
}
return await invokeIRC(context, IpcChannels.SHOW_OPEN_DIALOG, webCbk, options)
},
async showSaveDialog (context, { options, useModal = false }) {
// TODO: implement showSaveDialog web compatible callback
const webCbk = () => null
return await invokeIRC(context, IpcChannels.SHOW_SAVE_DIALOG, webCbk, { options, useModal })
},
async getUserDataPath (context) {
// TODO: implement getUserDataPath web compatible callback
const webCbk = () => null
return await invokeIRC(context, IpcChannels.GET_USER_DATA_PATH, webCbk)
},
async getPicturesPath (context) {
const webCbk = () => null
return await invokeIRC(context, IpcChannels.GET_PICTURES_PATH, webCbk)
},
parseScreenshotCustomFileName: function({ rootState }, payload) {
return new Promise((resolve, reject) => {
const { pattern = rootState.settings.screenshotFilenamePattern, date, playerTime, videoId } = payload
const keywords = [
['%Y', date.getFullYear()], // year 4 digits
['%M', (date.getMonth() + 1).toString().padStart(2, '0')], // month 2 digits
['%D', date.getDate().toString().padStart(2, '0')], // day 2 digits
['%H', date.getHours().toString().padStart(2, '0')], // hour 2 digits
['%N', date.getMinutes().toString().padStart(2, '0')], // minute 2 digits
['%S', date.getSeconds().toString().padStart(2, '0')], // second 2 digits
['%T', date.getMilliseconds().toString().padStart(3, '0')], // millisecond 3 digits
['%s', parseInt(playerTime)], // video position second n digits
['%t', (playerTime % 1).toString().slice(2, 5) || '000'], // video position millisecond 3 digits
['%i', videoId] // video id
]
let parsedString = pattern
for (const [key, value] of keywords) {
parsedString = parsedString.replaceAll(key, value)
}
const platform = process.platform
if (platform === 'win32') {
// https://www.boost.org/doc/libs/1_78_0/libs/filesystem/doc/portability_guide.htm
// https://stackoverflow.com/questions/1976007/
const noForbiddenChars = ['<', '>', ':', '"', '/', '|', '?', '*'].every(char => {
return parsedString.indexOf(char) === -1
})
if (!noForbiddenChars) {
reject(new Error('Forbidden Characters')) // use message as translation key
}
} else if (platform === 'darwin') {
// https://superuser.com/questions/204287/
if (parsedString.indexOf(':') !== -1) {
reject(new Error('Forbidden Characters'))
}
}
const dirChar = platform === 'win32' ? '\\' : '/'
let filename
if (parsedString.indexOf(dirChar) !== -1) {
const lastIndex = parsedString.lastIndexOf(dirChar)
filename = parsedString.substring(lastIndex + 1)
} else {
filename = parsedString
}
if (!filename) {
reject(new Error('Empty File Name'))
}
resolve(parsedString)
})
},
updateShowProgressBar ({ commit }, value) {
commit('setShowProgressBar', value)
},
getRegionData ({ commit }, payload) {
let fileData
/* eslint-disable-next-line */
const fileLocation = process.env.NODE_ENV === 'development' ? './static/geolocations/' : `${__dirname}/static/geolocations/`
if (fs.existsSync(`${fileLocation}${payload.locale}`)) {
fileData = fs.readFileSync(`${fileLocation}${payload.locale}/countries.json`)
} else {
fileData = fs.readFileSync(`${fileLocation}en-US/countries.json`)
}
const countries = JSON.parse(fileData).map((entry) => { return { id: entry.id, name: entry.name, code: entry.alpha2 } })
countries.sort((a, b) => { return a.id - b.id })
const regionNames = countries.map((entry) => { return entry.name })
const regionValues = countries.map((entry) => { return entry.code })
commit('setRegionNames', regionNames)
commit('setRegionValues', regionValues)
},
getVideoParamsFromUrl (_, url) {
/** @type {URL} */
let urlObject
const paramsObject = { videoId: null, timestamp: null, playlistId: null }
try {
urlObject = new URL(url)
} catch (e) {
return paramsObject
}
function extractParams(videoId) {
paramsObject.videoId = videoId
paramsObject.timestamp = urlObject.searchParams.get('t')
}
const extractors = [
// anything with /watch?v=
function() {
if (urlObject.pathname === '/watch' && urlObject.searchParams.has('v')) {
extractParams(urlObject.searchParams.get('v'))
paramsObject.playlistId = urlObject.searchParams.get('list')
return paramsObject
}
},
// youtu.be
function() {
if (urlObject.host === 'youtu.be' && urlObject.pathname.match(/^\/[A-Za-z0-9_-]+$/)) {
extractParams(urlObject.pathname.slice(1))
return paramsObject
}
},
// youtube.com/embed
function() {
if (urlObject.pathname.match(/^\/embed\/[A-Za-z0-9_-]+$/)) {
extractParams(urlObject.pathname.replace('/embed/', ''))
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_-]+$/)) {
extractParams(urlObject.pathname.slice('/cloudtube/video/'.length))
return paramsObject
}
}
]
return extractors.reduce((a, c) => a || c(), null) || paramsObject
},
getYoutubeUrlInfo ({ state }, urlStr) {
// Returns
// - urlType [String] `video`, `playlist`
//
// If `urlType` is "video"
// - videoId [String]
// - timestamp [String]
//
// If `urlType` is "playlist"
// - playlistId [String]
// - query [Object]
//
// If `urlType` is "search"
// - searchQuery [String]
// - query [Object]
//
// If `urlType` is "hashtag"
// Nothing else
//
// If `urlType` is "channel"
// - channelId [String]
//
// If `urlType` is "unknown"
// Nothing else
//
// If `urlType` is "invalid_url"
// Nothing else
const { videoId, timestamp, playlistId } = actions.getVideoParamsFromUrl(null, urlStr)
if (videoId) {
return {
urlType: 'video',
videoId,
playlistId,
timestamp
}
}
let url
try {
url = new URL(urlStr)
} catch {
return {
urlType: 'invalid_url'
}
}
let urlType = 'unknown'
const channelPattern =
/^\/(?:(?<type>channel|user|c)\/)?(?<channelId>[^/]+)(?:\/(join|featured|videos|playlists|about|community|channels))?\/?$/
const typePatterns = new Map([
['playlist', /^\/playlist\/?$/],
['search', /^\/results\/?$/],
['hashtag', /^\/hashtag\/([^/?&#]+)$/],
['channel', channelPattern]
])
for (const [type, pattern] of typePatterns) {
const matchFound = pattern.test(url.pathname)
if (matchFound) {
urlType = type
break
}
}
switch (urlType) {
case 'playlist': {
if (!url.searchParams.has('list')) {
throw new Error('Playlist: "list" field not found')
}
const playlistId = url.searchParams.get('list')
url.searchParams.delete('list')
const query = {}
for (const [param, value] of url.searchParams) {
query[param] = value
}
return {
urlType: 'playlist',
playlistId,
query
}
}
case 'search': {
if (!url.searchParams.has('search_query')) {
throw new Error('Search: "search_query" field not found')
}
const searchQuery = url.searchParams.get('search_query')
url.searchParams.delete('search_query')
const searchSettings = state.searchSettings
const query = {
sortBy: searchSettings.sortBy,
time: searchSettings.time,
type: searchSettings.type,
duration: searchSettings.duration
}
for (const [param, value] of url.searchParams) {
query[param] = value
}
return {
urlType: 'search',
searchQuery,
query
}
}
case 'hashtag': {
return {
urlType: 'hashtag'
}
}
/*
Using RegExp named capture groups from ES2018
To avoid access to specific captured value broken
Channel URL (ID-based)
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/about
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/channels
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/community
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/featured
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/join
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/playlists
https://www.youtube.com/channel/UCfMJ2MchTSW2kWaT0kK94Yw/videos
Custom URL
https://www.youtube.com/c/YouTubeCreators
https://www.youtube.com/c/YouTubeCreators/about
etc.
Legacy Username URL
https://www.youtube.com/user/ufoludek
https://www.youtube.com/user/ufoludek/about
etc.
*/
case 'channel': {
const match = url.pathname.match(channelPattern)
const channelId = match.groups.channelId
const idType = ['channel', 'user', 'c'].indexOf(match.groups.type) + 1
if (!channelId) {
throw new Error('Channel: could not extract id')
}
let subPath = null
switch (url.pathname.split('/').filter(i => i)[2]) {
case 'playlists':
subPath = 'playlists'
break
case 'channels':
case 'about':
subPath = 'about'
break
case 'community':
default:
subPath = 'videos'
break
}
return {
urlType: 'channel',
channelId,
idType,
subPath
}
}
default: {
// Unknown URL type
return {
urlType: 'unknown'
}
}
}
},
toLocalePublicationString ({ dispatch }, payload) {
if (payload.isLive) {
return '0' + i18n.t('Video.Watching')
} else if (payload.isUpcoming || payload.publishText === null) {
// the check for null is currently just an inferring of knowledge, because there is no other possibility left
return `${i18n.t('Video.Published.Upcoming')}: ${payload.publishText}`
} else if (payload.isRSS) {
return payload.publishText
}
const strings = payload.publishText.split(' ')
// filters out the streamed x hours ago and removes the streamed in order to keep the rest of the code working
if (strings[0].toLowerCase() === 'streamed') {
strings.shift()
}
const singular = (strings[0] === '1')
let unit
switch (strings[1].substring(0, 2)) {
case 'se':
if (singular) {
unit = i18n.t('Video.Published.Second')
} else {
unit = i18n.t('Video.Published.Seconds')
}
break
case 'mi':
if (singular) {
unit = i18n.t('Video.Published.Minute')
} else {
unit = i18n.t('Video.Published.Minutes')
}
break
case 'ho':
if (singular) {
unit = i18n.t('Video.Published.Hour')
} else {
unit = i18n.t('Video.Published.Hours')
}
break
case 'da':
if (singular) {
unit = i18n.t('Video.Published.Day')
} else {
unit = i18n.t('Video.Published.Days')
}
break
case 'we':
if (singular) {
unit = i18n.t('Video.Published.Week')
} else {
unit = i18n.t('Video.Published.Weeks')
}
break
case 'mo':
if (singular) {
unit = i18n.t('Video.Published.Month')
} else {
unit = i18n.t('Video.Published.Months')
}
break
case 'ye':
if (singular) {
unit = i18n.t('Video.Published.Year')
} else {
unit = i18n.t('Video.Published.Years')
}
break
}
return i18n.t('Video.Publicationtemplate', { number: strings[0], unit })
},
clearSessionSearchHistory ({ commit }) {
commit('setSessionSearchHistory', [])
},
showToast (_, payload) {
FtToastEvents.$emit('toast-open', payload.message, payload.action, payload.time)
},
showExternalPlayerUnsupportedActionToast: function ({ dispatch }, { externalPlayer, action }) {
dispatch('showToast', {
message: i18n.t('Video.External Player.UnsupportedActionTemplate', { externalPlayer, action })
})
},
getExternalPlayerCmdArgumentsData ({ commit }, payload) {
const fileName = 'external-player-map.json'
let fileData
/* eslint-disable-next-line */
const fileLocation = process.env.NODE_ENV === 'development' ? './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, nameTranslationKey: entry.nameTranslationKey, value: entry.value, cmdArguments: entry.cmdArguments }
})
const externalPlayerNames = externalPlayerMap.map((entry) => { return entry.name })
const externalPlayerNameTranslationKeys = externalPlayerMap.map((entry) => { return entry.nameTranslationKey })
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('setExternalPlayerNameTranslationKeys', externalPlayerNameTranslationKeys)
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
// Append custom user-defined arguments,
// or use the default ones specified for the external player.
if (typeof customArgs === 'string' && customArgs !== '') {
const custom = customArgs.split(';')
args.push(...custom)
} else if (typeof cmdArgs.defaultCustomArguments === 'string' && cmdArgs.defaultCustomArguments !== '') {
const defaultCustomArguments = cmdArgs.defaultCustomArguments.split(';')
args.push(...defaultCustomArguments)
}
if (payload.watchProgress > 0 && payload.watchProgress < payload.videoLength - 10) {
if (typeof cmdArgs.startOffset === 'string') {
args.push(`${cmdArgs.startOffset}${payload.watchProgress}`)
} else if (!ignoreWarnings) {
dispatch('showExternalPlayerUnsupportedActionToast', {
externalPlayer,
action: i18n.t('Video.External Player.Unsupported Actions.starting video at offset')
})
}
}
if (payload.playbackRate !== null) {
if (typeof cmdArgs.playbackRate === 'string') {
args.push(`${cmdArgs.playbackRate}${payload.playbackRate}`)
} else if (!ignoreWarnings) {
dispatch('showExternalPlayerUnsupportedActionToast', {
externalPlayer,
action: i18n.t('Video.External Player.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 if (!ignoreWarnings) {
dispatch('showExternalPlayerUnsupportedActionToast', {
externalPlayer,
action: i18n.t('Video.External Player.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 if (!ignoreWarnings) {
dispatch('showExternalPlayerUnsupportedActionToast', {
externalPlayer,
action: i18n.t('Video.External Player.Unsupported Actions.reversing playlists')
})
}
}
if (payload.playlistShuffle) {
if (typeof cmdArgs.playlistShuffle === 'string') {
args.push(cmdArgs.playlistShuffle)
} else if (!ignoreWarnings) {
dispatch('showExternalPlayerUnsupportedActionToast', {
externalPlayer,
action: i18n.t('Video.External Player.Unsupported Actions.shuffling playlists')
})
}
}
if (payload.playlistLoop) {
if (typeof cmdArgs.playlistLoop === 'string') {
args.push(cmdArgs.playlistLoop)
} else if (!ignoreWarnings) {
dispatch('showExternalPlayerUnsupportedActionToast', {
externalPlayer,
action: i18n.t('Video.External Player.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 !== '' && !ignoreWarnings) {
dispatch('showExternalPlayerUnsupportedActionToast', {
externalPlayer,
action: i18n.t('Video.External Player.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}`)
}
}
}
const videoOrPlaylist = payload.playlistId === null || payload.playlistId === ''
? i18n.t('Video.External Player.video')
: i18n.t('Video.External Player.playlist')
dispatch('showToast', {
message: i18n.t('Video.External Player.OpeningTemplate', { videoOrPlaylist, externalPlayer })
})
const { ipcRenderer } = require('electron')
ipcRenderer.send(IpcChannels.OPEN_IN_EXTERNAL_PLAYER, { executable, args })
}
}
const mutations = {
toggleSideNav (state) {
state.isSideNavOpen = !state.isSideNavOpen
},
setShowProgressBar (state, value) {
state.showProgressBar = value
},
setProgressBarPercentage (state, value) {
state.progressBarPercentage = value
},
setSessionSearchHistory (state, history) {
state.sessionSearchHistory = history
},
addToSessionSearchHistory (state, payload) {
const sameSearch = state.sessionSearchHistory.findIndex((search) => {
return search.query === payload.query && IsEqual(payload.searchSettings, search.searchSettings)
})
if (sameSearch !== -1) {
state.sessionSearchHistory[sameSearch].data = payload.data
state.sessionSearchHistory[sameSearch].nextPageRef = payload.nextPageRef
} else {
state.sessionSearchHistory.push(payload)
}
},
setPopularCache (state, value) {
state.popularCache = value
},
setTrendingCache (state, { value, page }) {
state.trendingCache[page] = value
},
setSearchSortBy (state, value) {
state.searchSettings.sortBy = value
},
setSearchTime (state, value) {
state.searchSettings.time = value
},
setSearchType (state, value) {
state.searchSettings.type = value
},
setSearchDuration (state, value) {
state.searchSettings.duration = value
},
setRegionNames (state, value) {
state.regionNames = value
},
setRegionValues (state, value) {
state.regionValues = value
},
setRecentBlogPosts (state, value) {
state.recentBlogPosts = value
},
setExternalPlayerNames (state, value) {
state.externalPlayerNames = value
},
setExternalPlayerNameTranslationKeys (state, value) {
state.externalPlayerNameTranslationKeys = value
},
setExternalPlayerValues (state, value) {
state.externalPlayerValues = value
},
setExternalPlayerCmdArguments (state, value) {
state.externalPlayerCmdArguments = value
}
}
export default {
state,
getters,
actions,
mutations
}