mirror of
https://github.com/FreeTubeApp/FreeTube
synced 2024-11-22 18:07:13 +01:00
Merge branch 'development' of github.com:FreeTubeApp/FreeTube into feat/add-page-bookmarking
This commit is contained in:
commit
13ba73e6f1
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 () {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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)))
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
*/
|
||||
|
52
src/renderer/helpers/playlists.js
Normal file
52
src/renderer/helpers/playlists.js
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -16,6 +16,7 @@
|
||||
.version {
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
margin-block-end: 1em;
|
||||
}
|
||||
|
||||
.about-chunks {
|
||||
|
@ -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)')
|
||||
|
@ -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 ?? ''
|
||||
|
@ -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): 'تمكين وضع التنقيح (يطبع البيانات
|
||||
|
@ -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}".'
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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 voorkomen
|
||||
Advanced Settings:
|
||||
Advanced Settings: 'Geavanceerde Instellingen'
|
||||
Enable Debug Mode (Prints data to the console): 'Schakel Debug Modus in (Print
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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: 'Сакриј прегледе видео снимка'
|
||||
|
@ -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ç'
|
||||
|
@ -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): '允许调试模式(打印数据在控制板)'
|
||||
|
@ -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): '允許除錯型態(列印資料在控制板)'
|
||||
|
24
yarn.lock
24
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user