diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index cd1d259b4e..21e1f14b04 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -98,7 +98,7 @@ jobs: if [ ${#commit_messages} -gt $max_length ]; then commit_messages="${commit_messages:0:$max_length}... (truncated)" fi - contentbody=$( jq -nc --arg msg "Alpha-Build: <@719439449423085569> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' ) + contentbody=$( jq -nc --arg msg "Alpha-Build: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' ) curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }} #Telegram diff --git a/app/build.gradle b/app/build.gradle index 1762fa5413..cdf6652a26 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { minSdk 23 targetSdk 34 versionCode((System.currentTimeMillis() / 60000).toInteger()) - versionName "2.1.0" - versionCode 210000000 + versionName "2.2.0" + versionCode 220000000 signingConfig signingConfigs.debug } @@ -138,10 +138,11 @@ dependencies { implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07' implementation 'com.squareup.logcat:logcat:0.1' implementation 'com.github.inorichi.injekt:injekt-core:65b0440' - implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11' - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11' + implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12' + implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps' implementation 'com.squareup.okio:okio:3.7.0' + implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12' implementation 'ch.acra:acra-http:5.11.3' implementation 'org.jsoup:jsoup:1.15.4' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 145c6febb4..57e6b8c247 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -52,7 +52,7 @@ android:icon="${icon_placeholder}" android:label="@string/app_name" android:largeHeap="true" - android:requestLegacyExternalStorage="false" + android:requestLegacyExternalStorage="true" android:roundIcon="${icon_placeholder_round}" android:supportsRtl="true" android:theme="@style/Theme.Dantotsu" diff --git a/app/src/main/java/ani/dantotsu/Functions.kt b/app/src/main/java/ani/dantotsu/Functions.kt index 623b8aa74b..f8756e3980 100644 --- a/app/src/main/java/ani/dantotsu/Functions.kt +++ b/app/src/main/java/ani/dantotsu/Functions.kt @@ -651,7 +651,7 @@ fun savePrefs( context: Context, password: CharArray ): File? { - var file = File(path, "$title.ani") + var file = File(path, "$title.sani") var counter = 1 while (file.exists()) { file = File(path, "${title}_${counter}.sani") diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt index 117b2fa829..85979714cc 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt @@ -3,6 +3,7 @@ package ani.dantotsu.connections.anilist import android.content.ActivityNotFoundException import android.content.Context import android.net.Uri +import android.util.Log import androidx.browser.customtabs.CustomTabsIntent import ani.dantotsu.R import ani.dantotsu.client @@ -128,7 +129,6 @@ object Anilist { toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds") throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds") } - val data = mapOf( "query" to query, "variables" to variables @@ -147,6 +147,8 @@ object Anilist { data = data, cacheTime = cache ?: 10 ) + val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1 + Log.d("AnilistQuery", "Remaining requests: $remaining") if (json.code == 429) { val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1 val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0 diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt index 14bb86b729..92cfc52dc9 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -1,6 +1,5 @@ package ani.dantotsu.connections.anilist -import android.app.Activity import android.util.Base64 import ani.dantotsu.R import ani.dantotsu.checkGenreTime @@ -266,12 +265,33 @@ class AnilistQueries { suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList { val returnArray = arrayListOf() val map = mutableMapOf() - val statuses = if (!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING") - suspend fun repeat(status: String) { - val response = - executeQuery(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """) + val query = if (planned) { + """{ planned: ${continueMediaQuery(type, "PLANNING")} }""" + } else { + """{ + current: ${continueMediaQuery(type, "CURRENT")}, + repeating: ${continueMediaQuery(type, "REPEATING")} + }""" + } - response?.data?.mediaListCollection?.lists?.forEach { li -> + val response = executeQuery(query) + if (planned) { + response?.data?.planned?.lists?.forEach { li -> + li.entries?.reversed()?.forEach { + val m = Media(it) + m.cameFromContinue = true + map[m.id] = m + } + } + } else { + response?.data?.current?.lists?.forEach { li -> + li.entries?.reversed()?.forEach { + val m = Media(it) + m.cameFromContinue = true + map[m.id] = m + } + } + response?.data?.repeating?.lists?.forEach { li -> li.entries?.reversed()?.forEach { val m = Media(it) m.cameFromContinue = true @@ -279,8 +299,6 @@ class AnilistQueries { } } } - - statuses.forEach { repeat(it) } val set = PrefManager.getCustomVal>("continue_$type", setOf()).toMutableSet() if (set.isNotEmpty()) { set.reversed().forEach { @@ -293,13 +311,16 @@ class AnilistQueries { return returnArray } + private fun continueMediaQuery(type: String, status: String): String { + return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """ + } + suspend fun favMedia(anime: Boolean): ArrayList { var hasNextPage = true var page = 0 suspend fun getNextPage(page: Int): List { - val response = - executeQuery("""{User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}}""") + val response = executeQuery("""{${favMediaQuery(anime, page)}}""") val favourites = response?.data?.user?.favourites val apiMediaList = if (anime) favourites?.anime else favourites?.manga hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false @@ -318,9 +339,12 @@ class AnilistQueries { return responseArray } + private fun favMediaQuery(anime: Boolean, page: Int): String { + return """User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}""" + } + suspend fun recommendations(): ArrayList { - val response = - executeQuery(""" { Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } } """) + val response = executeQuery("""{${recommendationQuery()}}""") val map = mutableMapOf() response?.data?.page?.apply { recommendations?.onEach { @@ -336,7 +360,7 @@ class AnilistQueries { val types = arrayOf("ANIME", "MANGA") suspend fun repeat(type: String) { val res = - executeQuery(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING , sort: MEDIA_POPULARITY_DESC ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """) + executeQuery("""{${recommendationPlannedQuery(type)}}""") res?.data?.mediaListCollection?.lists?.forEach { li -> li.entries?.forEach { val m = Media(it) @@ -354,6 +378,185 @@ class AnilistQueries { return list } + private fun recommendationQuery(): String { + return """ Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """ + } + + private fun recommendationPlannedQuery(type: String): String { + return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING , sort: MEDIA_POPULARITY_DESC ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }""" + } + + suspend fun initHomePage(): Map> { + val toShow: List = + PrefManager.getVal(PrefName.HomeLayoutShow) // anime continue, anime fav, anime planned, manga continue, manga fav, manga planned, recommendations + var query = """{""" + if (toShow.getOrNull(0) == true) query += """currentAnime: ${ + continueMediaQuery( + "ANIME", + "CURRENT" + ) + }, repeatingAnime: ${continueMediaQuery("ANIME", "REPEATING")}""" + if (toShow.getOrNull(1) == true) query += """favoriteAnime: ${favMediaQuery(true, 1)}""" + if (toShow.getOrNull(2) == true) query += """plannedAnime: ${ + continueMediaQuery( + "ANIME", + "PLANNING" + ) + }""" + if (toShow.getOrNull(3) == true) query += """currentManga: ${ + continueMediaQuery( + "MANGA", + "CURRENT" + ) + }, repeatingManga: ${continueMediaQuery("MANGA", "REPEATING")}""" + if (toShow.getOrNull(4) == true) query += """favoriteManga: ${favMediaQuery(false, 1)}""" + if (toShow.getOrNull(5) == true) query += """plannedManga: ${ + continueMediaQuery( + "MANGA", + "PLANNING" + ) + }""" + if (toShow.getOrNull(6) == true) query += """recommendationQuery: ${recommendationQuery()}, recommendationPlannedQueryAnime: ${ + recommendationPlannedQuery( + "ANIME" + ) + }, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}""" + query += """}""".trimEnd(',') + + val response = executeQuery(query) + val returnMap = mutableMapOf>() + fun current(type: String) { + val subMap = mutableMapOf() + val returnArray = arrayListOf() + val current = + if (type == "Anime") response?.data?.currentAnime else response?.data?.currentManga + val repeating = + if (type == "Anime") response?.data?.repeatingAnime else response?.data?.repeatingManga + current?.lists?.forEach { li -> + li.entries?.reversed()?.forEach { + val m = Media(it) + m.cameFromContinue = true + subMap[m.id] = m + } + } + repeating?.lists?.forEach { li -> + li.entries?.reversed()?.forEach { + val m = Media(it) + m.cameFromContinue = true + subMap[m.id] = m + } + } + val set = PrefManager.getCustomVal>("continue_${type.uppercase()}", setOf()) + .toMutableSet() + if (set.isNotEmpty()) { + set.reversed().forEach { + if (subMap.containsKey(it)) returnArray.add(subMap[it]!!) + } + for (i in subMap) { + if (i.value !in returnArray) returnArray.add(i.value) + } + } else returnArray.addAll(subMap.values) + returnMap["current$type"] = returnArray + + } + + fun planned(type: String) { + val subMap = mutableMapOf() + val returnArray = arrayListOf() + val current = + if (type == "Anime") response?.data?.plannedAnime else response?.data?.plannedManga + current?.lists?.forEach { li -> + li.entries?.reversed()?.forEach { + val m = Media(it) + m.cameFromContinue = true + subMap[m.id] = m + } + } + val set = PrefManager.getCustomVal>("continue_$type", setOf()).toMutableSet() + if (set.isNotEmpty()) { + set.reversed().forEach { + if (subMap.containsKey(it)) returnArray.add(subMap[it]!!) + } + for (i in subMap) { + if (i.value !in returnArray) returnArray.add(i.value) + } + } else returnArray.addAll(subMap.values) + returnMap["planned$type"] = returnArray + } + + fun favorite(type: String) { + val favourites = + if (type == "Anime") response?.data?.favoriteAnime?.favourites else response?.data?.favoriteManga?.favourites + val apiMediaList = if (type == "Anime") favourites?.anime else favourites?.manga + val returnArray = arrayListOf() + apiMediaList?.edges?.forEach { + it.node?.let { i -> + returnArray.add(Media(i).apply { isFav = true }) + } + } + returnMap["favorite$type"] = returnArray + } + + if (toShow.getOrNull(0) == true) { + current("Anime") + } + if (toShow.getOrNull(1) == true) { + favorite("Anime") + } + if (toShow.getOrNull(2) == true) { + planned("Anime") + } + if (toShow.getOrNull(3) == true) { + current("Manga") + } + if (toShow.getOrNull(4) == true) { + favorite("Manga") + } + if (toShow.getOrNull(5) == true) { + planned("Manga") + } + if (toShow.getOrNull(6) == true) { + val subMap = mutableMapOf() + response?.data?.recommendationQuery?.apply { + recommendations?.onEach { + val json = it.mediaRecommendation + if (json != null) { + val m = Media(json) + m.relation = json.type?.toString() + subMap[m.id] = m + } + } + } + response?.data?.recommendationPlannedQueryAnime?.apply { + lists?.forEach { li -> + li.entries?.forEach { + val m = Media(it) + if (m.status == "RELEASING" || m.status == "FINISHED") { + m.relation = it.media?.type?.toString() + subMap[m.id] = m + } + } + } + } + response?.data?.recommendationPlannedQueryManga?.apply { + lists?.forEach { li -> + li.entries?.forEach { + val m = Media(it) + if (m.status == "RELEASING" || m.status == "FINISHED") { + m.relation = it.media?.type?.toString() + subMap[m.id] = m + } + } + } + } + val list = ArrayList(subMap.values.toList()) + list.sortByDescending { it.meanScore } + returnMap["recommendations"] = list + } + return returnMap + } + + private suspend fun bannerImage(type: String): String? { //var image = loadData("banner_$type") val image: BannerImage? = BannerImage( diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt index 5e3bacb9e7..95d84ec9f9 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt @@ -97,6 +97,17 @@ class AnilistHomeViewModel : ViewModel() { fun getRecommendation(): LiveData> = recommendation suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations()) + suspend fun initHomePage() { + val res = Anilist.query.initHomePage() + res["currentAnime"]?.let { animeContinue.postValue(it) } + res["favoriteAnime"]?.let { animeFav.postValue(it) } + res["plannedAnime"]?.let { animePlanned.postValue(it) } + res["currentManga"]?.let { mangaContinue.postValue(it) } + res["favoriteManga"]?.let { mangaFav.postValue(it) } + res["plannedManga"]?.let { mangaPlanned.postValue(it) } + res["recommendations"]?.let { recommendation.postValue(it) } + } + suspend fun loadMain(context: FragmentActivity) { Anilist.getSavedToken() MAL.getSavedToken(context) diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt index 5b3a16eaba..8e53de02fb 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt @@ -105,6 +105,40 @@ class Query { ) } + @Serializable + data class CombinedMediaListResponse( + @SerialName("data") + val data: Data? + ) { + @Serializable + data class Data( + @SerialName("current") val current: ani.dantotsu.connections.anilist.api.MediaListCollection?, + @SerialName("planned") val planned: ani.dantotsu.connections.anilist.api.MediaListCollection?, + @SerialName("repeating") val repeating: ani.dantotsu.connections.anilist.api.MediaListCollection?, + ) + } + + @Serializable + data class HomePageMedia( + @SerialName("data") + val data: Data? + ) { + @Serializable + data class Data( + @SerialName("currentAnime") val currentAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?, + @SerialName("repeatingAnime") val repeatingAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?, + @SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?, + @SerialName("plannedAnime") val plannedAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?, + @SerialName("currentManga") val currentManga: ani.dantotsu.connections.anilist.api.MediaListCollection?, + @SerialName("repeatingManga") val repeatingManga: ani.dantotsu.connections.anilist.api.MediaListCollection?, + @SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?, + @SerialName("plannedManga") val plannedManga: ani.dantotsu.connections.anilist.api.MediaListCollection?, + @SerialName("recommendationQuery") val recommendationQuery: ani.dantotsu.connections.anilist.api.Page?, + @SerialName("recommendationPlannedQueryAnime") val recommendationPlannedQueryAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?, + @SerialName("recommendationPlannedQueryManga") val recommendationPlannedQueryManga: ani.dantotsu.connections.anilist.api.MediaListCollection?, + ) + } + @Serializable data class GenreCollection( @SerialName("data") diff --git a/app/src/main/java/ani/dantotsu/home/HomeFragment.kt b/app/src/main/java/ani/dantotsu/home/HomeFragment.kt index 2792a83217..dc03d34b3c 100644 --- a/app/src/main/java/ani/dantotsu/home/HomeFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/HomeFragment.kt @@ -306,13 +306,13 @@ class HomeFragment : Fragment() { } val array = arrayOf( - Runnable { runBlocking { model.setAnimeContinue() } }, - Runnable { runBlocking { model.setAnimeFav() } }, - Runnable { runBlocking { model.setAnimePlanned() } }, - Runnable { runBlocking { model.setMangaContinue() } }, - Runnable { runBlocking { model.setMangaFav() } }, - Runnable { runBlocking { model.setMangaPlanned() } }, - Runnable { runBlocking { model.setRecommendation() } } + "AnimeContinue", + "AnimeFav", + "AnimePlanned", + "MangaContinue", + "MangaFav", + "MangaPlanned", + "Recommendation" ) val containers = arrayOf( @@ -339,9 +339,11 @@ class HomeFragment : Fragment() { var empty = true val homeLayoutShow: List = PrefManager.getVal(PrefName.HomeLayoutShow) + runBlocking { + model.initHomePage() + } (array.indices).forEach { i -> if (homeLayoutShow.elementAt(i)) { - array[i].run() empty = false } else withContext(Dispatchers.Main) { containers[i].visibility = View.GONE diff --git a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt index ced90bc4ae..9327fc770b 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt @@ -22,6 +22,7 @@ import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemSearchHeaderBinding +import ani.dantotsu.openLinkInBrowser import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import com.google.android.material.checkbox.MaterialCheckBox.* @@ -105,6 +106,9 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri onList = listOnly isAdult = adult } + if (binding.searchBarText.text.toString().equals("hentai", true)) { + openLinkInBrowser("https://www.youtube.com/watch?v=GgJrEOo0QoA") + } activity.search() } diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt index 0ad6ecafe3..1d7a8497d5 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt @@ -257,8 +257,15 @@ class AnimeWatchAdapter( val url = sourceHttp?.baseUrl url?.let { refresh = true + val headersMap = try { + sourceHttp.headers.toMultimap() + .mapValues { it.value.getOrNull(0) ?: "" } + } catch (e: Exception) { + emptyMap() + } val intent = Intent(fragment.requireContext(), CookieCatcher::class.java) .putExtra("url", url) + .putExtra("headers", headersMap as HashMap) startActivity(fragment.requireContext(), intent, null) } } diff --git a/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt b/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt index 78f4fbef7d..7a3d8aeca6 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt @@ -372,18 +372,18 @@ class EpisodeAdapter( if (activeDownloads.contains(episodeNumber)) { // Show spinner binding.itemDownload.setImageResource(R.drawable.ic_sync) - startOrContinueRotation(episodeNumber) + startOrContinueRotation(episodeNumber) { + binding.itemDownload.rotation = 0f + } binding.itemEpisodeDesc.visibility = View.GONE } else if (downloadedEpisodes.contains(episodeNumber)) { binding.itemEpisodeDesc.visibility = View.GONE binding.itemDownloadStatus.visibility = View.VISIBLE // Show checkmark binding.itemDownload.setImageResource(R.drawable.ic_circle_check) - //binding.itemDownload.setColorFilter(typedValue2.data) //TODO: colors go to wrong places binding.itemDownload.postDelayed({ binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24) binding.itemDownload.rotation = 0f - //binding.itemDownload.setColorFilter(typedValue2.data) }, 1000) } else { binding.itemDownloadStatus.visibility = View.GONE @@ -396,7 +396,7 @@ class EpisodeAdapter( } - private fun startOrContinueRotation(episodeNumber: String) { + private fun startOrContinueRotation(episodeNumber: String, resetRotation: () -> Unit) { if (!isRotationCoroutineRunningFor(episodeNumber)) { val scope = fragment.lifecycle.coroutineScope scope.launch { @@ -411,6 +411,7 @@ class EpisodeAdapter( } // Remove chapter number from active coroutines set activeCoroutines.remove(episodeNumber) + resetRotation() } } } diff --git a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt index cbe39704ca..b8e78c611e 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -97,7 +97,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import okhttp3.internal.immutableListOf import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.* @@ -1080,17 +1079,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL //Cast if (PrefManager.getVal(PrefName.Cast)) { - playerView.findViewById(R.id.exo_cast).apply { + playerView.findViewById(R.id.exo_cast).apply { visibility = View.VISIBLE - try { - CastButtonFactory.setUpMediaRouteButton(context, this) - dialogFactory = CustomCastThemeFactory() - } catch (e: Exception) { - isCastApiAvailable = false - } - setOnLongClickListener { - cast() - true + if(PrefManager.getVal(PrefName.UseInternalCast)) { + try { + CastButtonFactory.setUpMediaRouteButton(context, this) + dialogFactory = CustomCastThemeFactory() + } catch (e: Exception) { + isCastApiAvailable = false + } + } else { + setCastCallback { cast() } } } } @@ -1375,16 +1374,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL logger("mimeType: $mimeType") if (sub != null) { - val listofnotnullsubs = immutableListOf(sub).filterNotNull() + val listofnotnullsubs = listOfNotNull(sub) builder.setSubtitleConfigurations(listofnotnullsubs) } builder.build() } else { val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon() if (sub != null) { - val listofnotnullsubs = immutableListOf(sub).filterNotNull() + val listofnotnullsubs = listOfNotNull(sub) val addLanguage = listofnotnullsubs[0].buildUpon().setLanguage("en").build() - addedSubsDownloadedMediaItem.setSubtitleConfigurations(immutableListOf(addLanguage)) + addedSubsDownloadedMediaItem.setSubtitleConfigurations(listOf(addLanguage)) episode.selectedSubtitle = 0 } addedSubsDownloadedMediaItem.build() @@ -2005,3 +2004,31 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL } } } + +class CustomCastButton : MediaRouteButton { + + private var castCallback: (() -> Unit)? = null + + fun setCastCallback(castCallback: () -> Unit) { + this.castCallback = castCallback + } + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + constructor(context: Context, attrs: AttributeSet, castCallback: () -> Unit) : super(context, attrs) { + this.castCallback = castCallback + } + + + override fun performClick(): Boolean { + return if (PrefManager.getVal(PrefName.UseInternalCast)) { + super.performClick() + } else { + castCallback?.let { it() } + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt index a00253eae0..e016dc29b6 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt @@ -1,6 +1,7 @@ package ani.dantotsu.media.anime import android.annotation.SuppressLint +import android.app.Activity import android.app.AlertDialog import android.content.DialogInterface import android.content.Intent @@ -316,6 +317,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { val subtitles = extractor.subtitles val subtitleNames = subtitles.map { it.language } var subtitleToDownload: Subtitle? = null + val activity = currActivity()?:requireActivity() if (subtitles.isNotEmpty()) { val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) .setTitle("Download Subtitle") @@ -329,7 +331,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { dialog?.dismiss() if (selectedVideo != null) { Helper.startAnimeDownloadService( - currActivity()!!, + activity, media!!.mainName(), episode.number, selectedVideo, @@ -337,7 +339,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { media, episode.thumb?.url ?: media!!.banner ?: media!!.cover ) - broadcastDownloadStarted(episode.number) + broadcastDownloadStarted(episode.number, activity) } else { snackString("No Video Selected") } @@ -354,7 +356,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { media, episode.thumb?.url ?: media!!.banner ?: media!!.cover ) - broadcastDownloadStarted(episode.number) + broadcastDownloadStarted(episode.number, activity) } else { snackString("No Video Selected") } @@ -378,7 +380,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { media, episode.thumb?.url ?: media!!.banner ?: media!!.cover ) - broadcastDownloadStarted(episode.number) + broadcastDownloadStarted(episode.number, activity) } else { snackString("No Video Selected") } @@ -399,11 +401,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { binding.urlQuality.text = extractor.server.name } - private fun broadcastDownloadStarted(episodeNumber: String) { + private fun broadcastDownloadStarted(episodeNumber: String, activity: Activity) { val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_STARTED).apply { putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber) } - requireActivity().sendBroadcast(intent) + activity.sendBroadcast(intent) } override fun getItemCount(): Int = extractor.videos.size diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt index 5467676512..6cad3b9053 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt @@ -154,15 +154,15 @@ class MangaChapterAdapter( if (activeDownloads.contains(chapterNumber)) { // Show spinner binding.itemDownload.setImageResource(R.drawable.ic_sync) - startOrContinueRotation(chapterNumber) + startOrContinueRotation(chapterNumber) { + binding.itemDownload.rotation = 0f + } } else if (downloadedChapters.contains(chapterNumber)) { // Show checkmark binding.itemDownload.setImageResource(R.drawable.ic_circle_check) - //binding.itemDownload.setColorFilter(typedValue2.data) //TODO: colors go to wrong places binding.itemDownload.postDelayed({ binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24) binding.itemDownload.rotation = 0f - //binding.itemDownload.setColorFilter(typedValue2.data) }, 1000) } else { // Show download icon @@ -172,7 +172,7 @@ class MangaChapterAdapter( } - private fun startOrContinueRotation(chapterNumber: String) { + private fun startOrContinueRotation(chapterNumber: String, resetRotation: () -> Unit) { if (!isRotationCoroutineRunningFor(chapterNumber)) { val scope = fragment.lifecycle.coroutineScope scope.launch { @@ -187,6 +187,7 @@ class MangaChapterAdapter( } // Remove chapter number from active coroutines set activeCoroutines.remove(chapterNumber) + resetRotation() } } } diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt index 8e59713e89..ef8725e148 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt @@ -122,7 +122,7 @@ class MangaReaderActivity : AppCompatActivity() { } private fun hideBars() { - if (PrefManager.getVal(PrefName.ShowSystemBars)) hideSystemBars() + if (!PrefManager.getVal(PrefName.ShowSystemBars)) hideSystemBars() } override fun onDestroy() { diff --git a/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt b/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt index d1f3635e1e..22f11f3f67 100644 --- a/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt +++ b/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt @@ -17,12 +17,14 @@ import uy.kohesive.injekt.api.get class CookieCatcher : AppCompatActivity() { @SuppressLint("SetJavaScriptEnabled") + @Suppress("UNCHECKED_CAST") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ThemeManager(this).applyTheme() //get url from intent val url = intent.getStringExtra("url") ?: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + val headers: Map = intent.getSerializableExtra("headers") as? Map ?: emptyMap() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val process = Application.getProcessName() @@ -54,7 +56,7 @@ class CookieCatcher : AppCompatActivity() { } } - webView.loadUrl(url) + webView.loadUrl(url, headers) } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt index 9ae30a274c..b7a40da98d 100644 --- a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt @@ -234,6 +234,11 @@ class PlayerSettingsActivity : AppCompatActivity() { PrefManager.setVal(PrefName.Cast, isChecked) } + binding.playerSettingsInternalCast.isChecked = PrefManager.getVal(PrefName.UseInternalCast) + binding.playerSettingsInternalCast.setOnCheckedChangeListener { _, isChecked -> + PrefManager.setVal(PrefName.UseInternalCast, isChecked) + } + binding.playerSettingsRotate.isChecked = PrefManager.getVal(PrefName.RotationPlayer) binding.playerSettingsRotate.setOnCheckedChangeListener { _, isChecked -> PrefManager.setVal(PrefName.RotationPlayer, isChecked) diff --git a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt index 09027a3a14..3810cf3ffa 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt @@ -24,7 +24,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files Pref( Location.General, String::class, - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0" + "Mozilla/5.0 (Linux; Android 13; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36" ) ), AnimeSourcesOrder(Pref(Location.General, List::class, listOf())), @@ -94,6 +94,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files SeekTime(Pref(Location.Player, Int::class, 10)), SkipTime(Pref(Location.Player, Int::class, 85)), Cast(Pref(Location.Player, Boolean::class, true)), + UseInternalCast(Pref(Location.Player, Boolean::class, false)), Pip(Pref(Location.Player, Boolean::class, true)), RotationPlayer(Pref(Location.Player, Boolean::class, true)), ContinuedAnime(Pref(Location.Player, List::class, listOf())), diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/api/AnimeExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/api/AnimeExtensionGithubApi.kt index 93820fcd89..a927373da2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/api/AnimeExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/api/AnimeExtensionGithubApi.kt @@ -131,7 +131,7 @@ internal class AnimeExtensionGithubApi { hasChangelog = it.hasChangelog == 1, sources = it.sources?.toAnimeExtensionSources().orEmpty(), apkName = it.apk, - iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}", + iconUrl = "${getUrlPrefix()}icon/${it.pkg}.png", ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index 4d14f6493f..a4fe08b4ad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor +import okhttp3.brotli.BrotliInterceptor import java.io.File import java.util.concurrent.TimeUnit @@ -19,26 +20,23 @@ class NetworkHelper( context: Context ) { - private val cacheDir = File(context.cacheDir, "network_cache") - private val cacheSize = 5L * 1024 * 1024 // 5 MiB - val cookieJar = AndroidCookieJar() - private val userAgentInterceptor by lazy { - UserAgentInterceptor(::defaultUserAgentProvider) - } - private val cloudflareInterceptor by lazy { - CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider) - } - - private fun baseClientBuilder(callTimeout: Int = 2): OkHttpClient.Builder { + val client: OkHttpClient = run { val builder = OkHttpClient.Builder() .cookieJar(cookieJar) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) - .callTimeout(callTimeout.toLong(), TimeUnit.MINUTES) + .callTimeout(2, TimeUnit.MINUTES) + .cache( + Cache( + directory = File(context.cacheDir, "network_cache"), + maxSize = 5L * 1024 * 1024, // 5 MiB + ), + ) + .addInterceptor(BrotliInterceptor) .addInterceptor(UncaughtExceptionInterceptor()) - .addInterceptor(userAgentInterceptor) + .addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider)) if (PrefManager.getVal(PrefName.VerboseLogging)) { val httpLoggingInterceptor = HttpLoggingInterceptor().apply { @@ -47,6 +45,10 @@ class NetworkHelper( builder.addNetworkInterceptor(httpLoggingInterceptor) } + builder.addInterceptor( + CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider), + ) + when (PrefManager.getVal(PrefName.DohProvider)) { PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() PREF_DOH_GOOGLE -> builder.dohGoogle() @@ -63,19 +65,17 @@ class NetworkHelper( PREF_DOH_LIBREDNS -> builder.dohLibreDNS() } - return builder + builder.build() } + val downloadClient = client.newBuilder().callTimeout(20, TimeUnit.MINUTES).build() - val client by lazy { baseClientBuilder().cache(Cache(cacheDir, cacheSize)).build() } - val downloadClient by lazy { baseClientBuilder(20).build() } - + /** + * @deprecated Since extension-lib 1.5 + */ + @Deprecated("The regular client handles Cloudflare by default") @Suppress("UNUSED") - val cloudflareClient by lazy { - client.newBuilder() - .addInterceptor(cloudflareInterceptor) - .build() - } + val cloudflareClient: OkHttpClient = client val requestClient = Requests( client, diff --git a/app/src/main/res/layout/activity_player_settings.xml b/app/src/main/res/layout/activity_player_settings.xml index 8b76081ccc..6f7102a537 100644 --- a/app/src/main/res/layout/activity_player_settings.xml +++ b/app/src/main/res/layout/activity_player_settings.xml @@ -1072,6 +1072,28 @@ + + + + - diff --git a/app/src/main/res/layout/item_episode_list.xml b/app/src/main/res/layout/item_episode_list.xml index 041da9b861..c198234c56 100644 --- a/app/src/main/res/layout/item_episode_list.xml +++ b/app/src/main/res/layout/item_episode_list.xml @@ -150,7 +150,7 @@ android:layout_width="24dp" android:layout_height="48dp" android:layout_marginEnd="5dp" - android:background="?android:attr/selectableItemBackground" + android:background="@android:color/transparent" app:srcCompat="@drawable/ic_download_24" app:tint="?attr/colorOnBackground" tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/item_media_compact.xml b/app/src/main/res/layout/item_media_compact.xml index 20b891c7ad..389b0ae975 100644 --- a/app/src/main/res/layout/item_media_compact.xml +++ b/app/src/main/res/layout/item_media_compact.xml @@ -16,7 +16,7 @@ android:layout_marginTop="-16dp" android:layout_marginBottom="-16dp" android:clipToPadding="false" - android:padding="24dp"> + android:padding="22dp"> Pinned Sources Import/Export Settings Import Settings + Try Internal Cast (Experimental) diff --git a/stable.md b/stable.md index 78b7637d80..d7fc24f9ca 100644 --- a/stable.md +++ b/stable.md @@ -1,29 +1,29 @@ -# 2.1.0 +# 2.2.0 -## Update????? +- **Important:** + - All settings will be reset due to the new settings system. Sorry for the inconvenience! -- **IMPORTANT PLEASE READ** - - Manga extensions installed will need to be uninstalled before they can be updated. You can do this in your phone settings, or long press the extension in Dantotsu settings. - - If you used a pretest from discord, PLEASE DELETE ALL ANIME DOWNLOADS. you can do this in settings. - - This also could break previous manga downloads. +- **New Features:** + - Import/Export settings + - New source organization system in extension settings + - Filter sources by language + - Defaulting to the external casting system (internal cast can be enabled in settings) + - sub/dub toggle for some sources (requires source settings page to be opened at least once) + - SoftSub downloads (when available) + - Various UI uplifts + - Many small features (see beta changelogs) + - New easter egg :3 - **Bugfixes:** - - Source fixes - - Manga extension fix + - Many source fixes + - Better information on Anilist rate limiting + - User will get a notification when rate limited + - Rate limiting less likely to occur (especially on app startup) + - Various bug/crash fixes - General theme tweaks - - Various bugs/crashes - - Many others - -- **New Features:** - - Anime Downloads - - Tap to scroll in manga (paged mode) - - Webview to set cookies - - New theme - - Internal casting (long press cast to use old method) - - Manga page ui rework - - Video source ui cleanup - - Offline mode declutter - - Better incognito awareness + - Popups will now follow OLED mode + - Fix for file permissions on older Android versions + - Many small bug fixes (see beta changelogs) - **Like what you see?** - Consider supporting me on [Github](https://github.com/sponsors/rebelonion) or [Buy Me a Coffee](https://www.buymeacoffee.com/rebelonion)!