Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 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
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 @@ -6,6 +6,7 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.uwetrottmann.tmdb2.Tmdb
import com.uwetrottmann.tmdb2.entities.*
import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem
Expand Down Expand Up @@ -37,6 +38,7 @@ open class TmdbProvider : MainAPI() {
open val disableSeasonZero = true

override val hasMainPage = true
override val hasReviews = true
override val providerType = ProviderType.MetaProvider

// Fuck it, public private api key because github actions won't co-operate.
Expand Down Expand Up @@ -198,6 +200,8 @@ open class TmdbProvider : MainAPI() {
rating = this@toLoadResponse.rating
addTrailer(videos.toTrailers())

reviewsData = tmdb.moviesService().reviews(id, 1, "en-US").awaitResponse().body()?.results?.toJson()

recommendations = (this@toLoadResponse.recommendations
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
addActors(credits?.cast?.toList().toActors())
Expand All @@ -206,6 +210,16 @@ open class TmdbProvider : MainAPI() {
}
}

override suspend fun loadReviews(data: String, page: Int): List<ReviewResponse> {
val reviews = tryParseJson<List<Review>>(data)
return reviews?.map { review ->
newReviewResponse {
this@newReviewResponse.content = review.content
this@newReviewResponse.author = review.author
}
} ?: emptyList()
}

override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {

// SAME AS DISCOVER IT SEEMS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.metaproviders
import android.net.Uri
import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.Actor
import com.lagradost.cloudstream3.ActorData
Expand All @@ -17,6 +16,7 @@ import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainPageRequest
import com.lagradost.cloudstream3.NextAiring
import com.lagradost.cloudstream3.ProviderType
import com.lagradost.cloudstream3.ReviewResponse
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.TvType
Expand All @@ -28,6 +28,7 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.newHomePageResponse
import com.lagradost.cloudstream3.newMovieLoadResponse
import com.lagradost.cloudstream3.newMovieSearchResponse
import com.lagradost.cloudstream3.newReviewResponse
import com.lagradost.cloudstream3.newTvSeriesLoadResponse
import com.lagradost.cloudstream3.newTvSeriesSearchResponse
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
Expand All @@ -38,6 +39,7 @@ import kotlin.math.roundToInt

open class TraktProvider : MainAPI() {
override var name = "Trakt"
override val hasReviews = true
override val hasMainPage = true
override val providerType = ProviderType.MetaProvider
override val supportedTypes = setOf(
Expand Down Expand Up @@ -109,6 +111,22 @@ open class TraktProvider : MainAPI() {
return results
}

override suspend fun loadReviews(data: String, page: Int): List<ReviewResponse> {
return try {
parseJson<List<TraktReview>>(data).filter { it.review == true }.map {
newReviewResponse {
author = it.user?.username
content = it.comment
rating = it.userRating
isSpoiler = it.spoiler == true
addDate(it.createdAt)
}
}
} catch (_: Exception) {
emptyList<ReviewResponse>()
}
}

override suspend fun load(url: String): LoadResponse {

val data = parseJson<Data>(url)
Expand Down Expand Up @@ -185,6 +203,8 @@ open class TraktProvider : MainAPI() {
//posterHeaders
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
this.contentRating = mediaDetails.certification
this.reviewsData =
getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.slug}/comments/newest")
addTrailer(mediaDetails.trailer)
addImdbId(mediaDetails.ids?.imdb)
addTMDbId(mediaDetails.ids?.tmdb.toString())
Expand Down Expand Up @@ -335,6 +355,19 @@ open class TraktProvider : MainAPI() {
val mediaDetails: MediaDetails? = null,
)

data class TraktReview(
@JsonProperty("user") val user: TraktUser? = null,
@JsonProperty("comment") val comment: String? = null,
@JsonProperty("review") val review: Boolean? = null,
@JsonProperty("spoiler") val spoiler: Boolean? = null,
@JsonProperty("user_rating") val userRating: Int? = null,
@JsonProperty("created_at") val createdAt: String? = null
)

data class TraktUser(
@JsonProperty("username") val username: String? = null
)

data class MediaDetails(
@JsonProperty("title") val title: String? = null,
@JsonProperty("year") val year: Int? = null,
Expand Down
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.SearchResponse
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType
Expand Down Expand Up @@ -194,4 +195,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 @@ -29,6 +29,8 @@ import com.google.android.gms.cast.framework.CastButtonFactory
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.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 @@ -41,6 +43,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.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe
Expand Down Expand Up @@ -76,6 +79,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.populateChips
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
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 @@ -423,6 +427,10 @@ open class ResultFragmentPhone : FullScreenPlayer() {
player.handleEvent(CSPlayerEvent.Pause)
}
}

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

Expand Down Expand Up @@ -732,6 +740,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 @@ -773,6 +862,11 @@ open class ResultFragmentPhone : FullScreenPlayer() {
isVisible = true
extend()
}

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

Expand Down
Loading