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..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() 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..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 @@ -3,19 +3,27 @@ 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 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 +31,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 +71,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 +100,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 +110,7 @@ class ResultFragmentTv : BaseFragment( private fun updateUI(id: Int?) { viewModel.reloadEpisodes() + syncModel.updateUserData() } private var currentRecommendations: List = emptyList() @@ -250,6 +267,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 +306,10 @@ class ResultFragmentTv : BaseFragment( storedData.dubStatus, storedData.start ) + + //setUrl(storedData.url) + syncModel.addFromUrl(storedData.url) + // ===== ===== ===== var comingSoon = false @@ -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,21 @@ 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 +416,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 +571,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 +630,167 @@ 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()) + } + + 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) + + 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 + + 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()) + } + + 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())) + } + + 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) @@ -693,9 +918,6 @@ class ResultFragmentTv : BaseFragment( if (comingSoon) { resultBookmarkButton.requestFocus() } else resultPlayMovieButton.requestFocus() - - // Stops last button right focus - resultSearchButton.nextFocusRightId = R.id.result_search_Button } } } @@ -847,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() } } @@ -940,6 +1164,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..987380fc84f 100644 --- a/app/src/main/res/layout/result_sync.xml +++ b/app/src/main/res/layout/result_sync.xml @@ -9,8 +9,11 @@ @@ -24,25 +27,35 @@ 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:visibility="gone" + tools:visibility="visible"> + app:tint="?attr/textColor" + tools:ignore="contentDescription" /> + app:tint="?attr/textColor" + tools:ignore="contentDescription" /> + tools:text="7/10" /> @@ -159,6 +189,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 +226,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">