Add support for External Players (closes #418) (#1271)

* feat: add support for opening videos/playlists in external players (like mpv) #418

Signed-off-by: Randshot <randshot@norealm.xyz>

* feat: move external player settings into own section
feat: add warnings for when the external player doesn't support the current action (e.g. reversing playlists)
feat: add toggle in settings for ignoring unsupported action warnings

Signed-off-by: Randshot <randshot@norealm.xyz>

* improvement: do not append start offset argument when the watch progress is 0

Signed-off-by: Randshot <randshot@norealm.xyz>

* fix: fix undefined showToast error when clicking on the external player playlist button

Signed-off-by: Randshot <randshot@norealm.xyz>

* feat: add icon button for external player to watch-video-info (below video player) component
improvement: refactor the code for opening the external player into a separate function in utils.js

Signed-off-by: Randshot <randshot@norealm.xyz>

* feat: add support for ytdl protocol urls (supportsYtdlProtocol)
chore: fix lint error

Signed-off-by: Randshot <randshot@norealm.xyz>

* feat: add support for passing default playback rate to external player
improvement: add warning message for when the external player does not support starting playback at
             a given offset
chore: rename reverse, shuffle, and loopPlaylist fields for consistency

Signed-off-by: Randshot <randshot@norealm.xyz>

* feat: add setting for custom external player command line arguments

Signed-off-by: Randshot <randshot@norealm.xyz>

* chore: fix lint error

Signed-off-by: Randshot <randshot@norealm.xyz>

* improvement(watch-video-info.js): change the default for playlistId back to null (consistent with other occurrences)
improvement(utils.js/openInExternalPlayer): also check for empty playlistId string
fix(watch-video-info.js): fix merge error

Signed-off-by: Randshot <randshot@norealm.xyz>

* improvement(components/ft-list-video): check whether watch history is turned on, before adding a video to it
fix(store/utils): fix playlistReverse typo, causing `undefined` being set as a command line argument
fix(store/utils): check for 'string' type, instead of `null` and `undefined`
fix(views/Watch): fix getPlaylistIndex returning an incorrect index, when reverse was turned on
chore(locales/en-US): fix thumbnail and suppress typo
chore(locales/en_GB): fix thumbnail and suppress typo

Signed-off-by: Randshot <randshot@norealm.xyz>

* feat: pause player when opening video in external player

Signed-off-by: Randshot <randshot@norealm.xyz>

* feat(externalPlayer): refactor externalPlayerCmdArguments into a separate static file `static/external-player-map.json`
chore(components/ft-list-video): fix lint error

Signed-off-by: Randshot <randshot@norealm.xyz>

* Revert "feat: pause player when opening video in external player"

This reverts commit 28b4713334.

* feat: pause the app's player when opening video in external player

* This commit addresses above requested changes.

improvement(components/external-player-settings): move `externalPlayer` check to `ft-flex-box`
improvement(components/external-player-settings): use `update*` methods, instead of `handle*`

improvement(store/utils): move child_process invocation to `main/index.js` via IPC call to renderer
improvement(store/utils): use `dispatch` for calling actions
improvement(store/utils): get external player related settings directly in the action

improvement(renderer/App): move `checkExternalPlayer` call down into `usingElectron` if statement
fix(renderer/App): fix lint error

improvement(components/ft-list-playlist): remove unnecessary payload fields
fix(components/ft-list-playlist): fix typo in component name

improvement(components/ft-list-video): remove unnecessary payload fields

improvement(components/watch-video-info): remove unnecessary payload fields
improvement(views/Settings): add `usingElectron` condition

Signed-off-by: Randshot <randshot@norealm.xyz>

* fix(store/utils): fix toast message error

Signed-off-by: Randshot <randshot@norealm.xyz>

* fix(store/utils): fix a few code mess-ups

Co-authored-by: Svallinn <41585298+Svallinn@users.noreply.github.com>
This commit is contained in:
kuhaku 2021-06-13 15:31:43 +00:00 committed by GitHub
parent 003eeabb78
commit 52fa523df1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 641 additions and 9 deletions

View File

@ -4,6 +4,7 @@ import {
} from 'electron'
import Datastore from 'nedb'
import path from 'path'
import cp from 'child_process'
if (process.argv.includes('--version')) {
console.log(`v${app.getVersion()}`)
@ -397,6 +398,11 @@ function runApp() {
}
})
ipcMain.on('openInExternalPlayer', (_, payload) => {
const child = cp.spawn(payload.executable, payload.args, { detached: true, stdio: 'ignore' })
child.unref()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()

View File

@ -76,6 +76,9 @@ export default Vue.extend({
},
defaultProfile: function () {
return this.$store.getters.getDefaultProfile
},
externalPlayer: function () {
return this.$store.getters.getExternalPlayer
}
},
mounted: function () {
@ -87,8 +90,6 @@ export default Vue.extend({
this.checkThemeSettings()
await this.checkLocale()
this.dataReady = true
if (this.usingElectron) {
console.log('User is using Electron')
ipcRenderer = require('electron').ipcRenderer
@ -96,8 +97,11 @@ export default Vue.extend({
this.openAllLinksExternally()
this.enableOpenUrl()
this.setBoundsOnClose()
await this.checkExternalPlayer()
}
this.dataReady = true
setTimeout(() => {
this.checkForNewUpdates()
this.checkForNewBlogPosts()
@ -230,6 +234,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
@ -406,6 +418,7 @@ export default Vue.extend({
'getRegionData',
'getYoutubeUrlInfo',
'getLocale',
'getExternalPlayerCmdArgumentsData',
'setUpListenerToSyncSettings'
])
}

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

@ -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,
@ -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,6 +63,20 @@ 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')
@ -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
@ -178,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 () {
@ -185,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()
@ -365,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,
@ -437,6 +485,7 @@ export default Vue.extend({
...mapActions([
'showToast',
'toLocalePublicationString',
'openInExternalPlayer',
'updateHistory',
'removeFromHistory',
'addVideo',

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

@ -84,7 +84,23 @@ export default Vue.extend({
},
playlistId: {
type: String,
default: ''
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,
@ -224,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 () {
@ -243,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}` })
},
@ -388,6 +428,7 @@ export default Vue.extend({
...mapActions([
'showToast',
'openInExternalPlayer',
'updateProfile',
'addVideo',
'removeVideo',

View File

@ -70,6 +70,14 @@
:theme="favoriteIconTheme"
@click="toggleSave"
/>
<ft-icon-button
v-if="externalPlayer !== ''"
:title="$t('Video.External Player.OpenInTemplate').replace('$', externalPlayer)"
icon="external-link-alt"
class="option"
theme="secondary"
@click="handleExternalPlayer"
/>
<ft-icon-button
v-if="theatrePossible"
:title="$t('Toggle Theatre Mode')"

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

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

@ -25,5 +25,5 @@
width: 90%
@media only screen and (max-width: 460px)
.generalSettingsFlexBox, .playerSettingsFlexBox
.generalSettingsFlexBox, .playerSettingsFlexBox, .externalPlayerSettingsFlexBox
justify-content: flex-start

View File

@ -177,6 +177,10 @@ const state = {
displayVideoPlayButton: true,
enableSearchSuggestions: true,
enableSubtitles: true,
externalPlayer: '',
externalPlayerExecutable: '',
externalPlayerIgnoreWarnings: false,
externalPlayerCustomArgs: '',
forceLocalBackendForLegacy: false,
hideActiveSubscriptions: false,
hideChannelSubscriptions: false,

View File

@ -52,7 +52,10 @@ const state = {
'#FFAB00',
'#FF6D00',
'#DD2C00'
]
],
externalPlayerNames: [],
externalPlayerValues: [],
externalPlayerCmdArguments: {}
}
const getters = {
@ -102,6 +105,18 @@ const getters = {
getRecentBlogPosts () {
return state.recentBlogPosts
},
getExternalPlayerNames () {
return state.externalPlayerNames
},
getExternalPlayerValues () {
return state.externalPlayerValues
},
getExternalPlayerCmdArguments () {
return state.externalPlayerCmdArguments
}
}
@ -596,6 +611,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
})
}
}
@ -663,6 +853,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

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

View File

@ -4,6 +4,7 @@ import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import GeneralSettings from '../../components/general-settings/general-settings.vue'
import ThemeSettings from '../../components/theme-settings/theme-settings.vue'
import PlayerSettings from '../../components/player-settings/player-settings.vue'
import ExternalPlayerSettings from '../../components/external-player-settings/external-player-settings.vue'
import SubscriptionSettings from '../../components/subscription-settings/subscription-settings.vue'
import PrivacySettings from '../../components/privacy-settings/privacy-settings.vue'
import DataSettings from '../../components/data-settings/data-settings.vue'
@ -19,11 +20,17 @@ export default Vue.extend({
'general-settings': GeneralSettings,
'theme-settings': ThemeSettings,
'player-settings': PlayerSettings,
'external-player-settings': ExternalPlayerSettings,
'subscription-settings': SubscriptionSettings,
'privacy-settings': PrivacySettings,
'data-settings': DataSettings,
'distraction-settings': DistractionSettings,
'proxy-settings': ProxySettings,
'sponsor-block-settings': SponsorBlockSettings
},
computed: {
usingElectron: function () {
return this.$store.getters.getUsingElectron
}
}
})

View File

@ -3,6 +3,7 @@
<general-settings />
<theme-settings />
<player-settings />
<external-player-settings v-if="usingElectron" />
<subscription-settings />
<distraction-settings />
<privacy-settings />

View File

@ -1174,6 +1174,13 @@ export default Vue.extend({
}))
},
pausePlayer: function () {
const player = this.$refs.videoPlayer.player
if (player && !player.paused()) {
player.pause()
}
},
getWatchedProgress: function () {
return this.$refs.videoPlayer && this.$refs.videoPlayer.player ? this.$refs.videoPlayer.player.currentTime() : 0
},
@ -1182,6 +1189,26 @@ export default Vue.extend({
return Math.floor(this.getWatchedProgress())
},
getPlaylistIndex: function () {
return this.$refs.watchVideoPlaylist
? this.getPlaylistReverse()
? this.$refs.watchVideoPlaylist.playlistItems.length - this.$refs.watchVideoPlaylist.currentVideoIndex
: this.$refs.watchVideoPlaylist.currentVideoIndex - 1
: -1
},
getPlaylistReverse: function () {
return this.$refs.watchVideoPlaylist ? this.$refs.watchVideoPlaylist.reversePlaylist : false
},
getPlaylistShuffle: function () {
return this.$refs.watchVideoPlaylist ? this.$refs.watchVideoPlaylist.shuffleEnabled : false
},
getPlaylistLoop: function () {
return this.$refs.watchVideoPlaylist ? this.$refs.watchVideoPlaylist.loopEnabled : false
},
updateTitle: function () {
document.title = `${this.videoTitle} - FreeTube`
},

View File

@ -82,14 +82,19 @@
:is-live="isLive"
:is-upcoming="isUpcoming"
:download-links="downloadLinks"
:watching-playlist="watchingPlaylist"
:playlist-id="playlistId"
:watching-playlist="watchingPlaylist"
:get-playlist-index="getPlaylistIndex"
:get-playlist-reverse="getPlaylistReverse"
:get-playlist-shuffle="getPlaylistShuffle"
:get-playlist-loop="getPlaylistLoop"
:theatre-possible="theatrePossible"
:length-seconds="videoLengthSeconds"
:video-thumbnail="thumbnail"
class="watchVideo"
:class="{ theatreWatchVideo: useTheatreMode }"
@theatre-mode="toggleTheatreMode"
@pause-player="pausePlayer"
/>
<watch-video-description
v-if="!isLoading"
@ -125,6 +130,7 @@
:video-id="videoId"
class="watchVideoSideBar watchVideoPlaylist"
:class="{ theatrePlaylist: useTheatreMode }"
@pause-player="pausePlayer"
/>
<watch-video-recommendations
v-if="!isLoading"

View File

@ -0,0 +1,23 @@
[
{
"name": "None",
"value": "",
"cmdArguments": null
},
{
"name": "mpv",
"value": "mpv",
"cmdArguments": {
"defaultExecutable": "mpv",
"supportsYtdlProtocol": true,
"videoUrl": "",
"playlistUrl": "",
"startOffset": "--start=",
"playbackRate": "--speed=",
"playlistIndex": "--playlist-start=",
"playlistReverse": null,
"playlistShuffle": "--shuffle",
"playlistLoop": "--loop-playlist"
}
}
]

View File

@ -195,6 +195,12 @@ Settings:
1440p: 1440p
4k: 4k
8k: 8k
External Player Settings:
External Player Settings: External Player Settings
External Player: External Player
Ignore Unsupported Action Warnings: Ignore Unsupported Action Warnings
Custom External Player Executable: Custom External Player Executable
Custom External Player Arguments: Custom External Player Arguments
Privacy Settings:
Privacy Settings: Privacy Settings
Remember History: Remember History
@ -487,6 +493,23 @@ Video:
self-promotion: self-promotion
interaction: interaction
music offtopic: music offtopic
External Player:
# $ is replaced with the external player
OpenInTemplate: Open in $
video: video
playlist: playlist
# $ is replaced with the current context (see video/playlist above) and % the external player setting
OpeningTemplate: Opening $ in %...
# $ is replaced with the external player and % with the unsupported action
UnsupportedActionTemplate: '$ does not support: %'
Unsupported Actions:
starting video at offset: starting video at offset
setting a playback rate: setting a playback rate
opening playlists: opening playlists
opening specific video in a playlist (falling back to opening the video): opening specific video in a playlist (falling back to opening the video)
reversing playlists: reversing playlists
shuffling playlists: shuffling playlists
looping playlists: looping playlists
#& Videos
Videos:
#& Sort By
@ -586,6 +609,16 @@ Tooltips:
Default Video Format: Set the formats used when a video plays. DASH formats can
play higher qualities. Legacy formats are limited to a max of 720p but use less
bandwidth. Audio formats are audio only streams.
External Player Settings:
External Player: Choosing an external player will display an icon, for opening the
video (playlist if supported) in the external player, on the thumbnail.
Custom External Player Executable: By default, FreeTube will assume that the chosen external
player can be found via the PATH environment variable. If needed, a custom path can
be set here.
Ignore Warnings: Suppress warnings for when the current external player does not support
the current action (e.g. reversing playlists, etc.).
Custom External Player Arguments: Any custom command line arguments, separated by semicolons (';'),
you want to be passed to the external player.
Subscription Settings:
Fetch Feeds from RSS: When enabled, FreeTube will use RSS instead of its default
method for grabbing your subscription feed. RSS is faster and prevents IP blocking,

View File

@ -130,6 +130,8 @@ Settings:
#! List countries
View all Invidious instance information: View all Invidious instance information
System Default: System Default
External Player: External Player
External Player Executable: Custom External Player Executable
Theme Settings:
Theme Settings: 'Theme Settings'
Match Top Bar with Main Color: 'Match top bar with main colour'
@ -193,6 +195,12 @@ Settings:
Playlist Next Video Interval: Playlist Next Video Interval
Next Video Interval: Next Video Interval
Display Play Button In Video Player: Display Play Button In Video Player
External Player Settings:
External Player Settings: External Player Settings
External Player: External Player
Ignore Unsupported Action Warnings: Ignore Unsupported Action Warnings
Custom External Player Executable: Custom External Player Executable
Custom External Player Arguments: Custom External Player Arguments
Privacy Settings:
Privacy Settings: 'Privacy Settings'
Remember History: 'Remember History'
@ -529,6 +537,23 @@ Video:
intro: intro
sponsor: sponsor
Skipped segment: Skipped segment
External Player:
# $ is replaced with the external player
OpenInTemplate: Open in $
video: video
playlist: playlist
# $ is replaced with the current context (see video/playlist above) and % the external player setting
OpeningTemplate: Opening $ in %...
# $ is replaced with the external player and % with the unsupported action
UnsupportedActionTemplate: '$ does not support: %'
Unsupported Actions:
starting video at offset: starting video at offset
setting a playback rate: setting a playback rate
opening playlists: opening playlists
opening specific video in a playlist (falling back to opening the video): opening specific video in a playlist (falling back to opening the video)
reversing playlists: reversing playlists
shuffling playlists: shuffling playlists
looping playlists: looping playlists
Videos:
#& Sort By
Sort By:
@ -654,6 +679,16 @@ Tooltips:
Preferred API Backend: Choose the back-end that FreeTube uses to obtain data.
The local API is a built-in extractor. The Invidious API requires an Invidious
server to connect to.
External Player Settings:
External Player: Choosing an external player will display an icon, for opening the
video (playlist if supported) in the external player, on the thumbnail.
Custom External Player Executable: By default, FreeTube will assume that the chosen external
player can be found via the PATH environment variable. If needed, a custom path can
be set here.
Ignore Warnings: Suppress warnings for when the current external player does not support
the current action (e.g. reversing playlists, etc.).
Custom External Player Arguments: Any custom command line arguments, separated by semicolons (';'),
you want to be passed to the external player.
Privacy Settings:
Remove Video Meta Files: When enabled, FreeTube automatically deletes meta files
created during video playback, when the watch page is closed.