sync fixes + UI

This commit is contained in:
LagradOst 2022-04-03 03:13:02 +02:00
parent a933aa8493
commit 4447196ebc
13 changed files with 556 additions and 372 deletions

View file

@ -693,13 +693,13 @@ interface LoadResponse {
val url: String val url: String
val apiName: String val apiName: String
val type: TvType val type: TvType
val posterUrl: String? var posterUrl: String?
val year: Int? val year: Int?
val plot: String? var plot: String?
val rating: Int? // 1-1000 var rating: Int? // 1-1000
val tags: List<String>? var tags: List<String>?
var duration: Int? // in minutes var duration: Int? // in minutes
val trailerUrl: String? var trailerUrl: String?
var recommendations: List<SearchResponse>? var recommendations: List<SearchResponse>?
var actors: List<ActorData>? var actors: List<ActorData>?
var comingSoon: Boolean var comingSoon: Boolean

View file

@ -73,6 +73,8 @@ interface SyncAPI : OAuth2API {
var synonyms: List<String>? = null, var synonyms: List<String>? = null,
var trailerUrl: String? = null, var trailerUrl: String? = null,
var isAdult : Boolean? = null, var isAdult : Boolean? = null,
var posterUrl: String? = null,
var backgroundPosterUrl : String? = null,
/** In unixtime */ /** In unixtime */
var startDate: Long? = null, var startDate: Long? = null,

View file

@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.mvvm.safeApiCall
class SyncRepo(private val repo: SyncAPI) { class SyncRepo(private val repo: SyncAPI) {
val idPrefix = repo.idPrefix val idPrefix = repo.idPrefix
val name = repo.name val name = repo.name
val icon = repo.icon
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> { suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
return safeApiCall { repo.score(id, status) } return safeApiCall { repo.score(id, status) }

View file

@ -9,6 +9,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -20,6 +21,7 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import java.net.URL import java.net.URL
@ -86,7 +88,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun getResult(id: String): SyncAPI.SyncResult? { override suspend fun getResult(id: String): SyncAPI.SyncResult? {
val internalId = id.toIntOrNull() ?: return null val internalId = id.toIntOrNull() ?: return null
val season = getSeason(internalId)?.data?.Media ?: return null val season = getSeason(internalId).data?.Media ?: throw ErrorLoadingException("No media")
return SyncAPI.SyncResult( return SyncAPI.SyncResult(
season.id.toString(), season.id.toString(),
@ -100,7 +102,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
synonyms = season.synonyms, synonyms = season.synonyms,
isAdult = season.isAdult, isAdult = season.isAdult,
totalEpisodes = season.episodes, totalEpisodes = season.episodes,
//synopsis = season. synopsis = season.description,
//TODO REST //TODO REST
) )
} }
@ -295,28 +298,42 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
return fromIntToAnimeStatus(aniListStatusString.indexOf(string)) return fromIntToAnimeStatus(aniListStatusString.indexOf(string))
} }
private suspend fun getSeason(id: Int): SeasonResponse {
private suspend fun getSeason(id: Int): SeasonResponse? {
val q = """ val q = """
query (${'$'}id: Int = $id) { query (${'$'}id: Int = $id) {
Media (id: ${'$'}id, type: ANIME) { Media (id: ${'$'}id, type: ANIME) {
id id
idMal idMal
coverImage coverImage {
extraLarge
large
medium
color
}
duration duration
episodes episodes
genres genres
synonyms synonyms
averageScore averageScore
isAdult isAdult
trailer description(asHtml: false)
trailer {
id
site
thumbnail
}
relations { relations {
edges { edges {
id id
relationType(version: 2) relationType(version: 2)
node { node {
id id
coverImage coverImage {
extraLarge
large
medium
color
}
} }
} }
} }
@ -328,19 +345,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
""" """
val data = app.post( val data = app.post(
"https://graphql.anilist.co", "https://graphql.anilist.co",
data = mapOf("query" to q), data = mapOf("query" to q),
cacheTime = 0, cacheTime = 0,
).text ).text
if (data == "") return null
return try { return tryParseJson(data) ?: throw ErrorLoadingException("Error parsing $data")
mapper.readValue(data)
} catch (e: Exception) {
logError(e)
null
}
} }
} }
@ -661,7 +672,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val seasons = mutableListOf<SeasonResponse?>() val seasons = mutableListOf<SeasonResponse?>()
suspend fun getSeasonRecursive(id: Int) { suspend fun getSeasonRecursive(id: Int) {
val season = getSeason(id) val season = getSeason(id)
if (season != null) {
seasons.add(season) seasons.add(season)
if (season.data?.Media?.format?.startsWith("TV") == true) { if (season.data?.Media?.format?.startsWith("TV") == true) {
season.data.Media.relations?.edges?.forEach { season.data.Media.relations?.edges?.forEach {
@ -674,7 +684,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
} }
}
getSeasonRecursive(id) getSeasonRecursive(id)
return seasons.toList() return seasons.toList()
} }
@ -701,7 +710,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("averageScore") val averageScore: Int?, @JsonProperty("averageScore") val averageScore: Int?,
@JsonProperty("isAdult") val isAdult: Boolean?, @JsonProperty("isAdult") val isAdult: Boolean?,
@JsonProperty("trailer") val trailer: MediaTrailer?, @JsonProperty("trailer") val trailer: MediaTrailer?,
@JsonProperty("description") val description: String?,
) )
data class MediaTrailer( data class MediaTrailer(

View file

@ -0,0 +1,82 @@
package com.lagradost.cloudstream3.ui.result
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
/*
class ImageAdapter(context: Context, val resource: Int) : ArrayAdapter<Int>(context, resource) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val newConvertView = convertView ?: run {
val mInflater = context
.getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
mInflater.inflate(resource, null)
}
getItem(position)?.let { (newConvertView as? ImageView?)?.setImageResource(it) }
return newConvertView
}
}*/
class ImageAdapter(
val layout: Int,
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val images: MutableList<Int> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ImageViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false)
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ImageViewHolder -> {
holder.bind(images[position])
}
}
}
override fun getItemCount(): Int {
return images.size
}
override fun getItemId(position: Int): Long {
return images[position].toLong()
}
fun updateList(newList: List<Int>) {
val diffResult = DiffUtil.calculateDiff(
DiffCallback(this.images, newList)
)
images.clear()
images.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
class ImageViewHolder
constructor(itemView: View) :
RecyclerView.ViewHolder(itemView) {
fun bind(img: Int) {
(itemView as? ImageView?)?.setImageResource(img)
}
}
}
class DiffCallback<T>(private val oldList: List<T>, private val newList: List<T>) :
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition]
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition]
}

View file

@ -636,7 +636,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
val apiName = arguments?.getString("apiName") ?: return val apiName = arguments?.getString("apiName") ?: return
startAction = arguments?.getInt("startAction") ?: START_ACTION_NORMAL startAction = arguments?.getInt("startAction") ?: START_ACTION_NORMAL
startValue = arguments?.getInt("startValue") ?: START_VALUE_NORMAL startValue = arguments?.getInt("startValue") ?: START_VALUE_NORMAL
syncModel.addFromUrl(url) syncModel.addFromUrl(url)
val api = getApiFromName(apiName) val api = getApiFromName(apiName)
@ -1198,17 +1197,26 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} }
} }
} }
val imgAdapter = ImageAdapter(R.layout.result_mini_image)
result_mini_sync?.adapter = imgAdapter
observe(syncModel.synced) { list -> observe(syncModel.synced) { list ->
result_sync_names?.text = result_sync_names?.text =
list.filter { it.isSynced && it.hasAccount }.joinToString { it.name } list.filter { it.isSynced && it.hasAccount }.joinToString { it.name }
val newList = list.filter { it.isSynced }
result_mini_sync?.isVisible = newList.isNotEmpty()
(result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.map { it.icon })
} }
var currentSyncProgress = 0
observe(syncModel.metadata) { meta -> observe(syncModel.metadata) { meta ->
when (meta) { when (meta) {
is Resource.Success -> { is Resource.Success -> {
val d = meta.value val d = meta.value
result_sync_episodes?.max = (d.totalEpisodes ?: 0) * 1000 result_sync_episodes?.max = (d.totalEpisodes ?: 0) * 1000
result_sync_episodes?.progress = currentSyncProgress * 1000
normalSafeApiCall { normalSafeApiCall {
val ctx = result_sync_max_episodes?.context val ctx = result_sync_max_episodes?.context
result_sync_max_episodes?.text = result_sync_max_episodes?.text =
@ -1218,6 +1226,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
ctx?.getString(R.string.sync_total_episodes_none) ctx?.getString(R.string.sync_total_episodes_none)
} }
} }
viewModel.setMeta(d)
} }
is Resource.Loading -> { is Resource.Loading -> {
result_sync_max_episodes?.text = result_sync_max_episodes?.text =
@ -1249,6 +1258,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
result_sync_rating?.value = d.score?.toFloat() ?: 0.0f result_sync_rating?.value = d.score?.toFloat() ?: 0.0f
result_sync_check?.setItemChecked(d.status + 1, true) result_sync_check?.setItemChecked(d.status + 1, true)
val watchedEpisodes = d.watchedEpisodes ?: 0 val watchedEpisodes = d.watchedEpisodes ?: 0
currentSyncProgress = watchedEpisodes
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
result_sync_episodes?.setProgress(watchedEpisodes * 1000, true) result_sync_episodes?.setProgress(watchedEpisodes * 1000, true)
} else { } else {
@ -1447,11 +1457,10 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
currentId = it currentId = it
} }
observe(viewModel.resultResponse) { data -> observe(viewModel.result) { data ->
when (data) { when (data) {
is Resource.Success -> { is Resource.Success -> {
val d = data.value val d = data.value
if (d is LoadResponse) {
if (d !is AnimeLoadResponse && result_episode_loading.isVisible) { // no episode loading when not anime if (d !is AnimeLoadResponse && result_episode_loading.isVisible) { // no episode loading when not anime
result_episode_loading.isVisible = false result_episode_loading.isVisible = false
} }
@ -1533,6 +1542,8 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
setAniListSync(d.anilistId) setAniListSync(d.anilistId)
) { ) {
syncModel.updateMetaAndUser() syncModel.updateMetaAndUser()
} else {
syncModel.addFromUrl(d.url)
} }
} }
@ -1799,7 +1810,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
result_meta_type?.text = it result_meta_type?.text = it
} }
when (d) { when (d) {
is AnimeLoadResponse -> { is AnimeLoadResponse -> {
@ -1811,9 +1821,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} }
else -> result_title.text = d.name else -> result_title.text = d.name
} }
} else {
updateVisStatus(1)
}
} }
is Resource.Failure -> { is Resource.Failure -> {
result_error_text.text = url?.plus("\n") + data.errorString result_error_text.text = url?.plus("\n") + data.errorString
@ -1873,7 +1880,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
it.context?.openBrowser(tempUrl) it.context?.openBrowser(tempUrl)
} }
if (restart || viewModel.resultResponse.value == null) { if (restart || viewModel.result.value == null) {
//viewModel.clear() //viewModel.clear()
viewModel.load(tempUrl, apiName, showFillers) viewModel.load(tempUrl, apiName, showFillers)
} }

View file

@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.ui.result package com.lagradost.cloudstream3.ui.result
import android.content.Context import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.player.IGenerator import com.lagradost.cloudstream3.ui.player.IGenerator
@ -43,7 +44,7 @@ class ResultViewModel : ViewModel() {
private var repo: APIRepository? = null private var repo: APIRepository? = null
private var generator: IGenerator? = null private var generator: IGenerator? = null
private val _resultResponse: MutableLiveData<Resource<Any?>> = MutableLiveData() private val _resultResponse: MutableLiveData<Resource<LoadResponse>> = MutableLiveData()
private val _episodes: MutableLiveData<List<ResultEpisode>> = MutableLiveData() private val _episodes: MutableLiveData<List<ResultEpisode>> = MutableLiveData()
private val episodeById: MutableLiveData<HashMap<Int, Int>> = private val episodeById: MutableLiveData<HashMap<Int, Int>> =
MutableLiveData() // lookup by ID to get Index MutableLiveData() // lookup by ID to get Index
@ -55,7 +56,8 @@ class ResultViewModel : ViewModel() {
private val selectedRangeInt: MutableLiveData<Int> = MutableLiveData() private val selectedRangeInt: MutableLiveData<Int> = MutableLiveData()
val rangeOptions: LiveData<List<String>> = _rangeOptions val rangeOptions: LiveData<List<String>> = _rangeOptions
val resultResponse: LiveData<Resource<Any?>> get() = _resultResponse val result: LiveData<Resource<LoadResponse>> get() = _resultResponse
val episodes: LiveData<List<ResultEpisode>> get() = _episodes val episodes: LiveData<List<ResultEpisode>> get() = _episodes
val publicEpisodes: LiveData<Resource<List<ResultEpisode>>> get() = _publicEpisodes val publicEpisodes: LiveData<Resource<List<ResultEpisode>>> get() = _publicEpisodes
val publicEpisodesCount: LiveData<Int> get() = _publicEpisodesCount val publicEpisodesCount: LiveData<Int> get() = _publicEpisodesCount
@ -106,6 +108,41 @@ class ResultViewModel : ViewModel() {
} }
} }
companion object {
const val TAG = "RVM"
}
var lastMeta: SyncAPI.SyncResult? = null
private fun applyMeta(resp: LoadResponse, meta: SyncAPI.SyncResult?): LoadResponse {
if (meta == null) return resp
lastMeta = meta
return resp.apply {
Log.i(TAG, "applyMeta")
duration = duration ?: meta.duration
rating = rating ?: meta.publicScore
tags = tags ?: meta.genres
plot = if (plot.isNullOrBlank()) meta.synopsis else plot
trailerUrl = trailerUrl ?: meta.trailerUrl
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
actors = actors ?: meta.actors?.map {
ActorData(
Actor(
name = it.name,
image = it.posterUrl
)
)
}
}
}
fun setMeta(meta: SyncAPI.SyncResult) {
Log.i(TAG, "setMeta")
(result.value as? Resource.Success<LoadResponse>?)?.value?.let { resp ->
_resultResponse.postValue(Resource.Success(applyMeta(resp, meta)))
}
}
private fun loadWatchStatus(localId: Int? = null) { private fun loadWatchStatus(localId: Int? = null) {
val currentId = localId ?: id.value ?: return val currentId = localId ?: id.value ?: return
val currentWatch = getResultWatchState(currentId) val currentWatch = getResultWatchState(currentId)
@ -289,7 +326,7 @@ class ResultViewModel : ViewModel() {
when (data) { when (data) {
is Resource.Success -> { is Resource.Success -> {
val d = data.value val d = applyMeta(data.value, lastMeta)
page.postValue(d) page.postValue(d)
val mainId = d.getId() val mainId = d.getId()
id.postValue(mainId) id.postValue(mainId)

View file

@ -18,6 +18,7 @@ data class CurrentSynced(
val idPrefix: String, val idPrefix: String,
val isSynced: Boolean, val isSynced: Boolean,
val hasAccount: Boolean, val hasAccount: Boolean,
val icon : Int,
) )
class SyncViewModel : ViewModel() { class SyncViewModel : ViewModel() {
@ -48,7 +49,8 @@ class SyncViewModel : ViewModel() {
it.name, it.name,
it.idPrefix, it.idPrefix,
syncIds.containsKey(it.idPrefix), syncIds.containsKey(it.idPrefix),
it.hasAccount() it.hasAccount(),
it.icon,
) )
} }
} }
@ -67,8 +69,13 @@ class SyncViewModel : ViewModel() {
updateSynced() updateSynced()
} }
var hasAddedFromUrl : HashSet<String> = hashSetOf()
fun addFromUrl(url: String?) = viewModelScope.launch { fun addFromUrl(url: String?) = viewModelScope.launch {
if(url == null || hasAddedFromUrl.contains(url)) return@launch
SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) -> SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) ->
hasAddedFromUrl.add(url)
setMalId(malId) setMalId(malId)
setAniListId(aniListId) setAniListId(aniListId)
if (malId != null || aniListId != null) { if (malId != null || aniListId != null) {

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.utils package com.lagradost.cloudstream3.utils
import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
@ -14,6 +15,8 @@ object SyncUtil {
Regex("""(twist\.moe)/a/([^/?]*)"""), Regex("""(twist\.moe)/a/([^/?]*)"""),
) )
private const val TAG = "SNC"
private const val GOGOANIME = "Gogoanime" private const val GOGOANIME = "Gogoanime"
private const val NINE_ANIME = "9anime" private const val NINE_ANIME = "9anime"
private const val TWIST_MOE = "Twistmoe" private const val TWIST_MOE = "Twistmoe"
@ -28,6 +31,7 @@ object SyncUtil {
suspend fun getIdsFromUrl(url: String?): Pair<String?, String?>? { suspend fun getIdsFromUrl(url: String?): Pair<String?, String?>? {
if (url == null) return null if (url == null) return null
Log.i(TAG, "getIdsFromUrl $url")
for (regex in regexs) { for (regex in regexs) {
regex.find(url)?.let { match -> regex.find(url)?.let { match ->
@ -51,6 +55,7 @@ object SyncUtil {
slug: String, slug: String,
site: String = "GogoanimeGogoanime" site: String = "GogoanimeGogoanime"
): Pair<String?, String?>? { ): Pair<String?, String?>? {
Log.i(TAG, "getIdsFromSlug $slug $site")
try { try {
//Gogoanime, Twistmoe and 9anime //Gogoanime, Twistmoe and 9anime
val url = val url =

View file

@ -1,6 +1,6 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="20dp"
android:height="24dp" android:height="20dp"
android:viewportWidth="172" android:viewportWidth="172"
android:viewportHeight="172" android:viewportHeight="172"
android:tint="?attr/white" android:tint="?attr/white"

View file

@ -375,6 +375,7 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/result_bookmark_button" android:id="@+id/result_bookmark_button"
style="@style/BlackButton" style="@style/BlackButton"

View file

@ -12,7 +12,12 @@
android:paddingEnd="@dimen/result_padding" android:paddingEnd="@dimen/result_padding"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView <ImageView
android:nextFocusDown="@id/result_bookmark_button" android:nextFocusDown="@id/result_bookmark_button"
android:nextFocusRight="@id/result_share" android:nextFocusRight="@id/result_share"
@ -28,6 +33,24 @@
android:src="@drawable/ic_baseline_arrow_back_24" android:src="@drawable/ic_baseline_arrow_back_24"
android:contentDescription="@string/go_back" android:contentDescription="@string/go_back"
app:tint="?attr/white" /> app:tint="?attr/white" />
<androidx.recyclerview.widget.RecyclerView
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:id="@+id/result_mini_sync"
android:layout_width="match_parent"
android:descendantFocusability="afterDescendants"
android:layout_height="wrap_content"
android:fadingEdge="horizontal"
android:focusableInTouchMode="false"
android:focusable="false"
android:layout_gravity="center"
android:orientation="horizontal"
android:paddingTop="5dp"
android:requiresFadingEdge="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="2"
tools:listitem="@layout/result_mini_image" />
</LinearLayout>
<LinearLayout <LinearLayout
android:gravity="end" android:gravity="end"

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
tools:src="@drawable/ic_anilist_icon"
app:tint="?attr/white">
</ImageView>