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 } ?: 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? * 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 * @return Boolean: ture if it was found, else false
*/ */
suspend fun isWatchlist(seriesId: String): Boolean { 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 parameters = listOf("locale" to locale)
val result = request(watchlistEndpoint, parameters) val result = request(watchlistSeriesEndpoint, parameters)
// if needed implement parsing // if needed implement parsing
return result.component1()?.obj()?.has(seriesId) ?: false return result.component1()?.obj()?.has(seriesId) ?: false
@ -298,14 +322,14 @@ object Crunchyroll {
* @param seriesId The crunchyroll series id of the media to check * @param seriesId The crunchyroll series id of the media to check
*/ */
suspend fun postWatchlist(seriesId: String) { suspend fun postWatchlist(seriesId: String) {
val watchlistEndpoint = "/content/v1/watchlist/$accountID" val watchlistPostEndpoint = "/content/v1/watchlist/$accountID"
val parameters = listOf("locale" to locale) val parameters = listOf("locale" to locale)
val json = buildJsonObject { val json = buildJsonObject {
put("content_id", seriesId) 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 * @param seriesId The crunchyroll series id of the media to check
*/ */
suspend fun deleteWatchlist(seriesId: String) { 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) val parameters = listOf("locale" to locale)
requestDelete(watchlistEndpoint, parameters) requestDelete(watchlistDeleteEndpoint, parameters)
} }
/** /**
@ -327,4 +351,21 @@ object Crunchyroll {
// implement // 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 @Serializable
data class SearchResult( data class SearchResult(
@SerialName("total") val total: Int, @SerialName("total") val total: Int,
@ -24,17 +34,21 @@ data class SearchResult(
) )
@Serializable @Serializable
data class SearchCollection( data class ContinueWatchingList(
@SerialName("type") val type: String, @SerialName("total") val total: Int,
@SerialName("items") val items: List<Item> @SerialName("items") val items: List<ContinueWatchingItem>
) )
val NoneSearchResult = SearchResult(0, emptyList())
@Serializable @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 // the data class Item is used in browse and search
// TODO rename to MediaPanel // TODO rename to MediaPanel
@ -49,8 +63,17 @@ data class Item(
// TODO metadata etc. // TODO metadata etc.
) )
val NoneItem = Item("", "", "", "", "", Images(listOf(), listOf())) // EpisodePanel is used in ContinueWatchingItem
val NoneBrowseResult = BrowseResult(0, listOf()) @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 @Serializable
data class Images(val poster_tall: List<List<Poster>>, val poster_wide: List<List<Poster>>) 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 @Serializable
data class Poster(val height: Int, val width: Int, val source: String, val type: String) 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 * 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.activity.player.PlayerActivity
import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.util.DataTypes import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.MetaDBController
import org.mosad.teapod.util.StorageController
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener { class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListener {
@ -139,7 +137,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
// load all saved stuff here // load all saved stuff here
Preferences.load(this) Preferences.load(this)
EncryptedPreferences.readCredentials(this) EncryptedPreferences.readCredentials(this)
StorageController.load(this)
// show onboarding TODO rework // show onboarding TODO rework
if (EncryptedPreferences.password.isEmpty()) { 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.activity.main.MainActivity
import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.util.DataTypes.Theme import org.mosad.teapod.util.DataTypes.Theme
import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.showFragment import org.mosad.teapod.util.showFragment
class AccountFragment : Fragment() { class AccountFragment : Fragment() {
@ -32,7 +31,7 @@ class AccountFragment : Fragment() {
private val getUriExport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val getUriExport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.also { uri -> 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 -> private val getUriImport = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.also { uri -> result.data?.data?.also { uri ->
val success = StorageController.importMyList(requireContext(), uri) // val success = StorageController.importMyList(requireContext(), uri)
if (success == 0) { // if (success == 0) {
Toast.makeText( // Toast.makeText(
context, getString(R.string.import_data_success), // context, getString(R.string.import_data_success),
Toast.LENGTH_SHORT // Toast.LENGTH_SHORT
).show() // ).show()
} // }
} }
} }
} }

View File

@ -6,22 +6,21 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mosad.teapod.R
import org.mosad.teapod.databinding.FragmentHomeBinding import org.mosad.teapod.databinding.FragmentHomeBinding
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.util.ItemMedia import org.mosad.teapod.util.ItemMedia
import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.adapter.MediaItemAdapter import org.mosad.teapod.util.adapter.MediaItemAdapter
import org.mosad.teapod.util.decoration.MediaItemDecoration import org.mosad.teapod.util.decoration.MediaItemDecoration
import org.mosad.teapod.util.setDrawableTop
import org.mosad.teapod.util.showFragment import org.mosad.teapod.util.showFragment
class HomeFragment : Fragment() { class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding private lateinit var binding: FragmentHomeBinding
private lateinit var adapterMyList: MediaItemAdapter private lateinit var adapterWatchlist: MediaItemAdapter
private lateinit var adapterNewEpisodes: MediaItemAdapter private lateinit var adapterNewEpisodes: MediaItemAdapter
private lateinit var adapterNewSimulcasts: MediaItemAdapter
private lateinit var adapterNewTitles: MediaItemAdapter private lateinit var adapterNewTitles: MediaItemAdapter
private lateinit var adapterTopTen: 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.recyclerNewEpisodes.addItemDecoration(MediaItemDecoration(9))
binding.recyclerNewSimulcasts.addItemDecoration(MediaItemDecoration(9))
binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9)) binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9))
binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9)) binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
// my list val asyncJobList = arrayListOf<Job>()
adapterMyList = MediaItemAdapter(mapMyListToItemMedia())
binding.recyclerMyList.adapter = adapterMyList
// TODO // watchlist
// new episodes 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) // adapterNewEpisodes = MediaItemAdapter(AoDParser.newEpisodesList)
// binding.recyclerNewEpisodes.adapter = adapterNewEpisodes // binding.recyclerNewEpisodes.adapter = adapterNewEpisodes
// new simulcasts // new titles TODO
// adapterNewSimulcasts = MediaItemAdapter(AoDParser.newSimulcastsList)
// binding.recyclerNewSimulcasts.adapter = adapterNewSimulcasts
// new titles
// adapterNewTitles = MediaItemAdapter(AoDParser.newTitlesList) // adapterNewTitles = MediaItemAdapter(AoDParser.newTitlesList)
// binding.recyclerNewTitles.adapter = adapterNewTitles // binding.recyclerNewTitles.adapter = adapterNewTitles
// top ten // top ten TODO
// adapterTopTen = MediaItemAdapter(AoDParser.topTenList) // adapterTopTen = MediaItemAdapter(AoDParser.topTenList)
// binding.recyclerTopTen.adapter = adapterTopTen // binding.recyclerTopTen.adapter = adapterTopTen
asyncJobList.joinAll()
} }
private fun initActions() { private fun initActions() {
@ -103,23 +106,23 @@ class HomeFragment : Fragment() {
} }
binding.textHighlightMyList.setOnClickListener { binding.textHighlightMyList.setOnClickListener {
if (StorageController.myList.contains(0)) { // TODO implement if needed
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())
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 { binding.textHighlightInfo.setOnClickListener {
activity?.showFragment(MediaFragment("")) activity?.showFragment(MediaFragment(""))
} }
adapterMyList.onItemClick = { id, _ -> adapterWatchlist.onItemClick = { id, _ ->
activity?.showFragment(MediaFragment("")) //(mediaId)) activity?.showFragment(MediaFragment("")) //(mediaId))
} }
@ -140,27 +143,10 @@ class HomeFragment : Fragment() {
// } // }
} }
/** private suspend fun mapMyListToItemMedia(): List<ItemMedia> {
* update my media list return Crunchyroll.watchlist(50).items.map {
* TODO ItemMedia(it.id, it.title, it.images.poster_wide[0][0].source)
* * 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.")
// }
// }
// }
} }
} }

View File

@ -29,6 +29,9 @@ import kotlinx.coroutines.*
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.net.URL import java.net.URL
/**
* TODO remove gson usage
*/
class MetaDBController { class MetaDBController {
companion object { 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>
<LinearLayout <LinearLayout
android:id="@+id/linear_my_list" android:id="@+id/linear_watchlist"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="7dp"> android:paddingBottom="7dp">
<TextView <TextView
android:id="@+id/text_my_list" android:id="@+id/text_watchlist"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="10dp" android:paddingStart="10dp"
@ -127,7 +127,7 @@
android:textStyle="bold" /> android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_my_list" android:id="@+id/recycler_watchlist"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal" android:orientation="horizontal"
@ -163,34 +163,6 @@
tools:listitem="@layout/item_media" /> tools:listitem="@layout/item_media" />
</LinearLayout> </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 <LinearLayout
android:id="@+id/linear_new_titles" android:id="@+id/linear_new_titles"
android:layout_width="match_parent" android:layout_width="match_parent"