mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge branch 'master' into AndroidTVSeekBehavior
# Conflicts: # app/src/main/res/values-es/strings.xml
This commit is contained in:
commit
537d764953
80 changed files with 2468 additions and 331 deletions
12
.github/workflows/issue_action.yml
vendored
12
.github/workflows/issue_action.yml
vendored
|
@ -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:
|
||||
|
|
|
@ -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) {
|
||||
|
@ -250,4 +253,4 @@ tasks.withType<DokkaTask>().configureEach {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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*/
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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(
|
||||
name,
|
||||
it.file,
|
||||
"$mainUrl/",
|
||||
).forEach { m3uData -> sources.add(m3uData) }
|
||||
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/",
|
||||
Qualities.Unknown.value,
|
||||
URI(it.file).path.endsWith(".m3u8")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
private data class ResponseSource(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
* */
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
/** WARNING ASSUMES ONE USER! **/
|
||||
getKeys(ANILIST_USER_KEY)?.forEach { key ->
|
||||
getKey<AniListUser>(key, null)?.let {
|
||||
userID = it.id
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
||||
val fixedUserID = userID ?: return null
|
||||
// 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! **/
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
5
app/src/main/res/drawable/ic_baseline_sort_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_sort_24.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
6
app/src/main/res/drawable/indicator_background.xml
Normal file
6
app/src/main/res/drawable/indicator_background.xml
Normal 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>
|
6
app/src/main/res/drawable/rating_bg_color.xml
Normal file
6
app/src/main/res/drawable/rating_bg_color.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
177
app/src/main/res/layout/fragment_library.xml
Normal file
177
app/src/main/res/layout/fragment_library.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
11
app/src/main/res/layout/library_viewpager_page.xml
Normal file
11
app/src/main/res/layout/library_viewpager_page.xml
Normal 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" />
|
||||
|
37
app/src/main/res/layout/loading_poster_dynamic.xml
Normal file
37
app/src/main/res/layout/loading_poster_dynamic.xml
Normal 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>
|
|
@ -96,33 +96,36 @@
|
|||
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: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_rez"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="2.5dp"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/white"
|
||||
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-->
|
||||
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -5,62 +5,109 @@
|
|||
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.cardview.widget.CardView
|
||||
android:id="@+id/background_card"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="2dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:elevation="10dp"
|
||||
app:cardBackgroundColor="?attr/primaryGrayBackground"
|
||||
app:cardCornerRadius="@dimen/rounded_image_radius">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/background_card"
|
||||
android:layout_width="match_parent"
|
||||
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:layout_constraintDimensionRatio="1:1.414"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/search_poster_img_des"
|
||||
android:duplicateParentState="true"
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/example_poster" />
|
||||
|
||||
<TextView android:id="@+id/text_quality" style="@style/TypeButton" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:orientation="vertical">
|
||||
<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"
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/example_poster" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_is_dub"
|
||||
style="@style/DubButton"
|
||||
android:layout_gravity="end" />
|
||||
android:id="@+id/text_quality"
|
||||
style="@style/TypeButton" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_is_sub"
|
||||
style="@style/SubButton"
|
||||
android:layout_gravity="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_flag"
|
||||
style="@style/SearchBox"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="@color/transparent"
|
||||
android:textSize="20sp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_is_dub"
|
||||
style="@style/DubButton"
|
||||
android:layout_gravity="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_is_sub"
|
||||
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"
|
||||
android:layout_gravity="end"
|
||||
android:background="@color/transparent"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone"
|
||||
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:text="🇸🇪"
|
||||
tools:progress="50"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/imageText"
|
||||
|
@ -78,4 +125,4 @@
|
|||
android:textColor="?attr/textColor"
|
||||
android:textSize="13sp"
|
||||
tools:text="The Perfect Run\nThe Perfect Run" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -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:id="@+id/navigation_home"
|
||||
android:icon="@drawable/home_alt"
|
||||
android:title="@string/title_home" />
|
||||
<item
|
||||
android:id="@+id/navigation_search"
|
||||
android:icon="@drawable/search_icon"
|
||||
android:title="@string/title_search"/>
|
||||
android:id="@+id/navigation_search"
|
||||
android:icon="@drawable/search_icon"
|
||||
android:title="@string/title_search" />
|
||||
<item
|
||||
android:id="@+id/navigation_downloads"
|
||||
android:icon="@drawable/netflix_download"
|
||||
android:title="@string/title_downloads"/>
|
||||
android:id="@+id/navigation_library"
|
||||
android:icon="@drawable/ic_outline_account_circle_24"
|
||||
android:title="@string/library" />
|
||||
<item
|
||||
android:id="@+id/navigation_settings"
|
||||
android:icon="@drawable/settings_alt"
|
||||
android:title="@string/title_settings"/>
|
||||
android:id="@+id/navigation_downloads"
|
||||
android:icon="@drawable/netflix_download"
|
||||
android:title="@string/title_downloads" />
|
||||
<item
|
||||
android:id="@+id/navigation_settings"
|
||||
android:icon="@drawable/ic_outline_settings_24"
|
||||
android:title="@string/title_settings" />
|
||||
</menu>
|
17
app/src/main/res/menu/library_menu.xml
Normal file
17
app/src/main/res/menu/library_menu.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
107
app/src/main/res/values-sk/strings.xml
Normal file
107
app/src/main/res/values-sk/strings.xml
Normal 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 -> %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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">Đã dù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">Có</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>
|
|
@ -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>
|
|
@ -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 -->
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue