* 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'
|
||||
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()
|
||||
|
|
|
@ -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'
|
||||
])
|
||||
}
|
||||
|
|
|
@ -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 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'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -23,6 +23,16 @@
|
|||
</div>
|
||||
</router-link>
|
||||
<div class="info">
|
||||
<ft-icon-button
|
||||
v-if="externalPlayer !== ''"
|
||||
:title="$t('Video.External Player.OpenInTemplate').replace('$', externalPlayer)"
|
||||
icon="external-link-alt"
|
||||
class="externalPlayerButton"
|
||||
theme="base-no-default"
|
||||
:size="16"
|
||||
:use-shadow="false"
|
||||
@click="handleExternalPlayer"
|
||||
/>
|
||||
<router-link
|
||||
class="title"
|
||||
:to="`/playlist/${playlistId}`"
|
||||
|
|
|
@ -16,6 +16,22 @@ export default Vue.extend({
|
|||
type: String,
|
||||
default: null
|
||||
},
|
||||
playlistIndex: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
playlistReverse: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
playlistShuffle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
playlistLoop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
forceListType: {
|
||||
type: String,
|
||||
default: null
|
||||
|
@ -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',
|
||||
|
|
|
@ -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')"
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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')"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -25,5 +25,5 @@
|
|||
width: 90%
|
||||
|
||||
@media only screen and (max-width: 460px)
|
||||
.generalSettingsFlexBox, .playerSettingsFlexBox
|
||||
.generalSettingsFlexBox, .playerSettingsFlexBox, .externalPlayerSettingsFlexBox
|
||||
justify-content: flex-start
|
||||
|
|
|
@ -177,6 +177,10 @@ const state = {
|
|||
displayVideoPlayButton: true,
|
||||
enableSearchSuggestions: true,
|
||||
enableSubtitles: true,
|
||||
externalPlayer: '',
|
||||
externalPlayerExecutable: '',
|
||||
externalPlayerIgnoreWarnings: false,
|
||||
externalPlayerCustomArgs: '',
|
||||
forceLocalBackendForLegacy: false,
|
||||
hideActiveSubscriptions: false,
|
||||
hideChannelSubscriptions: false,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
:key="index"
|
||||
:data="item"
|
||||
:playlist-id="playlistId"
|
||||
:playlist-index="index"
|
||||
appearance="result"
|
||||
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 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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<general-settings />
|
||||
<theme-settings />
|
||||
<player-settings />
|
||||
<external-player-settings v-if="usingElectron" />
|
||||
<subscription-settings />
|
||||
<distraction-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 () {
|
||||
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`
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
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,
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue