add watchlist to home fragment

This commit is contained in:
Jannik 2022-01-03 14:10:41 +01:00
parent 450fd259c6
commit 9062474180
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
8 changed files with 144 additions and 200 deletions

View File

@ -211,6 +211,30 @@ object Crunchyroll {
} ?: NoneSearchResult
}
/**
* Get a collection of series objects.
* Note: episode objects are currently not supported
*
* @param objects The object IDs as list of Strings
* @return A Collection of Panels
*/
suspend fun objects(objects: List<String>): Collection {
val episodesEndpoint = "/cms/v2/DE/M3/crunchyroll/objects/${objects.joinToString(",")}"
val parameters = listOf(
"locale" to locale,
"Signature" to signature,
"Policy" to policy,
"Key-Pair-Id" to keyPairID
)
val result = request(episodesEndpoint, parameters)
println(result.component1()?.obj()?.toString())
return result.component1()?.obj()?.let {
json.decodeFromString(it.toString())
} ?: NoneCollection
}
/**
* series id == crunchyroll id?
*/
@ -273,7 +297,7 @@ object Crunchyroll {
}
/**
* Additional media functions: watchlist, playhead
* Additional media functions: watchlist (series), playhead
*/
/**
@ -283,10 +307,10 @@ object Crunchyroll {
* @return Boolean: ture if it was found, else false
*/
suspend fun isWatchlist(seriesId: String): Boolean {
val watchlistEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
val watchlistSeriesEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
val parameters = listOf("locale" to locale)
val result = request(watchlistEndpoint, parameters)
val result = request(watchlistSeriesEndpoint, parameters)
// if needed implement parsing
return result.component1()?.obj()?.has(seriesId) ?: false
@ -298,14 +322,14 @@ object Crunchyroll {
* @param seriesId The crunchyroll series id of the media to check
*/
suspend fun postWatchlist(seriesId: String) {
val watchlistEndpoint = "/content/v1/watchlist/$accountID"
val watchlistPostEndpoint = "/content/v1/watchlist/$accountID"
val parameters = listOf("locale" to locale)
val json = buildJsonObject {
put("content_id", seriesId)
}
requestPost(watchlistEndpoint, parameters, json.toString())
requestPost(watchlistPostEndpoint, parameters, json.toString())
}
/**
@ -314,10 +338,10 @@ object Crunchyroll {
* @param seriesId The crunchyroll series id of the media to check
*/
suspend fun deleteWatchlist(seriesId: String) {
val watchlistEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
val watchlistDeleteEndpoint = "/content/v1/watchlist/$accountID/$seriesId"
val parameters = listOf("locale" to locale)
requestDelete(watchlistEndpoint, parameters)
requestDelete(watchlistDeleteEndpoint, parameters)
}
/**
@ -327,4 +351,21 @@ object Crunchyroll {
// implement
}
/**
* Listing functions: watchlist (list), up_next_account
*/
suspend fun watchlist(n: Int = 20): Watchlist {
val watchlistEndpoint = "/content/v1/$accountID/watchlist"
val parameters = listOf("locale" to locale, "n" to n)
val watchlistResult = request(watchlistEndpoint, parameters)
val list: ContinueWatchingList = watchlistResult.component1()?.obj()?.let {
json.decodeFromString(it.toString())
} ?: NoneContinueWatchingList
val objects = list.items.map{ it.panel.episodeMetadata.seriesId }
return objects(objects)
}
}

View File

@ -15,8 +15,18 @@ enum class SortBy(val str: String) {
}
/**
* Search data type
* search, browse, watchlist data types (all collections)
*/
@Serializable
data class Collection(
@SerialName("total") val total: Int,
@SerialName("items") val items: List<Item>
)
typealias SearchCollection = Collection
typealias BrowseResult = Collection
typealias Watchlist = Collection
@Serializable
data class SearchResult(
@SerialName("total") val total: Int,
@ -24,17 +34,21 @@ data class SearchResult(
)
@Serializable
data class SearchCollection(
@SerialName("type") val type: String,
@SerialName("items") val items: List<Item>
data class ContinueWatchingList(
@SerialName("total") val total: Int,
@SerialName("items") val items: List<ContinueWatchingItem>
)
val NoneSearchResult = SearchResult(0, emptyList())
@Serializable
data class BrowseResult(val total: Int, val items: List<Item>)
data class ContinueWatchingItem(
@SerialName("panel") val panel: EpisodePanel,
@SerialName("new") val new: Boolean,
@SerialName("new_content") val newContent: Boolean,
@SerialName("is_favorite") val isFavorite: Boolean,
@SerialName("never_watched") val neverWatched: Boolean,
@SerialName("completion_status") val completionStatus: Boolean,
@SerialName("playhead") val playhead: Int,
)
// the data class Item is used in browse and search
// TODO rename to MediaPanel
@ -49,8 +63,17 @@ data class Item(
// TODO metadata etc.
)
val NoneItem = Item("", "", "", "", "", Images(listOf(), listOf()))
val NoneBrowseResult = BrowseResult(0, listOf())
// EpisodePanel is used in ContinueWatchingItem
@Serializable
data class EpisodePanel(
@SerialName("id") val id: String,
@SerialName("title") val title: String,
@SerialName("type") val type: String,
@SerialName("channel_id") val channelId: String,
@SerialName("description") val description: String,
@SerialName("images") val images: Thumbnail,
@SerialName("episode_metadata") val episodeMetadata: EpisodeMetadata,
)
@Serializable
data class Images(val poster_tall: List<List<Poster>>, val poster_wide: List<List<Poster>>)
@ -59,6 +82,18 @@ data class Images(val poster_tall: List<List<Poster>>, val poster_wide: List<Lis
@Serializable
data class Poster(val height: Int, val width: Int, val source: String, val type: String)
@Serializable
data class EpisodeMetadata(
@SerialName("series_id") val seriesId: String,
@SerialName("series_title") val seriesTitle: String,
)
val NoneItem = Item("", "", "", "", "", Images(emptyList(), emptyList()))
val NoneCollection = Collection(0, emptyList())
val NoneSearchResult = SearchResult(0, emptyList())
val NoneBrowseResult = BrowseResult(0, emptyList())
val NoneContinueWatchingList = ContinueWatchingList(0, emptyList())
/**
* Series data type
*/

View File

@ -44,8 +44,6 @@ import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
import org.mosad.teapod.ui.activity.player.PlayerActivity
import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.MetaDBController
import org.mosad.teapod.util.StorageController
import kotlin.system.measureTimeMillis
class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
@ -139,7 +137,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
// load all saved stuff here
Preferences.load(this)
EncryptedPreferences.readCredentials(this)
StorageController.load(this)
// show onboarding TODO rework
if (EncryptedPreferences.password.isEmpty()) {

View File

@ -22,7 +22,6 @@ import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.activity.main.MainActivity
import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.util.DataTypes.Theme
import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.showFragment
class AccountFragment : Fragment() {
@ -32,7 +31,7 @@ class AccountFragment : Fragment() {
private val getUriExport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.also { uri ->
StorageController.exportMyList(requireContext(), uri)
//StorageController.exportMyList(requireContext(), uri)
}
}
}
@ -40,13 +39,13 @@ class AccountFragment : Fragment() {
private val getUriImport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.also { uri ->
val success = StorageController.importMyList(requireContext(), uri)
if (success == 0) {
Toast.makeText(
context, getString(R.string.import_data_success),
Toast.LENGTH_SHORT
).show()
}
// val success = StorageController.importMyList(requireContext(), uri)
// if (success == 0) {
// Toast.makeText(
// context, getString(R.string.import_data_success),
// Toast.LENGTH_SHORT
// ).show()
// }
}
}
}

View File

@ -6,22 +6,21 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentHomeBinding
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.util.ItemMedia
import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.adapter.MediaItemAdapter
import org.mosad.teapod.util.decoration.MediaItemDecoration
import org.mosad.teapod.util.setDrawableTop
import org.mosad.teapod.util.showFragment
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private lateinit var adapterMyList: MediaItemAdapter
private lateinit var adapterWatchlist: MediaItemAdapter
private lateinit var adapterNewEpisodes: MediaItemAdapter
private lateinit var adapterNewSimulcasts: MediaItemAdapter
private lateinit var adapterNewTitles: MediaItemAdapter
private lateinit var adapterTopTen: MediaItemAdapter
@ -61,33 +60,37 @@ class HomeFragment : Fragment() {
// }
}
private fun initRecyclerViews() {
binding.recyclerMyList.addItemDecoration(MediaItemDecoration(9))
/**
* Suspend, since adapters need to be initialized before we can initialize the actions.
*/
private suspend fun initRecyclerViews() {
binding.recyclerWatchlist.addItemDecoration(MediaItemDecoration(9))
binding.recyclerNewEpisodes.addItemDecoration(MediaItemDecoration(9))
binding.recyclerNewSimulcasts.addItemDecoration(MediaItemDecoration(9))
binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9))
binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
// my list
adapterMyList = MediaItemAdapter(mapMyListToItemMedia())
binding.recyclerMyList.adapter = adapterMyList
val asyncJobList = arrayListOf<Job>()
// TODO
// new episodes
// watchlist
val watchlistJob = lifecycleScope.launch {
adapterWatchlist = MediaItemAdapter(mapMyListToItemMedia())
binding.recyclerWatchlist.adapter = adapterWatchlist
}
asyncJobList.add(watchlistJob)
// new episodes TODO replace with continue watching
// adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList)
// binding.recyclerNewEpisodes.adapter = adapterNewEpisodes
// new simulcasts
// adapterNewSimulcasts = MediaItemAdapter(AoDParser.newSimulcastsList)
// binding.recyclerNewSimulcasts.adapter = adapterNewSimulcasts
// new titles
// new titles TODO
// adapterNewTitles = MediaItemAdapter(AoDParser.newTitlesList)
// binding.recyclerNewTitles.adapter = adapterNewTitles
// top ten
// top ten TODO
// adapterTopTen = MediaItemAdapter(AoDParser.topTenList)
// binding.recyclerTopTen.adapter = adapterTopTen
asyncJobList.joinAll()
}
private fun initActions() {
@ -103,23 +106,23 @@ class HomeFragment : Fragment() {
}
binding.textHighlightMyList.setOnClickListener {
if (StorageController.myList.contains(0)) {
StorageController.myList.remove(0)
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
} else {
StorageController.myList.add(0)
binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
}
StorageController.saveMyList(requireContext())
// TODO implement if needed
updateMyListMedia() // update my list, since it has changed
// if (StorageController.myList.contains(0)) {
// StorageController.myList.remove(0)
// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_add_24)
// } else {
// StorageController.myList.add(0)
// binding.textHighlightMyList.setDrawableTop(R.drawable.ic_baseline_check_24)
// }
// StorageController.saveMyList(requireContext())
}
binding.textHighlightInfo.setOnClickListener {
activity?.showFragment(MediaFragment(""))
}
adapterMyList.onItemClick = { id, _ ->
adapterWatchlist.onItemClick = { id, _ ->
activity?.showFragment(MediaFragment("")) //(mediaId))
}
@ -140,27 +143,10 @@ class HomeFragment : Fragment() {
// }
}
/**
* update my media list
* TODO
* * auto call when StorageController.myList is changed
* * only update actual change and not all data (performance)
*/
fun updateMyListMedia() {
//adapterMyList.updateMediaList(mapMyListToItemMedia())
//adapterMyList.notifyDataSetChanged()
}
private fun mapMyListToItemMedia(): List<ItemMedia> {
return emptyList()
// return StorageController.myList.mapNotNull { elementId ->
// AoDParser.guiMediaList.firstOrNull { it.id == elementId.toString() }.also {
// // it the my list entry wasn't found in itemMediaList Log it
// if (it == null) {
// Log.w(javaClass.name, "The element with the id $elementId was not found.")
// }
// }
// }
private suspend fun mapMyListToItemMedia(): List<ItemMedia> {
return Crunchyroll.watchlist(50).items.map {
ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
}
}
}

View File

@ -29,6 +29,9 @@ import kotlinx.coroutines.*
import java.io.FileNotFoundException
import java.net.URL
/**
* TODO remove gson usage
*/
class MetaDBController {
companion object {

View File

@ -1,89 +0,0 @@
package org.mosad.teapod.util
import android.content.Context
import android.net.Uri
import android.util.Log
import com.google.gson.Gson
import com.google.gson.JsonParser
import kotlinx.coroutines.*
import java.io.File
import java.io.FileReader
import java.io.FileWriter
/**
* This controller contains the logic for permanently saved data.
* On load, it loads the saved files into the variables
*/
object StorageController {
private const val fileNameMyList = "my_list.json"
val myList = ArrayList<Int>() // a list of saved mediaIds
fun load(context: Context) {
loadMyList(context)
}
fun loadMyList(context: Context) {
val file = File(context.filesDir, fileNameMyList)
if (!file.exists()) runBlocking { saveMyList(context).join() }
try {
myList.clear()
myList.addAll(JsonParser.parseString(file.readText()).asJsonArray.map { it.asInt }.distinct())
} catch (ex: Exception) {
myList.clear()
Log.e(javaClass.name, "Parsing of My-List failed.")
}
}
fun saveMyList(context: Context): Job {
val file = File(context.filesDir, fileNameMyList)
return CoroutineScope(Dispatchers.IO).launch {
file.writeText(Gson().toJson(myList.distinct()))
}
}
fun exportMyList(context: Context, uri: Uri) {
try {
context.contentResolver.openFileDescriptor(uri, "w")?.use {
FileWriter(it.fileDescriptor).use { writer ->
writer.write(Gson().toJson(myList.distinct()))
}
}
} catch (ex: Exception) {
Log.e(javaClass.name, "Exporting my list failed.", ex)
}
}
/**
* import my list from a (previously exported) json file
* @param context the current context
* @param uri the uri of the selected file
* @return 0 if import was successfull, else 1
*/
fun importMyList(context: Context, uri: Uri): Int {
try {
val text = context.contentResolver.openFileDescriptor(uri, "r")?.use {
FileReader(it.fileDescriptor).use { reader ->
reader.readText()
}
}
myList.clear()
myList.addAll(JsonParser.parseString(text).asJsonArray.map { it.asInt }.distinct())
// after the list has been imported also save it
saveMyList(context)
} catch (ex: Exception) {
myList.clear()
Log.e(javaClass.name, "Importing my list failed.", ex)
return 1
}
return 0
}
}

View File

@ -108,14 +108,14 @@
</LinearLayout>
<LinearLayout
android:id="@+id/linear_my_list"
android:id="@+id/linear_watchlist"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="7dp">
<TextView
android:id="@+id/text_my_list"
android:id="@+id/text_watchlist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="10dp"
@ -127,7 +127,7 @@
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_my_list"
android:id="@+id/recycler_watchlist"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
@ -163,34 +163,6 @@
tools:listitem="@layout/item_media" />
</LinearLayout>
<LinearLayout
android:id="@+id/linear_new_simulcasts"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="7dp">
<TextView
android:id="@+id/text_new_simulcasts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="10dp"
android:paddingTop="15dp"
android:paddingEnd="5dp"
android:paddingBottom="5dp"
android:text="@string/new_simulcasts"
android:textSize="16sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_new_simulcasts"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_media" />
</LinearLayout>
<LinearLayout
android:id="@+id/linear_new_titles"
android:layout_width="match_parent"