Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking

This commit is contained in:
Jason Henriquez 2024-06-15 08:43:39 -05:00
commit 13ba73e6f1
37 changed files with 631 additions and 183 deletions

View File

@ -92,7 +92,7 @@
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
"electron": "^30.0.9",
"electron": "^31.0.1",
"electron-builder": "^24.13.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",

View File

@ -42,8 +42,8 @@ export default defineComponent({
this.selectedValues = this.initialValues
},
methods: {
removeSelectedValues: function() {
this.selectedValues = []
setSelectedValues: function(arr) {
this.selectedValues = arr
},
change: function(event) {
const targ = event.target

View File

@ -16,7 +16,7 @@
:disabled="disabled"
class="checkbox"
type="checkbox"
:checked="initialValues.includes(values[index]) ?? null"
:checked="selectedValues.includes(values[index]) ?? null"
@change="change"
>
<label

View File

@ -33,6 +33,25 @@ export default defineComponent({
newPlaylistVideoObject: function () {
return this.$store.getters.getNewPlaylistVideoObject
},
playlistNameEmpty() {
return this.playlistName === ''
},
playlistNameBlank() {
return !this.playlistNameEmpty && this.playlistName.trim() === ''
},
playlistWithNameExists() {
// Don't show the message with no name input
const playlistName = this.playlistName
if (this.playlistName === '') { return false }
return this.allPlaylists.some((playlist) => {
return playlist.playlistName === playlistName
})
},
playlistPersistenceDisabled() {
return this.playlistNameEmpty || this.playlistNameBlank || this.playlistWithNameExists
},
},
mounted: function () {
this.playlistName = this.newPlaylistVideoObject.title
@ -40,19 +59,19 @@ export default defineComponent({
nextTick(() => this.$refs.playlistNameInput.focus())
},
methods: {
createNewPlaylist: function () {
if (this.playlistName === '') {
showToast(this.$t('User Playlists.SinglePlaylistView.Toast["Playlist name cannot be empty. Please input a name."]'))
handlePlaylistNameInput(input) {
if (input.trim() === '') {
// Need to show message for blank input
this.playlistName = input
return
}
const nameExists = this.allPlaylists.findIndex((playlist) => {
return playlist.playlistName === this.playlistName
})
if (nameExists !== -1) {
showToast(this.$t('User Playlists.CreatePlaylistPrompt.Toast["There is already a playlist with this name. Please pick a different name."]'))
return
}
this.playlistName = input.trim()
},
createNewPlaylist: function () {
// Still possible to attempt to create via pressing enter
if (this.playlistPersistenceDisabled) { return }
const playlistObject = {
playlistName: this.playlistName,

View File

@ -15,13 +15,24 @@
:value="playlistName"
:maxlength="255"
class="playlistNameInput"
@input="(input) => playlistName = input"
@input="handlePlaylistNameInput"
@click="createNewPlaylist"
/>
</ft-flex-box>
<ft-flex-box v-if="playlistNameBlank">
<p>
{{ $t('User Playlists.SinglePlaylistView.Toast["Playlist name cannot be empty. Please input a name."]') }}
</p>
</ft-flex-box>
<ft-flex-box v-if="playlistWithNameExists">
<p>
{{ $t('User Playlists.CreatePlaylistPrompt.Toast["There is already a playlist with this name. Please pick a different name."]') }}
</p>
</ft-flex-box>
<ft-flex-box>
<ft-button
:label="$t('User Playlists.CreatePlaylistPrompt.Create')"
:disabled="playlistPersistenceDisabled"
@click="createNewPlaylist"
/>
<ft-button

View File

@ -98,7 +98,6 @@ export default defineComponent({
lengthSeconds: 0,
duration: '',
description: '',
watchProgress: 0,
published: undefined,
isLive: false,
is4k: false,
@ -119,6 +118,14 @@ export default defineComponent({
return typeof this.historyEntry !== 'undefined'
},
watchProgress: function () {
if (!this.historyEntryExists || !this.saveWatchedProgress) {
return 0
}
return this.historyEntry.watchProgress
},
listType: function () {
return this.$store.getters.getListType
},
@ -494,9 +501,6 @@ export default defineComponent({
},
},
watch: {
historyEntry() {
this.checkIfWatched()
},
showAddToPlaylistPrompt(value) {
if (value) { return }
// Execute on prompt close
@ -507,7 +511,6 @@ export default defineComponent({
},
created: function () {
this.parseVideoData()
this.checkIfWatched()
if ((this.useDeArrowTitles || this.useDeArrowThumbnails) && !this.deArrowCache) {
this.fetchDeArrowData()
@ -697,19 +700,6 @@ export default defineComponent({
}
},
checkIfWatched: function () {
if (this.historyEntryExists) {
const historyEntry = this.historyEntry
if (this.saveWatchedProgress) {
// For UX consistency, no progress reading if writing disabled
this.watchProgress = historyEntry.watchProgress
}
} else {
this.watchProgress = 0
}
},
markAsWatched: function () {
const videoData = {
videoId: this.id,
@ -733,8 +723,6 @@ export default defineComponent({
this.removeFromHistory(this.id)
showToast(this.$t('Video.Video has been removed from your history'))
this.watchProgress = 0
},
togglePlaylistPrompt: function () {

View File

@ -60,6 +60,12 @@ export default defineComponent({
'location',
'hdr',
'vr180'
],
notAllowedForMoviesFeatures: [
'live',
'subtitles',
'3d',
'creative_commons'
]
}
},
@ -164,9 +170,9 @@ export default defineComponent({
},
updateFeatures: function(value) {
if (!this.isVideoOrMovieOrAll(this.searchSettings.type)) {
const featuresCheck = this.$refs.featuresCheck
featuresCheck.removeSelectedValues()
if (!this.isVideoOrMovieOrAll(this.searchSettings.type) || this.notAllowedForMoviesFeatures.some(item => value.includes(item))) {
const typeRadio = this.$refs.typeRadio
typeRadio.updateSelectedValue('all')
this.$store.commit('setSearchType', 'all')
}
@ -183,11 +189,16 @@ export default defineComponent({
timeRadio.updateSelectedValue('')
durationRadio.updateSelectedValue('')
sortByRadio.updateSelectedValue(this.sortByValues[0])
featuresCheck.removeSelectedValues()
featuresCheck.setSelectedValues([])
this.$store.commit('setSearchTime', '')
this.$store.commit('setSearchDuration', '')
this.$store.commit('setSearchFeatures', [])
this.$store.commit('setSearchSortBy', this.sortByValues[0])
} else if (value === 'movie') {
const featuresCheck = this.$refs.featuresCheck
const filteredFeatures = this.searchSettings.features.filter(e => !this.notAllowedForMoviesFeatures.includes(e))
featuresCheck.setSelectedValues([...filteredFeatures])
this.$store.commit('setSearchFeatures', filteredFeatures)
}
this.$store.commit('setSearchType', value)
this.$store.commit('setSearchFilterValueChanged', this.searchFilterValueChanged)

View File

@ -110,6 +110,7 @@ export default defineComponent({
editMode: false,
showDeletePlaylistPrompt: false,
showRemoveVideosOnWatchPrompt: false,
showRemoveDuplicateVideosPrompt: false,
newTitle: '',
newDescription: '',
deletePlaylistPromptValues: [
@ -159,12 +160,30 @@ export default defineComponent({
return this.$store.getters.getPlaylist(this.id)
},
allPlaylists: function () {
return this.$store.getters.getAllPlaylists
},
deletePlaylistPromptNames: function () {
return [
this.$t('Yes, Delete'),
this.$t('Cancel')
]
},
removeVideosOnWatchPromptLabelText() {
return this.$tc(
'User Playlists.Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone',
this.userPlaylistWatchedVideoCount,
{ playlistItemCount: this.userPlaylistWatchedVideoCount },
)
},
removeDuplicateVideosPromptLabelText() {
return this.$tc(
'User Playlists.Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone',
this.userPlaylistDuplicateItemCount,
{ playlistItemCount: this.userPlaylistDuplicateItemCount },
)
},
firstVideoIdExists() {
return this.firstVideoId !== ''
@ -211,6 +230,38 @@ export default defineComponent({
return this.isUserPlaylist ? 'user' : ''
},
userPlaylistAnyVideoWatched() {
if (!this.isUserPlaylist) { return false }
const historyCacheById = this.$store.getters.getHistoryCacheById
return this.selectedUserPlaylist.videos.some((video) => {
return typeof historyCacheById[video.videoId] !== 'undefined'
})
},
// `userPlaylistAnyVideoWatched` is faster than this & this is only needed when prompt shown
userPlaylistWatchedVideoCount() {
if (!this.isUserPlaylist) { return false }
const historyCacheById = this.$store.getters.getHistoryCacheById
return this.selectedUserPlaylist.videos.reduce((count, video) => {
return typeof historyCacheById[video.videoId] !== 'undefined' ? count + 1 : count
}, 0)
},
userPlaylistUniqueVideoIds() {
if (!this.isUserPlaylist) { return new Set() }
return this.selectedUserPlaylist.videos.reduce((set, video) => {
set.add(video.videoId)
return set
}, new Set())
},
userPlaylistDuplicateItemCount() {
if (this.userPlaylistUniqueVideoIds.size === 0) { return 0 }
return this.selectedUserPlaylist.videos.length - this.userPlaylistUniqueVideoIds.size
},
deletePlaylistButtonVisible: function() {
if (!this.isUserPlaylist) { return false }
// Cannot delete during edit
@ -241,6 +292,29 @@ export default defineComponent({
playlistDeletionDisabledLabel: function () {
return this.$t('User Playlists["Cannot delete the quick bookmark target playlist."]')
},
inputPlaylistNameEmpty() {
return this.newTitle === ''
},
inputPlaylistNameBlank() {
return !this.inputPlaylistNameEmpty && this.newTitle.trim() === ''
},
inputPlaylistWithNameExists() {
// Don't show the message with no name input
const playlistName = this.newTitle
const selectedUserPlaylist = this.selectedUserPlaylist
if (this.newTitle === '') { return false }
return this.allPlaylists.some((playlist) => {
// Only compare with other playlists
if (selectedUserPlaylist._id === playlist._id) { return false }
return playlist.playlistName === playlistName
})
},
playlistPersistenceDisabled() {
return this.inputPlaylistNameEmpty || this.inputPlaylistNameBlank || this.inputPlaylistWithNameExists
},
},
watch: {
showDeletePlaylistPrompt(shown) {
@ -269,6 +343,16 @@ export default defineComponent({
document.removeEventListener('keydown', this.keyboardShortcutHandler)
},
methods: {
handlePlaylistNameInput(input) {
if (input.trim() === '') {
// Need to show message for blank input
this.newTitle = input
return
}
this.newTitle = input.trim()
},
toggleCopyVideosPrompt: function (force = false) {
if (this.moreVideoDataAvailable && !this.isUserPlaylist && !force) {
showToast(this.$t('User Playlists.SinglePlaylistView.Toast["Some videos in the playlist are not loaded yet. Click here to copy anyway."]'), 5000, () => {
@ -279,15 +363,15 @@ export default defineComponent({
this.showAddToPlaylistPromptForManyVideos({
videos: this.videos,
newPlaylistDefaultProperties: { title: this.title },
newPlaylistDefaultProperties: {
title: this.channelName === '' ? this.title : `${this.title} | ${this.channelName}`,
},
})
},
savePlaylistInfo: function () {
if (this.newTitle === '') {
showToast(this.$t('User Playlists.SinglePlaylistView.Toast["Playlist name cannot be empty. Please input a name."]'))
return
}
// Still possible to attempt to create via pressing enter
if (this.playlistPersistenceDisabled) { return }
const playlist = {
playlistName: this.newTitle,
@ -334,6 +418,43 @@ export default defineComponent({
this.$emit('exit-edit-mode')
},
handleRemoveDuplicateVideosPromptAnswer(option) {
this.showRemoveDuplicateVideosPrompt = false
if (option !== 'delete') { return }
const videoIdsAdded = new Set()
const newVideoItems = this.selectedUserPlaylist.videos.reduce((ary, video) => {
if (videoIdsAdded.has(video.videoId)) { return ary }
ary.push(video)
videoIdsAdded.add(video.videoId)
return ary
}, [])
const removedVideosCount = this.userPlaylistDuplicateItemCount
if (removedVideosCount === 0) {
showToast(this.$t('User Playlists.SinglePlaylistView.Toast["There were no videos to remove."]'))
return
}
const playlist = {
playlistName: this.title,
protected: this.selectedUserPlaylist.protected,
description: this.description,
videos: newVideoItems,
_id: this.id,
}
try {
this.updatePlaylist(playlist)
showToast(this.$tc('User Playlists.SinglePlaylistView.Toast.{videoCount} video(s) have been removed', removedVideosCount, {
videoCount: removedVideosCount,
}))
} catch (e) {
showToast(this.$t('User Playlists.SinglePlaylistView.Toast["There was an issue with updating this playlist."]'))
console.error(e)
}
},
handleRemoveVideosOnWatchPromptAnswer: function (option) {
this.showRemoveVideosOnWatchPrompt = false
if (option !== 'delete') { return }
@ -346,7 +467,6 @@ export default defineComponent({
if (removedVideosCount === 0) {
showToast(this.$t('User Playlists.SinglePlaylistView.Toast["There were no videos to remove."]'))
this.showRemoveVideosOnWatchPrompt = false
return
}

View File

@ -34,36 +34,51 @@
</div>
<div class="playlistStats">
<ft-input
<template
v-if="editMode"
ref="playlistTitleInput"
class="inputElement"
:placeholder="$t('User Playlists.Playlist Name')"
:show-action-button="false"
:show-label="false"
:value="newTitle"
:maxlength="255"
@input="(input) => (newTitle = input)"
@keydown.enter.native="savePlaylistInfo"
/>
<h2
v-else
id="playlistTitle"
class="playlistTitle"
>
{{ title }}
</h2>
<p>
{{ $tc('Global.Counts.Video Count', videoCount, {count: parsedVideoCount}) }}
<span v-if="!hideViews && !isUserPlaylist">
- {{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
</span>
<span>- </span>
<span v-if="infoSource !== 'local'">
{{ $t("Playlist.Last Updated On") }}
</span>
{{ lastUpdated }}
</p>
<ft-input
ref="playlistTitleInput"
class="inputElement"
:placeholder="$t('User Playlists.Playlist Name')"
:show-action-button="false"
:show-label="false"
:value="newTitle"
:maxlength="255"
@input="handlePlaylistNameInput"
@keydown.enter.native="savePlaylistInfo"
/>
<ft-flex-box v-if="inputPlaylistNameEmpty || inputPlaylistNameBlank">
<p>
{{ $t('User Playlists.SinglePlaylistView.Toast["Playlist name cannot be empty. Please input a name."]') }}
</p>
</ft-flex-box>
<ft-flex-box v-if="inputPlaylistWithNameExists">
<p>
{{ $t('User Playlists.CreatePlaylistPrompt.Toast["There is already a playlist with this name. Please pick a different name."]') }}
</p>
</ft-flex-box>
</template>
<template
v-else
>
<h2
class="playlistTitle"
>
{{ title }}
</h2>
<p>
{{ $tc('Global.Counts.Video Count', videoCount, {count: parsedVideoCount}) }}
<span v-if="!hideViews && !isUserPlaylist">
- {{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
</span>
<span>- </span>
<span v-if="infoSource !== 'local'">
{{ $t("Playlist.Last Updated On") }}
</span>
{{ lastUpdated }}
</p>
</template>
</div>
<ft-input
@ -119,6 +134,7 @@
<ft-icon-button
v-if="editMode"
:title="$t('User Playlists.Save Changes')"
:disabled="playlistPersistenceDisabled"
:icon="['fas', 'save']"
theme="secondary"
@click="savePlaylistInfo"
@ -154,7 +170,14 @@
@click="toggleCopyVideosPrompt"
/>
<ft-icon-button
v-if="!editMode && isUserPlaylist && videoCount > 0"
v-if="!editMode && userPlaylistDuplicateItemCount > 0"
:title="$t('User Playlists.Remove Duplicate Videos')"
:icon="['fas', 'users-slash']"
theme="destructive"
@click="showRemoveDuplicateVideosPrompt = true"
/>
<ft-icon-button
v-if="!editMode && userPlaylistAnyVideoWatched"
:title="$t('User Playlists.Remove Watched Videos')"
:icon="['fas', 'eye-slash']"
theme="destructive"
@ -204,12 +227,20 @@
/>
<ft-prompt
v-if="showRemoveVideosOnWatchPrompt"
:label="$t('User Playlists.Are you sure you want to remove all watched videos from this playlist? This cannot be undone')"
:label="removeVideosOnWatchPromptLabelText"
:option-names="deletePlaylistPromptNames"
:option-values="deletePlaylistPromptValues"
:is-first-option-destructive="true"
@click="handleRemoveVideosOnWatchPromptAnswer"
/>
<ft-prompt
v-if="showRemoveDuplicateVideosPrompt"
:label="removeDuplicateVideosPromptLabelText"
:option-names="deletePlaylistPromptNames"
:option-values="deletePlaylistPromptValues"
:is-first-option-destructive="true"
@click="handleRemoveDuplicateVideosPromptAnswer"
/>
</div>
</div>
</template>

View File

@ -260,6 +260,12 @@ export default defineComponent({
/** @type {import('youtubei.js').YTNodes.CommentThread} */
const commentThread = this.replyTokens.get(comment.id)
if (commentThread == null) {
this.replyTokens.delete(comment.id)
comment.hasReplyToken = false
return
}
if (comment.replies.length > 0) {
await commentThread.getContinuation()
comment.replies = comment.replies.concat(commentThread.replies.map(reply => parseLocalComment(reply)))

View File

@ -10,6 +10,7 @@ import {
untilEndOfLocalPlayList,
} from '../../helpers/api/local'
import { invidiousGetPlaylistInfo } from '../../helpers/api/invidious'
import { getSortedPlaylistItems, SORT_BY_VALUES } from '../../helpers/playlists'
export default defineComponent({
name: 'WatchVideoPlaylist',
@ -66,7 +67,9 @@ export default defineComponent({
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
currentLocale: function () {
return this.$i18n.locale.replace('_', '-')
},
isUserPlaylist: function () {
return this.playlistType === 'user'
},
@ -134,6 +137,12 @@ export default defineComponent({
},
}
},
userPlaylistSortOrder: function () {
return this.$store.getters.getUserPlaylistSortOrder
},
sortOrder: function () {
return this.isUserPlaylist ? this.userPlaylistSortOrder : SORT_BY_VALUES.Custom
},
},
watch: {
userPlaylistsReady: function() {
@ -495,11 +504,7 @@ export default defineComponent({
this.prevVideoBeforeDeletion = this.playlistItems[targetVideoIndex]
}
let playlistItems = playlist.videos
if (this.reversePlaylist) {
playlistItems = playlistItems.toReversed()
}
this.playlistItems = playlistItems
this.playlistItems = getSortedPlaylistItems(playlist.videos, this.sortOrder, this.currentLocale, this.reversePlaylist)
// grab the first video of the parsed playlit if the current video is not in either the current or parsed data
// (e.g., reloading the page after the current video has already been removed from the playlist)

View File

@ -1,4 +1,4 @@
import { ClientType, Endpoints, Innertube, Misc, UniversalCache, Utils, YT } from 'youtubei.js'
import { ClientType, Endpoints, Innertube, Misc, Parser, UniversalCache, Utils, YT } from 'youtubei.js'
import Autolinker from 'autolinker'
import { SEARCH_CHAR_LIMIT } from '../../../constants'
@ -410,6 +410,55 @@ export async function getLocalChannelCommunity(id) {
}
}
/**
* @param {YT.Channel} channel
*/
export async function getLocalArtistTopicChannelReleases(channel) {
const rawEngagementPanel = channel.shelves[0]?.menu?.top_level_buttons?.[0]?.endpoint.payload?.engagementPanel
if (!rawEngagementPanel) {
return {
releases: channel.playlists.map(playlist => parseLocalListPlaylist(playlist)),
continuationData: null
}
}
/** @type {import('youtubei.js').YTNodes.EngagementPanelSectionList} */
const engagementPanelSectionList = Parser.parseItem(rawEngagementPanel)
/** @type {import('youtubei.js').YTNodes.ContinuationItem|undefined} */
const continuationItem = engagementPanelSectionList?.content?.contents?.[0]?.contents?.[0]
if (!continuationItem) {
return {
releases: channel.playlists.map(playlist => parseLocalListPlaylist(playlist)),
continuationData: null
}
}
return await getLocalArtistTopicChannelReleasesContinuation(channel, continuationItem)
}
/**
* @param {YT.Channel} channel
* @param {import('youtubei.js').YTNodes.ContinuationItem} continuationData
*/
export async function getLocalArtistTopicChannelReleasesContinuation(channel, continuationData) {
const response = await continuationData.endpoint.call(channel.actions, { parse: true })
const memo = response.on_response_received_endpoints_memo
const playlists = memo.get('GridPlaylist') ?? memo.get('LockupView') ?? memo.get('Playlist')
/** @type {import('youtubei.js').YTNodes.ContinuationItem | null} */
const continuationItem = memo.get('ContinuationItem')?.[0] ?? null
return {
releases: playlists ? playlists.map(playlist => parseLocalListPlaylist(playlist)) : [],
continuationData: continuationItem
}
}
/**
* @param {YT.Channel} channel
* @param {boolean} onlyIdNameThumbnail
@ -674,6 +723,24 @@ export function parseLocalListPlaylist(playlist, channelId = undefined, channelN
}
}
/**
* @param {import('youtubei.js').YTNodes.CompactStation} compactStation
* @param {string} channelId
* @param {string} channelName
*/
export function parseLocalCompactStation(compactStation, channelId, channelName) {
return {
type: 'playlist',
dataSource: 'local',
title: compactStation.title.text,
thumbnail: compactStation.thumbnail[1].url,
channelName,
channelId,
playlistId: compactStation.endpoint.payload.playlistId,
videoCount: extractNumberFromString(compactStation.video_count.text)
}
}
/**
* @param {YT.Search} response
*/

View File

@ -0,0 +1,52 @@
export const SORT_BY_VALUES = {
DateAddedNewest: 'date_added_descending',
DateAddedOldest: 'date_added_ascending',
AuthorAscending: 'author_ascending',
AuthorDescending: 'author_descending',
VideoTitleAscending: 'video_title_ascending',
VideoTitleDescending: 'video_title_descending',
Custom: 'custom'
}
export function getSortedPlaylistItems(playlistItems, sortOrder, locale, reversed = false) {
if (sortOrder === SORT_BY_VALUES.Custom) {
return reversed ? playlistItems.toReversed() : playlistItems
}
let collator
if (
sortOrder === SORT_BY_VALUES.VideoTitleAscending ||
sortOrder === SORT_BY_VALUES.VideoTitleDescending ||
sortOrder === SORT_BY_VALUES.AuthorAscending ||
sortOrder === SORT_BY_VALUES.AuthorDescending
) {
collator = new Intl.Collator([locale, 'en'])
}
return playlistItems.toSorted((a, b) => {
const first = !reversed ? a : b
const second = !reversed ? b : a
return compareTwoPlaylistItems(first, second, sortOrder, collator)
})
}
function compareTwoPlaylistItems(a, b, sortOrder, collator) {
switch (sortOrder) {
case SORT_BY_VALUES.DateAddedNewest:
return b.timeAdded - a.timeAdded
case SORT_BY_VALUES.DateAddedOldest:
return a.timeAdded - b.timeAdded
case SORT_BY_VALUES.VideoTitleAscending:
return collator.compare(a.title, b.title)
case SORT_BY_VALUES.VideoTitleDescending:
return collator.compare(b.title, a.title)
case SORT_BY_VALUES.AuthorAscending:
return collator.compare(a.author, b.author)
case SORT_BY_VALUES.AuthorDescending:
return collator.compare(b.author, a.author)
default:
console.error(`Unknown sortOrder: ${sortOrder}`)
return 0
}
}

View File

@ -66,8 +66,8 @@ export async function deArrowThumbnail(videoId, timestamp) {
try {
const response = await fetch(requestUrl)
// 404 means that there are no thumbnails found for the video
if (response.status === 404) {
// 204 means that there are no thumbnails found for the video
if (response.status === 204) {
return undefined
}

View File

@ -91,6 +91,7 @@ import {
faTimesCircle,
faTrash,
faUsers,
faUsersSlash,
} from '@fortawesome/free-solid-svg-icons'
import {
faBookmark as farBookmark
@ -190,6 +191,7 @@ library.add(
faTimesCircle,
faTrash,
faUsers,
faUsersSlash,
// solid icons
farBookmark,

View File

@ -108,27 +108,29 @@ const mutations = {
},
updateRecordWatchProgressInHistoryCache(state, { videoId, watchProgress }) {
const i = state.historyCacheSorted.findIndex((currentRecord) => {
return currentRecord.videoId === videoId
})
// historyCacheById and historyCacheSorted reference the same object instances,
// so modifying an existing object in one of them will update both.
const targetRecord = Object.assign({}, state.historyCacheSorted[i])
targetRecord.watchProgress = watchProgress
state.historyCacheSorted.splice(i, 1, targetRecord)
vueSet(state.historyCacheById, videoId, targetRecord)
const record = state.historyCacheById[videoId]
// Don't set, if the item was removed from the watch history, as we don't have any video details
if (record) {
vueSet(record, 'watchProgress', watchProgress)
}
},
updateRecordLastViewedPlaylistIdInHistoryCache(state, { videoId, lastViewedPlaylistId, lastViewedPlaylistType, lastViewedPlaylistItemId }) {
const i = state.historyCacheSorted.findIndex((currentRecord) => {
return currentRecord.videoId === videoId
})
// historyCacheById and historyCacheSorted reference the same object instances,
// so modifying an existing object in one of them will update both.
const targetRecord = Object.assign({}, state.historyCacheSorted[i])
targetRecord.lastViewedPlaylistId = lastViewedPlaylistId
targetRecord.lastViewedPlaylistType = lastViewedPlaylistType
targetRecord.lastViewedPlaylistItemId = lastViewedPlaylistItemId
state.historyCacheSorted.splice(i, 1, targetRecord)
vueSet(state.historyCacheById, videoId, targetRecord)
const record = state.historyCacheById[videoId]
// Don't set, if the item was removed from the watch history, as we don't have any video details
if (record) {
vueSet(record, 'lastViewedPlaylistId', lastViewedPlaylistId)
vueSet(record, 'lastViewedPlaylistType', lastViewedPlaylistType)
vueSet(record, 'lastViewedPlaylistItemId', lastViewedPlaylistItemId)
}
},
removeFromHistoryCacheById(state, videoId) {

View File

@ -16,6 +16,7 @@
.version {
text-align: center;
font-size: 2em;
margin-block-end: 1em;
}
.about-chunks {

View File

@ -34,13 +34,16 @@ import {
import {
getLocalChannel,
getLocalChannelId,
getLocalArtistTopicChannelReleases,
parseLocalChannelHeader,
parseLocalChannelShorts,
parseLocalChannelVideos,
parseLocalCommunityPosts,
parseLocalCompactStation,
parseLocalListPlaylist,
parseLocalListVideo,
parseLocalSubscriberCount
parseLocalSubscriberCount,
getLocalArtistTopicChannelReleasesContinuation
} from '../../helpers/api/local'
export default defineComponent({
@ -71,6 +74,7 @@ export default defineComponent({
thumbnailUrl: '',
subCount: 0,
searchPage: 2,
isArtistTopicChannel: false,
videoContinuationData: null,
shortContinuationData: null,
liveContinuationData: null,
@ -329,6 +333,7 @@ export default defineComponent({
this.shownElementList = []
this.apiUsed = ''
this.channelInstance = ''
this.isArtistTopicChannel = false
this.videoContinuationData = null
this.shortContinuationData = null
this.liveContinuationData = null
@ -560,6 +565,7 @@ export default defineComponent({
this.thumbnailUrl = channelThumbnailUrl
this.bannerUrl = parsedHeader.bannerUrl ?? null
this.isFamilyFriendly = !!channel.metadata.is_family_safe
this.isArtistTopicChannel = channelName.endsWith('- Topic') && !!channel.metadata.music_artist_name
if (channel.metadata.tags) {
tags.push(...channel.metadata.tags)
@ -661,14 +667,30 @@ export default defineComponent({
this.getChannelPodcastsLocal()
}
if (!this.hideChannelReleases && channel.has_releases) {
if (!this.hideChannelReleases && (channel.has_releases || this.isArtistTopicChannel)) {
tabs.push('releases')
this.getChannelReleasesLocal()
}
if (!this.hideChannelPlaylists && channel.has_playlists) {
tabs.push('playlists')
this.getChannelPlaylistsLocal()
if (!this.hideChannelPlaylists) {
if (channel.has_playlists) {
tabs.push('playlists')
this.getChannelPlaylistsLocal()
} else if (channelId === 'UC-9-kyTW8ZkZNDHQJ6FgpwQ') {
// Special handling for "The Music Channel" (https://youtube.com/music)
tabs.push('playlists')
const playlists = channel.playlists.map(playlist => parseLocalListPlaylist(playlist))
const compactStations = channel.memo.get('CompactStation')
if (compactStations) {
for (const compactStation of compactStations) {
playlists.push(parseLocalCompactStation(compactStation, channelId, channelName))
}
}
this.showPlaylistSortBy = false
this.latestPlaylists = playlists
}
}
if (!this.hideChannelCommunity && channel.has_community) {
@ -699,7 +721,7 @@ export default defineComponent({
}
},
getChannelAboutLocal: async function () {
getChannelAboutLocal: async function (channel) {
try {
/**
* @type {import('youtubei.js').YT.Channel}
@ -1247,20 +1269,13 @@ export default defineComponent({
// for the moment we just want the "Created Playlists" category that has all playlists in it
if (playlistsTab.content_type_filters.length > 1) {
let viewId = '1'
// Artist topic channels don't have any created playlists, so we went to select the "Albums & Singles" category instead
if (this.channelName.endsWith('- Topic') && channel.metadata.music_artist_name) {
viewId = '50'
}
/**
* @type {import('youtubei.js').YTNodes.ChannelSubMenu}
*/
const menu = playlistsTab.current_tab.content.sub_menu
const createdPlaylistsFilter = menu.content_type_sub_menu_items.find(contentType => {
const url = `https://youtube.com/${contentType.endpoint.metadata.url}`
return new URL(url).searchParams.get('view') === viewId
return new URL(url).searchParams.get('view') === '1'
}).title
playlistsTab = await playlistsTab.applyContentTypeFilter(createdPlaylistsFilter)
@ -1396,14 +1411,27 @@ export default defineComponent({
* @type {import('youtubei.js').YT.Channel}
*/
const channel = this.channelInstance
const releaseTab = await channel.getReleases()
if (expectedId !== this.id) {
return
if (this.isArtistTopicChannel) {
const { releases, continuationData } = await getLocalArtistTopicChannelReleases(channel)
if (expectedId !== this.id) {
return
}
this.latestReleases = releases
this.releaseContinuationData = continuationData
} else {
const releaseTab = await channel.getReleases()
if (expectedId !== this.id) {
return
}
this.latestReleases = releaseTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
this.releaseContinuationData = releaseTab.has_continuation ? releaseTab : null
}
this.latestReleases = releaseTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
this.releaseContinuationData = releaseTab.has_continuation ? releaseTab : null
this.isElementListLoading = false
} catch (err) {
console.error(err)
@ -1422,14 +1450,23 @@ export default defineComponent({
getChannelReleasesLocalMore: async function () {
try {
/**
* @type {import('youtubei.js').YT.ChannelListContinuation}
*/
const continuation = await this.releaseContinuationData.getContinuation()
if (this.isArtistTopicChannel) {
const { releases, continuationData } = await getLocalArtistTopicChannelReleasesContinuation(
this.channelInstance, this.releaseContinuationData
)
const parsedReleases = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
this.latestReleases = this.latestReleases.concat(parsedReleases)
this.releaseContinuationData = continuation.has_continuation ? continuation : null
this.latestReleases.push(...releases)
this.releaseContinuationData = continuationData
} else {
/**
* @type {import('youtubei.js').YT.ChannelListContinuation}
*/
const continuation = await this.releaseContinuationData.getContinuation()
const parsedReleases = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
this.latestReleases = this.latestReleases.concat(parsedReleases)
this.releaseContinuationData = continuation.has_continuation ? continuation : null
}
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')

View File

@ -22,19 +22,10 @@ import {
showToast,
} from '../../helpers/utils'
import { invidiousGetPlaylistInfo, youtubeImageUrlToInvidious } from '../../helpers/api/invidious'
import { getSortedPlaylistItems, SORT_BY_VALUES } from '../../helpers/playlists'
import packageDetails from '../../../../package.json'
import { MOBILE_WIDTH_THRESHOLD, PLAYLIST_HEIGHT_FORCE_LIST_THRESHOLD } from '../../../constants'
const SORT_BY_VALUES = {
DateAddedNewest: 'date_added_descending',
DateAddedOldest: 'date_added_ascending',
AuthorAscending: 'author_ascending',
AuthorDescending: 'author_descending',
VideoTitleAscending: 'video_title_ascending',
VideoTitleDescending: 'video_title_descending',
Custom: 'custom',
}
export default defineComponent({
name: 'Playlist',
components: {
@ -49,13 +40,13 @@ export default defineComponent({
'ft-auto-load-next-page-wrapper': FtAutoLoadNextPageWrapper,
},
beforeRouteLeave(to, from, next) {
if (!this.isLoading && !this.isUserPlaylistRequested && to.path.startsWith('/watch') && to.query.playlistId === this.playlistId) {
if (!this.isLoading && to.path.startsWith('/watch') && to.query.playlistId === this.playlistId) {
this.setCachedPlaylist({
id: this.playlistId,
title: this.playlistTitle,
channelName: this.channelName,
channelId: this.channelId,
items: this.playlistItems,
items: this.sortedPlaylistItems,
continuationData: this.continuationData,
})
}
@ -189,29 +180,7 @@ export default defineComponent({
return this.sortOrder === SORT_BY_VALUES.Custom
},
sortedPlaylistItems: function () {
if (this.sortOrder === SORT_BY_VALUES.Custom) {
return this.playlistItems
}
return this.playlistItems.toSorted((a, b) => {
switch (this.sortOrder) {
case SORT_BY_VALUES.DateAddedNewest:
return b.timeAdded - a.timeAdded
case SORT_BY_VALUES.DateAddedOldest:
return a.timeAdded - b.timeAdded
case SORT_BY_VALUES.VideoTitleAscending:
return a.title.localeCompare(b.title, this.currentLocale)
case SORT_BY_VALUES.VideoTitleDescending:
return b.title.localeCompare(a.title, this.currentLocale)
case SORT_BY_VALUES.AuthorAscending:
return a.author.localeCompare(b.author, this.currentLocale)
case SORT_BY_VALUES.AuthorDescending:
return b.author.localeCompare(a.author, this.currentLocale)
default:
console.error(`Unknown sortOrder: ${this.sortOrder}`)
return 0
}
})
return getSortedPlaylistItems(this.playlistItems, this.sortOrder, this.currentLocale)
},
visiblePlaylistItems: function () {
if (!this.isUserPlaylistRequested) {
@ -340,7 +309,7 @@ export default defineComponent({
this.playlistDescription = result.info.description ?? ''
this.firstVideoId = result.items[0].id
this.playlistThumbnail = result.info.thumbnails[0].url
this.viewCount = extractNumberFromString(result.info.views)
this.viewCount = result.info.views.toLowerCase() === 'no views' ? 0 : extractNumberFromString(result.info.views)
this.videoCount = extractNumberFromString(result.info.total_items)
this.lastUpdated = result.info.last_updated ?? ''
this.channelName = channelName ?? ''

View File

@ -235,6 +235,15 @@ User Playlists:
Quick Bookmark Enabled: تم تمكين الإشارة المرجعية السريعة
Cannot delete the quick bookmark target playlist.: لا يمكن حذف قائمة التشغيل المستهدفة
للإشارات المرجعية السريعة.
Remove Duplicate Videos: إزالة مقاطع الفيديو المكررة
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: هل
أنت متأكد أنك تريد إزالة مقطع فيديو واحد تمت مشاهدته من قائمة التشغيل هذه؟ هذا
لا يمكن التراجع عنها. | هل أنت متأكد أنك تريد إزالة {playlistItemCount} من مقاطع
الفيديو التي تمت مشاهدتها من قائمة التشغيل هذه؟ هذا لا يمكن التراجع عنها.
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: هل
أنت متأكد أنك تريد إزالة مقطع فيديو مكرر واحد من قائمة التشغيل هذه؟ هذا لا يمكن
التراجع عنها. | هل أنت متأكد أنك تريد إزالة {playlistItemCount} من مقاطع الفيديو
المكررة من قائمة التشغيل هذه؟ هذا لا يمكن التراجع عنها.
History:
# On History Page
History: 'السجلّ'
@ -454,6 +463,7 @@ Settings:
How do I import my subscriptions?: 'كيف استورد اشتراكاتي؟'
Fetch Automatically: جلب الخلاصة تلقائيا
Only Show Latest Video for Each Channel: عرض أحدث فيديو فقط لكل قناة
Avoid Accidental Unsubscription: تجنب إلغاء الاشتراك عن طريق الخطأ
Advanced Settings:
Advanced Settings: 'الإعدادات المتقدمة'
Enable Debug Mode (Prints data to the console): 'تمكين وضع التنقيح (يطبع البيانات

View File

@ -225,7 +225,12 @@ User Playlists:
към {playlistCount} плейлиста | Добавени {videoCount} видеа към {playlistCount}
плейлиста
Save: Запазване
Added {count} Times: Добавено {count} път | Добавено {count} пъти
Added {count} Times: Вече е добавено | Добавено {count} пъти
"{videoCount}/{totalVideoCount} Videos Already Added": Вече са добавени {videoCount}/{totalVideoCount}
видеа
"{videoCount}/{totalVideoCount} Videos Will Be Added": Ще бъдат добавени {videoCount}/{totalVideoCount}
видеа
Allow Adding Duplicate Video(s): Разрешаване на добавянето на дублиращи се видеа
Remove from Playlist: Премахване от плейлиста за изпълнение
Playlist Name: Име на плейлиста
Save Changes: Запазване на промените
@ -241,6 +246,15 @@ User Playlists:
Quick Bookmark Enabled: Бързите отметки са активирани
Cannot delete the quick bookmark target playlist.: Не може да се изтрие целевия
списък за плейлисти с бързи отметки.
Remove Duplicate Videos: Премахване на дублирани видеа
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: Сигурни
ли сте, че искате да премахнете 1 дублиращо се видео от този плейлист? Това не
може да бъде отменено. | Сигурни ли сте, че искате да премахнете {playlistItemCount}
дублиращи се видеа от този плейлист? Това не може да бъде отменено.
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: Сигурни
ли сте, че искате да премахнете 1 гледано видео от този плейлист? Това не може
да бъде отменено. | Сигурни ли сте, че искате да премахнете {playlistItemCount}
гледани видеа от този плейлист? Това не може да бъде отменено.
History:
# On History Page
History: 'История'
@ -458,6 +472,7 @@ Settings:
Fetch Automatically: Автоматично извличане на съдържание
Only Show Latest Video for Each Channel: Показване само най-новите видеа за всеки
канал
Avoid Accidental Unsubscription: Избягване на случайно отписване
Data Settings:
Data Settings: 'Настройки на данни'
Select Import Type: 'Избор на тип за внасяне'
@ -1250,3 +1265,10 @@ Cancel: Отказ
Moments Ago: току що
checkmark:
Display Label: '{label}: {value}'
Search Listing:
Label:
4K: 4K
Subtitles: Субтитри
Closed Captions: Затворени субтитри
'Blocked opening potentially unsafe URL': 'Блокирано отваряне на потенциално опасен
URL адрес: "{url}".'

View File

@ -244,6 +244,15 @@ User Playlists:
Quick Bookmark Enabled: Rychlá záložka zapnuta
Cannot delete the quick bookmark target playlist.: Nemůžete odstranit cílový playlist
rychlé záložky.
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: Opravdu
chcete odstranit 1 duplicitní video z tohoto playlistu? Tato akce je nevratná.
| Opravdu chcete odstranit {playlistItemCount} duplicitních videí z tohoto playlistu?
Tato akce je nevratná.
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: Opravdu
chcete odstranit 1 zhlédnuté video z tohoto playlistu? Tato akce je nevratná.
| Opravdu chcete odstranit {playlistItemCount} zhlédnutých videí z tohoto playlistu?
Tato akce je nevratná.
Remove Duplicate Videos: Odstranit duplicitní videa
History:
# On History Page
History: 'Historie'
@ -459,6 +468,7 @@ Settings:
Fetch Automatically: Automaticky načítat odběry
Only Show Latest Video for Each Channel: U každého kanálu zobrazit pouze nejnovější
video
Avoid Accidental Unsubscription: Zamezit nechtěným odběrům
Distraction Free Settings:
Distraction Free Settings: 'Nastavení rozptylování'
Hide Video Views: 'Skrýt počet přehrání videa'

View File

@ -251,6 +251,17 @@ User Playlists:
Quick Bookmark Enabled: Schnelles Lesezeichen aktiviert
Cannot delete the quick bookmark target playlist.: Die Wiedergabeliste für das schnelle
Lesezeichen kann nicht gelöscht werden.
Remove Duplicate Videos: Doppelte Videos entfernen
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: Sind
Sie sicher, dass Sie 1 doppeltes Video aus dieser Wiedergabeliste entfernen möchten?
Dies kann nicht rückgängig gemacht werden. | Sind Sie sicher, dass Sie {playlistItemCount}
doppelte Videos aus dieser Wiedergabeliste entfernen möchten? Dies kann nicht
rückgängig gemacht werden.
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: Sind
Sie sicher, dass Sie 1 angesehenes Video aus dieser Wiedergabeliste entfernen
möchten? Dies kann nicht rückgängig gemacht werden. | Sind Sie sicher, dass Sie
{playlistItemCount} angesehene Videos aus dieser Wiedergabeliste entfernen möchten?
Dies kann nicht rückgängig gemacht werden.
History:
# On History Page
History: Verlauf
@ -452,6 +463,7 @@ Settings:
Fetch Automatically: Feed automatisch abrufen
Only Show Latest Video for Each Channel: Nur das neueste Video für jeden Kanal
anzeigen
Avoid Accidental Unsubscription: Unbeabsichtigtes Deabonnieren vermeiden
Advanced Settings:
Advanced Settings: Erweiterte Einstellungen
Enable Debug Mode (Prints data to the console): Aktiviere Debug-Modus (Konsolenausgabe

View File

@ -189,10 +189,12 @@ User Playlists:
Cancel: Cancel
Edit Playlist Info: Edit Playlist Info
Copy Playlist: Copy Playlist
Remove Duplicate Videos: Remove Duplicate Videos
Remove Watched Videos: Remove Watched Videos
Enable Quick Bookmark With This Playlist: Enable Quick Bookmark With This Playlist
Quick Bookmark Enabled: Quick Bookmark Enabled
Are you sure you want to remove all watched videos from this playlist? This cannot be undone: Are you sure you want to remove all watched videos from this playlist? This cannot be undone.
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: Are you sure you want to remove 1 duplicate video from this playlist? This cannot be undone. | Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone.
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: Are you sure you want to remove 1 watched video from this playlist? This cannot be undone. | Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone.
Delete Playlist: Delete Playlist
Cannot delete the quick bookmark target playlist.: Cannot delete the quick bookmark target playlist.
Are you sure you want to delete this playlist? This cannot be undone: Are you sure you want to delete this playlist? This cannot be undone.

View File

@ -244,6 +244,15 @@ User Playlists:
Quick Bookmark Enabled: Marcador rápido habilitado
Cannot delete the quick bookmark target playlist.: No se puede eliminar la lista
de reproducción de destino de marcadores rápidos.
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: ¿Estás
seguro de que deseas eliminar 1 video visto de esta lista de reproducción? Esto
no se puede deshacer. | ¿Estás seguro de que deseas eliminar {playlistItemCount}
videos vistos de esta lista de reproducción? Esto no se puede deshacer.
Remove Duplicate Videos: Eliminar vídeos duplicados
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: ¿Estás
seguro de que deseas eliminar 1 video duplicado de esta lista de reproducción?
Esto no se puede deshacer. | ¿Estás seguro de que deseas eliminar {playlistItemCount}
videos duplicados de esta lista de reproducción? Esto no se puede deshacer.
History:
# On History Page
History: 'Historial'
@ -461,6 +470,7 @@ Settings:
Fetch Automatically: Obtener los feed automáticamente
Only Show Latest Video for Each Channel: Mostrar solo los últimos vídeos de cada
canal
Avoid Accidental Unsubscription: Evitar bajas accidentales
Data Settings:
Data Settings: 'Ajustes de Datos'
Select Import Type: 'Seleccionar tipo de importación'

View File

@ -243,6 +243,15 @@ User Playlists:
Quick Bookmark Enabled: Kasuta kiirjärjehoidjaid
Cannot delete the quick bookmark target playlist.: Ei saa kustutada esitusloendit,
mis on kasutusel kiirjärjehoidjate jaoks.
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: Kas
sa oled kindel, et soovid sellest esitusloendist eemaldada 1 topeltvideo? Seda
tegevust ei saa tagasi pöörata. | Kas sa oled kindel, et soovid sellest esitusloendist
eemaldada {playlistItemCount} topeltvideot? Seda tegevust ei saa tagasi pöörata.
Remove Duplicate Videos: Eemalda topeltvideod
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: Kas
sa oled kindel, et soovid sellest esitusloendist eemaldada 1 vaadatud video? Seda
tegevust ei saa tagasi pöörata. | Kas sa oled kindel, et soovid sellest esitusloendist
eemaldada {playlistItemCount} vaadatud videot? Seda tegevust ei saa tagasi pöörata.
History:
# On History Page
History: 'Ajalugu'
@ -456,6 +465,7 @@ Settings:
Manage Subscriptions: 'Halda tellimusi'
Fetch Automatically: Laadi tellimuste voog automaatselt
Only Show Latest Video for Each Channel: Iga kanali puhul näita vaid viimast videot
Avoid Accidental Unsubscription: Väldi ekslikku ja juhuslikku tellimusest loobumist
Data Settings:
Data Settings: 'Andmehaldus'
Select Import Type: 'Vali imporditava faili vorming'

View File

@ -457,6 +457,7 @@ Settings:
Fetch Automatically: Automatski dohvati feed
Only Show Latest Video for Each Channel: Prikaži samo najnoviji video za svaki
kanal
Avoid Accidental Unsubscription: Izbjegni slučajno otkazivanje pretplate
Advanced Settings:
Advanced Settings: 'Napredne postavke'
Enable Debug Mode (Prints data to the console): 'Aktiviraj modus otklanjanja grešaka

View File

@ -254,6 +254,15 @@ User Playlists:
Cannot delete the quick bookmark target playlist.: Nem lehet törölni a gyors könyvjelző
cél lejátszási listát.
Quick Bookmark Enabled: Gyors könyvjelző engedélyezve
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: Biztosan
eltávolít 1 duplikált videót ebből a lejátszási listából? Ez a művelet nem vonható
vissza. | Biztosan eltávolít {playlistItemCount} duplikált videót ebből a lejátszási
listából? Ez a művelet nem vonható vissza.
Remove Duplicate Videos: Duplikált videók törlése
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: Biztosan
eltávolít 1 megtekintett videót ebből a lejátszási listából? Ez a művelet nem
vonható vissza. | Biztosan eltávolít {playlistItemCount} megtekintett videót ebből
a lejátszási listából? Ez a művelet nem vonható vissza.
History:
# On History Page
History: 'Előzmények'
@ -470,6 +479,7 @@ Settings:
Fetch Automatically: Hírcsatorna automatikus lekérdezése
Only Show Latest Video for Each Channel: Csak a legújabb videókat jelenítse meg
a csatornáktól
Avoid Accidental Unsubscription: Kerülje el a véletlen leiratkozást
Data Settings:
Data Settings: 'Adatbeállítások'
Select Import Type: 'Importálás típusa kiválasztása'

View File

@ -244,6 +244,15 @@ User Playlists:
Cannot delete the quick bookmark target playlist.: Impossibile eliminare la playlist
di destinazione dei segnalibri rapidi.
Quick Bookmark Enabled: Segnalibro rapido abilitato
Remove Duplicate Videos: Rimuovi i video duplicati
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: Sei
sicuro di voler rimuovere 1 video duplicato da questa playlist? Questa operazione
non può essere annullata. | Vuoi rimuovere {playlistItemCount} video duplicati
da questa playlist? Questa operazione non può essere annullata.
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: Sei
sicuro di voler rimuovere 1 video guardato da questa playlist? Questa operazione
non può essere annullata. | Vuoi rimuovere {playlistItemCount} video guardati
da questa playlist? Questa operazione non può essere annullata.
History:
# On History Page
History: 'Cronologia'
@ -466,6 +475,7 @@ Settings:
Fetch Automatically: Recupera i feed automaticamente
Only Show Latest Video for Each Channel: Mostra solo il video più recente per
ciascun canale
Avoid Accidental Unsubscription: Evita la cancellazione accidentale dell'iscrizione
Advanced Settings:
Advanced Settings: 'Impostazioni Avanzate'
Enable Debug Mode (Prints data to the console): 'Abilità modalità Sviluppatore

View File

@ -457,6 +457,7 @@ Settings:
Fetch Automatically: Feed automatisch ophalen
Only Show Latest Video for Each Channel: Alleen nieuwste video voor elk kanaal
tonen
Avoid Accidental Unsubscription: Onbedoeld deabonneren voor­komen
Advanced Settings:
Advanced Settings: 'Geavanceerde Instellingen'
Enable Debug Mode (Prints data to the console): 'Schakel Debug Modus in (Print

View File

@ -445,6 +445,7 @@ Settings:
Fetch Automatically: Automatycznie odświeżaj subskrypcje
Only Show Latest Video for Each Channel: Pokaż tylko najnowszy film z każdego
kanału
Avoid Accidental Unsubscription: Uniknij przypadkowego usunięcia subskrypcji
Advanced Settings:
Advanced Settings: 'Ustawienia zaawansowane'
Enable Debug Mode (Prints data to the console): 'Włącz tryb dubugowania (pokazuje

View File

@ -239,6 +239,15 @@ User Playlists:
Quick Bookmark Enabled: Favoritos Rápidos ativado
Cannot delete the quick bookmark target playlist.: Não é possível excluir a playlist
de destino em "Favoritos Rápidos".
Remove Duplicate Videos: Remover vídeos duplicados
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: Tem
certeza de que deseja remover 1 vídeo duplicado desta playlist? Isso não pode
ser desfeito. | Tem certeza de que deseja remover {playlistItemCount} vídeos duplicados
desta playlist? Isso não pode ser desfeito.
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: Tem
certeza de que deseja remover 1 vídeo assistido desta playlist? Isso não pode
ser desfeito. | Tem certeza de que deseja remover {playlistItemCount} vídeos assistidos
desta playlist? Isso não pode ser desfeito.
History:
# On History Page
History: 'Histórico'
@ -442,6 +451,7 @@ Settings:
Fetch Automatically: Buscar feed automaticamente
Only Show Latest Video for Each Channel: Mostrar apenas vídeo mais recente para
cada canal
Avoid Accidental Unsubscription: Evitar cancelamento acidental de inscrição
Advanced Settings:
Advanced Settings: 'Configurações avançadas'
Enable Debug Mode (Prints data to the console): 'Habilitar modo de depuração (Mostra

View File

@ -248,6 +248,15 @@ User Playlists:
Cannot delete the quick bookmark target playlist.: Није могуће избрисати циљну плејлисту
за брзо обележавање.
Quick Bookmark Enabled: Брзо обележавање омогућено
Remove Duplicate Videos: Уклони дуплиране видео снимке
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: Желите
ли заиста да уклоните 1 дуплирани видео снимак с ове плејлисте? Ово не може бити
опозвано. | Желите ли заиста да уклоните {playlistItemCount} дуплираних видео
снимака с ове плејлисте? Ово не може бити опозвано.
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: Желите
ли заиста да уклоните 1 одгледани видео с ове плејлисте? Ово не може бити опозвано.
| Желите ли заиста да уклоните {playlistItemCount} одледаних видео снимака с ове
плејлисте? Ово не може бити опозвано.
History:
# On History Page
History: 'Историја'
@ -467,6 +476,7 @@ Settings:
Fetch Automatically: Аутоматски прикупи фид
Only Show Latest Video for Each Channel: Прикажи само најновији видео снимак за
сваки канал
Avoid Accidental Unsubscription: Избегни случајно отпраћивање
Distraction Free Settings:
Distraction Free Settings: 'Подешавања „Без ометања“'
Hide Video Views: 'Сакриј прегледе видео снимка'

View File

@ -247,6 +247,15 @@ User Playlists:
Quick Bookmark Enabled: Hızlı Yer İmi Etkin
Cannot delete the quick bookmark target playlist.: Hızlı yer imi hedef oynatma listesi
silinemiyor.
Remove Duplicate Videos: Yinelenen Videoları Kaldır
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: Bu
oynatma listesinden izlenen 1 videoyu kaldırmak istediğinizden emin misiniz? Bu
geri alınamaz. | Bu oynatma listesinden izlenen {playlistItemCount} videoyu kaldırmak
istediğinizden emin misiniz? Bu geri alınamaz.
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: Bu
oynatma listesinden 1 yinelenen videoyu kaldırmak istediğinizden emin misiniz?
Bu geri alınamaz. | Bu oynatma listesinden {playlistItemCount} yinelenen videoyu
kaldırmak istediğinizden emin misiniz? Bu geri alınamaz.
History:
# On History Page
History: 'Geçmiş'
@ -460,6 +469,7 @@ Settings:
Fetch Automatically: Akışı Otomatik Olarak Getir
Only Show Latest Video for Each Channel: Her Kanal için Yalnızca En Son Videoyu
Göster
Avoid Accidental Unsubscription: Yanlışlıkla Abonelikten Çıkmayı Önle
Data Settings:
Data Settings: 'Veri Ayarları'
Select Import Type: 'İçe Aktarma Türünü Seç'

View File

@ -210,6 +210,11 @@ User Playlists:
Playlists with Matching Videos: 有匹配视频的播放列表
Quick Bookmark Enabled: 启用了快速书签
Cannot delete the quick bookmark target playlist.: 无法删除快速书签目标播放列表。
Remove Duplicate Videos: 删除重复视频
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: 你确定要从此播放列表删除
1 个已观看视频吗?此操作无法撤销。 | 你确定要从此播放列表删除 {playlistItemCount} 个已观看视频吗?此操作无法撤销。
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: 你确定要从此播放列表中删除
1 个重复视频吗?无法撤销删除。| 你确定要从此播放列表中删除 {playlistItemCount} 个重复视频吗?无法撤销删除。
History:
# On History Page
History: '历史记录'
@ -401,6 +406,7 @@ Settings:
Fetch Feeds from RSS: 从RSS摘取推送
Fetch Automatically: 自动抓取订阅源
Only Show Latest Video for Each Channel: 只显示每个频道的最新视频
Avoid Accidental Unsubscription: 避免意外取消订阅
Advanced Settings:
Advanced Settings: '高级设置'
Enable Debug Mode (Prints data to the console): '允许调试模式(打印数据在控制板)'

View File

@ -210,6 +210,11 @@ User Playlists:
Playlists with Matching Videos: 包含相符影片的播放清單
Quick Bookmark Enabled: 已啟用快速書籤
Cannot delete the quick bookmark target playlist.: 無法刪除快速書籤目標播放清單。
Remove Duplicate Videos: 移除重複的影片
Are you sure you want to remove {playlistItemCount} duplicate videos from this playlist? This cannot be undone: 您確定要從此播放清單中刪除
1 個重複的影片嗎?這無法還原。 | 您確定要從此播放清單中刪除 {playlistItemCount} 個重複影片嗎?這無法還原。
Are you sure you want to remove {playlistItemCount} watched videos from this playlist? This cannot be undone: 您確定要從此播放清單中刪除
1 個觀看過的影片嗎?這無法還原。 | 您確定要從此播放清單中刪除已觀看的 {playlistItemCount} 個影片嗎?這無法還原。
History:
# On History Page
History: '觀看紀錄'
@ -403,6 +408,7 @@ Settings:
Fetch Feeds from RSS: 從RSS擷取推送
Fetch Automatically: 自動擷取 Feed
Only Show Latest Video for Each Channel: 只顯示每個頻道的最新影片
Avoid Accidental Unsubscription: 避免意外取消訂閱
Advanced Settings:
Advanced Settings: '進階設定'
Enable Debug Mode (Prints data to the console): '允許除錯型態(列印資料在控制板)'

View File

@ -2520,20 +2520,13 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
braces@^3.0.3:
braces@^3.0.3, braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
browserslist@^4.0.0, browserslist@^4.21.10, browserslist@^4.22.2:
version "4.22.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.3.tgz#299d11b7e947a6b843981392721169e27d60c5a6"
@ -3559,10 +3552,10 @@ electron-to-chromium@^1.4.668:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz#bb16bcf2a3537962fccfa746b5c98c5f7404ff46"
integrity sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==
electron@^30.0.9:
version "30.0.9"
resolved "https://registry.yarnpkg.com/electron/-/electron-30.0.9.tgz#b11400e4642a4b635e79244ba365f1d401ee60b5"
integrity sha512-ArxgdGHVu3o5uaP+Tqj8cJDvU03R6vrGrOqiMs7JXLnvQHMqXJIIxmFKQAIdJW8VoT3ac3hD21tA7cPO10RLow==
electron@^31.0.1:
version "31.0.1"
resolved "https://registry.yarnpkg.com/electron/-/electron-31.0.1.tgz#0039524f8f38c24da802c3b18a42c3951acb5897"
integrity sha512-2eBcp4iqLkTsml6mMq+iqrS5u3kJ/2mpOLP7Mj7lo0uNK3OyfNqRS9z1ArsHjBF2/HV250Te/O9nKrwQRTX/+g==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^20.9.0"
@ -4308,13 +4301,6 @@ filelist@^1.0.1:
dependencies:
minimatch "^5.0.1"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"