Add favorites (#682)

* Add favorites
This commit is contained in:
Luna712 2023-10-13 17:02:12 -06:00 committed by GitHub
parent 7e9d1ded7f
commit 3cb2196e62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 238 additions and 15 deletions

View File

@ -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<SyncAPI.LibraryItem>()
} + 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<SyncAPI.LibraryItem>()
} + 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,

View File

@ -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!
}

View File

@ -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

View File

@ -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<Boolean?> = MutableLiveData(null)
val subscribeStatus: LiveData<Boolean?> = _subscribeStatus
private val _favoriteStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
val favoriteStatus: LiveData<Boolean?> = _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)
}

View File

@ -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<String, String>? = 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<FavoritesData> {
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

View File

@ -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" />
<ImageView
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_description"
android:nextFocusLeft="@id/result_subscribe"
android:nextFocusRight="@id/result_share"
android:id="@+id/result_favorite"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_margin="5dp"
android:elevation="10dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_favorite_border_24"
android:layout_gravity="end|center_vertical"
app:tint="?attr/textColor" />
<ImageView
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_description"
android:nextFocusLeft="@id/result_subscribe"
android:nextFocusLeft="@id/result_favorite"
android:nextFocusRight="@id/result_open_in_browser"
android:id="@+id/result_share"

View File

@ -313,19 +313,30 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_play_trailer"
android:nextFocusDown="@id/result_episodes_show"
android:nextFocusDown="@id/result_favorite_button"
android:text="@string/type_none"
android:visibility="visible"
app:icon="@drawable/ic_baseline_bookmark_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_favorite_button"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/result_description"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_episodes_show"
android:text="@string/action_add_to_favorites"
android:visibility="visible"
app:icon="@drawable/ic_baseline_favorite_border_24" />
<com.google.android.material.button.MaterialButton
android:id="@+id/result_episodes_show"
style="@style/ResultButtonTV"
android:nextFocusRight="@id/redirect_to_episodes"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusUp="@id/result_favorite_button"
android:nextFocusDown="@id/result_cast_items"
android:text="@string/episodes"

View File

@ -690,4 +690,9 @@
<string name="tv_no_focus_tag" translatable="false">tv_no_focus_tag</string>
<string name="already_voted">You have already voted</string>
<string name="favorites_list_name">Favorites</string>
<string name="favorite_added">%s added to favorites</string>
<string name="favorite_removed">%s removed from favorites</string>
<string name="action_add_to_favorites">Add to favorites</string>
<string name="action_remove_from_favorites">Remove from favorites</string>
</resources>