add frontend support

This commit is contained in:
Bnyro 2022-11-20 15:54:55 +01:00
parent ea1d2765c9
commit 0d65071c79
26 changed files with 635 additions and 236 deletions

View File

@ -0,0 +1,330 @@
{
"formatVersion": 1,
"database": {
"version": 9,
"identityHash": "8c1e428cb526415347639e49f7757f76",
"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": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"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": {
"columnNames": [
"videoId"
],
"autoGenerate": false
},
"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": {
"columnNames": [
"query"
],
"autoGenerate": false
},
"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": {
"columnNames": [
"name"
],
"autoGenerate": false
},
"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": {
"columnNames": [
"channelId"
],
"autoGenerate": false
},
"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": {
"columnNames": [
"playlistId"
],
"autoGenerate": false
},
"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": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"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": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"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, '8c1e428cb526415347639e49f7757f76')"
]
}
}

View File

@ -3,7 +3,6 @@ package com.github.libretube
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import android.util.Log
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.ExistingPeriodicWorkPolicy
@ -13,10 +12,6 @@ import com.github.libretube.constants.BACKGROUND_CHANNEL_ID
import com.github.libretube.constants.DOWNLOAD_CHANNEL_ID
import com.github.libretube.constants.PUSH_CHANNEL_ID
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.LocalPlaylist
import com.github.libretube.db.obj.LocalPlaylistItem
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.query
import com.github.libretube.util.ExceptionHandler
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NotificationHelper
@ -41,8 +36,6 @@ class LibreTubeApp : Application() {
*/
DatabaseHolder().initializeDatabase(this)
runDatabaseTests()
/**
* Bypassing fileUriExposedException, see https://stackoverflow.com/questions/38200282/android-os-fileuriexposedexception-file-storage-emulated-0-test-txt-exposed
*/
@ -107,23 +100,4 @@ class LibreTubeApp : Application() {
)
)
}
private fun runDatabaseTests() {
awaitQuery {
val playlist = LocalPlaylist(
name = "TEstlist",
thumbnailUrl = "thumb"
)
DatabaseHolder.Database.localPlaylistsDao().createPlaylist(playlist)
val playlistId = DatabaseHolder.Database.localPlaylistsDao().getAll().first().playlist.id
val video = LocalPlaylistItem(
videoId = "video",
playlistId = playlistId,
title = "awesomePlaylistTitle"
)
DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(video)
val lists = DatabaseHolder.Database.localPlaylistsDao().getAll()
Log.e("lists", lists.toString())
}
}
}

View File

@ -0,0 +1,127 @@
package com.github.libretube.api
import android.content.Context
import android.util.Log
import com.github.libretube.R
import com.github.libretube.api.obj.Playlist
import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.api.obj.Playlists
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.LocalPlaylist
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.toLocalPlaylistItem
import com.github.libretube.extensions.toStreamItem
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.util.PreferenceHelper
import retrofit2.HttpException
import java.io.IOException
object PlaylistsHelper {
val token get() = PreferenceHelper.getToken()
suspend fun getPlaylists(): List<Playlists> {
if (token != "") return RetrofitInstance.authApi.getUserPlaylists(token)
val localPlaylists = awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().getAll()
}
val playlists = mutableListOf<Playlists>()
localPlaylists.forEach {
playlists.add(
Playlists(
id = it.playlist.id.toString(),
name = it.playlist.name,
thumbnail = it.playlist.thumbnailUrl,
videos = it.videos.size.toLong()
)
)
}
return playlists
}
suspend fun getPlaylist(playlistType: PlaylistType, playlistId: String): Playlist {
// load locally stored playlists with the auth api
return when (playlistType) {
PlaylistType.OWNED -> RetrofitInstance.authApi.getPlaylist(playlistId)
PlaylistType.PUBLIC -> RetrofitInstance.api.getPlaylist(playlistId)
PlaylistType.LOCAL -> {
val relation = awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().getAll()
}.first { it.playlist.id.toString() == playlistId }
return Playlist(
name = relation.playlist.name,
thumbnailUrl = relation.playlist.thumbnailUrl,
videos = relation.videos.size,
relatedStreams = relation.videos.map { it.toStreamItem() }
)
}
}
}
suspend fun createPlaylist(playlistName: String, appContext: Context, onSuccess: () -> Unit) {
if (token == "") {
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().createPlaylist(
LocalPlaylist(
name = playlistName,
thumbnailUrl = ""
)
)
}
onSuccess.invoke()
return
}
val response = try {
RetrofitInstance.authApi.createPlaylist(
token,
Playlists(name = playlistName)
)
} catch (e: IOException) {
appContext.toastFromMainThread(R.string.unknown_error)
return
} catch (e: HttpException) {
Log.e(TAG(), e.toString())
appContext.toastFromMainThread(R.string.server_error)
return
}
if (response.playlistId != null) {
appContext.toastFromMainThread(R.string.playlistCreated)
onSuccess.invoke()
} else {
appContext.toastFromMainThread(R.string.unknown_error)
}
}
suspend fun addToPlaylist(playlistId: String, videoId: String): Boolean {
if (token == "") {
val localPlaylistItem = RetrofitInstance.api.getStreams(videoId).toLocalPlaylistItem(playlistId, videoId)
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem)
val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }.playlist
if (localPlaylist.thumbnailUrl == "") {
localPlaylistItem.thumbnailUrl?.let {
localPlaylist.thumbnailUrl = it
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(localPlaylist)
}
}
}
return true
}
return RetrofitInstance.authApi.addToPlaylist(
token,
PlaylistId(playlistId, videoId)
).message == "ok"
}
fun getType(): PlaylistType {
return if (PreferenceHelper.getToken() != "") {
PlaylistType.PUBLIC
} else {
PlaylistType.LOCAL
}
}
}

View File

@ -9,4 +9,5 @@ object IntentData {
const val position = "position"
const val fileName = "fileName"
const val openQueueOnce = "openQueue"
const val playlistType = "playlistType"
}

View File

@ -4,16 +4,16 @@ import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import com.github.libretube.db.dao.CustomInstanceDao
import com.github.libretube.db.dao.LocalSubscriptionDao
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.WatchHistoryDao
import com.github.libretube.db.dao.WatchPositionDao
import com.github.libretube.db.obj.CustomInstance
import com.github.libretube.db.obj.LocalSubscription
import com.github.libretube.db.obj.LocalPlaylist
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.WatchHistoryItem

View File

@ -25,9 +25,15 @@ interface LocalPlaylistsDao {
@Delete
fun deletePlaylist(playlist: LocalPlaylist)
@Query("DELETE FROM localPlaylist WHERE id = :playlistId")
fun deletePlaylistById(playlistId: String)
@Insert
fun addPlaylistVideo(playlistVideo: LocalPlaylistItem)
@Delete
fun removePlaylistVideo(playlistVideo: LocalPlaylistItem)
@Query("DELETE FROM localPlaylistItem WHERE playlistId = :playlistId")
fun deletePlaylistItemsByPlaylistId(playlistId: String)
}

View File

@ -8,5 +8,5 @@ data class LocalPlaylist(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val thumbnailUrl: String
var thumbnailUrl: String
)

View File

@ -0,0 +1,7 @@
package com.github.libretube.enums
enum class PlaylistType {
LOCAL,
OWNED,
PUBLIC
}

View File

@ -0,0 +1,10 @@
package com.github.libretube.ui.extensions
import android.os.Build
import android.os.Bundle
import java.io.Serializable
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
}

View File

@ -0,0 +1,18 @@
package com.github.libretube.extensions
import com.github.libretube.api.obj.Streams
import com.github.libretube.db.obj.LocalPlaylistItem
fun Streams.toLocalPlaylistItem(playlistId: String, videoId: String): LocalPlaylistItem {
return LocalPlaylistItem(
playlistId = playlistId.toInt(),
videoId = videoId,
title = title,
thumbnailUrl = thumbnailUrl,
uploader = uploader,
uploaderUrl = uploaderUrl,
uploaderAvatar = uploaderAvatar,
uploadDate = uploadDate,
duration = duration
)
}

View File

@ -2,6 +2,7 @@ package com.github.libretube.extensions
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Streams
import com.github.libretube.db.obj.LocalPlaylistItem
fun Streams.toStreamItem(videoId: String): StreamItem {
return StreamItem(
@ -19,3 +20,17 @@ fun Streams.toStreamItem(videoId: String): StreamItem {
shortDescription = description
)
}
fun LocalPlaylistItem.toStreamItem(): StreamItem {
return StreamItem(
url = videoId,
title = title,
thumbnail = thumbnailUrl,
uploaderName = uploader,
uploaderUrl = uploaderUrl,
uploaderAvatar = uploaderAvatar,
uploadedDate = uploadDate,
uploaded = null,
duration = duration
)
}

View File

@ -5,16 +5,6 @@ import android.os.Handler
import android.os.Looper
import android.widget.Toast
fun Context.toastFromMainThread(stringId: Int) {
Handler(Looper.getMainLooper()).post {
Toast.makeText(
this,
stringId,
Toast.LENGTH_SHORT
).show()
}
}
fun Context.toastFromMainThread(text: String) {
Handler(Looper.getMainLooper()).post {
Toast.makeText(
@ -24,3 +14,7 @@ fun Context.toastFromMainThread(text: String) {
).show()
}
}
fun Context.toastFromMainThread(stringId: Int) {
toastFromMainThread(getString(stringId))
}

View File

@ -11,6 +11,7 @@ import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.databinding.PlaylistRowBinding
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toID
import com.github.libretube.ui.base.BaseActivity
@ -30,7 +31,7 @@ import java.io.IOException
class PlaylistAdapter(
private val videoFeed: MutableList<StreamItem>,
private val playlistId: String,
private val isOwner: Boolean
private val playlistType: PlaylistType
) : RecyclerView.Adapter<PlaylistViewHolder>() {
override fun getItemCount(): Int {
@ -70,7 +71,7 @@ class PlaylistAdapter(
true
}
if (isOwner) {
if (playlistType != PlaylistType.PUBLIC) {
deletePlaylist.visibility = View.VISIBLE
deletePlaylist.setOnClickListener {
removeFromPlaylist(root.context, position)

View File

@ -6,6 +6,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.PlaylistBookmarkRowBinding
import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.toDp
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.ui.viewholders.PlaylistBookmarkViewHolder
@ -39,14 +40,14 @@ class PlaylistBookmarkAdapter(
uploaderName.text = bookmark.uploader
root.setOnClickListener {
NavigationHelper.navigatePlaylist(root.context, bookmark.playlistId, false)
NavigationHelper.navigatePlaylist(root.context, bookmark.playlistId, PlaylistType.PUBLIC)
}
root.setOnLongClickListener {
PlaylistOptionsBottomSheet(
playlistId = bookmark.playlistId,
playlistName = bookmark.playlistName ?: "",
isOwner = false
playlistType = PlaylistType.PUBLIC
).show(
(root.context as AppCompatActivity).supportFragmentManager
)

View File

@ -6,6 +6,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.api.obj.Playlists
import com.github.libretube.databinding.PlaylistsRowBinding
import com.github.libretube.enums.PlaylistType
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.dialogs.DeletePlaylistDialog
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
@ -14,7 +15,8 @@ import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
class PlaylistsAdapter(
private val playlists: MutableList<Playlists>
private val playlists: MutableList<Playlists>,
private val playlistType: PlaylistType
) : RecyclerView.Adapter<PlaylistsViewHolder>() {
override fun getItemCount(): Int {
@ -48,7 +50,7 @@ class PlaylistsAdapter(
videoCount.text = playlist.videos.toString()
deletePlaylist.setOnClickListener {
DeletePlaylistDialog(playlist.id!!) {
DeletePlaylistDialog(playlist.id!!, playlistType) {
playlists.removeAt(position)
(root.context as BaseActivity).runOnUiThread {
notifyItemRemoved(position)
@ -60,14 +62,14 @@ class PlaylistsAdapter(
)
}
root.setOnClickListener {
NavigationHelper.navigatePlaylist(root.context, playlist.id, true)
NavigationHelper.navigatePlaylist(root.context, playlist.id, playlistType)
}
root.setOnLongClickListener {
val playlistOptionsDialog = PlaylistOptionsBottomSheet(
playlistId = playlist.id!!,
playlistName = playlist.name!!,
isOwner = true
playlistType = playlistType
)
playlistOptionsDialog.show(
(root.context as BaseActivity).supportFragmentManager,

View File

@ -10,6 +10,7 @@ import com.github.libretube.api.obj.ContentItem
import com.github.libretube.databinding.ChannelRowBinding
import com.github.libretube.databinding.PlaylistsRowBinding
import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toID
import com.github.libretube.ui.base.BaseActivity
@ -140,13 +141,13 @@ class SearchAdapter(
playlistTitle.text = item.name
playlistDescription.text = item.uploaderName
root.setOnClickListener {
NavigationHelper.navigatePlaylist(root.context, item.url, false)
NavigationHelper.navigatePlaylist(root.context, item.url, PlaylistType.PUBLIC)
}
deletePlaylist.visibility = View.GONE
root.setOnLongClickListener {
val playlistId = item.url!!.toID()
val playlistName = item.name!!
PlaylistOptionsBottomSheet(playlistId, playlistName, false)
PlaylistOptionsBottomSheet(playlistId, playlistName, PlaylistType.PUBLIC)
.show((root.context as BaseActivity).supportFragmentManager, PlaylistOptionsBottomSheet::class.java.name)
true
}

View File

@ -10,28 +10,23 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.DialogAddtoplaylistBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.ui.models.PlaylistViewModel
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ThemeHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
class AddToPlaylistDialog : DialogFragment() {
private lateinit var binding: DialogAddtoplaylistBinding
private val viewModel: PlaylistViewModel by activityViewModels()
private lateinit var videoId: String
private lateinit var token: String
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
videoId = arguments?.getString(IntentData.videoId)!!
@ -46,9 +41,7 @@ class AddToPlaylistDialog : DialogFragment() {
}
}
token = PreferenceHelper.getToken()
if (token != "") fetchPlaylists()
fetchPlaylists()
return MaterialAlertDialogBuilder(requireContext())
.setView(binding.root)
@ -58,16 +51,11 @@ class AddToPlaylistDialog : DialogFragment() {
private fun fetchPlaylists() {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.authApi.getUserPlaylists(token)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
PlaylistsHelper.getPlaylists()
} catch (e: Exception) {
Log.e(TAG(), e.toString())
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
}
if (response.isNotEmpty()) {
val names = response.map { it.name }
@ -81,8 +69,7 @@ class AddToPlaylistDialog : DialogFragment() {
var selectionIndex = 0
response.forEachIndexed { index, playlist ->
if (playlist.id == viewModel.lastSelectedPlaylistId) {
selectionIndex =
index
selectionIndex = index
}
}
binding.playlistsSpinner.setSelection(selectionIndex)
@ -102,23 +89,15 @@ class AddToPlaylistDialog : DialogFragment() {
private fun addToPlaylist(playlistId: String) {
val appContext = context?.applicationContext ?: return
CoroutineScope(Dispatchers.IO).launch {
val response = try {
RetrofitInstance.authApi.addToPlaylist(
token,
PlaylistId(playlistId, videoId)
)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
val success = try {
PlaylistsHelper.addToPlaylist(playlistId, videoId)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
appContext.toastFromMainThread(R.string.unknown_error)
return@launch
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
appContext.toastFromMainThread(R.string.server_error)
return@launch
}
appContext.toastFromMainThread(
if (response.message == "ok") R.string.added_to_playlist else R.string.fail
if (success) R.string.added_to_playlist else R.string.fail
)
}
}

View File

@ -2,25 +2,18 @@ package com.github.libretube.ui.dialogs
import android.app.Dialog
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.Playlists
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.databinding.DialogCreatePlaylistBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ThemeHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException
import java.io.IOException
class CreatePlaylistDialog(
private val onSuccess: () -> Unit = {}
) : DialogFragment() {
private var token: String = ""
private lateinit var binding: DialogCreatePlaylistBinding
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -32,14 +25,17 @@ class CreatePlaylistDialog(
dismiss()
}
token = PreferenceHelper.getToken()
binding.createNewPlaylist.setOnClickListener {
// avoid creating the same playlist multiple times by spamming the button
binding.createNewPlaylist.setOnClickListener(null)
val listName = binding.playlistName.text.toString()
if (listName != "") {
createPlaylist(listName)
lifecycleScope.launchWhenCreated {
PlaylistsHelper.createPlaylist(listName, requireContext().applicationContext) {
onSuccess.invoke()
dismiss()
}
}
} else {
Toast.makeText(context, R.string.emptyPlaylistName, Toast.LENGTH_LONG).show()
}
@ -49,37 +45,4 @@ class CreatePlaylistDialog(
.setView(binding.root)
.show()
}
private fun createPlaylist(name: String) {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.authApi.createPlaylist(
token,
Playlists(name = name)
)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response $e")
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
}
if (response.playlistId != null) {
Toast.makeText(context, R.string.playlistCreated, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, getString(R.string.unknown_error), Toast.LENGTH_SHORT)
.show()
}
// refresh the playlists in the library
try {
onSuccess.invoke()
} catch (e: Exception) {
Log.e(TAG(), e.toString())
}
dismiss()
}
}
}

View File

@ -7,7 +7,10 @@ import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.util.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
@ -16,6 +19,7 @@ import kotlinx.coroutines.launch
class DeletePlaylistDialog(
private val playlistId: String,
private val playlistType: PlaylistType,
private val onSuccess: () -> Unit = {}
) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -31,6 +35,14 @@ class DeletePlaylistDialog(
}
private fun deletePlaylist() {
if (playlistType == PlaylistType.LOCAL) {
awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistById(playlistId)
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByPlaylistId(playlistId)
}
return
}
CoroutineScope(Dispatchers.IO).launch {
val response = try {
RetrofitInstance.authApi.deletePlaylist(

View File

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.databinding.FragmentHomeBinding
@ -101,13 +102,12 @@ class HomeFragment : BaseFragment() {
}
runOrError {
if (token == "") return@runOrError
val playlists = RetrofitInstance.authApi.getUserPlaylists(token).withMaxSize(20)
val playlists = PlaylistsHelper.getPlaylists().withMaxSize(20)
if (playlists.isEmpty()) return@runOrError
runOnUiThread {
makeVisible(binding.playlistsRV, binding.playlistsTV)
binding.playlistsRV.layoutManager = LinearLayoutManager(context)
binding.playlistsRV.adapter = PlaylistsAdapter(playlists.toMutableList())
binding.playlistsRV.adapter = PlaylistsAdapter(playlists.toMutableList(), PlaylistsHelper.getType())
binding.playlistsRV.adapter?.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {

View File

@ -12,7 +12,7 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentLibraryBinding
import com.github.libretube.extensions.TAG
@ -22,8 +22,6 @@ import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.dialogs.CreatePlaylistDialog
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.util.PreferenceHelper
import retrofit2.HttpException
import java.io.IOException
class LibraryFragment : BaseFragment() {
@ -70,26 +68,16 @@ class LibraryFragment : BaseFragment() {
findNavController().navigate(R.id.downloadsFragment)
}
if (token != "") {
binding.boogh.setImageResource(R.drawable.ic_list)
binding.textLike.text = getString(R.string.emptyList)
fetchPlaylists()
binding.loginOrRegister.visibility = View.GONE
binding.playlistRefresh.isEnabled = true
binding.playlistRefresh.setOnRefreshListener {
fetchPlaylists()
binding.playlistRefresh.isEnabled = true
binding.playlistRefresh.setOnRefreshListener {
}
binding.createPlaylist.setOnClickListener {
CreatePlaylistDialog {
fetchPlaylists()
}
binding.createPlaylist.setOnClickListener {
val newFragment = CreatePlaylistDialog {
fetchPlaylists()
}
newFragment.show(childFragmentManager, CreatePlaylistDialog::class.java.name)
}
} else {
binding.playlistRefresh.isEnabled = false
binding.createPlaylist.visibility = View.GONE
}.show(childFragmentManager, CreatePlaylistDialog::class.java.name)
}
}
@ -101,26 +89,19 @@ class LibraryFragment : BaseFragment() {
binding.createPlaylist.layoutParams = layoutParams
}
fun fetchPlaylists() {
private fun fetchPlaylists() {
binding.playlistRefresh.isRefreshing = true
lifecycleScope.launchWhenCreated {
var playlists = try {
RetrofitInstance.authApi.getUserPlaylists(token)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
PlaylistsHelper.getPlaylists()
} catch (e: Exception) {
Log.e(TAG(), e.toString())
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
} finally {
binding.playlistRefresh.isRefreshing = false
}
if (playlists.isNotEmpty()) {
binding.loginOrRegister.visibility = View.GONE
playlists = when (
PreferenceHelper.getString(
PreferenceKeys.PLAYLISTS_ORDER,
@ -135,16 +116,15 @@ class LibraryFragment : BaseFragment() {
}
val playlistsAdapter = PlaylistsAdapter(
playlists.toMutableList()
playlists.toMutableList(),
PlaylistsHelper.getType()
)
// listen for playlists to become deleted
playlistsAdapter.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onChanged() {
if (playlistsAdapter.itemCount == 0) {
binding.loginOrRegister.visibility = View.VISIBLE
}
binding.nothingHere.visibility = if (playlistsAdapter.itemCount == 0) View.VISIBLE else View.GONE
super.onChanged()
}
})
@ -152,7 +132,7 @@ class LibraryFragment : BaseFragment() {
binding.playlistRecView.adapter = playlistsAdapter
} else {
runOnUiThread {
binding.loginOrRegister.visibility = View.VISIBLE
binding.nothingHere.visibility = View.VISIBLE
}
}
}

View File

@ -11,17 +11,20 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.FragmentPlaylistBinding
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.PlaylistBookmark
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.query
import com.github.libretube.extensions.toID
import com.github.libretube.ui.adapters.PlaylistAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.extensions.serializable
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NavigationHelper
@ -34,7 +37,7 @@ class PlaylistFragment : BaseFragment() {
private var playlistId: String? = null
private var playlistName: String? = null
private var isOwner: Boolean = false
private var playlistType: PlaylistType = PlaylistType.PUBLIC
private var nextPage: String? = null
private var playlistAdapter: PlaylistAdapter? = null
private var isLoading = true
@ -44,7 +47,7 @@ class PlaylistFragment : BaseFragment() {
super.onCreate(savedInstanceState)
arguments?.let {
playlistId = it.getString(IntentData.playlistId)
isOwner = it.getBoolean("isOwner")
playlistType = it.serializable(IntentData.playlistType)!!
}
}
@ -84,12 +87,7 @@ class PlaylistFragment : BaseFragment() {
binding.playlistScrollview.visibility = View.GONE
lifecycleScope.launchWhenCreated {
val response = try {
// load locally stored playlists with the auth api
if (isOwner) {
RetrofitInstance.authApi.getPlaylist(playlistId!!)
} else {
RetrofitInstance.api.getPlaylist(playlistId!!)
}
PlaylistsHelper.getPlaylist(playlistType, playlistId!!)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
@ -116,7 +114,7 @@ class PlaylistFragment : BaseFragment() {
// show playlist options
binding.optionsMenu.setOnClickListener {
PlaylistOptionsBottomSheet(playlistId!!, playlistName ?: "", isOwner).show(
PlaylistOptionsBottomSheet(playlistId!!, playlistName ?: "", playlistType).show(
childFragmentManager,
PlaylistOptionsBottomSheet::class.java.name
)
@ -131,7 +129,7 @@ class PlaylistFragment : BaseFragment() {
)
}
if (isOwner) binding.bookmark.visibility = View.GONE
if (playlistType != PlaylistType.PUBLIC) binding.bookmark.visibility = View.GONE
binding.bookmark.setOnClickListener {
isBookmarked = !isBookmarked
@ -157,7 +155,7 @@ class PlaylistFragment : BaseFragment() {
playlistAdapter = PlaylistAdapter(
response.relatedStreams.orEmpty().toMutableList(),
playlistId!!,
isOwner
playlistType
)
// listen for playlist items to become deleted
@ -189,7 +187,7 @@ class PlaylistFragment : BaseFragment() {
/**
* listener for swiping to the left or right
*/
if (isOwner) {
if (playlistType != PlaylistType.PUBLIC) {
val itemTouchCallback = object : ItemTouchHelper.SimpleCallback(
0,
ItemTouchHelper.LEFT
@ -219,34 +217,27 @@ class PlaylistFragment : BaseFragment() {
}
private fun fetchNextPage() {
fun run() {
lifecycleScope.launchWhenCreated {
val response = try {
// load locally stored playlists with the auth api
if (isOwner) {
RetrofitInstance.authApi.getPlaylistNextPage(
playlistId!!,
nextPage!!
)
} else {
RetrofitInstance.api.getPlaylistNextPage(
playlistId!!,
nextPage!!
)
}
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response," + e.response())
return@launchWhenCreated
lifecycleScope.launchWhenCreated {
val response = try {
// load locally stored playlists with the auth api
if (playlistType == PlaylistType.OWNED) {
RetrofitInstance.authApi.getPlaylistNextPage(
playlistId!!,
nextPage!!
)
} else {
RetrofitInstance.api.getPlaylistNextPage(
playlistId!!,
nextPage!!
)
}
nextPage = response.nextpage
playlistAdapter?.updateItems(response.relatedStreams!!)
isLoading = false
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return@launchWhenCreated
}
nextPage = response.nextpage
playlistAdapter?.updateItems(response.relatedStreams!!)
isLoading = false
}
run()
}
}

View File

@ -7,6 +7,7 @@ import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.databinding.DialogTextPreferenceBinding
import com.github.libretube.enums.PlaylistType
import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toastFromMainThread
@ -25,8 +26,8 @@ import java.io.IOException
class PlaylistOptionsBottomSheet(
private val playlistId: String,
private val playlistName: String,
private val isOwner: Boolean
playlistName: String,
private val playlistType: PlaylistType
) : BaseBottomSheet() {
private val shareData = ShareData(currentPlaylist = playlistName)
override fun onCreate(savedInstanceState: Bundle?) {
@ -37,7 +38,7 @@ class PlaylistOptionsBottomSheet(
context?.getString(R.string.share)!!
)
if (isOwner) {
if (playlistType != PlaylistType.PUBLIC) {
optionsList = optionsList +
context?.getString(R.string.renamePlaylist)!! +
context?.getString(R.string.deletePlaylist)!! -
@ -50,7 +51,7 @@ class PlaylistOptionsBottomSheet(
context?.getString(R.string.playOnBackground) -> {
runBlocking {
val playlist =
if (isOwner) {
if (playlistType == PlaylistType.OWNED) {
RetrofitInstance.authApi.getPlaylist(playlistId)
} else {
RetrofitInstance.api.getPlaylist(playlistId)
@ -82,7 +83,7 @@ class PlaylistOptionsBottomSheet(
shareDialog.show(parentFragmentManager, ShareDialog::class.java.name)
}
context?.getString(R.string.deletePlaylist) -> {
DeletePlaylistDialog(playlistId)
DeletePlaylistDialog(playlistId, playlistType)
.show(parentFragmentManager, null)
}
context?.getString(R.string.renamePlaylist) -> {

View File

@ -1,7 +1,7 @@
package com.github.libretube.ui.sheets
import android.os.Bundle
import android.widget.Toast
import androidx.core.os.bundleOf
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.IntentData
@ -13,7 +13,6 @@ import com.github.libretube.ui.dialogs.DownloadDialog
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.util.BackgroundHelper
import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.PreferenceHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -37,14 +36,6 @@ class VideoOptionsBottomSheet(
context?.getString(R.string.share)!!
)
// remove the add to playlist option if not logged in
if (PreferenceHelper.getToken() == "") {
optionsList.remove(
context?.getString(R.string.addToPlaylist)
)
}
/**
* Check whether the player is running and add queue options
*/
@ -61,19 +52,12 @@ class VideoOptionsBottomSheet(
}
// Add Video to Playlist Dialog
context?.getString(R.string.addToPlaylist) -> {
val token = PreferenceHelper.getToken()
if (token != "") {
val newFragment = AddToPlaylistDialog()
val bundle = Bundle()
bundle.putString(IntentData.videoId, videoId)
newFragment.arguments = bundle
newFragment.show(
parentFragmentManager,
AddToPlaylistDialog::class.java.name
)
} else {
Toast.makeText(context, R.string.login_first, Toast.LENGTH_SHORT).show()
}
AddToPlaylistDialog().apply {
arguments = bundleOf(IntentData.videoId to videoId)
}.show(
parentFragmentManager,
AddToPlaylistDialog::class.java.name
)
}
context?.getString(R.string.download) -> {
val downloadDialog = DownloadDialog(videoId)

View File

@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import com.github.libretube.R
import com.github.libretube.constants.IntentData
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.toID
import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.fragments.PlayerFragment
@ -59,14 +60,14 @@ object NavigationHelper {
fun navigatePlaylist(
context: Context,
playlistId: String?,
isOwner: Boolean
playlistType: PlaylistType
) {
if (playlistId == null) return
val activity = context as MainActivity
val bundle = Bundle()
bundle.putString(IntentData.playlistId, playlistId)
bundle.putBoolean("isOwner", isOwner)
bundle.putSerializable(IntentData.playlistType, playlistType)
activity.navController.navigate(R.id.playlistFragment, bundle)
}

View File

@ -7,9 +7,10 @@
tools:context=".ui.fragments.LibraryFragment">
<RelativeLayout
android:id="@+id/loginOrRegister"
android:id="@+id/nothing_here"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:visibility="gone">
<ImageView
android:id="@+id/boogh"
@ -17,7 +18,7 @@
android:layout_height="100dp"
android:layout_centerInParent="true"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_login" />
android:src="@drawable/ic_list" />
<TextView
android:id="@+id/text_like"
@ -27,7 +28,7 @@
android:layout_centerHorizontal="true"
android:layout_marginHorizontal="10dp"
android:gravity="center"
android:text="@string/please_login"
android:text="@string/emptyList"
android:textSize="20sp"
android:textStyle="bold" />
</RelativeLayout>