From 74546d33cf344025641a6fd0960c69c5080bc9ae Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 4 Jul 2026 12:19:12 +0300 Subject: [PATCH 1/6] feat(TV): Sync support implementation --- .../lagradost/cloudstream3/MainActivity.kt | 1 + .../ui/result/ResultFragmentTv.kt | 249 +++++++++++++++++- .../main/res/layout/fragment_result_tv.xml | 154 ++++++++--- app/src/main/res/layout/result_sync.xml | 53 +++- 4 files changed, 409 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 0cbed1809a1..d3f5843dd53 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1262,6 +1262,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa R.id.result_favorite_Button, R.id.result_subscribe_Button, R.id.result_search_Button, + R.id.result_sync_Button, R.id.result_episodes_show_button, ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index cfbacc5d13f..5a9c6c5d16b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -3,19 +3,26 @@ package com.lagradost.cloudstream3.ui.result import android.animation.Animator import android.annotation.SuppressLint import android.app.Dialog +import android.os.Build import android.os.Bundle +import android.text.Editable +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.DecelerateInterpolator +import android.widget.AbsListView +import android.widget.ArrayAdapter import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView +import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView +import com.discord.panels.OverlappingPanelsLayout import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.CommonActivity @@ -23,11 +30,13 @@ import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.Score import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable +import com.lagradost.cloudstream3.mvvm.safe import com.lagradost.cloudstream3.services.SubscriptionWorkManager import com.lagradost.cloudstream3.ui.BaseFragment import com.lagradost.cloudstream3.ui.WatchType @@ -61,17 +70,20 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.populateChips +import com.lagradost.cloudstream3.utils.UIHelper.setListViewHeightBasedOnItems import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat import com.lagradost.cloudstream3.utils.getImageFromDrawable import com.lagradost.cloudstream3.utils.setText import com.lagradost.cloudstream3.utils.setTextHtml import com.lagradost.cloudstream3.utils.txt +import kotlin.math.roundToInt class ResultFragmentTv : BaseFragment( BindingCreator.Inflate(FragmentResultTvBinding::inflate) ) { private lateinit var viewModel: ResultViewModel2 + private lateinit var syncModel: SyncViewModel override fun onDestroyView() { updateUIEvent -= ::updateUI @@ -87,6 +99,9 @@ class ResultFragmentTv : BaseFragment( viewModel = ViewModelProvider(this)[ResultViewModel2::class.java] viewModel.EPISODE_RANGE_SIZE = 50 + + syncModel = ViewModelProvider(this)[SyncViewModel::class.java] + updateUIEvent += ::updateUI return super.onCreateView(inflater, container, savedInstanceState) @@ -94,6 +109,7 @@ class ResultFragmentTv : BaseFragment( private fun updateUI(id: Int?) { viewModel.reloadEpisodes() + syncModel.updateUserData() } private var currentRecommendations: List = emptyList() @@ -250,6 +266,25 @@ class ResultFragmentTv : BaseFragment( } } + private fun toggleSync(show: Boolean) { + binding?.apply { + if (show) { + activity?.attachBackPressedCallback(this@ResultFragmentTv.toString()) { + toggleSync(false) + } + } else { + activity?.detachBackPressedCallback(this@ResultFragmentTv.toString()) + } + syncShadow.fade(show) + resultSyncUi.root.fade(show) + if (syncShadow.isRtl()) { + syncShadowBackground.scaleX = -1f + } else { + syncShadowBackground.scaleX = 1f + } + } + } + override fun fixLayout(view: View) { fixSystemBarsPadding(view, padTop = false) } @@ -270,6 +305,10 @@ class ResultFragmentTv : BaseFragment( storedData.dubStatus, storedData.start ) + + //setUrl(storedData.url) + syncModel.addFromUrl(storedData.url) + // ===== ===== ===== var comingSoon = false @@ -294,7 +333,8 @@ class ResultFragmentTv : BaseFragment( resultBookmarkButton, resultFavoriteButton, resultSubscribeButton, - resultSearchButton + resultSearchButton, + resultSyncButton ) for (requestView in views) { if (!requestView.isVisible) continue @@ -321,6 +361,7 @@ class ResultFragmentTv : BaseFragment( } } + // Please add those buttons to [MainActivity.exceptionButtons] list to stop centering the screen of focus them mapOf( resultPlayMovieButton to resultPlayMovieText, resultPlaySeriesButton to resultPlaySeriesText, @@ -330,13 +371,15 @@ class ResultFragmentTv : BaseFragment( resultFavoriteButton to resultFavoriteText, resultSubscribeButton to resultSubscribeText, resultSearchButton to resultSearchText, - resultEpisodesShowButton to resultEpisodesShowText + resultEpisodesShowButton to resultEpisodesShowText, + resultSyncButton to resultSyncText ).forEach { (button, text) -> button.setOnFocusChangeListener { view, hasFocus -> if (!hasFocus) { text.isSelected = false if (view.id == R.id.result_episodes_show_button) toggleEpisodes(false) + //if (view.id == R.id.result_sync_Button) toggleSync(false) return@setOnFocusChangeListener } @@ -344,11 +387,20 @@ class ResultFragmentTv : BaseFragment( if (button.tag == context?.getString(R.string.tv_no_focus_tag)) { resultFinishLoading.scrollTo(0, 0) } + when (button.id) { R.id.result_episodes_show_button -> { toggleEpisodes(true) } + R.id.result_sync_Button -> { + + toggleSync(false) + toggleEpisodes(false) + + resultSyncButton.nextFocusRightId = if (resultEpisodesShow.isVisible) R.id.result_episodes_show_button else R.id.result_sync_Button + } + else -> { toggleEpisodes(false) } @@ -362,6 +414,13 @@ class ResultFragmentTv : BaseFragment( toggleEpisodes(!episodeHolderTv.isVisible) } + resultSyncButton.setOnClickListener { + // toggle, to make it more touch accessible just in case someone thinks that a + // tv layout is better but is using a touch device + toggleSync(!resultSyncUi.root.isVisible) + resultSyncButton.nextFocusRightId = R.id.result_sync_set_score + } + resultEpisodes.setLinearListLayout( isHorizontal = false, nextUp = FOCUS_SELF, @@ -510,8 +569,11 @@ class ResultFragmentTv : BaseFragment( focusPlayButton() // Stops last button right focus if it is a movie - if (resume.isMovie) - resultSearchButton.nextFocusRightId = R.id.result_search_Button + /*when { + resume.isMovie && resultSyncUi.root.isVisible -> resultSyncButton.nextFocusRightId = R.id.result_sync_Button + resume.isMovie -> resultSyncButton.nextFocusRightId = R.id.result_sync_Button + else -> resultSyncButton.nextFocusRightId = R.id.result_sync_Button + }*/ resultResumeSeriesText.text = when { @@ -566,6 +628,172 @@ class ResultFragmentTv : BaseFragment( } } + observe(syncModel.synced) { list -> + binding.resultSyncUi.resultSyncNames.text = + list.filter { it.isSynced && it.hasAccount }.joinToString { it.name } + + val newList = list.filter { it.isSynced && it.hasAccount } + + binding.resultSync.isVisible = newList.isNotEmpty() + } + + var currentSyncProgress = 0 + + fun setSyncMaxEpisodes(totalEpisodes: Int?) { + binding.resultSyncUi.resultSyncEpisodes.max = (totalEpisodes ?: 0) * 1000 + + safe { + val ctx = binding.resultSyncUi.resultSyncEpisodes.context + binding.resultSyncUi.resultSyncMaxEpisodes.text = + totalEpisodes?.let { episodes -> + ctx?.getString(R.string.sync_total_episodes_some)?.format(episodes) + } ?: run { + ctx?.getString(R.string.sync_total_episodes_none) + } + } + } + + observe(syncModel.metadata) { meta -> + when (meta) { + is Resource.Success -> { + val d = meta.value + binding.resultSyncUi.resultSyncEpisodes.progress = currentSyncProgress * 1000 + setSyncMaxEpisodes(d.totalEpisodes) + + viewModel.setMeta(d, syncModel.getSyncs()) + + Log.d("King", "metadata") + } + + is Resource.Loading -> { + binding.resultSyncUi.resultSyncMaxEpisodes.text = + binding.resultSyncUi.resultSyncMaxEpisodes.context?.getString(R.string.sync_total_episodes_none) + } + + else -> {} + } + } + + observe(syncModel.userData) { status -> + var closed = false + binding.resultSyncUi.apply { + when (status) { + is Resource.Failure -> { + resultSyncLoadingShimmer.stopShimmer() + resultSyncLoadingShimmer.isVisible = false + resultSyncHolder.isVisible = false + closed = true + } + + is Resource.Loading -> { + resultSyncLoadingShimmer.startShimmer() + resultSyncLoadingShimmer.isVisible = true + resultSyncHolder.isVisible = false + } + + is Resource.Success -> { + resultSyncLoadingShimmer.stopShimmer() + resultSyncLoadingShimmer.isVisible = false + resultSyncHolder.isVisible = true + + val d = status.value + val desiredScore = d.score?.toFloat(1) ?: 0.0f + val totalSteps = (resultSyncRating.valueTo / resultSyncRating.stepSize) + val desiredStep = (totalSteps * desiredScore).roundToInt() + resultSyncRating.value = desiredStep * resultSyncRating.stepSize + + resultSyncCheck.setItemChecked(d.status.internalId + 1, true) + val watchedEpisodes = d.watchedEpisodes ?: 0 + currentSyncProgress = watchedEpisodes + + d.maxEpisodes?.let { + // don't directly call it because we don't want to override metadata observe + setSyncMaxEpisodes(it) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + resultSyncEpisodes.setProgress(watchedEpisodes * 1000, true) + } else { + resultSyncEpisodes.progress = watchedEpisodes * 1000 + } + resultSyncCurrentEpisodes.text = + Editable.Factory.getInstance()?.newEditable(watchedEpisodes.toString()) + safe { // format might fail + val text = d.score?.toFloat(10)?.roundToInt()?.let { + context?.getString(R.string.sync_score_format)?.format(it) + } ?: "?" + resultSyncScoreText.text = text + } + + //resultSyncAddEpisode.requestFocus() + Log.d("King", "userData") + //resultSyncSetScore.requestFocus() + } + + null -> { + closed = false + } + } + } + //binding.resultOverlappingPanels.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) + } + + context?.let { ctx -> + val arrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) + /* + -1 -> None + 0 -> Watching + 1 -> Completed + 2 -> OnHold + 3 -> Dropped + 4 -> PlanToWatch + 5 -> ReWatching + */ + val items = listOf( + R.string.none, + R.string.type_watching, + R.string.type_completed, + R.string.type_on_hold, + R.string.type_dropped, + R.string.type_plan_to_watch, + R.string.type_re_watching + ).map { ctx.getString(it) } + arrayAdapter.addAll(items) + + binding.resultSyncUi.apply { + resultSyncCheck.choiceMode = AbsListView.CHOICE_MODE_SINGLE + resultSyncCheck.adapter = arrayAdapter + setListViewHeightBasedOnItems(resultSyncCheck) + + resultSyncCheck.setOnItemClickListener { _, _, which, _ -> + syncModel.setStatus(which - 1) + } + + resultSyncRating.addOnChangeListener { it, value, fromUser -> + if (fromUser) syncModel.setScore(Score.from(value, it.valueTo.roundToInt())) + } + + resultSyncAddEpisode.setOnClickListener { + syncModel.setEpisodesDelta(1) + } + + resultSyncSubEpisode.setOnClickListener { + syncModel.setEpisodesDelta(-1) + } + + resultSyncCurrentEpisodes.doOnTextChanged { text, _, before, count -> + if (count == before) return@doOnTextChanged + text?.toString()?.toIntOrNull()?.let { ep -> + syncModel.setEpisodes(ep) + } + } + } + } + + binding.resultSyncUi.resultSyncSetScore.setOnClickListener { + syncModel.publishUserData() + } + observe(viewModel.watchStatus) { watchType -> binding.apply { resultBookmarkText.setText(watchType.stringRes) @@ -695,7 +923,11 @@ class ResultFragmentTv : BaseFragment( } else resultPlayMovieButton.requestFocus() // Stops last button right focus - resultSearchButton.nextFocusRightId = R.id.result_search_Button + /*if (binding.resultSyncUi.root.isVisible) { + resultSyncButton.nextFocusRightId = R.id.result_sync_set_score + } else { + resultSyncButton.nextFocusRightId = R.id.result_sync_Button + }*/ } } } @@ -940,6 +1172,13 @@ class ResultFragmentTv : BaseFragment( resultMetaContentRating.width = 0 } + if (syncModel.addSyncs(d.syncData)) { + syncModel.updateMetaAndUser() + syncModel.updateSynced() + } else { + syncModel.addFromUrl(d.url) + } + resultSearchButton.setOnClickListener { QuickSearchFragment.pushSearch(activity, d.title) } diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 3dff8f0d535..303aedd976a 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -38,24 +38,25 @@ https://developer.android.com/design/ui/tv/samples/jet-fit tools:src="@drawable/profile_bg_dark_blue" /> + android:layout_marginBottom="-1dp" + android:src="@drawable/background_shadow" + tools:ignore="contentDescription"/> + android:contentDescription="@null" + android:maxWidth="220dp" + android:maxHeight="72dp" + android:scaleType="fitStart" /> @@ -238,43 +239,44 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:id="@+id/result_resume_progress_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:orientation="horizontal" - android:layout_marginTop="10dp" android:visibility="gone" tools:visibility="visible"> + + tools:text="S1E1 Episode 1" + tools:visibility="visible" /> - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + style="@style/ContinueWatchingPlayUnderlayProgress" + tools:ignore="contentDescription"/> @@ -513,6 +516,30 @@ https://developer.android.com/design/ui/tv/samples/jet-fit + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/result_sync.xml b/app/src/main/res/layout/result_sync.xml index af883633b1c..0a97f01cdee 100644 --- a/app/src/main/res/layout/result_sync.xml +++ b/app/src/main/res/layout/result_sync.xml @@ -9,8 +9,11 @@ @@ -24,9 +27,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="10dp" - android:text="MyAnimeList, AniList" android:textSize="16sp" - android:textStyle="bold" /> + android:textStyle="bold" + tools:text="MyAnimeList, AniList" /> + app:tint="?attr/textColor" + tools:ignore="contentDescription" /> + app:tint="?attr/textColor" + tools:ignore="contentDescription" /> + tools:text="7/10" /> @@ -159,6 +186,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_rowWeight="1" + android:focusable="true" tools:listitem="@layout/sort_bottom_single_choice" /> @@ -190,8 +223,8 @@ app:shimmer_base_alpha="0.2" app:shimmer_duration="@integer/loading_time" app:shimmer_highlight_alpha="0.3" - tools:visibility="gone" - tools:ignore="MissingClass"> + tools:ignore="MissingClass" + tools:visibility="gone"> Date: Sat, 4 Jul 2026 12:22:23 +0300 Subject: [PATCH 2/6] Reformat code --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 5a9c6c5d16b..5eeb8499d3c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -398,7 +398,8 @@ class ResultFragmentTv : BaseFragment( toggleSync(false) toggleEpisodes(false) - resultSyncButton.nextFocusRightId = if (resultEpisodesShow.isVisible) R.id.result_episodes_show_button else R.id.result_sync_Button + resultSyncButton.nextFocusRightId = + if (resultEpisodesShow.isVisible) R.id.result_episodes_show_button else R.id.result_sync_Button } else -> { @@ -698,7 +699,7 @@ class ResultFragmentTv : BaseFragment( val d = status.value val desiredScore = d.score?.toFloat(1) ?: 0.0f - val totalSteps = (resultSyncRating.valueTo / resultSyncRating.stepSize) + val totalSteps = resultSyncRating.valueTo / resultSyncRating.stepSize val desiredStep = (totalSteps * desiredScore).roundToInt() resultSyncRating.value = desiredStep * resultSyncRating.stepSize From c59c66154e63062c598b6ebfd611562ffa05d167 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 4 Jul 2026 13:19:08 +0300 Subject: [PATCH 3/6] Control visibility based on type --- .../ui/result/ResultFragmentTv.kt | 31 +++++++------------ app/src/main/res/layout/result_sync.xml | 5 ++- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 5eeb8499d3c..04a71b649b8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -22,6 +22,7 @@ import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView +import androidx.transition.Visibility import com.discord.panels.OverlappingPanelsLayout import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton @@ -662,8 +663,6 @@ class ResultFragmentTv : BaseFragment( setSyncMaxEpisodes(d.totalEpisodes) viewModel.setMeta(d, syncModel.getSyncs()) - - Log.d("King", "metadata") } is Resource.Loading -> { @@ -704,6 +703,14 @@ class ResultFragmentTv : BaseFragment( resultSyncRating.value = desiredStep * resultSyncRating.stepSize resultSyncCheck.setItemChecked(d.status.internalId + 1, true) + + safe { // format might fail + val text = d.score?.toFloat(10)?.roundToInt()?.let { + context?.getString(R.string.sync_score_format)?.format(it) + } ?: "?" + resultSyncScoreText.text = text + } + val watchedEpisodes = d.watchedEpisodes ?: 0 currentSyncProgress = watchedEpisodes @@ -719,16 +726,6 @@ class ResultFragmentTv : BaseFragment( } resultSyncCurrentEpisodes.text = Editable.Factory.getInstance()?.newEditable(watchedEpisodes.toString()) - safe { // format might fail - val text = d.score?.toFloat(10)?.roundToInt()?.let { - context?.getString(R.string.sync_score_format)?.format(it) - } ?: "?" - resultSyncScoreText.text = text - } - - //resultSyncAddEpisode.requestFocus() - Log.d("King", "userData") - //resultSyncSetScore.requestFocus() } null -> { @@ -736,7 +733,6 @@ class ResultFragmentTv : BaseFragment( } } } - //binding.resultOverlappingPanels.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) } context?.let { ctx -> @@ -922,13 +918,6 @@ class ResultFragmentTv : BaseFragment( if (comingSoon) { resultBookmarkButton.requestFocus() } else resultPlayMovieButton.requestFocus() - - // Stops last button right focus - /*if (binding.resultSyncUi.root.isVisible) { - resultSyncButton.nextFocusRightId = R.id.result_sync_set_score - } else { - resultSyncButton.nextFocusRightId = R.id.result_sync_Button - }*/ } } } @@ -1080,6 +1069,8 @@ class ResultFragmentTv : BaseFragment( hasLoadedEpisodesOnce = true resultPlaySeries.isVisible = resultResumeSeries.isGone && !comingSoon resultEpisodesShow.isVisible = true && !comingSoon + resultSyncUi.resultSyncEpisodeHolder.isVisible = true + resultSyncUi.resultSyncEpisodes.isVisible = true resultPlaySeriesButton.requestFocus() } } diff --git a/app/src/main/res/layout/result_sync.xml b/app/src/main/res/layout/result_sync.xml index 0a97f01cdee..987380fc84f 100644 --- a/app/src/main/res/layout/result_sync.xml +++ b/app/src/main/res/layout/result_sync.xml @@ -32,10 +32,12 @@ tools:text="MyAnimeList, AniList" /> + android:visibility="gone" + tools:visibility="visible"> Date: Sat, 4 Jul 2026 13:23:39 +0300 Subject: [PATCH 4/6] Fix for phone --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt | 2 ++ 1 file changed, 2 insertions(+) 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 38b24b26517..a3c189bd304 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 @@ -811,6 +811,8 @@ open class ResultFragmentPhone : BaseFragment( // no failure? resultEpisodeLoading.isVisible = episodes is Resource.Loading resultEpisodes.isVisible = episodes is Resource.Success + syncBinding?.resultSyncEpisodeHolder?.isVisible = true + syncBinding?.resultSyncEpisodes?.isVisible = true resultBatchDownloadButton.isVisible = episodes is Resource.Success && episodes.value.isNotEmpty() From c911650fbfa6aeed9f9c70792a9ef9cf12ef09e0 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sun, 5 Jul 2026 11:47:32 +0300 Subject: [PATCH 5/6] Some Fixes --- .../ui/result/ResultFragmentTv.kt | 52 ++++++++++++--- app/src/main/res/layout/result_sync.xml | 64 ++++++++++++++++++- app/src/main/res/values/strings.xml | 2 + 3 files changed, 105 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 04a71b649b8..463c9717b35 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -400,7 +400,7 @@ class ResultFragmentTv : BaseFragment( toggleEpisodes(false) resultSyncButton.nextFocusRightId = - if (resultEpisodesShow.isVisible) R.id.result_episodes_show_button else R.id.result_sync_Button + if (resultEpisodesShow.isVisible) R.id.result_episodes_show_button else R.id.result_sync_episodes_slider } else -> { @@ -420,7 +420,7 @@ class ResultFragmentTv : BaseFragment( // toggle, to make it more touch accessible just in case someone thinks that a // tv layout is better but is using a touch device toggleSync(!resultSyncUi.root.isVisible) - resultSyncButton.nextFocusRightId = R.id.result_sync_set_score + resultSyncButton.nextFocusRightId = R.id.result_sync_episodes_slider } resultEpisodes.setLinearListLayout( @@ -570,12 +570,6 @@ class ResultFragmentTv : BaseFragment( } focusPlayButton() - // Stops last button right focus if it is a movie - /*when { - resume.isMovie && resultSyncUi.root.isVisible -> resultSyncButton.nextFocusRightId = R.id.result_sync_Button - resume.isMovie -> resultSyncButton.nextFocusRightId = R.id.result_sync_Button - else -> resultSyncButton.nextFocusRightId = R.id.result_sync_Button - }*/ resultResumeSeriesText.text = when { @@ -655,6 +649,33 @@ class ResultFragmentTv : BaseFragment( } } + fun setSyncEpisodes(currentEpisodes: Int?, totalEpisodes: Int?) { + + Log.d("King", "current: $currentEpisodes, total: $totalEpisodes") + + safe { + val ctx = binding.resultSyncUi.resultSyncEpisodes.context + + binding.resultSyncUi.resultSyncEpisodesText.text = totalEpisodes?.let { episodes -> + + if (episodes != 0) { + binding.resultSyncUi.resultSyncEpisodesSlider.value = (currentEpisodes ?: 0).toFloat() + binding.resultSyncUi.resultSyncEpisodesSlider.valueTo = episodes.toFloat() + } else { + binding.resultSyncUi.resultSyncEpisodesHolder.isVisible = false + } + + ctx?.getString(R.string.sync_total_episodes_format)?.format(currentEpisodes, episodes) + + } ?: run { + binding.resultSyncUi.resultSyncEpisodesHolder.isVisible = false + binding.resultSyncUi.resultSyncSetScore.isClickable = false + + ctx?.getString(R.string.sync_total_episodes_format_none) + } + } + } + observe(syncModel.metadata) { meta -> when (meta) { is Resource.Success -> { @@ -714,9 +735,12 @@ class ResultFragmentTv : BaseFragment( val watchedEpisodes = d.watchedEpisodes ?: 0 currentSyncProgress = watchedEpisodes + Log.d("King", "Watched: $watchedEpisodes, Max: ${d.maxEpisodes}") + d.maxEpisodes?.let { // don't directly call it because we don't want to override metadata observe setSyncMaxEpisodes(it) + setSyncEpisodes(watchedEpisodes,it) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -770,6 +794,10 @@ class ResultFragmentTv : BaseFragment( if (fromUser) syncModel.setScore(Score.from(value, it.valueTo.roundToInt())) } + resultSyncEpisodesSlider.addOnChangeListener { it, value, fromUser -> + if (fromUser) syncModel.setEpisodes(value.roundToInt()) + } + resultSyncAddEpisode.setOnClickListener { syncModel.setEpisodesDelta(1) } @@ -1069,8 +1097,12 @@ class ResultFragmentTv : BaseFragment( hasLoadedEpisodesOnce = true resultPlaySeries.isVisible = resultResumeSeries.isGone && !comingSoon resultEpisodesShow.isVisible = true && !comingSoon - resultSyncUi.resultSyncEpisodeHolder.isVisible = true - resultSyncUi.resultSyncEpisodes.isVisible = true + //resultSyncUi.resultSyncEpisodeHolder.isVisible = true + //resultSyncUi.resultSyncEpisodes.isVisible = true + resultSyncUi.resultSyncEpisodesCounterHolder.isVisible = true + resultSyncUi.resultSyncEpisodesSlider.isVisible = true + + resultPlaySeriesButton.requestFocus() } } diff --git a/app/src/main/res/layout/result_sync.xml b/app/src/main/res/layout/result_sync.xml index 987380fc84f..6242cc2780e 100644 --- a/app/src/main/res/layout/result_sync.xml +++ b/app/src/main/res/layout/result_sync.xml @@ -37,7 +37,7 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:visibility="gone" - tools:visibility="visible"> + tools:visibility="gone"> + + + + + + + + + + + + + + + tools:visibility="gone" /> %d / 10 /?? /%d + %d/%d + %d/?? %s authenticated Could not log in at %s From 0ffd6dbf8191e981d7e1d67bd6ad0266c6c5211f Mon Sep 17 00:00:00 2001 From: KingLucius Date: Mon, 6 Jul 2026 22:04:36 +0300 Subject: [PATCH 6/6] Last touches --- .../ui/result/ResultFragmentPhone.kt | 65 +++++++------- .../ui/result/ResultFragmentTv.kt | 84 ++++-------------- app/src/main/res/layout/result_sync.xml | 88 +------------------ app/src/main/res/values/strings.xml | 1 + 4 files changed, 48 insertions(+), 190 deletions(-) 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 a3c189bd304..7dde8c34cfd 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 @@ -811,8 +811,9 @@ open class ResultFragmentPhone : BaseFragment( // no failure? resultEpisodeLoading.isVisible = episodes is Resource.Loading resultEpisodes.isVisible = episodes is Resource.Success - syncBinding?.resultSyncEpisodeHolder?.isVisible = true - syncBinding?.resultSyncEpisodes?.isVisible = true + syncBinding?.resultSyncEpisodesHolder?.isVisible = true + syncBinding?.resultSyncEpisodesCounterHolder?.isVisible = true + syncBinding?.resultSyncEpisodesSlider?.isVisible = true resultBatchDownloadButton.isVisible = episodes is Resource.Success && episodes.value.isNotEmpty() @@ -1116,32 +1117,44 @@ open class ResultFragmentPhone : BaseFragment( var currentSyncProgress = 0 - fun setSyncMaxEpisodes(totalEpisodes: Int?) { - syncBinding?.resultSyncEpisodes?.max = (totalEpisodes ?: 0) * 1000 + + fun setSyncMaxEpisodes(currentEpisodes: Int?, totalEpisodes: Int?) { safe { - val ctx = syncBinding?.resultSyncEpisodes?.context - syncBinding?.resultSyncMaxEpisodes?.text = - totalEpisodes?.let { episodes -> - ctx?.getString(R.string.sync_total_episodes_some)?.format(episodes) - } ?: run { - ctx?.getString(R.string.sync_total_episodes_none) + //val ctx = syncBinding?.resultSyncEpisodes?.context + + syncBinding?.resultSyncEpisodesText?.text = totalEpisodes?.let { episodes -> + + if (episodes != 0) { + syncBinding?.resultSyncEpisodesSlider?.value = (currentEpisodes ?: 0).toFloat() + syncBinding?.resultSyncEpisodesSlider?.valueTo = episodes.toFloat() + } else { + syncBinding?.resultSyncEpisodesHolder?.isVisible = false } + + context?.getString(R.string.sync_total_episodes_format)?.format(currentEpisodes, episodes) + + } ?: run { + syncBinding?.resultSyncEpisodesHolder?.isVisible = false + syncBinding?.resultSyncSetScore?.isClickable = false + + context?.getString(R.string.sync_total_episodes_format_none) + } } } + observe(syncModel.metadata) { meta -> when (meta) { is Resource.Success -> { val d = meta.value - syncBinding?.resultSyncEpisodes?.progress = currentSyncProgress * 1000 - setSyncMaxEpisodes(d.totalEpisodes) + setSyncMaxEpisodes(null, d.totalEpisodes) viewModel.setMeta(d, syncModel.getSyncs()) } is Resource.Loading -> { - syncBinding?.resultSyncMaxEpisodes?.text = - syncBinding?.resultSyncMaxEpisodes?.context?.getString(R.string.sync_total_episodes_none) + syncBinding?.resultSyncEpisodesText?.text = + context?.getString(R.string.sync_total_episodes_none) } else -> {} @@ -1183,16 +1196,9 @@ open class ResultFragmentPhone : BaseFragment( d.maxEpisodes?.let { // don't directly call it because we don't want to override metadata observe - setSyncMaxEpisodes(it) + setSyncMaxEpisodes(watchedEpisodes, it) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - resultSyncEpisodes.setProgress(watchedEpisodes * 1000, true) - } else { - resultSyncEpisodes.progress = watchedEpisodes * 1000 - } - resultSyncCurrentEpisodes.text = - Editable.Factory.getInstance()?.newEditable(watchedEpisodes.toString()) safe { // format might fail val text = d.score?.toFloat(10)?.roundToInt()?.let { context?.getString(R.string.sync_score_format)?.format(it) @@ -1245,19 +1251,8 @@ open class ResultFragmentPhone : BaseFragment( if (fromUser) syncModel.setScore(Score.from(value, it.valueTo.roundToInt())) } - resultSyncAddEpisode.setOnClickListener { - syncModel.setEpisodesDelta(1) - } - - resultSyncSubEpisode.setOnClickListener { - syncModel.setEpisodesDelta(-1) - } - - resultSyncCurrentEpisodes.doOnTextChanged { text, _, before, count -> - if (count == before) return@doOnTextChanged - text?.toString()?.toIntOrNull()?.let { ep -> - syncModel.setEpisodes(ep) - } + resultSyncEpisodesSlider.addOnChangeListener { it, value, fromUser -> + if (fromUser) syncModel.setEpisodes(value.roundToInt()) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 463c9717b35..ab96cc27257 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -12,6 +12,7 @@ import android.view.View import android.view.ViewGroup import android.view.animation.DecelerateInterpolator import android.widget.AbsListView +import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -314,7 +315,6 @@ class ResultFragmentTv : BaseFragment( var comingSoon = false binding.apply { - //episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f // parallax on background resultFinishLoading.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { view, _, scrollY, _, oldScrollY -> @@ -400,7 +400,12 @@ class ResultFragmentTv : BaseFragment( toggleEpisodes(false) resultSyncButton.nextFocusRightId = - if (resultEpisodesShow.isVisible) R.id.result_episodes_show_button else R.id.result_sync_episodes_slider + if (resultEpisodesShow.isVisible) + R.id.result_episodes_show_button + else if (binding.resultSyncUi.root.isVisible) + R.id.result_sync_rating + else + R.id.result_sync_Button } else -> { @@ -420,7 +425,7 @@ class ResultFragmentTv : BaseFragment( // toggle, to make it more touch accessible just in case someone thinks that a // tv layout is better but is using a touch device toggleSync(!resultSyncUi.root.isVisible) - resultSyncButton.nextFocusRightId = R.id.result_sync_episodes_slider + resultSyncButton.nextFocusRightId = R.id.result_sync_rating } resultEpisodes.setLinearListLayout( @@ -445,11 +450,6 @@ class ResultFragmentTv : BaseFragment( nextDown = FOCUS_SELF, ) - /*.layoutManager = - LinearListLayout(resultEpisodes.context, resultEpisodes.isRtl()).apply { - setVertical() - }*/ - resultReloadConnectionerror.setOnClickListener { viewModel.load( activity, @@ -473,9 +473,6 @@ class ResultFragmentTv : BaseFragment( // Always escape focus if (hasFocus) binding.resultBookmarkButton.requestFocus() } - //resultBack.setOnClickListener { - // activity?.popCurrentPage() - //} resultRecommendationsList.spanCount = 8 resultRecommendationsList.setRecycledViewPool(SearchAdapter.sharedPool) @@ -633,28 +630,9 @@ class ResultFragmentTv : BaseFragment( binding.resultSync.isVisible = newList.isNotEmpty() } - var currentSyncProgress = 0 - - fun setSyncMaxEpisodes(totalEpisodes: Int?) { - binding.resultSyncUi.resultSyncEpisodes.max = (totalEpisodes ?: 0) * 1000 + fun setSyncMaxEpisodes(currentEpisodes: Int?, totalEpisodes: Int?) { safe { - val ctx = binding.resultSyncUi.resultSyncEpisodes.context - binding.resultSyncUi.resultSyncMaxEpisodes.text = - totalEpisodes?.let { episodes -> - ctx?.getString(R.string.sync_total_episodes_some)?.format(episodes) - } ?: run { - ctx?.getString(R.string.sync_total_episodes_none) - } - } - } - - fun setSyncEpisodes(currentEpisodes: Int?, totalEpisodes: Int?) { - - Log.d("King", "current: $currentEpisodes, total: $totalEpisodes") - - safe { - val ctx = binding.resultSyncUi.resultSyncEpisodes.context binding.resultSyncUi.resultSyncEpisodesText.text = totalEpisodes?.let { episodes -> @@ -665,13 +643,13 @@ class ResultFragmentTv : BaseFragment( binding.resultSyncUi.resultSyncEpisodesHolder.isVisible = false } - ctx?.getString(R.string.sync_total_episodes_format)?.format(currentEpisodes, episodes) + context?.getString(R.string.sync_total_episodes_format)?.format(currentEpisodes, episodes) } ?: run { binding.resultSyncUi.resultSyncEpisodesHolder.isVisible = false binding.resultSyncUi.resultSyncSetScore.isClickable = false - ctx?.getString(R.string.sync_total_episodes_format_none) + context?.getString(R.string.sync_total_episodes_format_none) } } } @@ -680,15 +658,14 @@ class ResultFragmentTv : BaseFragment( when (meta) { is Resource.Success -> { val d = meta.value - binding.resultSyncUi.resultSyncEpisodes.progress = currentSyncProgress * 1000 - setSyncMaxEpisodes(d.totalEpisodes) + setSyncMaxEpisodes(null, d.totalEpisodes) viewModel.setMeta(d, syncModel.getSyncs()) } is Resource.Loading -> { - binding.resultSyncUi.resultSyncMaxEpisodes.text = - binding.resultSyncUi.resultSyncMaxEpisodes.context?.getString(R.string.sync_total_episodes_none) + binding.resultSyncUi.resultSyncEpisodesText.text = + context?.getString(R.string.sync_total_episodes_all_none) } else -> {} @@ -733,23 +710,11 @@ class ResultFragmentTv : BaseFragment( } val watchedEpisodes = d.watchedEpisodes ?: 0 - currentSyncProgress = watchedEpisodes - - Log.d("King", "Watched: $watchedEpisodes, Max: ${d.maxEpisodes}") d.maxEpisodes?.let { // don't directly call it because we don't want to override metadata observe - setSyncMaxEpisodes(it) - setSyncEpisodes(watchedEpisodes,it) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - resultSyncEpisodes.setProgress(watchedEpisodes * 1000, true) - } else { - resultSyncEpisodes.progress = watchedEpisodes * 1000 + setSyncMaxEpisodes(watchedEpisodes,it) } - resultSyncCurrentEpisodes.text = - Editable.Factory.getInstance()?.newEditable(watchedEpisodes.toString()) } null -> { @@ -761,6 +726,7 @@ class ResultFragmentTv : BaseFragment( context?.let { ctx -> val arrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) + /* -1 -> None 0 -> Watching @@ -797,21 +763,6 @@ class ResultFragmentTv : BaseFragment( resultSyncEpisodesSlider.addOnChangeListener { it, value, fromUser -> if (fromUser) syncModel.setEpisodes(value.roundToInt()) } - - resultSyncAddEpisode.setOnClickListener { - syncModel.setEpisodesDelta(1) - } - - resultSyncSubEpisode.setOnClickListener { - syncModel.setEpisodesDelta(-1) - } - - resultSyncCurrentEpisodes.doOnTextChanged { text, _, before, count -> - if (count == before) return@doOnTextChanged - text?.toString()?.toIntOrNull()?.let { ep -> - syncModel.setEpisodes(ep) - } - } } } @@ -1097,8 +1048,6 @@ class ResultFragmentTv : BaseFragment( hasLoadedEpisodesOnce = true resultPlaySeries.isVisible = resultResumeSeries.isGone && !comingSoon resultEpisodesShow.isVisible = true && !comingSoon - //resultSyncUi.resultSyncEpisodeHolder.isVisible = true - //resultSyncUi.resultSyncEpisodes.isVisible = true resultSyncUi.resultSyncEpisodesCounterHolder.isVisible = true resultSyncUi.resultSyncEpisodesSlider.isVisible = true @@ -1107,7 +1056,6 @@ class ResultFragmentTv : BaseFragment( } } - (resultEpisodes.adapter as? EpisodeAdapter)?.submitList(episodes.value) } } diff --git a/app/src/main/res/layout/result_sync.xml b/app/src/main/res/layout/result_sync.xml index 6242cc2780e..38352783015 100644 --- a/app/src/main/res/layout/result_sync.xml +++ b/app/src/main/res/layout/result_sync.xml @@ -31,78 +31,6 @@ android:textStyle="bold" tools:text="MyAnimeList, AniList" /> - - - - - - - - - - - - - - Rated %d / 10 /?? + "??/??" /%d %d/%d %d/??