diff --git a/src/renderer/components/distraction-settings/distraction-settings.js b/src/renderer/components/distraction-settings/distraction-settings.js index c704206642e4f..a540b94bde48b 100644 --- a/src/renderer/components/distraction-settings/distraction-settings.js +++ b/src/renderer/components/distraction-settings/distraction-settings.js @@ -120,13 +120,16 @@ export default defineComponent({ return ch }) }, + forbiddenTitles: function() { + return JSON.parse(this.$store.getters.getForbiddenTitles) + }, hideSubscriptionsLiveTooltip: function () { return this.$t('Tooltips.Distraction Free Settings.Hide Subscriptions Live', { appWideSetting: this.$t('Settings.Distraction Free Settings.Hide Live Streams'), subsection: this.$t('Settings.Distraction Free Settings.Sections.General'), settingsSection: this.$t('Settings.Distraction Free Settings.Distraction Free Settings') }) - } + }, }, mounted: function () { this.verifyChannelsHidden() @@ -148,6 +151,9 @@ export default defineComponent({ handleChannelsHidden: function (value) { this.updateChannelsHidden(JSON.stringify(value)) }, + handleForbiddenTitles: function (value) { + this.updateForbiddenTitles(JSON.stringify(value)) + }, handleChannelsExists: function () { showToast(this.$t('Settings.Distraction Free Settings.Hide Channels Already Exists')) }, @@ -206,6 +212,7 @@ export default defineComponent({ 'updateHideSharingActions', 'updateHideChapters', 'updateChannelsHidden', + 'updateForbiddenTitles', 'updateShowDistractionFreeTitles', 'updateHideFeaturedChannels', 'updateHideChannelShorts', diff --git a/src/renderer/components/distraction-settings/distraction-settings.vue b/src/renderer/components/distraction-settings/distraction-settings.vue index eed6640160ff6..6420d447c8916 100644 --- a/src/renderer/components/distraction-settings/distraction-settings.vue +++ b/src/renderer/components/distraction-settings/distraction-settings.vue @@ -239,12 +239,24 @@ :tooltip="$t('Tooltips.Distraction Free Settings.Hide Channels')" :validate-tag-name="validateChannelId" :find-tag-info="findChannelTagInfo" + :are-channel-tags="true" @invalid-name="handleInvalidChannel" @error-find-tag-info="handleChannelAPIError" @change="handleChannelsHidden" @already-exists="handleChannelsExists" /> + + + diff --git a/src/renderer/components/ft-community-post/ft-community-post.js b/src/renderer/components/ft-community-post/ft-community-post.js index f3a229c184a58..350eccc102020 100644 --- a/src/renderer/components/ft-community-post/ft-community-post.js +++ b/src/renderer/components/ft-community-post/ft-community-post.js @@ -25,6 +25,10 @@ export default defineComponent({ appearance: { type: String, required: true + }, + hideForbiddenTitles: { + type: Boolean, + default: true } }, data: function () { @@ -44,6 +48,15 @@ export default defineComponent({ computed: { listType: function () { return this.$store.getters.getListType + }, + + forbiddenTitles() { + if (!this.hideForbiddenTitles) { return [] } + return JSON.parse(this.$store.getters.getForbiddenTitles) + }, + + hideVideo() { + return this.forbiddenTitles.some((text) => this.data.postContent.content.title?.toLowerCase().includes(text.toLowerCase())) } }, created: function () { diff --git a/src/renderer/components/ft-community-post/ft-community-post.scss b/src/renderer/components/ft-community-post/ft-community-post.scss index b55876fe91a41..64a618cfe9f4e 100644 --- a/src/renderer/components/ft-community-post/ft-community-post.scss +++ b/src/renderer/components/ft-community-post/ft-community-post.scss @@ -13,6 +13,12 @@ box-sizing: border-box; } +.hiddenVideo { + font-style: italic; + opacity: 0.85; + text-align: center; +} + .communityImage { block-size: 100%; inline-size: 100%; diff --git a/src/renderer/components/ft-community-post/ft-community-post.vue b/src/renderer/components/ft-community-post/ft-community-post.vue index 34379ce287d82..d5701b660fe24 100644 --- a/src/renderer/components/ft-community-post/ft-community-post.vue +++ b/src/renderer/components/ft-community-post/ft-community-post.vue @@ -90,9 +90,16 @@ v-if="type === 'video'" > +

+ {{ '[' + $t('Channel.Community.Video hidden by FreeTube') + ']' }} +

diff --git a/src/renderer/components/ft-input-tags/ft-input-tags.js b/src/renderer/components/ft-input-tags/ft-input-tags.js index 0c04f021be1f1..e139db3b76b9e 100644 --- a/src/renderer/components/ft-input-tags/ft-input-tags.js +++ b/src/renderer/components/ft-input-tags/ft-input-tags.js @@ -1,5 +1,6 @@ import { defineComponent } from 'vue' import FtInput from '../ft-input/ft-input.vue' +import { showToast } from '../../helpers/utils' export default defineComponent({ name: 'FtInputTags', @@ -7,6 +8,10 @@ export default defineComponent({ 'ft-input': FtInput, }, props: { + areChannelTags: { + type: Boolean, + default: false + }, disabled: { type: Boolean, default: false @@ -23,6 +28,10 @@ export default defineComponent({ type: String, required: true }, + minInputLength: { + type: Number, + default: 1 + }, showActionButton: { type: Boolean, default: true @@ -46,6 +55,30 @@ export default defineComponent({ }, methods: { updateTags: async function (text, _e) { + if (this.areChannelTags) { + await this.updateChannelTags(text, _e) + return + } + // add tag and update tag list + const trimmedText = text.trim() + + if (this.minInputLength > trimmedText.length) { + showToast(this.$tc('Trimmed input must be at least N characters long', this.minInputLength, { length: this.minInputLength })) + return + } + + if (this.tagList.includes(trimmedText)) { + showToast(this.$t('Tag already exists', { tagName: trimmedText })) + return + } + + const newList = this.tagList.slice(0) + newList.push(trimmedText) + this.$emit('change', newList) + // clear input box + this.$refs.tagNameInput.handleClearTextClick() + }, + updateChannelTags: async function (text, _e) { // get text without spaces after last '/' in url, if any const name = text.split('/').pop().trim() @@ -73,6 +106,20 @@ export default defineComponent({ this.$refs.tagNameInput.handleClearTextClick() }, removeTag: function (tag) { + if (this.areChannelTags) { + this.removeChannelTag(tag) + return + } + // Remove tag from list + const tagName = tag.trim() + if (this.tagList.includes(tagName)) { + const newList = this.tagList.slice(0) + const index = newList.indexOf(tagName) + newList.splice(index, 1) + this.$emit('change', newList) + } + }, + removeChannelTag: function (tag) { // Remove tag from list if (this.tagList.some((tmpTag) => tmpTag.name === tag.name)) { const newList = this.tagList.filter((tmpTag) => tmpTag.name !== tag.name) diff --git a/src/renderer/components/ft-input-tags/ft-input-tags.vue b/src/renderer/components/ft-input-tags/ft-input-tags.vue index 6b236e937ca77..58464b5476cd4 100644 --- a/src/renderer/components/ft-input-tags/ft-input-tags.vue +++ b/src/renderer/components/ft-input-tags/ft-input-tags.vue @@ -13,6 +13,7 @@ :disabled="disabled" :placeholder="tagNamePlaceholder" :label="label" + :min-input-length="minInputLength" :show-label="true" :tooltip="tooltip" :show-action-button="showActionButton" @@ -26,18 +27,21 @@ v-for="tag in tagList" :key="tag.id" > - - + - - {{ (tag.preferredName) ? tag.preferredName : tag.name }} + + + {{ (tag.preferredName) ? tag.preferredName : tag.name }} + + {{ tag }} this.data.title?.toLowerCase().includes(text.toLowerCase()))) { + return false + } } else if (dataType === 'channel') { const attrsToCheck = [ // Local API @@ -117,6 +128,9 @@ export default defineComponent({ return false } } else if (dataType === 'playlist') { + if (this.forbiddenTitles.some((text) => this.data.title?.toLowerCase().includes(text.toLowerCase()))) { + return false + } const attrsToCheck = [ // Local API data.channelId, diff --git a/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.vue b/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.vue index 1cace8c779682..654d6bbde5aa8 100644 --- a/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.vue +++ b/src/renderer/components/ft-list-lazy-wrapper/ft-list-lazy-wrapper.vue @@ -31,6 +31,7 @@ /> diff --git a/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js b/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js index ef5a044b27d44..5fb6ba9f8604f 100644 --- a/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js +++ b/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js @@ -75,10 +75,15 @@ export default defineComponent({ type: Boolean, default: false, }, + hideForbiddenTitles: { + type: Boolean, + default: true + } }, data: function () { return { - visible: false + visible: false, + display: 'block' } }, computed: { @@ -95,9 +100,15 @@ export default defineComponent({ }) }, + forbiddenTitles() { + if (!this.hideForbiddenTitles) { return [] } + return JSON.parse(this.$store.getters.getForbiddenTitles) + }, + shouldBeVisible() { return !(this.channelsHidden.some(ch => ch.name === this.data.authorId) || - this.channelsHidden.some(ch => ch.name === this.data.author)) + this.channelsHidden.some(ch => ch.name === this.data.author) || + this.forbiddenTitles.some((text) => this.data.title?.toLowerCase().includes(text.toLowerCase()))) } }, created() { @@ -107,6 +118,8 @@ export default defineComponent({ onVisibilityChanged: function (visible) { if (visible && this.shouldBeVisible) { this.visible = visible + } else if (visible) { + this.display = 'none' } } } diff --git a/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.vue b/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.vue index 9af01de8af16a..69abd6a3a9221 100644 --- a/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.vue +++ b/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.vue @@ -4,6 +4,7 @@ callback: onVisibilityChanged, once: true, }" + :style="{ display }" > { + // Legacy support + if (typeof ch === 'string') { + return { name: ch, preferredName: '', icon: '' } + } + return ch + }) + }, + forbiddenTitles() { + return JSON.parse(this.$store.getters.getForbiddenTitles) + }, isUserPlaylistRequested: function () { return this.$route.query.playlistType === 'user' }, @@ -1316,6 +1327,19 @@ export default defineComponent({ this.$refs.watchVideoPlaylist.playNextVideo() return } + + let nextVideoId = null + if (!this.watchingPlaylist) { + const forbiddenTitles = this.forbiddenTitles + const channelsHidden = this.channelsHidden + nextVideoId = this.recommendedVideos.find((video) => + !this.isHiddenVideo(forbiddenTitles, channelsHidden, video) + )?.videoId + if (!nextVideoId) { + return + } + } + const nextVideoInterval = this.defaultInterval this.playNextTimeout = setTimeout(() => { const player = this.$refs.videoPlayer.player @@ -1323,7 +1347,6 @@ export default defineComponent({ if (this.watchingPlaylist) { this.$refs.watchVideoPlaylist.playNextVideo() } else { - const nextVideoId = this.recommendedVideos[0].videoId this.$router.push({ path: `/watch/${nextVideoId}` }) @@ -1736,6 +1759,12 @@ export default defineComponent({ document.title = `${this.videoTitle} - FreeTube` }, + isHiddenVideo: function (forbiddenTitles, channelsHidden, video) { + return channelsHidden.some(ch => ch.name === video.authorId) || + channelsHidden.some(ch => ch.name === video.author) || + forbiddenTitles.some((text) => video.title?.toLowerCase().includes(text.toLowerCase())) + }, + updateLocalPlaylistLastPlayedAtSometimes() { if (this.selectedUserPlaylist == null) { return } diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index b18dc697596d6..b8fc52b81a3b5 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -53,6 +53,8 @@ Global: Subscriber Count: 1 subscriber | {count} subscribers View Count: 1 view | {count} views Watching Count: 1 watching | {count} watching + Input Tags: + Length Requirement: Tag must be at least {number} characters long # Search Bar Search / Go to URL: Search / Go to URL @@ -465,6 +467,8 @@ Settings: Hide Channel Shorts: Hide Channel Shorts Hide Channel Podcasts: Hide Channel Podcasts Hide Channel Releases: Hide Channel Releases + Hide Videos and Playlists Containing Text: Hide Videos and Playlists Containing Text + Hide Videos and Playlists Containing Text Placeholder: Word, Word Fragment, or Phrase Hide Subscriptions Videos: Hide Subscriptions Videos Hide Subscriptions Shorts: Hide Subscriptions Shorts Hide Subscriptions Live: Hide Subscriptions Live @@ -715,6 +719,7 @@ Channel: votes: '{votes} votes' Reveal Answers: Reveal Answers Hide Answers: Hide Answers + Video hidden by FreeTube: Video hidden by FreeTube Video: Mark As Watched: Mark As Watched Remove From History: Remove From History @@ -983,6 +988,7 @@ Tooltips: Hide Channels: Enter a channel ID to hide all videos, playlists and the channel itself from appearing in search, trending, most popular and recommended. The channel ID entered must be a complete match and is case sensitive. Hide Subscriptions Live: 'This setting is overridden by the app-wide "{appWideSetting}" setting, in the "{subsection}" section of the "{settingsSection}"' + Hide Videos and Playlists Containing Text: Enter a word, word fragment, or phrase (case insensitive) to hide all videos & playlists whose original titles contain it throughout all of FreeTube, excluding only History, Your Playlists, and videos inside of playlists. Subscription Settings: Fetch Feeds from RSS: When enabled, FreeTube will use RSS instead of its default method for grabbing your subscription feed. RSS is faster and prevents IP blocking, @@ -1038,6 +1044,8 @@ Screenshot Success: Saved screenshot as "{filePath}" Screenshot Error: Screenshot failed. {error} Channel Hidden: '{channel} added to channel filter' Channel Unhidden: '{channel} removed from channel filter' +Trimmed input must be at least N characters long: Trimmed input must be at least 1 character long | Trimmed input must be at least {length} characters long +Tag already exists: '"{tagName}" tag already exists' Hashtag: Hashtag: Hashtag