Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscription groups #3431

Merged
merged 4 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
496 changes: 496 additions & 0 deletions app/schemas/com.github.libretube.db.AppDatabase/11.json

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions app/src/main/java/com/github/libretube/db/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -84,4 +88,9 @@ abstract class AppDatabase : RoomDatabase() {
* Downloads
*/
abstract fun downloadDao(): DownloadDao

/**
* Subscription groups
*/
abstract fun subscriptionGroupsDao(): SubscriptionGroupsDao
}
9 changes: 9 additions & 0 deletions app/src/main/java/com/github/libretube/db/Converters.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 12 additions & 0 deletions app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt
Original file line number Diff line number Diff line change
@@ -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>
)
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/com/github/libretube/obj/BackupFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
)
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -73,6 +77,7 @@ class BackupDialog(
BackupOption.CustomInstances,
BackupOption.PlaylistBookmarks,
BackupOption.LocalPlaylists,
BackupOption.SubscriptionGroups,
BackupOption.Preferences
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading