From 3cb2196e62a2cd435d1d76172d8a9113dff5d573 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Fri, 13 Oct 2023 17:02:12 -0600 Subject: [PATCH] Add favorites (#682) * Add favorites --- .../syncproviders/providers/LocalList.kt | 45 ++++++++++++---- .../ui/result/ResultFragmentPhone.kt | 27 ++++++++++ .../ui/result/ResultFragmentTv.kt | 42 ++++++++++++++- .../ui/result/ResultViewModel2.kt | 47 +++++++++++++++++ .../cloudstream3/utils/DataStoreHelper.kt | 51 +++++++++++++++++++ .../main/res/layout/fragment_result_swipe.xml | 21 +++++++- .../main/res/layout/fragment_result_tv.xml | 15 +++++- app/src/main/res/values/strings.xml | 5 ++ 8 files changed, 238 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt index e6ca9711..71bb2633 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -8,7 +8,9 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.Coroutines.ioWork +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData @@ -69,24 +71,47 @@ class LocalList : SyncAPI { }?.distinctBy { it.first } ?: return null val list = ioWork { - watchStatusIds.groupBy { - it.second.stringRes - }.mapValues { group -> + val isTv = isTvSettings() + + val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate { + // None is not something to display + it.stringRes to emptyList() + } + mapOf( + R.string.favorites_list_name to emptyList() + ) + if (!isTv) { + mapOf( + R.string.subscription_list_name to emptyList(), + ) + } else { + emptyMap() + } + + val watchStatusMap = watchStatusIds.groupBy { it.second.stringRes }.mapValues { group -> group.value.mapNotNull { getBookmarkedData(it.first)?.toLibraryItem(it.first.toString()) } - } + mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull { + } + + val favoritesMap = mapOf(R.string.favorites_list_name to getAllFavorites().mapNotNull { it.toLibraryItem() }) + + // Don't show subscriptions or favorites on TV + val result = if (isTv) { + baseMap + watchStatusMap + favoritesMap + } else { + val subscriptionsMap = mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull { + it.toLibraryItem() + }) + + baseMap + watchStatusMap + subscriptionsMap + favoritesMap + } + + result } - val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate { - // None is not something to display - it.stringRes to emptyList() - } + mapOf(R.string.subscription_list_name to emptyList()) - return SyncAPI.LibraryMetadata( - (baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) }, + list.map { SyncAPI.LibraryList(txt(it.key), it.value) }, setOf( ListSorting.AlphabeticalA, ListSorting.AlphabeticalZ, 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 e5f16dd5..a0d82062 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 @@ -445,6 +445,20 @@ open class ResultFragmentPhone : FullScreenPlayer() { ?: txt(R.string.no_data).asStringNull(context) ?: "" CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) } + resultFavorite.setOnClickListener { + val isFavorite = + viewModel.toggleFavoriteStatus() ?: return@setOnClickListener + + val message = if (isFavorite) { + R.string.favorite_added + } else { + R.string.favorite_removed + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + } mediaRouteButton.apply { val chromecastSupport = api?.hasChromecastSupport == true alpha = if (chromecastSupport) 1f else 0.3f @@ -564,6 +578,19 @@ open class ResultFragmentPhone : FullScreenPlayer() { binding?.resultSubscribe?.setImageResource(drawable) } + observeNullable(viewModel.favoriteStatus) { isFavorite -> + binding?.resultFavorite?.isVisible = isFavorite != null + if (isFavorite == null) return@observeNullable + + val drawable = if (isFavorite) { + R.drawable.ic_baseline_favorite_24 + } else { + R.drawable.ic_baseline_favorite_border_24 + } + + binding?.resultFavorite?.setImageResource(drawable) + } + observe(viewModel.trailers) { trailers -> setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet! } 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 5e4869cc..13734b67 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 @@ -8,6 +8,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.DecelerateInterpolator +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.core.view.isVisible @@ -17,6 +18,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder.updateHasTrailers +import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent @@ -265,6 +267,7 @@ class ResultFragmentTv : Fragment() { resultEpisodesShow.onFocusChangeListener = rightListener resultDescription.onFocusChangeListener = leftListener resultBookmarkButton.onFocusChangeListener = leftListener + resultFavoriteButton.onFocusChangeListener = leftListener resultEpisodesShow.setOnClickListener { // toggle, to make it more touch accessable just in case someone thinks that a // tv layout is better but is using a touch device @@ -283,7 +286,8 @@ class ResultFragmentTv : Fragment() { resultPlaySeries, resultResumeSeries, resultPlayTrailer, - resultBookmarkButton + resultBookmarkButton, + resultFavoriteButton ) for (requestView in views) { if (!requestView.isVisible) continue @@ -424,6 +428,7 @@ class ResultFragmentTv : Fragment() { val aboveCast = listOf( binding?.resultEpisodesShow, binding?.resultBookmarkButton, + binding?.resultFavoriteButton, ).firstOrNull { it?.isVisible == true } @@ -532,6 +537,41 @@ class ResultFragmentTv : Fragment() { } } + observeNullable(viewModel.favoriteStatus) { isFavorite -> + binding?.resultFavoriteButton?.apply { + isVisible = isFavorite != null + if (isFavorite == null) return@observeNullable + + val drawable = if (isFavorite) { + R.drawable.ic_baseline_favorite_24 + } else { + R.drawable.ic_baseline_favorite_border_24 + } + + val text = if (isFavorite) { + R.string.action_remove_from_favorites + } else { + R.string.action_add_to_favorites + } + + setIconResource(drawable) + setText(text) + setOnClickListener { + val isFavorite = viewModel.toggleFavoriteStatus() ?: return@setOnClickListener + + val message = if (isFavorite) { + R.string.favorite_added + } else { + R.string.favorite_removed + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + } + } + } + observeNullable(viewModel.movie) { data -> binding?.apply { resultPlayMovie.isVisible = data is Resource.Success 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 6acf476a..e5ed7b92 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 @@ -51,11 +51,14 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub +import com.lagradost.cloudstream3.utils.DataStoreHelper.getFavoritesData import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos +import com.lagradost.cloudstream3.utils.DataStoreHelper.removeFavoritesData import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub +import com.lagradost.cloudstream3.utils.DataStoreHelper.setFavoritesData import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason import com.lagradost.cloudstream3.utils.UIHelper.navigate @@ -425,6 +428,9 @@ class ResultViewModel2 : ViewModel() { private val _subscribeStatus: MutableLiveData = MutableLiveData(null) val subscribeStatus: LiveData = _subscribeStatus + private val _favoriteStatus: MutableLiveData = MutableLiveData(null) + val favoriteStatus: LiveData = _favoriteStatus + companion object { const val TAG = "RVM2" //private const val EPISODE_RANGE_SIZE = 20 @@ -868,6 +874,40 @@ class ResultViewModel2 : ViewModel() { return !isSubscribed } + /** + * @return true if added to favorites, false if not. Null if not possible to favorite. + **/ + fun toggleFavoriteStatus(): Boolean? { + val isFavorite = _favoriteStatus.value ?: return null + val response = currentResponse ?: return null + + val currentId = response.getId() + + if (isFavorite) { + removeFavoritesData(currentId) + } else { + val current = getFavoritesData(currentId) + + setFavoritesData( + currentId, + DataStoreHelper.FavoritesData( + currentId, + current?.favoritesTime ?: unixTimeMS, + unixTimeMS, + response.name, + response.url, + response.apiName, + response.type, + response.posterUrl, + response.year + ) + ) + } + + _favoriteStatus.postValue(!isFavorite) + return !isFavorite + } + private fun startChromecast( activity: Activity?, result: ResultEpisode, @@ -1750,6 +1790,12 @@ class ResultViewModel2 : ViewModel() { } } + private fun postFavorites(loadResponse: LoadResponse) { + val id = loadResponse.getId() + val isFavorite = getFavoritesData(id) != null + _favoriteStatus.postValue(isFavorite) + } + private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) { if (range == null || indexer == null) { return @@ -1887,6 +1933,7 @@ class ResultViewModel2 : ViewModel() { currentResponse = loadResponse postPage(loadResponse, apiRepository) postSubscription(loadResponse) + postFavorites(loadResponse) if (updateEpisodes) postEpisodes(loadResponse, updateFillers) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 952422a4..4b4157d6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -42,6 +42,7 @@ const val VIDEO_WATCH_STATE = "video_watch_state" const val RESULT_WATCH_STATE = "result_watch_state" const val RESULT_WATCH_STATE_DATA = "result_watch_state_data" const val RESULT_SUBSCRIBED_STATE_DATA = "result_subscribed_state_data" +const val RESULT_FAVORITES_STATE_DATA = "result_favorites_state_data" const val RESULT_RESUME_WATCHING = "result_resume_watching_2" // changed due to id changes const val RESULT_RESUME_WATCHING_OLD = "result_resume_watching" const val RESULT_RESUME_WATCHING_HAS_MIGRATED = "result_resume_watching_migrated" @@ -406,6 +407,33 @@ object DataStoreHelper { } } + data class FavoritesData( + @JsonProperty("id") override var id: Int?, + @JsonProperty("favoritesTime") val favoritesTime: Long, + @JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long, + @JsonProperty("name") override val name: String, + @JsonProperty("url") override val url: String, + @JsonProperty("apiName") override val apiName: String, + @JsonProperty("type") override var type: TvType? = null, + @JsonProperty("posterUrl") override var posterUrl: String?, + @JsonProperty("year") val year: Int?, + @JsonProperty("quality") override var quality: SearchQuality? = null, + @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, + ) : SearchResponse { + fun toLibraryItem(): SyncAPI.LibraryItem? { + return SyncAPI.LibraryItem( + name, + url, + id?.toString() ?: return null, + null, + null, + null, + latestUpdatedTime, + apiName, type, posterUrl, posterHeaders, quality, this.id + ) + } + } + data class ResumeWatchingResult( @JsonProperty("name") override val name: String, @JsonProperty("url") override val url: String, @@ -579,6 +607,29 @@ object DataStoreHelper { return getKey("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA", id.toString()) } + fun getAllFavorites(): List { + return getKeys("$currentAccount/$RESULT_FAVORITES_STATE_DATA")?.mapNotNull { + getKey(it) + } ?: emptyList() + } + + fun removeFavoritesData(id: Int?) { + if (id == null) return + AccountManager.localListApi.requireLibraryRefresh = true + removeKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString()) + } + + fun setFavoritesData(id: Int?, data: FavoritesData) { + if (id == null) return + setKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString(), data) + AccountManager.localListApi.requireLibraryRefresh = true + } + + fun getFavoritesData(id: Int?): FavoritesData? { + if (id == null) return null + return getKey("$currentAccount/$RESULT_FAVORITES_STATE_DATA", id.toString()) + } + fun setViewPos(id: Int?, pos: Long, dur: Long) { if (id == null) return if (dur < 30_000) return // too short diff --git a/app/src/main/res/layout/fragment_result_swipe.xml b/app/src/main/res/layout/fragment_result_swipe.xml index 4e8e3c14..eb2653d0 100644 --- a/app/src/main/res/layout/fragment_result_swipe.xml +++ b/app/src/main/res/layout/fragment_result_swipe.xml @@ -74,7 +74,7 @@ android:nextFocusUp="@id/result_back" android:nextFocusDown="@id/result_description" android:nextFocusLeft="@id/result_add_sync" - android:nextFocusRight="@id/result_share" + android:nextFocusRight="@id/result_favorite" tools:visibility="visible" @@ -89,10 +89,27 @@ android:layout_gravity="end|center_vertical" app:tint="?attr/textColor" /> + + + + tv_no_focus_tag You have already voted + Favorites + %s added to favorites + %s removed from favorites + Add to favorites + Remove from favorites