Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ae054ef
Add reviews support to API
Luna712 Feb 1, 2025
50f9774
Fix
Luna712 Feb 1, 2025
c455f44
Push initial UI
Luna712 Feb 1, 2025
a9b3ca9
Fix
Luna712 Feb 1, 2025
33c6fb5
Add paging
Luna712 Feb 1, 2025
0a95146
Update test method
Luna712 Feb 1, 2025
f937df4
Update rating format
Luna712 Feb 1, 2025
67c7b87
Some cleanup
Luna712 Feb 1, 2025
54b0e56
Some fixes
Luna712 Feb 2, 2025
3019033
Update date format, add helper methods, and remove subList logic
Luna712 Feb 2, 2025
266d95d
Add isSpoiler to API
Luna712 Feb 2, 2025
9b6da80
Add spoiler UI
Luna712 Feb 2, 2025
bfd4431
Add back to top and fix scroll bug
Luna712 Feb 2, 2025
1f5fa9b
Add RatingFormat and documentation
Luna712 Feb 2, 2025
a990e2f
Cleanup adapter
Luna712 Feb 2, 2025
fe48035
Improve spoiler and chip UI
Luna712 Feb 2, 2025
e4e1879
Add reviewsData
Luna712 Feb 2, 2025
623ee2c
Rename and fix
Luna712 Feb 2, 2025
7212b50
Add reviews to tmdb API
Luna712 Feb 3, 2025
0d7e1bd
Add support to trakt
Luna712 Feb 3, 2025
55b8947
Sort by newest
Luna712 Feb 3, 2025
5b116f4
parse html
Luna712 Feb 3, 2025
7bd30e0
Catch errors
Luna712 Feb 3, 2025
aef9121
Add no reviews UI
Luna712 Feb 3, 2025
0db68b3
Cleanup and fix
Luna712 Feb 3, 2025
7c53a85
Rounded spoiler button
Luna712 Feb 3, 2025
148d6f3
Make chips the same size
Luna712 Feb 3, 2025
f469b01
New UI
Luna712 Feb 4, 2025
2e7714a
Fix alignment
Luna712 Feb 4, 2025
e26f115
Revert (was fine before maybe)
Luna712 Feb 4, 2025
132745a
different tag positions depending on how many
Luna712 Feb 4, 2025
651d3f0
Add default avatar image
Luna712 Feb 4, 2025
ef3e97e
Fix some bugs and add review tests to TestingUtils
Luna712 Feb 4, 2025
534c6c6
Fix alignment
Luna712 Feb 4, 2025
46a6255
Use smaller button
Luna712 Feb 4, 2025
01e4f06
Alignment
Luna712 Feb 4, 2025
d3fe98e
Fix
Luna712 Oct 25, 2025
e716e77
Merge branch 'master' into reviews-api
Luna712 Oct 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.MainPageRequest
import com.lagradost.cloudstream3.ReviewResponse
import com.lagradost.cloudstream3.SearchResponseList
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType
Expand Down Expand Up @@ -215,4 +216,10 @@ class APIRepository(val api: MainAPI) {
return false
}
}

suspend fun loadReviews(data: String, page: Int): Resource<List<ReviewResponse>> {
return safeApiCall {
api.loadReviews(data, page)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.util.AttributeSet
import android.view.View
import androidx.core.view.children
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs

Expand All @@ -20,7 +21,7 @@ class GrdLayoutManager(val context: Context, spanCount: Int) :
val fromPos = getPosition(focused)
val nextPos = getNextViewPos(fromPos, focusDirection)
findViewByPosition(nextPos)
} catch (e: Exception) {
} catch (_: Exception) {
null
}
}
Expand Down Expand Up @@ -51,7 +52,7 @@ class GrdLayoutManager(val context: Context, spanCount: Int) :
val fromPos = getPosition(focused)
val nextPos = getNextViewPos(fromPos, direction)
findViewByPosition(nextPos)
} catch (e: Exception) {
} catch (_: Exception) {
null
}
}
Expand Down Expand Up @@ -190,4 +191,30 @@ class MaxRecyclerView(ctx: Context, attrs: AttributeSet) : RecyclerView(ctx, att
}
super.onChildAttachedToWindow(child)
}
}

class ScrollableRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : RecyclerView(context, attrs) {

var loadMoreListener: (() -> Unit)? = null
private var isLoading = false

private val layoutManager
get() = super.layoutManager as? LinearLayoutManager

override fun onScrolled(dx: Int, dy: Int) {
super.onScrolled(dx, dy)

if (dy <= 0 || isLoading) return // Only trigger when scrolling down and not already loading

val lm = layoutManager ?: return
val totalItemCount = adapter?.itemCount ?: 0
val lastVisibleItemPosition = lm.findLastVisibleItemPosition()

if (lastVisibleItemPosition >= totalItemCount - 1) {
isLoading = true
loadMoreListener?.invoke()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.tabs.TabLayout
import com.lagradost.api.Log
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.DubStatus
Expand All @@ -45,6 +47,7 @@ import com.lagradost.cloudstream3.databinding.FragmentResultSwipeBinding
import com.lagradost.cloudstream3.databinding.ResultRecommendationsBinding
import com.lagradost.cloudstream3.databinding.ResultSyncBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugException
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
Expand Down Expand Up @@ -83,6 +86,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.populateChips
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.setListViewHeightBasedOnItems
import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.getImageFromDrawable
import com.lagradost.cloudstream3.utils.setText
Expand Down Expand Up @@ -457,6 +461,10 @@ open class ResultFragmentPhone : FullScreenPlayer() {
player.handleEvent(CSPlayerEvent.Pause)
}
}

if (viewModel.isInReviews()) {
binding?.reviewsFab?.alpha = scrollY / 50.toPx.toFloat()
}
})
}

Expand Down Expand Up @@ -804,6 +812,87 @@ open class ResultFragmentPhone : FullScreenPlayer() {

populateChips(resultTag, d.tags)

resultTabs.removeAllTabs()
resultTabs.isVisible = false
if (api?.hasReviews == true) {
resultTabs.isVisible = true
resultTabs.addTab(resultTabs.newTab().setText(R.string.details).setId(0))
resultTabs.addTab(
resultTabs.newTab().setText(R.string.reviews).setId(1)
)
}

val target = viewModel.currentTabIndex.value
if (target != null) {
resultTabs.getTabAt(target)?.let { new ->
resultTabs.selectTab(new)
}
}

val reviewAdapter = ReviewAdapter()

resultReviews.adapter = reviewAdapter
resultReviews.loadMoreListener = { viewModel.loadMoreReviews() }

resultReviews.setLinearListLayout(isHorizontal = false)

observe(viewModel.reviews) { reviews ->
when (reviews) {
is Resource.Success -> {
resultviewReviewsLoading.isVisible = false
resultviewReviewsLoadingShimmer.startShimmer()
resultReviews.isVisible = true
resultNoReviews.isVisible = reviews.value.isEmpty()
reviewAdapter.submitList(reviews.value)
}

is Resource.Loading -> {
resultviewReviewsLoadingShimmer.stopShimmer()
resultviewReviewsLoading.isVisible = true
resultReviews.isVisible = false
}

is Resource.Failure -> {
debugException { "This should never happen." }
}
}
}

resultTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
Log.i("ResultFragment", "addOnTabSelectedListener ${resultTabs.selectedTabPosition}")
viewModel.switchTab(tab?.id, resultTabs.selectedTabPosition)

tab?.id?.let { tabId ->
val observer = PanelsChildGestureRegionObserver.Provider.get()
when (tabId) {
0 -> observer.unregister(resultReviews)
1 -> observer.register(resultReviews)
}
}
}

override fun onTabUnselected(tab: TabLayout.Tab?) {}

override fun onTabReselected(tab: TabLayout.Tab?) {}
})

observe(viewModel.currentTabIndex) { pos ->
binding.apply {
resultDescription.isVisible = 0 == pos
resultDetailsholder.isVisible = 0 == pos
binding?.resultBookmarkFab?.isVisible = 0 == pos
binding?.reviewsFab?.isVisible = 1 == pos
resultReviewsholder.isVisible = 1 == pos
}
}

observe(viewModel.currentTabPosition) { pos ->
if (resultTabs.selectedTabPosition != pos) {
resultTabs.selectTab(resultTabs.getTabAt(pos))
}
}

resultComingSoon.isVisible = d.comingSoon
resultDataHolder.isGone = d.comingSoon

Expand Down Expand Up @@ -853,6 +942,11 @@ open class ResultFragmentPhone : FullScreenPlayer() {
isVisible = true
extend()
}

reviewsFab.setOnClickListener {
resultReviews.smoothScrollToPosition(0)
resultScroll.smoothScrollTo(0, 0)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.lagradost.cloudstream3.actions.AlwaysAskAction
import com.lagradost.cloudstream3.actions.VideoClickActionHolder
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.getApiFromUrlNull
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.CommonActivity.activity
Expand Down Expand Up @@ -89,6 +90,8 @@ import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToEnglishLanguageN
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

/** This starts at 1 */
data class EpisodeRange(
Expand Down Expand Up @@ -514,6 +517,72 @@ class ResultViewModel2 : ViewModel() {
private val _favoriteStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
val favoriteStatus: LiveData<Boolean?> = _favoriteStatus

val currentTabIndex: MutableLiveData<Int> by lazy {
MutableLiveData<Int>(0)
}

val currentTabPosition: MutableLiveData<Int> by lazy {
MutableLiveData<Int>(0)
}

val reviews: MutableLiveData<Resource<ArrayList<ReviewResponse>>> by lazy {
MutableLiveData<Resource<ArrayList<ReviewResponse>>>()
}
private var currentReviews: ArrayList<ReviewResponse> = arrayListOf()

private val reviewPage: MutableLiveData<Int> by lazy {
MutableLiveData<Int>(0)
}

private val loadMoreReviewsMutex = Mutex()
private fun loadMoreReviews(data: String) {
viewModelScope.launch {
if (loadMoreReviewsMutex.isLocked) return@launch
loadMoreReviewsMutex.withLock {
val loadPage = (reviewPage.value ?: 0) + 1
if (loadPage == 1) {
reviews.postValue(Resource.Loading())
}
val repo = currentRepo ?: return@launch
when (val response = repo.loadReviews(data, loadPage)) {
is Resource.Success -> {
val moreReviews = response.value
currentReviews.addAll(moreReviews)

reviews.postValue(Resource.Success(currentReviews))
reviewPage.postValue(loadPage)
}

else -> {}
}
}
}
}

private val loadMutex = Mutex()
fun loadMoreReviews(verify: Boolean = true) = viewModelScope.launch {
loadMutex.withLock {
if (!hasLoaded()) return@launch
if (verify && currentTabIndex.value == 0) return@launch
loadMoreReviews(
currentResponse?.reviewsData ?: currentResponse?.url ?: return@launch
)
}
}

fun switchTab(index: Int?, position: Int?) {
val newPos = index ?: return
currentTabPosition.postValue(position ?: return)
currentTabIndex.postValue(newPos)
if (newPos == 1 && currentReviews.isEmpty()) {
loadMoreReviews(verify = false)
}
}

fun isInReviews(): Boolean {
return currentTabIndex.value == 1
}

companion object {
const val TAG = "RVM2"
//private const val EPISODE_RANGE_SIZE = 20
Expand Down Expand Up @@ -2694,6 +2763,7 @@ class ResultViewModel2 : ViewModel() {
override var posterHeaders: Map<String, String>? = null,
override var backgroundPosterUrl: String? = null,
override var contentRating: String? = null,
override var reviewsData: String? = null,
override var uniqueUrl: String = url,
val id: Int?,
) : LoadResponse
Expand All @@ -2703,7 +2773,7 @@ class ResultViewModel2 : ViewModel() {
_page.postValue(Resource.Loading(url))
_episodes.postValue(Resource.Loading())
val api =
APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull(
getApiFromNameNull(searchResponse.apiName) ?: getApiFromUrlNull(
searchResponse.url
) ?: APIRepository.noneApi
val repo = APIRepository(api)
Expand Down Expand Up @@ -2754,7 +2824,7 @@ class ResultViewModel2 : ViewModel() {
currentShowFillers = showFillers

// set api
val api = APIHolder.getApiFromNameNull(apiName) ?: APIHolder.getApiFromUrlNull(url)
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
if (api == null) {
_page.postValue(
Resource.Failure(
Expand Down
Loading
Loading