* 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 28b4713334bf941be9e403abf517bb4b89beb04f. * 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:
parent
003eeabb78
commit
52fa523df1
|
@ -4,6 +4,7 @@ import {
|
||||||
} from 'electron'
|
} from 'electron'
|
||||||
import Datastore from 'nedb'
|
import Datastore from 'nedb'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import cp from 'child_process'
|
||||||
|
|
||||||
if (process.argv.includes('--version')) {
|
if (process.argv.includes('--version')) {
|
||||||
console.log(`v${app.getVersion()}`)
|
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', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit()
|
app.quit()
|
||||||
|
|
|
@ -76,6 +76,9 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
defaultProfile: function () {
|
defaultProfile: function () {
|
||||||
return this.$store.getters.getDefaultProfile
|
return this.$store.getters.getDefaultProfile
|
||||||
|
},
|
||||||
|
externalPlayer: function () {
|
||||||
|
return this.$store.getters.getExternalPlayer
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
|
@ -87,8 +90,6 @@ export default Vue.extend({
|
||||||
this.checkThemeSettings()
|
this.checkThemeSettings()
|
||||||
await this.checkLocale()
|
await this.checkLocale()
|
||||||
|
|
||||||
this.dataReady = true
|
|
||||||
|
|
||||||
if (this.usingElectron) {
|
if (this.usingElectron) {
|
||||||
console.log('User is using Electron')
|
console.log('User is using Electron')
|
||||||
ipcRenderer = require('electron').ipcRenderer
|
ipcRenderer = require('electron').ipcRenderer
|
||||||
|
@ -96,8 +97,11 @@ export default Vue.extend({
|
||||||
this.openAllLinksExternally()
|
this.openAllLinksExternally()
|
||||||
this.enableOpenUrl()
|
this.enableOpenUrl()
|
||||||
this.setBoundsOnClose()
|
this.setBoundsOnClose()
|
||||||
|
await this.checkExternalPlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.dataReady = true
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.checkForNewUpdates()
|
this.checkForNewUpdates()
|
||||||
this.checkForNewBlogPosts()
|
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) {
|
handleUpdateBannerClick: function (response) {
|
||||||
if (response !== false) {
|
if (response !== false) {
|
||||||
this.showReleaseNotes = true
|
this.showReleaseNotes = true
|
||||||
|
@ -406,6 +418,7 @@ export default Vue.extend({
|
||||||
'getRegionData',
|
'getRegionData',
|
||||||
'getYoutubeUrlInfo',
|
'getYoutubeUrlInfo',
|
||||||
'getLocale',
|
'getLocale',
|
||||||
|
'getExternalPlayerCmdArgumentsData',
|
||||||
'setUpListenerToSyncSettings'
|
'setUpListenerToSyncSettings'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import { mapActions } from 'vuex'
|
||||||
|
import FtCard from '../ft-card/ft-card.vue'
|
||||||
|
import FtSelect from '../ft-select/ft-select.vue'
|
||||||
|
import FtInput from '../ft-input/ft-input.vue'
|
||||||
|
import FtToggleSwitch from '../ft-toggle-switch/ft-toggle-switch.vue'
|
||||||
|
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'ExternalPlayerSettings',
|
||||||
|
components: {
|
||||||
|
'ft-card': FtCard,
|
||||||
|
'ft-select': FtSelect,
|
||||||
|
'ft-input': FtInput,
|
||||||
|
'ft-toggle-switch': FtToggleSwitch,
|
||||||
|
'ft-flex-box': FtFlexBox
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isDev: function () {
|
||||||
|
return process.env.NODE_ENV === 'development'
|
||||||
|
},
|
||||||
|
|
||||||
|
externalPlayerNames: function () {
|
||||||
|
return this.$store.getters.getExternalPlayerNames
|
||||||
|
},
|
||||||
|
externalPlayerValues: function () {
|
||||||
|
return this.$store.getters.getExternalPlayerValues
|
||||||
|
},
|
||||||
|
externalPlayer: function () {
|
||||||
|
return this.$store.getters.getExternalPlayer
|
||||||
|
},
|
||||||
|
externalPlayerExecutable: function () {
|
||||||
|
return this.$store.getters.getExternalPlayerExecutable
|
||||||
|
},
|
||||||
|
externalPlayerIgnoreWarnings: function () {
|
||||||
|
return this.$store.getters.getExternalPlayerIgnoreWarnings
|
||||||
|
},
|
||||||
|
externalPlayerCustomArgs: function () {
|
||||||
|
return this.$store.getters.getExternalPlayerCustomArgs
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions([
|
||||||
|
'updateExternalPlayer',
|
||||||
|
'updateExternalPlayerExecutable',
|
||||||
|
'updateExternalPlayerIgnoreWarnings',
|
||||||
|
'updateExternalPlayerCustomArgs'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1 @@
|
||||||
|
@use "../../sass-partials/settings"
|
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<ft-card
|
||||||
|
class="relative card"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="videoTitle"
|
||||||
|
>
|
||||||
|
{{ $t("Settings.External Player Settings.External Player Settings") }}
|
||||||
|
</h3>
|
||||||
|
<div class="switchColumnGrid">
|
||||||
|
<div class="switchColumn">
|
||||||
|
<ft-select
|
||||||
|
:placeholder="$t('Settings.External Player Settings.External Player')"
|
||||||
|
:value="externalPlayer"
|
||||||
|
:select-names="externalPlayerNames"
|
||||||
|
:select-values="externalPlayerValues"
|
||||||
|
:tooltip="$t('Tooltips.External Player Settings.External Player')"
|
||||||
|
@change="updateExternalPlayer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="switchColumn">
|
||||||
|
<ft-toggle-switch
|
||||||
|
:label="$t('Settings.External Player Settings.Ignore Unsupported Action Warnings')"
|
||||||
|
:default-value="externalPlayerIgnoreWarnings"
|
||||||
|
:compact="true"
|
||||||
|
:tooltip="$t('Tooltips.External Player Settings.Ignore Warnings')"
|
||||||
|
@change="updateExternalPlayerIgnoreWarnings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ft-flex-box
|
||||||
|
v-if="externalPlayer !== ''"
|
||||||
|
class="externalPlayerSettingsFlexBox"
|
||||||
|
>
|
||||||
|
<ft-input
|
||||||
|
:placeholder="$t('Settings.External Player Settings.Custom External Player Executable')"
|
||||||
|
:show-arrow="false"
|
||||||
|
:show-label="true"
|
||||||
|
:value="externalPlayerExecutable"
|
||||||
|
:tooltip="$t('Tooltips.External Player Settings.Custom External Player Executable')"
|
||||||
|
@input="updateExternalPlayerExecutable"
|
||||||
|
/>
|
||||||
|
<ft-input
|
||||||
|
:placeholder="$t('Settings.External Player Settings.Custom External Player Arguments')"
|
||||||
|
:show-arrow="false"
|
||||||
|
:show-label="true"
|
||||||
|
:value="externalPlayerCustomArgs"
|
||||||
|
:tooltip="$t('Tooltips.External Player Settings.Custom External Player Arguments')"
|
||||||
|
@input="updateExternalPlayerCustomArgs"
|
||||||
|
/>
|
||||||
|
</ft-flex-box>
|
||||||
|
</ft-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./external-player-settings.js" />
|
||||||
|
<style scoped lang="sass" src="./external-player-settings.sass" />
|
|
@ -1,7 +1,12 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
|
||||||
|
import { mapActions } from 'vuex'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'FtListVideo',
|
name: 'FtListPlaylist',
|
||||||
|
components: {
|
||||||
|
'ft-icon-button': FtIconButton
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -40,6 +45,14 @@ export default Vue.extend({
|
||||||
let id = this.channelLink.replace('https://www.youtube.com/user/', '')
|
let id = this.channelLink.replace('https://www.youtube.com/user/', '')
|
||||||
id = id.replace('https://www.youtube.com/channel/', '')
|
id = id.replace('https://www.youtube.com/channel/', '')
|
||||||
return id
|
return id
|
||||||
|
},
|
||||||
|
|
||||||
|
externalPlayer: function () {
|
||||||
|
return this.$store.getters.getExternalPlayer
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultPlayback: function () {
|
||||||
|
return this.$store.getters.getDefaultPlayback
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
|
@ -50,6 +63,20 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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 () {
|
parseInvidiousData: function () {
|
||||||
this.title = this.data.title
|
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.invidiousInstance).replace('hqdefault', 'mqdefault')
|
||||||
|
@ -70,6 +97,10 @@ export default Vue.extend({
|
||||||
this.channelLink = this.data.owner.url
|
this.channelLink = this.data.owner.url
|
||||||
this.playlistLink = this.data.url
|
this.playlistLink = this.data.url
|
||||||
this.videoCount = this.data.length
|
this.videoCount = this.data.length
|
||||||
}
|
},
|
||||||
|
|
||||||
|
...mapActions([
|
||||||
|
'openInExternalPlayer'
|
||||||
|
])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,6 +23,16 @@
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="info">
|
<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
|
<router-link
|
||||||
class="title"
|
class="title"
|
||||||
:to="`/playlist/${playlistId}`"
|
:to="`/playlist/${playlistId}`"
|
||||||
|
|
|
@ -16,6 +16,22 @@ export default Vue.extend({
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
playlistIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
playlistReverse: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
playlistShuffle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
playlistLoop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
forceListType: {
|
forceListType: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
|
@ -178,6 +194,18 @@ export default Vue.extend({
|
||||||
|
|
||||||
favoriteIconTheme: function () {
|
favoriteIconTheme: function () {
|
||||||
return this.inFavoritesPlaylist ? 'base favorite' : 'base'
|
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 () {
|
mounted: function () {
|
||||||
|
@ -185,6 +213,26 @@ export default Vue.extend({
|
||||||
this.checkIfWatched()
|
this.checkIfWatched()
|
||||||
},
|
},
|
||||||
methods: {
|
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 () {
|
toggleSave: function () {
|
||||||
if (this.inFavoritesPlaylist) {
|
if (this.inFavoritesPlaylist) {
|
||||||
this.removeFromPlaylist()
|
this.removeFromPlaylist()
|
||||||
|
@ -365,7 +413,7 @@ export default Vue.extend({
|
||||||
title: this.title,
|
title: this.title,
|
||||||
author: this.channelName,
|
author: this.channelName,
|
||||||
authorId: this.channelId,
|
authorId: this.channelId,
|
||||||
published: this.publishedText.split(',')[0],
|
published: this.publishedText ? this.publishedText.split(',')[0] : this.publishedText,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
viewCount: this.viewCount,
|
viewCount: this.viewCount,
|
||||||
lengthSeconds: this.data.lengthSeconds,
|
lengthSeconds: this.data.lengthSeconds,
|
||||||
|
@ -437,6 +485,7 @@ export default Vue.extend({
|
||||||
...mapActions([
|
...mapActions([
|
||||||
'showToast',
|
'showToast',
|
||||||
'toLocalePublicationString',
|
'toLocalePublicationString',
|
||||||
|
'openInExternalPlayer',
|
||||||
'updateHistory',
|
'updateHistory',
|
||||||
'removeFromHistory',
|
'removeFromHistory',
|
||||||
'addVideo',
|
'addVideo',
|
||||||
|
|
|
@ -31,6 +31,16 @@
|
||||||
>
|
>
|
||||||
{{ isLive ? $t("Video.Live") : duration }}
|
{{ isLive ? $t("Video.Live") : duration }}
|
||||||
</div>
|
</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
|
<ft-icon-button
|
||||||
v-if="!isLive"
|
v-if="!isLive"
|
||||||
:title="$t('Video.Save Video')"
|
:title="$t('Video.Save Video')"
|
||||||
|
|
|
@ -84,7 +84,23 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
playlistId: {
|
playlistId: {
|
||||||
type: String,
|
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: {
|
theatrePossible: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -224,6 +240,14 @@ export default Vue.extend({
|
||||||
} else {
|
} else {
|
||||||
return this.$t('Video.Published on')
|
return this.$t('Video.Published on')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
externalPlayer: function () {
|
||||||
|
return this.$store.getters.getExternalPlayer
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultPlayback: function () {
|
||||||
|
return this.$store.getters.getDefaultPlayback
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
|
@ -243,6 +267,22 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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 () {
|
goToChannel: function () {
|
||||||
this.$router.push({ path: `/channel/${this.channelId}` })
|
this.$router.push({ path: `/channel/${this.channelId}` })
|
||||||
},
|
},
|
||||||
|
@ -388,6 +428,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
...mapActions([
|
...mapActions([
|
||||||
'showToast',
|
'showToast',
|
||||||
|
'openInExternalPlayer',
|
||||||
'updateProfile',
|
'updateProfile',
|
||||||
'addVideo',
|
'addVideo',
|
||||||
'removeVideo',
|
'removeVideo',
|
||||||
|
|
|
@ -70,6 +70,14 @@
|
||||||
:theme="favoriteIconTheme"
|
:theme="favoriteIconTheme"
|
||||||
@click="toggleSave"
|
@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
|
<ft-icon-button
|
||||||
v-if="theatrePossible"
|
v-if="theatrePossible"
|
||||||
:title="$t('Toggle Theatre Mode')"
|
:title="$t('Toggle Theatre Mode')"
|
||||||
|
|
|
@ -83,8 +83,13 @@
|
||||||
<ft-list-video
|
<ft-list-video
|
||||||
:data="item"
|
:data="item"
|
||||||
:playlist-id="playlistId"
|
:playlist-id="playlistId"
|
||||||
|
:playlist-index="reversePlaylist ? playlistItems.length - index - 1 : index"
|
||||||
|
:playlist-reverse="reversePlaylist"
|
||||||
|
:playlist-shuffle="shuffleEnabled"
|
||||||
|
:playlist-loop="loopEnabled"
|
||||||
appearance="watchPlaylistItem"
|
appearance="watchPlaylistItem"
|
||||||
force-list-type="list"
|
force-list-type="list"
|
||||||
|
@pause-player="$emit('pause-player')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -89,6 +89,13 @@ $thumbnail-overlay-opacity: 0.85
|
||||||
@include is-watch-playlist-item
|
@include is-watch-playlist-item
|
||||||
font-size: 12px
|
font-size: 12px
|
||||||
|
|
||||||
|
.externalPlayerIcon
|
||||||
|
position: absolute
|
||||||
|
bottom: 4px
|
||||||
|
left: 4px
|
||||||
|
font-size: 17px
|
||||||
|
opacity: $thumbnail-overlay-opacity
|
||||||
|
|
||||||
.favoritesIcon
|
.favoritesIcon
|
||||||
position: absolute
|
position: absolute
|
||||||
top: 3px
|
top: 3px
|
||||||
|
@ -144,6 +151,9 @@ $thumbnail-overlay-opacity: 0.85
|
||||||
.optionsButton
|
.optionsButton
|
||||||
float: right // ohhhh man, float was finally the right choice for something
|
float: right // ohhhh man, float was finally the right choice for something
|
||||||
|
|
||||||
|
.externalPlayerButton
|
||||||
|
float: right
|
||||||
|
|
||||||
.title
|
.title
|
||||||
font-size: 20px
|
font-size: 20px
|
||||||
@include low-contrast-when-watched(var(--primary-text-color))
|
@include low-contrast-when-watched(var(--primary-text-color))
|
||||||
|
|
|
@ -25,5 +25,5 @@
|
||||||
width: 90%
|
width: 90%
|
||||||
|
|
||||||
@media only screen and (max-width: 460px)
|
@media only screen and (max-width: 460px)
|
||||||
.generalSettingsFlexBox, .playerSettingsFlexBox
|
.generalSettingsFlexBox, .playerSettingsFlexBox, .externalPlayerSettingsFlexBox
|
||||||
justify-content: flex-start
|
justify-content: flex-start
|
||||||
|
|
|
@ -177,6 +177,10 @@ const state = {
|
||||||
displayVideoPlayButton: true,
|
displayVideoPlayButton: true,
|
||||||
enableSearchSuggestions: true,
|
enableSearchSuggestions: true,
|
||||||
enableSubtitles: true,
|
enableSubtitles: true,
|
||||||
|
externalPlayer: '',
|
||||||
|
externalPlayerExecutable: '',
|
||||||
|
externalPlayerIgnoreWarnings: false,
|
||||||
|
externalPlayerCustomArgs: '',
|
||||||
forceLocalBackendForLegacy: false,
|
forceLocalBackendForLegacy: false,
|
||||||
hideActiveSubscriptions: false,
|
hideActiveSubscriptions: false,
|
||||||
hideChannelSubscriptions: false,
|
hideChannelSubscriptions: false,
|
||||||
|
|
|
@ -52,7 +52,10 @@ const state = {
|
||||||
'#FFAB00',
|
'#FFAB00',
|
||||||
'#FF6D00',
|
'#FF6D00',
|
||||||
'#DD2C00'
|
'#DD2C00'
|
||||||
]
|
],
|
||||||
|
externalPlayerNames: [],
|
||||||
|
externalPlayerValues: [],
|
||||||
|
externalPlayerCmdArguments: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getters = {
|
const getters = {
|
||||||
|
@ -102,6 +105,18 @@ const getters = {
|
||||||
|
|
||||||
getRecentBlogPosts () {
|
getRecentBlogPosts () {
|
||||||
return state.recentBlogPosts
|
return state.recentBlogPosts
|
||||||
|
},
|
||||||
|
|
||||||
|
getExternalPlayerNames () {
|
||||||
|
return state.externalPlayerNames
|
||||||
|
},
|
||||||
|
|
||||||
|
getExternalPlayerValues () {
|
||||||
|
return state.externalPlayerValues
|
||||||
|
},
|
||||||
|
|
||||||
|
getExternalPlayerCmdArguments () {
|
||||||
|
return state.externalPlayerCmdArguments
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -596,6 +611,181 @@ const actions = {
|
||||||
|
|
||||||
showToast (_, payload) {
|
showToast (_, payload) {
|
||||||
FtToastEvents.$emit('toast-open', payload.message, payload.action, payload.time)
|
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) {
|
setRecentBlogPosts (state, value) {
|
||||||
state.recentBlogPosts = value
|
state.recentBlogPosts = value
|
||||||
|
},
|
||||||
|
|
||||||
|
setExternalPlayerNames (state, value) {
|
||||||
|
state.externalPlayerNames = value
|
||||||
|
},
|
||||||
|
|
||||||
|
setExternalPlayerValues (state, value) {
|
||||||
|
state.externalPlayerValues = value
|
||||||
|
},
|
||||||
|
|
||||||
|
setExternalPlayerCmdArguments (state, value) {
|
||||||
|
state.externalPlayerCmdArguments = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
:key="index"
|
:key="index"
|
||||||
:data="item"
|
:data="item"
|
||||||
:playlist-id="playlistId"
|
:playlist-id="playlistId"
|
||||||
|
:playlist-index="index"
|
||||||
appearance="result"
|
appearance="result"
|
||||||
force-list-type="list"
|
force-list-type="list"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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 GeneralSettings from '../../components/general-settings/general-settings.vue'
|
||||||
import ThemeSettings from '../../components/theme-settings/theme-settings.vue'
|
import ThemeSettings from '../../components/theme-settings/theme-settings.vue'
|
||||||
import PlayerSettings from '../../components/player-settings/player-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 SubscriptionSettings from '../../components/subscription-settings/subscription-settings.vue'
|
||||||
import PrivacySettings from '../../components/privacy-settings/privacy-settings.vue'
|
import PrivacySettings from '../../components/privacy-settings/privacy-settings.vue'
|
||||||
import DataSettings from '../../components/data-settings/data-settings.vue'
|
import DataSettings from '../../components/data-settings/data-settings.vue'
|
||||||
|
@ -19,11 +20,17 @@ export default Vue.extend({
|
||||||
'general-settings': GeneralSettings,
|
'general-settings': GeneralSettings,
|
||||||
'theme-settings': ThemeSettings,
|
'theme-settings': ThemeSettings,
|
||||||
'player-settings': PlayerSettings,
|
'player-settings': PlayerSettings,
|
||||||
|
'external-player-settings': ExternalPlayerSettings,
|
||||||
'subscription-settings': SubscriptionSettings,
|
'subscription-settings': SubscriptionSettings,
|
||||||
'privacy-settings': PrivacySettings,
|
'privacy-settings': PrivacySettings,
|
||||||
'data-settings': DataSettings,
|
'data-settings': DataSettings,
|
||||||
'distraction-settings': DistractionSettings,
|
'distraction-settings': DistractionSettings,
|
||||||
'proxy-settings': ProxySettings,
|
'proxy-settings': ProxySettings,
|
||||||
'sponsor-block-settings': SponsorBlockSettings
|
'sponsor-block-settings': SponsorBlockSettings
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
usingElectron: function () {
|
||||||
|
return this.$store.getters.getUsingElectron
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<general-settings />
|
<general-settings />
|
||||||
<theme-settings />
|
<theme-settings />
|
||||||
<player-settings />
|
<player-settings />
|
||||||
|
<external-player-settings v-if="usingElectron" />
|
||||||
<subscription-settings />
|
<subscription-settings />
|
||||||
<distraction-settings />
|
<distraction-settings />
|
||||||
<privacy-settings />
|
<privacy-settings />
|
||||||
|
|
|
@ -1174,6 +1174,13 @@ export default Vue.extend({
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pausePlayer: function () {
|
||||||
|
const player = this.$refs.videoPlayer.player
|
||||||
|
if (player && !player.paused()) {
|
||||||
|
player.pause()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
getWatchedProgress: function () {
|
getWatchedProgress: function () {
|
||||||
return this.$refs.videoPlayer && this.$refs.videoPlayer.player ? this.$refs.videoPlayer.player.currentTime() : 0
|
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())
|
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 () {
|
updateTitle: function () {
|
||||||
document.title = `${this.videoTitle} - FreeTube`
|
document.title = `${this.videoTitle} - FreeTube`
|
||||||
},
|
},
|
||||||
|
|
|
@ -82,14 +82,19 @@
|
||||||
:is-live="isLive"
|
:is-live="isLive"
|
||||||
:is-upcoming="isUpcoming"
|
:is-upcoming="isUpcoming"
|
||||||
:download-links="downloadLinks"
|
:download-links="downloadLinks"
|
||||||
:watching-playlist="watchingPlaylist"
|
|
||||||
:playlist-id="playlistId"
|
: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"
|
:theatre-possible="theatrePossible"
|
||||||
:length-seconds="videoLengthSeconds"
|
:length-seconds="videoLengthSeconds"
|
||||||
:video-thumbnail="thumbnail"
|
:video-thumbnail="thumbnail"
|
||||||
class="watchVideo"
|
class="watchVideo"
|
||||||
:class="{ theatreWatchVideo: useTheatreMode }"
|
:class="{ theatreWatchVideo: useTheatreMode }"
|
||||||
@theatre-mode="toggleTheatreMode"
|
@theatre-mode="toggleTheatreMode"
|
||||||
|
@pause-player="pausePlayer"
|
||||||
/>
|
/>
|
||||||
<watch-video-description
|
<watch-video-description
|
||||||
v-if="!isLoading"
|
v-if="!isLoading"
|
||||||
|
@ -125,6 +130,7 @@
|
||||||
:video-id="videoId"
|
:video-id="videoId"
|
||||||
class="watchVideoSideBar watchVideoPlaylist"
|
class="watchVideoSideBar watchVideoPlaylist"
|
||||||
:class="{ theatrePlaylist: useTheatreMode }"
|
:class="{ theatrePlaylist: useTheatreMode }"
|
||||||
|
@pause-player="pausePlayer"
|
||||||
/>
|
/>
|
||||||
<watch-video-recommendations
|
<watch-video-recommendations
|
||||||
v-if="!isLoading"
|
v-if="!isLoading"
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -195,6 +195,12 @@ Settings:
|
||||||
1440p: 1440p
|
1440p: 1440p
|
||||||
4k: 4k
|
4k: 4k
|
||||||
8k: 8k
|
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: Privacy Settings
|
Privacy Settings: Privacy Settings
|
||||||
Remember History: Remember History
|
Remember History: Remember History
|
||||||
|
@ -487,6 +493,23 @@ Video:
|
||||||
self-promotion: self-promotion
|
self-promotion: self-promotion
|
||||||
interaction: interaction
|
interaction: interaction
|
||||||
music offtopic: music offtopic
|
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
|
||||||
Videos:
|
Videos:
|
||||||
#& Sort By
|
#& Sort By
|
||||||
|
@ -586,6 +609,16 @@ Tooltips:
|
||||||
Default Video Format: Set the formats used when a video plays. DASH formats can
|
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
|
play higher qualities. Legacy formats are limited to a max of 720p but use less
|
||||||
bandwidth. Audio formats are audio only streams.
|
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:
|
Subscription Settings:
|
||||||
Fetch Feeds from RSS: When enabled, FreeTube will use RSS instead of its default
|
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,
|
method for grabbing your subscription feed. RSS is faster and prevents IP blocking,
|
||||||
|
|
|
@ -130,6 +130,8 @@ Settings:
|
||||||
#! List countries
|
#! List countries
|
||||||
View all Invidious instance information: View all Invidious instance information
|
View all Invidious instance information: View all Invidious instance information
|
||||||
System Default: System Default
|
System Default: System Default
|
||||||
|
External Player: External Player
|
||||||
|
External Player Executable: Custom External Player Executable
|
||||||
Theme Settings:
|
Theme Settings:
|
||||||
Theme Settings: 'Theme Settings'
|
Theme Settings: 'Theme Settings'
|
||||||
Match Top Bar with Main Color: 'Match top bar with main colour'
|
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
|
Playlist Next Video Interval: Playlist Next Video Interval
|
||||||
Next Video Interval: Next Video Interval
|
Next Video Interval: Next Video Interval
|
||||||
Display Play Button In Video Player: Display Play Button In Video Player
|
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: 'Privacy Settings'
|
Privacy Settings: 'Privacy Settings'
|
||||||
Remember History: 'Remember History'
|
Remember History: 'Remember History'
|
||||||
|
@ -529,6 +537,23 @@ Video:
|
||||||
intro: intro
|
intro: intro
|
||||||
sponsor: sponsor
|
sponsor: sponsor
|
||||||
Skipped segment: Skipped segment
|
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:
|
Videos:
|
||||||
#& Sort By
|
#& Sort By
|
||||||
Sort By:
|
Sort By:
|
||||||
|
@ -654,6 +679,16 @@ Tooltips:
|
||||||
Preferred API Backend: Choose the back-end that FreeTube uses to obtain data.
|
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
|
The local API is a built-in extractor. The Invidious API requires an Invidious
|
||||||
server to connect to.
|
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:
|
Privacy Settings:
|
||||||
Remove Video Meta Files: When enabled, FreeTube automatically deletes meta files
|
Remove Video Meta Files: When enabled, FreeTube automatically deletes meta files
|
||||||
created during video playback, when the watch page is closed.
|
created during video playback, when the watch page is closed.
|
||||||
|
|
Loading…
Reference in New Issue