add watchlist to home fragment
This commit is contained in:
		| @ -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) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
|  */ | ||||
|  | ||||
| @ -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()) { | ||||
|  | ||||
| @ -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() | ||||
| //                } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -29,6 +29,9 @@ import kotlinx.coroutines.* | ||||
| import java.io.FileNotFoundException | ||||
| import java.net.URL | ||||
|  | ||||
| /** | ||||
|  * TODO remove gson usage | ||||
|  */ | ||||
| class MetaDBController { | ||||
|  | ||||
|     companion object { | ||||
|  | ||||
| @ -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 | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -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" | ||||
|  | ||||
		Reference in New Issue
	
	Block a user