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/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 38b24b26517..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,6 +811,9 @@ open class ResultFragmentPhone : BaseFragment( // no failure? resultEpisodeLoading.isVisible = episodes is Resource.Loading resultEpisodes.isVisible = episodes is Resource.Success + syncBinding?.resultSyncEpisodesHolder?.isVisible = true + syncBinding?.resultSyncEpisodesCounterHolder?.isVisible = true + syncBinding?.resultSyncEpisodesSlider?.isVisible = true resultBatchDownloadButton.isVisible = episodes is Resource.Success && episodes.value.isNotEmpty() @@ -1114,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 -> {} @@ -1181,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) @@ -1243,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 cfbacc5d13f..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 @@ -3,19 +3,28 @@ 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.AdapterView +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 androidx.transition.Visibility +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 +32,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 +72,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 +101,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 +111,7 @@ class ResultFragmentTv : BaseFragment( private fun updateUI(id: Int?) { viewModel.reloadEpisodes() + syncModel.updateUserData() } private var currentRecommendations: List = emptyList() @@ -250,6 +268,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,11 +307,14 @@ class ResultFragmentTv : BaseFragment( storedData.dubStatus, storedData.start ) + + //setUrl(storedData.url) + syncModel.addFromUrl(storedData.url) + // ===== ===== ===== 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 -> @@ -294,7 +334,8 @@ class ResultFragmentTv : BaseFragment( resultBookmarkButton, resultFavoriteButton, resultSubscribeButton, - resultSearchButton + resultSearchButton, + resultSyncButton ) for (requestView in views) { if (!requestView.isVisible) continue @@ -321,6 +362,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 +372,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 +388,26 @@ 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 if (binding.resultSyncUi.root.isVisible) + R.id.result_sync_rating + else + R.id.result_sync_Button + } + else -> { toggleEpisodes(false) } @@ -362,6 +421,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_rating + } + resultEpisodes.setLinearListLayout( isHorizontal = false, nextUp = FOCUS_SELF, @@ -384,11 +450,6 @@ class ResultFragmentTv : BaseFragment( nextDown = FOCUS_SELF, ) - /*.layoutManager = - LinearListLayout(resultEpisodes.context, resultEpisodes.isRtl()).apply { - setVertical() - }*/ - resultReloadConnectionerror.setOnClickListener { viewModel.load( activity, @@ -412,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) @@ -509,9 +567,6 @@ class ResultFragmentTv : BaseFragment( } focusPlayButton() - // Stops last button right focus if it is a movie - if (resume.isMovie) - resultSearchButton.nextFocusRightId = R.id.result_search_Button resultResumeSeriesText.text = when { @@ -566,6 +621,155 @@ 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() + } + + fun setSyncMaxEpisodes(currentEpisodes: Int?, totalEpisodes: Int?) { + + safe { + + 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 + } + + context?.getString(R.string.sync_total_episodes_format)?.format(currentEpisodes, episodes) + + } ?: run { + binding.resultSyncUi.resultSyncEpisodesHolder.isVisible = false + binding.resultSyncUi.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 + setSyncMaxEpisodes(null, d.totalEpisodes) + + viewModel.setMeta(d, syncModel.getSyncs()) + } + + is Resource.Loading -> { + binding.resultSyncUi.resultSyncEpisodesText.text = + context?.getString(R.string.sync_total_episodes_all_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) + + 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 + + d.maxEpisodes?.let { + // don't directly call it because we don't want to override metadata observe + setSyncMaxEpisodes(watchedEpisodes,it) + } + } + + null -> { + closed = false + } + } + } + } + + 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())) + } + + resultSyncEpisodesSlider.addOnChangeListener { it, value, fromUser -> + if (fromUser) syncModel.setEpisodes(value.roundToInt()) + } + } + } + + binding.resultSyncUi.resultSyncSetScore.setOnClickListener { + syncModel.publishUserData() + } + observe(viewModel.watchStatus) { watchType -> binding.apply { resultBookmarkText.setText(watchType.stringRes) @@ -693,9 +897,6 @@ class ResultFragmentTv : BaseFragment( if (comingSoon) { resultBookmarkButton.requestFocus() } else resultPlayMovieButton.requestFocus() - - // Stops last button right focus - resultSearchButton.nextFocusRightId = R.id.result_search_Button } } } @@ -847,11 +1048,14 @@ class ResultFragmentTv : BaseFragment( hasLoadedEpisodesOnce = true resultPlaySeries.isVisible = resultResumeSeries.isGone && !comingSoon resultEpisodesShow.isVisible = true && !comingSoon + resultSyncUi.resultSyncEpisodesCounterHolder.isVisible = true + resultSyncUi.resultSyncEpisodesSlider.isVisible = true + + resultPlaySeriesButton.requestFocus() } } - (resultEpisodes.adapter as? EpisodeAdapter)?.submitList(episodes.value) } } @@ -940,6 +1144,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..38352783015 100644 --- a/app/src/main/res/layout/result_sync.xml +++ b/app/src/main/res/layout/result_sync.xml @@ -9,8 +9,11 @@ @@ -24,72 +27,66 @@ 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" /> - - + android:orientation="vertical" > - - - + + + + + + + + android:layout_marginStart="-5dp" + android:layout_marginEnd="-5dp" + android:focusable="true" + android:nextFocusUp="@id/result_sync_episodes_slider" + android:nextFocusDown="@id/result_sync_rating" + android:stepSize="1" + android:value="4" + android:valueFrom="0" + android:valueTo="10" + app:labelStyle="@style/BlackLabel" + app:thumbHeight="20dp" + app:thumbRadius="10dp" + app:tickVisible="false" + android:visibility="gone" + tools:visibility="visible"/> - - - + tools:text="7/10" /> @@ -159,6 +159,9 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_rowWeight="1" + android:focusable="true" + android:nextFocusLeft="@id/result_sync_Button" + android:nextFocusRight="@id/result_sync_set_score" tools:listitem="@layout/sort_bottom_single_choice" /> @@ -190,8 +198,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"> Rated %d / 10 /?? + "??/??" /%d + %d/%d + %d/?? %s authenticated Could not log in at %s