mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-15 06:40:30 +05:30
Merge pull request #3431 from Bnyro/subscription-groups
Subscription groups
This commit is contained in:
commit
98a094ffd7
496
app/schemas/com.github.libretube.db.AppDatabase/11.json
Normal file
496
app/schemas/com.github.libretube.db.AppDatabase/11.json
Normal file
@ -0,0 +1,496 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 11,
|
||||
"identityHash": "435fb16c318b2b9fdd0d0120931f7400",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "watchHistoryItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER, PRIMARY KEY(`videoId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploadDate",
|
||||
"columnName": "uploadDate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "uploader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderUrl",
|
||||
"columnName": "uploaderUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderAvatar",
|
||||
"columnName": "uploaderAvatar",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnailUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"videoId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "watchPosition",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"videoId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "searchHistoryItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, PRIMARY KEY(`query`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "query",
|
||||
"columnName": "query",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"query"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "customInstance",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `apiUrl` TEXT NOT NULL, `frontendUrl` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "apiUrl",
|
||||
"columnName": "apiUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "frontendUrl",
|
||||
"columnName": "frontendUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "localSubscription",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`channelId` TEXT NOT NULL, PRIMARY KEY(`channelId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "channelId",
|
||||
"columnName": "channelId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"channelId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "playlistBookmark",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlistId` TEXT NOT NULL, `playlistName` TEXT, `thumbnailUrl` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, PRIMARY KEY(`playlistId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "playlistId",
|
||||
"columnName": "playlistId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "playlistName",
|
||||
"columnName": "playlistName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnailUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "uploader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderUrl",
|
||||
"columnName": "uploaderUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderAvatar",
|
||||
"columnName": "uploaderAvatar",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"playlistId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "LocalPlaylist",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnailUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "LocalPlaylistItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` INTEGER NOT NULL, `videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "playlistId",
|
||||
"columnName": "playlistId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploadDate",
|
||||
"columnName": "uploadDate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "uploader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderUrl",
|
||||
"columnName": "uploaderUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderAvatar",
|
||||
"columnName": "uploaderAvatar",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnailUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "download",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `uploader` TEXT NOT NULL, `uploadDate` TEXT, `thumbnailPath` TEXT, PRIMARY KEY(`videoId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "uploader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploadDate",
|
||||
"columnName": "uploadDate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailPath",
|
||||
"columnName": "thumbnailPath",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"videoId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "downloadItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `videoId` TEXT NOT NULL, `fileName` TEXT NOT NULL, `path` TEXT NOT NULL, `url` TEXT, `format` TEXT, `quality` TEXT, `downloadSize` INTEGER NOT NULL, FOREIGN KEY(`videoId`) REFERENCES `download`(`videoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "fileName",
|
||||
"columnName": "fileName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "path",
|
||||
"columnName": "path",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "format",
|
||||
"columnName": "format",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "quality",
|
||||
"columnName": "quality",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "downloadSize",
|
||||
"columnName": "downloadSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_downloadItem_path",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"path"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_downloadItem_path` ON `${TABLE_NAME}` (`path`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "download",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"videoId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"videoId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "subscriptionGroups",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `channels` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "channels",
|
||||
"columnName": "channels",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"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, '435fb16c318b2b9fdd0d0120931f7400')"
|
||||
]
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import com.github.libretube.db.dao.LocalPlaylistsDao
|
||||
import com.github.libretube.db.dao.LocalSubscriptionDao
|
||||
import com.github.libretube.db.dao.PlaylistBookmarkDao
|
||||
import com.github.libretube.db.dao.SearchHistoryDao
|
||||
import com.github.libretube.db.dao.SubscriptionGroupsDao
|
||||
import com.github.libretube.db.dao.WatchHistoryDao
|
||||
import com.github.libretube.db.dao.WatchPositionDao
|
||||
import com.github.libretube.db.obj.CustomInstance
|
||||
@ -20,6 +21,7 @@ import com.github.libretube.db.obj.LocalPlaylistItem
|
||||
import com.github.libretube.db.obj.LocalSubscription
|
||||
import com.github.libretube.db.obj.PlaylistBookmark
|
||||
import com.github.libretube.db.obj.SearchHistoryItem
|
||||
import com.github.libretube.db.obj.SubscriptionGroup
|
||||
import com.github.libretube.db.obj.WatchHistoryItem
|
||||
import com.github.libretube.db.obj.WatchPosition
|
||||
|
||||
@ -34,13 +36,15 @@ import com.github.libretube.db.obj.WatchPosition
|
||||
LocalPlaylist::class,
|
||||
LocalPlaylistItem::class,
|
||||
Download::class,
|
||||
DownloadItem::class
|
||||
DownloadItem::class,
|
||||
SubscriptionGroup::class
|
||||
],
|
||||
version = 10,
|
||||
version = 11,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 7, to = 8),
|
||||
AutoMigration(from = 8, to = 9),
|
||||
AutoMigration(from = 9, to = 10)
|
||||
AutoMigration(from = 9, to = 10),
|
||||
AutoMigration(from = 10, to = 11)
|
||||
]
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
@ -84,4 +88,9 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
* Downloads
|
||||
*/
|
||||
abstract fun downloadDao(): DownloadDao
|
||||
|
||||
/**
|
||||
* Subscription groups
|
||||
*/
|
||||
abstract fun subscriptionGroupsDao(): SubscriptionGroupsDao
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
package com.github.libretube.db
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.github.libretube.api.JsonHelper
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.toLocalDate
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
object Converters {
|
||||
@TypeConverter
|
||||
@ -18,4 +21,10 @@ object Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun stringToPath(string: String?) = string?.let { Paths.get(it) }
|
||||
|
||||
@TypeConverter
|
||||
fun stringListToJson(value: List<String>) = JsonHelper.json.encodeToString(value)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToStringList(value: String) = JsonHelper.json.decodeFromString<List<String>>(value)
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
package com.github.libretube.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.github.libretube.db.obj.SubscriptionGroup
|
||||
|
||||
@Dao()
|
||||
interface SubscriptionGroupsDao {
|
||||
@Query("SELECT * FROM subscriptionGroups")
|
||||
suspend fun getAll(): List<SubscriptionGroup>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun createGroup(subscriptionGroup: SubscriptionGroup)
|
||||
|
||||
@Insert
|
||||
suspend fun insertAll(subscriptionGroups: List<SubscriptionGroup>)
|
||||
|
||||
@Query("DELETE FROM subscriptionGroups WHERE name = :name")
|
||||
suspend fun deleteGroup(name: String)
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.github.libretube.db.obj
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "subscriptionGroups")
|
||||
data class SubscriptionGroup(
|
||||
@PrimaryKey var name: String,
|
||||
val channels: MutableList<String>
|
||||
)
|
@ -52,13 +52,14 @@ object BackupHelper {
|
||||
Database.localSubscriptionDao().insertAll(backupFile.localSubscriptions.orEmpty())
|
||||
Database.customInstanceDao().insertAll(backupFile.customInstances.orEmpty())
|
||||
Database.playlistBookmarkDao().insertAll(backupFile.playlistBookmarks.orEmpty())
|
||||
Database.subscriptionGroupsDao().insertAll(backupFile.channelGroups.orEmpty())
|
||||
|
||||
backupFile.localPlaylists?.forEach {
|
||||
Database.localPlaylistsDao().createPlaylist(it.playlist)
|
||||
val playlistId = Database.localPlaylistsDao().getAll().last().playlist.id
|
||||
it.videos.forEach {
|
||||
it.playlistId = playlistId
|
||||
Database.localPlaylistsDao().addPlaylistVideo(it)
|
||||
it.videos.forEach { playlistItem ->
|
||||
playlistItem.playlistId = playlistId
|
||||
Database.localPlaylistsDao().addPlaylistVideo(playlistItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import com.github.libretube.db.obj.LocalPlaylistWithVideos
|
||||
import com.github.libretube.db.obj.LocalSubscription
|
||||
import com.github.libretube.db.obj.PlaylistBookmark
|
||||
import com.github.libretube.db.obj.SearchHistoryItem
|
||||
import com.github.libretube.db.obj.SubscriptionGroup
|
||||
import com.github.libretube.db.obj.WatchHistoryItem
|
||||
import com.github.libretube.db.obj.WatchPosition
|
||||
import kotlinx.serialization.Serializable
|
||||
@ -18,5 +19,6 @@ data class BackupFile(
|
||||
var customInstances: List<CustomInstance>? = emptyList(),
|
||||
var playlistBookmarks: List<PlaylistBookmark>? = emptyList(),
|
||||
var localPlaylists: List<LocalPlaylistWithVideos>? = emptyList(),
|
||||
var preferences: List<PreferenceItem>? = emptyList()
|
||||
var preferences: List<PreferenceItem>? = emptyList(),
|
||||
var channelGroups: List<SubscriptionGroup>? = emptyList()
|
||||
)
|
||||
|
@ -0,0 +1,42 @@
|
||||
package com.github.libretube.ui.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.api.obj.Subscription
|
||||
import com.github.libretube.databinding.SubscriptionGroupChannelRowBinding
|
||||
import com.github.libretube.db.obj.SubscriptionGroup
|
||||
import com.github.libretube.extensions.toID
|
||||
import com.github.libretube.helpers.ImageHelper
|
||||
import com.github.libretube.ui.viewholders.SubscriptionGroupChannelRowViewHolder
|
||||
|
||||
class SubscriptionGroupChannelsAdapter(
|
||||
private val channels: List<Subscription>,
|
||||
private val group: SubscriptionGroup,
|
||||
private val onGroupChanged: (SubscriptionGroup) -> Unit
|
||||
) : RecyclerView.Adapter<SubscriptionGroupChannelRowViewHolder>() {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): SubscriptionGroupChannelRowViewHolder {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
val binding = SubscriptionGroupChannelRowBinding.inflate(layoutInflater, parent, false)
|
||||
return SubscriptionGroupChannelRowViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount() = channels.size
|
||||
|
||||
override fun onBindViewHolder(holder: SubscriptionGroupChannelRowViewHolder, position: Int) {
|
||||
val channel = channels[position]
|
||||
val channelId = channel.url.toID()
|
||||
holder.binding.apply {
|
||||
subscriptionChannelName.text = channel.name
|
||||
ImageHelper.loadImage(channel.avatar, subscriptionChannelImage)
|
||||
channelIncluded.isChecked = group.channels.contains(channelId)
|
||||
channelIncluded.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) group.channels.add(channelId) else group.channels.remove(channelId)
|
||||
onGroupChanged(group)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package com.github.libretube.ui.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.databinding.SubscriptionGroupRowBinding
|
||||
import com.github.libretube.db.DatabaseHolder
|
||||
import com.github.libretube.db.obj.SubscriptionGroup
|
||||
import com.github.libretube.ui.dialogs.EditChannelGroupDialog
|
||||
import com.github.libretube.ui.viewholders.SubscriptionGroupsViewHolder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class SubscriptionGroupsAdapter(
|
||||
private val groups: MutableList<SubscriptionGroup>,
|
||||
private val parentFragmentManager: FragmentManager,
|
||||
private val onGroupsChanged: (List<SubscriptionGroup>) -> Unit
|
||||
) : RecyclerView.Adapter<SubscriptionGroupsViewHolder>() {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): SubscriptionGroupsViewHolder {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
val binding = SubscriptionGroupRowBinding.inflate(layoutInflater, parent, false)
|
||||
return SubscriptionGroupsViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount() = groups.size
|
||||
|
||||
override fun onBindViewHolder(holder: SubscriptionGroupsViewHolder, position: Int) {
|
||||
val subscriptionGroup = groups[position]
|
||||
holder.binding.apply {
|
||||
groupName.text = subscriptionGroup.name
|
||||
deleteGroup.setOnClickListener {
|
||||
groups.removeAt(position)
|
||||
runBlocking(Dispatchers.IO) {
|
||||
DatabaseHolder.Database.subscriptionGroupsDao().deleteGroup(
|
||||
subscriptionGroup.name
|
||||
)
|
||||
}
|
||||
onGroupsChanged(groups)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, itemCount)
|
||||
}
|
||||
editGroup.setOnClickListener {
|
||||
EditChannelGroupDialog(subscriptionGroup) {
|
||||
groups[position] = it
|
||||
runBlocking(Dispatchers.IO) {
|
||||
// delete the old one as it might have a different name
|
||||
DatabaseHolder.Database.subscriptionGroupsDao().deleteGroup(
|
||||
groupName.text.toString()
|
||||
)
|
||||
DatabaseHolder.Database.subscriptionGroupsDao().createGroup(it)
|
||||
}
|
||||
notifyItemChanged(position)
|
||||
onGroupsChanged(groups)
|
||||
}.show(parentFragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertItem(subscriptionsGroup: SubscriptionGroup) {
|
||||
groups.add(subscriptionsGroup)
|
||||
notifyItemInserted(itemCount - 1)
|
||||
}
|
||||
}
|
@ -51,6 +51,10 @@ class BackupDialog(
|
||||
it.localPlaylists = Database.localPlaylistsDao().getAll()
|
||||
})
|
||||
|
||||
object SubscriptionGroups : BackupOption(R.string.channel_groups, onSelected = {
|
||||
it.channelGroups = Database.subscriptionGroupsDao().getAll()
|
||||
})
|
||||
|
||||
object Preferences : BackupOption(R.string.preferences, onSelected = { file ->
|
||||
file.preferences = PreferenceHelper.settings.all.map { (key, value) ->
|
||||
val jsonValue = when (value) {
|
||||
@ -73,6 +77,7 @@ class BackupDialog(
|
||||
BackupOption.CustomInstances,
|
||||
BackupOption.PlaylistBookmarks,
|
||||
BackupOption.LocalPlaylists,
|
||||
BackupOption.SubscriptionGroups,
|
||||
BackupOption.Preferences
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,47 @@
|
||||
package com.github.libretube.ui.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.databinding.DialogSubscriptionGroupsBinding
|
||||
import com.github.libretube.db.DatabaseHolder
|
||||
import com.github.libretube.db.obj.SubscriptionGroup
|
||||
import com.github.libretube.ui.adapters.SubscriptionGroupsAdapter
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class ChannelGroupsDialog(
|
||||
private val groups: MutableList<SubscriptionGroup>,
|
||||
private val onGroupsChanged: (List<SubscriptionGroup>) -> Unit
|
||||
) : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val binding = DialogSubscriptionGroupsBinding.inflate(layoutInflater)
|
||||
|
||||
binding.groupsRV.layoutManager = LinearLayoutManager(context)
|
||||
val adapter = SubscriptionGroupsAdapter(
|
||||
groups.toMutableList(),
|
||||
parentFragmentManager,
|
||||
onGroupsChanged
|
||||
)
|
||||
binding.groupsRV.adapter = adapter
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.channel_groups)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(R.string.okay, null)
|
||||
.setNeutralButton(R.string.new_group) { _, _ ->
|
||||
EditChannelGroupDialog(SubscriptionGroup("", mutableListOf())) {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
DatabaseHolder.Database.subscriptionGroupsDao().createGroup(it)
|
||||
}
|
||||
groups.add(it)
|
||||
adapter.insertItem(it)
|
||||
onGroupsChanged(groups)
|
||||
}.show(parentFragmentManager, null)
|
||||
}
|
||||
.create()
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package com.github.libretube.ui.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.SubscriptionHelper
|
||||
import com.github.libretube.api.obj.Subscription
|
||||
import com.github.libretube.databinding.DialogEditChannelGroupBinding
|
||||
import com.github.libretube.db.obj.SubscriptionGroup
|
||||
import com.github.libretube.ui.adapters.SubscriptionGroupChannelsAdapter
|
||||
import com.github.libretube.ui.models.SubscriptionsViewModel
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class EditChannelGroupDialog(
|
||||
private var group: SubscriptionGroup,
|
||||
private val onGroupChanged: (SubscriptionGroup) -> Unit
|
||||
) : DialogFragment() {
|
||||
private val subscriptionsModel: SubscriptionsViewModel by activityViewModels()
|
||||
private lateinit var binding: DialogEditChannelGroupBinding
|
||||
private var channels: List<Subscription> = listOf()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
binding = DialogEditChannelGroupBinding.inflate(layoutInflater)
|
||||
binding.groupName.setText(group.name)
|
||||
|
||||
binding.channelsRV.layoutManager = LinearLayoutManager(context)
|
||||
fetchSubscriptions()
|
||||
|
||||
binding.searchInput.addTextChangedListener {
|
||||
showChannels(channels, it?.toString())
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.edit_group)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.okay) { _, _ ->
|
||||
group.name = binding.groupName.text.toString()
|
||||
if (group.name.isBlank()) return@setPositiveButton
|
||||
onGroupChanged(group)
|
||||
}
|
||||
.setView(binding.root)
|
||||
.create()
|
||||
}
|
||||
|
||||
private fun fetchSubscriptions() {
|
||||
subscriptionsModel.subscriptions.value?.let {
|
||||
channels = it
|
||||
showChannels(it, null)
|
||||
return
|
||||
}
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
channels = runCatching {
|
||||
SubscriptionHelper.getSubscriptions()
|
||||
}.getOrNull().orEmpty()
|
||||
withContext(Dispatchers.Main) {
|
||||
showChannels(channels, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showChannels(channels: List<Subscription>, query: String?) {
|
||||
binding.channelsRV.adapter = SubscriptionGroupChannelsAdapter(
|
||||
channels.filter { query == null || it.name.lowercase().contains(query.lowercase()) },
|
||||
group
|
||||
) {
|
||||
group = it
|
||||
}
|
||||
binding.subscriptionsContainer.isVisible = true
|
||||
binding.progress.isVisible = false
|
||||
}
|
||||
}
|
@ -1,14 +1,17 @@
|
||||
package com.github.libretube.ui.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.libretube.R
|
||||
@ -16,19 +19,25 @@ import com.github.libretube.api.obj.StreamItem
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.databinding.FragmentSubscriptionsBinding
|
||||
import com.github.libretube.db.DatabaseHolder
|
||||
import com.github.libretube.db.obj.SubscriptionGroup
|
||||
import com.github.libretube.extensions.toID
|
||||
import com.github.libretube.helpers.PreferenceHelper
|
||||
import com.github.libretube.ui.adapters.LegacySubscriptionAdapter
|
||||
import com.github.libretube.ui.adapters.SubscriptionChannelAdapter
|
||||
import com.github.libretube.ui.adapters.VideosAdapter
|
||||
import com.github.libretube.ui.dialogs.ChannelGroupsDialog
|
||||
import com.github.libretube.ui.models.SubscriptionsViewModel
|
||||
import com.github.libretube.ui.sheets.BaseBottomSheet
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class SubscriptionsFragment : Fragment() {
|
||||
private lateinit var binding: FragmentSubscriptionsBinding
|
||||
private val viewModel: SubscriptionsViewModel by activityViewModels()
|
||||
private var channelGroups: List<SubscriptionGroup> = listOf()
|
||||
private var selectedFilterGroup: Int = 0
|
||||
|
||||
var subscriptionsAdapter: VideosAdapter? = null
|
||||
private var selectedSortOrder = PreferenceHelper.getInt(PreferenceKeys.FEED_SORT_ORDER, 0)
|
||||
@ -82,14 +91,12 @@ class SubscriptionsFragment : Fragment() {
|
||||
}
|
||||
|
||||
viewModel.videoFeed.observe(viewLifecycleOwner) {
|
||||
if (!isShowingFeed()) return@observe
|
||||
if (it == null) return@observe
|
||||
if (!isShowingFeed() || it == null) return@observe
|
||||
showFeed()
|
||||
}
|
||||
|
||||
viewModel.subscriptions.observe(viewLifecycleOwner) {
|
||||
if (isShowingFeed()) return@observe
|
||||
if (it == null) return@observe
|
||||
if (isShowingFeed() || it == null) return@observe
|
||||
showSubscriptions()
|
||||
}
|
||||
|
||||
@ -150,13 +157,59 @@ class SubscriptionsFragment : Fragment() {
|
||||
binding.subRefresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
initChannelGroups()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private suspend fun initChannelGroups() {
|
||||
channelGroups = DatabaseHolder.Database.subscriptionGroupsDao().getAll()
|
||||
|
||||
binding.chipAll.isChecked = true
|
||||
binding.channelGroups.removeAllViews()
|
||||
|
||||
binding.channelGroups.addView(binding.chipAll)
|
||||
channelGroups.forEach { group ->
|
||||
val chip = layoutInflater.inflate(R.layout.filter_chip, null) as Chip
|
||||
chip.apply {
|
||||
id = View.generateViewId()
|
||||
isCheckable = true
|
||||
text = group.name
|
||||
}
|
||||
|
||||
binding.channelGroups.addView(chip)
|
||||
}
|
||||
|
||||
binding.channelGroups.setOnCheckedStateChangeListener { group, checkedIds ->
|
||||
selectedFilterGroup = group.children.indexOfFirst { it.id == checkedIds.first() }
|
||||
showFeed()
|
||||
}
|
||||
|
||||
binding.editGroups.setOnClickListener {
|
||||
ChannelGroupsDialog(channelGroups.toMutableList()) {
|
||||
lifecycleScope.launch { initChannelGroups() }
|
||||
}.show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFeed() {
|
||||
if (viewModel.videoFeed.value == null) return
|
||||
|
||||
binding.subRefresh.isRefreshing = false
|
||||
val feed = viewModel.videoFeed.value!!.filter {
|
||||
val feed = viewModel.videoFeed.value!!
|
||||
.filter { streamItem ->
|
||||
// filter for selected channel groups
|
||||
if (selectedFilterGroup == 0) {
|
||||
true
|
||||
} else {
|
||||
val channelId = streamItem.uploaderUrl.orEmpty().toID()
|
||||
val group = channelGroups.getOrNull(selectedFilterGroup - 1)
|
||||
group?.channels?.contains(channelId) != false
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
// apply the selected filter
|
||||
when (selectedFilter) {
|
||||
0 -> true
|
||||
@ -166,7 +219,11 @@ class SubscriptionsFragment : Fragment() {
|
||||
}
|
||||
}.let { streams ->
|
||||
runBlocking {
|
||||
if (!PreferenceHelper.getBoolean(PreferenceKeys.HIDE_WATCHED_FROM_FEED, false)) {
|
||||
if (!PreferenceHelper.getBoolean(
|
||||
PreferenceKeys.HIDE_WATCHED_FROM_FEED,
|
||||
false
|
||||
)
|
||||
) {
|
||||
streams
|
||||
} else {
|
||||
removeWatchVideosFromFeed(streams)
|
||||
|
@ -0,0 +1,8 @@
|
||||
package com.github.libretube.ui.viewholders
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.databinding.SubscriptionGroupChannelRowBinding
|
||||
|
||||
class SubscriptionGroupChannelRowViewHolder(
|
||||
val binding: SubscriptionGroupChannelRowBinding
|
||||
) : RecyclerView.ViewHolder(binding.root)
|
@ -0,0 +1,8 @@
|
||||
package com.github.libretube.ui.viewholders
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.libretube.databinding.SubscriptionGroupRowBinding
|
||||
|
||||
class SubscriptionGroupsViewHolder(
|
||||
val binding: SubscriptionGroupRowBinding
|
||||
) : RecyclerView.ViewHolder(binding.root)
|
10
app/src/main/res/drawable/ic_edit.xml
Normal file
10
app/src/main/res/drawable/ic_edit.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||
</vector>
|
59
app/src/main/res/layout/dialog_edit_channel_group.xml
Normal file
59
app/src/main/res/layout/dialog_edit_channel_group.xml
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:hint="@string/group_name"
|
||||
app:startIconDrawable="@drawable/ic_edit">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/group_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/subscriptions_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:hint="@string/search_hint"
|
||||
app:startIconDrawable="@drawable/ic_search">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/search_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/channelsRV"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginVertical="200dp" />
|
||||
|
||||
</LinearLayout>
|
12
app/src/main/res/layout/dialog_subscription_groups.xml
Normal file
12
app/src/main/res/layout/dialog_subscription_groups.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/groupsRV"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp" />
|
||||
|
||||
</LinearLayout>
|
5
app/src/main/res/layout/filter_chip.xml
Normal file
5
app/src/main/res/layout/filter_chip.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/Widget.Material3.Chip.Filter.Elevated"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
@ -131,6 +131,47 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="3dp"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/channel_groups"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:checkedChip="@id/chip_all"
|
||||
android:layout_marginStart="5dp"
|
||||
app:selectionRequired="true"
|
||||
app:singleLine="true"
|
||||
app:singleSelection="true">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_all"
|
||||
style="@style/ElevatedFilterChip"
|
||||
android:text="@string/all" />
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/edit_groups"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_edit" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
34
app/src/main/res/layout/subscription_group_channel_row.xml
Normal file
34
app/src/main/res/layout/subscription_group_channel_row.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:paddingHorizontal="20dp" >
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/subscription_channel_image"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="8dp"
|
||||
app:shapeAppearance="@style/CircleImageView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subscription_channel_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="16sp"
|
||||
tools:text="Channel Name" />
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/channel_included"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
35
app/src/main/res/layout/subscription_group_row.xml
Normal file
35
app/src/main/res/layout/subscription_group_row.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="25dp"
|
||||
android:paddingVertical="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/edit_group"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_edit" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/delete_group"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_delete" />
|
||||
|
||||
</LinearLayout>
|
@ -456,6 +456,10 @@
|
||||
<string name="disable_proxy">Disable Piped proxy</string>
|
||||
<string name="disable_proxy_summary">Load videos and images directly from YouTube\'s servers. Only enable the option if you use a VPN anyways! Note that this might not work with content from YT music.</string>
|
||||
<string name="auto_fullscreen_shorts">Auto fullscreen on short videos</string>
|
||||
<string name="channel_groups">Channel groups</string>
|
||||
<string name="new_group">New</string>
|
||||
<string name="group_name">Group name</string>
|
||||
<string name="edit_group">Edit group</string>
|
||||
|
||||
<!-- Notification channel strings -->
|
||||
<string name="download_channel_name">Download Service</string>
|
||||
|
@ -250,4 +250,13 @@
|
||||
|
||||
</style>
|
||||
|
||||
<style name="ElevatedFilterChip" parent="Widget.Material3.Chip.Filter.Elevated">
|
||||
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginStart">5dp</item>
|
||||
<item name="android:layout_marginEnd">5dp</item>
|
||||
|
||||
</style>
|
||||
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user