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,15 +672,13 @@ 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 { if (it.node?.format != null) {
if (it.node?.format != null) { if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) {
if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) { getSeasonRecursive(it.node.id)
getSeasonRecursive(it.node.id) return@forEach
return@forEach
}
} }
} }
} }
@ -701,8 +710,8 @@ 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(
@JsonProperty("id") val id: String?, @JsonProperty("id") val id: String?,

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,372 +1457,369 @@ 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 }
updateVisStatus(2)
result_vpn?.text = when (api.vpnStatus) {
VPNStatus.MightBeNeeded -> getString(R.string.vpn_might_be_needed)
VPNStatus.Torrent -> getString(R.string.vpn_torrent)
else -> ""
}
result_vpn?.isGone = api.vpnStatus == VPNStatus.None
result_info?.text = when (api.providerType) {
ProviderType.MetaProvider -> getString(R.string.provider_info_meta)
else -> ""
}
result_info?.isVisible = api.providerType == ProviderType.MetaProvider
if (d.type.isEpisodeBased()) {
val ep = d as? TvSeriesLoadResponse
val epCount = ep?.episodes?.size ?: 1
if (epCount < 1) {
result_info?.text = getString(R.string.no_episodes_found)
result_info?.isVisible = true
} }
}
updateVisStatus(2) currentHeaderName = d.name
currentType = d.type
result_vpn?.text = when (api.vpnStatus) { currentPoster = d.posterUrl
VPNStatus.MightBeNeeded -> getString(R.string.vpn_might_be_needed) currentIsMovie = !d.isEpisodeBased()
VPNStatus.Torrent -> getString(R.string.vpn_torrent)
else -> "" result_open_in_browser?.setOnClickListener {
val i = Intent(ACTION_VIEW)
i.data = Uri.parse(d.url)
try {
startActivity(i)
} catch (e: Exception) {
logError(e)
} }
result_vpn?.isGone = api.vpnStatus == VPNStatus.None }
result_info?.text = when (api.providerType) { result_search?.setOnClickListener {
ProviderType.MetaProvider -> getString(R.string.provider_info_meta) QuickSearchFragment.pushSearch(activity, d.name)
else -> "" }
result_share?.setOnClickListener {
try {
val i = Intent(ACTION_SEND)
i.type = "text/plain"
i.putExtra(EXTRA_SUBJECT, d.name)
i.putExtra(EXTRA_TEXT, d.url)
startActivity(createChooser(i, d.name))
} catch (e: Exception) {
logError(e)
} }
result_info?.isVisible = api.providerType == ProviderType.MetaProvider }
if (d.type.isEpisodeBased()) { val showStatus = when (d) {
val ep = d as? TvSeriesLoadResponse is TvSeriesLoadResponse -> d.showStatus
val epCount = ep?.episodes?.size ?: 1 is AnimeLoadResponse -> d.showStatus
if (epCount < 1) { else -> null
result_info?.text = getString(R.string.no_episodes_found) }
result_info?.isVisible = true
setShow(showStatus)
setDuration(d.duration)
setYear(d.year)
setRating(d.rating)
setRecommendations(d.recommendations)
setActors(d.actors)
if (SettingsFragment.accountEnabled)
if (d is AnimeLoadResponse) {
if (
setMalSync(d.malId)
||
setAniListSync(d.anilistId)
) {
syncModel.updateMetaAndUser()
} else {
syncModel.addFromUrl(d.url)
} }
} }
currentHeaderName = d.name result_meta_site?.text = d.apiName
currentType = d.type
currentPoster = d.posterUrl val posterImageLink = d.posterUrl
currentIsMovie = !d.isEpisodeBased() if (!posterImageLink.isNullOrEmpty()) {
result_poster?.setImage(posterImageLink)
result_open_in_browser?.setOnClickListener { result_poster_blur?.setImageBlur(posterImageLink, 10, 3)
val i = Intent(ACTION_VIEW) //Full screen view of Poster image
i.data = Uri.parse(d.url) result_poster_holder?.setOnClickListener {
try { try {
startActivity(i) context?.let { ctx ->
} catch (e: Exception) { val bitmap = result_poster.drawable.toBitmap()
logError(e) val sourceBuilder = AlertDialog.Builder(ctx)
} sourceBuilder.setView(R.layout.result_poster)
}
result_search?.setOnClickListener { val sourceDialog = sourceBuilder.create()
QuickSearchFragment.pushSearch(activity, d.name) sourceDialog.show()
}
result_share?.setOnClickListener { sourceDialog.findViewById<ImageView?>(R.id.imgPoster)
try { ?.apply {
val i = Intent(ACTION_SEND) setImageBitmap(bitmap)
i.type = "text/plain" setOnClickListener {
i.putExtra(EXTRA_SUBJECT, d.name) sourceDialog.dismissSafe()
i.putExtra(EXTRA_TEXT, d.url)
startActivity(createChooser(i, d.name))
} catch (e: Exception) {
logError(e)
}
}
val showStatus = when (d) {
is TvSeriesLoadResponse -> d.showStatus
is AnimeLoadResponse -> d.showStatus
else -> null
}
setShow(showStatus)
setDuration(d.duration)
setYear(d.year)
setRating(d.rating)
setRecommendations(d.recommendations)
setActors(d.actors)
if (SettingsFragment.accountEnabled)
if (d is AnimeLoadResponse) {
if (
setMalSync(d.malId)
||
setAniListSync(d.anilistId)
) {
syncModel.updateMetaAndUser()
}
}
result_meta_site?.text = d.apiName
val posterImageLink = d.posterUrl
if (!posterImageLink.isNullOrEmpty()) {
result_poster?.setImage(posterImageLink)
result_poster_blur?.setImageBlur(posterImageLink, 10, 3)
//Full screen view of Poster image
result_poster_holder?.setOnClickListener {
try {
context?.let { ctx ->
val bitmap = result_poster.drawable.toBitmap()
val sourceBuilder = AlertDialog.Builder(ctx)
sourceBuilder.setView(R.layout.result_poster)
val sourceDialog = sourceBuilder.create()
sourceDialog.show()
sourceDialog.findViewById<ImageView?>(R.id.imgPoster)
?.apply {
setImageBitmap(bitmap)
setOnClickListener {
sourceDialog.dismissSafe()
}
} }
} }
} catch (e: Exception) {
logError(e)
} }
} catch (e: Exception) {
logError(e)
} }
} else {
result_poster?.setImageResource(R.drawable.default_cover)
result_poster_blur?.setImageResource(R.drawable.default_cover)
} }
} else {
result_poster?.setImageResource(R.drawable.default_cover)
result_poster_blur?.setImageResource(R.drawable.default_cover)
}
result_poster_holder?.visibility = VISIBLE result_poster_holder?.visibility = VISIBLE
/*result_play_movie?.text = /*result_play_movie?.text =
if (d.type == TvType.Torrent) getString(R.string.play_torrent_button) else getString( if (d.type == TvType.Torrent) getString(R.string.play_torrent_button) else getString(
R.string.play_movie_button R.string.play_movie_button
)*/ )*/
//result_plot_header?.text = //result_plot_header?.text =
// if (d.type == TvType.Torrent) getString(R.string.torrent_plot) else getString(R.string.result_plot) // if (d.type == TvType.Torrent) getString(R.string.torrent_plot) else getString(R.string.result_plot)
if (!d.plot.isNullOrEmpty()) { if (!d.plot.isNullOrEmpty()) {
var syno = d.plot!! var syno = d.plot!!
if (syno.length > MAX_SYNO_LENGH) { if (syno.length > MAX_SYNO_LENGH) {
syno = syno.substring(0, MAX_SYNO_LENGH) + "..." syno = syno.substring(0, MAX_SYNO_LENGH) + "..."
}
result_descript.setOnClickListener {
val builder: AlertDialog.Builder =
AlertDialog.Builder(requireContext())
builder.setMessage(d.plot)
.setTitle(if (d.type == TvType.Torrent) R.string.torrent_plot else R.string.result_plot)
.show()
}
result_descript.text = syno
} else {
result_descript.text =
if (d.type == TvType.Torrent) getString(R.string.torrent_no_plot) else getString(
R.string.normal_no_plot
)
} }
result_descript.setOnClickListener {
val builder: AlertDialog.Builder =
AlertDialog.Builder(requireContext())
builder.setMessage(d.plot)
.setTitle(if (d.type == TvType.Torrent) R.string.torrent_plot else R.string.result_plot)
.show()
}
result_descript.text = syno
} else {
result_descript.text =
if (d.type == TvType.Torrent) getString(R.string.torrent_no_plot) else getString(
R.string.normal_no_plot
)
}
result_tag?.removeAllViews() result_tag?.removeAllViews()
//result_tag_holder?.visibility = GONE
// result_status.visibility = GONE
d.comingSoon.let { soon ->
result_coming_soon?.isVisible = soon
result_data_holder?.isGone = soon
}
val tags = d.tags
if (tags.isNullOrEmpty()) {
//result_tag_holder?.visibility = GONE //result_tag_holder?.visibility = GONE
// result_status.visibility = GONE } else {
//result_tag_holder?.visibility = VISIBLE
d.comingSoon.let { soon -> for ((index, tag) in tags.withIndex()) {
result_coming_soon?.isVisible = soon val viewBtt = layoutInflater.inflate(R.layout.result_tag, null)
result_data_holder?.isGone = soon val btt = viewBtt.findViewById<MaterialButton>(R.id.result_tag_card)
btt.text = tag
result_tag?.addView(viewBtt, index)
}
}
if (d.type.isMovieType()) {
val hasDownloadSupport = api.hasDownloadSupport
lateFixDownloadButton(true)
result_play_movie?.setOnClickListener {
val card =
currentEpisodes?.firstOrNull() ?: return@setOnClickListener
handleAction(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
} }
val tags = d.tags result_play_movie?.setOnLongClickListener {
if (tags.isNullOrEmpty()) { val card = currentEpisodes?.firstOrNull()
//result_tag_holder?.visibility = GONE ?: return@setOnLongClickListener true
} else { handleAction(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
//result_tag_holder?.visibility = VISIBLE return@setOnLongClickListener true
for ((index, tag) in tags.withIndex()) {
val viewBtt = layoutInflater.inflate(R.layout.result_tag, null)
val btt = viewBtt.findViewById<MaterialButton>(R.id.result_tag_card)
btt.text = tag
result_tag?.addView(viewBtt, index)
}
} }
if (d.type.isMovieType()) { result_download_movie?.setOnLongClickListener {
val hasDownloadSupport = api.hasDownloadSupport val card = currentEpisodes?.firstOrNull()
lateFixDownloadButton(true) ?: return@setOnLongClickListener true
handleAction(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
result_play_movie?.setOnClickListener { return@setOnLongClickListener true
val card = }
currentEpisodes?.firstOrNull() ?: return@setOnClickListener
handleAction(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
}
result_play_movie?.setOnLongClickListener {
val card = currentEpisodes?.firstOrNull()
?: return@setOnLongClickListener true
handleAction(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
return@setOnLongClickListener true
}
result_download_movie?.setOnLongClickListener {
val card = currentEpisodes?.firstOrNull()
?: return@setOnLongClickListener true
handleAction(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
return@setOnLongClickListener true
}
// result_options.setOnClickListener { // result_options.setOnClickListener {
// val card = currentEpisodes?.first() ?: return@setOnClickListener // val card = currentEpisodes?.first() ?: return@setOnClickListener
// handleAction(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) // handleAction(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
// } // }
result_download_movie?.visibility = result_download_movie?.visibility =
if (hasDownloadSupport) VISIBLE else GONE if (hasDownloadSupport) VISIBLE else GONE
if (hasDownloadSupport) { if (hasDownloadSupport) {
val localId = d.getId() val localId = d.getId()
val file = val file =
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
requireContext(), requireContext(),
localId localId
) )
downloadButton?.dispose() downloadButton?.dispose()
downloadButton = EasyDownloadButton() downloadButton = EasyDownloadButton()
downloadButton?.setUpMoreButton( downloadButton?.setUpMoreButton(
file?.fileLength, file?.fileLength,
file?.totalBytes, file?.totalBytes,
result_movie_progress_downloaded, result_movie_progress_downloaded,
result_movie_download_icon, result_movie_download_icon,
result_movie_download_text, result_movie_download_text,
result_movie_download_text_precentage, result_movie_download_text_precentage,
result_download_movie, result_download_movie,
true, true,
VideoDownloadHelper.DownloadEpisodeCached( VideoDownloadHelper.DownloadEpisodeCached(
d.name, d.name,
d.posterUrl, d.posterUrl,
0, 0,
null, null,
localId, localId,
localId, localId,
d.rating, d.rating,
d.plot, d.plot,
System.currentTimeMillis(), System.currentTimeMillis(),
) )
) { downloadClickEvent -> ) { downloadClickEvent ->
if (downloadClickEvent.action == DOWNLOAD_ACTION_DOWNLOAD) { if (downloadClickEvent.action == DOWNLOAD_ACTION_DOWNLOAD) {
currentEpisodes?.firstOrNull()?.let { episode -> currentEpisodes?.firstOrNull()?.let { episode ->
handleAction( handleAction(
EpisodeClickEvent( EpisodeClickEvent(
ACTION_DOWNLOAD_EPISODE, ACTION_DOWNLOAD_EPISODE,
ResultEpisode( ResultEpisode(
d.name, d.name,
d.name, d.name,
null, null,
0, 0,
null, null,
episode.data, episode.data,
d.apiName, d.apiName,
localId, localId,
0, 0,
0L, 0L,
0L, 0L,
null, null,
null, null,
null, null,
d.type, d.type,
localId, localId,
)
) )
) )
}
} else {
handleDownloadClick(
activity,
currentHeaderName,
downloadClickEvent
) )
} }
} } else {
handleDownloadClick(
result_download_movie?.setOnLongClickListener { activity,
val card = currentHeaderName,
currentEpisodes?.firstOrNull() downloadClickEvent
?: return@setOnLongClickListener false
handleAction(EpisodeClickEvent(ACTION_DOWNLOAD_MIRROR, card))
return@setOnLongClickListener true
}
/*downloadButton?.setUpMaterialButton(
file?.fileLength,
file?.totalBytes,
result_movie_progress_downloaded,
result_download_movie,
null, //result_movie_text_progress
VideoDownloadHelper.DownloadEpisodeCached(
d.name,
d.posterUrl,
0,
null,
localId,
localId,
d.rating,
d.plot,
System.currentTimeMillis(),
) )
) { downloadClickEvent -> }
if (downloadClickEvent.action == DOWNLOAD_ACTION_DOWNLOAD) { }
currentEpisodes?.firstOrNull()?.let { episode ->
handleAction( result_download_movie?.setOnLongClickListener {
EpisodeClickEvent( val card =
ACTION_DOWNLOAD_EPISODE, currentEpisodes?.firstOrNull()
ResultEpisode( ?: return@setOnLongClickListener false
d.name, handleAction(EpisodeClickEvent(ACTION_DOWNLOAD_MIRROR, card))
d.name, return@setOnLongClickListener true
null, }
0,
null, /*downloadButton?.setUpMaterialButton(
episode.data, file?.fileLength,
d.apiName, file?.totalBytes,
localId, result_movie_progress_downloaded,
0, result_download_movie,
0L, null, //result_movie_text_progress
0L, VideoDownloadHelper.DownloadEpisodeCached(
null, d.name,
null, d.posterUrl,
null, 0,
d.type, null,
localId, localId,
) localId,
d.rating,
d.plot,
System.currentTimeMillis(),
)
) { downloadClickEvent ->
if (downloadClickEvent.action == DOWNLOAD_ACTION_DOWNLOAD) {
currentEpisodes?.firstOrNull()?.let { episode ->
handleAction(
EpisodeClickEvent(
ACTION_DOWNLOAD_EPISODE,
ResultEpisode(
d.name,
d.name,
null,
0,
null,
episode.data,
d.apiName,
localId,
0,
0L,
0L,
null,
null,
null,
d.type,
localId,
) )
) )
}
} else {
handleDownloadClick(
activity,
currentHeaderName,
downloadClickEvent
) )
} }
}*/ } else {
} handleDownloadClick(
} else { activity,
lateFixDownloadButton(false) currentHeaderName,
} downloadClickEvent
)
context?.getString( }
when (d.type) { }*/
TvType.TvSeries -> R.string.tv_series_singular
TvType.Anime -> R.string.anime_singular
TvType.OVA -> R.string.ova_singular
TvType.AnimeMovie -> R.string.movies_singular
TvType.Cartoon -> R.string.cartoons_singular
TvType.Documentary -> R.string.documentaries_singular
TvType.Movie -> R.string.movies_singular
TvType.Torrent -> R.string.torrent_singular
TvType.AsianDrama -> R.string.asian_drama_singular
}
)?.let {
result_meta_type?.text = it
}
when (d) {
is AnimeLoadResponse -> {
// val preferEnglish = true
//val titleName = (if (preferEnglish) d.engName else d.japName) ?: d.name
val titleName = d.name
result_title.text = titleName
//result_toolbar.title = titleName
}
else -> result_title.text = d.name
} }
} else { } else {
updateVisStatus(1) lateFixDownloadButton(false)
}
context?.getString(
when (d.type) {
TvType.TvSeries -> R.string.tv_series_singular
TvType.Anime -> R.string.anime_singular
TvType.OVA -> R.string.ova_singular
TvType.AnimeMovie -> R.string.movies_singular
TvType.Cartoon -> R.string.cartoons_singular
TvType.Documentary -> R.string.documentaries_singular
TvType.Movie -> R.string.movies_singular
TvType.Torrent -> R.string.torrent_singular
TvType.AsianDrama -> R.string.asian_drama_singular
}
)?.let {
result_meta_type?.text = it
}
when (d) {
is AnimeLoadResponse -> {
// val preferEnglish = true
//val titleName = (if (preferEnglish) d.engName else d.japName) ?: d.name
val titleName = d.name
result_title.text = titleName
//result_toolbar.title = titleName
}
else -> result_title.text = d.name
} }
} }
is Resource.Failure -> { is Resource.Failure -> {
@ -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,22 +12,45 @@
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
android:nextFocusDown="@id/result_bookmark_button"
android:nextFocusRight="@id/result_share"
android:background="?android:attr/selectableItemBackgroundBorderless"
<ImageView android:id="@+id/result_back"
android:nextFocusDown="@id/result_bookmark_button" android:clickable="true"
android:nextFocusRight="@id/result_share" android:focusable="true"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:id="@+id/result_back" android:layout_width="30dp"
android:clickable="true" android:layout_height="30dp"
android:focusable="true" android:layout_gravity="center_vertical|start"
android:src="@drawable/ic_baseline_arrow_back_24"
android:layout_width="30dp" android:contentDescription="@string/go_back"
android:layout_height="30dp" app:tint="?attr/white" />
android:layout_gravity="center_vertical|start" <androidx.recyclerview.widget.RecyclerView
android:src="@drawable/ic_baseline_arrow_back_24" android:paddingStart="10dp"
android:contentDescription="@string/go_back" android:paddingEnd="10dp"
app:tint="?attr/white" /> 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>