Skip to content

Commit

Permalink
Stay in fullscreen/fullwindow/PiP + default viewing mode setting (#5903)
Browse files Browse the repository at this point in the history
* MHave fullscreen persist when videos autoplay

* Have fullscreen and PiP be re-requested on autoplay when they are open

* Implement Default Viewing Mode setting

* Implement external player default viewing mode

Current limitations: does not work for the search bar, randomly encountered YT video links (e.g., in descriptions), or the video thumbnail link in the playlist list view.

* Disable & hide 'External Player' default viewing mode when no external player is set

This will prevent issues with users who accidentally change this setting and report that clicking on videos results in errors.

* Fix fullscreen issue with icons by calling requestFullscreen on videoContainer element

* Revert Enable Theater Mode by Default removal

Theatre mode is not mutually exclusive with the viewing mode and thus should not be included here. This also saves us the work of having to update the default viewing mode to theatre mode on first load for 1-2 releases that we would have otherwise needed.

* Update src/renderer/components/player-settings/player-settings.js

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>

* Update to trigger setFullWindow event when starting in fullwindow

* Update to use expand icon instead

* Clean up External Player Default Viewing Mode link template logic

* Remove PiP and fullscreen default viewing mode options in settings when Electron is not available

* Improve stay-in-mode handling to save state values on player destroy

This allows staying in PiP when clicking on other videos, staying in fullscreen/fullwindow when using Ctrl+Left Arrow / Ctrl+Right Arrow, and staying in PiP when using the watch-video-playlist Play Prev / Play Next buttons.

* Fix linting

* Update values to check IS_ELECTRON

* Add clarifying code comments

* Revert "Revert Enable Theater Mode by Default removal"

This reverts commit 937935f.

* Implement theatre mode setting migration

* Add 'Theater' label in lieu of reusing 'Theater Mode' label

* Use native Shaka functions for toggling FS and PiP

* Implement code review suggestions

* Remove entries of removed theatre mode key in all other locales

* Apply suggestions from code review

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>

* Implement changes from review

* Remove key from additional languages

* Remove label from two more languages

* Revert changes in other locales to prevent merge conflicts

---------

Co-authored-by: absidue <48293849+absidue@users.noreply.github.com>
  • Loading branch information
kommunarr and absidue authored Jan 20, 2025
1 parent 3b84a86 commit 64f89ac
Show file tree
Hide file tree
Showing 15 changed files with 241 additions and 55 deletions.
3 changes: 3 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',

Expand Down
13 changes: 13 additions & 0 deletions src/datastores/handlers/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' } })
}

Expand Down
12 changes: 12 additions & 0 deletions src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,6 @@ export default defineComponent({
'updateHideLiveChat',
'updateHideActiveSubscriptions',
'updatePlayNextVideo',
'updateDefaultTheatreMode',
'updateHideVideoDescription',
'updateHideComments',
'updateHideCommentPhotos',
Expand Down
24 changes: 19 additions & 5 deletions src/renderer/components/ft-list-video/ft-list-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions src/renderer/components/ft-list-video/ft-list-video.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
<router-link
class="thumbnailLink"
tabindex="-1"
:to="watchPageLinkTo"
:to="watchVideoRouterLink"
@click.native="handleWatchPageLinkClick"
>
<img
:src="thumbnail"
Expand All @@ -34,7 +35,7 @@
{{ isLive ? $t("Video.Live") : (isUpcoming ? $t("Video.Upcoming") : displayDuration) }}
</div>
<ft-icon-button
v-if="externalPlayer !== ''"
v-if="externalPlayer !== '' && !externalPlayerIsDefaultViewingMode"
:title="$t('Video.External Player.OpenInTemplate', { externalPlayer })"
:icon="['fas', 'external-link-alt']"
class="externalPlayerIcon"
Expand Down Expand Up @@ -112,7 +113,8 @@
<div class="info">
<router-link
class="title"
:to="watchPageLinkTo"
:to="watchVideoRouterLink"
@click.native="handleWatchPageLinkClick"
>
<h3 class="h3Title">
{{ displayTitle }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1719,6 +1744,12 @@ export default defineComponent({
}
})

if (startInFullwindow) {
events.dispatchEvent(new CustomEvent('setFullWindow', {
detail: true
}))
}

/**
* @implements {shaka.extern.IUIElement.Factory}
*/
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -2867,6 +2918,8 @@ export default defineComponent({
if (video.value) {
video.value.ui = null
}

return uiState
}

expose({
Expand Down Expand Up @@ -2920,6 +2973,7 @@ export default defineComponent({

handlePlay,
handlePause,
handleCanPlay,
handleEnded,
updateVolume,
handleTimeupdate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
@play="handlePlay"
@pause="handlePause"
@ended="handleEnded"
@canplay="handleCanPlay"
@volumechange="updateVolume"
@timeupdate="handleTimeupdate"
/>
Expand Down
56 changes: 53 additions & 3 deletions src/renderer/components/player-settings/player-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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
},
Expand Down Expand Up @@ -296,7 +346,7 @@ export default defineComponent({
'updatePlayNextVideo',
'updateEnableSubtitlesByDefault',
'updateProxyVideos',
'updateDefaultTheatreMode',
'updateDefaultViewingMode',
'updateDefaultSkipInterval',
'updateDefaultInterval',
'updateDefaultVolume',
Expand Down
Loading

0 comments on commit 64f89ac

Please sign in to comment.