add watchlist to home fragment
parent
f100b4abf3
commit
d5d70e49d2
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue