Merge remote-tracking branch 'origin/master'

This commit is contained in:
Blatzar 2022-07-30 17:44:24 +02:00
commit 59b5732925
97 changed files with 570 additions and 237 deletions

View file

@ -376,6 +376,32 @@ data class ProvidersInfoJson(
@JsonProperty("status") var status: Int,
)
data class MainPageData(
val name: String,
val data: String,
)
/** return list of MainPageData with url to name, make for more readable code */
fun mainPageOf(vararg elements: Pair<String, String>): List<MainPageData> {
return elements.map { (url, name) -> MainPageData(name = name, data = url) }
}
fun newHomePageResponse(
name: String,
list: List<SearchResponse>,
hasNext: Boolean? = null
): HomePageResponse {
return HomePageResponse(
listOf(HomePageList(name, list)),
hasNext = hasNext ?: list.isNotEmpty()
)
}
fun newHomePageResponse(list: HomePageList, hasNext: Boolean? = null): HomePageResponse {
return HomePageResponse(listOf(list), hasNext = hasNext ?: list.list.isNotEmpty())
}
/**Every provider will **not** have try catch built in, so handle exceptions when calling these functions*/
abstract class MainAPI {
companion object {
@ -431,8 +457,14 @@ abstract class MainAPI {
open val vpnStatus = VPNStatus.None
open val providerType = ProviderType.DirectProvider
open val mainPage = listOf(MainPageData("", ""))
@WorkerThread
open suspend fun getMainPage(): HomePageResponse? {
open suspend fun getMainPage(
page: Int,
categoryName: String,
categoryData: String
): HomePageResponse? {
throw NotImplementedError()
}
@ -632,7 +664,8 @@ fun TvType.isAnimeOp(): Boolean {
data class SubtitleFile(val lang: String, val url: String)
data class HomePageResponse(
val items: List<HomePageList>
val items: List<HomePageList>,
val hasNext: Boolean = false
)
data class HomePageList(

View file

@ -104,7 +104,7 @@ class AllAnimeProvider : MainAPI() {
@JsonProperty("__typename") val _typename: String? = null
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
// Pair(

View file

@ -114,7 +114,7 @@ class AniPlayProvider : MainAPI() {
@JsonProperty("videoUrl") val url: String
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val response = app.get("$mainUrl/api/home/latest-episodes?page=0").parsed<List<ApiMainPageAnime>>()
val results = response.map{

View file

@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.animeproviders
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
import java.net.URLDecoder
@ -45,7 +44,7 @@ class AniflixProvider : MainAPI() {
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val soup = app.get(mainUrl).document
val elements = listOf(
@ -75,7 +74,6 @@ class AniflixProvider : MainAPI() {
val token = getToken()
val url = "$mainUrl/_next/data/$token/search.json?keyword=$query"
val response = app.get(url)
println("resp: $url ===> ${response.text}")
val searchResponse =
response.parsedSafe<Search>()
?: throw ErrorLoadingException("No Media")

View file

@ -62,7 +62,7 @@ class AnimeIndoProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = request(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -58,7 +58,7 @@ class AnimePaheProvider : MainAPI() {
TvType.OVA
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
data class Data(
@JsonProperty("id") val id: Int,
@JsonProperty("anime_id") val animeId: Int,

View file

@ -47,7 +47,7 @@ class AnimeSailProvider : MainAPI() {
)
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = request(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -64,7 +64,7 @@ class AnimeSaturnProvider : MainAPI() {
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val list = ArrayList<HomePageList>()
document.select("div.container:has(span.badge-saturn)").forEach {

View file

@ -130,7 +130,7 @@ class AnimeWorldProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = request(mainUrl).document
val list = ArrayList<HomePageList>()

View file

@ -30,7 +30,7 @@ class AnimefenixProvider:MainAPI() {
else DubStatus.Subbed
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = listOf(
Pair("$mainUrl/", "Animes"),
Pair("$mainUrl/animes?type[]=movie&order=default", "Peliculas", ),

View file

@ -22,7 +22,7 @@ class AnimeflvIOProvider:MainAPI() {
TvType.OVA,
TvType.Anime,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/series", "Series actualizadas",),

View file

@ -34,7 +34,7 @@ class AnimeflvnetProvider : MainAPI() {
TvType.Anime,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = listOf(
Pair("$mainUrl/browse?type[]=movie&order=updated", "Películas"),
Pair("$mainUrl/browse?status[]=2&order=default", "Animes"),

View file

@ -26,7 +26,7 @@ class AnimekisaProvider : MainAPI() {
@JsonProperty("html") val html: String
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = listOf(
Pair("$mainUrl/ajax/list/views?type=all", "All animes"),
Pair("$mainUrl/ajax/list/views?type=day", "Trending now"),

View file

@ -96,7 +96,7 @@ class DubbedAnimeProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val trendingUrl = "$mainUrl/xz/trending.php?_=$unixTimeMS"
val lastEpisodeUrl = "$mainUrl/xz/epgrid.php?p=1&_=$unixTimeMS"
val recentlyAddedUrl = "$mainUrl/xz/gridgrabrecent.php?p=1&_=$unixTimeMS"

View file

@ -178,7 +178,7 @@ class GogoanimeProvider : MainAPI() {
TvType.OVA
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val headers = mapOf(
"authority" to "ajax.gogo-load.com",
"sec-ch-ua" to "\"Google Chrome\";v=\"89\", \"Chromium\";v=\"89\", \";Not A Brand\";v=\"99\"",

View file

@ -46,7 +46,7 @@ class GomunimeProvider : MainAPI() {
@JsonProperty("html") val html: String
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = listOf(
Pair("e", "Episode Baru"),
Pair("c", "Completed"),

View file

@ -32,7 +32,7 @@ class JKAnimeProvider : MainAPI() {
TvType.Anime,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = listOf(
Pair(
"$mainUrl/directorio/?filtro=fecha&tipo=TV&estado=1&fecha=none&temporada=none&orden=desc",

View file

@ -14,7 +14,7 @@ class KawaiifuProvider : MainAPI() {
override val supportedTypes = setOf(TvType.Anime, TvType.AnimeMovie)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val resp = app.get(mainUrl).text

View file

@ -17,7 +17,7 @@ class KimCartoonProvider : MainAPI() {
return if (url.startsWith("/")) mainUrl + url else url
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val doc = app.get(mainUrl).document.select("#container")
val response = mutableListOf(
HomePageList(

View file

@ -38,7 +38,7 @@ class KuramanimeProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -39,7 +39,7 @@ class KuronimeProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -34,7 +34,7 @@ class MonoschinosProvider : MainAPI() {
TvType.Anime,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = listOf(
Pair("$mainUrl/emision", "En emisión"),
Pair(

View file

@ -23,7 +23,7 @@ class MundoDonghuaProvider : MainAPI() {
TvType.Anime,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = listOf(
Pair("$mainUrl/lista-donghuas", "Donghuas"),
)

View file

@ -40,7 +40,7 @@ class NeonimeProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -137,44 +137,45 @@ class NineAnimeProvider : MainAPI() {
private fun decode(input: String): String = java.net.URLDecoder.decode(input, "utf-8")
}
override suspend fun getMainPage(): HomePageResponse {
val items = listOf(
"$mainUrl/ajax/home/widget/trending?page=1" to "Trending",
"$mainUrl/ajax/home/widget/updated-all?page=1" to "All",
"$mainUrl/ajax/home/widget/updated-sub?page=1" to "Recently Updated (SUB)",
"$mainUrl/ajax/home/widget/updated-dub?page=1" to
"Recently Updated (DUB)",
"$mainUrl/ajax/home/widget/updated-china?page=1" to
"Recently Updated (Chinese)",
"$mainUrl/ajax/home/widget/random?page=1" to "Random",
).apmap { (url, name) ->
val home = Jsoup.parse(
app.get(
url
).parsed<Response>().html
).select("div.item").mapNotNull { element ->
val title = element.selectFirst(".info > .name") ?: return@mapNotNull null
val link = title.attr("href")
val poster = element.selectFirst(".poster > a > img")?.attr("src")
val meta = element.selectFirst(".poster > a > .meta > .inner > .left")
val subbedEpisodes = meta?.selectFirst(".sub")?.text()?.toIntOrNull()
val dubbedEpisodes = meta?.selectFirst(".dub")?.text()?.toIntOrNull()
override val mainPage = mainPageOf(
"$mainUrl/ajax/home/widget/trending?page=" to "Trending",
"$mainUrl/ajax/home/widget/updated-all?page=" to "All",
"$mainUrl/ajax/home/widget/updated-sub?page=" to "Recently Updated (SUB)",
"$mainUrl/ajax/home/widget/updated-dub?page=" to "Recently Updated (DUB)",
"$mainUrl/ajax/home/widget/updated-china?page=" to "Recently Updated (Chinese)",
"$mainUrl/ajax/home/widget/random?page=" to "Random",
)
newAnimeSearchResponse(title.text() ?: return@mapNotNull null, link) {
this.posterUrl = poster
addDubStatus(
dubbedEpisodes != null,
subbedEpisodes != null,
dubbedEpisodes,
subbedEpisodes
)
}
override suspend fun getMainPage(
page: Int,
categoryName: String,
categoryData: String
): HomePageResponse {
val url = categoryData + page
val home = Jsoup.parse(
app.get(
url
).parsed<Response>().html
).select("div.item").mapNotNull { element ->
val title = element.selectFirst(".info > .name") ?: return@mapNotNull null
val link = title.attr("href")
val poster = element.selectFirst(".poster > a > img")?.attr("src")
val meta = element.selectFirst(".poster > a > .meta > .inner > .left")
val subbedEpisodes = meta?.selectFirst(".sub")?.text()?.toIntOrNull()
val dubbedEpisodes = meta?.selectFirst(".dub")?.text()?.toIntOrNull()
newAnimeSearchResponse(title.text() ?: return@mapNotNull null, link) {
this.posterUrl = poster
addDubStatus(
dubbedEpisodes != null,
subbedEpisodes != null,
dubbedEpisodes,
subbedEpisodes
)
}
HomePageList(name, home)
}
return HomePageResponse(items)
return newHomePageResponse(categoryName, home)
}
data class Response(

View file

@ -40,7 +40,7 @@ class NontonAnimeIDProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -40,7 +40,7 @@ class OploverzProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -38,7 +38,7 @@ class OtakudesuProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -44,7 +44,7 @@ class TenshiProvider : MainAPI() {
}
}*/
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val soup = app.get(mainUrl, interceptor = ddosGuardKiller).document
for (section in soup.select("#content > section")) {

View file

@ -39,7 +39,7 @@ class TocanimeProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -30,7 +30,7 @@ class WcoProvider : MainAPI() {
TvType.OVA
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = listOf(
Pair("$mainUrl/ajax/list/recently_updated?type=tv", "Recently Updated Anime"),
Pair("$mainUrl/ajax/list/recently_updated?type=movie", "Recently Updated Movies"),

View file

@ -77,7 +77,7 @@ class ZoroProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val html = app.get("$mainUrl/home").text
val document = Jsoup.parse(html)

View file

@ -37,7 +37,7 @@ class EjaTv : MainAPI() {
)
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
// Maybe this based on app language or as setting?
val language = "English"
val dataMap = mapOf(

View file

@ -196,7 +196,7 @@ open class TmdbProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
// SAME AS DISCOVER IT SEEMS
// val popularSeries = tmdb.tvService().popular(1, "en-US").execute().body()?.results?.map {

View file

@ -34,7 +34,7 @@ class AkwamProvider : MainAPI() {
)
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
// Title, Url
val moviesUrl = listOf(
"Movies" to "$mainUrl/movies",

View file

@ -27,7 +27,7 @@ class AllMoviesForYouProvider : MainAPI() {
TvType.TvSeries
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val soup = app.get(mainUrl).document
val urls = listOf(

View file

@ -18,7 +18,7 @@ class AltadefinizioneProvider : MainAPI() {
TvType.Movie
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/azione/", "Azione"),

View file

@ -110,7 +110,7 @@ class AsiaFlixProvider : MainAPI() {
)
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val headers = mapOf("X-Requested-By" to "asiaflix-web")
val response = app.get("$apiUrl/dashboard", headers = headers).text

View file

@ -23,7 +23,7 @@ open class BflixProvider : MainAPI() {
//override val uniqueId: Int by lazy { "BflixProvider".hashCode() }
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val soup = app.get("$mainUrl/home").document
val testa = listOf(

View file

@ -41,7 +41,7 @@ class CimaNowProvider : MainAPI() {
)
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val doc = app.get("$mainUrl/home", headers = mapOf("user-agent" to "MONKE")).document
val pages = doc.select("section").not("section:contains(أختر وجهتك المفضلة)").not("section:contains(تم اضافته حديثاً)").apmap {

View file

@ -17,7 +17,7 @@ class CineblogProvider : MainAPI() {
TvType.TvSeries,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/genere/azione/", "Azione"),

View file

@ -19,7 +19,7 @@ class CinecalidadProvider : MainAPI() {
)
override val vpnStatus = VPNStatus.MightBeNeeded //Due to evoload sometimes not loading
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/ver-serie/", "Series"),

View file

@ -19,7 +19,7 @@ class CuevanaProvider : MainAPI() {
TvType.TvSeries,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair(mainUrl, "Recientemente actualizadas"),

View file

@ -31,7 +31,7 @@ class DoramasYTProvider : MainAPI() {
TvType.AsianDrama,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = listOf(
Pair("$mainUrl/emision", "En emisión"),
Pair(

View file

@ -14,7 +14,7 @@ class DramaSeeProvider : MainAPI() {
override val hasDownloadSupport = true
override val supportedTypes = setOf(TvType.AsianDrama)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val headers = mapOf("X-Requested-By" to mainUrl)
val document = app.get(mainUrl, headers = headers).document
val mainbody = document.getElementsByTag("body")

View file

@ -30,7 +30,7 @@ class DramaidProvider : MainAPI() {
}
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -41,7 +41,7 @@ class EgyBestProvider : MainAPI() {
)
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
// url, title
val doc = app.get(mainUrl).document
val pages = arrayListOf<HomePageList>()

View file

@ -15,7 +15,7 @@ class ElifilmsProvider : MainAPI() {
TvType.Movie,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val newest = app.get(mainUrl).document.selectFirst("a.fav_link.premiera")?.attr("href")
val urls = listOf(

View file

@ -18,7 +18,7 @@ class EntrepeliculasyseriesProvider:MainAPI() {
)
override val vpnStatus = VPNStatus.MightBeNeeded //Due to evoload sometimes not loading
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/series/", "Series"),

View file

@ -28,7 +28,7 @@ class EstrenosDoramasProvider : MainAPI() {
TvType.AsianDrama,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = listOf(
Pair(mainUrl, "Últimas series"),
Pair("$mainUrl/category/peliculas", "Películas"),

View file

@ -35,7 +35,7 @@ class FaselHDProvider : MainAPI() {
)
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
// Title, Url
val moviesUrl = listOf(
Pair("Movies", "$mainUrl/all-movies/page/"+(0..10).random()),

View file

@ -18,7 +18,7 @@ class FilmanProvider : MainAPI() {
TvType.TvSeries
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val lists = document.select("#item-list,#series-list")
val categories = ArrayList<HomePageList>()

View file

@ -23,7 +23,7 @@ class FilmpertuttiProvider : MainAPI() {
TvType.TvSeries
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/category/serie-tv/", "Serie Tv"),

View file

@ -232,7 +232,7 @@ class FrenchStreamProvider : MainAPI() {
}
override suspend fun getMainPage(): HomePageResponse? {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse? {
val document = app.get(mainUrl).document
val docs = document.select("div.sect")
val returnList = docs.mapNotNull {

View file

@ -69,7 +69,7 @@ class HDMProvider : MainAPI() {
)
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val html = app.get(mainUrl, timeout = 25).text
val document = Jsoup.parse(html)
val all = ArrayList<HomePageList>()

View file

@ -19,7 +19,7 @@ class HDMovie5 : MainAPI() {
TvType.TvSeries,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val doc = app.get(mainUrl).document.select("div.content")
val list = mapOf(
"Featured Movies" to "featured",

View file

@ -28,7 +28,7 @@ class HDrezkaProvider : MainAPI() {
TvType.AsianDrama
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()

View file

@ -14,7 +14,7 @@ class IHaveNoTvProvider : MainAPI() {
override val supportedTypes = setOf(TvType.Documentary)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
// Uhh, I am too lazy to scrape the "latest documentaries" and "recommended documentaries",
// so I am just scraping 3 random categories
val allCategories = listOf(

View file

@ -22,7 +22,7 @@ class IdlixProvider : MainAPI() {
TvType.TvSeries,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -26,7 +26,7 @@ class KdramaHoodProvider : MainAPI() {
@JsonProperty("file") val file: String
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val doc = app.get("$mainUrl/home2").document
val home = ArrayList<HomePageList>()

View file

@ -21,7 +21,7 @@ class LayarKacaProvider : MainAPI() {
TvType.AsianDrama
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -22,7 +22,7 @@ class MultiplexProvider : MainAPI() {
TvType.AsianDrama
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -44,7 +44,7 @@ class MyCimaProvider : MainAPI() {
)
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
// Title, Url
val moviesUrl = listOf(
"Movies" to "$mainUrl/movies/page/" + (0..25).random(),

View file

@ -212,7 +212,7 @@ class NginxProvider : MainAPI() {
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val authHeader =
getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after

View file

@ -17,7 +17,7 @@ class PeliSmartProvider: MainAPI() {
)
override val vpnStatus = VPNStatus.MightBeNeeded //Due to evoload sometimes not loading
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/peliculas/", "Peliculas"),

View file

@ -18,7 +18,7 @@ class PelisflixProvider : MainAPI() {
TvType.TvSeries,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/ver-peliculas-online-gratis-fullhdc3/", "Películas"),

View file

@ -16,7 +16,7 @@ class PelisplusHDProvider:MainAPI() {
TvType.Movie,
TvType.TvSeries,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val document = app.get(mainUrl).document
val map = mapOf(

View file

@ -142,7 +142,7 @@ open class PelisplusProviderTemplate : MainAPI() {
// This loads the homepage, which is basically a collection of search results with labels.
// Optional function, but make sure to enable hasMainPage if you program this.
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = homePageUrlList
val homePageList = ArrayList<HomePageList>()
// .pmap {} is used to fetch the different pages in parallel

View file

@ -23,7 +23,7 @@ class PhimmoichillProvider : MainAPI() {
TvType.AsianDrama
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -15,7 +15,7 @@ class PinoyHDXyzProvider : MainAPI() {
override val hasMainPage = true
override val hasQuickSearch = false
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val all = ArrayList<HomePageList>()
val document = app.get(mainUrl, referer = mainUrl).document
val mainbody = document.getElementsByTag("body")

View file

@ -16,7 +16,7 @@ class PinoyMoviePediaProvider : MainAPI() {
override val hasMainPage = true
override val hasQuickSearch = false
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val all = ArrayList<HomePageList>()
val document = app.get(mainUrl).document
val mainbody = document.getElementsByTag("body")

View file

@ -90,7 +90,7 @@ class PinoyMoviesEsProvider : MainAPI() {
return all
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val all = ArrayList<HomePageList>()
val document = app.get(mainUrl).document
val mainbody = document.getElementsByTag("body")

View file

@ -27,7 +27,7 @@ class RebahinProvider : MainAPI() {
TvType.AsianDrama
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = listOf(
Pair("Featured", "xtab1"),
Pair("Film Terbaru", "xtab2"),

View file

@ -18,7 +18,7 @@ class SeriesflixProvider : MainAPI() {
TvType.TvSeries,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/ver-series-online/", "Series"),

View file

@ -43,7 +43,7 @@ open class SflixProvider : MainAPI() {
)
override val vpnStatus = VPNStatus.None
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val html = app.get("$mainUrl/home").text
val document = Jsoup.parse(html)

View file

@ -19,7 +19,7 @@ class SoaptwoDayProvider : MainAPI() {
TvType.TvSeries,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/movielist/", "Movies"),

View file

@ -174,7 +174,7 @@ class StreamingcommunityProvider : MainAPI() {
val posterMap = hashMapOf<String, String>()
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val document = app.get(mainUrl).document
document.select("slider-title").subList(0, 6).map { it ->

View file

@ -18,7 +18,7 @@ class TantifilmProvider : MainAPI() {
TvType.TvSeries,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val urls = listOf(
Pair("$mainUrl/watch-genre/serie-tv/", "Serie Tv"),

View file

@ -143,7 +143,7 @@ class TheFlixToProvider : MainAPI() {
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val items = ArrayList<HomePageList>()
val doc = app.get(mainUrl).document
val scriptText = doc.selectFirst("script[type=application/json]")!!.data()

View file

@ -23,7 +23,7 @@ class UakinoProvider : MainAPI() {
TvType.Anime
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -210,7 +210,7 @@ open class VidstreamProviderTemplate : MainAPI() {
// This loads the homepage, which is basically a collection of search results with labels.
// Optional function, but make sure to enable hasMainPage if you program this.
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val urls = homePageUrlList
val homePageList = ArrayList<HomePageList>()
// .pmap {} is used to fetch the different pages in parallel

View file

@ -18,7 +18,7 @@ class WatchAsianProvider : MainAPI() {
override val hasDownloadSupport = true
override val supportedTypes = setOf(TvType.AsianDrama)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val headers = mapOf("X-Requested-By" to mainUrl)
val doc = app.get(mainUrl, headers = headers).document
val rowPair = mutableListOf<Pair<String, String>>()

View file

@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.movieproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import org.jsoup.nodes.Element
@ -41,7 +40,7 @@ class XcineProvider : MainAPI() {
)
}
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val sections = document.select("div.group-film")
return HomePageResponse(sections.mapNotNull { section ->

View file

@ -19,7 +19,7 @@ class YomoviesProvider : MainAPI() {
TvType.TvSeries,
)
override suspend fun getMainPage(): HomePageResponse {
override suspend fun getMainPage(page: Int, categoryName: String, categoryData: String): HomePageResponse {
val document = app.get(mainUrl).document
val homePageList = ArrayList<HomePageList>()

View file

@ -4,6 +4,7 @@ import android.util.Log
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import com.bumptech.glide.load.HttpException
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.ErrorLoadingException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -11,6 +12,34 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException
import javax.net.ssl.SSLHandshakeException
const val DEBUG_EXCEPTION = "THIS IS A DEBUG EXCEPTION!"
class DebugException(message: String) : Exception("$DEBUG_EXCEPTION\n$message")
inline fun debugException(message: () -> String) {
if (BuildConfig.DEBUG) {
throw DebugException(message.invoke())
}
}
inline fun debugWarning(message: () -> String) {
if (BuildConfig.DEBUG) {
logError(DebugException(message.invoke()))
}
}
inline fun debugAssert(assert: () -> Boolean, message: () -> String) {
if (BuildConfig.DEBUG && assert.invoke()) {
throw DebugException(message.invoke())
}
}
inline fun debugWarning(assert: () -> Boolean, message: () -> String) {
if (BuildConfig.DEBUG && assert.invoke()) {
logError(DebugException(message.invoke()))
}
}
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.observe(this) { it?.let { t -> action(t) } }
}
@ -61,11 +90,12 @@ suspend fun <T> suspendSafeApiCall(apiCall: suspend () -> T): T? {
}
fun <T> safeFail(throwable: Throwable): Resource<T> {
val stackTraceMsg = (throwable.localizedMessage ?: "") + "\n\n" + throwable.stackTrace.joinToString(
separator = "\n"
) {
"${it.fileName} ${it.lineNumber}"
}
val stackTraceMsg =
(throwable.localizedMessage ?: "") + "\n\n" + throwable.stackTrace.joinToString(
separator = "\n"
) {
"${it.fileName} ${it.lineNumber}"
}
return Resource.Failure(false, null, null, stackTraceMsg)
}
@ -92,16 +122,31 @@ suspend fun <T> safeApiCall(
safeFail(throwable)
}
is SocketTimeoutException -> {
Resource.Failure(true, null, null, "Connection Timeout\nPlease try again later.")
Resource.Failure(
true,
null,
null,
"Connection Timeout\nPlease try again later."
)
}
is HttpException -> {
Resource.Failure(false, throwable.statusCode, null, throwable.message ?: "HttpException")
Resource.Failure(
false,
throwable.statusCode,
null,
throwable.message ?: "HttpException"
)
}
is UnknownHostException -> {
Resource.Failure(true, null, null, "Cannot connect to server, try again later.")
}
is ErrorLoadingException -> {
Resource.Failure(true, null, null, throwable.message ?: "Error loading, try again later.")
Resource.Failure(
true,
null,
null,
throwable.message ?: "Error loading, try again later."
)
}
is NotImplementedError -> {
Resource.Failure(false, null, null, "This operation is not implemented.")

View file

@ -29,6 +29,7 @@ class APIRepository(val api: MainAPI) {
val hasMainPage = api.hasMainPage
val name = api.name
val mainUrl = api.mainUrl
val mainPage = api.mainPage
val hasQuickSearch = api.hasQuickSearch
suspend fun load(url: String): Resource<LoadResponse> {
@ -59,9 +60,13 @@ class APIRepository(val api: MainAPI) {
}
}
suspend fun getMainPage(): Resource<HomePageResponse?> {
suspend fun getMainPage(page: Int, nameIndex: Int? = null): Resource<List<HomePageResponse?>> {
return safeApiCall {
api.getMainPage() ?: throw ErrorLoadingException()
nameIndex?.let { api.mainPage.getOrNull(it) }?.let { data ->
listOf(api.getMainPage(page, data.name, data.data))
} ?: api.mainPage.apmap { data ->
api.getMainPage(page, data.name, data.data)
}
}
}

View file

@ -24,6 +24,7 @@ class HomeChildItemAdapter(
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var isHorizontal: Boolean = false
var hasNext : Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layout = overrideLayout

View file

@ -20,7 +20,6 @@ import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSnapHelper
import androidx.recyclerview.widget.RecyclerView
@ -44,11 +43,12 @@ import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.search.*
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.setMaxViewPoolSize
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper
@ -122,11 +122,27 @@ class HomeFragment : Fragment() {
val errorProfilePic = errorProfilePics.random()
fun Activity.loadHomepageList(item: HomePageList, deleteCallback: (() -> Unit)? = null) {
fun Activity.loadHomepageList(
item: HomePageList,
deleteCallback: (() -> Unit)? = null,
) {
loadHomepageList(
expand = HomeViewModel.ExpandableHomepageList(item, 1, false),
deleteCallback = deleteCallback,
expandCallback = null
)
}
fun Activity.loadHomepageList(
expand: HomeViewModel.ExpandableHomepageList,
deleteCallback: (() -> Unit)? = null,
expandCallback: (suspend (String) -> HomeViewModel.ExpandableHomepageList?)? = null
) {
val context = this
val bottomSheetDialogBuilder = BottomSheetDialog(context)
bottomSheetDialogBuilder.setContentView(R.layout.home_episodes_expanded)
val title = bottomSheetDialogBuilder.findViewById<TextView>(R.id.home_expanded_text)!!
val item = expand.list
title.text = item.name
val recycle =
bottomSheetDialogBuilder.findViewById<AutofitRecyclerView>(R.id.home_expanded_recycler)!!
@ -179,8 +195,36 @@ class HomeFragment : Fragment() {
if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) {
bottomSheetDialogBuilder.dismissSafe(this)
}
}.apply {
hasNext = expand.hasNext
}
recycle.addOnScrollListener(object : RecyclerView.OnScrollListener() {
var expandCount = 0
val name = expand.list.name
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val adapter = recyclerView.adapter
if (adapter !is SearchAdapter) return
val count = adapter.itemCount
val currentHasNext = adapter.hasNext
if (!recyclerView.canScrollVertically(1) && currentHasNext && expandCount != count) {
expandCount = count
ioSafe {
expandCallback?.invoke(name)?.let { newExpand ->
(recyclerView.adapter as? SearchAdapter?)?.apply {
hasNext = newExpand.hasNext
updateList(newExpand.list.list)
}
}
}
}
}
})
val spanListener = { span: Int ->
recycle.spanCount = span
//(recycle.adapter as SearchAdapter).notifyDataSetChanged()
@ -349,8 +393,6 @@ class HomeFragment : Fragment() {
return inflater.inflate(layout, container, false)
}
private var currentHomePage: HomePageResponse? = null
private fun toggleMainVisibility(visible: Boolean) {
home_main_holder?.isVisible = visible
home_main_poster_recyclerview?.isVisible = visible
@ -541,18 +583,11 @@ class HomeFragment : Fragment() {
val d = data.value
listHomepageItems.clear()
currentHomePage = d
// println("ITEMCOUNT: ${d.values.size} ${home_master_recycler?.adapter?.itemCount}")
(home_master_recycler?.adapter as? ParentItemAdapter?)?.updateList(
d?.items?.mapNotNull {
try {
val filter = it.list.filterSearchResponse()
listHomepageItems.addAll(filter)
it.copy(list = filter)
} catch (e: Exception) {
logError(e)
null
}
} ?: listOf())
d.values.toMutableList(),
home_master_recycler
)
home_loading?.isVisible = false
home_loading_error?.isVisible = false
@ -603,13 +638,6 @@ class HomeFragment : Fragment() {
}
}
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
ParentItemAdapter(mutableListOf(), { callback ->
homeHandleSearch(callback)
}, { item ->
activity?.loadHomepageList(item)
})
val toggleList = listOf(
Pair(home_type_watching_btt, WatchType.WATCHING),
Pair(home_type_completed_btt, WatchType.COMPLETED),
@ -861,9 +889,22 @@ class HomeFragment : Fragment() {
context?.fixPaddingStatusbarView(home_statusbar)
context?.fixPaddingStatusbar(home_loading_statusbar)
home_master_recycler.adapter = adapter
home_master_recycler.layoutManager = GridLayoutManager(context, 1)
home_master_recycler.adapter =
ParentItemAdapter(mutableListOf(), { callback ->
homeHandleSearch(callback)
}, { item ->
activity?.loadHomepageList(item, expandCallback = {
homeViewModel.expandAndReturn(it)
})
}, { name ->
homeViewModel.expand(name)
})
home_master_recycler?.setMaxViewPoolSize(0, Int.MAX_VALUE)
home_master_recycler.layoutManager = object : LinearLayoutManager(context) {
override fun supportsPredictiveItemAnimations(): Boolean {
return false
}
} // GridLayoutManager(context, 1).also { it.supportsPredictiveItemAnimations() }
if (context?.isTvSettings() == false) {
LinearSnapHelper().attachToRecyclerView(home_main_poster_recyclerview) // snap

View file

@ -6,29 +6,38 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import kotlinx.android.synthetic.main.homepage_parent.view.*
class ParentItemAdapter(
private var items: MutableList<HomePageList>,
private var items: MutableList<HomeViewModel.ExpandableHomepageList>,
private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomePageList) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
private val expandCallback: ((String) -> Unit)? = null,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, i: Int): ParentViewHolder {
//println("onCreateViewHolder $i")
val layout =
if (parent.context.isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent
return ParentViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false),
clickCallback,
moreInfoClickCallback
moreInfoClickCallback,
expandCallback
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
//println("onBindViewHolder $position")
when (holder) {
is ParentViewHolder -> {
holder.bind(items[position])
@ -41,44 +50,113 @@ class ParentItemAdapter(
}
override fun getItemId(position: Int): Long {
return items[position].name.hashCode().toLong()
return items[position].list.name.hashCode().toLong()
}
@JvmName("updateListHomePageList")
fun updateList(newList: List<HomePageList>) {
// this moves all bad results to the bottom
val endList = mutableListOf<HomePageList>()
val newFilteredList = mutableListOf<HomePageList>()
for (item in newList) {
if (item.list.isEmpty()) {
endList.add(item)
} else {
newFilteredList.add(item)
}
}
newFilteredList.addAll(endList)
updateList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
.toMutableList())
}
@JvmName("updateListExpandableHomepageList")
fun updateList(
newList: MutableList<HomeViewModel.ExpandableHomepageList>,
recyclerView: RecyclerView? = null
) {
// this
// 1. prevents deep copy that makes this.items == newList
// 2. filters out undesirable results
// 3. moves empty results to the bottom (sortedBy is a stable sort)
val new =
newList.map { it.copy(list = it.list.copy(list = it.list.list.filterSearchResponse())) }
.sortedBy { it.list.list.isEmpty() }
val diffResult = DiffUtil.calculateDiff(
SearchDiffCallback(this.items, newFilteredList)
SearchDiffCallback(items, new)
)
items.clear()
items.addAll(newFilteredList)
items.addAll(new)
diffResult.dispatchUpdatesTo(this)
val mAdapter = this
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) {
mAdapter.notifyItemRangeInserted(position, count)
}
override fun onRemoved(position: Int, count: Int) {
mAdapter.notifyItemRangeRemoved(position, count)
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
mAdapter.notifyItemMoved(fromPosition, toPosition)
}
override fun onChanged(position: Int, count: Int, payload: Any?) {
// I know kinda messy, what this does is using the update or bind instead of onCreateViewHolder -> bind
recyclerView?.apply {
// this loops every viewHolder in the recycle view and checks the position to see if it is within the update range
val missingUpdates = (position until (position + count)).toMutableSet()
for (i in 0 until mAdapter.itemCount) {
val viewHolder = getChildViewHolder(getChildAt(i))
val absolutePosition = viewHolder.absoluteAdapterPosition
if (absolutePosition >= position && absolutePosition < position + count) {
val expand = items.getOrNull(absolutePosition) ?: continue
if (viewHolder is ParentViewHolder) {
missingUpdates -= absolutePosition
if (viewHolder.title.text == expand.list.name) {
viewHolder.update(expand)
} else {
viewHolder.bind(expand)
}
}
}
}
// just in case some item did not get updated
for (i in missingUpdates) {
mAdapter.notifyItemChanged(i, payload)
}
} ?: run { // in case we don't have a nice
mAdapter.notifyItemRangeChanged(position, count, payload)
}
}
})
//diffResult.dispatchUpdatesTo(this)
}
class ParentViewHolder
constructor(
itemView: View,
private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomePageList) -> Unit
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
private val expandCallback: ((String) -> Unit)? = null,
) :
RecyclerView.ViewHolder(itemView) {
val title: TextView = itemView.home_parent_item_title
val recyclerView: RecyclerView = itemView.home_child_recyclerview
private val moreInfo: FrameLayout? = itemView.home_child_more_info
fun bind(info: HomePageList) {
title.text = info.name
fun update(expand: HomeViewModel.ExpandableHomepageList) {
val info = expand.list
(recyclerView.adapter as? HomeChildItemAdapter?)?.apply {
updateList(info.list.toMutableList())
hasNext = expand.hasNext
} ?: run {
recyclerView.adapter = HomeChildItemAdapter(
info.list.toMutableList(),
clickCallback = clickCallback,
nextFocusUp = recyclerView.nextFocusUpId,
nextFocusDown = recyclerView.nextFocusDownId,
).apply {
isHorizontal = info.isHorizontalImages
}
}
}
fun bind(expand: HomeViewModel.ExpandableHomepageList) {
val info = expand.list
recyclerView.adapter = HomeChildItemAdapter(
info.list.toMutableList(),
clickCallback = clickCallback,
@ -86,29 +164,56 @@ class ParentItemAdapter(
nextFocusDown = recyclerView.nextFocusDownId,
).apply {
isHorizontal = info.isHorizontalImages
hasNext = expand.hasNext
}
title.text = info.name
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
var expandCount = 0
val name = expand.list.name
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val adapter = recyclerView.adapter
if (adapter !is HomeChildItemAdapter) return
val count = adapter.itemCount
val hasNext = adapter.hasNext
if (!recyclerView.canScrollHorizontally(1) && hasNext && expandCount != count) {
expandCount = count
expandCallback?.invoke(name)
}
}
})
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
moreInfo?.setOnClickListener {
moreInfoClickCallback.invoke(info)
moreInfoClickCallback.invoke(expand)
}
}
}
}
class SearchDiffCallback(
private val oldList: List<HomePageList>,
private val newList: List<HomePageList>
private val oldList: List<HomeViewModel.ExpandableHomepageList>,
private val newList: List<HomeViewModel.ExpandableHomepageList>
) :
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition].name == newList[newItemPosition].name
oldList[oldItemPosition].list.name == newList[newItemPosition].list.name
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition] == newList[newItemPosition]
//{
// val ret = oldList[oldItemPosition].list.list.size == newList[newItemPosition].list.list.size
// println(">>>>>>>>>>>>>>>> $ret ${oldList[oldItemPosition].list.list.size} == ${newList[newItemPosition].list.list.size}")
// return ret
//}
}

View file

@ -10,10 +10,12 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.HomePageResponse
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.debugWarning
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
@ -34,6 +36,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.collections.set
class HomeViewModel : ViewModel() {
private var repo: APIRepository? = null
@ -41,9 +44,6 @@ class HomeViewModel : ViewModel() {
private val _apiName = MutableLiveData<String>()
val apiName: LiveData<String> = _apiName
private val _page = MutableLiveData<Resource<HomePageResponse?>>()
val page: LiveData<Resource<HomePageResponse?>> = _page
private val _randomItems = MutableLiveData<List<SearchResponse>?>(null)
val randomItems: LiveData<List<SearchResponse>?> = _randomItems
@ -51,8 +51,10 @@ class HomeViewModel : ViewModel() {
return APIRepository(apis.first { it.hasMainPage })
}
private val _availableWatchStatusTypes = MutableLiveData<Pair<EnumSet<WatchType>, EnumSet<WatchType>>>()
val availableWatchStatusTypes: LiveData<Pair<EnumSet<WatchType>, EnumSet<WatchType>>> = _availableWatchStatusTypes
private val _availableWatchStatusTypes =
MutableLiveData<Pair<EnumSet<WatchType>, EnumSet<WatchType>>>()
val availableWatchStatusTypes: LiveData<Pair<EnumSet<WatchType>, EnumSet<WatchType>>> =
_availableWatchStatusTypes
private val _bookmarks = MutableLiveData<Pair<Boolean, List<SearchResponse>>>()
val bookmarks: LiveData<Pair<Boolean, List<SearchResponse>>> = _bookmarks
@ -143,6 +145,67 @@ class HomeViewModel : ViewModel() {
onGoingLoad = load(api)
}
data class ExpandableHomepageList(
var list: HomePageList,
var currentPage: Int,
var hasNext: Boolean,
)
private val expandable: MutableMap<String, ExpandableHomepageList> = mutableMapOf()
private val _page =
MutableLiveData<Resource<Map<String, ExpandableHomepageList>>>(Resource.Loading())
val page: LiveData<Resource<Map<String, ExpandableHomepageList>>> = _page
val lock: MutableSet<String> = mutableSetOf()
suspend fun expandAndReturn(name: String) : ExpandableHomepageList? {
if (lock.contains(name)) return null
lock += name
repo?.apply {
expandable[name]?.let { current ->
debugAssert({ !current.hasNext }) {
"Expand called when not needed"
}
val nextPage = current.currentPage + 1
val next = getMainPage(nextPage, mainPage.indexOfFirst { it.name == name })
if (next is Resource.Success) {
next.value.filterNotNull().forEach { main ->
main.items.forEach { newList ->
val key = newList.name
expandable[key]?.apply {
hasNext = main.hasNext
currentPage = nextPage
debugWarning({ newList.list.any { outer -> this.list.list.any { it.url == outer.url } } }) {
"Expanded contained an item that was previously already in the list\n${list.name} = ${this.list.list}\n${newList.name} = ${newList.list}"
}
this.list.list += newList.list
this.list.list.distinctBy { it.url } // just to be sure we are not adding the same shit for some reason
} ?: debugWarning {
"Expanded an item not in main load named $key, current list is ${expandable.keys}"
}
}
}
} else {
current.hasNext = false
}
}
_page.postValue(Resource.Success(expandable))
}
lock -= name
return expandable[name]
}
// this is soo over engineered, but idk how I can make it clean without making the main api harder to use :pensive:
fun expand(name: String) = viewModelScope.launch {
expandAndReturn(name)
}
private fun load(api: MainAPI?) = viewModelScope.launch {
repo = if (api != null) {
APIRepository(api)
@ -156,35 +219,43 @@ class HomeViewModel : ViewModel() {
if (repo?.hasMainPage == true) {
_page.postValue(Resource.Loading())
val data = repo?.getMainPage()
when (data) {
when (val data = repo?.getMainPage(1, null)) {
is Resource.Success -> {
try {
val home = data.value
if (home?.items?.isNullOrEmpty() == false) {
expandable.clear()
data.value.forEach { home ->
home?.items?.forEach { list ->
expandable[list.name] =
ExpandableHomepageList(list, 1, home.hasNext)
}
}
_page.postValue(Resource.Success(expandable))
val items = data.value.mapNotNull { it?.items }.flatten()
//val home = data.value
if (items.isNotEmpty()) {
val currentList =
home.items.shuffled().filter { !it.list.isNullOrEmpty() }.flatMap { it.list }
items.shuffled().filter { it.list.isNotEmpty() }
.flatMap { it.list }
.distinctBy { it.url }
.toList()
if (!currentList.isNullOrEmpty()) {
if (currentList.isNotEmpty()) {
val randomItems = currentList.shuffled()
_randomItems.postValue(randomItems)
}
}
} catch (e : Exception) {
} catch (e: Exception) {
_randomItems.postValue(emptyList())
logError(e)
}
}
is Resource.Failure -> {
_page.postValue(data!!)
}
else -> Unit
}
data?.let {
_page.postValue(it)
}
} else {
_page.postValue(Resource.Success(HomePageResponse(emptyList())))
}
}
@ -194,7 +265,7 @@ class HomeViewModel : ViewModel() {
loadAndCancel(noneApi)
else if (preferredApiName == randomApi.name || api == null) {
val validAPIs = context?.filterProviderByPreferredMedia()
if(validAPIs.isNullOrEmpty()) {
if (validAPIs.isNullOrEmpty()) {
loadAndCancel(noneApi)
} else {
val apiRandom = validAPIs.random()

View file

@ -27,7 +27,6 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageLis
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.search.SearchViewModel
import com.lagradost.cloudstream3.utils.UIHelper
@ -173,7 +172,7 @@ class QuickSearchFragment : Fragment() {
updateList(list.map { ongoing ->
val ongoingList = HomePageList(
ongoing.apiName,
if (ongoing.data is Resource.Success) ongoing.data.value.filterSearchResponse() else ArrayList()
if (ongoing.data is Resource.Success) ongoing.data.value else ArrayList()
)
ongoingList
})

View file

@ -27,6 +27,7 @@ class SearchAdapter(
private val resView: AutofitRecyclerView,
private val clickCallback: (SearchClickCallback) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var hasNext : Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layout = if(parent.context.IsBottomLayout()) R.layout.search_result_grid_expanded else R.layout.search_result_grid

View file

@ -426,7 +426,7 @@ class SearchFragment : Fragment() {
val newItems = list.map { ongoing ->
val ongoingList = HomePageList(
ongoing.apiName,
if (ongoing.data is Resource.Success) ongoing.data.value.filterSearchResponse() else ArrayList()
if (ongoing.data is Resource.Success) ongoing.data.value else ArrayList()
)
ongoingList
}

View file

@ -43,6 +43,7 @@ class SearchViewModel : ViewModel() {
_currentSearch.postValue(emptyList())
}
private var currentSearchIndex = 0
private var onGoingSearch: Job? = null
fun searchAndCancel(
query: String,
@ -50,6 +51,7 @@ class SearchViewModel : ViewModel() {
ignoreSettings: Boolean = false,
isQuickSearch: Boolean = false,
) {
currentSearchIndex++
onGoingSearch?.cancel()
onGoingSearch = search(query, providersActive, ignoreSettings, isQuickSearch)
}
@ -70,6 +72,7 @@ class SearchViewModel : ViewModel() {
isQuickSearch: Boolean = false,
) =
viewModelScope.launch {
val currentIndex = currentSearchIndex
if (query.length <= 1) {
clearSearch()
return@launch
@ -91,40 +94,44 @@ class SearchViewModel : ViewModel() {
_searchResponse.postValue(Resource.Loading())
val currentList = ArrayList<OnGoingSearch>()
_currentSearch.postValue(ArrayList())
withContext(Dispatchers.IO) { // This interrupts UI otherwise
val currentList = ArrayList<OnGoingSearch>()
repos.filter { a ->
(ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name))) && (!isQuickSearch || a.hasQuickSearch)
}.apmap { a -> // Parallel
val search = if (isQuickSearch) a.quickSearch(query) else a.search(query)
if(currentSearchIndex != currentIndex) return@apmap
currentList.add(OnGoingSearch(a.name, search))
_currentSearch.postValue(currentList)
}
}
_currentSearch.postValue(currentList)
val list = ArrayList<SearchResponse>()
val nestedList =
currentList.map { it.data }
.filterIsInstance<Resource.Success<List<SearchResponse>>>().map { it.value }
if(currentSearchIndex != currentIndex) return@withContext // this should prevent rewrite of existing data bug
// I do it this way to move the relevant search results to the top
var index = 0
while (true) {
var added = 0
for (sublist in nestedList) {
if (sublist.size > index) {
list.add(sublist[index])
added++
_currentSearch.postValue(currentList)
val list = ArrayList<SearchResponse>()
val nestedList =
currentList.map { it.data }
.filterIsInstance<Resource.Success<List<SearchResponse>>>().map { it.value }
// I do it this way to move the relevant search results to the top
var index = 0
while (true) {
var added = 0
for (sublist in nestedList) {
if (sublist.size > index) {
list.add(sublist[index])
added++
}
}
if (added == 0) break
index++
}
if (added == 0) break
index++
}
_searchResponse.postValue(Resource.Success(list))
_searchResponse.postValue(Resource.Success(list))
}
}
}

View file

@ -26,6 +26,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.text.toSpanned
import androidx.recyclerview.widget.RecyclerView
import androidx.tvprovider.media.tv.PreviewChannelHelper
import androidx.tvprovider.media.tv.TvContractCompat
import androidx.tvprovider.media.tv.WatchNextProgram
@ -52,6 +53,11 @@ import java.net.URL
import java.net.URLDecoder
object AppUtils {
fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) {
for (i in 0..maxViewTypeId)
recycledViewPool.setMaxRecycledViews(i, maxPoolSize)
}
//fun Context.deleteFavorite(data: SearchResponse) {
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
// normalSafeApiCall {

View file

@ -367,8 +367,8 @@
<string name="actor_background">Pomocniczy</string>
<string name="home_source">Źródło</string>
<string name="home_random">Losowy</string>
<string name="coming_soon">Już wkrótce…</string>
<string name="quality_cam">Cam</string>
<string name="quality_cam_rip">Cam</string>
@ -391,4 +391,11 @@
<string name="error">Błąd</string>
<string name="subtitles_remove_captions">Usuń informacje dla niesłyszących z napisów</string>
<string name="subtitles_remove_bloat">Usuń nadmiarowe informacje z napisów</string>
<string name="next">Dalej</string>
<string name="provider_languages_tip">Wyświetlaj filmy w wybranych językach</string>
<string name="previous">Cofnij</string>
<string name="skip_setup">Pomiń</string>
<string name="app_layout_subtext">Dostosuj wygląd aplikacji do urządzenia</string>
<string name="preferred_media_subtext">Preferowany rodzaj filmów</string>
<string name="setup_done">Gotowe</string>
</resources>

View file

@ -1,17 +1,17 @@
<!--https://newbedev.com/concatenate-multiple-strings-in-xml-->
<resources>
<!-- FORMAT MIGHT TRANSLATE, WILL CAUSE CRASH IF APPLIED WRONG -->
<string name="extra_info_format" translatable="false" formatted="true">%d %s | %sMB</string>
<string name="storage_size_format" translatable="false" formatted="true">%s • %sGB</string>
<string name="download_size_format" translatable="false" formatted="true">%sMB / %sMB</string>
<string name="mb_format" translatable="false" formatted="true">%dMB</string>
<string name="episode_name_format" translatable="false" formatted="true">%s %s</string>
<string name="ffw_text_format" translatable="false" formatted="true">+%d</string>
<string name="rew_text_format" translatable="false" formatted="true">-%d</string>
<string name="ffw_text_regular_format" translatable="false" formatted="true">%d</string>
<string name="rew_text_regular_format" translatable="false" formatted="true">%d</string>
<string name="rating_format" translatable="false" formatted="true">%.1f/10.0</string>
<string name="year_format" translatable="false" formatted="true">%d</string>
<string name="extra_info_format" formatted="true" translatable="false">%d %s | %sMB</string>
<string name="storage_size_format" formatted="true" translatable="false">%s • %sGB</string>
<string name="download_size_format" formatted="true" translatable="false">%sMB / %sMB</string>
<string name="mb_format" formatted="true" translatable="false">%dMB</string>
<string name="episode_name_format" formatted="true" translatable="false">%s %s</string>
<string name="ffw_text_format" formatted="true" translatable="false">+%d</string>
<string name="rew_text_format" formatted="true" translatable="false">-%d</string>
<string name="ffw_text_regular_format" formatted="true" translatable="false">%d</string>
<string name="rew_text_regular_format" formatted="true" translatable="false">%d</string>
<string name="rating_format" formatted="true" translatable="false">%.1f/10.0</string>
<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>
@ -39,6 +39,7 @@
<string name="duration_format" formatted="true">%d dakika</string>
<string name="app_name">CloudStream</string>
<string name="play_with_app_name">CloudStream ile oynat</string>
<string name="title_home">Ana sayfa</string>
<string name="title_search">Arama</string>
<string name="title_downloads">İndirilenler</string>
@ -66,6 +67,7 @@
<string name="type_re_watching">Yeniden izleniyor</string>
<string name="play_movie_button">Filmi oynat</string>
<string name="play_livestream_button">Canlı yayını oynat</string>
<string name="play_torrent_button">Torrent oynat</string>
<string name="pick_source">Kaynaklar</string>
<string name="pick_subtitle">Alt yazılar</string>
@ -268,6 +270,7 @@
<string name="documentaries">Belgeseller</string>
<string name="ova">OVA</string>
<string name="asian_drama">Asya dramaları</string>
<string name="livestreams">Canlı yayınlar</string>
<!--singular-->
<string name="movies_singular">Film</string>
@ -278,6 +281,7 @@
<string name="torrent_singular">Torrent</string>
<string name="documentaries_singular">Belgesel</string>
<string name="asian_drama_singular">Asya draması</string>
<string name="live_singular">Canlı yayın</string>
<string name="source_error">Kaynak hatası</string>
<string name="remote_error">Sunucu hatası</string>
@ -495,5 +499,15 @@
<string name="trailer">Fragman</string>
<string name="network_adress_example">Yayına bağlan</string>
<string name="referer">Yönlendiren</string>
<string name="next">İleri</string>
<string name="provider_languages_tip">Videoları bu dillerde izle</string>
<string name="previous">Geri</string>
<string name="skip_setup">Kurulumu atla</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Merhaba boş parça</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="setup_done">Bitti</string>
</resources>