diff --git a/_scripts/getRegions.mjs b/_scripts/getRegions.mjs new file mode 100644 index 0000000000000..90adce4d0dfd0 --- /dev/null +++ b/_scripts/getRegions.mjs @@ -0,0 +1,148 @@ +/** + * This script updates the files in static/geolocations with the available locations on YouTube. + * + * It tries to map every active FreeTube language (static/locales/activelocales.json) + * to it's equivalent on YouTube. + * + * It then uses those language mappings, + * to scrape the location selection menu on the YouTube website, in every mapped language. + * + * All languages it couldn't find on YouTube, that don't have manually added mapping, + * get logged to the console, as well as all unmapped YouTube languages. + */ + +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs' +import { dirname } from 'path' +import { fileURLToPath } from 'url' +import { Innertube, Misc } from 'youtubei.js' + +const STATIC_DIRECTORY = `${dirname(fileURLToPath(import.meta.url))}/../static` + +const activeLanguagesPath = `${STATIC_DIRECTORY}/locales/activeLocales.json` +/** @type {string[]} */ +const activeLanguages = JSON.parse(readFileSync(activeLanguagesPath, { encoding: 'utf8' })) + +// en-US is en on YouTube +const initialResponse = await scrapeLanguage('en') + +// Scrape language menu in en-US + +/** @type {string[]} */ +const youTubeLanguages = initialResponse.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[1].multiPageMenuSectionRenderer.items[1].compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].getMultiPageMenuAction.menu.multiPageMenuRenderer.sections[0].multiPageMenuSectionRenderer.items + .map(({ compactLinkRenderer }) => { + return compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].selectLanguageCommand.hl + }) + +// map FreeTube languages to their YouTube equivalents + +const foundLanguageNames = ['en-US'] +const unusedYouTubeLanguageNames = [] +const languagesToScrape = [] + +for (const language of youTubeLanguages) { + if (activeLanguages.includes(language)) { + foundLanguageNames.push(language) + languagesToScrape.push({ + youTube: language, + freeTube: language + }) + } else if (activeLanguages.includes(language.replace('-', '_'))) { + const withUnderScore = language.replace('-', '_') + foundLanguageNames.push(withUnderScore) + languagesToScrape.push({ + youTube: language, + freeTube: withUnderScore + }) + } + // special cases + else if (language === 'de') { + foundLanguageNames.push('de-DE') + languagesToScrape.push({ + youTube: 'de', + freeTube: 'de-DE' + }) + } else if (language === 'fr') { + foundLanguageNames.push('fr-FR') + languagesToScrape.push({ + youTube: 'fr', + freeTube: 'fr-FR' + }) + } else if (language === 'no') { + // according to https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes + // "no" is the macro language for "nb" and "nn" + foundLanguageNames.push('nb_NO', 'nn') + languagesToScrape.push({ + youTube: 'no', + freeTube: 'nb_NO' + }) + languagesToScrape.push({ + youTube: 'no', + freeTube: 'nn' + }) + } else if (language !== 'en') { + unusedYouTubeLanguageNames.push(language) + } +} + +console.log("Active FreeTube languages that aren't available on YouTube:") +console.log(activeLanguages.filter(lang => !foundLanguageNames.includes(lang)).sort()) + +console.log("YouTube languages that don't have an equivalent active FreeTube language:") +console.log(unusedYouTubeLanguageNames.sort()) + +// Scrape the location menu in various languages and write files to the file system + +rmSync(`${STATIC_DIRECTORY}/geolocations`, { recursive: true }) +mkdirSync(`${STATIC_DIRECTORY}/geolocations`) + +processGeolocations('en-US', 'en', initialResponse) + +for (const { youTube, freeTube } of languagesToScrape) { + const response = await scrapeLanguage(youTube) + + processGeolocations(freeTube, youTube, response) +} + + + +async function scrapeLanguage(youTubeLanguageCode) { + const session = await Innertube.create({ + retrieve_player: false, + generate_session_locally: true, + lang: youTubeLanguageCode + }) + + return await session.actions.execute('/account/account_menu') +} + +function processGeolocations(freeTubeLanguage, youTubeLanguage, response) { + const geolocations = response.data.actions[0].openPopupAction.popup.multiPageMenuRenderer.sections[1].multiPageMenuSectionRenderer.items[3].compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].getMultiPageMenuAction.menu.multiPageMenuRenderer.sections[0].multiPageMenuSectionRenderer.items + .map(({ compactLinkRenderer }) => { + return { + name: new Misc.Text(compactLinkRenderer.title).toString().trim(), + code: compactLinkRenderer.serviceEndpoint.signalServiceEndpoint.actions[0].selectCountryCommand.gl + } + }) + + const normalisedFreeTubeLanguage = freeTubeLanguage.replace('_', '-') + + // give Intl.Collator 4 locales, in the hopes that it supports one of them + // deduplicate the list so it doesn't have to do duplicate work + const localeSet = new Set() + localeSet.add(normalisedFreeTubeLanguage) + localeSet.add(youTubeLanguage) + localeSet.add(normalisedFreeTubeLanguage.split('-')[0]) + localeSet.add(youTubeLanguage.split('-')[0]) + + const locales = Array.from(localeSet) + + // only sort if node supports sorting the language, otherwise hope that YouTube's sorting was correct + // node 20.3.1 doesn't support sorting `eu` + if (Intl.Collator.supportedLocalesOf(locales).length > 0) { + const collator = new Intl.Collator(locales) + + geolocations.sort((a, b) => collator.compare(a.name, b.name)) + } + + writeFileSync(`${STATIC_DIRECTORY}/geolocations/${freeTubeLanguage}.json`, JSON.stringify(geolocations)) +} diff --git a/_scripts/webpack.web.config.js b/_scripts/webpack.web.config.js index 7a2ab81087a8e..e0ea06cc4b71c 100644 --- a/_scripts/webpack.web.config.js +++ b/_scripts/webpack.web.config.js @@ -174,7 +174,7 @@ config.plugins.push( processLocalesPlugin, new webpack.DefinePlugin({ 'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames), - 'process.env.GEOLOCATION_NAMES': JSON.stringify(fs.readdirSync(path.join(__dirname, '..', 'static', 'geolocations'))) + 'process.env.GEOLOCATION_NAMES': JSON.stringify(fs.readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))) }), new CopyWebpackPlugin({ patterns: [ diff --git a/package.json b/package.json index 9da3cf1e01cd9..b11d2ab5526ba 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dev:web": "node _scripts/dev-runner.js --web", "dev-runner": "node _scripts/dev-runner.js", "get-instances": "node _scripts/getInstances.js", + "get-regions": "node _scripts/getRegions.mjs", "lint-all": "run-p lint lint-json lint-style", "lint-fix": "eslint --fix --ext .js,.vue ./", "lint": "eslint --ext .js,.vue ./", @@ -82,56 +83,56 @@ "vue-router": "^3.6.5", "vue-tiny-slider": "^0.1.39", "vuex": "^3.6.2", - "youtubei.js": "^5.1.0", + "youtubei.js": "^5.2.1", "jintr-patch": "https://github.com/LuanRT/Jinter.git" }, "devDependencies": { - "@babel/core": "^7.22.5", - "@babel/eslint-parser": "^7.22.5", + "@babel/core": "^7.22.8", + "@babel/eslint-parser": "^7.22.7", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.22.5", + "@babel/preset-env": "^7.22.7", "@double-great/stylelint-a11y": "^2.0.2", - "babel-loader": "^9.1.2", + "babel-loader": "^9.1.3", "copy-webpack-plugin": "^11.0.0", "cordova": "^11.0.0", "core-js": "^3.27.2", "css-loader": "^6.8.1", "css-minimizer-webpack-plugin": "^5.0.1", - "electron": "^22.3.13", + "electron": "^22.3.16", "electron-builder": "^23.6.0", - "eslint": "^8.43.0", + "eslint": "^8.44.0", "eslint-config-prettier": "^8.8.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsonc": "^2.9.0", - "eslint-plugin-n": "^16.0.0", + "eslint-plugin-n": "^16.0.1", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-unicorn": "^47.0.0", - "eslint-plugin-vue": "^9.15.0", + "eslint-plugin-vue": "^9.15.1", "eslint-plugin-vuejs-accessibility": "^2.1.0", "eslint-plugin-yml": "^1.8.0", "html-webpack-plugin": "^5.5.3", "js-yaml": "^4.1.0", "json-minimizer-webpack-plugin": "^4.0.0", - "lefthook": "^1.4.2", + "lefthook": "^1.4.3", "mini-css-extract-plugin": "^2.7.6", "npm-run-all": "^4.1.5", - "postcss": "^8.4.24", + "postcss": "^8.4.25", "postcss-scss": "^4.0.6", "prettier": "^2.8.8", "rimraf": "^5.0.1", - "sass": "^1.63.4", + "sass": "^1.63.6", "sass-loader": "^13.3.2", - "stylelint": "^14.16.1", - "stylelint-config-sass-guidelines": "^9.0.1", - "stylelint-config-standard": "^29.0.0", + "stylelint": "^15.10.1", + "stylelint-config-sass-guidelines": "^10.0.0", + "stylelint-config-standard": "^34.0.0", "stylelint-high-performance-animation": "^1.8.0", "tree-kill": "1.2.2", "vue-devtools": "^5.1.4", "vue-eslint-parser": "^9.3.1", "vue-loader": "^15.10.0", - "webpack": "^5.87.0", + "webpack": "^5.88.1", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1", "xml2js": "^0.4.23", diff --git a/src/cordova/config.xml b/src/cordova/config.xml index 1a98fb643e236..792b1c04c786c 100644 --- a/src/cordova/config.xml +++ b/src/cordova/config.xml @@ -16,9 +16,13 @@ + + + + diff --git a/src/cordova/package.js b/src/cordova/package.js index ae94d90cbd3ce..4a456dfbc48a7 100644 --- a/src/cordova/package.js +++ b/src/cordova/package.js @@ -14,7 +14,7 @@ module.exports = { restore: 'npx cordova platform add android' }, devDependencies: { - 'cordova-android': '^11.0.0', + 'cordova-android': '^12.0.0', 'cordova-clipboard': '^1.3.0', 'cordova-plugin-background-mode': 'git+https://bitbucket.org/TheBosZ/cordova-plugin-run-in-background.git', 'cordova-plugin-android-permissions': '^1.1.4', diff --git a/src/main/index.js b/src/main/index.js index 9d44efd68020f..b5f08d62f01cd 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -11,6 +11,8 @@ import baseHandlers from '../datastores/handlers/base' import { extractExpiryTimestamp, ImageCache } from './ImageCache' import { existsSync } from 'fs' +import packageDetails from '../../package.json' + if (process.argv.includes('--version')) { app.exit() } else { @@ -263,6 +265,12 @@ function runApp() { }) } + const fixedUserAgent = session.defaultSession.getUserAgent() + .split(' ') + .filter(part => !part.includes('Electron') && !part.includes(packageDetails.productName)) + .join(' ') + session.defaultSession.setUserAgent(fixedUserAgent) + // Set CONSENT cookie on reasonable domains const consentCookieDomains = [ 'https://www.youtube.com', @@ -279,10 +287,19 @@ function runApp() { // make InnerTube requests work with the fetch function // InnerTube rejects requests if the referer isn't YouTube or empty - const innertubeRequestFilter = { urls: ['https://www.youtube.com/youtubei/*'] } + const innertubeAndMediaRequestFilter = { urls: ['https://www.youtube.com/youtubei/*', 'https://*.googlevideo.com/videoplayback?*'] } + + session.defaultSession.webRequest.onBeforeSendHeaders(innertubeAndMediaRequestFilter, ({ requestHeaders, url }, callback) => { + requestHeaders.Referer = 'https://www.youtube.com/' + requestHeaders.Origin = 'https://www.youtube.com' + + if (url.startsWith('https://www.youtube.com/youtubei/')) { + requestHeaders['Sec-Fetch-Site'] = 'same-origin' + } else { + // YouTube doesn't send the Content-Type header for the media requests, so we shouldn't either + delete requestHeaders['Content-Type'] + } - session.defaultSession.webRequest.onBeforeSendHeaders(innertubeRequestFilter, ({ requestHeaders }, callback) => { - requestHeaders.referer = 'https://www.youtube.com' // eslint-disable-next-line n/no-callback-literal callback({ requestHeaders }) }) diff --git a/src/renderer/components/ft-community-post/ft-community-post.js b/src/renderer/components/ft-community-post/ft-community-post.js index 3b832938bd892..8c7e2f718de3e 100644 --- a/src/renderer/components/ft-community-post/ft-community-post.js +++ b/src/renderer/components/ft-community-post/ft-community-post.js @@ -111,7 +111,8 @@ export default defineComponent({ return Number.parseInt(b.width) - Number.parseInt(a.width) }) - return imageArrayCopy.at(0)?.url ?? '' + // Remove cropping directives when applicable + return imageArrayCopy.at(0)?.url?.replace(/-c-fcrop64=.*/i, '') ?? '' } } }) diff --git a/src/renderer/components/ft-element-list/ft-element-list.js b/src/renderer/components/ft-element-list/ft-element-list.js index 9c25aed8b5846..31e57f31151c4 100644 --- a/src/renderer/components/ft-element-list/ft-element-list.js +++ b/src/renderer/components/ft-element-list/ft-element-list.js @@ -21,7 +21,11 @@ export default defineComponent({ showVideoWithLastViewedPlaylist: { type: Boolean, default: false - } + }, + useChannelsHiddenPreference: { + type: Boolean, + default: true, + }, }, computed: { listType: function () { diff --git a/src/renderer/components/ft-element-list/ft-element-list.vue b/src/renderer/components/ft-element-list/ft-element-list.vue index 28bd9be83a0c5..22c4a343fadad 100644 --- a/src/renderer/components/ft-element-list/ft-element-list.vue +++ b/src/renderer/components/ft-element-list/ft-element-list.vue @@ -10,6 +10,7 @@ :first-screen="index < 16" :layout="displayValue" :show-video-with-last-viewed-playlist="showVideoWithLastViewedPlaylist" + :use-channels-hidden-preference="useChannelsHiddenPreference" /> diff --git a/src/renderer/components/ft-icon-button/ft-icon-button.js b/src/renderer/components/ft-icon-button/ft-icon-button.js index eb08a55d8ac99..93c4db4df1421 100644 --- a/src/renderer/components/ft-icon-button/ft-icon-button.js +++ b/src/renderer/components/ft-icon-button/ft-icon-button.js @@ -94,8 +94,8 @@ export default defineComponent({ // wait until the dropdown is visible // then focus it so we can hide it automatically when it loses focus setTimeout(() => { - this.$refs.dropdown.focus() - }, 0) + this.$refs.dropdown?.focus() + }) } } else { this.$emit('click') diff --git a/src/renderer/components/ft-input/ft-input.js b/src/renderer/components/ft-input/ft-input.js index 763d72d807bd3..9df01584949f0 100644 --- a/src/renderer/components/ft-input/ft-input.js +++ b/src/renderer/components/ft-input/ft-input.js @@ -285,6 +285,10 @@ export default defineComponent({ this.$refs.input.focus() }, + blur() { + this.$refs.input.blur() + }, + ...mapActions([ 'getYoutubeUrlInfo' ]) diff --git a/src/renderer/components/ft-list-channel/ft-list-channel.js b/src/renderer/components/ft-list-channel/ft-list-channel.js index bd5409241cc47..db21d867c1bd8 100644 --- a/src/renderer/components/ft-list-channel/ft-list-channel.js +++ b/src/renderer/components/ft-list-channel/ft-list-channel.js @@ -53,11 +53,8 @@ export default defineComponent({ this.channelName = this.data.name this.id = this.data.id - if (this.hideChannelSubscriptions || this.data.subscribers == null) { - this.subscriberCount = null - } else { - this.subscriberCount = this.data.subscribers.replace(/ subscriber(s)?/, '') - } + this.subscriberCount = this.data.subscribers != null ? this.data.subscribers.replace(/ subscriber(s)?/, '') : null + if (this.data.videos === null) { this.videoCount = 0 } else { @@ -79,11 +76,7 @@ export default defineComponent({ this.channelName = this.data.author this.id = this.data.authorId - if (this.hideChannelSubscriptions) { - this.subscriberCount = null - } else { - this.subscriberCount = formatNumber(this.data.subCount) - } + this.subscriberCount = formatNumber(this.data.subCount) this.videoCount = formatNumber(this.data.videoCount) this.description = this.data.description } diff --git a/src/renderer/components/ft-list-channel/ft-list-channel.vue b/src/renderer/components/ft-list-channel/ft-list-channel.vue index 6ac9f8ed835dd..cfc85205005e1 100644 --- a/src/renderer/components/ft-list-channel/ft-list-channel.vue +++ b/src/renderer/components/ft-list-channel/ft-list-channel.vue @@ -26,7 +26,7 @@
{{ subscriberCount }} subscribers - diff --git a/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.js b/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.js index fae88932f3217..04ffbbc164df3 100644 --- a/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.js +++ b/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.js @@ -33,6 +33,10 @@ export default defineComponent({ type: Boolean, default: false }, + useChannelsHiddenPreference: { + type: Boolean, + default: true, + }, }, data: function () { return { @@ -44,17 +48,14 @@ export default defineComponent({ return this.$store.getters.getHideLiveStreams }, channelsHidden: function() { + // Some component users like channel view will have this disabled + if (!this.useChannelsHiddenPreference) { return [] } + return JSON.parse(this.$store.getters.getChannelsHidden) }, hideUpcomingPremieres: function () { return this.$store.getters.getHideUpcomingPremieres - } - }, - methods: { - onVisibilityChanged: function (visible) { - this.visible = visible }, - /** * Show or Hide results in the list * @@ -65,15 +66,18 @@ export default defineComponent({ if (!data.type) { return false } - if (data.type === 'video') { + if (data.type === 'video' || data.type === 'shortVideo') { if (this.hideLiveStreams && (data.liveNow || data.lengthSeconds == null)) { // hide livestreams return false } - if (this.hideUpcomingPremieres && // Observed for premieres in Local API Channels. - (data.durationText === 'PREMIERE' || + (data.premiereDate != null || + // Invidious API + // `premiereTimestamp` only available on premiered videos + // https://docs.invidious.io/api/common_types/#videoobject + data.premiereTimestamp != null || // viewCount is our only method of detecting premieres in RSS // data without sending an additional request. // If we ever get a better flag, use it here instead. @@ -86,18 +90,41 @@ export default defineComponent({ return false } } else if (data.type === 'channel') { - if (this.channelsHidden.includes(data.channelID) || this.channelsHidden.includes(data.name)) { + const attrsToCheck = [ + // Local API + data.id, + data.name, + // Invidious API + // https://docs.invidious.io/api/common_types/#channelobject + data.author, + data.authorId, + ] + if (attrsToCheck.some(a => a != null && this.channelsHidden.includes(a))) { // hide channels by author return false } } else if (data.type === 'playlist') { - if (this.channelsHidden.includes(data.authorId) || this.channelsHidden.includes(data.author)) { + const attrsToCheck = [ + // Local API + data.channelId, + data.channelName, + // Invidious API + // https://docs.invidious.io/api/common_types/#playlistobject + data.author, + data.authorId, + ] + if (attrsToCheck.some(a => a != null && this.channelsHidden.includes(a))) { // hide playlists by author return false } } return true } + }, + methods: { + onVisibilityChanged: function (visible) { + this.visible = visible + } } }) diff --git a/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js b/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js index 2ac01ba81357a..c255110b4b3a8 100644 --- a/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js +++ b/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js @@ -43,18 +43,35 @@ export default defineComponent({ type: Boolean, default: false, }, + useChannelsHiddenPreference: { + type: Boolean, + default: false, + }, }, data: function () { return { visible: false } }, + computed: { + channelsHidden() { + // Some component users like channel view will have this disabled + if (!this.useChannelsHiddenPreference) { return [] } + + return JSON.parse(this.$store.getters.getChannelsHidden) + }, + + shouldBeVisible() { + return !(this.channelsHidden.includes(this.data.authorId) || + this.channelsHidden.includes(this.data.author)) + } + }, created() { this.visible = this.initialVisibleState }, methods: { onVisibilityChanged: function (visible) { - if (visible) { + if (visible && this.shouldBeVisible) { this.visible = visible } } 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 df374042a5dba..13d4792692085 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.js +++ b/src/renderer/components/ft-list-video/ft-list-video.js @@ -10,6 +10,7 @@ import { toLocalePublicationString, toDistractionFreeTitle } from '../../helpers/utils' +import { deArrowData } from '../../helpers/sponsorblock' export default defineComponent({ name: 'FtListVideo', @@ -58,8 +59,8 @@ export default defineComponent({ return { id: '', title: '', - channelName: '', - channelId: '', + channelName: null, + channelId: null, viewCount: 0, parsedViewCount: '', uploadedTime: '', @@ -190,30 +191,34 @@ export default defineComponent({ { label: this.$t('Video.Open in Invidious'), value: 'openInvidious' - }, - { - type: 'divider' - }, - { - label: this.$t('Video.Copy YouTube Channel Link'), - value: 'copyYoutubeChannel' - }, - { - label: this.$t('Video.Copy Invidious Channel Link'), - value: 'copyInvidiousChannel' - }, - { - type: 'divider' - }, - { - label: this.$t('Video.Open Channel in YouTube'), - value: 'openYoutubeChannel' - }, - { - label: this.$t('Video.Open Channel in Invidious'), - value: 'openInvidiousChannel' } ) + if (this.channelId !== null) { + options.push( + { + type: 'divider' + }, + { + label: this.$t('Video.Copy YouTube Channel Link'), + value: 'copyYoutubeChannel' + }, + { + label: this.$t('Video.Copy Invidious Channel Link'), + value: 'copyInvidiousChannel' + }, + { + type: 'divider' + }, + { + label: this.$t('Video.Open Channel in YouTube'), + value: 'openYoutubeChannel' + }, + { + label: this.$t('Video.Open Channel in Invidious'), + value: 'openInvidiousChannel' + } + ) + } } return options @@ -316,6 +321,14 @@ export default defineComponent({ currentLocale: function () { return this.$i18n.locale.replace('_', '-') }, + + useDeArrowTitles: function () { + return this.$store.getters.getUseDeArrowTitles + }, + + deArrowCache: function () { + return this.$store.getters.getDeArrowCache(this.id) + } }, watch: { historyIndex() { @@ -327,6 +340,25 @@ export default defineComponent({ this.checkIfWatched() }, methods: { + getDeArrowDataEntry: async function() { + // Read from local cache or remote + // Write to cache if read from remote + if (!this.useDeArrowTitles) { return null } + + if (this.deArrowCache) { return this.deArrowCache } + + const videoId = this.id + const data = await deArrowData(this.id) + const cacheData = { videoId, title: null } + if (Array.isArray(data?.titles) && data.titles.length > 0 && (data.titles[0].locked || data.titles[0].votes > 0)) { + cacheData.title = data.titles[0].title + } + + // Save data to cache whether data available or not to prevent duplicate requests + this.$store.commit('addVideoToDeArrowCache', cacheData) + return cacheData + }, + handleExternalPlayer: function () { this.$emit('pause-player') @@ -397,13 +429,13 @@ export default defineComponent({ } }, - parseVideoData: function () { + parseVideoData: async function () { this.id = this.data.videoId - this.title = this.data.title + this.title = (await this.getDeArrowDataEntry())?.title ?? this.data.title // this.thumbnail = this.data.videoThumbnails[4].url - this.channelName = this.data.author - this.channelId = this.data.authorId + this.channelName = this.data.author ?? null + this.channelId = this.data.authorId ?? null this.duration = formatDurationAsTimestamp(this.data.lengthSeconds) this.description = this.data.description this.isLive = this.data.liveNow || this.data.lengthSeconds === 'undefined' 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 2e5967b383190..a6a039d2ae8dd 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.vue +++ b/src/renderer/components/ft-list-video/ft-list-video.vue @@ -78,13 +78,14 @@
{{ channelName }} diff --git a/src/renderer/components/ft-profile-selector/ft-profile-selector.js b/src/renderer/components/ft-profile-selector/ft-profile-selector.js index b14e222562c5f..24faf0fed6881 100644 --- a/src/renderer/components/ft-profile-selector/ft-profile-selector.js +++ b/src/renderer/components/ft-profile-selector/ft-profile-selector.js @@ -42,7 +42,7 @@ export default defineComponent({ // wait until the profile list is visible // then focus it so we can hide it automatically when it loses focus setTimeout(() => { - this.$refs.profileList.$el.focus() + this.$refs.profileList?.$el?.focus() }) } }, diff --git a/src/renderer/components/ft-select/ft-select.js b/src/renderer/components/ft-select/ft-select.js index e6050edadff1a..83ab2f523ac79 100644 --- a/src/renderer/components/ft-select/ft-select.js +++ b/src/renderer/components/ft-select/ft-select.js @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue' +import { defineComponent, nextTick } from 'vue' import FtTooltip from '../ft-tooltip/ft-tooltip.vue' import { sanitizeForHtmlId } from '../../helpers/accessibility' @@ -45,5 +45,17 @@ export default defineComponent({ sanitizedPlaceholder: function() { return sanitizeForHtmlId(this.placeholder) } + }, + watch: { + // update the selected value in the menu when the list of values changes + + // e.g. when you change the display language, the locations list gets updated + // as the locations list is sorted alphabetically for the language, the ordering can be different + // so we need to ensure that the correct location is selected after a language change + selectValues: function () { + nextTick(() => { + this.$refs.select.value = this.value + }) + } } }) diff --git a/src/renderer/components/ft-select/ft-select.vue b/src/renderer/components/ft-select/ft-select.vue index b935405f2f7ad..fb73a3b543238 100644 --- a/src/renderer/components/ft-select/ft-select.vue +++ b/src/renderer/components/ft-select/ft-select.vue @@ -2,6 +2,7 @@