diff --git a/src/constants.js b/src/constants.js index 9700514ff9376..a100a64213153 100644 --- a/src/constants.js +++ b/src/constants.js @@ -16,6 +16,9 @@ const IpcChannels = { APP_READY: 'app-ready', RELAUNCH_REQUEST: 'relaunch-request', + REQUEST_FULLSCREEN: 'request-fullscreen', + REQUEST_PIP: 'request-pip', + SEARCH_INPUT_HANDLING_READY: 'search-input-handling-ready', UPDATE_SEARCH_INPUT_TEXT: 'update-search-input-text', diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index d25963376f641..c7394f5dfd802 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -25,6 +25,19 @@ class Settings { await this.upsert('externalPlayerCustomArgs', newValue) } + // In FreeTube 0.23.0, the "Enable Theatre Mode by Default" setting was incoporated as an option + // of the "Default Viewing Mode" setting. This is a one time migration to preserve users' + // Theater Mode preference through this change. + const defaultTheatreMode = await db.settings.findOneAsync({ _id: 'defaultTheatreMode' }) + + if (defaultTheatreMode) { + if (defaultTheatreMode.value) { + await this.upsert('defaultViewingMode', 'theatre') + } + + await db.settings.removeAsync({ _id: 'defaultTheatreMode' }) + } + return db.settings.findAsync({ _id: { $ne: 'bounds' } }) } diff --git a/src/main/index.js b/src/main/index.js index cf0142c1a4f77..0e6d98353da89 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -981,6 +981,18 @@ function runApp() { return app.getPath('pictures') }) + // Allows programmatic toggling of fullscreen without accompanying user interaction. + // See: https://developer.mozilla.org/en-US/docs/Web/Security/User_activation#transient_activation + ipcMain.on(IpcChannels.REQUEST_FULLSCREEN, ({ sender }) => { + sender.executeJavaScript('document.querySelector("video.player").ui.getControls().toggleFullScreen()', true) + }) + + // Allows programmatic toggling of picture-in-picture mode without accompanying user interaction. + // See: https://developer.mozilla.org/en-US/docs/Web/Security/User_activation#transient_activation + ipcMain.on(IpcChannels.REQUEST_PIP, ({ sender }) => { + sender.executeJavaScript('document.querySelector("video.player").ui.getControls().togglePiP()', true) + }) + ipcMain.handle(IpcChannels.SHOW_OPEN_DIALOG, async ({ sender }, options) => { const senderWindow = findSenderWindow(sender) if (senderWindow) { diff --git a/src/renderer/components/distraction-settings/distraction-settings.js b/src/renderer/components/distraction-settings/distraction-settings.js index c0e8ca8a03c5b..80cc6837b0d21 100644 --- a/src/renderer/components/distraction-settings/distraction-settings.js +++ b/src/renderer/components/distraction-settings/distraction-settings.js @@ -218,7 +218,6 @@ export default defineComponent({ 'updateHideLiveChat', 'updateHideActiveSubscriptions', 'updatePlayNextVideo', - 'updateDefaultTheatreMode', 'updateHideVideoDescription', 'updateHideComments', 'updateHideCommentPhotos', diff --git a/src/renderer/components/ft-list-video/ft-list-video.js b/src/renderer/components/ft-list-video/ft-list-video.js index d7c87dca85d60..137e587306c60 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.js +++ b/src/renderer/components/ft-list-video/ft-list-video.js @@ -380,6 +380,10 @@ export default defineComponent({ return this.$store.getters.getExternalPlayer }, + externalPlayerIsDefaultViewingMode: function () { + return process.env.IS_ELECTRON && this.externalPlayer !== '' && this.$store.getters.getDefaultViewingMode === 'external_player' + }, + defaultPlayback: function () { return this.$store.getters.getDefaultPlayback }, @@ -482,13 +486,18 @@ export default defineComponent({ return this.isInQuickBookmarkPlaylist ? 'base favorite' : 'base' }, - watchPageLinkTo() { - // For `router-link` attribute `to` - return { - path: `/watch/${this.id}`, - query: this.watchPageLinkQuery, + watchVideoRouterLink() { + // For `router-link` attribute `to` + if (!this.externalPlayerIsDefaultViewingMode) { + return { + path: `/watch/${this.id}`, + query: this.watchPageLinkQuery, + } + } else { + return {} } }, + watchPageLinkQuery() { const query = {} if (this.playlistIdFinal) { query.playlistId = this.playlistIdFinal } @@ -547,6 +556,11 @@ export default defineComponent({ } }, methods: { + handleWatchPageLinkClick: function() { + if (this.externalPlayerIsDefaultViewingMode) { + this.handleExternalPlayer() + } + }, fetchDeArrowThumbnail: async function() { if (this.thumbnailPreference === 'hidden') { return } const videoId = this.id diff --git a/src/renderer/components/ft-list-video/ft-list-video.vue b/src/renderer/components/ft-list-video/ft-list-video.vue index e53bbf3a5a4f3..3cb4360460c01 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.vue +++ b/src/renderer/components/ft-list-video/ft-list-video.vue @@ -14,7 +14,8 @@

{{ displayTitle }} diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 7fded01010dcd..85a883fa22132 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -132,6 +132,18 @@ export default defineComponent({ type: String, default: null }, + startInFullscreen: { + type: Boolean, + default: false + }, + startInFullwindow: { + type: Boolean, + default: false + }, + startInPip: { + type: Boolean, + default: false + }, currentPlaybackRate: { type: Number, default: 1 @@ -172,11 +184,15 @@ export default defineComponent({ const isLive = ref(false) const useOverFlowMenu = ref(false) - const fullWindowEnabled = ref(false) const forceAspectRatio = ref(false) const activeLegacyFormat = shallowRef(null) + const fullWindowEnabled = ref(false) + const startInFullwindow = props.startInFullwindow + let startInFullscreen = props.startInFullscreen + let startInPip = props.startInPip + /** * @type {{ * url: string, @@ -1117,6 +1133,15 @@ export default defineComponent({ emit('ended') } + function handleCanPlay() { + // PiP can only be activated once the video's readState and video track are populated + if (startInPip && props.format !== 'audio' && ui.getControls().isPiPAllowed() && process.env.IS_ELECTRON) { + startInPip = false + const { ipcRenderer } = require('electron') + ipcRenderer.send(IpcChannels.REQUEST_PIP) + } + } + function updateVolume() { const video_ = video.value // https://docs.videojs.com/html5#volume @@ -1719,6 +1744,12 @@ export default defineComponent({ } }) + if (startInFullwindow) { + events.dispatchEvent(new CustomEvent('setFullWindow', { + detail: true + })) + } + /** * @implements {shaka.extern.IUIElement.Factory} */ @@ -1809,7 +1840,7 @@ export default defineComponent({ /** * As shaka-player doesn't let you unregister custom control factories, * overwrite them with `null` instead so the referenced objects - * (e.g. {@linkcode events}, {@linkcode fullWindowEnabled}) can get gargabe collected + * (e.g. {@linkcode events}, {@linkcode fullWindowEnabled}) can get garbage collected */ function cleanUpCustomPlayerControls() { shakaControls.registerElement('ft_audio_tracks', null) @@ -2633,6 +2664,12 @@ export default defineComponent({ if (props.chapters.length > 0) { createChapterMarkers() } + + if (startInFullscreen && process.env.IS_ELECTRON) { + startInFullscreen = false + const { ipcRenderer } = require('electron') + ipcRenderer.send(IpcChannels.REQUEST_FULLSCREEN) + } } watch( @@ -2844,11 +2881,25 @@ export default defineComponent({ * Vue's lifecycle hooks are synchonous, so if we destroy the player in {@linkcode onBeforeUnmount}, * it won't be finished in time, as the player destruction is asynchronous. * To workaround that we destroy the player first and wait for it to finish before we unmount this component. + * + * @returns {Promise<{ startNextVideoInFullscreen: boolean, startNextVideoInFullwindow: boolean, startNextVideoInPip: boolean }>} */ async function destroyPlayer() { ignoreErrors = true + let uiState = { startNextVideoInFullscreen: false, startNextVideoInFullwindow: false, startNextVideoInPip: false } + if (ui) { + if (ui.getControls()) { + // save the state of player settings to reinitialize them upon next creation + const controls = ui.getControls() + uiState = { + startNextVideoInFullscreen: controls.isFullScreenEnabled(), + startNextVideoInFullwindow: fullWindowEnabled.value, + startNextVideoInPip: controls.isPiPEnabled() + } + } + // destroying the ui also destroys the player await ui.destroy() ui = null @@ -2867,6 +2918,8 @@ export default defineComponent({ if (video.value) { video.value.ui = null } + + return uiState } expose({ @@ -2920,6 +2973,7 @@ export default defineComponent({ handlePlay, handlePause, + handleCanPlay, handleEnded, updateVolume, handleTimeupdate, diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue index ba0de6301c540..f1e8cc8d86e8f 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue @@ -19,6 +19,7 @@ @play="handlePlay" @pause="handlePause" @ended="handleEnded" + @canplay="handleCanPlay" @volumechange="updateVolume" @timeupdate="handleTimeupdate" /> diff --git a/src/renderer/components/player-settings/player-settings.js b/src/renderer/components/player-settings/player-settings.js index 55e98938b3e1a..a8e7591cf1969 100644 --- a/src/renderer/components/player-settings/player-settings.js +++ b/src/renderer/components/player-settings/player-settings.js @@ -125,8 +125,18 @@ export default defineComponent({ return this.$store.getters.getDefaultQuality }, - defaultTheatreMode: function () { - return this.$store.getters.getDefaultTheatreMode + defaultViewingMode: function () { + const defaultViewingMode = this.$store.getters.getDefaultViewingMode + if ((defaultViewingMode === 'external_player' && (!process.env.IS_ELECTRON || this.externalPlayer === '')) || + (!process.env.IS_ELECTRON && (defaultViewingMode === 'fullscreen' || defaultViewingMode === 'pip'))) { + return 'default' + } + + return defaultViewingMode + }, + + externalPlayer: function () { + return this.$store.getters.getExternalPlayer }, hideRecommendedVideos: function () { @@ -183,6 +193,46 @@ export default defineComponent({ ] }, + viewingModeNames: function () { + const viewingModeNames = [ + this.$t('Settings.General Settings.Thumbnail Preference.Default'), + this.$t('Settings.Player Settings.Default Viewing Mode.Theater'), + this.$t('Video.Player.Full Window'), + ] + + if (process.env.IS_ELECTRON) { + viewingModeNames.push( + this.$t('Settings.Player Settings.Default Viewing Mode.Full Screen'), + this.$t('Settings.Player Settings.Default Viewing Mode.Picture in Picture') + ) + if (this.externalPlayer !== '') { + viewingModeNames.push( + this.$t('Settings.Player Settings.Default Viewing Mode.External Player', { externalPlayerName: this.externalPlayer }) + ) + } + } + + return viewingModeNames + }, + + viewingModeValues: function () { + const viewingModeValues = [ + 'default', + 'theatre', + 'fullwindow' + ] + + if (process.env.IS_ELECTRON) { + viewingModeValues.push('fullscreen', 'pip') + + if (this.externalPlayer !== '') { + viewingModeValues.push('external_player') + } + } + + return viewingModeValues + }, + enableScreenshot: function() { return this.$store.getters.getEnableScreenshot }, @@ -296,7 +346,7 @@ export default defineComponent({ 'updatePlayNextVideo', 'updateEnableSubtitlesByDefault', 'updateProxyVideos', - 'updateDefaultTheatreMode', + 'updateDefaultViewingMode', 'updateDefaultSkipInterval', 'updateDefaultInterval', 'updateDefaultVolume', diff --git a/src/renderer/components/player-settings/player-settings.vue b/src/renderer/components/player-settings/player-settings.vue index 57002920b8e61..5dabfd7fcb06d 100644 --- a/src/renderer/components/player-settings/player-settings.vue +++ b/src/renderer/components/player-settings/player-settings.vue @@ -18,12 +18,6 @@ :default-value="enableSubtitlesByDefault" @change="updateEnableSubtitlesByDefault" /> - + + + + + + - - - - -
diff --git a/src/renderer/main.js b/src/renderer/main.js index 521233f25bbeb..65aa0ebedf23d 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -47,6 +47,7 @@ import { faEnvelope, faExchangeAlt, faExclamationCircle, + faExpand, faExternalLinkAlt, faEye, faEyeSlash, @@ -170,6 +171,7 @@ library.add( faEnvelope, faExchangeAlt, faExclamationCircle, + faExpand, faExternalLinkAlt, faEye, faEyeSlash, diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js index 8577e6e259cf7..306d3b82df692 100644 --- a/src/renderer/store/modules/settings.js +++ b/src/renderer/store/modules/settings.js @@ -166,7 +166,7 @@ const state = { defaultProfile: MAIN_PROFILE_ID, defaultQuality: '720', defaultSkipInterval: 5, - defaultTheatreMode: false, + defaultViewingMode: 'default', defaultVideoFormat: 'dash', disableSmoothScrolling: false, displayVideoPlayButton: false, diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 3f00082828b72..e768dd733869c 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -61,13 +61,16 @@ export default defineComponent({ document.removeEventListener('click', this.resetAutoplayInterruptionTimeout) if (this.$refs.player) { - await this.$refs.player.destroyPlayer() + await this.destroyPlayer() } next() }, data: function () { return { + startNextVideoInFullscreen: false, + startNextVideoInFullwindow: false, + startNextVideoInPip: false, isLoading: true, firstLoad: true, useTheatreMode: false, @@ -172,8 +175,8 @@ export default defineComponent({ defaultInterval: function () { return this.$store.getters.getDefaultInterval }, - defaultTheatreMode: function () { - return this.$store.getters.getDefaultTheatreMode + defaultViewingMode: function () { + return this.$store.getters.getDefaultViewingMode }, defaultVideoFormat: function () { return this.$store.getters.getDefaultVideoFormat @@ -279,7 +282,7 @@ export default defineComponent({ this.handleRouteChange() if (this.$refs.player) { - await this.$refs.player.destroyPlayer() + await this.destroyPlayer() } // react to route changes... @@ -335,7 +338,7 @@ export default defineComponent({ this.checkIfPlaylist() // this has to be below checkIfPlaylist() as theatrePossible needs to know if there is a playlist or not - this.useTheatreMode = this.defaultTheatreMode && this.theatrePossible + this.setViewingModeOnFirstLoad() if (!process.env.SUPPORTS_LOCAL_API || this.backendPreference === 'invidious') { this.getVideoInformationInvidious() @@ -352,6 +355,22 @@ export default defineComponent({ this.resetAutoplayInterruptionTimeout() }, + setViewingModeOnFirstLoad: function () { + switch (this.defaultViewingMode) { + case 'theatre': + this.useTheatreMode = this.theatrePossible + break + case 'fullscreen': + this.startNextVideoInFullscreen = true + break + case 'fullwindow': + this.startNextVideoInFullwindow = true + break + case 'pip': + this.startNextVideoInPip = true + } + }, + changeTimestamp: function (timestamp) { const player = this.$refs.player @@ -1683,6 +1702,13 @@ export default defineComponent({ this.currentPlaybackRate = newRate }, + destroyPlayer: async function() { + const uiState = await this.$refs.player.destroyPlayer() + this.startNextVideoInFullscreen = uiState.startNextVideoInFullscreen + this.startNextVideoInFullwindow = uiState.startNextVideoInFullwindow + this.startNextVideoInPip = uiState.startNextVideoInPip + }, + ...mapActions([ 'updateHistory', 'updateAutoplayPlaylists', diff --git a/src/renderer/views/Watch/Watch.vue b/src/renderer/views/Watch/Watch.vue index 00f3cf6b44673..e5eab3bc24364 100644 --- a/src/renderer/views/Watch/Watch.vue +++ b/src/renderer/views/Watch/Watch.vue @@ -36,6 +36,9 @@ :autoplay-possible="autoplayPossible" :autoplay-enabled="autoplayEnabled" :vr-projection="vrProjection" + :start-in-fullscreen="startNextVideoInFullscreen" + :start-in-fullwindow="startNextVideoInFullwindow" + :start-in-pip="startNextVideoInPip" :current-playback-rate="currentPlaybackRate" class="videoPlayer" @error="handlePlayerError" diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index ceb7175dbe2a3..109545e8eed49 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -417,9 +417,14 @@ Settings: Play Next Video: Autoplay Recommended Videos Autoplay Playlists: Autoplay Playlist Videos Autoplay Videos: Start Videos Automatically - Turn on Subtitles by Default: Turn on Subtitles by Default + Turn on Subtitles by Default: Enable Subtitles by Default Proxy Videos Through Invidious: Proxy Videos Through Invidious - Enable Theatre Mode by Default: Enable Theatre Mode by Default + Default Viewing Mode: + Theater: Theater + Default Viewing Mode: Default Viewing Mode + Full Screen: Full Screen + Picture in Picture: Picture in Picture + External Player: External Player ({externalPlayerName}) Scroll Volume Over Video Player: Scroll Volume Over Video Player Scroll Playback Rate Over Video Player: Scroll Playback Rate Over Video Player Skip by Scrolling Over Video Player: Skip by Scrolling Over Video Player