Merge branch 'master' of https://github.com/recloudstream/cloudstream into emulator-results

This commit is contained in:
Luna712 2023-10-25 21:07:38 -06:00
commit d597049d40
12 changed files with 576 additions and 280 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

@ -1,73 +0,0 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import com.lagradost.cloudstream3.utils.SyncUtil
// wont be implemented
class MultiAnimeProvider : MainAPI() {
override var name = "MultiAnime"
override var lang = "en"
override val usesWebView = true
override val supportedTypes = setOf(TvType.Anime)
private val syncApi: SyncAPI = aniListApi
private val syncUtilType by lazy {
when (syncApi) {
is AniListApi -> "anilist"
is MALApi -> "myanimelist"
else -> throw ErrorLoadingException("Invalid Api")
}
}
private val validApis
get() =
synchronized(APIHolder.apis) {
APIHolder.apis.filter {
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
TvType.Anime
)
}
}
private fun filterName(name: String): String {
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
}
override suspend fun search(query: String): List<SearchResponse>? {
return syncApi.search(query)?.map {
AnimeSearchResponse(it.name, it.url, this.name, TvType.Anime, it.posterUrl)
}
}
override suspend fun load(url: String): LoadResponse? {
return syncApi.getResult(url)?.let { res ->
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url ->
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
}.filterNotNull()
val type =
if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime
newAnimeLoadResponse(
res.title ?: throw ErrorLoadingException("No Title found"),
url,
type
) {
posterUrl = res.posterUrl
plot = res.synopsis
tags = res.genres
rating = res.publicScore
addTrailer(res.trailers)
addAniListId(res.id.toIntOrNull())
recommendations = res.recommendations
}
}
}
}

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,21 +378,25 @@ class HomeParentItemAdapterPreview(
showApply = false, showApply = false,
{}) { {}) {
val newValue = WatchType.values()[it] val newValue = WatchType.values()[it]
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
homePreviewBookmark.context,
newValue.iconRes
),
null,
null
)
homePreviewBookmark.setText(newValue.stringRes)
ResultViewModel2.updateWatchStatus( ResultViewModel2().updateWatchStatus(
item, newValue,
newValue fab.context,
) item
) { statusChanged: Boolean ->
if (!statusChanged) return@updateWatchStatus
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
homePreviewBookmark.context,
newValue.iconRes
),
null,
null
)
homePreviewBookmark.setText(newValue.stringRes)
}
} }
} }
} }

View file

@ -17,7 +17,6 @@ import android.view.animation.DecelerateInterpolator
import android.widget.AbsListView import android.widget.AbsListView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
@ -66,6 +65,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
@ -430,34 +430,36 @@ 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
} else { } else {
R.string.subscription_deleted R.string.subscription_deleted
}
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)
} }
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)
} }
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
}
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)
} }
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 { mediaRouteButton.apply {
val chromecastSupport = api?.hasChromecastSupport == true val chromecastSupport = api?.hasChromecastSupport == true
@ -681,14 +683,13 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultPoster.setImage(d.posterImage) resultPoster.setImage(d.posterImage)
resultPosterBackground.setImage(d.posterBackgroundImage) resultPosterBackground.setImage(d.posterBackgroundImage)
resultDescription.setTextHtml(d.plotText) resultDescription.setTextHtml(d.plotText)
resultDescription.setOnClickListener { view -> resultDescription.setOnClickListener {
// todo bottom view? activity?.let { activity ->
view.context?.let { ctx -> activity.showBottomDialogText(
val builder: AlertDialog.Builder = d.titleText.asString(activity),
AlertDialog.Builder(ctx, R.style.AlertDialogCustom) d.plotText.asString(activity).html(),
builder.setMessage(d.plotText.asString(ctx).html()) {}
.setTitle(d.plotHeaderText.asString(ctx)) )
.show()
} }
} }
@ -879,16 +880,11 @@ open class ResultFragmentPhone : FullScreenPlayer() {
setRecommendations(recommendations, null) setRecommendations(recommendations, null)
} }
observe(viewModel.episodeSynopsis) { description -> observe(viewModel.episodeSynopsis) { description ->
// TODO bottom dialog activity?.let { activity ->
view.context?.let { ctx -> activity.showBottomDialogText(
val builder: AlertDialog.Builder = activity.getString(R.string.synopsis),
AlertDialog.Builder(ctx, R.style.AlertDialogCustom) description.html()
builder.setMessage(description.html()) ) { viewModel.releaseEpisodeSynopsis() }
.setTitle(R.string.synopsis)
.setOnDismissListener {
viewModel.releaseEpisodeSynopsis()
}
.show()
} }
} }
context?.let { ctx -> context?.let { ctx ->
@ -966,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

@ -535,7 +535,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)
} }
} }
} }
@ -561,17 +561,19 @@ 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
}
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)
} }
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)
} }
} }
} }

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,
_watchStatus.postValue(status) 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)
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()) {
currentId, duplicateIds.forEach { duplicateId ->
DataStoreHelper.SubscribedData( removeSubscribedData(duplicateId)
}
}
val current = getSubscribedData(currentId)
setSubscribedData(
currentId, currentId,
current?.bookmarkedTime ?: unixTimeMS, DataStoreHelper.SubscribedData(
unixTimeMS, current?.subscribedTime ?: unixTimeMS,
response.getLatestEpisodes(), response.getLatestEpisodes(),
response.name, currentId,
response.url, unixTimeMS,
response.apiName, response.name,
response.type, response.url,
response.posterUrl, response.apiName,
response.year response.type,
response.posterUrl,
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 {
val current = getFavoritesData(currentId) 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
}
setFavoritesData( if (duplicateIds.isNotEmpty()) {
currentId, duplicateIds.forEach { duplicateId ->
DataStoreHelper.FavoritesData( removeFavoritesData(duplicateId)
}
}
val current = getFavoritesData(currentId)
setFavoritesData(
currentId, currentId,
current?.favoritesTime ?: unixTimeMS, DataStoreHelper.FavoritesData(
unixTimeMS, current?.favoritesTime ?: unixTimeMS,
response.name, currentId,
response.url, unixTimeMS,
response.apiName, response.name,
response.type, response.url,
response.posterUrl, response.apiName,
response.year response.type,
response.posterUrl,
response.year,
response.syncData
)
) )
)
_favoriteStatus.postValue(true)
statusChangedCallback?.invoke(true)
}
}
}
@MainThread
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, " ")
} }
_favoriteStatus.postValue(!isFavorite) val syncData = checkDuplicateData.syncData
return !isFavorite
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

@ -2,14 +2,12 @@ package com.lagradost.cloudstream3.utils
import android.app.Activity import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.text.Spanned
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.AbsListView import android.widget.AbsListView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ListView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone import androidx.core.view.isGone
@ -19,7 +17,10 @@ import androidx.core.view.marginRight
import androidx.core.view.marginTop import androidx.core.view.marginTop
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding
import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding
import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding
import com.lagradost.cloudstream3.databinding.OptionsPopupTvBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
@ -54,14 +55,14 @@ object SingleSelectionHelper {
if (this == null) return if (this == null) return
if (isTvSettings()) { if (isTvSettings()) {
val builder = val binding = OptionsPopupTvBinding.inflate(layoutInflater)
AlertDialog.Builder(this, R.style.AlertDialogCustom) val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(R.layout.options_popup_tv) .setView(binding.root)
.create()
val dialog = builder.create()
dialog.show() dialog.show()
dialog.findViewById<ListView>(R.id.listview1)?.let { listView -> binding.listview1.let { listView ->
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listView.adapter = listView.adapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice_color).apply { ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice_color).apply {
@ -74,7 +75,7 @@ object SingleSelectionHelper {
} }
} }
dialog.findViewById<ImageView>(R.id.imageView)?.apply { binding.imageView.apply {
isGone = poster.isNullOrEmpty() isGone = poster.isNullOrEmpty()
setImage(poster) setImage(poster)
} }
@ -105,12 +106,12 @@ object SingleSelectionHelper {
if (this == null) return if (this == null) return
val realShowApply = showApply || isMultiSelect val realShowApply = showApply || isMultiSelect
val listView = binding.listview1//.findViewById<ListView>(R.id.listview1)!! val listView = binding.listview1
val textView = binding.text1//.findViewById<TextView>(R.id.text1)!! val textView = binding.text1
val applyButton = binding.applyBtt//.findViewById<TextView>(R.id.apply_btt) val applyButton = binding.applyBtt
val cancelButton = binding.cancelBtt//findViewById<TextView>(R.id.cancel_btt) val cancelButton = binding.cancelBtt
val applyHolder = val applyHolder =
binding.applyBttHolder//.findViewById<LinearLayout>(R.id.apply_btt_holder) binding.applyBttHolder
applyHolder.isVisible = realShowApply applyHolder.isVisible = realShowApply
if (!realShowApply) { if (!realShowApply) {
@ -173,8 +174,8 @@ object SingleSelectionHelper {
} }
} }
private fun Activity?.showInputDialog( private fun Activity?.showInputDialog(
binding: BottomInputDialogBinding,
dialog: Dialog, dialog: Dialog,
value: String, value: String,
name: String, name: String,
@ -184,11 +185,11 @@ object SingleSelectionHelper {
) { ) {
if (this == null) return if (this == null) return
val inputView = dialog.findViewById<EditText>(R.id.nginx_text_input)!! val inputView = binding.nginxTextInput
val textView = dialog.findViewById<TextView>(R.id.text1)!! val textView = binding.text1
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!! val applyButton = binding.applyBtt
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!! val cancelButton = binding.cancelBtt
val applyHolder = dialog.findViewById<LinearLayout>(R.id.apply_btt_holder)!! val applyHolder = binding.applyBttHolder
applyHolder.isVisible = true applyHolder.isVisible = true
textView.text = name textView.text = name
@ -350,11 +351,17 @@ object SingleSelectionHelper {
dismissCallback: () -> Unit, dismissCallback: () -> Unit,
callback: (String) -> Unit, callback: (String) -> Unit,
) { ) {
val builder = BottomSheetDialog(this) // probably the stuff at the bottom val builder = BottomSheetDialog(this)
builder.setContentView(R.layout.bottom_input_dialog) // input layout
val binding: BottomInputDialogBinding = BottomInputDialogBinding.inflate(
LayoutInflater.from(this)
)
builder.setContentView(binding.root)
builder.show() builder.show()
showInputDialog( showInputDialog(
binding,
builder, builder,
value, value,
name, name,
@ -363,4 +370,24 @@ object SingleSelectionHelper {
dismissCallback dismissCallback
) )
} }
fun Activity.showBottomDialogText(
title: String,
text: Spanned,
dismissCallback: () -> Unit
) {
val binding = BottomTextDialogBinding.inflate(layoutInflater)
val dialog = BottomSheetDialog(this)
dialog.setContentView(binding.root)
binding.dialogTitle.text = title
binding.dialogText.text = text
dialog.setOnDismissListener {
dismissCallback.invoke()
}
dialog.show()
}
} }

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/dialog_title"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:textStyle="bold"
android:textSize="20sp"
android:textColor="?attr/textColor"
android:layout_width="match_parent"
android:layout_rowWeight="1"
tools:text="Test"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/dialog_text"
android:textAppearance="?android:attr/textAppearanceListItem"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingTop="10dp"
android:requiresFadingEdge="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_rowWeight="1" />
</LinearLayout>

View file

@ -688,13 +688,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>