Warn of potential duplicates when adding to library (#691)

This commit is contained in:
Luna712 2023-10-25 18:09:21 -06:00 committed by GitHub
parent e4ba852007
commit 968bd59188
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 481 additions and 166 deletions

View file

@ -1246,6 +1246,18 @@ interface LoadResponse {
return this.syncData[aniListIdPrefix] return this.syncData[aniListIdPrefix]
} }
fun LoadResponse.getImdbId(): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb)
}
}
fun LoadResponse.getTMDbId(): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb)
}
}
fun LoadResponse.addMalId(id: Int?) { fun LoadResponse.addMalId(id: Int?) {
this.syncData[malIdPrefix] = (id ?: return).toString() this.syncData[malIdPrefix] = (id ?: return).toString()
this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString())

View file

@ -1306,7 +1306,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this@MainActivity.getString(R.string.action_add_to_bookmarks), this@MainActivity.getString(R.string.action_add_to_bookmarks),
showApply = false, showApply = false,
{}) { {}) {
viewModel.updateWatchStatus(WatchType.values()[it]) viewModel.updateWatchStatus(WatchType.values()[it], this@MainActivity)
} }
} }

View file

@ -203,7 +203,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
} }
/** Read the id string to get all other ids */ /** Read the id string to get all other ids */
private fun readIdFromString(idString: String?): Map<SyncServices, String> { fun readIdFromString(idString: String?): Map<SyncServices, String> {
return tryParseJson(idString) ?: return emptyMap() return tryParseJson(idString) ?: return emptyMap()
} }

View file

@ -378,6 +378,14 @@ class HomeParentItemAdapterPreview(
showApply = false, showApply = false,
{}) { {}) {
val newValue = WatchType.values()[it] val newValue = WatchType.values()[it]
ResultViewModel2().updateWatchStatus(
newValue,
fab.context,
item
) { statusChanged: Boolean ->
if (!statusChanged) return@updateWatchStatus
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds( homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
null, null,
ContextCompat.getDrawable( ContextCompat.getDrawable(
@ -388,11 +396,7 @@ class HomeParentItemAdapterPreview(
null null
) )
homePreviewBookmark.setText(newValue.stringRes) homePreviewBookmark.setText(newValue.stringRes)
}
ResultViewModel2.updateWatchStatus(
item,
newValue
)
} }
} }
} }

View file

@ -430,10 +430,10 @@ open class ResultFragmentPhone : FullScreenPlayer() {
} }
}) })
resultSubscribe.setOnClickListener { resultSubscribe.setOnClickListener {
val isSubscribed = viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener if (newStatus == null) return@toggleSubscriptionStatus
val message = if (isSubscribed) { val message = if (newStatus) {
// Kinda icky to have this here, but it works. // Kinda icky to have this here, but it works.
SubscriptionWorkManager.enqueuePeriodicWork(context) SubscriptionWorkManager.enqueuePeriodicWork(context)
R.string.subscription_new R.string.subscription_new
@ -445,11 +445,12 @@ open class ResultFragmentPhone : FullScreenPlayer() {
?: txt(R.string.no_data).asStringNull(context) ?: "" ?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
} }
}
resultFavorite.setOnClickListener { resultFavorite.setOnClickListener {
val isFavorite = viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? ->
viewModel.toggleFavoriteStatus() ?: return@setOnClickListener if (newStatus == null) return@toggleFavoriteStatus
val message = if (isFavorite) { val message = if (newStatus) {
R.string.favorite_added R.string.favorite_added
} else { } else {
R.string.favorite_removed R.string.favorite_removed
@ -459,6 +460,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
?: txt(R.string.no_data).asStringNull(context) ?: "" ?: txt(R.string.no_data).asStringNull(context) ?: ""
CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT)
} }
}
mediaRouteButton.apply { mediaRouteButton.apply {
val chromecastSupport = api?.hasChromecastSupport == true val chromecastSupport = api?.hasChromecastSupport == true
alpha = if (chromecastSupport) 1f else 0.3f alpha = if (chromecastSupport) 1f else 0.3f
@ -960,7 +962,7 @@ open class ResultFragmentPhone : FullScreenPlayer() {
fab.context.getString(R.string.action_add_to_bookmarks), fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false, showApply = false,
{}) { {}) {
viewModel.updateWatchStatus(WatchType.values()[it]) viewModel.updateWatchStatus(WatchType.values()[it], context)
} }
} }
} }

View file

@ -531,7 +531,7 @@ class ResultFragmentTv : Fragment() {
view.context.getString(R.string.action_add_to_bookmarks), view.context.getString(R.string.action_add_to_bookmarks),
showApply = false, showApply = false,
{}) { {}) {
viewModel.updateWatchStatus(WatchType.values()[it]) viewModel.updateWatchStatus(WatchType.values()[it], context)
} }
} }
} }
@ -557,9 +557,10 @@ class ResultFragmentTv : Fragment() {
setIconResource(drawable) setIconResource(drawable)
setText(text) setText(text)
setOnClickListener { setOnClickListener {
val isFavorite = viewModel.toggleFavoriteStatus() ?: return@setOnClickListener viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? ->
if (newStatus == null) return@toggleFavoriteStatus
val message = if (isFavorite) { val message = if (newStatus) {
R.string.favorite_added R.string.favorite_added
} else { } else {
R.string.favorite_removed R.string.favorite_removed
@ -571,6 +572,7 @@ class ResultFragmentTv : Fragment() {
} }
} }
} }
}
observeNullable(viewModel.movie) { data -> observeNullable(viewModel.movie) { data ->
binding?.apply { binding?.apply {

View file

@ -7,6 +7,8 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@ -31,6 +33,7 @@ import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
@ -45,22 +48,37 @@ import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast 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.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.getFavoritesData import com.lagradost.cloudstream3.utils.DataStoreHelper.getFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeFavoritesData import com.lagradost.cloudstream3.utils.DataStoreHelper.removeFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.setFavoritesData import com.lagradost.cloudstream3.utils.DataStoreHelper.setFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
@ -113,6 +131,18 @@ data class ResultData(
val plotHeaderText: UiText, val plotHeaderText: UiText,
) )
data class CheckDuplicateData(
val name: String,
val year: Int?,
val syncData: Map<String, String>?
)
enum class LibraryListType {
BOOKMARKS,
FAVORITES,
SUBSCRIPTIONS
}
fun txt(status: DubStatus?): UiText? { fun txt(status: DubStatus?): UiText? {
return txt( return txt(
when (status) { when (status) {
@ -441,33 +471,6 @@ class ResultViewModel2 : ViewModel() {
return this?.firstOrNull { it.season == season } return this?.firstOrNull { it.season == season }
} }
fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) {
val currentId = currentResponse.getId()
val currentWatchType = getResultWatchState(currentId)
DataStoreHelper.setResultWatchState(currentId, status.internalId)
val current = DataStoreHelper.getBookmarkedData(currentId)
val currentTime = System.currentTimeMillis()
DataStoreHelper.setBookmarkedData(
currentId,
DataStoreHelper.BookmarkedData(
currentId,
current?.bookmarkedTime ?: currentTime,
currentTime,
currentResponse.name,
currentResponse.url,
currentResponse.apiName,
currentResponse.type,
currentResponse.posterUrl,
currentResponse.year
)
)
if (currentWatchType != status) {
MainActivity.bookmarksUpdatedEvent(true)
}
}
private fun filterName(name: String?): String? { private fun filterName(name: String?): String? {
if (name == null) return null if (name == null) return null
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let { Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
@ -822,9 +825,77 @@ class ResultViewModel2 : ViewModel() {
val selectPopup: LiveData<SelectPopup?> = _selectPopup val selectPopup: LiveData<SelectPopup?> = _selectPopup
fun updateWatchStatus(status: WatchType) { fun updateWatchStatus(
updateWatchStatus(currentResponse ?: return, status) status: WatchType,
context: Context?,
loadResponse: LoadResponse? = null,
statusChangedCallback: ((statusChanged: Boolean) -> Unit)? = null
) {
val response = loadResponse ?: currentResponse ?: return
val currentId = response.getId()
val currentStatus = getResultWatchState(currentId)
// If the current status is "NONE" and the new status is not "NONE",
// fetch the bookmarked data to check for duplicates, otherwise set this
// to an empty list, so that we don't show the duplicate warning dialog,
// but we still want to update the current bookmark and refresh the data anyway.
val bookmarkedData = if (currentStatus == WatchType.NONE && status != WatchType.NONE) {
getAllBookmarkedData()
} else emptyList()
checkAndWarnDuplicates(
context,
LibraryListType.BOOKMARKS,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
bookmarkedData
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) return@checkAndWarnDuplicates
if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
deleteBookmarkedData(duplicateId)
}
}
setResultWatchState(currentId, status.internalId)
// We don't need to store if WatchType.NONE.
// The key is removed in setResultWatchState, we don't want to
// re-add it again here if it was just removed.
if (status != WatchType.NONE) {
val current = getBookmarkedData(currentId)
setBookmarkedData(
currentId,
DataStoreHelper.BookmarkedData(
current?.bookmarkedTime ?: unixTimeMS,
currentId,
unixTimeMS,
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year,
response.syncData
)
)
}
if (currentStatus != status) {
MainActivity.bookmarksUpdatedEvent(true)
}
_watchStatus.postValue(status) _watchStatus.postValue(status)
statusChangedCallback?.invoke(true)
}
} }
private fun startChromecast( private fun startChromecast(
@ -839,73 +910,255 @@ class ResultViewModel2 : ViewModel() {
} }
/** /**
* @return true if the new status is Subscribed, false if not. Null if not possible to subscribe. * Toggles the subscription status of an item.
**/ *
fun toggleSubscriptionStatus(): Boolean? { * @param context The context to use for operations.
val isSubscribed = _subscribeStatus.value ?: return null * @param statusChangedCallback A callback that is invoked when the subscription status changes.
val response = currentResponse ?: return null * It provides the new subscription status (true if subscribed, false if unsubscribed, null if action was canceled).
if (response !is EpisodeResponse) return null */
fun toggleSubscriptionStatus(
context: Context?,
statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null
) {
val isSubscribed = _subscribeStatus.value ?: return
val response = currentResponse ?: return
if (response !is EpisodeResponse) return
val currentId = response.getId() val currentId = response.getId()
if (isSubscribed) { if (isSubscribed) {
DataStoreHelper.removeSubscribedData(currentId) removeSubscribedData(currentId)
statusChangedCallback?.invoke(false)
_subscribeStatus.postValue(false)
} else { } else {
val current = DataStoreHelper.getSubscribedData(currentId) checkAndWarnDuplicates(
context,
LibraryListType.SUBSCRIPTIONS,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
getAllSubscriptions(),
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) {
statusChangedCallback?.invoke(null)
return@checkAndWarnDuplicates
}
DataStoreHelper.setSubscribedData( if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
removeSubscribedData(duplicateId)
}
}
val current = getSubscribedData(currentId)
setSubscribedData(
currentId, currentId,
DataStoreHelper.SubscribedData( DataStoreHelper.SubscribedData(
currentId, current?.subscribedTime ?: unixTimeMS,
current?.bookmarkedTime ?: unixTimeMS,
unixTimeMS,
response.getLatestEpisodes(), response.getLatestEpisodes(),
currentId,
unixTimeMS,
response.name, response.name,
response.url, response.url,
response.apiName, response.apiName,
response.type, response.type,
response.posterUrl, response.posterUrl,
response.year response.year,
response.syncData
) )
) )
}
_subscribeStatus.postValue(!isSubscribed) _subscribeStatus.postValue(true)
return !isSubscribed
statusChangedCallback?.invoke(true)
}
}
} }
/** /**
* @return true if added to favorites, false if not. Null if not possible to favorite. * Toggles the favorite status of an item.
**/ *
fun toggleFavoriteStatus(): Boolean? { * @param context The context to use.
val isFavorite = _favoriteStatus.value ?: return null * @param statusChangedCallback A callback that is invoked when the favorite status changes.
val response = currentResponse ?: return null * It provides the new favorite status (true if added to favorites, false if removed, null if action was canceled).
*/
fun toggleFavoriteStatus(
context: Context?,
statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null
) {
val isFavorite = _favoriteStatus.value ?: return
val response = currentResponse ?: return
val currentId = response.getId() val currentId = response.getId()
if (isFavorite) { if (isFavorite) {
removeFavoritesData(currentId) removeFavoritesData(currentId)
statusChangedCallback?.invoke(false)
_favoriteStatus.postValue(false)
} else { } else {
checkAndWarnDuplicates(
context,
LibraryListType.FAVORITES,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
getAllFavorites(),
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) {
statusChangedCallback?.invoke(null)
return@checkAndWarnDuplicates
}
if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
removeFavoritesData(duplicateId)
}
}
val current = getFavoritesData(currentId) val current = getFavoritesData(currentId)
setFavoritesData( setFavoritesData(
currentId, currentId,
DataStoreHelper.FavoritesData( DataStoreHelper.FavoritesData(
currentId,
current?.favoritesTime ?: unixTimeMS, current?.favoritesTime ?: unixTimeMS,
currentId,
unixTimeMS, unixTimeMS,
response.name, response.name,
response.url, response.url,
response.apiName, response.apiName,
response.type, response.type,
response.posterUrl, response.posterUrl,
response.year response.year,
response.syncData
) )
) )
_favoriteStatus.postValue(true)
statusChangedCallback?.invoke(true)
}
}
} }
_favoriteStatus.postValue(!isFavorite) @MainThread
return !isFavorite private fun checkAndWarnDuplicates(
context: Context?,
listType: LibraryListType,
checkDuplicateData: CheckDuplicateData,
data: List<DataStoreHelper.LibrarySearchResponse>,
checkDuplicatesCallback: (shouldContinue: Boolean, duplicateIds: List<Int?>) -> Unit
) {
val whitespaceRegex = "\\s+".toRegex()
fun normalizeString(input: String): String {
/**
* Trim the input string and replace consecutive spaces with a single space.
* This covers some edge-cases where the title does not match exactly across providers,
* and one provider has the title with an extra whitespace. This is minor enough that
* it should still match in this case.
*/
return input.trim().replace(whitespaceRegex, " ")
}
val syncData = checkDuplicateData.syncData
val imdbId = getImdbIdFromSyncData(syncData)
val tmdbId = getTMDbIdFromSyncData(syncData)
val malId = syncData?.get(AccountManager.malApi.idPrefix)
val aniListId = syncData?.get(AccountManager.aniListApi.idPrefix)
val normalizedName = normalizeString(checkDuplicateData.name)
val year = checkDuplicateData.year
val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse ->
val librarySyncData = it.syncData
val checks = listOf(
{ imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId },
{ tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId },
{ malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId },
{ aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId },
{ normalizedName == normalizeString(it.name) && year == it.year }
)
checks.any { it() }
}
if (duplicateEntries.isEmpty() || context == null) {
checkDuplicatesCallback.invoke(true, emptyList())
return
}
val replaceMessage = if (duplicateEntries.size > 1) {
R.string.duplicate_replace_all
} else R.string.duplicate_replace
val message = if (duplicateEntries.size == 1) {
val list = when (listType) {
LibraryListType.BOOKMARKS -> getResultWatchState(duplicateEntries[0].id ?: 0).stringRes
LibraryListType.FAVORITES -> R.string.favorites_list_name
LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name
}
context.getString(R.string.duplicate_message_single,
"${normalizeString(duplicateEntries[0].name)} (${context.getString(list)}) — ${duplicateEntries[0].apiName}"
)
} else {
val bulletPoints = duplicateEntries.joinToString("\n") {
val list = when (listType) {
LibraryListType.BOOKMARKS -> getResultWatchState(it.id ?: 0).stringRes
LibraryListType.FAVORITES -> R.string.favorites_list_name
LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name
}
"${it.apiName}: ${normalizeString(it.name)} (${context.getString(list)})"
}
context.getString(R.string.duplicate_message_multiple, bulletPoints)
}
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
checkDuplicatesCallback.invoke(true, emptyList())
}
DialogInterface.BUTTON_NEGATIVE -> {
checkDuplicatesCallback.invoke(false, emptyList())
}
DialogInterface.BUTTON_NEUTRAL -> {
checkDuplicatesCallback.invoke(true, duplicateEntries.map { it.id })
}
}
}
builder.setTitle(R.string.duplicate_title)
.setMessage(message)
.setPositiveButton(R.string.duplicate_add, dialogClickListener)
.setNegativeButton(R.string.duplicate_cancel, dialogClickListener)
.setNeutralButton(replaceMessage, dialogClickListener)
.show().setDefaultFocus()
}
private fun getImdbIdFromSyncData(syncData: Map<String, String>?): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(
syncData?.get(AccountManager.simklApi.idPrefix)
)[SimklApi.Companion.SyncServices.Imdb]
}
}
private fun getTMDbIdFromSyncData(syncData: Map<String, String>?): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(
syncData?.get(AccountManager.simklApi.idPrefix)
)[SimklApi.Companion.SyncServices.Tmdb]
}
} }
private fun startChromecast( private fun startChromecast(
@ -1259,7 +1512,7 @@ class ResultViewModel2 : ViewModel() {
// Do not add mark as watched on movies // Do not add mark as watched on movies
if (!listOf(TvType.Movie, TvType.AnimeMovie).contains(click.data.tvType)) { if (!listOf(TvType.Movie, TvType.AnimeMovie).contains(click.data.tvType)) {
val isWatched = val isWatched =
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched getVideoWatchState(click.data.id) == VideoWatchState.Watched
val watchedText = if (isWatched) R.string.action_remove_from_watched val watchedText = if (isWatched) R.string.action_remove_from_watched
else R.string.action_mark_as_watched else R.string.action_mark_as_watched
@ -1508,12 +1761,12 @@ class ResultViewModel2 : ViewModel() {
ACTION_MARK_AS_WATCHED -> { ACTION_MARK_AS_WATCHED -> {
val isWatched = val isWatched =
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched getVideoWatchState(click.data.id) == VideoWatchState.Watched
if (isWatched) { if (isWatched) {
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.None) setVideoWatchState(click.data.id, VideoWatchState.None)
} else { } else {
DataStoreHelper.setVideoWatchState(click.data.id, VideoWatchState.Watched) setVideoWatchState(click.data.id, VideoWatchState.Watched)
} }
// Kinda dirty to reload all episodes :( // Kinda dirty to reload all episodes :(
@ -1722,7 +1975,7 @@ class ResultViewModel2 : ViewModel() {
list.subList(start, end).map { list.subList(start, end).map {
val posDur = getViewPos(it.id) val posDur = getViewPos(it.id)
val watchState = val watchState =
DataStoreHelper.getVideoWatchState(it.id) ?: VideoWatchState.None getVideoWatchState(it.id) ?: VideoWatchState.None
it.copy( it.copy(
position = posDur?.position ?: 0, position = posDur?.position ?: 0,
duration = posDur?.duration ?: 0, duration = posDur?.duration ?: 0,
@ -1783,8 +2036,8 @@ class ResultViewModel2 : ViewModel() {
private fun postSubscription(loadResponse: LoadResponse) { private fun postSubscription(loadResponse: LoadResponse) {
if (loadResponse.isEpisodeBased()) { if (loadResponse.isEpisodeBased()) {
val id = loadResponse.getId() val id = loadResponse.getId()
val data = DataStoreHelper.getSubscribedData(id) val data = getSubscribedData(id)
DataStoreHelper.updateSubscribedData(id, data, loadResponse as? EpisodeResponse) updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
val isSubscribed = data != null val isSubscribed = data != null
_subscribeStatus.postValue(isSubscribed) _subscribeStatus.postValue(isSubscribed)
} }
@ -2162,13 +2415,13 @@ class ResultViewModel2 : ViewModel() {
postResume() postResume()
} }
fun postResume() { private fun postResume() {
_resumeWatching.postValue(resume()) _resumeWatching.postValue(resume())
} }
private fun resume(): ResumeWatchingStatus? { private fun resume(): ResumeWatchingStatus? {
val correctId = currentId ?: return null val correctId = currentId ?: return null
val resume = DataStoreHelper.getLastWatched(correctId) val resume = getLastWatched(correctId)
val resumeParentId = resume?.parentId val resumeParentId = resume?.parentId
if (resumeParentId != correctId) return null // is null or smth went wrong with getLastWatched if (resumeParentId != correctId) return null // is null or smth went wrong with getLastWatched
val resumeId = resume.episodeId ?: return null// invalid episode id val resumeId = resume.episodeId ?: return null// invalid episode id

View file

@ -352,20 +352,35 @@ object DataStoreHelper {
/** /**
* Used to display notifications on new episodes and posters in library. * Used to display notifications on new episodes and posters in library.
**/ **/
data class SubscribedData( abstract class LibrarySearchResponse(
@JsonProperty("id") override var id: Int?, @JsonProperty("id") override var id: Int?,
@JsonProperty("subscribedTime") val bookmarkedTime: Long, @JsonProperty("latestUpdatedTime") open val latestUpdatedTime: Long,
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long,
@JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map<DubStatus, Int?>,
@JsonProperty("name") override val name: String, @JsonProperty("name") override val name: String,
@JsonProperty("url") override val url: String, @JsonProperty("url") override val url: String,
@JsonProperty("apiName") override val apiName: String, @JsonProperty("apiName") override val apiName: String,
@JsonProperty("type") override var type: TvType? = null, @JsonProperty("type") override var type: TvType?,
@JsonProperty("posterUrl") override var posterUrl: String?, @JsonProperty("posterUrl") override var posterUrl: String?,
@JsonProperty("year") val year: Int?, @JsonProperty("year") open val year: Int?,
@JsonProperty("quality") override var quality: SearchQuality? = null, @JsonProperty("syncData") open val syncData: Map<String, String>?,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null, @JsonProperty("quality") override var quality: SearchQuality?,
) : SearchResponse { @JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>?
) : SearchResponse
data class SubscribedData(
@JsonProperty("subscribedTime") val subscribedTime: Long,
@JsonProperty("lastSeenEpisodeCount") val lastSeenEpisodeCount: Map<DubStatus, Int?>,
override var id: Int?,
override val latestUpdatedTime: Long,
override val name: String,
override val url: String,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override val year: Int?,
override val syncData: Map<String, String>? = null,
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
fun toLibraryItem(): SyncAPI.LibraryItem? { fun toLibraryItem(): SyncAPI.LibraryItem? {
return SyncAPI.LibraryItem( return SyncAPI.LibraryItem(
name, name,
@ -381,18 +396,19 @@ object DataStoreHelper {
} }
data class BookmarkedData( data class BookmarkedData(
@JsonProperty("id") override var id: Int?,
@JsonProperty("bookmarkedTime") val bookmarkedTime: Long, @JsonProperty("bookmarkedTime") val bookmarkedTime: Long,
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long, override var id: Int?,
@JsonProperty("name") override val name: String, override val latestUpdatedTime: Long,
@JsonProperty("url") override val url: String, override val name: String,
@JsonProperty("apiName") override val apiName: String, override val url: String,
@JsonProperty("type") override var type: TvType? = null, override val apiName: String,
@JsonProperty("posterUrl") override var posterUrl: String?, override var type: TvType?,
@JsonProperty("year") val year: Int?, override var posterUrl: String?,
@JsonProperty("quality") override var quality: SearchQuality? = null, override val year: Int?,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null, override val syncData: Map<String, String>? = null,
) : SearchResponse { override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
fun toLibraryItem(id: String): SyncAPI.LibraryItem { fun toLibraryItem(id: String): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem( return SyncAPI.LibraryItem(
name, name,
@ -408,18 +424,19 @@ object DataStoreHelper {
} }
data class FavoritesData( data class FavoritesData(
@JsonProperty("id") override var id: Int?,
@JsonProperty("favoritesTime") val favoritesTime: Long, @JsonProperty("favoritesTime") val favoritesTime: Long,
@JsonProperty("latestUpdatedTime") val latestUpdatedTime: Long, override var id: Int?,
@JsonProperty("name") override val name: String, override val latestUpdatedTime: Long,
@JsonProperty("url") override val url: String, override val name: String,
@JsonProperty("apiName") override val apiName: String, override val url: String,
@JsonProperty("type") override var type: TvType? = null, override val apiName: String,
@JsonProperty("posterUrl") override var posterUrl: String?, override var type: TvType?,
@JsonProperty("year") val year: Int?, override var posterUrl: String?,
@JsonProperty("quality") override var quality: SearchQuality? = null, override val year: Int?,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null, override val syncData: Map<String, String>? = null,
) : SearchResponse { override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null
) : LibrarySearchResponse(id, latestUpdatedTime, name, url, apiName, type, posterUrl, year, syncData, quality, posterHeaders) {
fun toLibraryItem(): SyncAPI.LibraryItem? { fun toLibraryItem(): SyncAPI.LibraryItem? {
return SyncAPI.LibraryItem( return SyncAPI.LibraryItem(
name, name,
@ -572,6 +589,12 @@ object DataStoreHelper {
return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) return getKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString())
} }
fun getAllBookmarkedData(): List<BookmarkedData> {
return getKeys("$currentAccount/$RESULT_WATCH_STATE_DATA")?.mapNotNull {
getKey(it)
} ?: emptyList()
}
fun getAllSubscriptions(): List<SubscribedData> { fun getAllSubscriptions(): List<SubscribedData> {
return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull { return getKeys("$currentAccount/$RESULT_SUBSCRIBED_STATE_DATA")?.mapNotNull {
getKey(it) getKey(it)

View file

@ -686,13 +686,32 @@
<string name="qualities">Qualities</string> <string name="qualities">Qualities</string>
<string name="profile_background_des">Profile background</string> <string name="profile_background_des">Profile background</string>
<string name="unable_to_inflate">UI was unable to be created correctly, this is a MAJOR BUG and should be reported immediately %s</string> <string name="unable_to_inflate">UI was unable to be created correctly, this is a MAJOR BUG and should be reported immediately %s</string>
<string name="tv_no_focus_tag" translatable="false">tv_no_focus_tag</string>
<string name="already_voted">You have already voted</string> <string name="already_voted">You have already voted</string>
<string name="favorites_list_name">Favorites</string> <string name="favorites_list_name">Favorites</string>
<string name="favorite_added">%s added to favorites</string> <string name="favorite_added">%s added to favorites</string>
<string name="favorite_removed">%s removed from favorites</string> <string name="favorite_removed">%s removed from favorites</string>
<string name="action_add_to_favorites">Add to favorites</string> <string name="action_add_to_favorites">Add to favorites</string>
<string name="action_remove_from_favorites">Remove from favorites</string> <string name="action_remove_from_favorites">Remove from favorites</string>
<string name="duplicate_title">Potential Duplicate Found</string>
<string name="duplicate_add">Add</string>
<string name="duplicate_replace">Replace</string>
<string name="duplicate_replace_all">Replace All</string>
<string name="duplicate_cancel" translatable="false">@string/sort_cancel</string>
<string name="duplicate_message_single">
It appears that a potentially duplicate item already exists in your library: \'%1$s.\'
\n\nWould you like to add this item anyway, replace the existing one, or cancel the action?
</string>
<string name="duplicate_message_multiple">
Potential duplicate items have been found in your library:
\n\n%1$s
\n\nWould you like to add this item anyway, replace the existing ones, or cancel the action?
</string>
<string name="tv_no_focus_tag" translatable="false">tv_no_focus_tag</string>
</resources> </resources>