diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 93a79689e5..42b831a542 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -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 @@ -215,4 +216,10 @@ class APIRepository(val api: MainAPI) { return false } } + + suspend fun loadReviews(data: String, page: Int): Resource> { + return safeApiCall { + api.loadReviews(data, page) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 78ad2a6bfc..74d552e0a7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -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 @@ -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 } } @@ -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 } } @@ -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() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 676d268375..64b024d8d3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -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 @@ -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 @@ -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 @@ -457,6 +461,10 @@ open class ResultFragmentPhone : FullScreenPlayer() { player.handleEvent(CSPlayerEvent.Pause) } } + + if (viewModel.isInReviews()) { + binding?.reviewsFab?.alpha = scrollY / 50.toPx.toFloat() + } }) } @@ -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 @@ -853,6 +942,11 @@ open class ResultFragmentPhone : FullScreenPlayer() { isVisible = true extend() } + + reviewsFab.setOnClickListener { + resultReviews.smoothScrollToPosition(0) + resultScroll.smoothScrollTo(0, 0) + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 9cdbb736ad..17b234f486 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -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 @@ -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( @@ -514,6 +517,72 @@ class ResultViewModel2 : ViewModel() { private val _favoriteStatus: MutableLiveData = MutableLiveData(null) val favoriteStatus: LiveData = _favoriteStatus + val currentTabIndex: MutableLiveData by lazy { + MutableLiveData(0) + } + + val currentTabPosition: MutableLiveData by lazy { + MutableLiveData(0) + } + + val reviews: MutableLiveData>> by lazy { + MutableLiveData>>() + } + private var currentReviews: ArrayList = arrayListOf() + + private val reviewPage: MutableLiveData by lazy { + MutableLiveData(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 @@ -2694,6 +2763,7 @@ class ResultViewModel2 : ViewModel() { override var posterHeaders: Map? = 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 @@ -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) @@ -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( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ReviewAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ReviewAdapter.kt new file mode 100644 index 0000000000..2bfe89bd82 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ReviewAdapter.kt @@ -0,0 +1,252 @@ +package com.lagradost.cloudstream3.ui.result + +import android.content.Context +import android.view.LayoutInflater +import android.view.View.TEXT_ALIGNMENT_CENTER +import android.view.ViewGroup +import android.widget.RelativeLayout +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipDrawable +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.RatingFormat +import com.lagradost.cloudstream3.ReviewResponse +import com.lagradost.cloudstream3.databinding.ResultReviewBinding +import com.lagradost.cloudstream3.utils.AppContextUtils.html +import com.lagradost.cloudstream3.utils.ImageLoader.loadImage +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import com.lagradost.cloudstream3.utils.UIHelper.toPx +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class ReviewAdapter : + ListAdapter(DiffCallback()) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReviewAdapterHolder { + val binding = + ResultReviewBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ReviewAdapterHolder(binding) + } + + override fun onBindViewHolder(holder: ReviewAdapterHolder, position: Int) { + val currentItem = getItem(position) + holder.bind(currentItem) + } + + class ReviewAdapterHolder( + private val binding: ResultReviewBinding + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(card: ReviewResponse) { + val context = binding.root.context ?: return + + binding.apply { + setReviewText(card) + setReviewTitle(card) + handleSpoiler(card) + setReviewDate(card) + setReviewAuthor(card) + loadReviewAvatar(card) + setReviewTags(card, context) + handleReviewClick(card, context) + } + } + + private fun ResultReviewBinding.setReviewText(card: ReviewResponse) { + reviewContent.text = card.content?.let { + (if (it.length > 300) it.take(300) + "..." else it).html() + } ?: "" + } + + private fun ResultReviewBinding.setReviewTitle(card: ReviewResponse) { + reviewTitle.text = card.title ?: "" + reviewTitle.isVisible = reviewTitle.text.isNotEmpty() + } + + private fun ResultReviewBinding.handleSpoiler(card: ReviewResponse) { + if (card.isSpoiler) { + var isSpoilerRevealed = false + reviewContent.isVisible = false + reviewTitle.isVisible = false + spoilerButton.isVisible = true + spoilerButton.setOnClickListener { + isSpoilerRevealed = !isSpoilerRevealed + if (isSpoilerRevealed) { + reviewContent.isVisible = true + reviewTitle.isVisible = true + } else { + reviewContent.isVisible = false + reviewTitle.isVisible = false + } + } + } + } + + private fun ResultReviewBinding.setReviewDate(card: ReviewResponse) { + card.timestamp?.let { + reviewTime.text = SimpleDateFormat.getDateInstance( + SimpleDateFormat.LONG, + Locale.getDefault() + ).format(Date(it)) + } + } + + private fun ResultReviewBinding.setReviewAuthor(card: ReviewResponse) { + reviewAuthor.text = card.author + + if (card.timestamp == null) { + val params = reviewAuthor.layoutParams as? RelativeLayout.LayoutParams + params?.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE) + reviewAuthor.layoutParams = params + } + } + + private fun ResultReviewBinding.loadReviewAvatar(card: ReviewResponse) { + if (card.avatarUrl == null) return + reviewAvatar.loadImage(card.avatarUrl, card.avatarHeaders) + } + + private fun ResultReviewBinding.setReviewTags( + card: ReviewResponse, + context: Context + ) { + // When we don't have many tags we do this differently to make it look a bit nicer. + val rating = card.rating + val tagCount = (card.ratings?.count() ?: 0) + if (rating != null) 1 else 0 + val view = when { + tagCount == 1 && rating != null -> { + reviewTagsSingle.isVisible = true + reviewTagsScroll.isVisible = false + reviewTagsSingle.text = context.getString(R.string.overall_rating_format).format( + rating.formatRating(context, card.ratingFormat) + ) + return + } + tagCount == 2 -> { + reviewTagsSmallScroll.isVisible = true + reviewTagsScroll.isVisible = false + reviewTagsSmall + } + else -> { + reviewTagsScroll.isVisible = true + reviewTagsSmallScroll.isVisible = false + reviewTags + } + } + + view.removeAllViews() + val chips = mutableListOf() + + card.rating?.let { + val chip = createChip( + context, + context.getString(R.string.overall_rating_format).format( + it.formatRating(context, card.ratingFormat) + ), + R.style.ChipReviewAlt, + R.attr.primaryGrayBackground + ) + view.addView(chip) + chips.add(chip) + } + + card.ratings?.forEach { (rating, category) -> + val chip = createChip( + context, + "$category ${rating.formatRating(context, card.ratingFormat)}", + R.style.ChipReview, + R.attr.textColor + ) + view.addView(chip) + chips.add(chip) + } + + if (view == reviewTags) { + // We want to make sure all chips are the same size + reviewTags.viewTreeObserver.addOnDrawListener { + val minWidth = 140.toPx + val maxWidth = chips.maxOfOrNull { it.width } ?: 0 + if (minWidth < maxWidth) { + chips.forEach { it.width = maxWidth } + } + + // Continue + true + } + } + } + + private fun createChip( + context: Context, + text: String, + style: Int, + textColor: Int + ): Chip { + val chipDrawable = ChipDrawable.createFromAttributes(context, null, 0, style) + return Chip(context).apply { + setText(text) + setChipDrawable(chipDrawable) + setTextColor(context.colorFromAttribute(textColor)) + + isChecked = false + isCheckable = false + isFocusable = false + isClickable = false + + textAlignment = TEXT_ALIGNMENT_CENTER + minWidth = 140.toPx + } + } + + private fun Number.formatRating( + context: Context, + format: RatingFormat + ): String { + return when (format) { + RatingFormat.STAR -> "$this★" + RatingFormat.OUT_OF_10 -> "$this/10" + RatingFormat.OUT_OF_100 -> "$this/100" + RatingFormat.PERCENT -> "${this.toInt()}%" + RatingFormat.POSITIVE_NEGATIVE -> { + if (this.toInt() > 0) { + context.getString(R.string.positive_review) + } else context.getString(R.string.negative_review) + } + } + } + + private fun ResultReviewBinding.handleReviewClick( + card: ReviewResponse, + context: Context + ) { + reviewContent.setOnClickListener { + val builder = AlertDialog.Builder(context).apply { + setMessage(card.content.html()) + setTitle(card.title ?: card.author ?: card.rating?.let { + context.getString(R.string.overall_rating_format).format( + it.formatRating(context, card.ratingFormat) + ) + }) + } + builder.show() + } + } + } + + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ReviewResponse, + newItem: ReviewResponse + ): Boolean = oldItem == newItem + + override fun areContentsTheSame( + oldItem: ReviewResponse, + newItem: ReviewResponse + ): Boolean = oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 91c8a2fc1f..b3429eb6af 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -1,8 +1,17 @@ package com.lagradost.cloudstream3.utils -import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.AnimeLoadResponse +import com.lagradost.cloudstream3.LiveStreamLoadResponse +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainPageRequest +import com.lagradost.cloudstream3.MovieLoadResponse +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.TvSeriesLoadResponse +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.mvvm.logError -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.junit.Assert import kotlin.random.Random @@ -84,18 +93,62 @@ object TestingUtils { } val homePageList = homepage?.items?.flatMap { it.list } ?: emptyList() return TestResultList(homePageList) - } catch (e: Throwable) { - when (e) { + } catch (t: Throwable) { + when (t) { is NotImplementedError -> { Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") } is CancellationException -> { - throw e + throw t } else -> { - e.message?.let { logger.warn("Exception thrown when loading homepage: \"$it\"") } + t.message?.let { logger.warn("Exception thrown when loading homepage: \"$it\"") } + } + } + } + } + return TestResult.Pass + } + + @Throws(AssertionError::class, CancellationException::class) + private suspend fun testReviews( + api: MainAPI, + result: SearchResponse, + logger: Logger + ): TestResult { + if (api.hasReviews) { + try { + val loadResponse = api.load(result.url) + + if (loadResponse == null) { + logger.error("Returned null loadResponse on ${result.url} on ${api.name}") + return TestResult.Fail + } + + val reviews = api.loadReviews(loadResponse.reviewsData ?: loadResponse.url, 1) + + if (reviews.isEmpty()) { + logger.log("Api ${api.name} returned an empty reviews list on ${result.url}") + } else logger.log("Api ${api.name} loaded ${reviews.count()} reviews successfully.") + + // We don't need to fail if no reviews are actually + // returned since some may just not have any, + // but we do at least log above. + return TestResult.Pass + } catch (t: Throwable) { + when (t) { + is NotImplementedError -> { + Assert.fail("Provider marked as hasReviews, while in reality is has not been implemented") + } + + is CancellationException -> { + throw t + } + + else -> { + t.message?.let { logger.warn("Exception thrown when loading reviews: \"$it\"") } } } } @@ -113,13 +166,13 @@ object TestingUtils { try { logger.log("Searching for: $query") api.search(query, 1)?.items?.takeIf { it.isNotEmpty() } - } catch (e: Throwable) { - if (e is NotImplementedError) { + } catch (t: Throwable) { + if (t is NotImplementedError) { Assert.fail("Provider has not implemented search()") - } else if (e is CancellationException) { - throw e + } else if (t is CancellationException) { + throw t } - logError(e) + logError(t) null } } @@ -214,11 +267,11 @@ object TestingUtils { // } // return TestResult(validResults) - } catch (e: Throwable) { - if (e is NotImplementedError) { + } catch (t: Throwable) { + if (t is NotImplementedError) { Assert.fail("Provider has not implemented load()") } - throw e + throw t } } @@ -247,15 +300,15 @@ object TestingUtils { } else { Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") } - } catch (e: Throwable) { - when (e) { + } catch (t: Throwable) { + when (t) { is NotImplementedError -> { Assert.fail("Provider has not implemented loadLinks()") } else -> { logger.error("Failed link loading on ${api.name} using data: $url") - throw e + throw t } } } @@ -307,6 +360,10 @@ object TestingUtils { } } + // Test Reviews + val reviewsTest = testReviews(api, searchResults.results.first(), logger) + Assert.assertTrue("Reviews failed to load", reviewsTest.success) + if (success) { logger.log("Success ${api.name}") TestResultProvider(true, logger.getRawLog(), null) @@ -314,8 +371,8 @@ object TestingUtils { logger.error("Link loading failed") TestResultProvider(false, logger.getRawLog(), null) } - } catch (e: Throwable) { - TestResultProvider(false, logger.getRawLog(), e) + } catch (t: Throwable) { + TestResultProvider(false, logger.getRawLog(), t) } callback.invoke(api, result) } diff --git a/app/src/main/res/drawable/ic_baseline_arrow_upward_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_upward_24.xml new file mode 100644 index 0000000000..6d317b1315 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_arrow_upward_24.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_avatar_24.xml b/app/src/main/res/drawable/ic_baseline_avatar_24.xml new file mode 100644 index 0000000000..2887613fb8 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_avatar_24.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/white_card.xml b/app/src/main/res/drawable/white_card.xml new file mode 100644 index 0000000000..a70b8d7927 --- /dev/null +++ b/app/src/main/res/drawable/white_card.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 5edfac43bc..10df3edc2a 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -396,6 +396,92 @@ tools:text="121min" /> + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + --> + + + + +<<<<<< reviews-api + android:descendantFocusability="afterDescendants" + android:fadingEdge="horizontal" + android:focusable="false" + android:focusableInTouchMode="false" + android:nextFocusUp="@id/result_bookmark_Button" + android:nextFocusDown="@id/result_play_movie" + android:orientation="horizontal" + android:paddingTop="5dp" + android:requiresFadingEdge="horizontal" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:itemCount="2" + tools:listitem="@layout/cast_item" + tools:visibility="visible" /> + + + +====== >>>>> master + + + + + + + + + + + - android:id="@+id/result_download_movie" - style="@style/BlackButton" + + + + + - android:clickable="true" - android:focusable="true" - android:layout_width="match_parent" />--> + - + + + + - - - + android:layout_height="wrap_content" + app:download_layout="@layout/download_button_layout" /> + + + + - + +====== android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> @@ -769,11 +906,24 @@ android:visibility="gone" tools:text="Season 1" tools:visibility="visible" /> +>>>>>> master >>>>> master tools:visibility="visible" /> +====== android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:drawableEnd="@drawable/ic_baseline_keyboard_arrow_down_24" @@ -819,11 +982,19 @@ android:text="Sort" android:visibility="gone" tools:visibility="visible" /> +>>>>>> master + +====== android:layout_gravity="center_vertical" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" @@ -834,85 +1005,201 @@ android:textStyle="normal" tools:text="8 Episodes" /> +>>>>>> master - - - - + + android:layout_height="20dp" + android:layout_gravity="end|center_vertical" + android:layout_weight="1" + android:indeterminate="false" + android:max="100" + android:progress="0" + android:progressBackgroundTint="?attr/colorPrimary" + android:visibility="visible" + tools:progress="50" + tools:visibility="visible" /> + android:paddingStart="10dp" + android:textColor="?attr/grayTextColor" + tools:ignore="RtlSymmetry" + tools:text="69m\nremaining" /> - + android:layout_height="wrap_content" + android:orientation="vertical"> + android:layout_marginBottom="10dp" + android:gravity="center_vertical" + android:orientation="horizontal"> + + + + + + + + + + - + + - + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="?attr/grayTextColor" + android:textSize="17sp" + android:textStyle="normal" + tools:text="Episode 1022 will be released in" /> - + - - + android:layout_marginTop="15dp" + android:orientation="vertical" + app:shimmer_auto_start="true" + app:shimmer_base_alpha="0.2" + app:shimmer_duration="@integer/loading_time" + app:shimmer_highlight_alpha="0.3" + tools:visibility="visible"> + + - + + + + + + + + + + + + + @@ -942,4 +1229,4 @@ android:contentDescription="@string/search_poster_descript"/> --> - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result_swipe.xml b/app/src/main/res/layout/fragment_result_swipe.xml index 6ef9568118..d127a02b29 100644 --- a/app/src/main/res/layout/fragment_result_swipe.xml +++ b/app/src/main/res/layout/fragment_result_swipe.xml @@ -243,6 +243,25 @@ style="@style/ExtendedFloatingActionButton" tools:ignore="ContentDescription" /> + + - + \ No newline at end of file diff --git a/app/src/main/res/layout/loading_review.xml b/app/src/main/res/layout/loading_review.xml new file mode 100644 index 0000000000..6d2f8e5631 --- /dev/null +++ b/app/src/main/res/layout/loading_review.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_review.xml b/app/src/main/res/layout/result_review.xml new file mode 100644 index 0000000000..ffbefdd8fc --- /dev/null +++ b/app/src/main/res/layout/result_review.xml @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ba5599f06..12ee57f052 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -853,6 +853,16 @@ software_decoding_key2 Software decoding Software decoding enables the player to play video files not supported by your phone, but may cause laggy or unstable playback on high resolution + Details + Reviews + Avatar + Overall %s + Hide Spoiler + Reveal Spoiler + To top + Positive + Negative + There are no reviews to display. Volume has exceeded 100% Slide up again to go beyond 100% Update Plugins diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 97accf404d..dc41bdb6e6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -89,6 +89,22 @@ @color/amoledModeLight + + + +