diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index ad75aa9d..7109abcc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -74,17 +74,17 @@ class HomeViewModel : ViewModel() { val watchPos = getViewPos(resume.episodeId) DataStoreHelper.ResumeWatchingResult( + watchPos, + resume.parentId, + resume.episode, + resume.season, + resume.isFromDownload, + resume.episodeId, data.name, data.url, data.apiName, data.type, - data.poster, - watchPos, - resume.episodeId, - resume.parentId, - resume.episode, - resume.season, - resume.isFromDownload + data.poster ) } } 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 a0d82062..e4fcde0f 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 @@ -431,7 +431,11 @@ open class ResultFragmentPhone : FullScreenPlayer() { }) resultSubscribe.setOnClickListener { val isSubscribed = - viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener + viewModel.toggleSubscriptionStatus(context) ?: return@setOnClickListener + + if (viewModel.subscribeStatus.value == isSubscribed) { + return@setOnClickListener + } val message = if (isSubscribed) { // Kinda icky to have this here, but it works. @@ -447,7 +451,11 @@ open class ResultFragmentPhone : FullScreenPlayer() { } resultFavorite.setOnClickListener { val isFavorite = - viewModel.toggleFavoriteStatus() ?: return@setOnClickListener + viewModel.toggleFavoriteStatus(context) ?: return@setOnClickListener + + if (viewModel.favoriteStatus.value == isFavorite) { + return@setOnClickListener + } val message = if (isFavorite) { R.string.favorite_added 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 13734b67..fe8e9823 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 @@ -557,7 +557,11 @@ class ResultFragmentTv : Fragment() { setIconResource(drawable) setText(text) setOnClickListener { - val isFavorite = viewModel.toggleFavoriteStatus() ?: return@setOnClickListener + val isFavorite = viewModel.toggleFavoriteStatus(context) ?: return@setOnClickListener + + if (viewModel.favoriteStatus.value == isFavorite) { + return@setOnClickListener + } val message = if (isFavorite) { R.string.favorite_added 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 e5ed7b92..93ea5f29 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 @@ -7,6 +7,7 @@ import android.os.Build import android.os.Bundle import android.util.Log import android.widget.Toast +import androidx.appcompat.app.AlertDialog import androidx.core.content.FileProvider import androidx.core.net.toUri import androidx.lifecycle.LiveData @@ -28,6 +29,7 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.mvvm.* +import com.lagradost.cloudstream3.services.SubscriptionWorkManager import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.providers.Kitsu @@ -45,18 +47,23 @@ import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe 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.getAllFavorites +import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions 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.getSubscribedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.DataStoreHelper.removeFavoritesData +import com.lagradost.cloudstream3.utils.DataStoreHelper.removeSubscribedData import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub import com.lagradost.cloudstream3.utils.DataStoreHelper.setFavoritesData import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode @@ -66,6 +73,9 @@ import kotlinx.coroutines.* import java.io.File import java.util.concurrent.TimeUnit +interface AlertDialogResponseCallback { + fun onUserResponse(response: Boolean) +} /** This starts at 1 */ data class EpisodeRange( @@ -452,15 +462,15 @@ class ResultViewModel2 : ViewModel() { DataStoreHelper.setBookmarkedData( currentId, DataStoreHelper.BookmarkedData( - currentId, current?.bookmarkedTime ?: currentTime, currentTime, + currentResponse.year, + currentId, currentResponse.name, currentResponse.url, currentResponse.apiName, currentResponse.type, - currentResponse.posterUrl, - currentResponse.year + currentResponse.posterUrl ) ) if (currentWatchType != status) { @@ -841,7 +851,7 @@ class ResultViewModel2 : ViewModel() { /** * @return true if the new status is Subscribed, false if not. Null if not possible to subscribe. **/ - fun toggleSubscriptionStatus(): Boolean? { + fun toggleSubscriptionStatus(context: Context?): Boolean? { val isSubscribed = _subscribeStatus.value ?: return null val response = currentResponse ?: return null if (response !is EpisodeResponse) return null @@ -849,35 +859,61 @@ class ResultViewModel2 : ViewModel() { val currentId = response.getId() if (isSubscribed) { - DataStoreHelper.removeSubscribedData(currentId) + removeSubscribedData(currentId) + + _subscribeStatus.postValue(!isSubscribed) + return !isSubscribed } else { - val current = DataStoreHelper.getSubscribedData(currentId) + checkAndWarnDuplicates( + context, + R.string.subscription_duplicate_title, + R.string.subscription_duplicate_message, + response.name, + getAllSubscriptions(), + object : AlertDialogResponseCallback { + override fun onUserResponse(action: Boolean) { + if (!action) { + _subscribeStatus.postValue(false) + return + } - DataStoreHelper.setSubscribedData( - currentId, - DataStoreHelper.SubscribedData( - currentId, - current?.bookmarkedTime ?: unixTimeMS, - unixTimeMS, - response.getLatestEpisodes(), - response.name, - response.url, - response.apiName, - response.type, - response.posterUrl, - response.year - ) + val current = getSubscribedData(currentId) + + DataStoreHelper.setSubscribedData( + currentId, + DataStoreHelper.SubscribedData( + current?.subscribedTime ?: unixTimeMS, + unixTimeMS, + response.getLatestEpisodes(), + response.year, + currentId, + response.name, + response.url, + response.apiName, + response.type, + response.posterUrl + ) + ) + + _subscribeStatus.postValue(true) + + SubscriptionWorkManager.enqueuePeriodicWork(context) + + val name = (page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + showToast(txt(R.string.subscription_new, name), Toast.LENGTH_SHORT) + } + } ) - } - _subscribeStatus.postValue(!isSubscribed) - return !isSubscribed + return _subscribeStatus.value + } } /** * @return true if added to favorites, false if not. Null if not possible to favorite. **/ - fun toggleFavoriteStatus(): Boolean? { + fun toggleFavoriteStatus(context: Context?): Boolean? { val isFavorite = _favoriteStatus.value ?: return null val response = currentResponse ?: return null @@ -885,27 +921,81 @@ class ResultViewModel2 : ViewModel() { 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 + } else { + checkAndWarnDuplicates( + context, + R.string.favorites_duplicate_title, + R.string.favorites_duplicate_message, + response.name, + getAllFavorites(), + object : AlertDialogResponseCallback { + override fun onUserResponse(action: Boolean) { + if (!action) { + _favoriteStatus.postValue(false) + return + } + + val current = getFavoritesData(currentId) + + setFavoritesData( + currentId, + DataStoreHelper.FavoritesData( + current?.favoritesTime ?: unixTimeMS, + unixTimeMS, + response.year, + currentId, + response.name, + response.url, + response.apiName, + response.type, + response.posterUrl + ) + ) + + _favoriteStatus.postValue(true) + + val name = (page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + showToast(txt(R.string.favorite_added, name), Toast.LENGTH_SHORT) + } + } ) + + return _favoriteStatus.value + } + } + + private fun checkAndWarnDuplicates( + context: Context?, + title: Int, + message: Int, + name: String, + data: List, + callback: AlertDialogResponseCallback) + { + val isDuplicate = data.any { it.name == name } + if (!isDuplicate || context == null) { + callback.onUserResponse(true) + return } - _favoriteStatus.postValue(!isFavorite) - return !isFavorite + val builder: AlertDialog.Builder = AlertDialog.Builder(context) + + builder.setTitle(title) + builder.setMessage(message) + + builder.setNegativeButton(R.string.cancel) { _, _ -> + callback.onUserResponse(false) + } + + builder.setPositiveButton(R.string.ignore) { _, _ -> + callback.onUserResponse(true) + } + + builder.show().setDefaultFocus() } private fun startChromecast( 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 4b4157d6..586fda27 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -352,20 +352,31 @@ object DataStoreHelper { /** * Used to display notifications on new episodes and posters in library. **/ - data class SubscribedData( + abstract class BaseSearchResponse( @JsonProperty("id") override var id: Int?, - @JsonProperty("subscribedTime") val bookmarkedTime: Long, - @JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long, - @JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, @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 { + @JsonProperty("posterHeaders") override var posterHeaders: Map? = null + ) : SearchResponse + + data class SubscribedData( + @JsonProperty("subscribedTime") val subscribedTime: Long, + @JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long, + @JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map, + @JsonProperty("year") val year: Int?, + override var id: Int?, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null + ) : BaseSearchResponse(id, name, url, apiName, type, posterUrl, quality, posterHeaders) { fun toLibraryItem(): SyncAPI.LibraryItem? { return SyncAPI.LibraryItem( name, @@ -381,18 +392,18 @@ object DataStoreHelper { } data class BookmarkedData( - @JsonProperty("id") override var id: Int?, @JsonProperty("bookmarkedTime") val bookmarkedTime: 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 { + override var id: Int?, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null + ) : BaseSearchResponse(id, name, url, apiName, type, posterUrl, quality, posterHeaders) { fun toLibraryItem(id: String): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( name, @@ -408,18 +419,18 @@ 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 { + override var id: Int?, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null + ) : BaseSearchResponse(id, name, url, apiName, type, posterUrl, quality, posterHeaders) { fun toLibraryItem(): SyncAPI.LibraryItem? { return SyncAPI.LibraryItem( name, @@ -435,20 +446,20 @@ object DataStoreHelper { } data class ResumeWatchingResult( - @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("watchPos") val watchPos: PosDur?, - @JsonProperty("id") override var id: Int?, @JsonProperty("parentId") val parentId: Int?, @JsonProperty("episode") val episode: Int?, @JsonProperty("season") val season: Int?, @JsonProperty("isFromDownload") val isFromDownload: Boolean, - @JsonProperty("quality") override var quality: SearchQuality? = null, - @JsonProperty("posterHeaders") override var posterHeaders: Map? = null, - ) : SearchResponse + override var id: Int?, + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override var quality: SearchQuality? = null, + override var posterHeaders: Map? = null + ) : BaseSearchResponse(id, name, url, apiName, type, posterUrl, quality, posterHeaders) /** * A datastore wide account for future implementations of a multiple account system diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 23b1a7ed..49e43593 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -695,4 +695,9 @@ %s removed from favorites Add to favorites Remove from favorites + Possible Duplicate Subscription + A possible duplicate with the same name already exists in your subscrptions. + Possible Duplicate Found + A possible duplicate with the same name already exists in your favorites. + Ignore