Merge branch 'master' into AndroidTVSeekBehavior

# Conflicts:
#	app/src/main/res/values-es/strings.xml
This commit is contained in:
Terry Hanoman 2023-01-31 22:53:29 -05:00
commit 537d764953
80 changed files with 2468 additions and 331 deletions

View file

@ -53,6 +53,18 @@ jobs:
Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
Found provider name: `${{ steps.provider_check.outputs.name }}`
- name: Label if mentions provider
if: steps.provider_check.outputs.name != 'none'
uses: actions/github-script@v6
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ["possible provider issue"]
})
- name: Add eyes reaction to all issues
uses: actions-cool/emoji-helper@v1.0.0
with:

View file

@ -47,8 +47,8 @@ android {
minSdk = 21
targetSdk = 33
versionCode = 56
versionName = "3.5.0"
versionCode = 57
versionName = "4.0.0"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
@ -190,7 +190,7 @@ dependencies {
// Networking
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
implementation("com.github.Blatzar:NiceHttp:0.4.1")
implementation("com.github.Blatzar:NiceHttp:0.4.2")
// To fix SSL fuckery on android 9
implementation("org.conscrypt:conscrypt-android:2.2.1")
// Util to skip the URI file fuckery 🙏
@ -220,6 +220,9 @@ dependencies {
// Library/extensions searching with Levenshtein distance
implementation("me.xdrop:fuzzywuzzy:1.4.0")
// color pallette for images -> colors
implementation("androidx.palette:palette-ktx:1.0.0")
}
tasks.register("androidSourcesJar", Jar::class) {

View file

@ -13,7 +13,10 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
@ -510,6 +513,20 @@ abstract class MainAPI {
open val hasMainPage = false
open val hasQuickSearch = false
/**
* A set of which ids the provider can open with getLoadUrl()
* If the set contains SyncIdName.Imdb then getLoadUrl() can be started with
* an Imdb class which inherits from SyncId.
*
* getLoadUrl() is then used to get page url based on that ID.
*
* Example:
* "tt6723592" -> getLoadUrl(ImdbSyncId("tt6723592")) -> "mainUrl/imdb/tt6723592" -> load("mainUrl/imdb/tt6723592")
*
* This is used to launch pages from personal lists or recommendations using IDs.
**/
open val supportedSyncNames = setOf<SyncIdName>()
open val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
@ -580,6 +597,14 @@ abstract class MainAPI {
open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
return null
}
/**
* Get the load() url based on a sync ID like IMDb or MAL.
* Only contains SyncIds based on supportedSyncUrls.
**/
open suspend fun getLoadUrl(name: SyncIdName, id: String): String? {
return null
}
}
/** Might need a different implementation for desktop*/

View file

@ -347,7 +347,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
var lastPopup : SearchResponse? = null
var lastPopup: SearchResponse? = null
fun loadPopup(result: SearchResponse) {
lastPopup = result
viewModel.load(
@ -388,6 +388,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val isNavVisible = listOf(
R.id.navigation_home,
R.id.navigation_search,
R.id.navigation_library,
R.id.navigation_downloads,
R.id.navigation_settings,
R.id.navigation_download_child,
@ -438,6 +439,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
nav_view?.isVisible = isNavVisible && !landscape
nav_rail_view?.isVisible = isNavVisible && landscape
// Hide library on TV since it is not supported yet :(
val isTrueTv = isTrueTvSettings()
nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
}
//private var mCastSession: CastSession? = null
@ -710,7 +716,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
changeStatusBarState(isEmulatorSettings())
if (lastError == null) {
if (PluginManager.checkSafeModeFile()) {
normalSafeApiCall {
showToast(this, R.string.safe_mode_file, Toast.LENGTH_LONG)
}
} else if (lastError == null) {
ioSafe {
getKey<String>(USER_SELECTED_HOMEPAGE_API)?.let { homeApi ->
mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi))

View file

@ -1,32 +1,51 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import java.net.URI
class FileMoon : Filesim() {
override val mainUrl = "https://filemoon.to"
override val name = "FileMoon"
}
open class Filesim : ExtractorApi() {
override val name = "Filesim"
override val mainUrl = "https://files.im"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
with(app.get(url).document) {
this.select("script").map { script ->
this.select("script").forEach { script ->
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
val data = getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]")
tryParseJson<List<ResponseSource>>("[$data]")?.map {
M3u8Helper.generateM3u8(
val data = getAndUnpack(script.data())
val foundData = Regex("""sources:\[(.*?)]""").find(data)?.groupValues?.get(1) ?: return@forEach
val fixedData = foundData.replace("file:", """"file":""")
parseJson<List<ResponseSource>>("[$fixedData]").forEach {
callback.invoke(
ExtractorLink(
name,
name,
it.file,
"$mainUrl/",
).forEach { m3uData -> sources.add(m3uData) }
Qualities.Unknown.value,
URI(it.file).path.endsWith(".m3u8")
)
)
}
}
}
}
return sources
}
private data class ResponseSource(

View file

@ -6,6 +6,11 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
class Vanfem : GuardareStream() {
override var name = "Vanfem"
override var mainUrl = "https://vanfem.com/"
}
class CineGrabber : GuardareStream() {
override var name = "CineGrabber"
override var mainUrl = "https://cinegrabber.com"

View file

@ -59,8 +59,8 @@ open class VidSrcExtractor : ExtractorApi() {
if (datahash.isNotBlank()) {
val links = try {
app.get(
"$absoluteUrl/src/$datahash",
referer = "https://source.vidsrc.me/"
"$absoluteUrl/srcrcp/$datahash",
referer = "https://rcp.vidsrc.me/"
).url
} catch (e: Exception) {
""
@ -71,7 +71,7 @@ open class VidSrcExtractor : ExtractorApi() {
serverslist.amap { server ->
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
if (linkfixed.contains("/pro")) {
if (linkfixed.contains("/prorcp")) {
val srcresponse = app.get(server, referer = absoluteUrl).text
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap

View file

@ -1,30 +0,0 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.utils.SyncUtil
object SyncRedirector {
val syncApis = SyncApis
suspend fun redirect(url: String, preferredUrl: String): String {
for (api in syncApis) {
if (url.contains(api.mainUrl)) {
val otherApi = when (api.name) {
aniListApi.name -> "anilist"
malApi.name -> "myanimelist"
else -> return url
}
return SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
realUrl.contains(preferredUrl)
} ?: run {
throw ErrorLoadingException("Page does not exist on $preferredUrl")
}
}
}
return url
}
}

View file

@ -0,0 +1,56 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncIdName
object SyncRedirector {
val syncApis = SyncApis
private val syncIds =
listOf(
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
)
suspend fun redirect(
url: String,
providerApi: MainAPI
): String {
// Deprecated since providers should do this instead!
// Tries built in ID -> ProviderUrl
/*
for (api in syncApis) {
if (url.contains(api.mainUrl)) {
val otherApi = when (api.name) {
aniListApi.name -> "anilist"
malApi.name -> "myanimelist"
else -> return url
}
SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
realUrl.contains(providerApi.mainUrl)
}?.let {
return it
}
// ?: run {
// throw ErrorLoadingException("Page does not exist on $preferredUrl")
// }
}
}
*/
// Tries provider solution
// This goes through all sync ids and finds supported id by said provider
return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) ->
if (providerApi.supportedSyncNames.contains(syncName)) {
syncRegex.find(url)?.value?.let {
suspendSafeApiCall {
providerApi.getLoadUrl(syncName, it)
}
}
} else null
} ?: url
}
}

View file

@ -144,8 +144,10 @@ object PluginManager {
return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray()
}
private val LOCAL_PLUGINS_PATH =
Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins"
private val CLOUD_STREAM_FOLDER =
Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/"
private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins"
public var currentlyLoading: String? = null
@ -421,6 +423,21 @@ object PluginManager {
afterPluginsLoadedEvent.invoke(forceReload)
}
/**
* This can be used to override any extension loading to fix crashes!
* @return true if safe mode file is present
**/
fun checkSafeModeFile(): Boolean {
return normalSafeApiCall {
val folder = File(CLOUD_STREAM_FOLDER)
if (!folder.exists()) return@normalSafeApiCall false
val files = folder.listFiles { _, name ->
name.equals("safe", ignoreCase = true)
}
files?.any()
} ?: false
}
/**
* @return True if successful, false if not
* */

View file

@ -13,6 +13,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
val openSubtitlesApi = OpenSubtitlesApi(0)
val indexSubtitlesApi = IndexSubtitleApi()
val addic7ed = Addic7ed()
val localListApi = LocalList()
// used to login via app intent
val OAuth2Apis
@ -29,7 +30,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
// used for active syncing
val SyncApis
get() = listOf(
SyncRepo(malApi), SyncRepo(aniListApi)
SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi)
)
val inAppAuths

View file

@ -1,10 +1,31 @@
package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.UiText
import me.xdrop.fuzzywuzzy.FuzzySearch
enum class SyncIdName {
Anilist,
MyAnimeList,
Trakt,
Imdb,
LocalList
}
interface SyncAPI : OAuth2API {
/**
* Set this to true if the user updates something on the list like watch status or score
**/
var requireLibraryRefresh: Boolean
val mainUrl: String
/**
* Allows certain providers to open pages from
* library links.
**/
val syncIdName: SyncIdName
/**
-1 -> None
0 -> Watching
@ -22,7 +43,9 @@ interface SyncAPI : OAuth2API {
suspend fun search(name: String): List<SyncSearchResult>?
fun getIdFromUrl(url : String) : String
suspend fun getPersonalLibrary(): LibraryMetadata?
fun getIdFromUrl(url: String): String
data class SyncSearchResult(
override val name: String,
@ -42,7 +65,7 @@ interface SyncAPI : OAuth2API {
val score: Int?,
val watchedEpisodes: Int?,
var isFavorite: Boolean? = null,
var maxEpisodes : Int? = null,
var maxEpisodes: Int? = null,
)
data class SyncResult(
@ -63,9 +86,9 @@ interface SyncAPI : OAuth2API {
var genres: List<String>? = null,
var synonyms: List<String>? = null,
var trailers: List<String>? = null,
var isAdult : Boolean? = null,
var isAdult: Boolean? = null,
var posterUrl: String? = null,
var backgroundPosterUrl : String? = null,
var backgroundPosterUrl: String? = null,
/** In unixtime */
var startDate: Long? = null,
@ -76,4 +99,61 @@ interface SyncAPI : OAuth2API {
var prevSeason: SyncSearchResult? = null,
var actors: List<ActorData>? = null,
)
data class Page(
val title: UiText, var items: List<LibraryItem>
) {
fun sort(method: ListSorting?, query: String? = null) {
items = when (method) {
ListSorting.Query ->
if (query != null) {
items.sortedBy {
-FuzzySearch.partialRatio(
query.lowercase(), it.name.lowercase()
)
}
} else items
ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating ?: 0) }
ListSorting.RatingLow -> items.sortedBy { (it.personalRating ?: 0) }
ListSorting.AlphabeticalA -> items.sortedBy { it.name }
ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime }
else -> items
}
}
}
data class LibraryMetadata(
val allLibraryLists: List<LibraryList>,
val supportedListSorting: Set<ListSorting>
)
data class LibraryList(
val name: UiText,
val items: List<LibraryItem>
)
data class LibraryItem(
override val name: String,
override val url: String,
/**
* Unique unchanging string used for data storage.
* This should be the actual id when you change scores and status
* since score changes from library might get added in the future.
**/
val syncId: String,
val episodesCompleted: Int?,
val episodesTotal: Int?,
/** Out of 100 */
val personalRating: Int?,
val lastUpdatedUnixTime: Long?,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override var posterHeaders: Map<String, String>?,
override var quality: SearchQuality?,
override var id: Int? = null,
) : SearchResponse
}

View file

@ -11,26 +11,38 @@ class SyncRepo(private val repo: SyncAPI) {
val icon = repo.icon
val mainUrl = repo.mainUrl
val requiresLogin = repo.requiresLogin
val syncIdName = repo.syncIdName
var requireLibraryRefresh: Boolean
get() = repo.requireLibraryRefresh
set(value) {
repo.requireLibraryRefresh = value
}
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
return safeApiCall { repo.score(id, status) }
}
suspend fun getStatus(id : String) : Resource<SyncAPI.SyncStatus> {
suspend fun getStatus(id: String): Resource<SyncAPI.SyncStatus> {
return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") }
}
suspend fun getResult(id : String) : Resource<SyncAPI.SyncResult> {
suspend fun getResult(id: String): Resource<SyncAPI.SyncResult> {
return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") }
}
suspend fun search(query : String) : Resource<List<SyncAPI.SyncSearchResult>> {
suspend fun search(query: String): Resource<List<SyncAPI.SyncSearchResult>> {
return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() }
}
fun hasAccount() : Boolean {
suspend fun getPersonalLibrary(): Resource<SyncAPI.LibraryMetadata> {
return safeApiCall { repo.getPersonalLibrary() ?: throw ErrorLoadingException() }
}
fun hasAccount(): Boolean {
return normalSafeApiCall { repo.loginInfo() != null } ?: false
}
fun getIdFromUrl(url : String) : String = repo.getIdFromUrl(url)
fun getIdFromUrl(url: String): String? = normalSafeApiCall {
repo.getIdFromUrl(url)
}
}

View file

@ -1,10 +1,10 @@
package com.lagradost.cloudstream3.syncproviders.providers
import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.logError
@ -12,6 +12,9 @@ import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.toJson
@ -27,10 +30,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override val key = "6871"
override val redirectUrl = "anilistlogin"
override val idPrefix = "anilist"
override var requireLibraryRefresh = true
override var mainUrl = "https://anilist.co"
override val icon = R.drawable.ic_anilist_icon
override val requiresLogin = false
override val createAccountUrl = "$mainUrl/signup"
override val syncIdName = SyncIdName.Anilist
override fun loginInfo(): AuthAPI.LoginInfo? {
// context.getUser(true)?.
@ -45,6 +50,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
override fun logOut() {
requireLibraryRefresh = true
removeAccountKeys()
}
@ -64,8 +70,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
switchToNewAccount()
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
setKey(accountId, ANILIST_TOKEN_KEY, token)
setKey(ANILIST_SHOULD_UPDATE_LIST, true)
val user = getUser()
requireLibraryRefresh = true
return user != null
}
@ -140,7 +146,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
this.name,
recMedia.id?.toString() ?: return@mapNotNull null,
getUrlFromId(recMedia.id),
recMedia.coverImage?.large ?: recMedia.coverImage?.medium
recMedia.coverImage?.extraLarge ?: recMedia.coverImage?.large
?: recMedia.coverImage?.medium
)
},
trailers = when (season.trailer?.site?.lowercase()?.trim()) {
@ -170,7 +177,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
fromIntToAnimeStatus(status.status),
status.score,
status.watchedEpisodes
)
).also {
requireLibraryRefresh = requireLibraryRefresh || it
}
}
companion object {
@ -181,7 +190,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
const val ANILIST_TOKEN_KEY: String = "anilist_token" // anilist token for api
const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile
const val ANILIST_CACHED_LIST: String = "anilist_cached_list"
const val ANILIST_SHOULD_UPDATE_LIST: String = "anilist_should_update_list"
private fun fixName(name: String): String {
return name.lowercase(Locale.ROOT).replace(" ", "")
@ -219,7 +227,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
romaji
}
idMal
coverImage { medium large }
coverImage { medium large extraLarge }
averageScore
}
}
@ -232,7 +240,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
format
id
idMal
coverImage { medium large }
coverImage { medium large extraLarge }
averageScore
title {
english
@ -292,15 +300,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val shows = searchShows(name.replace(blackListRegex, ""))
shows?.data?.Page?.media?.find {
malId ?: "NONE" == it.idMal.toString()
(malId ?: "NONE") == it.idMal.toString()
}?.let { return it }
val filtered =
shows?.data?.Page?.media?.filter {
(
it.startDate.year ?: year.toString() == year.toString()
|| year == null
)
(((it.startDate.year ?: year.toString()) == year.toString()
|| year == null))
}
filtered?.forEach {
it.title.romaji?.let { romaji ->
@ -312,14 +318,14 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
// Changing names of these will show up in UI
enum class AniListStatusType(var value: Int) {
Watching(0),
Completed(1),
Paused(2),
Dropped(3),
Planning(4),
ReWatching(5),
None(-1)
enum class AniListStatusType(var value: Int, @StringRes val stringRes: Int) {
Watching(0, R.string.type_watching),
Completed(1, R.string.type_completed),
Paused(2, R.string.type_on_hold),
Dropped(3, R.string.type_dropped),
Planning(4, R.string.type_plan_to_watch),
ReWatching(5, R.string.type_re_watching),
None(-1, R.string.none)
}
fun fromIntToAnimeStatus(inp: Int): AniListStatusType {//= AniListStatusType.values().first { it.value == inp }
@ -335,7 +341,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
}
fun convertAnilistStringToStatus(string: String): AniListStatusType {
fun convertAniListStringToStatus(string: String): AniListStatusType {
return fromIntToAnimeStatus(aniListStatusString.indexOf(string))
}
@ -526,7 +532,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
app.post(
"https://graphql.anilist.co/",
headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return@suspendSafeApiCall null),
"Authorization" to "Bearer " + (getAuth()
?: return@suspendSafeApiCall null),
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
),
cacheTime = 0,
@ -575,7 +582,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
data class CoverImage(
@JsonProperty("medium") val medium: String?,
@JsonProperty("large") val large: String?
@JsonProperty("large") val large: String?,
@JsonProperty("extraLarge") val extraLarge: String?
)
data class Media(
@ -602,7 +610,29 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("score") val score: Int,
@JsonProperty("private") val private: Boolean,
@JsonProperty("media") val media: Media
) {
fun toLibraryItem(): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
// English title first
this.media.title.english ?: this.media.title.romaji
?: this.media.synonyms.firstOrNull()
?: "",
"https://anilist.co/anime/${this.media.id}/",
this.media.id.toString(),
this.progress,
this.media.episodes,
this.score,
this.updatedAt.toLong(),
"AniList",
TvType.Anime,
this.media.coverImage.extraLarge ?: this.media.coverImage.large
?: this.media.coverImage.medium,
null,
null,
null
)
}
}
data class Lists(
@JsonProperty("status") val status: String?,
@ -617,40 +647,59 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
)
fun getAnilistListCached(): Array<Lists>? {
private fun getAniListListCached(): Array<Lists>? {
return getKey(ANILIST_CACHED_LIST) as? Array<Lists>
}
suspend fun getAnilistAnimeListSmart(): Array<Lists>? {
private suspend fun getAniListAnimeListSmart(): Array<Lists>? {
if (getAuth() == null) return null
if (checkToken()) return null
return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) {
val list = getFullAnilistList()?.data?.MediaListCollection?.lists?.toTypedArray()
return if (requireLibraryRefresh) {
val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray()
if (list != null) {
setKey(ANILIST_CACHED_LIST, list)
setKey(ANILIST_SHOULD_UPDATE_LIST, false)
}
list
} else {
getAnilistListCached()
getAniListListCached()
}
}
private suspend fun getFullAnilistList(): FullAnilistList? {
var userID: Int? = null
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
val list = getAniListAnimeListSmart()?.groupBy {
convertAniListStringToStatus(it.status ?: "").stringRes
}?.mapValues { group ->
group.value.map { it.entries.map { entry -> entry.toLibraryItem() } }.flatten()
} ?: emptyMap()
// To fill empty lists when AniList does not return them
val baseMap =
AniListStatusType.values().filter { it.value >= 0 }.associate {
it.stringRes to emptyList<SyncAPI.LibraryItem>()
}
return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
)
}
private suspend fun getFullAniListList(): FullAnilistList? {
/** WARNING ASSUMES ONE USER! **/
getKeys(ANILIST_USER_KEY)?.forEach { key ->
getKey<AniListUser>(key, null)?.let {
userID = it.id
}
}
val fixedUserID = userID ?: return null
val userID = getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.id ?: return null
val mediaType = "ANIME"
val query = """
query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) {
query (${'$'}userID: Int = $userID, ${'$'}MEDIA: MediaType = $mediaType) {
MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) {
lists {
status
@ -661,7 +710,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
startedAt { year month day }
updatedAt
progress
score
score (format: POINT_100)
private
media
{
@ -677,7 +726,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
english
romaji
}
coverImage { medium }
coverImage { extraLarge large medium }
synonyms
nextAiringEpisode {
timeUntilAiring

View file

@ -0,0 +1,100 @@
package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
class LocalList : SyncAPI {
override val name = "Local"
override val icon: Int = R.drawable.ic_baseline_storage_24
override val requiresLogin = false
override val createAccountUrl: Nothing? = null
override val idPrefix = "local"
override var requireLibraryRefresh = true
override fun loginInfo(): AuthAPI.LoginInfo {
return AuthAPI.LoginInfo(
null,
null,
0
)
}
override fun logOut() {
}
override val key: String = ""
override val redirectUrl = ""
override suspend fun handleRedirect(url: String): Boolean {
return true
}
override fun authenticate(activity: FragmentActivity?) {
}
override val mainUrl = ""
override val syncIdName = SyncIdName.LocalList
override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
return true
}
override suspend fun getStatus(id: String): SyncAPI.SyncStatus? {
return null
}
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
return null
}
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
return null
}
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? {
val watchStatusIds = ioWork {
getAllWatchStateIds()?.map { id ->
Pair(id, getResultWatchState(id))
}
}?.distinctBy { it.first } ?: return null
val list = ioWork {
watchStatusIds.groupBy {
it.second.stringRes
}.mapValues { group ->
group.value.mapNotNull {
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
}
}
}
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
// None is not something to display
it.stringRes to emptyList<SyncAPI.LibraryItem>()
}
return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
// ListSorting.UpdatedNew,
// ListSorting.UpdatedOld,
// ListSorting.RatingHigh,
// ListSorting.RatingLow,
)
)
}
override fun getIdFromUrl(url: String): String {
return url
}
}

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Base64
import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
@ -8,11 +9,15 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
@ -31,13 +36,15 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override val redirectUrl = "mallogin"
override val idPrefix = "mal"
override var mainUrl = "https://myanimelist.net"
val apiUrl = "https://api.myanimelist.net"
private val apiUrl = "https://api.myanimelist.net"
override val icon = R.drawable.mal_logo
override val requiresLogin = false
override val syncIdName = SyncIdName.MyAnimeList
override var requireLibraryRefresh = true
override val createAccountUrl = "$mainUrl/register.php"
override fun logOut() {
requireLibraryRefresh = true
removeAccountKeys()
}
@ -90,7 +97,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
fromIntToAnimeStatus(status.status),
status.score,
status.watchedEpisodes
)
).also {
requireLibraryRefresh = requireLibraryRefresh || it
}
}
data class MalAnime(
@ -248,10 +257,45 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
const val MAL_USER_KEY: String = "mal_user" // user data like profile
const val MAL_CACHED_LIST: String = "mal_cached_list"
const val MAL_SHOULD_UPDATE_LIST: String = "mal_should_update_list"
const val MAL_UNIXTIME_KEY: String = "mal_unixtime" // When token expires
const val MAL_REFRESH_TOKEN_KEY: String = "mal_refresh_token" // refresh token
const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api
fun convertToStatus(string: String): MalStatusType {
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
}
enum class MalStatusType(var value: Int, @StringRes val stringRes: Int) {
Watching(0, R.string.type_watching),
Completed(1, R.string.type_completed),
OnHold(2, R.string.type_on_hold),
Dropped(3, R.string.type_dropped),
PlanToWatch(4, R.string.type_plan_to_watch),
None(-1, R.string.type_none)
}
private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp }
return when (inp) {
-1 -> MalStatusType.None
0 -> MalStatusType.Watching
1 -> MalStatusType.Completed
2 -> MalStatusType.OnHold
3 -> MalStatusType.Dropped
4 -> MalStatusType.PlanToWatch
5 -> MalStatusType.Watching
else -> MalStatusType.None
}
}
private fun parseDateLong(string: String?): Long? {
return try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(
string ?: return null
)?.time?.div(1000)
} catch (e: Exception) {
null
}
}
}
override suspend fun handleRedirect(url: String): Boolean {
@ -275,7 +319,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
switchToNewAccount()
storeToken(res)
val user = getMalUser()
setKey(MAL_SHOULD_UPDATE_LIST, true)
requireLibraryRefresh = true
return user != null
}
}
@ -308,9 +352,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime))
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token)
setKey(accountId, MAL_TOKEN_KEY, token.access_token)
requireLibraryRefresh = true
}
} catch (e: Exception) {
e.printStackTrace()
logError(e)
}
}
@ -329,7 +374,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
).text
storeToken(res)
} catch (e: Exception) {
e.printStackTrace()
logError(e)
}
}
@ -382,7 +427,24 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class Data(
@JsonProperty("node") val node: Node,
@JsonProperty("list_status") val list_status: ListStatus?,
) {
fun toLibraryItem(): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
this.node.title,
"https://myanimelist.net/anime/${this.node.id}/",
this.node.id.toString(),
this.list_status?.num_episodes_watched,
this.node.num_episodes,
this.list_status?.score?.times(10),
parseDateLong(this.list_status?.updated_at),
"MAL",
TvType.Anime,
this.node.main_picture?.large ?: this.node.main_picture?.medium,
null,
null,
)
}
}
data class Paging(
@JsonProperty("next") val next: String?
@ -413,18 +475,43 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return getKey(MAL_CACHED_LIST) as? Array<Data>
}
suspend fun getMalAnimeListSmart(): Array<Data>? {
private suspend fun getMalAnimeListSmart(): Array<Data>? {
if (getAuth() == null) return null
return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) {
return if (requireLibraryRefresh) {
val list = getMalAnimeList()
setKey(MAL_CACHED_LIST, list)
setKey(MAL_SHOULD_UPDATE_LIST, false)
list
} else {
getMalAnimeListCached()
}
}
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
val list = getMalAnimeListSmart()?.groupBy {
convertToStatus(it.list_status?.status ?: "").stringRes
}?.mapValues { group ->
group.value.map { it.toLibraryItem() }
} ?: emptyMap()
// To fill empty lists when MAL does not return them
val baseMap =
MalStatusType.values().filter { it.value >= 0 }.associate {
it.stringRes to emptyList<SyncAPI.LibraryItem>()
}
return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
)
}
private suspend fun getMalAnimeList(): Array<Data> {
checkMalToken()
var offset = 0
@ -440,10 +527,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return fullList.toTypedArray()
}
fun convertToStatus(string: String): MalStatusType {
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
}
private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? {
val user = "@me"
val auth = getAuth() ?: return null
@ -557,28 +640,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return user
}
enum class MalStatusType(var value: Int) {
Watching(0),
Completed(1),
OnHold(2),
Dropped(3),
PlanToWatch(4),
None(-1)
}
private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp }
return when (inp) {
-1 -> MalStatusType.None
0 -> MalStatusType.Watching
1 -> MalStatusType.Completed
2 -> MalStatusType.OnHold
3 -> MalStatusType.Dropped
4 -> MalStatusType.PlanToWatch
5 -> MalStatusType.Watching
else -> MalStatusType.None
}
}
private suspend fun setScoreRequest(
id: Int,
status: MalStatusType? = null,

View file

@ -7,7 +7,8 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManager(context, _spanCount) {
class GrdLayoutManager(val context: Context, _spanCount: Int) :
GridLayoutManager(context, _spanCount) {
override fun onFocusSearchFailed(
focused: View,
focusDirection: Int,
@ -34,7 +35,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManage
val pos = maxOf(0, getPosition(focused!!) - 2)
parent.scrollToPosition(pos)
super.onRequestChildFocus(parent, state, child, focused)
} catch (e: Exception){
} catch (e: Exception) {
false
}
}

View file

@ -569,7 +569,7 @@ class HomeFragment : Fragment() {
val mutableListOfResponse = mutableListOf<SearchResponse>()
listHomepageItems.clear()
(home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(
d.values.toMutableList(),
home_master_recycler
)
@ -621,7 +621,7 @@ class HomeFragment : Fragment() {
//home_loaded?.isVisible = false
}
is Resource.Loading -> {
(home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(listOf())
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(listOf())
home_loading_shimmer?.startShimmer()
home_loading?.isVisible = true
home_loading_error?.isVisible = false

View file

@ -0,0 +1,393 @@
package com.lagradost.cloudstream3.ui.library
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import com.google.android.material.tabs.TabLayoutMediator
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import kotlinx.android.synthetic.main.fragment_library.*
import kotlin.math.abs
const val LIBRARY_FOLDER = "library_folder"
enum class LibraryOpenerType(@StringRes val stringRes: Int) {
Default(R.string.default_subtitles), // TODO FIX AFTER MERGE
Provider(R.string.none),
Browser(R.string.browser),
Search(R.string.search),
None(R.string.none),
}
/** Used to store how the user wants to open said poster */
data class LibraryOpener(
val openType: LibraryOpenerType,
val providerData: ProviderLibraryData?,
)
data class ProviderLibraryData(
val apiName: String
)
class LibraryFragment : Fragment() {
companion object {
fun newInstance() = LibraryFragment()
/**
* Store which page was last seen when exiting the fragment and returning
**/
const val VIEWPAGER_ITEM_KEY = "viewpager_item"
}
private val libraryViewModel: LibraryViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_library, container, false)
}
override fun onSaveInstanceState(outState: Bundle) {
viewpager?.currentItem?.let { currentItem ->
outState.putInt(VIEWPAGER_ITEM_KEY, currentItem)
}
super.onSaveInstanceState(outState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(search_status_bar_padding)
sort_fab?.setOnClickListener {
val methods = libraryViewModel.sortingMethods.map {
txt(it.stringRes).asString(view.context)
}
activity?.showBottomDialog(methods,
libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod),
txt(R.string.sort_by).asString(view.context),
false,
{},
{
val method = libraryViewModel.sortingMethods[it]
libraryViewModel.sort(method)
})
}
main_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
libraryViewModel.sort(ListSorting.Query, query)
return true
}
// This is required to prevent the first text change
// When this is attached it'll immediately send a onQueryTextChange("")
// Which we do not want
var hasInitialized = false
override fun onQueryTextChange(newText: String?): Boolean {
if (!hasInitialized) {
hasInitialized = true
return true
}
libraryViewModel.sort(ListSorting.Query, newText)
return true
}
})
libraryViewModel.reloadPages(false)
list_selector?.setOnClickListener {
val items = libraryViewModel.availableApiNames
val currentItem = libraryViewModel.currentApiName.value
activity?.showBottomDialog(items,
items.indexOf(currentItem),
txt(R.string.select_library).asString(it.context),
false,
{}) { index ->
val selectedItem = items.getOrNull(index) ?: return@showBottomDialog
libraryViewModel.switchList(selectedItem)
}
}
/**
* Shows a plugin selection dialogue and saves the response
**/
fun Activity.showPluginSelectionDialog(
key: String,
syncId: SyncIdName,
apiName: String? = null,
) {
val availableProviders = allProviders.filter {
it.supportedSyncNames.contains(syncId)
}.map { it.name } +
// Add the api if it exists
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } ?: emptyList())
val baseOptions = listOf(
LibraryOpenerType.Default,
LibraryOpenerType.None,
LibraryOpenerType.Browser,
LibraryOpenerType.Search
)
val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, key)
val selectedIndex =
when {
savedSelection == null -> 0
// If provider
savedSelection.openType == LibraryOpenerType.Provider
&& savedSelection.providerData?.apiName != null -> {
availableProviders.indexOf(savedSelection.providerData.apiName)
.takeIf { it != -1 }
?.plus(baseOptions.size) ?: 0
}
// Else base option
else -> baseOptions.indexOf(savedSelection.openType)
}
this.showBottomDialog(
items,
selectedIndex,
txt(R.string.open_with).asString(this),
false,
{},
) {
val savedData = if (it < baseOptions.size) {
LibraryOpener(
baseOptions[it],
null
)
} else {
LibraryOpener(
LibraryOpenerType.Provider,
ProviderLibraryData(items[it])
)
}
setKey(
LIBRARY_FOLDER,
key,
savedData,
)
}
}
provider_selector?.setOnClickListener {
val syncName = libraryViewModel.currentSyncApi?.syncIdName ?: return@setOnClickListener
activity?.showPluginSelectionDialog(syncName.name, syncName)
}
viewpager?.setPageTransformer(LibraryScrollTransformer())
viewpager?.adapter =
viewpager.adapter ?: ViewpagerAdapter(mutableListOf(), { isScrollingDown: Boolean ->
if (isScrollingDown) {
sort_fab?.shrink()
} else {
sort_fab?.extend()
}
}) callback@{ searchClickCallback ->
// To prevent future accidents
debugAssert({
searchClickCallback.card !is SyncAPI.LibraryItem
}, {
"searchClickCallback ${searchClickCallback.card} is not a LibraryItem"
})
val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId
val syncName =
libraryViewModel.currentSyncApi?.syncIdName ?: return@callback
when (searchClickCallback.action) {
SEARCH_ACTION_SHOW_METADATA -> {
activity?.showPluginSelectionDialog(
syncId,
syncName,
searchClickCallback.card.apiName
)
}
SEARCH_ACTION_LOAD -> {
// This basically first selects the individual opener and if that is default then
// selects the whole list opener
val savedListSelection =
getKey<LibraryOpener>(LIBRARY_FOLDER, syncName.name)
val savedSelection = getKey<LibraryOpener>(LIBRARY_FOLDER, syncId).takeIf {
it?.openType != LibraryOpenerType.Default
} ?: savedListSelection
when (savedSelection?.openType) {
null, LibraryOpenerType.Default -> {
// Prevents opening MAL/AniList as a provider
if (APIHolder.getApiFromNameNull(searchClickCallback.card.apiName) != null) {
activity?.loadSearchResult(
searchClickCallback.card
)
} else {
// Search when no provider can open
QuickSearchFragment.pushSearch(
activity,
searchClickCallback.card.name
)
}
}
LibraryOpenerType.None -> {}
LibraryOpenerType.Provider ->
savedSelection.providerData?.apiName?.let { apiName ->
activity?.loadResult(
searchClickCallback.card.url,
apiName,
)
}
LibraryOpenerType.Browser ->
openBrowser(searchClickCallback.card.url)
LibraryOpenerType.Search -> {
QuickSearchFragment.pushSearch(
activity,
searchClickCallback.card.name
)
}
}
}
}
}
viewpager?.offscreenPageLimit = 2
viewpager?.reduceDragSensitivity()
val startLoading = Runnable {
gridview?.numColumns = context?.getSpanCount() ?: 3
gridview?.adapter =
context?.let { LoadingPosterAdapter(it, 6 * 3) }
library_loading_overlay?.isVisible = true
library_loading_shimmer?.startShimmer()
empty_list_textview?.isVisible = false
}
val stopLoading = Runnable {
gridview?.adapter = null
library_loading_overlay?.isVisible = false
library_loading_shimmer?.stopShimmer()
}
val handler = Handler(Looper.getMainLooper())
observe(libraryViewModel.pages) { resource ->
when (resource) {
is Resource.Success -> {
handler.removeCallbacks(startLoading)
val pages = resource.value
val showNotice = pages.all { it.items.isEmpty() }
empty_list_textview?.isVisible = showNotice
if (showNotice) {
if (libraryViewModel.availableApiNames.size > 1) {
empty_list_textview?.setText(R.string.empty_library_logged_in_message)
} else {
empty_list_textview?.setText(R.string.empty_library_no_accounts_message)
}
}
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
// Using notifyItemRangeChanged keeps the animations when sorting
viewpager.adapter?.notifyItemRangeChanged(0, viewpager.adapter?.itemCount ?: 0)
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
// Without this there would be a flashing effect:
// loading -> show old viewpager -> black screen -> show new viewpager
handler.postDelayed(stopLoading, 300)
savedInstanceState?.getInt(VIEWPAGER_ITEM_KEY)?.let { currentPos ->
viewpager?.setCurrentItem(currentPos, false)
savedInstanceState.remove(VIEWPAGER_ITEM_KEY)
}
// Since the animation to scroll multiple items is so much its better to just hide
// the viewpager a bit while the fastest animation is running
fun hideViewpager(distance: Int) {
if (distance < 3) return
val hideAnimation = AlphaAnimation(1f, 0f).apply {
duration = distance * 50L
fillAfter = true
}
val showAnimation = AlphaAnimation(0f, 1f).apply {
duration = distance * 50L
startOffset = distance * 100L
fillAfter = true
}
viewpager?.startAnimation(hideAnimation)
viewpager?.startAnimation(showAnimation)
}
TabLayoutMediator(
library_tab_layout,
viewpager,
) { tab, position ->
tab.text = pages.getOrNull(position)?.title?.asStringNull(context)
tab.view.setOnClickListener {
val currentItem = viewpager?.currentItem ?: return@setOnClickListener
val distance = abs(position - currentItem)
hideViewpager(distance)
}
}.attach()
}
is Resource.Loading -> {
// Only start loading after 200ms to prevent loading cached lists
handler.postDelayed(startLoading, 200)
}
is Resource.Failure -> {
stopLoading.run()
// No user indication it failed :(
// TODO
}
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
(viewpager.adapter as? ViewpagerAdapter)?.rebind()
super.onConfigurationChanged(newConfig)
}
}
class MenuSearchView(context: Context) : SearchView(context) {
override fun onActionViewCollapsed() {
super.onActionViewCollapsed()
}
}

View file

@ -0,0 +1,17 @@
package com.lagradost.cloudstream3.ui.library
import android.view.View
import androidx.viewpager2.widget.ViewPager2
import kotlinx.android.synthetic.main.library_viewpager_page.view.*
import kotlin.math.roundToInt
class LibraryScrollTransformer : ViewPager2.PageTransformer {
override fun transformPage(page: View, position: Float) {
val padding = (-position * page.width).roundToInt()
page.page_recyclerview.setPadding(
padding, 0,
-padding, 0
)
}
}

View file

@ -0,0 +1,104 @@
package com.lagradost.cloudstream3.ui.library
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import kotlinx.coroutines.delay
enum class ListSorting(@StringRes val stringRes: Int) {
Query(R.string.none),
RatingHigh(R.string.sort_rating_desc),
RatingLow(R.string.sort_rating_asc),
UpdatedNew(R.string.sort_updated_new),
UpdatedOld(R.string.sort_updated_old),
AlphabeticalA(R.string.sort_alphabetical_a),
AlphabeticalZ(R.string.sort_alphabetical_z),
}
const val LAST_SYNC_API_KEY = "last_sync_api"
class LibraryViewModel : ViewModel() {
private val _pages: MutableLiveData<Resource<List<SyncAPI.Page>>> = MutableLiveData(null)
val pages: LiveData<Resource<List<SyncAPI.Page>>> = _pages
private val _currentApiName: MutableLiveData<String> = MutableLiveData("")
val currentApiName: LiveData<String> = _currentApiName
private val availableSyncApis
get() = SyncApis.filter { it.hasAccount() }
var currentSyncApi = availableSyncApis.let { allApis ->
val lastSelection = getKey<String>(LAST_SYNC_API_KEY)
availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull()
}
private set(value) {
field = value
setKey(LAST_SYNC_API_KEY, field?.name)
}
val availableApiNames: List<String>
get() = availableSyncApis.map { it.name }
var sortingMethods = emptyList<ListSorting>()
private set
var currentSortingMethod: ListSorting? = sortingMethods.firstOrNull()
private set
fun switchList(name: String) {
currentSyncApi = availableSyncApis[availableApiNames.indexOf(name)]
_currentApiName.postValue(currentSyncApi?.name)
reloadPages(true)
}
fun sort(method: ListSorting, query: String? = null) {
val currentList = pages.value ?: return
currentSortingMethod = method
(currentList as? Resource.Success)?.value?.forEachIndexed { _, page ->
page.sort(method, query)
}
_pages.postValue(currentList)
}
fun reloadPages(forceReload: Boolean) {
// Only skip loading if its not forced and pages is not empty
if (!forceReload && (pages.value as? Resource.Success)?.value?.isNotEmpty() == true &&
currentSyncApi?.requireLibraryRefresh != true
) return
ioSafe {
currentSyncApi?.let { repo ->
_currentApiName.postValue(repo.name)
_pages.postValue(Resource.Loading())
val libraryResource = repo.getPersonalLibrary()
if (libraryResource is Resource.Failure) {
_pages.postValue(libraryResource)
return@let
}
val library = (libraryResource as? Resource.Success)?.value ?: return@let
sortingMethods = library.supportedListSorting.toList()
currentSortingMethod = null
repo.requireLibraryRefresh = false
val pages = library.allLibraryLists.map {
SyncAPI.Page(
it.name,
it.items
)
}
_pages.postValue(Resource.Success(pages))
}
}
}
}

View file

@ -0,0 +1,37 @@
package com.lagradost.cloudstream3.ui.library
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ListPopupWindow.MATCH_PARENT
import android.widget.RelativeLayout
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.loading_poster_dynamic.view.*
import kotlin.math.roundToInt
import kotlin.math.sqrt
class LoadingPosterAdapter(context: Context, private val itemCount: Int) :
BaseAdapter() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
override fun getCount(): Int {
return itemCount
}
override fun getItem(position: Int): Any? {
return null
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return convertView ?: inflater.inflate(R.layout.loading_poster_dynamic, parent, false)
}
}

View file

@ -0,0 +1,130 @@
package com.lagradost.cloudstream3.ui.library
import android.content.res.ColorStateList
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.search_result_grid_expanded.view.*
import kotlin.math.roundToInt
class PageAdapter(
override val items: MutableList<SyncAPI.LibraryItem>,
private val resView: AutofitRecyclerView,
val clickCallback: (SearchClickCallback) -> Unit
) :
AppUtils.DiffAdapter<SyncAPI.LibraryItem>(items) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return LibraryItemViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.search_result_grid_expanded, parent, false)
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is LibraryItemViewHolder -> {
holder.bind(items[position], position)
}
}
}
private fun isDark(color: Int): Boolean {
return ColorUtils.calculateLuminance(color) < 0.5
}
fun getDifferentColor(color: Int, ratio: Float = 0.7f): Int {
return if (isDark(color)) {
ColorUtils.blendARGB(color, Color.WHITE, ratio)
} else {
ColorUtils.blendARGB(color, Color.BLACK, ratio)
}
}
inner class LibraryItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val cardView: ImageView = itemView.imageView
private val compactView = false//itemView.context.getGridIsCompact()
private val coverHeight: Int =
if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt()
fun bind(item: SyncAPI.LibraryItem, position: Int) {
/** https://stackoverflow.com/questions/8817522/how-to-get-color-code-of-image-view */
SearchResultBuilder.bind(
this@PageAdapter.clickCallback,
item,
position,
itemView,
colorCallback = { palette ->
AcraApplication.context?.let { ctx ->
val defColor = ContextCompat.getColor(ctx, R.color.ratingColorBg)
var bg = palette.getDarkVibrantColor(defColor)
if (bg == defColor) {
bg = palette.getDarkMutedColor(defColor)
}
if (bg == defColor) {
bg = palette.getVibrantColor(defColor)
}
val fg =
getDifferentColor(bg)//palette.getVibrantColor(ContextCompat.getColor(ctx,R.color.ratingColor))
itemView.text_rating.apply {
setTextColor(ColorStateList.valueOf(fg))
}
itemView.text_rating_holder?.backgroundTintList = ColorStateList.valueOf(bg)
itemView.watchProgress?.apply {
progressTintList = ColorStateList.valueOf(fg)
progressBackgroundTintList = ColorStateList.valueOf(bg)
}
}
}
)
// See searchAdaptor for this, it basically fixes the height
if (!compactView) {
cardView.apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
coverHeight
)
}
}
val showProgress = item.episodesCompleted != null && item.episodesTotal != null
itemView.watchProgress.isVisible = showProgress
if (showProgress) {
itemView.watchProgress.max = item.episodesTotal!!
itemView.watchProgress.progress = item.episodesCompleted!!
}
itemView.imageText.text = item.name
val showRating = (item.personalRating ?: 0) != 0
itemView.text_rating_holder.isVisible = showRating
if (showRating) {
// We want to show 8.5 but not 8.0 hence the replace
val rating = ((item.personalRating ?: 0).toDouble() / 10).toString()
.replace(".0", "")
itemView.text_rating.text = "$rating"
}
}
}
}

View file

@ -0,0 +1,90 @@
package com.lagradost.cloudstream3.ui.library
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.doOnAttach
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnFlingListener
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import kotlinx.android.synthetic.main.library_viewpager_page.view.*
class ViewpagerAdapter(
var pages: List<SyncAPI.Page>,
val scrollCallback: (isScrollingDown: Boolean) -> Unit,
val clickCallback: (SearchClickCallback) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PageViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.library_viewpager_page, parent, false)
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is PageViewHolder -> {
holder.bind(pages[position], unbound.remove(position))
}
}
}
private val unbound = mutableSetOf<Int>()
/**
* Used to mark all pages for re-binding and forces all items to be refreshed
* Without this the pages will still use the same adapters
**/
fun rebind() {
unbound.addAll(0..pages.size)
this.notifyItemRangeChanged(0, pages.size)
}
inner class PageViewHolder(private val itemViewTest: View) :
RecyclerView.ViewHolder(itemViewTest) {
fun bind(page: SyncAPI.Page, rebind: Boolean) {
itemView.page_recyclerview?.spanCount =
this@PageViewHolder.itemView.context.getSpanCount() ?: 3
if (itemViewTest.page_recyclerview?.adapter == null || rebind) {
// Only add the items after it has been attached since the items rely on ItemWidth
// Which is only determined after the recyclerview is attached.
// If this fails then item height becomes 0 when there is only one item
itemViewTest.page_recyclerview?.doOnAttach {
itemViewTest.page_recyclerview?.adapter = PageAdapter(
page.items.toMutableList(),
itemViewTest.page_recyclerview,
clickCallback
)
}
} else {
(itemViewTest.page_recyclerview?.adapter as? PageAdapter)?.updateList(page.items)
itemViewTest.page_recyclerview?.scrollToPosition(0)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
itemViewTest.page_recyclerview.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
val diff = scrollY - oldScrollY
if (diff == 0) return@setOnScrollChangeListener
scrollCallback.invoke(diff > 0)
}
} else {
itemViewTest.page_recyclerview.onFlingListener = object : OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
scrollCallback.invoke(velocityY > 0)
return false
}
}
}
}
}
override fun getItemCount(): Int {
return pages.size
}
}

View file

@ -607,7 +607,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
player_top_holder?.isGone = isGone
//player_episodes_button?.isVisible = !isGone && hasEpisodes
player_video_title?.isGone = togglePlayerTitleGone
player_video_title_rez?.isGone = isGone
// player_video_title_rez?.isGone = isGone
player_episode_filler?.isGone = isGone
player_center_menu?.isGone = isGone
player_lock?.isGone = !isShowing

View file

@ -11,9 +11,7 @@ import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.*
import android.widget.TextView.OnEditorActionListener
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.animation.addListener
@ -528,7 +526,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
var selectSourceDialog: AlertDialog? = null
var selectSourceDialog: Dialog? = null
// var selectTracksDialog: AlertDialog? = null
override fun showMirrorsDialogue() {
@ -540,10 +538,8 @@ class GeneratorPlayer : FullScreenPlayer() {
player.handleEvent(CSPlayerEvent.Pause)
val currentSubtitles = sortSubs(currentSubs)
val sourceBuilder = AlertDialog.Builder(ctx, R.style.AlertDialogCustomBlack)
.setView(R.layout.player_select_source_and_subs)
val sourceDialog = sourceBuilder.create()
val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack)
sourceDialog.setContentView(R.layout.player_select_source_and_subs)
selectSourceDialog = sourceDialog
@ -1149,13 +1145,15 @@ class GeneratorPlayer : FullScreenPlayer() {
val source = currentSelectedLink?.first?.name ?: currentSelectedLink?.second?.name ?: "NULL"
player_video_title_rez?.text = when (titleRez) {
val title = when (titleRez) {
0 -> ""
1 -> extra
2 -> source
3 -> "$source - $extra"
else -> ""
}
player_video_title_rez?.text = title
player_video_title_rez?.isVisible = title.isNotBlank()
}
override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) {

View file

@ -220,7 +220,7 @@ class QuickSearchFragment : Fragment() {
when (it) {
is Resource.Success -> {
it.value.let { data ->
(quick_search_autofit_results?.adapter as? SearchAdapter?)?.updateList(
(quick_search_autofit_results?.adapter as? SearchAdapter)?.updateList(
context?.filterSearchResultByFilmQuality(data) ?: data
)
}

View file

@ -277,7 +277,7 @@ open class ResultFragment : ResultTrailerPlayer() {
private var downloadButton: EasyDownloadButton? = null
override fun onDestroyView() {
updateUIListener = null
(result_episodes?.adapter as EpisodeAdapter?)?.killAdapter()
(result_episodes?.adapter as? EpisodeAdapter)?.killAdapter()
downloadButton?.dispose()
super.onDestroyView()
@ -458,7 +458,7 @@ open class ResultFragment : ResultTrailerPlayer() {
temporary_no_focus?.requestFocus()
}
(result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value)
(result_episodes?.adapter as? EpisodeAdapter)?.updateList(episodes.value)
if (isTv && hasEpisodes) main {
delay(500)
@ -687,7 +687,7 @@ open class ResultFragment : ResultTrailerPlayer() {
val newList = list.filter { it.isSynced && it.hasAccount }
result_mini_sync?.isVisible = newList.isNotEmpty()
(result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.mapNotNull { it.icon })
(result_mini_sync?.adapter as? ImageAdapter)?.updateList(newList.mapNotNull { it.icon })
}
var currentSyncProgress = 0
@ -900,7 +900,7 @@ open class ResultFragment : ResultTrailerPlayer() {
result_cast_items?.isVisible = d.actors != null
(result_cast_items?.adapter as ActorAdaptor?)?.apply {
(result_cast_items?.adapter as? ActorAdaptor)?.apply {
updateList(d.actors ?: emptyList())
}

View file

@ -485,7 +485,7 @@ class ResultFragmentPhone : ResultFragment() {
result_recommendations?.post {
rec?.let { list ->
(result_recommendations?.adapter as SearchAdapter?)?.updateList(list.filter { it.apiName == matchAgainst })
(result_recommendations?.adapter as? SearchAdapter)?.updateList(list.filter { it.apiName == matchAgainst })
}
}
}

View file

@ -107,7 +107,7 @@ class ResultFragmentTv : ResultFragment() {
result_recommendations?.isGone = isInvalid
result_recommendations_holder?.isGone = isInvalid
val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName
(result_recommendations?.adapter as SearchAdapter?)?.updateList(rec?.filter { it.apiName == matchAgainst }
(result_recommendations?.adapter as? SearchAdapter)?.updateList(rec?.filter { it.apiName == matchAgainst }
?: emptyList())
rec?.map { it.apiName }?.distinct()?.let { apiNames ->

View file

@ -13,6 +13,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
@ -1443,12 +1444,18 @@ class ResultViewModel2 : ViewModel() {
val realRecommendations = ArrayList<SearchResponse>()
// TODO: fix
//val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
// meta.recommendations?.forEach { rec ->
// apiNames.forEach { name ->
// realRecommendations.add(rec.copy(apiName = name))
// }
// }
val apiNames = apis.filter {
it.name.contains("gogoanime", true) ||
it.name.contains("9anime", true)
}.map {
it.name
}
meta.recommendations?.forEach { rec ->
apiNames.forEach { name ->
realRecommendations.add(rec.copy(apiName = name))
}
}
recommendations = recommendations?.union(realRecommendations)?.toList()
?: realRecommendations
@ -2143,7 +2150,7 @@ class ResultViewModel2 : ViewModel() {
val validUrlResource = safeApiCall {
SyncRedirector.redirect(
url,
api.mainUrl
api
)
}
// TODO: fix

View file

@ -10,12 +10,16 @@ import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.search_result_compact.view.*
import kotlin.math.roundToInt
/** Click */
const val SEARCH_ACTION_LOAD = 0
/** Long press */
const val SEARCH_ACTION_SHOW_METADATA = 1
const val SEARCH_ACTION_PLAY_FILE = 2
const val SEARCH_ACTION_FOCUSED = 4

View file

@ -420,7 +420,7 @@ class SearchFragment : Fragment() {
is Resource.Success -> {
it.value.let { data ->
if (data.isNotEmpty()) {
(search_autofit_results?.adapter as SearchAdapter?)?.updateList(data)
(search_autofit_results?.adapter as? SearchAdapter)?.updateList(data)
}
}
searchExitIcon.alpha = 1f

View file

@ -1,12 +1,14 @@
package com.lagradost.cloudstream3.ui.search
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.core.view.isVisible
import androidx.palette.graphics.Palette
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
@ -41,6 +43,7 @@ object SearchResultBuilder {
nextFocusBehavior: Boolean? = null,
nextFocusUp: Int? = null,
nextFocusDown: Int? = null,
colorCallback : ((Palette) -> Unit)? = null
) {
val cardView: ImageView = itemView.imageView
val cardText: TextView? = itemView.imageText
@ -100,7 +103,7 @@ object SearchResultBuilder {
cardText?.isVisible = showTitle
cardView.isVisible = true
if (!cardView.setImage(card.posterUrl, card.posterHeaders)) {
if (!cardView.setImage(card.posterUrl, card.posterHeaders, colorCallback = colorCallback)) {
cardView.setImageResource(R.drawable.default_cover)
}

View file

@ -85,6 +85,7 @@ val appLanguages = arrayListOf(
Triple("\uD83C\uDDF5\uD83C\uDDF9", "Portuguese", "pt"),
Triple("", "Romanian", "ro"),
Triple("", "Russian", "ru"),
Triple("", "Slovak", "sk"),
Triple("", "Somali", "so"),
Triple("", "Swedish", "sv"),
Triple("", "Tamil", "ta"),

View file

@ -143,7 +143,7 @@ class PluginsFragment : Fragment() {
}
observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) ->
(plugin_recycler_view?.adapter as? PluginAdapter?)?.updateList(list)
(plugin_recycler_view?.adapter as? PluginAdapter)?.updateList(list)
if (scrollToTop)
plugin_recycler_view?.scrollToPosition(0)

View file

@ -28,10 +28,12 @@ import androidx.core.text.toSpanned
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.tvprovider.media.tv.*
import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
import androidx.viewpager2.widget.ViewPager2
import com.fasterxml.jackson.module.kotlin.readValue
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState
@ -65,6 +67,7 @@ import okhttp3.Cache
import java.io.*
import java.net.URL
import java.net.URLDecoder
import kotlin.system.measureTimeMillis
object AppUtils {
fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) {
@ -164,6 +167,18 @@ object AppUtils {
return builder.build()
}
// https://stackoverflow.com/a/67441735/13746422
fun ViewPager2.reduceDragSensitivity(f: Int = 4) {
val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
recyclerViewField.isAccessible = true
val recyclerView = recyclerViewField.get(this) as RecyclerView
val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop")
touchSlopField.isAccessible = true
val touchSlop = touchSlopField.get(recyclerView) as Int
touchSlopField.set(recyclerView, touchSlop * f) // "8" was obtained experimentally
}
@SuppressLint("RestrictedApi")
fun getAllWatchNextPrograms(context: Context): Set<Long> {
val COLUMN_WATCH_NEXT_ID_INDEX = 0
@ -329,6 +344,46 @@ object AppUtils {
}
}
abstract class DiffAdapter<T>(
open val items: MutableList<T>,
val comparison: (first: T, second: T) -> Boolean = { first, second ->
first.hashCode() == second.hashCode()
}
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun getItemCount(): Int {
return items.size
}
fun updateList(newList: List<T>) {
val diffResult = DiffUtil.calculateDiff(
GenericDiffCallback(this.items, newList)
)
items.clear()
items.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
inner class GenericDiffCallback(
private val oldList: List<T>,
private val newList: List<T>
) :
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
comparison(oldList[oldItemPosition], newList[newItemPosition])
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition]
}
}
fun Activity.downloadAllPluginsDialog(repositoryUrl: String, repositoryName: String) {
runOnUiThread {
val context = this

View file

@ -18,13 +18,11 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY
import com.lagradost.cloudstream3.plugins.PLUGINS_KEY_LOCAL
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_SHOULD_UPDATE_LIST
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_TOKEN_KEY
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_UNIXTIME_KEY
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_USER_KEY
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_CACHED_LIST
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_REFRESH_TOKEN_KEY
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_SHOULD_UPDATE_LIST
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_TOKEN_KEY
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY
@ -52,12 +50,10 @@ object BackupUtils {
// When sharing backup we do not want to transfer what is essentially the password
ANILIST_TOKEN_KEY,
ANILIST_CACHED_LIST,
ANILIST_SHOULD_UPDATE_LIST,
ANILIST_UNIXTIME_KEY,
ANILIST_USER_KEY,
MAL_TOKEN_KEY,
MAL_REFRESH_TOKEN_KEY,
MAL_SHOULD_UPDATE_LIST,
MAL_CACHED_LIST,
MAL_UNIXTIME_KEY,
MAL_USER_KEY,

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.utils
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder.capitalize
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
@ -10,6 +11,8 @@ import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.result.VideoWatchState
@ -51,7 +54,20 @@ object DataStoreHelper {
@JsonProperty("year") val year: Int?,
@JsonProperty("quality") override var quality: SearchQuality? = null,
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
) : SearchResponse
) : SearchResponse {
fun toLibraryItem(id: String): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
name,
url,
id,
null,
null,
null,
null,
apiName, type, posterUrl, posterHeaders, quality, this.id
)
}
}
data class ResumeWatchingResult(
@JsonProperty("name") override val name: String,
@ -71,6 +87,9 @@ object DataStoreHelper {
@JsonProperty("posterHeaders") override var posterHeaders: Map<String, String>? = null,
) : SearchResponse
/**
* A datastore wide account for future implementations of a multiple account system
**/
private var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION
fun getAllWatchStateIds(): List<Int>? {
@ -177,6 +196,7 @@ object DataStoreHelper {
fun setBookmarkedData(id: Int?, data: BookmarkedData) {
if (id == null) return
setKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString(), data)
AccountManager.localListApi.requireLibraryRefresh = true
}
fun getBookmarkedData(id: Int?): BookmarkedData? {

View file

@ -291,6 +291,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Supervideo(),
GuardareStream(),
CineGrabber(),
Vanfem(),
// StreamSB.kt works
// SBPlay(),
@ -321,6 +322,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
DesuDrive(),
Filesim(),
FileMoon(),
Linkbox(),
Acefile(),
SpeedoStream(),

View file

@ -4,6 +4,7 @@ package com.lagradost.cloudstream3.utils
import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder.apis
//import com.lagradost.cloudstream3.animeproviders.AniflixProvider
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
@ -78,17 +79,21 @@ object SyncUtil {
return null
}
suspend fun getUrlsFromId(id: String, type: String = "anilist") : List<String> {
return arrayListOf()
// val url =
// "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json"
// val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).parsed<SyncPage>()
// val pages = response.pages ?: return emptyList()
// val current = pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values).mapNotNull { it.url }.toMutableList()
// if(type == "anilist") { // TODO MAKE BETTER
// current.add("${AniflixProvider().mainUrl}/anime/$id")
// }
// return current
suspend fun getUrlsFromId(id: String, type: String = "anilist"): List<String> {
val url =
"https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json"
val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).parsed<SyncPage>()
val pages = response.pages ?: return emptyList()
val current =
pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values)
.mapNotNull { it.url }.toMutableList()
if (type == "anilist") { // TODO MAKE BETTER
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
current.add("${it.mainUrl}/anime/$id")
}
}
return current
}
data class SyncPage(

View file

@ -9,7 +9,9 @@ import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.view.*
@ -28,15 +30,21 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.drawable.toBitmapOrNull
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.NavHostFragment
import androidx.palette.graphics.Palette
import androidx.preference.PreferenceManager
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
@ -105,7 +113,7 @@ object UIHelper {
listView.requestLayout()
}
fun Activity?.getSpanCount(): Int? {
fun Context?.getSpanCount(): Int? {
val compactView = false
val spanCountLandscape = if (compactView) 2 else 6
val spanCountPortrait = if (compactView) 1 else 3
@ -158,12 +166,27 @@ object UIHelper {
return color
}
var createPaletteAsyncCache: HashMap<String, Palette> = hashMapOf()
fun createPaletteAsync(url: String, bitmap: Bitmap, callback: (Palette) -> Unit) {
createPaletteAsyncCache[url]?.let { palette ->
callback.invoke(palette)
return
}
Palette.from(bitmap).generate { paletteNull ->
paletteNull?.let { palette ->
createPaletteAsyncCache[url] = palette
callback(palette)
}
}
}
fun ImageView?.setImage(
url: String?,
headers: Map<String, String>? = null,
@DrawableRes
errorImageDrawable: Int? = null,
fadeIn: Boolean = true
fadeIn: Boolean = true,
colorCallback: ((Palette) -> Unit)? = null
): Boolean {
if (this == null || url.isNullOrBlank()) return false
@ -177,6 +200,33 @@ object UIHelper {
else req
}
if (colorCallback != null) {
builder.listener(object : RequestListener<Drawable> {
@SuppressLint("CheckResult")
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
resource?.toBitmapOrNull()
?.let { bitmap -> createPaletteAsync(url, bitmap, colorCallback) }
return false
}
@SuppressLint("CheckResult")
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
return false
}
})
}
val res = if (errorImageDrawable != null)
builder.error(errorImageDrawable).into(this)
else

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorPrimary" android:state_checked="true"/>
<item android:color="?attr/colorPrimary" android:state_focused="true"/>
<item android:color="?attr/colorPrimary" android:state_selected="true"/>
<item android:color="?attr/grayTextColor" android:state_checked="false"/>
</selector>

View file

@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6z"/>
<path android:fillColor="@android:color/white" android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,12l-2.5,-1.5L15,12L15,4h5v8z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="?attr/white" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
</vector>

View file

@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="?attr/white"
<vector android:height="12dp" android:tint="?attr/white"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
android:width="12dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
</vector>

View file

@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM7.35,18.5C8.66,17.56 10.26,17 12,17s3.34,0.56 4.65,1.5C15.34,19.44 13.74,20 12,20S8.66,19.44 7.35,18.5zM18.14,17.12L18.14,17.12C16.45,15.8 14.32,15 12,15s-4.45,0.8 -6.14,2.12l0,0C4.7,15.73 4,13.95 4,12c0,-4.42 3.58,-8 8,-8s8,3.58 8,8C20,13.95 19.3,15.73 18.14,17.12z"/>
<path android:fillColor="@android:color/white" android:pathData="M12,6c-1.93,0 -3.5,1.57 -3.5,3.5S10.07,13 12,13s3.5,-1.57 3.5,-3.5S13.93,6 12,6zM12,11c-0.83,0 -1.5,-0.67 -1.5,-1.5S11.17,8 12,8s1.5,0.67 1.5,1.5S12.83,11 12,11z"/>
</vector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/textColor"/>
<corners android:radius="16dp" />
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/ratingColorBg"/>
<corners android:radius="@dimen/rounded_image_radius"/>
<!-- <stroke android:color="@color/subColor" android:width="2dp"/>-->
</shape>

View file

@ -35,9 +35,9 @@
-->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_height="wrap_content"
android:layout_height="70dp"
android:layout_width="0dp"
app:labelVisibilityMode="labeled"
app:labelVisibilityMode="unlabeled"
android:background="?attr/primaryGrayBackground"
app:itemIconTint="@color/item_select_color"

View file

@ -5,7 +5,6 @@
android:id="@+id/download_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/primaryGrayBackground"
android:orientation="vertical"
tools:context=".ui.download.DownloadFragment">
@ -132,7 +131,8 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/download_list"
android:layout_width="match_parent"
android:paddingBottom="100dp"
android:clipToPadding="false"
android:layout_height="match_parent"
android:background="?attr/primaryBlackBackground"
android:descendantFocusability="afterDescendants"

View file

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/library_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/empty_list_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="30dp"
android:gravity="center"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/primaryGrayBackground">
<LinearLayout
android:id="@+id/search_status_bar_padding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_scrollFlags="scroll|enterAlways">
<ImageView
android:id="@+id/provider_selector"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="10dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:src="@drawable/ic_baseline_extension_24"
app:tint="?attr/textColor" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/search_background"
android:visibility="visible"
app:layout_scrollFlags="scroll|enterAlways">
<androidx.appcompat.widget.SearchView
android:id="@+id/main_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:iconifiedByDefault="false"
android:imeOptions="actionSearch"
android:inputType="text"
android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusRight="@id/search_filter"
android:paddingStart="-10dp"
app:iconifiedByDefault="false"
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
tools:ignore="RtlSymmetry">
</androidx.appcompat.widget.SearchView>
<ImageView
android:id="@+id/list_selector"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_gravity="end|center_vertical"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:nextFocusLeft="@id/main_search"
android:nextFocusRight="@id/main_search"
android:padding="10dp"
android:src="@drawable/ic_baseline_filter_list_24"
app:tint="?attr/textColor" />
</FrameLayout>
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="40dp"
tools:listitem="@layout/library_viewpager_page" />
<LinearLayout
android:id="@+id/library_loading_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/primaryBlackBackground"
android:visibility="gone"
tools:visibility="visible">
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/library_loading_shimmer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_margin="2dp"
app:shimmer_auto_start="true"
app:shimmer_base_alpha="0.2"
app:shimmer_duration="@integer/loading_time"
app:shimmer_highlight_alpha="0.3">
<GridView
android:id="@+id/gridview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:gravity="center"
android:horizontalSpacing="10dp"
android:numColumns="3"
android:paddingBottom="120dp"
android:verticalSpacing="10dp"
tools:listitem="@layout/loading_poster_dynamic" />
</com.facebook.shimmer.ShimmerFrameLayout>
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="40dp">
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/sort_fab"
style="@style/ExtendedFloatingActionButton"
android:text="@string/sort"
android:textColor="?attr/textColor"
app:icon="@drawable/ic_baseline_sort_24"
tools:ignore="ContentDescription" />
</FrameLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/library_tab_layout"
style="@style/Theme.Widget.Tabs"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_gravity="bottom"
android:background="?attr/primaryGrayBackground"
android:descendantFocusability="blocksDescendants"
android:focusable="false"
android:paddingHorizontal="5dp"
app:layout_scrollFlags="noScroll"
app:tabGravity="center"
app:tabIndicator="@drawable/indicator_background"
app:tabIndicatorColor="@color/textColor"
app:tabIndicatorGravity="center"
app:tabIndicatorHeight="30dp"
app:tabMode="scrollable"
app:tabSelectedTextColor="@color/lightTextColor"
app:tabTextAppearance="@style/TabNoCaps"
app:tabTextColor="@color/textColor" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -129,9 +129,9 @@
<androidx.core.widget.NestedScrollView
android:id="@+id/result_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/primaryGrayBackground">
android:paddingBottom="100dp"
android:clipToPadding="false"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<com.lagradost.cloudstream3.ui.AutofitRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/page_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
tools:listitem="@layout/home_result_grid_expanded" />

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="10dp"
android:background="@color/grayShimmer"
app:cardCornerRadius="@dimen/loading_radius"
app:layout_constraintDimensionRatio="1:1.414"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
layout="@layout/loading_line_short_center"
android:layout_width="match_parent"
android:layout_height="15dp"
android:layout_marginHorizontal="20dp"
android:layout_marginVertical="10dp" />
</LinearLayout>

View file

@ -96,34 +96,37 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/player_video_title"
<LinearLayout
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="80dp"
android:layout_marginTop="35dp"
android:paddingTop="20dp"
android:layout_marginEnd="80dp"
android:gravity="center"
android:textColor="@color/white"
android:textStyle="bold"
android:visibility="visible"
android:orientation="vertical"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Hello world" />
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/player_video_title_rez"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="80dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="80dp"
android:layout_marginBottom="2.5dp"
android:gravity="center"
android:textColor="@color/white"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/player_video_title"
tools:text="1920x1080" />
<TextView
android:id="@+id/player_video_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/white"
android:textStyle="bold"
android:visibility="visible"
tools:text="Hello world" />
</LinearLayout>
<!-- Removed as it has no use anymore-->
<!--<androidx.mediarouter.app.MediaRouteButton
@ -319,23 +322,23 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.button.MaterialButton
tools:visibility="visible"
android:id="@+id/skip_chapter_button"
style="@style/NiceButton"
android:layout_width="150dp"
android:layout_height="40dp"
android:layout_marginEnd="100dp"
android:visibility="gone"
android:maxLines="1"
android:backgroundTint="@color/skipOpTransparent"
android:maxLines="1"
android:padding="10dp"
android:textColor="@color/white"
android:visibility="gone"
app:cornerRadius="@dimen/rounded_button_radius"
app:layout_constraintBottom_toTopOf="@+id/bottom_player_bar"
app:layout_constraintEnd_toEndOf="parent"
app:strokeColor="@color/white"
app:strokeWidth="1dp"
tools:text="Skip Opening" />
tools:text="Skip Opening"
tools:visibility="visible" />
<LinearLayout
android:layout_width="match_parent"

View file

@ -44,6 +44,7 @@
android:nextFocusLeft="@id/sort_subtitles"
android:nextFocusRight="@id/apply_btt"
android:requiresFadingEdge="vertical"
tools:layout_height="100dp"
tools:listitem="@layout/sort_bottom_single_choice" />
</LinearLayout>
@ -117,6 +118,7 @@
android:nextFocusLeft="@id/sort_providers"
android:nextFocusRight="@id/cancel_btt"
android:requiresFadingEdge="vertical"
tools:layout_height="200dp"
tools:listfooter="@layout/sort_bottom_footer_add_choice"
tools:listitem="@layout/sort_bottom_single_choice" />
</LinearLayout>

View file

@ -5,26 +5,32 @@
android:id="@+id/search_result_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="@drawable/outline_drawable"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:id="@+id/background_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_margin="2dp"
android:layout_marginBottom="2dp"
android:elevation="10dp"
app:cardBackgroundColor="?attr/primaryGrayBackground"
app:cardCornerRadius="@dimen/rounded_image_radius">
app:cardCornerRadius="@dimen/rounded_image_radius"
app:layout_constraintDimensionRatio="1:1.414"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/search_poster_img_des"
android:duplicateParentState="true"
@ -32,7 +38,9 @@
android:scaleType="centerCrop"
tools:src="@drawable/example_poster" />
<TextView android:id="@+id/text_quality" style="@style/TypeButton" />
<TextView
android:id="@+id/text_quality"
style="@style/TypeButton" />
<LinearLayout
android:layout_width="match_parent"
@ -50,6 +58,30 @@
style="@style/SubButton"
android:layout_gravity="end" />
<androidx.cardview.widget.CardView
android:id="@+id/text_rating_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_margin="2dp"
android:backgroundTint="@color/ratingColorBg"
android:elevation="0dp"
android:visibility="gone"
app:cardCornerRadius="@dimen/rounded_image_radius"
app:cardElevation="0dp"
tools:visibility="visible">
<TextView
android:id="@+id/text_rating"
style="@style/SearchBox"
android:layout_margin="0dp"
android:minWidth="40dp"
android:textColor="@color/ratingColor"
tools:text="★ 7.7" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/text_flag"
style="@style/SearchBox"
@ -60,7 +92,22 @@
tools:text="🇸🇪"
tools:visibility="visible" />
</LinearLayout>
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/watchProgress"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="5dp"
android:layout_gravity="bottom"
android:layout_marginBottom="-1.5dp"
android:progressBackgroundTint="?attr/colorPrimary"
android:progressTint="?attr/colorPrimary"
android:visibility="gone"
tools:progress="50"
tools:visibility="visible" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/imageText"

View file

@ -1,20 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@drawable/home_alt"
android:title="@string/title_home"/>
android:title="@string/title_home" />
<item
android:id="@+id/navigation_search"
android:icon="@drawable/search_icon"
android:title="@string/title_search"/>
android:title="@string/title_search" />
<item
android:id="@+id/navigation_library"
android:icon="@drawable/ic_outline_account_circle_24"
android:title="@string/library" />
<item
android:id="@+id/navigation_downloads"
android:icon="@drawable/netflix_download"
android:title="@string/title_downloads"/>
android:title="@string/title_downloads" />
<item
android:id="@+id/navigation_settings"
android:icon="@drawable/settings_alt"
android:title="@string/title_settings"/>
android:icon="@drawable/ic_outline_settings_24"
android:title="@string/title_settings" />
</menu>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/search_button"
android:icon="@drawable/search_icon"
android:title="@string/title_search"
app:searchHintIcon="@drawable/search_icon"
app:showAsAction="collapseActionView|ifRoom"
app:actionViewClass="com.lagradost.cloudstream3.ui.library.MenuSearchView" />
<item
android:id="@+id/sort_button"
android:icon="@drawable/ic_baseline_sort_24"
android:title="Sort"
app:showAsAction="collapseActionView|ifRoom" />
</menu>

View file

@ -144,6 +144,15 @@
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<fragment
android:id="@+id/navigation_library"
android:name="com.lagradost.cloudstream3.ui.library.LibraryFragment"
android:label="@string/library"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<fragment
android:id="@+id/navigation_settings_general"
android:name="com.lagradost.cloudstream3.ui.settings.SettingsGeneral"

View file

@ -521,4 +521,21 @@
<string name="update_started">بدأ التحديث</string>
<string name="plugin_downloaded">تم تنزيل الإضافة</string>
<string name="action_remove_from_watched">إزالة من المشاهدة</string>
<string name="sort_alphabetical_a">الترتيب الأبجدي (من الألف إلى الياء)</string>
<string name="select_library">اختر المكتبة</string>
<string name="browser">المتصفح</string>
<string name="sort_updated_new">محدث (من الأحدث إلى الأقدم)</string>
<string name="empty_library_logged_in_message">يبدو أن هذه القائمة فارغة ، حاول التبديل إلى قائمة أخرى</string>
<string name="sort_rating_desc">التقييم (من الأعلى إلى الأدنى)</string>
<string name="sort_rating_asc">التقييم (من الأدنى إلى الأعلى)</string>
<string name="sort_alphabetical_z">الترتيب الأبجدي (من ي إلى أ)</string>
<string name="empty_library_no_accounts_message">يبدو أن مكتبتك فارغة :(
\nتسجيل الدخول إلى حساب مكتبة أو إضافة عروض إلى مكتبتك المحلية</string>
<string name="sort_updated_old">محدث (من القديم إلى الجديد)</string>
<string name="sort_by">فرز حسب</string>
<string name="sort">افرز</string>
<string name="open_with">فتح بواسطة</string>
<string name="library">المكتبة</string>
<string name="safe_mode_file">تم العثور على ملف الوضع الآمن!
\nلا يتم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف.</string>
</resources>

View file

@ -497,4 +497,5 @@
<string name="plugin_downloaded">Приставката е изтеглена</string>
<string name="delayed_update_notice">Приложението ще се актуализира при изход от него</string>
<string name="update_started">Започна Актуализация</string>
<string name="action_remove_from_watched">Премахване от гледани</string>
</resources>

View file

@ -17,7 +17,7 @@
<string name="skip_type_intro">Intro</string>
<string name="clear_history">Verlauf löschen</string>
<string name="history">Verlauf</string>
<string name="enable_skip_op_from_database_des">Überspringen Button für Openings/Endings anzeigen</string>
<string name="enable_skip_op_from_database_des">Überspringen Knopf für Openings/Endings anzeigen</string>
<string name="clipboard_too_large">Zu viel Text. Kann nicht in der Zwischenablage gespeichert werden.</string>
<string name="episode_poster_img_des">Episodenvorschaubild</string>
<string name="home_main_poster_img_des">Medienvorschaubild</string>
@ -489,4 +489,21 @@
<string name="delayed_update_notice">Die Anwendung wird beim Beenden aktualisiert</string>
<string name="plugin_downloaded">Das Plugin wurde heruntergeladen</string>
<string name="action_remove_from_watched">Von geschaut entfernen</string>
<string name="library">Bibliothek</string>
<string name="browser">Browser</string>
<string name="sort_by">Sortieren nach</string>
<string name="sort">Sortieren</string>
<string name="sort_rating_desc">Bewertung (gut bis schlecht)</string>
<string name="sort_rating_asc">Bewertung (schlecht bis gut)</string>
<string name="sort_updated_new">Aktualisiert (neu bis alt)</string>
<string name="sort_updated_old">Aktualisiert (alt bis neu)</string>
<string name="sort_alphabetical_a">Alphabetisch (A bis Z)</string>
<string name="sort_alphabetical_z">Alphabetisch (Z bis A)</string>
<string name="select_library">Bibliothek auswählen</string>
<string name="open_with">Öffnen mit</string>
<string name="empty_library_no_accounts_message">Sieht aus, als wäre deine Bibliothek leer :(
\nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu</string>
<string name="empty_library_logged_in_message">Diese Liste scheint leer zu sein. Versuche, zu einer anderen Liste zu wechseln.</string>
<string name="safe_mode_file">Datei für abgesicherten Modus gefunden!
\nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird.</string>
</resources>

View file

@ -314,7 +314,7 @@
<string name="crash_reporting_title">Αναφορά κατάρρευσης</string>
<string name="preferred_media_subtext">Τι θα θέλατε να δείτε</string>
<string name="setup_done">Έγινε</string>
<string name="extensions">Πρόσθετα</string>
<string name="extensions">Extensions</string>
<string name="add_repository">Προσθήκη αποθετηρίου</string>
<string name="repository_name_hint">Όνομα αποθετηρίου</string>
<string name="repository_url_hint">Σύνδεσμος αποθετηρίου</string>
@ -490,4 +490,22 @@
<string name="plugin_downloaded">Το πρόσθετο κατέβει</string>
<string name="update_started">Ενημέρωση ξεκίνησε</string>
<string name="delayed_update_notice">Η εφαρμογή θα ενημερωθεί κατά την έξοδο</string>
<string name="sort_alphabetical_z">Αλφαβητικά (Ω προς Α)</string>
<string name="sort">Ταξινόμηση</string>
<string name="sort_rating_asc">Κριτική (Χαμηλή προς Υψηλή)</string>
<string name="sort_updated_new">Ενημερωμένο (Καινούριο προς παλιό)</string>
<string name="sort_updated_old">Ενημερωμένο (Παλιό προς Καινούργιο)</string>
<string name="library">Βιβλιοθήκη</string>
<string name="sort_rating_desc">Κριτική (Υψηλή προς χαμηλή)</string>
<string name="sort_by">Ταξινόμηση με βάση</string>
<string name="sort_alphabetical_a">Αλφαβητικά (Α προς Ω)</string>
<string name="select_library">Διάλεξε βιβλιοθήκη</string>
<string name="empty_library_logged_in_message">Φαίνεται πως η λίστα είναι άδεια, δοκίμασε να μεταβείς σε μία άλλη</string>
<string name="action_remove_from_watched">Αφαίρεση από παρακολουθημένα</string>
<string name="browser">Περιηγητής</string>
<string name="open_with">Άνοιγμα με</string>
<string name="empty_library_no_accounts_message">Φαίνεται πως η βιβλιοθήκη σου είναι άδεια :(
\nΣυνδέσου σε έναν λογαριασμό που έχει βιβλιοθήκη, ή πρόσθεσε σειρές στην τοπική βιβλιοθήκη σου</string>
<string name="safe_mode_file">Βρέθηκε αρχείο Ασφαλούς Λειτουργίας!
\nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο.</string>
</resources>

View file

@ -489,6 +489,23 @@
<string name="update_started">Actualización iniciada</string>
<string name="plugin_downloaded">Complemento descargado</string>
<string name="action_remove_from_watched">Quitar de visto</string>
<string name="sort_by">Ordenar por</string>
<string name="sort">Ordenar</string>
<string name="sort_rating_desc">Valoración (más a menos)</string>
<string name="sort_rating_asc">Valoración (menos a más)</string>
<string name="sort_updated_new">Actualizado (nuevo a viejo)</string>
<string name="sort_updated_old">Actualizado (viejo a nuevo)</string>
<string name="sort_alphabetical_a">Alfabéticamente (A a Z)</string>
<string name="browser">Navegador</string>
<string name="library">Biblioteca</string>
<string name="empty_library_logged_in_message">Parece que esta lista está vacía, intenta cambiar a otra</string>
<string name="sort_alphabetical_z">Alfabéticamente (Z a A)</string>
<string name="select_library">Seleccionar biblioteca</string>
<string name="open_with">Abrir con</string>
<string name="empty_library_no_accounts_message">Parece que tu biblioteca está vacía :(
\nInicia sesión en una cuenta de biblioteca o añade series desde tu biblioteca local</string>
<string name="safe_mode_file">¡Se encontró un archivo en modo seguro!
\nNo cargar ninguna extensión al inicio hasta que se elimine el archivo.</string>
<string name="android_tv_interface_on_seek_settings">Jugadora mostrada - buscar cantidad</string>
<string name="android_tv_interface_off_seek_settings">Jugadora oculta - buscar cantidad</string>
<string name="pref_category_android_tv">Android TV</string>

View file

@ -453,7 +453,7 @@
<string name="safe_mode_description">Semua fitur tambahkan dimatikan karena crash, untuk memudahkanmu mencari penyebab crash.</string>
<string name="example_lang_name">Kode bahasa (en)</string>
<string name="player_load_subtitles_online">Ambil dari internet</string>
<string name="provider_languages_tip">Putar vidio di bahasa ini</string>
<string name="provider_languages_tip">Putar video di bahasa ini</string>
<string name="add_repository">Tambah Repositori</string>
<string name="delete_repository_plugins">Pilih ini untuk menghapus semua repositori plugin</string>
<string name="skip_setup">Lewati pengaturan</string>
@ -483,7 +483,7 @@
<string name="pref_category_gestures">Gerakan</string>
<string name="apk_installer_settings_des">Beberapa perangkat tidak mendukung penginstal paket mode baru. Coba mode lama jika pembaruan tidak dapat diinstal.</string>
<string name="pref_category_actions">Aksi</string>
<string name="referer">Referensi</string>
<string name="referer">Referer</string>
<string name="yes">Ya</string>
<string name="extension_install_first">Pasang dulu fitur tambahan</string>
<string name="all_languages_preference">Semua Bahasa</string>
@ -512,4 +512,21 @@
<string name="delayed_update_notice">Aplikasi akan diperbaharui pada saat keluar</string>
<string name="update_started">Pembaharuan Dimulai</string>
<string name="action_remove_from_watched">Hapus dari tontonan</string>
<string name="browser">Browser</string>
<string name="select_library">Pilih pustaka</string>
<string name="empty_library_no_accounts_message">Yahh daftar pustaka kamu kosong :(
\nMasuk ke akun pustaka atau tambah perlihatkan ke lokal pustaka kamu</string>
<string name="library">Pustaka</string>
<string name="sort_by">Urutkan berdasar</string>
<string name="sort">Urutkan</string>
<string name="sort_rating_asc">Peringkat (Rendah ke Tinggi)</string>
<string name="sort_updated_old">Update (Lama ke Terbaru)</string>
<string name="sort_rating_desc">Peringkat (Tinggi ke Rendah)</string>
<string name="sort_updated_new">Update (Terbaru ke Lama)</string>
<string name="sort_alphabetical_a">Abjad (A ke Z)</string>
<string name="sort_alphabetical_z">Abjad (Z ke A)</string>
<string name="open_with">Buka dengan</string>
<string name="empty_library_logged_in_message">Yahh daftar ini kosong, coba ganti ke yang lain</string>
<string name="safe_mode_file">Mode aman file ditemukan!
\nTidak memuat ekstensi pada startup sampai berkas dihapus.</string>
</resources>

View file

@ -511,4 +511,21 @@
<string name="update_started">Aggiornamento avviato</string>
<string name="plugin_downloaded">Plugin scaricato</string>
<string name="action_remove_from_watched">Rimuovi dai già visti</string>
<string name="browser">Browser</string>
<string name="sort_by">Ordina per</string>
<string name="sort_rating_desc">Punteggio (Decrescente)</string>
<string name="sort_rating_asc">Punteggio (Crescente)</string>
<string name="sort_updated_new">Aggiornato (Da nuovo a vecchio)</string>
<string name="sort_updated_old">Aggiornato (Da vecchio a nuovo)</string>
<string name="sort_alphabetical_a">Alfabetico (A - Z)</string>
<string name="sort_alphabetical_z">Alfabetico (Z - A)</string>
<string name="empty_library_no_accounts_message">Sembra che la tua libreria sia vuota :(
\nAccedi a un account di libreria o aggiungi degli show alla tua libreria locale</string>
<string name="select_library">Seleziona libreria</string>
<string name="open_with">Apri con</string>
<string name="library">Libreria</string>
<string name="sort">Ordina</string>
<string name="empty_library_logged_in_message">Sembra che questa lista sia vuota, prova a passare a un\'altra</string>
<string name="safe_mode_file">File \"safe mode\" trovato!
\nAll\'avvio non sarà caricata alcuna estensione finchè il file non verrà rimosso.</string>
</resources>

View file

@ -492,4 +492,21 @@
<string name="update_started">Rozpoczęto aktualizację</string>
<string name="plugin_downloaded">Pobrano rozszerzenie</string>
<string name="action_remove_from_watched">Usuń z obejrzanych</string>
<string name="browser">Przeglądarka</string>
<string name="sort_updated_new">Data aktualizacji (od nowego do starego)</string>
<string name="sort_by">Sortuj według</string>
<string name="sort">Sortuj</string>
<string name="open_with">Otwórz za pomocą</string>
<string name="sort_rating_desc">Ocena (od najwyższej do najniższej)</string>
<string name="sort_rating_asc">Ocena (od najniższej do najwyższej)</string>
<string name="sort_updated_old">Data aktualizacji (od starego do nowego)</string>
<string name="sort_alphabetical_a">Alfabetycznie (od A do Z)</string>
<string name="sort_alphabetical_z">Alfabetycznie (od Z do A)</string>
<string name="select_library">Wybierz bibliotekę</string>
<string name="library">Biblioteka</string>
<string name="empty_library_no_accounts_message">Wygląda na to, że twoja biblioteka jest pusta :(
\nZaloguj się na swoje konto lub dodaj programy do swojej lokalnej biblioteki</string>
<string name="empty_library_logged_in_message">Wygląda na to, że ta lista jest pusta, spróbuj przełączyć się na inną</string>
<string name="safe_mode_file">Znaleziono plik trybu bezpiecznego.
\nRozszerzenia nie zostaną wczytane, dopóki plik nie zostanie usunięty.</string>
</resources>

View file

@ -141,7 +141,7 @@
<string name="backup_settings">Copie de rezervă a datelor</string>
<string name="restore_success">Fișier de rezervă încărcat</string>
<string name="restore_failed_format" formatted="true">Imposibilitatea de a restaura datele din %s</string>
<string name="backup_success">Datele au fost salvate cu succes</string>
<string name="backup_success">Date stocate</string>
<string name="backup_failed">Permisiuni de arhivare lipsă, vă rugăm să încercați din nou</string>
<string name="backup_failed_error_format">Eroare de backup %s</string>
<string name="search">Căutare</string>
@ -380,4 +380,8 @@
<string name="app_name">CloudStream</string>
<string name="play_trailer_button">Vizionează trailerul</string>
<string name="update_started">Actualizarea a început</string>
<string name="episode_sync_settings">Actualizați progresul ceasului</string>
<string name="autoplay_next_settings_des">Începe următorul episod când se termină episodul curent</string>
<string name="pref_filter_search_quality">Ascundeți calitatea video selectată în rezultatele căutării</string>
<string name="play_livestream_button">Redare Livestream</string>
</resources>

View file

@ -12,7 +12,7 @@
<string name="download_failed">Скачать неудачный</string>
<string name="resize_fit">Подогнать</string>
<string name="delete">Удалить</string>
<string name="all">Всё</string>
<string name="all">Все</string>
<string name="pause">Пауза</string>
<string name="cast_format" formatted="true">Актёрский состав: %s</string>
<string name="show_title">Название источника</string>
@ -25,12 +25,12 @@
<string name="next_episode_format" formatted="true">Серия %d будет выпущен в</string>
<string name="result_poster_img_des">Плакат</string>
<string name="search_poster_img_des">\@нить/результат_плокат_картинка_</string>
<string name="episode_poster_img_des">Серия плакат</string>
<string name="home_main_poster_img_des">Главный плакат</string>
<string name="episode_poster_img_des">Постер Эпизода</string>
<string name="home_main_poster_img_des">Главный постер</string>
<string name="home_next_random_img_des">Следующий случайный</string>
<string name="go_back_img_des">Вернуться</string>
<string name="home_change_provider_img_des">Изменить поставщика</string>
<string name="preview_background_img_des">Фон предпросмотр</string>
<string name="preview_background_img_des">Предпросмотр фона</string>
<string name="player_speed_text_format" formatted="true">Скорость (%.2fx)</string>
<string name="rated_format" formatted="true">Оценили: %.1f</string>
<string name="new_update_format" formatted="true">Новое обновление найдено!
@ -48,31 +48,31 @@
<string name="search_hint_site" formatted="true">Поиск %s…</string>
<string name="no_data">Нет данных</string>
<string name="episode_more_options_des">Дополнительные опции</string>
<string name="next_episode">Следующий серия</string>
<string name="next_episode">Следующий эпизод</string>
<string name="result_tags">Жанры</string>
<string name="result_share">Поделиться</string>
<string name="result_open_in_browser">Открыть в браузер</string>
<string name="result_open_in_browser">Открыть в браузере</string>
<string name="skip_loading">Пропустить загрузку</string>
<string name="type_watching">Смотрю</string>
<string name="type_watching">Просмотр</string>
<string name="type_on_hold">Приостановленно</string>
<string name="type_completed">Завершено</string>
<string name="type_dropped">Брошенный</string>
<string name="type_plan_to_watch">План по смотреть</string>
<string name="type_plan_to_watch">План посмотреть</string>
<string name="type_none">Никто</string>
<string name="type_re_watching">Пересмотрю</string>
<string name="play_movie_button">Смотреть фильм</string>
<string name="play_trailer_button">Проиграть трейлер</string>
<string name="play_trailer_button">Воспроизвести трейлер</string>
<string name="play_livestream_button">Воспроизвести Livestream</string>
<string name="pick_source">Источники</string>
<string name="pick_subtitle">Субтитры</string>
<string name="play_episode">Проиграть серия</string>
<string name="play_episode">Воспроизвести эпизод</string>
<string name="reload_error">Повторная попытка подключение…</string>
<string name="go_back">Вернуться</string>
<string name="downloaded">Скачали</string>
<string name="downloaded">Скачано</string>
<string name="downloading">Скачивание</string>
<string name="download_paused">Скачать приостановленный</string>
<string name="download_started">Скачать начатый</string>
<string name="download_canceled">Скачать отменено</string>
<string name="download_canceled">Скачать отменённый</string>
<string name="download_done">Скачать выполнено</string>
<string name="home_info">Инфо</string>
<string name="update_started">Обновление началось</string>
@ -81,8 +81,8 @@
<string name="home_more_info">Подробнее</string>
<string name="filter_bookmarks">Фильтр закладки</string>
<string name="error_bookmarks_text">Закладки</string>
<string name="sort_apply">Наносить</string>
<string name="sort_cancel">Прервать</string>
<string name="sort_apply">Применить</string>
<string name="sort_cancel">Отмена</string>
<string name="sort_copy">Копия</string>
<string name="sort_close">Закрыть</string>
<string name="sort_clear">Очистить</string>
@ -116,8 +116,8 @@
<string name="popup_delete_file">Удалить файл</string>
<string name="popup_play_file">Проиграть файл</string>
<string name="download_storage_text">Внутренняя память</string>
<string name="popup_resume_download">Скачать резюме</string>
<string name="popup_pause_download">Приостановить скачать</string>
<string name="popup_resume_download">Продолжить Скачать</string>
<string name="popup_pause_download">Приостановить скачивание</string>
<string name="pref_disable_acra">Отключить автоматическое информирование об ошибках</string>
<string name="subs_import_text" formatted="true">Импортируйте шрифты поместив их в %s</string>
<string name="continue_watching">Продолжить смотреть</string>
@ -174,7 +174,7 @@
<string name="episode_short">Э</string>
<string name="no_episodes_found">Эпизоды не найдены</string>
<string name="delete_file">Удалить файл</string>
<string name="resume">Возобновить</string>
<string name="resume">Продолжить</string>
<string name="go_back_30">-30</string>
<string name="go_forward_30">+30</string>
<string name="delete_message" formatted="true">Это будет удалено безвозвратно%s
@ -193,7 +193,7 @@
<string name="others">Другое</string>
<string name="storage_error">Ошибка загрузки, проверьте разрешения хранилища</string>
<string name="episode_action_copy_link">Копировать ссылку</string>
<string name="episode_action_auto_download">Автоматическая загрузка</string>
<string name="episode_action_auto_download">Автоскачивание</string>
<string name="episode_action_download_mirror">Загрузка. Зеркало</string>
<string name="season">Сезон</string>
<string name="anim">Аниме приложение от тех же разработчиков</string>
@ -217,7 +217,7 @@
<string name="torrent_singular">Торрент</string>
<string name="documentaries_singular">Документальный</string>
<string name="asian_drama_singular">Азиатская драма</string>
<string name="category_general">Общий</string>
<string name="category_general">Основные</string>
<string name="category_providers">Провайдеры</string>
<string name="category_ui">Макет</string>
<string name="pref_category_extensions">Расширения</string>
@ -240,7 +240,7 @@
<string name="no_update_found">Обновление не найдено</string>
<string name="video_aspect_ratio_resize">Изменить размер</string>
<string name="video_source">Источник</string>
<string name="check_for_update">Проверьте наличие обновления</string>
<string name="check_for_update">Проверить обновления</string>
<string name="add_site_pref">Клон сайта</string>
<string name="dns_pref">DNS через HTTPS</string>
<string name="remove_site_pref">Удалить сайт</string>
@ -248,7 +248,7 @@
<string name="subtitle_offset">Синхронизация субтитров</string>
<string name="add_site_summary">Добавить клон существующего сайта с другим URL-адресом</string>
<string name="dns_pref_summary">Используется для обхода блокировок интернет провайдера</string>
<string name="download_path_pref">Путь загрузки</string>
<string name="download_path_pref">Путь скачивания</string>
<string name="benene_des">учитывая бенен</string>
<string name="update">Обновить</string>
<string name="primary_color_settings">Основной цвет</string>
@ -279,7 +279,7 @@
<string name="use_system_brightness_settings_des">Используйте яркость системы в проигрывателе приложения вместо темного наложения</string>
<string name="episode_sync_settings">Обновить состояние хода просмотра</string>
<string name="backup_success">Данные сохранены</string>
<string name="advanced_search_des">Дает вам результаты поиска, разделенные по провайдеру</string>
<string name="advanced_search_des">Показывает результаты поиска, разделенные по провайдеру</string>
<string name="uprereleases_settings_des">Поиск предварительных обновлений вместо полных выпусков</string>
<string name="redo_setup_process">Повторить процесс настройки</string>
<string name="no_chromecast_support_toast">Этот провайдер не поддерживает Chromecast</string>
@ -304,7 +304,7 @@
<string name="skip_update">Пропустить это обновление</string>
<string name="nginx_url_pref">URL-адрес NGINX-сервера</string>
<string name="create_account">Создать учётную запись</string>
<string name="add_sync">Добавить трекинг</string>
<string name="add_sync">Добавить слежение</string>
<string name="added_sync_format" formatted="true">Добавлено %s</string>
<string name="upload_sync">Синхронизировать</string>
<string name="sync_score">Оценено</string>
@ -350,7 +350,7 @@
<string name="add_account">Добавить учётную запись</string>
<string name="example_site_name">МойКрутойСайт</string>
<string name="example_site_url">example.com</string>
<string name="example_lang_name">Язык (en)</string>
<string name="example_lang_name">Код языка (ru)</string>
<string name="account">учётная запись</string>
<string name="automatic">Автоматически</string>
<string name="example_ip">127.0.0.1</string>
@ -393,8 +393,8 @@
<string name="plugins_disabled" formatted="true">Отключено: %d</string>
<string name="login_format" formatted="true">%s %s</string>
<string name="authenticated_user" formatted="true">%s аутентифицировано</string>
<string name="authenticated_user_fail" formatted="true">Не удалось перейти к %s</string>
<string name="max">Максимум</string>
<string name="authenticated_user_fail" formatted="true">Не удается логин на %s</string>
<string name="max">Макс</string>
<string name="min">Минимум</string>
<string name="subtitles_outline">Очертание</string>
<string name="subtitles_shadow">Тень</string>
@ -408,4 +408,91 @@
<string name="subtitles_example_text">Съешь ещё этих мягких французских булок, да выпей же чаю</string>
<string name="recommended">Рекомендуется</string>
<string name="player_loaded_subtitles" formatted="true">Загружено %s</string>
<string name="anime_singular">\@нить/аниме</string>
<string name="ova_singular">\@нить/ova</string>
<string name="show_dub">Этикетка Dub</string>
<string name="site">Сайт</string>
<string name="pref_category_ui_features">Функции</string>
<string name="actor_main">Главное</string>
<string name="home_source">Источник</string>
<string name="home_random">Случайный</string>
<string name="coming_soon">Скоро…</string>
<string name="show_sub">Этикетка Sub</string>
<string name="actor_background">Фон</string>
<string name="pref_category_looks">Oтoбpaжeниe</string>
<string name="trailer">Трейлер</string>
<string name="single_plugin_disabled" formatted="true">%s (отключено)</string>
<string name="next">Следующий</string>
<string name="blank_repo_message">В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев.
\n
\nИз-за безмозглой DMCA-атаки со стороны Sky UK Limited 🤮 мы не можем привязать сайт репозитория в приложении.
\n
\nПрисоединяйтесь к нашему Discord или ищите в интернете.</string>
<string name="error_invalid_data">Недопустимые данные</string>
<string name="resolution_and_title">Разрешение и название</string>
<string name="previous">Предыдущий</string>
<string name="resolution">Разрешение</string>
<string name="browser">Браузер</string>
<string name="library">Библиотека</string>
<string name="sort_updated_old">Обновленный (старый - новый)</string>
<string name="sort_alphabetical_a">Алфавитный (А - Я)</string>
<string name="sort_alphabetical_z">Алфавитный (Я - А)</string>
<string name="select_library">Выбрать библиотеку</string>
<string name="open_with">Открыть с</string>
<string name="empty_library_no_accounts_message">Похоже, ваша библиотека пуста :(
\nВойдите в аккаунт с библиотекой или добавьте сериалы в локальную библиотеку</string>
<string name="sort">Сортировка</string>
<string name="view_public_repositories_button_short">Открытый список</string>
<string name="sort_rating_desc">Рейтинг (высокий - низкий)</string>
<string name="sort_rating_asc">Рейтинг (низкий - высокий)</string>
<string name="sort_updated_new">Обновленный (новый - старый)</string>
<string name="sort_by">Сортировать по</string>
<string name="apk_installer_package_installer">PackageInstaller</string>
<string name="subtitles_encoding">Кодировка субтитров</string>
<string name="player_load_subtitles">Загрузить из файла</string>
<string name="extension_rating" formatted="true">Рейтинг: %s</string>
<string name="batch_download_finish_format" formatted="true">Скачано %d %s</string>
<string name="batch_download_nothing_to_download_format" formatted="true">Все %s уже скачаны</string>
<string name="batch_download_start_format" formatted="true">Начата загрузка %d %s…</string>
<string name="plugins_not_downloaded" formatted="true">Не скачано: %d</string>
<string name="download_all_plugins_from_repo">Скачать все плагины из этого репозитория\?</string>
<string name="safe_mode_title">Включен безопасный режим</string>
<string name="plugins_downloaded" formatted="true">Скачано: %d</string>
<string name="plugins_updated" formatted="true">Обновлено %d плагинов</string>
<string name="player_load_subtitles_online">Загрузить из интернета</string>
<string name="update_notification_downloading">Загрузка обновления приложения…</string>
<string name="error_invalid_url">Недопустимый URL</string>
<string name="apply_on_restart">Применить при перезапуске</string>
<string name="crash_reporting_title">Отчеты ошибках</string>
<string name="preferred_media_subtext">Что вы хотите увидеть</string>
<string name="provider_languages_tip">Смотрите видео на этих языках</string>
<string name="downloaded_file">Скачано файл</string>
<string name="poster_image">Изображение постера</string>
<string name="batch_download">Пакетная загрузка</string>
<string name="setup_extensions_subtext">Скачайте список сайтов, который вы хотите использовать</string>
<string name="display_subbed_dubbed_settings">Отображать Аниме с Дубляжом/Субтитрами</string>
<string name="enable_nsfw_on_providers">Включить NSFW на поддерживаемых провайдерах</string>
<string name="subtitles_remove_captions">Убрать скрытые субтитры из субтитров</string>
<string name="extras">Дополнительно</string>
<string name="app_layout_subtext">Изменить вид интерфейса, чтобы соответствовать устройству</string>
<string name="audio_tracks">Аудио дорожки</string>
<string name="delete_repository_plugins">Это также удалит все плагины репозитория</string>
<string name="view_public_repositories_button">Просмотреть репозитории сообщества</string>
<string name="video_tracks">Видео дорожки</string>
<string name="safe_mode_description">Все расширения были отключены из-за сбоя, чтобы помочь вам найти то, которое вызывает проблемы.</string>
<string name="skip_type_recap">Повтор</string>
<string name="clipboard_too_large">Слишком много текста. Не удалось сохранить в буфер обмена.</string>
<string name="update_notification_installing">Установка обновления приложения…</string>
<string name="update_notification_failed">Не удалось установить новую версию приложения</string>
<string name="safe_mode_file">Файл безопасного режима найден!
\nНе загружаются никакие расширения при запуске, пока файл не будет удален.</string>
<string name="delayed_update_notice">Приложение будет обновлено после выхода</string>
<string name="empty_library_logged_in_message">Похоже, этот список пуст, попробуйте переключиться на другой</string>
<string name="uppercase_all_subtitles">Все субтитры заглавными</string>
<string name="enable_skip_op_from_database_des">Показывать всплывающие окна для пропуска вступления/заключения</string>
<string name="subtitles_filter_lang">Фильтровать по предпочитаемому языку медиа</string>
<string name="error_invalid_id">Неверный ID</string>
<string name="network_adress_example">Ссылка на стрим</string>
<string name="random_button_settings_desc">Отображать рандомную кнопку на Главной странице</string>
<string name="random_button_settings">Рандомная кнопка</string>
</resources>

View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="new_update_format" formatted="true">Našla sa nová aktualizácia!
\n%s -&gt; %s</string>
<string name="filler" formatted="true">Výplň</string>
<string name="next_episode_time_hour_format" formatted="true">%dh %dm</string>
<string name="next_episode_format" formatted="true">Epizóda %d bude vydaná za</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
<string name="next_episode">Ďalšia epizóda</string>
<string name="result_tags">Žánre</string>
<string name="result_share">Zdielať</string>
<string name="result_open_in_browser">Otvoriť v prehliadači</string>
<string name="skip_loading">Preskočiť načítavanie</string>
<string name="cast_format" formatted="true">Hrajú: %s</string>
<string name="app_name">CloudStream</string>
<string name="result_poster_img_des">Plagát</string>
<string name="next_episode_time_day_format" formatted="true">%dd %dh %dm</string>
<string name="next_episode_time_min_format" formatted="true">%dm</string>
<string name="duration_format" formatted="true">%d min</string>
<string name="search_poster_img_des">\@string/result_poster_img_des</string>
<string name="episode_poster_img_des">Plagát epizódy</string>
<string name="home_main_poster_img_des">Hlavný plagát</string>
<string name="play_with_app_name">Prehrať s CloudStream</string>
<string name="title_settings">Nastavenia</string>
<string name="search_hint_site" formatted="true">Hľadať %s…</string>
<string name="popup_resume_download">Pokračovať v sťahovaní</string>
<string name="rated_format" formatted="true">Hodnotenie: %.1f</string>
<string name="go_back_img_des">Ísť späť</string>
<string name="player_speed_text_format" formatted="true">Rýchlosť (%.2fx)</string>
<string name="home_change_provider_img_des">Zmeniť poskytovateľa</string>
<string name="title_home">Domov</string>
<string name="title_search">Hľadať</string>
<string name="search_hint">Hľadať…</string>
<string name="title_downloads">Sťahovanie</string>
<string name="no_data">Žiadne dáta</string>
<string name="sort_cancel">Zrušiť</string>
<string name="sort_copy">Kopírovať</string>
<string name="sort_close">Zavrieť</string>
<string name="sort_save">Uložiť</string>
<string name="download">Stiahnuť</string>
<string name="downloaded">Stiahnuté</string>
<string name="episode_more_options_des">Ďalšie možnosti</string>
<string name="pick_source">Zdroje</string>
<string name="go_back">Ísť späť</string>
<string name="download_failed">Sťahovanie zlyhalo</string>
<string name="download_paused">Sťahovanie pozastavené</string>
<string name="download_done">Sťahovanie dokončené</string>
<string name="error_loading_links_toast">Chyba pri načítavaní odkazov</string>
<string name="update_started">Aktualizácia spustená</string>
<string name="download_storage_text">Interné úložisko</string>
<string name="loading">Načítavanie…</string>
<string name="type_completed">Dokončené</string>
<string name="type_plan_to_watch">Plánujem pozerať</string>
<string name="pref_disable_acra">Zakázať automatické nahlasovanie chýb</string>
<string name="home_more_info">Viac informácií</string>
<string name="error_bookmarks_text">Záložky</string>
<string name="play_movie_button">Prehrať film</string>
<string name="play_trailer_button">Prehrať upútavku</string>
<string name="downloading">Sťahovanie</string>
<string name="download_canceled">Sťahovanie zrušené</string>
<string name="app_dubbed_text">Dab</string>
<string name="popup_delete_file">Zmazať súbor</string>
<string name="type_none">Žiadny</string>
<string name="app_subbed_text">Tit</string>
<string name="type_re_watching">Opätovné sledovanie</string>
<string name="popup_play_file">Prehrať súbor</string>
<string name="home_info">Info</string>
<string name="play_livestream_button">Prehrať živý prenos</string>
<string name="pick_subtitle">Titulky</string>
<string name="play_episode">Prehrať epizódu</string>
<string name="popup_pause_download">Pozastaviť sťahovanie</string>
<string name="home_expanded_hide">Skryť</string>
<string name="filter_bookmarks">Filtrovať záložky</string>
<string name="action_remove_from_bookmarks">Odstrániť</string>
<string name="sort_apply">Použiť</string>
<string name="download_started">Sťahovanie spustené</string>
<string name="sort_clear">Vyčistiť</string>
<string name="home_play">Prehrať</string>
<string name="action_add_to_bookmarks">Nastaviť stav sledovania</string>
<string name="player_speed">Rýchlosť prehrávania</string>
<string name="subs_outline_color">Farba obrysu</string>
<string name="subs_window_color">Farba okna</string>
<string name="subs_edge_type">Typ hrany</string>
<string name="subtitles_settings">Nastavenia titulkov</string>
<string name="subs_background_color">Farba pozadia</string>
<string name="subs_text_color">Farba textu</string>
<string name="subs_subtitle_elevation">Vyvýšenie titulkov</string>
<string name="search_provider_text_providers">Hľadať pomocou poskytovateľov</string>
<string name="subs_font">Písmo</string>
<string name="search_provider_text_types">Hľadať pomocou typov</string>
<string name="subs_auto_select_language">Automaticky vybrať jazyk</string>
<string name="subs_subtitle_languages">Jazyk titulkov</string>
<string name="subs_font_size">Veľkosť písma</string>
<string name="benene_count_text_none">Nedarovali ste žiadne benény</string>
<string name="subs_hold_to_reset_to_default">Podržaním obnovíte predvolené nastavenia</string>
<string name="benene_count_text">%d benénov darovaných vývojárom</string>
<string name="subs_download_languages">Stiahnuť jazyky</string>
<string name="action_remove_watching">Odstrániť</string>
<string name="vpn_torrent">Tento poskytovateľ je torrent, odporúča sa VPN</string>
<string name="subs_import_text" formatted="true">Importovať písma ich umiestnením do %s</string>
<string name="action_open_watching">Viac informácií</string>
<string name="action_open_play">\@string/home_play</string>
<string name="continue_watching">Pokračovať v sledovaní</string>
<string name="vpn_might_be_needed">Na správne fungovanie tohto poskytovateľa môže byť potrebná VPN</string>
<string name="provider_info_meta">Stránka neposkytla žiadne metadáta, načítanie videa zlyhá, ak na stránke neexistuje.</string>
<string name="torrent_plot">Popis</string>
</resources>

View file

@ -4,7 +4,7 @@
<string name="next_episode_time_day_format" formatted="true">%dm %ds %dd</string>
<string name="next_episode_time_hour_format" formatted="true">%ds %dd</string>
<string name="next_episode_time_min_format" formatted="true">%dd</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%s Xlq %d</string>
<string name="next_episode_format" formatted="true">Xalqadda %d waxa lasoo deyn doonaa</string>
<string name="download">Daji</string>
<string name="download_failed">Dejintii ma guulaysan</string>

View file

@ -13,7 +13,7 @@
<string name="year_format" formatted="true" translatable="false">%d</string>
<string name="app_dub_sub_episode_text_format" formatted="true">%s Ep %d</string>
<string name="cast_format" formatted="true">Cast: %s</string>
<string name="next_episode_format" formatted="true">Bölüm %d şu tarihte yayınlanacak:</string>
<string name="next_episode_format" formatted="true">Bölüm %d şu tarihte yayınlanacak</string>
<string name="next_episode_time_day_format" formatted="true">%dd %dh %dm</string>
<string name="next_episode_time_hour_format" formatted="true">%dh %dm</string>
<string name="next_episode_time_min_format" formatted="true">%dm</string>
@ -435,18 +435,18 @@
<string name="skip_setup">Kurulumu atla</string>
<string name="app_layout_subtext">Cihazınıza uygun görünümü seçin</string>
<string name="crash_reporting_title">Çökme raporları</string>
<string name="preferred_media_subtext">Ne izlemek istiyorsunuz\?</string>
<string name="preferred_media_subtext">Ne izlemek istiyorsunuz</string>
<string name="setup_done">Bitti</string>
<string name="extensions">Eklentiler</string>
<string name="add_repository">Depo ekle</string>
<string name="repository_name_hint">Depo ismi</string>
<string name="repository_url_hint">Depo URL\'i</string>
<string name="repository_url_hint">Depo URL\'si</string>
<string name="plugin_loaded">Eklenti yüklendi</string>
<string name="plugin_deleted">Eklenti silindi</string>
<string name="plugin_load_fail" formatted="true">%s yüklenemedi</string>
<string name="is_adult">+18</string>
<string name="batch_download_start_format" formatted="true">%d %s indirilmeye başlandı</string>
<string name="batch_download_finish_format" formatted="true">%d %s başarıyla indirildi</string>
<string name="batch_download_start_format" formatted="true">%d %s indirilmeye başlandı</string>
<string name="batch_download_finish_format" formatted="true">%d %s indirildi</string>
<string name="batch_download_nothing_to_download_format" formatted="true">%s\'nin tamamı zaten indirildi</string>
<string name="batch_download">Toplu indir</string>
<string name="plugin_singular">eklenti</string>
@ -458,7 +458,11 @@
<string name="plugins_disabled" formatted="true">Devre dışı: %d</string>
<string name="plugins_not_downloaded" formatted="true">İndirilmeyen: %d</string>
<string name="plugins_updated" formatted="true">%d eklenti(ler) güncellendi</string>
<string name="blank_repo_message">Site eklentilerini yüklemek için bir depo ekleyin</string>
<string name="blank_repo_message">CloudStream\'in varsayılan olarak yüklü sitesi yoktur. Siteleri depolardan kurmanız gerekir.
\n
\nSky UK Limited tarafından beyinsiz bir DMCA yayından kaldırma 🤮 nedeniyle uygulama içinde depo linklerini bulunduramıyoruz.
\n
\nDiscord\'umuza katılın veya çevrimiçi arama yapın.</string>
<string name="view_public_repositories_button">Topluluk depolarını görüntüle</string>
<string name="view_public_repositories_button_short">Herkese açık liste</string>
<string name="uppercase_all_subtitles">Tüm alt yazılar büyük harf</string>
@ -525,4 +529,12 @@
<string name="all_languages_preference">Tüm diller</string>
<string name="skip_type_format" formatted="true">Geç %s</string>
<string name="action_remove_from_watched">İzlenenlerden kaldır</string>
<string name="skip_type_mixed_ed">Karışık son</string>
<string name="skip_type_mixed_op">Karışık başlangıç</string>
<string name="skip_type_creddits">Kredi</string>
<string name="skip_type_intro">Giriş</string>
<string name="plugin_downloaded">Eklenti İndirildi</string>
<string name="pref_category_actions">Aksiyonlar</string>
<string name="enable_skip_op_from_database_des">Açma/bitiş için atlama açılır pencerelerini göster</string>
<string name="clipboard_too_large">Çok fazla metin. Panoya kaydedilemiyor.</string>
</resources>

View file

@ -489,4 +489,19 @@
<string name="app_not_found_error">Програму не знайдено</string>
<string name="skip_type_mixed_op">Змішаний опенінг</string>
<string name="action_remove_from_watched">Видалити з переглянутого</string>
<string name="sort_updated_old">За оновленням (від старого до нового)</string>
<string name="sort_updated_new">За оновленням (від нового до старого)</string>
<string name="library">Бібліотека</string>
<string name="sort">Сортувати</string>
<string name="sort_rating_desc">За рейтингом (від високого до низького)</string>
<string name="sort_by">Сортувати за</string>
<string name="sort_alphabetical_a">За алфавітом (від А до Я)</string>
<string name="sort_rating_asc">За рейтингом (від низького до високого)</string>
<string name="empty_library_no_accounts_message">Схоже, ваша бібліотека порожня :(
\nУвійдіть в обліковий запис бібліотеки або додайте серіали до вашої локальної бібліотеки</string>
<string name="sort_alphabetical_z">За алфавітом (від Я до А)</string>
<string name="select_library">Виберіть бібліотеку</string>
<string name="open_with">Відкрити з</string>
<string name="browser">Браузер</string>
<string name="empty_library_logged_in_message">Схоже, цей список порожній, спробуйте перейти до іншого</string>
</resources>

View file

@ -66,7 +66,7 @@
<string name="download_done">Tải thành công</string>
<string name="stream">Trực tiếp</string>
<string name="error_loading_links_toast">Đã có lỗi xảy ra</string>
<string name="download_storage_text">Bộ nhớ máy</string>
<string name="download_storage_text">Bộ nhớ trong</string>
<string name="app_dubbed_text">Lồng Tiếng</string>
<string name="app_subbed_text">Phụ Đề</string>
<string name="popup_delete_file">Xóa Tệp</string>
@ -113,13 +113,12 @@
<string name="action_open_play">\@string/home_play</string>
<string name="vpn_might_be_needed">Bạn có thể sẽ cần sử dụng VPN để xem phim này</string>
<string name="vpn_torrent">Phim này được chiếu dưới dạng Torrent. Hãy sử dụng VPN để xem</string>
<string name="provider_info_meta">Siêu dữ liệu không được cung cấp bởi trang web, quá trình tải video sẽ không thành công nếu nó không tồn tại trên trang web.</string>
<string name="torrent_plot">Thông tin phim</string>
<string name="normal_no_plot">Đang cập nhật</string>
<string name="torrent_no_plot">Không tìm thấy thông tin</string>
<string name="show_log_cat">Hiển thị Logcat 🐈</string>
<string name="picture_in_picture">Chế độ cửa sổ nhỏ</string>
<string name="picture_in_picture_des">Tiếp tục xem phim khi thoát app hoặc đang tìm kiếm</string>
<string name="picture_in_picture_des">Tiếp tục xem phim khi thoát ứng dụng hoặc khi tìm kiếm</string>
<string name="player_size_settings">Bật nút thu phóng khi xem</string>
<string name="player_size_settings_des">Xóa khoảng đen của phim</string>
<string name="player_subtitles_settings">Phụ đề</string>
@ -160,7 +159,7 @@
<string name="bug_report_settings_on">Không gửi dữ liệu</string>
<string name="show_fillers_settings">Hiển thị tập phụ cho anime</string>
<string name="show_trailers_settings">Hiển thị trailer</string>
<string name="kitsu_settings">Hiển thị poster từ kitsu</string>
<string name="kitsu_settings">Hiển thị poster từ Kitsu</string>
<string name="pref_filter_search_quality">Ẩn chất lượng video khi tìm kiếm</string>
<string name="automatic_plugin_updates">Tự động cập nhật plugin</string>
<string name="updates_settings">Hiển thị thông báo cập nhật App</string>
@ -211,7 +210,7 @@
<string name="no_subtitles">Không có phụ đề</string>
<string name="default_subtitles">Mặc Định</string>
<string name="free_storage">Còn trống</string>
<string name="used_storage">Đã ng</string>
<string name="used_storage">Đã sử dụng</string>
<string name="app_storage">App</string>
<!--plural-->
<string name="movies">Phim Lẻ</string>
@ -229,7 +228,7 @@
<string name="movies_singular">Phim Lẻ</string>
<string name="tv_series_singular">Phim Bộ</string>
<string name="cartoons_singular">Hoạt Hình</string>
<string name="anime_singular">\@string/anime</string>
<string name="anime_singular">Anime</string>
<string name="ova_singular">\@string/ova</string>
<string name="torrent_singular">Torrent</string>
<string name="documentaries_singular">Phim Tài Liệu</string>
@ -262,7 +261,7 @@
<string name="video_lock">Khóa</string>
<string name="video_aspect_ratio_resize">Thu Phóng</string>
<string name="video_source">Tuỳ chọn</string>
<string name="video_skip_op">Tập tiếp</string>
<string name="video_skip_op">Tua nhanh</string>
<string name="dont_show_again">Không hiện lại</string>
<string name="skip_update">Bỏ qua</string>
<string name="update">Cập nhật</string>
@ -273,8 +272,8 @@
<string name="video_buffer_length_settings">Thời lượng bộ nhớ đệm</string>
<string name="video_buffer_disk_settings">Dung lượng video cache</string>
<string name="video_buffer_clear_settings">Xoá hình ảnh và video</string>
<string name="video_ram_description">Sẽ gây lỗi nếu đặt quá cao. Không thay đổi nếu máy có dung lượng ram thấp, chẳng hạn như Android TV hoặc điện thoại cũ</string>
<string name="video_disk_description">Sẽ thể gây lỗi trên các máy có dung lượng lưu trữ thấp, chẳng hạn như thiết bị Android TV nếu bạn đặt nó quá cao</string>
<string name="video_ram_description">Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng ram thấp như Android TV.</string>
<string name="video_disk_description">Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng lưu trữ thấp như Android TV.</string>
<string name="dns_pref">DNS over HTTPS</string>
<string name="dns_pref_summary">Rất hữu ích để bỏ chặn ISP</string>
<string name="add_site_pref">Sao chép trang web</string>
@ -411,7 +410,7 @@
<string name="plugin_deleted">Đã xoá plugin</string>
<string name="plugin_load_fail" formatted="true">Không tải được %s</string>
<string name="is_adult">18+</string>
<string name="batch_download_start_format" formatted="true">Bắt đầu tải %d %s</string>
<string name="batch_download_start_format" formatted="true">Bắt đầu tải %d %s</string>
<string name="batch_download_finish_format" formatted="true">Tải xuống %d %s thành công</string>
<string name="batch_download_nothing_to_download_format" formatted="true">Toàn bộ %s đã được tải xuống</string>
<string name="batch_download">Tải hàng loạt</string>
@ -423,7 +422,11 @@
<string name="plugins_downloaded" formatted="true">Đã tải: %d</string>
<string name="plugins_disabled" formatted="true">Đã vô hiệu: %d</string>
<string name="plugins_not_downloaded" formatted="true">Không tải: %d</string>
<string name="blank_repo_message">Thêm kho lưu trữ để cài tiện ích</string>
<string name="blank_repo_message">CloudStream không có sẵn trang web nào. Bạn cần cài đặt các trang web từ kho lưu trữ.
\n
\nDo Sky UK Limited đã gỡ xuống theo DMCA một cách thiếu suy nghĩ 🤮 chúng tôi không thể cài sẵn trang web.
\n
\nHãy tham gia Discord của chúng tôi hoặc tìm kiếm trực tuyến.</string>
<string name="view_public_repositories_button">Xem kho lưu trữ của cộng đồng</string>
<string name="view_public_repositories_button_short">Danh sách công khai</string>
<string name="uppercase_all_subtitles">In hoa toàn bộ phụ đề</string>
@ -438,13 +441,82 @@
<string name="safe_mode_crash_info">Xem thông tin sự cố</string>
<string name="history">Lịch sử</string>
<string name="action_mark_as_watched">Đánh dấu là đã xem</string>
<string name="automatic_plugin_download">Tự động tải plugin</string>
<string name="automatic_plugin_download">Tự động tải xuống plugin</string>
<string name="redo_setup_process">Thiết lập lại</string>
<string name="apk_installer_settings">Bộ cài APK</string>
<string name="apk_installer_settings_des">Một số máy không hỗ trợ trình cài đặt gói mới. Hãy thử tùy chọn cũ nếu các bản cập nhật không cài đặt.</string>
<string name="season_format">%s %d%s</string>
<string name="play_trailer_button">Xem Trailer</string>
<string name="automatic_plugin_download_summary">Tự động tải plugins còn thiếu.</string>
<string name="play_trailer_button">Xem giới thiệu</string>
<string name="automatic_plugin_download_summary">Tự động tải plugin còn thiếu.</string>
<string name="update_started">Bắt đầu cập nhật</string>
<string name="pref_category_links">Liên kết</string>
<string name="hls_playlist">Danh sách HLS</string>
<string name="player_pref">Trình phát ưu tiên</string>
<string name="player_settings_play_in_app">Trình phát mặc định</string>
<string name="extension_rating" formatted="true">Đánh giá: %s</string>
<string name="no">Không</string>
<string name="extension_version">Phiên bản</string>
<string name="extension_authors">Tác giả</string>
<string name="pref_category_app_updates">Cập nhật ứng dụng</string>
<string name="pref_category_backup">Sao lưu</string>
<string name="pref_category_extensions">Tiện ích</string>
<string name="pref_category_actions">Hành động</string>
<string name="pref_category_cache">Cache</string>
<string name="pref_category_gestures">Cử chỉ</string>
<string name="pref_category_player_features">Tính năng trình phát</string>
<string name="pref_category_subtitles">Phụ đề</string>
<string name="pref_category_player_layout">Bố cục</string>
<string name="pref_category_defaults">Mặc định</string>
<string name="pref_category_looks">Giao diện</string>
<string name="pref_category_ui_features">Tính năng</string>
<string name="plugins_updated" formatted="true">Đã cập nhật %d plugin</string>
<string name="extension_description">Mô tả</string>
<string name="extension_status">Trạng thái</string>
<string name="extension_size">Kích thước</string>
<string name="extension_types">Hỗ trợ</string>
<string name="extension_language">Ngôn ngữ</string>
<string name="extension_install_first">Cài đặt tiện ích trước</string>
<string name="player_settings_play_in_vlc">VLC</string>
<string name="player_settings_play_in_mpv">MPV</string>
<string name="player_settings_play_in_web">Web Video Cast</string>
<string name="player_settings_play_in_browser">Trình duyệt web</string>
<string name="app_not_found_error">Không thấy ứng dụng</string>
<string name="all_languages_preference">Tất cả ngôn ngữ</string>
<string name="skip_type_format" formatted="true">Tua %s</string>
<string name="skip_type_op">Mở đầu</string>
<string name="skip_type_ed">Kết thúc</string>
<string name="skip_type_recap">Tóm tắt</string>
<string name="skip_type_mixed_ed">Mở đầu tuỳ chọn</string>
<string name="skip_type_mixed_op">Kết thúc tuỳ chọn</string>
<string name="skip_type_creddits">Danh đề</string>
<string name="skip_type_intro">Giới thiệu</string>
<string name="clear_history">Xoá lịch sử</string>
<string name="enable_skip_op_from_database_des">Hiển thị nút tua nhanh cho mở đầu/kết thúc</string>
<string name="clipboard_too_large">Văn bản quá dài. Không thể lưu vào bộ nhớ tạm.</string>
<string name="action_remove_from_watched">Xoá khỏi đã xem</string>
<string name="confirm_exit_dialog">Bạn có chắc muốn thoát\?</string>
<string name="yes"></string>
<string name="update_notification_downloading">Đang tải bản cập nhật…</string>
<string name="update_notification_installing">Đang cài bản cập nhật…</string>
<string name="update_notification_failed">Không thể cài đặt phiên bản mới</string>
<string name="delayed_update_notice">Ứng dụng sẽ được cập nhật khi thoát</string>
<string name="library">Thư viện</string>
<string name="browser">Trình duyệt</string>
<string name="plugin_downloaded">Plugin đã tải</string>
<string name="apk_installer_legacy">Mặc định</string>
<string name="sort_updated_new">Tải lên (Mới đến Cũ)</string>
<string name="sort_updated_old">Tải lên (Cũ đến Mới)</string>
<string name="empty_library_no_accounts_message">Thư viện của bạn đang trống :(
\nHãy đăng nhập vào thư viện hoặc thêm phim vào thư viện cục bộ</string>
<string name="open_with">Mở với</string>
<string name="provider_info_meta">Siêu dữ liệu không có sẵn, video sẽ không được tải nếu nó không tồn tại trên trang web.</string>
<string name="apk_installer_package_installer">PackageInstaller</string>
<string name="sort">Sắp xếp</string>
<string name="sort_rating_desc">Xếp hạng (Cao đến Thấp)</string>
<string name="sort_rating_asc">Xếp hạng (Thấp đến Cao)</string>
<string name="sort_alphabetical_z">Chữ cái (Z đến A)</string>
<string name="sort_by">Sắp xếp</string>
<string name="empty_library_logged_in_message">Có vẻ như danh sách này trống, hãy thử chuyển sang danh sách khác</string>
<string name="sort_alphabetical_a">Chữ cái (A đến Z)</string>
<string name="select_library">Chọn Thư viện</string>
</resources>

View file

@ -537,4 +537,21 @@
<string name="delayed_update_notice">应用退出后将会更新</string>
<string name="plugin_downloaded">插件已下载</string>
<string name="action_remove_from_watched">从已观看中移除</string>
<string name="safe_mode_file">发现安全模式文件!
\n启动时不加载任何扩展直到文件被删除。</string>
<string name="browser">浏览器</string>
<string name="library"></string>
<string name="sort_by">排序方式</string>
<string name="sort">排序</string>
<string name="sort_rating_desc">评分(从高到低)</string>
<string name="sort_rating_asc">评分(从低到高)</string>
<string name="sort_updated_new">更新(从新到旧)</string>
<string name="sort_updated_old">更新(从旧到新)</string>
<string name="sort_alphabetical_a">字母排序(从 A 到 Z</string>
<string name="sort_alphabetical_z">字母排序(从 Z 到 A</string>
<string name="select_library">选择库</string>
<string name="open_with">打开方式</string>
<string name="empty_library_no_accounts_message">看来您的库是空的 :(
\n登录库账户或添加节目到您的本地库</string>
<string name="empty_library_logged_in_message">看来此列表是空的,请尝试切换到另一个</string>
</resources>

View file

@ -36,6 +36,8 @@
<color name="subColorBg">#F53B66</color>
<color name="typeColorText">#BEC8FF</color>
<color name="typeColorBg">?attr/colorPrimaryDark</color>
<color name="ratingColor">#4C3115</color>
<color name="ratingColorBg">#FFA662</color>
<color name="adultColor">#FF6F63</color> <!-- same as sub color -->

View file

@ -112,6 +112,7 @@
<string name="result_tags">Genres</string>
<string name="result_share">Share</string>
<string name="result_open_in_browser">Open In Browser</string>
<string name="browser">Browser</string>
<string name="skip_loading">Skip Loading</string>
<string name="loading">Loading…</string>
<string name="type_watching">Watching</string>
@ -231,6 +232,7 @@
<string name="backup_failed">Storage permissions missing. Please try again.</string>
<string name="backup_failed_error_format">Error backing up %s</string>
<string name="search">Search</string>
<string name="library">Library</string>
<string name="category_account">Accounts</string>
<string name="category_updates">Updates and backup</string>
<string name="settings_info">Info</string>
@ -623,5 +625,17 @@
<string name="apk_installer_legacy">Legacy</string>
<string name="apk_installer_package_installer">PackageInstaller</string>
<string name="delayed_update_notice">App will be updated upon exit</string>
<string name="sort_by">Sort by</string>
<string name="sort">Sort</string>
<string name="sort_rating_desc">Rating (High to Low)</string>
<string name="sort_rating_asc">Rating (Low to High)</string>
<string name="sort_updated_new">Updated (New to Old)</string>
<string name="sort_updated_old">Updated (Old to New)</string>
<string name="sort_alphabetical_a">Alphabetical (A to Z)</string>
<string name="sort_alphabetical_z">Alphabetical (Z to A)</string>
<string name="select_library">Select Library</string>
<string name="open_with">Open with</string>
<string name="empty_library_no_accounts_message">Looks like your library is empty :(\nLogin to a library account or add shows to your local library</string>
<string name="empty_library_logged_in_message">Looks like this list is empty, try switching to another one</string>
<string name="safe_mode_file">Safe mode file found!\nNot loading any extensions on startup until file is removed.</string>
</resources>

View file

@ -90,11 +90,13 @@
<item name="android:fontFamily">@font/google_sans</item>
<item name="chipMinTouchTargetSize">0dp</item>
</style>
<style name="ChipFilledSemiTransparent" parent="@style/ChipFilled">
<item name="chipBackgroundColor">@color/transparent</item>
<item name="chipSurfaceColor">@color/semiWhite</item>
<item name="backgroundColor">@color/transparent</item>
</style>
<style name="ChipParent">
<item name="chipSpacingVertical">5dp</item>
<item name="chipSpacingHorizontal">5dp</item>
@ -123,6 +125,14 @@
<item name="android:textColor">@color/subColorText</item>
</style>
<style name="RatingButton" parent="@style/SearchBox">
<item name="android:minWidth">30dp</item>
<item name="android:background">@drawable/rating_bg_color</item>
<item name="drawableTint">@color/ratingColor</item>
<item name="android:textColor">@color/ratingColor</item>
<item name="drawableStartCompat">@drawable/ic_baseline_star_24</item>
</style>
<style name="TypeButton" parent="@style/SearchBox">
<item name="android:background">@drawable/type_bg_color</item>
<item name="android:text">@string/quality_hd</item>
@ -375,6 +385,11 @@
<item name="textAllCaps">false</item>
</style>
<style name="TabNoCaps" parent="TextAppearance.Design.Tab">
<item name="textAllCaps">false</item>
<item name="fontFamily">@font/google_sans</item>
</style>
<style name="AppTextViewStyle" parent="android:Widget.TextView">
<item name="android:fontFamily">@font/google_sans</item>
</style>