diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json new file mode 100644 index 00000000000..c831f6d17cf --- /dev/null +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/10.json @@ -0,0 +1,741 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "590bfc76d796cf3a0c06af47a761f7ba", + "entities": [ + { + "tableName": "blocked_channels", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_blocked_channels_url", + "unique": true, + "columnNames": [ + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_blocked_channels_url` ON `${TABLE_NAME}` (`url`)" + } + ] + }, + { + "tableName": "subscriptions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "avatarUrl", + "columnName": "avatar_url", + "affinity": "TEXT" + }, + { + "fieldPath": "subscriberCount", + "columnName": "subscriber_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "notificationMode", + "columnName": "notification_mode", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_subscriptions_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "creationDate", + "columnName": "creation_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "search", + "columnName": "search", + "affinity": "TEXT" + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_search_history_search", + "unique": false, + "columnNames": [ + "search" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)" + } + ] + }, + { + "tableName": "streams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "streamType", + "columnName": "stream_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uploaderUrl", + "columnName": "uploader_url", + "affinity": "TEXT" + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT" + }, + { + "fieldPath": "viewCount", + "columnName": "view_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "textualUploadDate", + "columnName": "textual_upload_date", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadDate", + "columnName": "upload_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "isUploadDateApproximation", + "columnName": "is_upload_date_approximation", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_streams_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ] + }, + { + "tableName": "stream_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessDate", + "columnName": "access_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatCount", + "columnName": "repeat_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stream_id", + "access_date" + ] + }, + "indices": [ + { + "name": "index_stream_history_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "stream_state", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "progressMillis", + "columnName": "progress_time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stream_id" + ] + }, + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL, `display_index` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "isThumbnailPermanent", + "columnName": "is_thumbnail_permanent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thumbnailStreamId", + "columnName": "thumbnail_stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "displayIndex", + "columnName": "display_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + } + }, + { + "tableName": "playlist_stream_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "playlistUid", + "columnName": "playlist_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamUid", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "index", + "columnName": "join_index", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "playlist_id", + "join_index" + ] + }, + "indices": [ + { + "name": "index_playlist_stream_join_playlist_id_join_index", + "unique": true, + "columnNames": [ + "playlist_id", + "join_index" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)" + }, + { + "name": "index_playlist_stream_join_stream_id", + "unique": false, + "columnNames": [ + "stream_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)" + } + ], + "foreignKeys": [ + { + "table": "playlists", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "playlist_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "remote_playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `display_index` INTEGER NOT NULL, `stream_count` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serviceId", + "columnName": "service_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "orderingName", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnail_url", + "affinity": "TEXT" + }, + { + "fieldPath": "uploader", + "columnName": "uploader", + "affinity": "TEXT" + }, + { + "fieldPath": "displayIndex", + "columnName": "display_index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "streamCount", + "columnName": "stream_count", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_remote_playlists_service_id_url", + "unique": true, + "columnNames": [ + "service_id", + "url" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)" + } + ] + }, + { + "tableName": "feed", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "streamId", + "columnName": "stream_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stream_id", + "subscription_id" + ] + }, + "indices": [ + { + "name": "index_feed_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "streams", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "stream_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "feed_group", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sortOrder", + "columnName": "sort_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "uid" + ] + }, + "indices": [ + { + "name": "index_feed_group_sort_order", + "unique": false, + "columnNames": [ + "sort_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)" + } + ] + }, + { + "tableName": "feed_group_subscription_join", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "feedGroupId", + "columnName": "group_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "group_id", + "subscription_id" + ] + }, + "indices": [ + { + "name": "index_feed_group_subscription_join_subscription_id", + "unique": false, + "columnNames": [ + "subscription_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)" + } + ], + "foreignKeys": [ + { + "table": "feed_group", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "group_id" + ], + "referencedColumns": [ + "uid" + ] + }, + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + }, + { + "tableName": "feed_last_updated", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "subscriptionId", + "columnName": "subscription_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "last_updated", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "subscription_id" + ] + }, + "foreignKeys": [ + { + "table": "subscriptions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "subscription_id" + ], + "referencedColumns": [ + "uid" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '590bfc76d796cf3a0c06af47a761f7ba')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt index 6527bd2ae88..c11ac822133 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt @@ -18,6 +18,7 @@ import org.schabi.newpipe.database.Migrations.MIGRATION_5_6 import org.schabi.newpipe.database.Migrations.MIGRATION_6_7 import org.schabi.newpipe.database.Migrations.MIGRATION_7_8 import org.schabi.newpipe.database.Migrations.MIGRATION_8_9 +import org.schabi.newpipe.database.Migrations.MIGRATION_9_10 object NewPipeDatabase { @@ -37,7 +38,8 @@ object NewPipeDatabase { MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, - MIGRATION_8_9 + MIGRATION_8_9, + MIGRATION_9_10 ).build() } diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt index 286eddf7b76..cf5defa459a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt @@ -9,6 +9,8 @@ package org.schabi.newpipe.database import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters +import org.schabi.newpipe.database.block.BlockedChannelDAO +import org.schabi.newpipe.database.block.BlockedChannelEntity import org.schabi.newpipe.database.feed.dao.FeedDAO import org.schabi.newpipe.database.feed.dao.FeedGroupDAO import org.schabi.newpipe.database.feed.model.FeedEntity @@ -34,8 +36,9 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity @TypeConverters(Converters::class) @Database( - version = Migrations.DB_VER_9, + version = Migrations.DB_VER_10, entities = [ + BlockedChannelEntity::class, SubscriptionEntity::class, SearchHistoryEntry::class, StreamEntity::class, @@ -51,6 +54,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity ] ) abstract class AppDatabase : RoomDatabase() { + abstract fun blockedChannelDAO(): BlockedChannelDAO abstract fun feedDAO(): FeedDAO abstract fun feedGroupDAO(): FeedGroupDAO abstract fun playlistDAO(): PlaylistDAO diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt index 414f7489390..a70a1a4ba52 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt @@ -29,6 +29,7 @@ object Migrations { const val DB_VER_7 = 7 const val DB_VER_8 = 8 const val DB_VER_9 = 9 + const val DB_VER_10 = 10 private val TAG = Migrations::class.java.getName() private val isDebug = MainActivity.DEBUG @@ -348,4 +349,16 @@ object Migrations { db.endTransaction() } } + + val MIGRATION_9_10 = Migration(DB_VER_9, DB_VER_10) { db -> + db.execSQL( + "CREATE TABLE IF NOT EXISTS `blocked_channels` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`url` TEXT NOT NULL, `name` TEXT NOT NULL)" + ) + db.execSQL( + "CREATE UNIQUE INDEX IF NOT EXISTS `index_blocked_channels_url` " + + "ON `blocked_channels` (`url`)" + ) + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/block/BlockedChannelDAO.kt b/app/src/main/java/org/schabi/newpipe/database/block/BlockedChannelDAO.kt new file mode 100644 index 00000000000..e8e70b80fb0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/block/BlockedChannelDAO.kt @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.block + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.reactivex.rxjava3.core.Flowable + +@Dao +abstract class BlockedChannelDAO { + @Query( + """ + SELECT * FROM blocked_channels + ORDER BY name COLLATE NOCASE ASC, url COLLATE NOCASE ASC + """ + ) + abstract fun getAll(): Flowable> + + @Query("SELECT url FROM blocked_channels") + abstract fun getBlockedUrls(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun upsert(blockedChannel: BlockedChannelEntity): Long + + @Query("DELETE FROM blocked_channels WHERE url = :url") + abstract fun deleteByUrl(url: String): Int +} diff --git a/app/src/main/java/org/schabi/newpipe/database/block/BlockedChannelEntity.kt b/app/src/main/java/org/schabi/newpipe/database/block/BlockedChannelEntity.kt new file mode 100644 index 00000000000..61207b33244 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/block/BlockedChannelEntity.kt @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.block + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = BlockedChannelEntity.BLOCKED_CHANNEL_TABLE, + indices = [Index(value = [BlockedChannelEntity.BLOCKED_CHANNEL_URL], unique = true)] +) +data class BlockedChannelEntity( + @PrimaryKey(autoGenerate = true) + val uid: Long = 0, + + @ColumnInfo(name = BLOCKED_CHANNEL_URL) + val url: String, + + @ColumnInfo(name = BLOCKED_CHANNEL_NAME) + val name: String +) { + companion object { + const val BLOCKED_CHANNEL_TABLE = "blocked_channels" + const val BLOCKED_CHANNEL_URL = "url" + const val BLOCKED_CHANNEL_NAME = "name" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt index 2d149ec2344..ee055706c6c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt @@ -89,6 +89,7 @@ import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment.Companion.g import org.schabi.newpipe.ktx.AnimationType import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animateRotation +import org.schabi.newpipe.local.channel.BlockedChannelManager import org.schabi.newpipe.local.dialog.PlaylistDialog import org.schabi.newpipe.local.history.HistoryRecordManager import org.schabi.newpipe.local.playlist.LocalPlaylistFragment @@ -203,6 +204,9 @@ class VideoDetailFragment : private var currentWorker: Disposable? = null private val disposables = CompositeDisposable() private var positionSubscriber: Disposable? = null + private val blockedChannelManager by lazy(LazyThreadSafetyMode.NONE) { + BlockedChannelManager(requireContext()) + } /*////////////////////////////////////////////////////////////////////////// // Service management @@ -780,6 +784,7 @@ class VideoDetailFragment : val prefs = PreferenceManager.getDefaultSharedPreferences(activity) currentWorker = ExtractorHelper.getStreamInfo(serviceId, url, forceLoad) .subscribeOn(Schedulers.io()) + .flatMap { blockedChannelManager.filterStreamInfo(it) } .observeOn(AndroidSchedulers.mainThread()) .subscribe( { result -> diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index 848dfe6f55e..49ea71b1b47 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -20,6 +20,7 @@ import org.schabi.newpipe.extractor.ListInfo; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; +import org.schabi.newpipe.local.channel.BlockedChannelManager; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.views.NewPipeRecyclerView; @@ -46,6 +47,7 @@ public abstract class BaseListInfoFragment blockedChannelManager.filterList(result.getRelatedItems()) + .map(filteredItems -> { + result.setRelatedItems(filteredItems); + return result; + })) .observeOn(AndroidSchedulers.mainThread()) .subscribe((@NonNull final L result) -> { isLoading.set(false); @@ -177,6 +185,12 @@ protected void loadMoreItems() { currentWorker = loadMoreItemsLogic() .subscribeOn(Schedulers.io()) + .flatMap(result -> blockedChannelManager.filterList(result.getItems()) + .map(filteredItems -> new ListExtractor.InfoItemsPage<>( + filteredItems, + result.getNextPage(), + result.getErrors() + ))) .observeOn(AndroidSchedulers.mainThread()) .doFinally(this::allowDownwardFocusScroll) .subscribe(infoItemsPage -> { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 0f961f9713c..5375463b379 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -64,6 +64,7 @@ import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.ktx.ExceptionUtils; +import org.schabi.newpipe.local.channel.BlockedChannelManager; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.ui.emptystate.EmptyStateSpec; @@ -158,6 +159,7 @@ public class SearchFragment extends BaseListFragment blockedChannelManager.filterList(result.getRelatedItems()) + .map(filteredItems -> { + result.setRelatedItems(filteredItems); + return result; + })) .observeOn(AndroidSchedulers.mainThread()) .doOnEvent((searchResult, throwable) -> isLoading.set(false)) .subscribe(this::handleResult, this::onItemError); @@ -918,6 +926,12 @@ protected void loadMoreItems() { sortFilter, nextPage) .subscribeOn(Schedulers.io()) + .flatMap(result -> blockedChannelManager.filterList(result.getItems()) + .map(filteredItems -> new ListExtractor.InfoItemsPage<>( + filteredItems, + result.getNextPage(), + result.getErrors() + ))) .observeOn(AndroidSchedulers.mainThread()) .doOnEvent((nextItemsResult, throwable) -> isLoading.set(false)) .subscribe(this::handleNextItems, this::onItemError); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java index cbaae2834b8..1563012efeb 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java @@ -325,7 +325,8 @@ public Builder addDefaultEndEntries() { StreamDialogDefaultEntry.DOWNLOAD, StreamDialogDefaultEntry.APPEND_PLAYLIST, StreamDialogDefaultEntry.SHARE, - StreamDialogDefaultEntry.OPEN_IN_BROWSER + StreamDialogDefaultEntry.OPEN_IN_BROWSER, + StreamDialogDefaultEntry.BLOCK_CHANNEL ); addPlayWithKodiEntryIfNeeded(); addMarkAsWatchedEntryIfNeeded(); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java index 6e06827716e..ec2f215a47b 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java @@ -19,6 +19,7 @@ import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog; +import org.schabi.newpipe.local.channel.BlockedChannelUiHelper; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.external_communication.KoreUtils; @@ -138,6 +139,8 @@ public enum StreamDialogDefaultEntry { OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) -> ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())), + BLOCK_CHANNEL(R.string.block_channel, (fragment, item) -> + BlockedChannelUiHelper.blockChannel(fragment, item)), MARK_AS_WATCHED(R.string.mark_as_watched, (fragment, item) -> new HistoryRecordManager(fragment.getContext()) diff --git a/app/src/main/java/org/schabi/newpipe/local/channel/BlockedChannelManager.kt b/app/src/main/java/org/schabi/newpipe/local/channel/BlockedChannelManager.kt new file mode 100644 index 00000000000..41031af8959 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/channel/BlockedChannelManager.kt @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.local.channel + +import android.content.Context +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single +import org.schabi.newpipe.NewPipeDatabase +import org.schabi.newpipe.database.block.BlockedChannelDAO +import org.schabi.newpipe.database.block.BlockedChannelEntity +import org.schabi.newpipe.extractor.InfoItem +import org.schabi.newpipe.extractor.channel.ChannelInfoItem +import org.schabi.newpipe.extractor.stream.StreamInfo +import org.schabi.newpipe.extractor.stream.StreamInfoItem + +class BlockedChannelManager(context: Context) { + private val blockedChannelTable: BlockedChannelDAO = + NewPipeDatabase.getInstance(context).blockedChannelDAO() + + fun blockedChannels(): Flowable> = blockedChannelTable.getAll() + + fun blockChannel(url: String, name: String): Completable = Completable.fromAction { + blockedChannelTable.upsert( + BlockedChannelEntity( + url = url, + name = name.ifBlank { url } + ) + ) + } + + fun unblockChannel(url: String): Completable = Completable.fromAction { + blockedChannelTable.deleteByUrl(url) + } + + fun filterList(items: List): Single> = Single.fromCallable { + val blockedUrls = blockedChannelTable.getBlockedUrls().toHashSet() + if (blockedUrls.isEmpty()) { + items + } else { + items.filterNot { it.belongsToBlockedChannel(blockedUrls) } + } + } + + fun filterStreamInfo(info: StreamInfo): Single = filterList(info.relatedItems).map { filteredItems -> + info.relatedItems = filteredItems + info + } + + private fun InfoItem.belongsToBlockedChannel(blockedUrls: Set): Boolean { + return when (this) { + is StreamInfoItem -> !uploaderUrl.isNullOrBlank() && blockedUrls.contains(uploaderUrl) + is ChannelInfoItem -> !url.isNullOrBlank() && blockedUrls.contains(url) + else -> false + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/local/channel/BlockedChannelUiHelper.kt b/app/src/main/java/org/schabi/newpipe/local/channel/BlockedChannelUiHelper.kt new file mode 100644 index 00000000000..d491695233a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/channel/BlockedChannelUiHelper.kt @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.local.channel + +import android.app.Activity +import android.content.Context +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import com.google.android.material.snackbar.Snackbar +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.util.SparseItemUtil +import org.schabi.newpipe.util.ThemeHelper + +object BlockedChannelUiHelper { + @JvmStatic + fun blockChannel(fragment: Fragment, streamInfoItem: StreamInfoItem) { + blockChannel(fragment.requireContext(), fragment.requireActivity(), streamInfoItem) + } + + @JvmStatic + fun blockChannel(activity: FragmentActivity, streamInfoItem: StreamInfoItem) { + blockChannel(activity, activity, streamInfoItem) + } + + private fun blockChannel(context: Context, activity: Activity, streamInfoItem: StreamInfoItem) { + SparseItemUtil.fetchUploaderUrlIfSparse( + context, + streamInfoItem.serviceId, + streamInfoItem.url, + streamInfoItem.uploaderUrl + ) { uploaderUrl -> + if (uploaderUrl.isNullOrBlank()) { + return@fetchUploaderUrlIfSparse + } + + val blockedChannelManager = BlockedChannelManager(context) + blockedChannelManager.blockChannel(uploaderUrl, streamInfoItem.uploaderName ?: "") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + showUndoSnackbar( + context = context, + activity = activity, + blockedChannelManager = blockedChannelManager, + uploaderUrl = uploaderUrl, + channelName = streamInfoItem.uploaderName + ?: context.getString(R.string.unknown_content) + ) + }, + { } + ) + } + } + + private fun showUndoSnackbar( + context: Context, + activity: Activity, + blockedChannelManager: BlockedChannelManager, + uploaderUrl: String, + channelName: String + ) { + val rootView = activity.findViewById(android.R.id.content) ?: return + Snackbar.make( + rootView, + context.getString(R.string.channel_blocked_message, channelName), + Snackbar.LENGTH_LONG + ).setAction(R.string.undo) { + blockedChannelManager.unblockChannel(uploaderUrl) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({}, { }) + }.setActionTextColor(ThemeHelper.resolveColorFromAttr(context, android.R.attr.colorAccent)) + .show() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsAdapter.kt b/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsAdapter.kt new file mode 100644 index 00000000000..07d8d9aef58 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsAdapter.kt @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.schabi.newpipe.database.block.BlockedChannelEntity +import org.schabi.newpipe.databinding.ItemBlockedChannelBinding + +class BlockedChannelsAdapter( + private val onUnblock: (BlockedChannelEntity) -> Unit +) : ListAdapter(DiffCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BlockedChannelViewHolder { + val binding = ItemBlockedChannelBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return BlockedChannelViewHolder(binding, onUnblock) + } + + override fun onBindViewHolder(holder: BlockedChannelViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + class BlockedChannelViewHolder( + private val binding: ItemBlockedChannelBinding, + private val onUnblock: (BlockedChannelEntity) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: BlockedChannelEntity) { + binding.channelName.text = item.name + binding.unblockButton.setOnClickListener { onUnblock(item) } + } + } + + private object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: BlockedChannelEntity, + newItem: BlockedChannelEntity + ): Boolean = oldItem.url == newItem.url + + override fun areContentsTheSame( + oldItem: BlockedChannelEntity, + newItem: BlockedChannelEntity + ): Boolean = oldItem == newItem + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsFragment.kt new file mode 100644 index 00000000000..d62e6efe824 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsFragment.kt @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import org.schabi.newpipe.R +import org.schabi.newpipe.databinding.FragmentBlockedChannelsBinding +import org.schabi.newpipe.util.ThemeHelper + +class BlockedChannelsFragment : Fragment() { + private var _binding: FragmentBlockedChannelsBinding? = null + private val binding get() = _binding!! + private val viewModel: BlockedChannelsViewModel by viewModels() + + private lateinit var adapter: BlockedChannelsAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentBlockedChannelsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + adapter = BlockedChannelsAdapter(viewModel::unblockChannel) + binding.blockedChannelsList.layoutManager = LinearLayoutManager(requireContext()) + binding.blockedChannelsList.adapter = adapter + + viewModel.blockedChannelsLiveData.observe(viewLifecycleOwner) { blockedChannels -> + adapter.submitList(blockedChannels) + binding.emptyState.isVisible = blockedChannels.isEmpty() + } + } + + override fun onResume() { + super.onResume() + ThemeHelper.setTitleToAppCompatActivity(activity, getString(R.string.blocked_channels)) + } + + override fun onDestroyView() { + _binding = null + super.onDestroyView() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsViewModel.kt b/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsViewModel.kt new file mode 100644 index 00000000000..ee2ab98a35b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/BlockedChannelsViewModel.kt @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.schedulers.Schedulers +import org.schabi.newpipe.database.block.BlockedChannelEntity +import org.schabi.newpipe.local.channel.BlockedChannelManager + +class BlockedChannelsViewModel(application: Application) : AndroidViewModel(application) { + private val blockedChannelManager = BlockedChannelManager(application) + private val disposables = CompositeDisposable() + private val mutableBlockedChannels = MutableLiveData>(emptyList()) + val blockedChannelsLiveData: LiveData> = mutableBlockedChannels + + init { + disposables.add( + blockedChannelManager.blockedChannels() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(mutableBlockedChannels::postValue) + ) + } + + fun unblockChannel(channel: BlockedChannelEntity) { + disposables.add( + blockedChannelManager.unblockChannel(channel.url) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({}, { }) + ) + } + + override fun onCleared() { + disposables.dispose() + super.onCleared() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt index 099a9300508..6ae207557b0 100644 --- a/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt +++ b/app/src/main/java/org/schabi/newpipe/ui/components/items/stream/StreamMenu.kt @@ -12,6 +12,7 @@ import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.download.DownloadDialog import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.ktx.findFragmentActivity +import org.schabi.newpipe.local.channel.BlockedChannelUiHelper import org.schabi.newpipe.local.dialog.PlaylistAppendDialog import org.schabi.newpipe.local.dialog.PlaylistDialog import org.schabi.newpipe.player.helper.PlayerHolder @@ -116,6 +117,13 @@ fun StreamMenu( ShareUtils.openUrlInBrowser(context, stream.url) } ) + DropdownMenuItem( + text = { Text(text = stringResource(R.string.block_channel)) }, + onClick = { + onDismissRequest() + BlockedChannelUiHelper.blockChannel(context.findFragmentActivity(), stream) + } + ) DropdownMenuItem( text = { Text(text = stringResource(R.string.mark_as_watched)) }, onClick = { diff --git a/app/src/main/res/layout/fragment_blocked_channels.xml b/app/src/main/res/layout/fragment_blocked_channels.xml new file mode 100644 index 00000000000..f4bfb862e5f --- /dev/null +++ b/app/src/main/res/layout/fragment_blocked_channels.xml @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/item_blocked_channel.xml b/app/src/main/res/layout/item_blocked_channel.xml new file mode 100644 index 00000000000..b2ebd4325f1 --- /dev/null +++ b/app/src/main/res/layout/item_blocked_channel.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 207f1363f5f..e4e9e1b7942 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -142,6 +142,13 @@ Default content country Default content language PeerTube instances + Blocked channels + Hide videos and channels from selected creators in Trending, Search, and recommendations + blocked_channels + No blocked channels yet + Block channel + Unblock + %1$s blocked Select your favorite PeerTube instances Find the instances you like on %s Add instance diff --git a/app/src/main/res/xml/content_settings.xml b/app/src/main/res/xml/content_settings.xml index f17783a2285..2d12cbf5e75 100644 --- a/app/src/main/res/xml/content_settings.xml +++ b/app/src/main/res/xml/content_settings.xml @@ -66,6 +66,14 @@ app:singleLineTitle="false" app:iconSpaceReserved="false" /> + +