diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 34f5ce3ee5439..248241a93065d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -20,6 +20,7 @@ updates: - "@eslint/*" - "yaml-eslint-parser" - "vue-eslint-parser" + - "neostandard" stylelint: patterns: - "stylelint" diff --git a/_scripts/ProcessLocalesPlugin.js b/_scripts/ProcessLocalesPlugin.js index d69ea4a0dbd02..d08bb3734f833 100644 --- a/_scripts/ProcessLocalesPlugin.js +++ b/_scripts/ProcessLocalesPlugin.js @@ -26,11 +26,11 @@ class ProcessLocalesPlugin { } this.outputDir = options.outputDir - /** @type {Map} */ + /** @type {Map} */ this.locales = new Map() this.localeNames = [] - /** @type {Map} */ + /** @type {Map} */ this.cache = new Map() this.filePaths = [] diff --git a/_scripts/getShakaLocales.js b/_scripts/getShakaLocales.js index 76ed42b2633c1..644a13a8ef6a1 100644 --- a/_scripts/getShakaLocales.js +++ b/_scripts/getShakaLocales.js @@ -32,7 +32,7 @@ function getMappings(shakaLocales, freeTubeLocales) { * @type {[string, string][]} * Using this structure as it gets passed to `new Map()` in the player component * The first element is the FreeTube locale, the second one is the shaka-player one - **/ + */ const mappings = [] for (const locale of freeTubeLocales) { diff --git a/eslint.config.mjs b/eslint.config.mjs index 5971c36f0f75d..29a7e2ecc4a96 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,6 +10,7 @@ import eslintPluginJsonc from 'eslint-plugin-jsonc' import eslintPluginYml from 'eslint-plugin-yml' import yamlEslintParser from 'yaml-eslint-parser' import neostandard from 'neostandard' +import jsdoc from 'eslint-plugin-jsdoc' import activeLocales from './static/locales/activeLocales.json' with { type: 'json' } @@ -40,6 +41,7 @@ export default [ ], plugins: { unicorn: eslintPluginUnicorn, + jsdoc, }, languageOptions: { @@ -115,6 +117,15 @@ export default [ '@intlify/vue-i18n/no-deprecated-tc': 'off', 'vue/require-explicit-emits': 'error', 'vue/no-unused-emit-declarations': 'error', + + 'jsdoc/check-alignment': 'error', + 'jsdoc/check-property-names': 'error', + 'jsdoc/check-param-names': 'error', + 'jsdoc/check-syntax': 'error', + 'jsdoc/check-template-names': 'error', + 'jsdoc/check-types': 'error', + 'jsdoc/no-bad-blocks': 'error', + 'jsdoc/no-multi-asterisks': 'error', }, }, diff --git a/package.json b/package.json index c24f4c088db64..bd6fe6a7dc83e 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "electron-builder": "^25.1.8", "eslint": "^9.11.1", "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.4.3", "eslint-plugin-jsonc": "^2.16.0", "eslint-plugin-unicorn": "^56.0.0", "eslint-plugin-vue": "^9.30.0", diff --git a/src/renderer/components/ChannelDetails/ChannelDetails.vue b/src/renderer/components/ChannelDetails/ChannelDetails.vue index 296457569d381..8b70d92a76e70 100644 --- a/src/renderer/components/ChannelDetails/ChannelDetails.vue +++ b/src/renderer/components/ChannelDetails/ChannelDetails.vue @@ -280,14 +280,17 @@ const props = defineProps({ const emit = defineEmits(['change-tab', 'search', 'subscribed']) +/** @type {import('vue').ComputedRef} */ const hideChannelSubscriptions = computed(() => { return store.getters.getHideChannelSubscriptions }) +/** @type {import('vue').ComputedRef} */ const hideSharingActions = computed(() => { return store.getters.getHideSharingActions }) +/** @type {import('vue').ComputedRef} */ const hideUnsubscribeButton = computed(() => { return store.getters.getHideUnsubscribeButton }) diff --git a/src/renderer/components/FtCommunityPost/FtCommunityPost.vue b/src/renderer/components/FtCommunityPost/FtCommunityPost.vue index 677cba360d710..c3292e2fb91f8 100644 --- a/src/renderer/components/FtCommunityPost/FtCommunityPost.vue +++ b/src/renderer/components/FtCommunityPost/FtCommunityPost.vue @@ -198,10 +198,12 @@ const props = defineProps({ }, }) +/** @type {import('vue').ComputedRef<'grid' | 'list'>} */ const listType = computed(() => { return store.getters.getListType }) +/** @type {import('vue').ComputedRef} */ const forbiddenTitles = computed(() => { if (!props.hideForbiddenTitles) { return [] } return JSON.parse(store.getters.getForbiddenTitles) @@ -211,10 +213,12 @@ const hideVideo = computed(() => { return forbiddenTitles.value.some((text) => props.data.postContent.content.title?.toLowerCase().includes(text.toLowerCase())) }) +/** @type {import('vue').ComputedRef<'local' | 'invidious'>} */ const backendPreference = computed(() => { return store.getters.getBackendPreference }) +/** @type {import('vue').ComputedRef} */ const backendFallback = computed(() => { return store.getters.getBackendFallback }) @@ -226,11 +230,13 @@ const isInvidiousAllowed = computed(() => { let postType = '' let postText = '' let postId = '' +/** @type {string[]?} */ let authorThumbnails = null let postContent = '' let author = '' let authorId = '' let voteCount = 0 +/** @type {number?} */ let commentCount = null parseCommunityData() diff --git a/src/renderer/components/FtElementList/FtElementList.vue b/src/renderer/components/FtElementList/FtElementList.vue index e6f8f3feed11a..13204b53a51e9 100644 --- a/src/renderer/components/FtElementList/FtElementList.vue +++ b/src/renderer/components/FtElementList/FtElementList.vue @@ -104,10 +104,12 @@ const props = defineProps({ const emit = defineEmits(['move-video-down', 'move-video-up', 'remove-from-playlist']) +/** @type {import('vue').ComputedRef<'grid' | 'list'>} */ const listType = computed(() => { return store.getters.getListType }) +/** @type {import('vue').ComputedRef<'grid' | 'list'>} */ const displayValue = computed(() => { return props.display === '' ? listType.value : props.display }) diff --git a/src/renderer/components/FtListChannel/FtListChannel.vue b/src/renderer/components/FtListChannel/FtListChannel.vue index 75b7e9dc57b7d..b96c76d70b60b 100644 --- a/src/renderer/components/FtListChannel/FtListChannel.vue +++ b/src/renderer/components/FtListChannel/FtListChannel.vue @@ -91,14 +91,17 @@ const props = defineProps({ } }) +/** @type {import('vue').ComputedRef} */ const currentInvidiousInstanceUrl = computed(() => { return store.getters.getCurrentInvidiousInstanceUrl }) +/** @type {import('vue').ComputedRef<'grid' | 'list'>} */ const listType = computed(() => { return store.getters.getListType }) +/** @type {import('vue').ComputedRef} */ const hideChannelSubscriptions = computed(() => { return store.getters.getHideChannelSubscriptions }) @@ -106,8 +109,11 @@ const hideChannelSubscriptions = computed(() => { let id = '' let thumbnail = '' let name = '' +/** @type {number?} */ let subscriberCount = null +/** @type {number?} */ let videoCount = null +/** @type {string?} */ let handle = null let description = '' @@ -155,6 +161,7 @@ function parseLocalData() { function parseInvidiousData() { // Can be prefixed with `https://` or `//` (protocol relative) + /** @type {string} */ const thumbnailUrl = props.data.authorThumbnails[2].url thumbnail = youtubeImageUrlToInvidious(thumbnailUrl, currentInvidiousInstanceUrl.value) diff --git a/src/renderer/components/FtListHashtag/FtListHashtag.vue b/src/renderer/components/FtListHashtag/FtListHashtag.vue index bf39d56f2cfd6..21ad3a36573f7 100644 --- a/src/renderer/components/FtListHashtag/FtListHashtag.vue +++ b/src/renderer/components/FtListHashtag/FtListHashtag.vue @@ -67,13 +67,18 @@ const props = defineProps({ } }) +/** @type {import('vue').ComputedRef<'list'| 'grid'>} */ const listType = computed(() => { return store.getters.getListType }) +/** @type {string} */ const title = props.data.title +/** @type {number} */ const channelCount = props.data.channelCount +/** @type {number} */ const videoCount = props.data.videoCount +/** @type {string} */ const url = `/hashtag/${encodeURIComponent(title.substring(1))}` const formattedChannelCount = computed(() => { diff --git a/src/renderer/components/FtListLazyWrapper/FtListLazyWrapper.vue b/src/renderer/components/FtListLazyWrapper/FtListLazyWrapper.vue index ceca6b38953cd..786054dfbea6e 100644 --- a/src/renderer/components/FtListLazyWrapper/FtListLazyWrapper.vue +++ b/src/renderer/components/FtListLazyWrapper/FtListLazyWrapper.vue @@ -141,18 +141,22 @@ const props = defineProps({ const emit = defineEmits(['move-video-down', 'move-video-up', 'remove-from-playlist']) +/** @type {import('vue').ComputedRef<'video' | 'shortVideo' | 'channel' | 'playlist' | 'community'>} */ const finalDataType = computed(() => { return props.data.type ?? props.dataType }) +/** @type {import('vue').ComputedRef} */ const hideLiveStreams = computed(() => { return store.getters.getHideLiveStreams }) +/** @type {import('vue').ComputedRef} */ const hideUpcomingPremieres = computed(() => { return store.getters.getHideUpcomingPremieres }) +/** @type {import('vue').ComputedRef<{name : string, preferredName: string, icon: string}[]>} */ const channelsHidden = computed(() => { // Some component users like channel view will have this disabled if (!props.useChannelsHiddenPreference) { return [] } @@ -166,6 +170,7 @@ const channelsHidden = computed(() => { }) }) +/** @type {string[]} */ const forbiddenTitles = computed(() => { if (!props.hideForbiddenTitles) { return [] } return JSON.parse(store.getters.getForbiddenTitles) diff --git a/src/renderer/components/WatchVideoChapters/WatchVideoChapters.vue b/src/renderer/components/WatchVideoChapters/WatchVideoChapters.vue index ac27a48059e7d..7e63aab8c7ac0 100644 --- a/src/renderer/components/WatchVideoChapters/WatchVideoChapters.vue +++ b/src/renderer/components/WatchVideoChapters/WatchVideoChapters.vue @@ -106,10 +106,12 @@ const currentChapter = computed(() => { return props.chapters[currentIndex.value] }) +/** @type {import('vue').ComputedRef} */ const currentTitle = computed(() => { return currentChapter.value.title }) +/** @type {import('vue').ComputedRef} */ const compact = computed(() => { return !props.chapters[0].thumbnail }) diff --git a/src/renderer/components/WatchVideoDescription/WatchVideoDescription.vue b/src/renderer/components/WatchVideoDescription/WatchVideoDescription.vue index 37a12ea6eafca..d0ab063901680 100644 --- a/src/renderer/components/WatchVideoDescription/WatchVideoDescription.vue +++ b/src/renderer/components/WatchVideoDescription/WatchVideoDescription.vue @@ -60,6 +60,7 @@ function onTimestamp(timestamp) { /** * @param {string} descriptionText + * @returns {string} */ function parseDescriptionHtml(descriptionText) { return descriptionText diff --git a/src/renderer/components/WatchVideoRecommendations/WatchVideoRecommendations.vue b/src/renderer/components/WatchVideoRecommendations/WatchVideoRecommendations.vue index 103422236a555..44e5ffe3c4e1f 100644 --- a/src/renderer/components/WatchVideoRecommendations/WatchVideoRecommendations.vue +++ b/src/renderer/components/WatchVideoRecommendations/WatchVideoRecommendations.vue @@ -46,6 +46,7 @@ defineProps({ } }) +/** @type {import('vue').ComputedRef} */ const playNextVideo = computed(() => { return store.getters.getPlayNextVideo }) 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 5f3c5e5be6266..839e90cf533b2 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 @@ -355,7 +355,7 @@ export default defineComponent({ * color: string, * skip: 'autoSkip' | 'promptToSkip' | 'showInSeekBar' | 'doNothing' * } - * }} */ + }} */ const categoryData = {} sponsorCategories.forEach(x => { @@ -537,7 +537,7 @@ export default defineComponent({ * @param {'dash'|'audio'|'legacy'} format * @param {boolean} useAutoQuality * @returns {shaka.extern.PlayerConfiguration} - **/ + */ function getPlayerConfig(format, useAutoQuality = false) { return { // YouTube uses these values and they seem to work well in FreeTube too, @@ -1870,7 +1870,7 @@ export default defineComponent({ /** * @param {WheelEvent} event - * */ + */ function mouseScrollVolume(event) { if (!event.ctrlKey && !event.metaKey) { event.preventDefault() @@ -2145,7 +2145,7 @@ export default defineComponent({ /** * @param {shaka.util.Error} error * @param {string} context - * @param {object=} details + * @param {object?} details */ function handleError(error, context, details) { logShakaError(error, context, props.videoId, details) diff --git a/src/renderer/components/ft-shaka-video-player/player-components/AudioTrackSelection.js b/src/renderer/components/ft-shaka-video-player/player-components/AudioTrackSelection.js index 66cb393db3107..6a1ea8435dc76 100644 --- a/src/renderer/components/ft-shaka-video-player/player-components/AudioTrackSelection.js +++ b/src/renderer/components/ft-shaka-video-player/player-components/AudioTrackSelection.js @@ -42,7 +42,7 @@ export class AudioTrackSelection extends shaka.ui.SettingsMenu { /** * @private - * @param {shaka.extern.TrackList=} tracks + * @param {shaka.extern.TrackList?} tracks */ updateAudioTracks_(tracks) { if (!tracks) { diff --git a/src/renderer/components/ft-share-button/ft-share-button.js b/src/renderer/components/ft-share-button/ft-share-button.js index 6d9eb6f73f149..9130a6355620e 100644 --- a/src/renderer/components/ft-share-button/ft-share-button.js +++ b/src/renderer/components/ft-share-button/ft-share-button.js @@ -19,7 +19,7 @@ export default defineComponent({ * Allows to render the dropdown conditionally * 'Channel' will exclude embed links * 'Video' (default) keeps the original behaviour - **/ + */ type: String, default: 'Video' }, diff --git a/src/renderer/helpers/accessibility.js b/src/renderer/helpers/accessibility.js index 880bf0ac45373..48f06552b7938 100644 --- a/src/renderer/helpers/accessibility.js +++ b/src/renderer/helpers/accessibility.js @@ -1,3 +1,7 @@ +/** + * @param {string} attribute + * @returns {string} + */ export function sanitizeForHtmlId(attribute) { return attribute.replaceAll(/\s+/g, '') } diff --git a/src/renderer/helpers/api/invidious.js b/src/renderer/helpers/api/invidious.js index d46d4079dd081..d6b3785efe1bc 100644 --- a/src/renderer/helpers/api/invidious.js +++ b/src/renderer/helpers/api/invidious.js @@ -142,6 +142,11 @@ export async function invidiousGetCommentReplies({ id, replyToken }) { return { commentData: parseInvidiousCommentData(response), continuation: response.continuation ?? null } } +/** + * @param {string} url + * @param {string?} currentInstance + * @returns {string} + */ export function youtubeImageUrlToInvidious(url, currentInstance = null) { if (url == null) { return null diff --git a/src/renderer/helpers/api/local.js b/src/renderer/helpers/api/local.js index 6eb23dbf838ae..13a237ee5253a 100644 --- a/src/renderer/helpers/api/local.js +++ b/src/renderer/helpers/api/local.js @@ -550,15 +550,15 @@ export async function getLocalArtistTopicChannelReleasesContinuation(channel, co * @param {boolean} onlyIdNameThumbnail */ export function parseLocalChannelHeader(channel, onlyIdNameThumbnail = false) { - /** @type {string=} */ + /** @type {string?} */ let id /** @type {string} */ let name - /** @type {string=} */ + /** @type {string?} */ let thumbnailUrl - /** @type {string=} */ + /** @type {string?} */ let bannerUrl - /** @type {string=} */ + /** @type {string?} */ let subscriberText /** @type {string[]} */ const tags = [] @@ -766,7 +766,7 @@ export function parseLocalChannelShorts(shorts, channelId, channelName) { /** * @param {import('youtubei.js').YTNodes.Playlist|import('youtubei.js').YTNodes.GridPlaylist|import('youtubei.js').YTNodes.LockupView} playlist * @param {string} channelId - * @param {string} chanelName + * @param {string} channelName */ export function parseLocalListPlaylist(playlist, channelId = undefined, channelName = undefined) { if (playlist.type === 'LockupView') { diff --git a/src/renderer/helpers/channels.js b/src/renderer/helpers/channels.js index 3161e5f8dcec8..39c32089336f6 100644 --- a/src/renderer/helpers/channels.js +++ b/src/renderer/helpers/channels.js @@ -4,11 +4,11 @@ import { getLocalChannel, parseLocalChannelHeader } from './api/local' /** * @param {string} id * @param {{ -* preference: string, -* fallback: boolean, -* invalid: boolean, -* }} backendOptions -*/ + * preference: string, + * fallback: boolean, + * invalid: boolean, + * }} backendOptions + */ async function findChannelById(id, backendOptions) { try { if (!process.env.SUPPORTS_LOCAL_API || backendOptions.preference === 'invidious') { @@ -37,11 +37,11 @@ async function findChannelById(id, backendOptions) { /** * @param {string} id * @param {{ -* preference: string, -* fallback: boolean, -* }} backendOptions -* @returns {Promise<{icon: string, iconHref: string, preferredName: string} | { invalidId: boolean }>} -*/ + * preference: string, + * fallback: boolean, + * }} backendOptions + * @returns {Promise<{icon: string, iconHref: string, preferredName: string} | { invalidId: boolean }>} + */ export async function findChannelTagInfo(id, backendOptions) { if (!checkYoutubeChannelId(id)) return { invalidId: true } try { diff --git a/src/renderer/helpers/player/utils.js b/src/renderer/helpers/player/utils.js index 983a275bba2b3..0c68dcae097e0 100644 --- a/src/renderer/helpers/player/utils.js +++ b/src/renderer/helpers/player/utils.js @@ -9,7 +9,7 @@ import { sponsorBlockSkipSegments } from '../sponsorblock' * @param {shaka.util.Error} error * @param {string} context * @param {string} videoId - * @param {object=} details + * @param {object?} details */ export function logShakaError(error, context, videoId, details) { const { Severity, Category, Code } = shaka.util.Error @@ -189,13 +189,13 @@ export function sortCaptions(captions) { * This function cleans it up, so that we can use it. * * Here is a list of things this function does: - * * Removes bogus roles and labels - * * Extracts the languages from the audio URLs if available and adds it to the adapation sets - * * Adds roles and labels when possible to add support for multiple audio tracks + * - Removes bogus roles and labels + * - Extracts the languages from the audio URLs if available and adds it to the adapation sets + * - Adds roles and labels when possible to add support for multiple audio tracks * * Things this function does not do: - * * Separate DRC (Stable Volume) from their original counterparts - * * Tag HDR video streams (Invidious puts all video streams in the same adaptation set, + * - Separate DRC (Stable Volume) from their original counterparts + * - Tag HDR video streams (Invidious puts all video streams in the same adaptation set, * to tag HDR and SDR streams we would have to separate them out into multiple adaptation sets) * @param {shaka.extern.xml.Node[]} periods */ diff --git a/src/renderer/helpers/strings.js b/src/renderer/helpers/strings.js index 6c9e79465b8e0..2a95fb9491fb1 100644 --- a/src/renderer/helpers/strings.js +++ b/src/renderer/helpers/strings.js @@ -26,6 +26,9 @@ export function isKeyboardEventKeyPrintableChar(eventKey) { return false } +/** + * @param {string} title + */ export function translateWindowTitle(title) { switch (title) { case 'Subscriptions': diff --git a/src/renderer/helpers/subscriptions.js b/src/renderer/helpers/subscriptions.js index f41205244d3aa..41afe8c061ba3 100644 --- a/src/renderer/helpers/subscriptions.js +++ b/src/renderer/helpers/subscriptions.js @@ -65,7 +65,7 @@ export function updateVideoListAfterProcessing(videos) { /** * @param {string} rssString * @param {string} channelId -*/ + */ export async function parseYouTubeRSSFeed(rssString, channelId) { // doesn't need to be asynchronous, but doing it allows us to do the relatively slow DOM querying in parallel try { diff --git a/src/renderer/helpers/utils.js b/src/renderer/helpers/utils.js index 12871b1779bb6..a96dda78e2a14 100644 --- a/src/renderer/helpers/utils.js +++ b/src/renderer/helpers/utils.js @@ -12,6 +12,10 @@ export const CHANNEL_HANDLE_REGEX = /^@[\w.-]{3,30}$/ const PUBLISHED_TEXT_REGEX = /(\d+)\s?([a-z]+)/i +/** + * @param {string} sortPreference + * @returns {string[]} + */ export function getIconForSortPreference(sortPreference) { switch (sortPreference) { case 'name_descending': @@ -192,6 +196,11 @@ export function toLocalePublicationString ({ publishText, isLive = false, isUpco return i18n.t('Video.Publicationtemplate', { number: match[1], unit }) } +/** + * @param {import('youtubei.js/dist/src/parser/classes/PlayerStoryboardSpec').StoryboardData} storyboard + * @param {number} videoLengthSeconds + * @returns {string} + */ export function buildVTTFileLocally(storyboard, videoLengthSeconds) { let vttString = 'WEBVTT\n\n' // how many images are in one image @@ -249,6 +258,11 @@ export function buildVTTFileLocally(storyboard, videoLengthSeconds) { return vttString } +/** + * @param {string} message + * @param {number} time + * @param {Function} action + */ export function showToast(message, time = null, action = null) { FtToastEvents.dispatchEvent(new CustomEvent('toast-open', { detail: { @@ -260,13 +274,14 @@ export function showToast(message, time = null, action = null) { } /** - * This writes to the clipboard. If an error occurs during the copy, - * a toast with the error is shown. If the copy is successful and - * there is a success message, a toast with that message is shown. - * @param {string} content the content to be copied to the clipboard - * @param {null|string} messageOnSuccess the message to be displayed as a toast when the copy succeeds (optional) - * @param {null|string} messageOnError the message to be displayed as a toast when the copy fails (optional) - */ + * This writes to the clipboard. If an error occurs during the copy, + * a toast with the error is shown. If the copy is successful and + * there is a success message, a toast with that message is shown. + * @param {string} content the content to be copied to the clipboard + * @param {object} [options] - Optional settings for the copy operation. + * @param {null|string} options.messageOnSuccess the message to be displayed as a toast when the copy succeeds (optional) + * @param {null|string} options.messageOnError the message to be displayed as a toast when the copy fails (optional) + */ export async function copyToClipboard(content, { messageOnSuccess = null, messageOnError = null } = {}) { if (navigator.clipboard !== undefined && window.isSecureContext) { try { @@ -370,7 +385,7 @@ export async function showOpenDialog (options) { /** * @param {object} response the response from `showOpenDialog` * @param {number} index which file to read (defaults to the first in the response) - * @returns the text contents of the selected file + * @returns {string} the text contents of the selected file */ export function readFileFromDialog(response, index = 0) { return new Promise((resolve, reject) => { @@ -396,6 +411,10 @@ export function readFileFromDialog(response, index = 0) { }) } +/** + * @param {{defaultPath: string, filters: {name: string, extensions: string[]}[]}} options + * @returns { Promise | {canceled: boolean?, filePath: string } | { canceled: boolean?, handle?: Promise }} + */ export async function showSaveDialog (options) { if (process.env.IS_ELECTRON) { const { ipcRenderer } = require('electron') @@ -423,10 +442,10 @@ export async function showSaveDialog (options) { } /** -* Write to a file picked out from the `showSaveDialog` picker -* @param {object} response the response from `showSaveDialog` -* @param {string} content the content to be written to the file selected by the dialog -*/ + * Write to a file picked out from the `showSaveDialog` picker + * @param {object} response the response from `showSaveDialog` + * @param {string} content the content to be written to the file selected by the dialog + */ export async function writeFileFromDialog (response, content) { if (process.env.IS_ELECTRON) { const { filePath } = response @@ -475,7 +494,11 @@ export function createWebURL(path) { return `${origin}${windowPath}/${path}` } -// strip html tags but keep
, , , , , +/** + * strip html tags but keep
, , , , , + * @param {string} value + * @returns {string} + */ export function stripHTML(value) { return value.replaceAll(/(<(?!br|\/?[abis]|img>)([^>]+)>)/gi, '') } @@ -522,6 +545,11 @@ export function formatDurationAsTimestamp(lengthSeconds) { return timestamp } +/** + * @param {{sortBy? : string, time?: string, duration?: string, features: string[]}?} filtersA + * @param {{sortBy? : string, time?: string, duration?: string, features: string[]}?} filtersB + * @returns {boolean} + */ export function searchFiltersMatch(filtersA, filtersB) { return filtersA?.sortBy === filtersB?.sortBy && filtersA?.time === filtersB?.time && @@ -530,6 +558,10 @@ export function searchFiltersMatch(filtersA, filtersB) { filtersA?.features?.length === filtersB?.features?.length && filtersA?.features?.every((val, index) => val === filtersB?.features[index]) } +/** + * @param {string} filenameOriginal + * @returns {string} + */ export function replaceFilenameForbiddenChars(filenameOriginal) { let filenameNew = filenameOriginal let forbiddenChars = {} @@ -563,6 +595,9 @@ export function replaceFilenameForbiddenChars(filenameOriginal) { return filenameNew } +/** + * @returns {Promise} + */ export async function getSystemLocale() { let locale if (process.env.IS_ELECTRON) { @@ -715,6 +750,11 @@ export function toDistractionFreeTitle(title, minUpperCase = 3) { .replace(reg, x => capitalizedWord(x.toLowerCase())) } +/** + * @param {number} number + * @param {Intl.NumberFormatOptions?} options + * @returns {string} + */ export function formatNumber(number, options = undefined) { return Intl.NumberFormat([i18n.locale, 'en'], options).format(number) } @@ -730,6 +770,13 @@ export function getTodayDateStrLocalTimezone() { return timeNowStr.split('T')[0] } +/** + * + * @param {number} date + * @param {boolean} hideSeconds + * @param {boolean} useThirtyDayMonths + * @returns {string} + */ export function getRelativeTimeFromDate(date, hideSeconds = false, useThirtyDayMonths = true) { if (!date) { return '' @@ -800,8 +847,9 @@ export function escapeHTML(untrusted) { /** * Performs a deep copy of a javascript object - * @param {Object} obj - * @returns {Object} + * @template T + * @param {T} obj + * @returns {T} */ export function deepCopy(obj) { return JSON.parse(JSON.stringify(obj)) @@ -811,7 +859,8 @@ export function deepCopy(obj) { * Check if the `name` of the error is `TimeoutError` to know if the error was caused by a timeout or something else. * @param {number} timeoutMs * @param {RequestInfo|URL} input - * @param {RequestInit=} init + * @param {RequestInit?} init + * @returns {Promise} */ export async function fetchWithTimeout(timeoutMs, input, init) { const timeoutSignal = AbortSignal.timeout(timeoutMs) @@ -839,6 +888,10 @@ export async function fetchWithTimeout(timeoutMs, input, init) { } } +/** + * @param {KeyboardEvent} event + * @param {HTMLInputElement} inputElement + */ export function ctrlFHandler(event, inputElement) { switch (event.key) { case 'F': @@ -879,11 +932,13 @@ export function base64EncodeUtf8(text) { * @param {string} channelId * @param {'all'} type * @returns {string} -* + * * @param {string} channelId * @param {'all' | 'videos' | 'live' | 'shorts'} type - * @param {'newest' | 'popular'} sortBy + * @param {('newest' | 'popular')?} [sortBy] + * @returns {string} */ + export function getChannelPlaylistId(channelId, type, sortBy) { switch (type) { case 'videos': diff --git a/src/renderer/views/Channel/Channel.js b/src/renderer/views/Channel/Channel.js index 2230ce23bda46..33fd2307a7009 100644 --- a/src/renderer/views/Channel/Channel.js +++ b/src/renderer/views/Channel/Channel.js @@ -712,7 +712,7 @@ export default defineComponent({ try { /** * @type {import('youtubei.js').YT.Channel} - */ + */ const channel = this.channelInstance const about = await channel.getAbout() @@ -784,7 +784,7 @@ export default defineComponent({ } else { /** * @type {import('youtubei.js').YT.Channel} - */ + */ const channel = this.channelInstance let videosTab = await channel.getVideos() @@ -934,7 +934,7 @@ export default defineComponent({ try { /** * @type {import('youtubei.js').YT.Channel} - */ + */ const channel = this.channelInstance let liveTab = await channel.getLiveStreams() @@ -1426,7 +1426,7 @@ export default defineComponent({ try { /** * @type {import('youtubei.js').YT.Channel} - */ + */ const channel = this.channelInstance if (this.isArtistTopicChannel) { @@ -1561,7 +1561,7 @@ export default defineComponent({ try { /** * @type {import('youtubei.js').YT.Channel} - */ + */ const channel = this.channelInstance const podcastTab = await channel.getPodcasts() diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 7f93657e75c86..369b03d6a53b5 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -722,7 +722,8 @@ export default defineComponent({ } if (result.storyboards?.type === 'PlayerStoryboardSpec') { - let source = result.storyboards.boards + /** @type {import('youtubei.js/dist/src/parser/classes/PlayerStoryboardSpec').StoryboardData[]} */ + let source = result.storyboardSpec.boards if (window.innerWidth < 500) { source = source.filter((board) => board.thumbnail_height <= 90) } @@ -1485,6 +1486,10 @@ export default defineComponent({ return result.adaptiveFormats }, + /** + * @param {import('youtubei.js/dist/src/parser/classes/PlayerStoryboardSpec').StoryboardData} storyboardInfo + * @returns {string} + */ createLocalStoryboardUrls: function (storyboardInfo) { const results = buildVTTFileLocally(storyboardInfo, this.videoLengthSeconds) diff --git a/yarn.lock b/yarn.lock index 7743085a3ba92..a9888ea607362 100644 --- a/yarn.lock +++ b/yarn.lock @@ -978,6 +978,15 @@ minimatch "^9.0.3" plist "^3.1.0" +"@es-joy/jsdoccomment@~0.49.0": + version "0.49.0" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz#e5ec1eda837c802eca67d3b29e577197f14ba1db" + integrity sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q== + dependencies: + comment-parser "1.4.1" + esquery "^1.6.0" + jsdoc-type-pratt-parser "~4.1.0" + "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -2277,6 +2286,11 @@ app-builder-lib@25.1.8: resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== +are-docs-informative@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/are-docs-informative/-/are-docs-informative-0.0.2.tgz#387f0e93f5d45280373d387a59d34c96db321963" + integrity sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig== + are-we-there-yet@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" @@ -2996,6 +3010,11 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +comment-parser@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.1.tgz#bdafead37961ac079be11eb7ec65c4d021eaf9cc" + integrity sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg== + common-path-prefix@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" @@ -3330,7 +3349,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.7: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.6, debug@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -3988,6 +4007,11 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.2.1.tgz#ba303831f63e6a394983fde2f97ad77b22324527" integrity sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg== +es-module-lexer@^1.5.3: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + es-object-atoms@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" @@ -4138,6 +4162,23 @@ eslint-plugin-import@^2.31.0: string.prototype.trimend "^1.0.8" tsconfig-paths "^3.15.0" +eslint-plugin-jsdoc@^50.4.3: + version "50.4.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.4.3.tgz#38adf595555933775943771e906422b25cdfc780" + integrity sha512-uWtwFxGRv6B8sU63HZM5dAGDhgsatb+LONwmILZJhdRALLOkCX2HFZhdL/Kw2ls8SQMAVEfK+LmnEfxInRN8HA== + dependencies: + "@es-joy/jsdoccomment" "~0.49.0" + are-docs-informative "^0.0.2" + comment-parser "1.4.1" + debug "^4.3.6" + escape-string-regexp "^4.0.0" + espree "^10.1.0" + esquery "^1.6.0" + parse-imports "^2.1.1" + semver "^7.6.3" + spdx-expression-parse "^4.0.0" + synckit "^0.9.1" + eslint-plugin-jsonc@^2.16.0: version "2.16.0" resolved "https://registry.yarnpkg.com/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.16.0.tgz#e90eca15aa2e172f5aca52a77fc8c819f52862d7" @@ -5964,6 +6005,11 @@ jsbn@1.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== +jsdoc-type-pratt-parser@~4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz#ff6b4a3f339c34a6c188cbf50a16087858d22113" + integrity sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg== + jsesc@^3.0.2, jsesc@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" @@ -7036,6 +7082,14 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-imports@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/parse-imports/-/parse-imports-2.2.1.tgz#0a6e8b5316beb5c9905f50eb2bbb8c64a4805642" + integrity sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ== + dependencies: + es-module-lexer "^1.5.3" + slashes "^3.0.12" + parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -8341,6 +8395,11 @@ slash@^5.1.0: resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== +slashes@^3.0.12: + version "3.0.12" + resolved "https://registry.yarnpkg.com/slashes/-/slashes-3.0.12.tgz#3d664c877ad542dc1509eaf2c50f38d483a6435a" + integrity sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA== + slice-ansi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" @@ -8451,6 +8510,14 @@ spdx-expression-parse@^3.0.0: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" +spdx-expression-parse@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz#a23af9f3132115465dac215c099303e4ceac5794" + integrity sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + spdx-license-ids@^3.0.0: version "3.0.11" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" @@ -8853,6 +8920,14 @@ synckit@^0.9.0: "@pkgr/core" "^0.1.0" tslib "^2.6.2" +synckit@^0.9.1: + version "0.9.2" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62" + integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + table@^6.8.2: version "6.8.2" resolved "https://registry.yarnpkg.com/table/-/table-6.8.2.tgz#c5504ccf201213fa227248bdc8c5569716ac6c58"