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 10639a3a43bae..0dae7e216fb0e 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
@@ -71,10 +71,15 @@ export default defineComponent({
type: Boolean,
default: false,
},
+ hideForbiddenTitles: {
+ type: Boolean,
+ default: true
+ }
},
data: function () {
return {
- visible: false
+ visible: false,
+ display: 'block'
}
},
computed: {
@@ -91,9 +96,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() {
@@ -103,6 +114,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 197d7226ca079..61fc149b3b760 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}`
})
@@ -1735,6 +1758,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 c26943a73c73c..cdb9fb0579106 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
@@ -454,6 +456,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
@@ -702,6 +706,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
@@ -967,6 +972,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,
@@ -1021,6 +1027,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