chore: lint performance optimization
This includes shadowed variables, unnecessary parameters, layouts with string literals, items that cause performance bottlenecks, and the merge of extension types into only the necessary separate classes.
This commit is contained in:
parent
958aa634b1
commit
37ec165319
111 changed files with 1561 additions and 2091 deletions
|
@ -19,7 +19,7 @@
|
||||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="29" />
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
||||||
|
@ -53,11 +53,13 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:roundIcon="${icon_placeholder_round}"
|
android:roundIcon="${icon_placeholder_round}"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Dantotsu"
|
android:theme="@style/Theme.Dantotsu"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:ignore="AllowBackup">
|
tools:ignore="AllowBackup"
|
||||||
|
tools:targetApi="tiramisu">
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".widgets.CurrentlyAiringWidget"
|
android:name=".widgets.CurrentlyAiringWidget"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
|
@ -300,11 +302,7 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
|
||||||
android:exported="false"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
|
||||||
<activity
|
|
||||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
|
@ -355,11 +353,7 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
|
||||||
android:exported="false"
|
|
||||||
android:foregroundServiceType="dataSync" />
|
|
||||||
<service
|
|
||||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="dataSync" />
|
||||||
<service
|
<service
|
||||||
|
|
|
@ -86,7 +86,7 @@ class App : MultiDexApplication() {
|
||||||
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
|
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
|
||||||
Logger.log("App: Logging started")
|
Logger.log("App: Logging started")
|
||||||
|
|
||||||
initializeNetwork(baseContext)
|
initializeNetwork()
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
if (!LogcatLogger.isInstalled) {
|
if (!LogcatLogger.isInstalled) {
|
||||||
|
|
|
@ -159,16 +159,16 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
|
||||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
val backgroundDrawable = bottomNavBar.background as GradientDrawable
|
||||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
||||||
backgroundDrawable.setColor(semiTransparentColor)
|
backgroundDrawable.setColor(semiTransparentColor)
|
||||||
_bottomBar.background = backgroundDrawable
|
bottomNavBar.background = backgroundDrawable
|
||||||
}
|
}
|
||||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||||
|
|
||||||
val offset = try {
|
val offset = try {
|
||||||
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||||
|
@ -337,7 +337,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
startActivity(Intent(this, NoInternet::class.java))
|
startActivity(Intent(this, NoInternet::class.java))
|
||||||
} else {
|
} else {
|
||||||
val model: AnilistHomeViewModel by viewModels()
|
val model: AnilistHomeViewModel by viewModels()
|
||||||
model.genres.observe(this) { it ->
|
model.genres.observe(this) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
if (it) {
|
if (it) {
|
||||||
val navbar = binding.includedNavbar.navbar
|
val navbar = binding.includedNavbar.navbar
|
||||||
|
@ -362,7 +362,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
mainViewPager.setCurrentItem(newIndex, false)
|
mainViewPager.setCurrentItem(newIndex, false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (mainViewPager.getCurrentItem() != selectedOption) {
|
if (mainViewPager.currentItem != selectedOption) {
|
||||||
navbar.selectTabAt(selectedOption)
|
navbar.selectTabAt(selectedOption)
|
||||||
mainViewPager.post {
|
mainViewPager.post {
|
||||||
mainViewPager.setCurrentItem(
|
mainViewPager.setCurrentItem(
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package ani.dantotsu
|
package ani.dantotsu
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import ani.dantotsu.others.webview.CloudFlare
|
import ani.dantotsu.others.webview.CloudFlare
|
||||||
|
@ -35,7 +34,7 @@ lateinit var defaultHeaders: Map<String, String>
|
||||||
lateinit var okHttpClient: OkHttpClient
|
lateinit var okHttpClient: OkHttpClient
|
||||||
lateinit var client: Requests
|
lateinit var client: Requests
|
||||||
|
|
||||||
fun initializeNetwork(context: Context) {
|
fun initializeNetwork() {
|
||||||
|
|
||||||
val networkHelper = Injekt.get<NetworkHelper>()
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
|
||||||
|
|
|
@ -387,6 +387,7 @@ class AnilistQueries {
|
||||||
returnArray.addAll(map.values)
|
returnArray.addAll(map.values)
|
||||||
return returnArray
|
return returnArray
|
||||||
}
|
}
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val list = PrefManager.getNullableCustomVal(
|
val list = PrefManager.getNullableCustomVal(
|
||||||
"continueAnimeList",
|
"continueAnimeList",
|
||||||
listOf<Int>(),
|
listOf<Int>(),
|
||||||
|
@ -544,6 +545,7 @@ class AnilistQueries {
|
||||||
returnMap["current$type"] = returnArray
|
returnMap["current$type"] = returnArray
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val list = PrefManager.getNullableCustomVal(
|
val list = PrefManager.getNullableCustomVal(
|
||||||
"continueAnimeList",
|
"continueAnimeList",
|
||||||
listOf<Int>(),
|
listOf<Int>(),
|
||||||
|
@ -573,6 +575,7 @@ class AnilistQueries {
|
||||||
subMap[m.id] = m
|
subMap[m.id] = m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val list = PrefManager.getNullableCustomVal(
|
val list = PrefManager.getNullableCustomVal(
|
||||||
"continueAnimeList",
|
"continueAnimeList",
|
||||||
listOf<Int>(),
|
listOf<Int>(),
|
||||||
|
@ -734,7 +737,7 @@ class AnilistQueries {
|
||||||
}
|
}
|
||||||
|
|
||||||
sorted["All"] = all
|
sorted["All"] = all
|
||||||
val listSort: String = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
|
val listSort: String? = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
|
||||||
else PrefManager.getVal(PrefName.MangaListSortOrder)
|
else PrefManager.getVal(PrefName.MangaListSortOrder)
|
||||||
val sort = listSort ?: sortOrder ?: options?.rowOrder
|
val sort = listSort ?: sortOrder ?: options?.rowOrder
|
||||||
for (i in sorted.keys) {
|
for (i in sorted.keys) {
|
||||||
|
|
|
@ -112,8 +112,8 @@ class AnilistHomeViewModel : ViewModel() {
|
||||||
|
|
||||||
suspend fun loadMain(context: FragmentActivity) {
|
suspend fun loadMain(context: FragmentActivity) {
|
||||||
Anilist.getSavedToken()
|
Anilist.getSavedToken()
|
||||||
MAL.getSavedToken(context)
|
MAL.getSavedToken()
|
||||||
Discord.getSavedToken(context)
|
Discord.getSavedToken()
|
||||||
if (!BuildConfig.FLAVOR.contains("fdroid")) {
|
if (!BuildConfig.FLAVOR.contains("fdroid")) {
|
||||||
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
|
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
|
||||||
}
|
}
|
||||||
|
@ -159,7 +159,7 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||||
fun getPopular(): LiveData<SearchResults?> = animePopular
|
fun getPopular(): LiveData<SearchResults?> = animePopular
|
||||||
suspend fun loadPopular(
|
suspend fun loadPopular(
|
||||||
type: String,
|
type: String,
|
||||||
search_val: String? = null,
|
searchVal: String? = null,
|
||||||
genres: ArrayList<String>? = null,
|
genres: ArrayList<String>? = null,
|
||||||
sort: String = Anilist.sortBy[1],
|
sort: String = Anilist.sortBy[1],
|
||||||
onList: Boolean = true,
|
onList: Boolean = true,
|
||||||
|
@ -167,7 +167,7 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||||
animePopular.postValue(
|
animePopular.postValue(
|
||||||
Anilist.query.search(
|
Anilist.query.search(
|
||||||
type,
|
type,
|
||||||
search = search_val,
|
search = searchVal,
|
||||||
onList = if (onList) null else false,
|
onList = if (onList) null else false,
|
||||||
sort = sort,
|
sort = sort,
|
||||||
genres = genres
|
genres = genres
|
||||||
|
@ -231,7 +231,7 @@ class AnilistMangaViewModel : ViewModel() {
|
||||||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||||
suspend fun loadPopular(
|
suspend fun loadPopular(
|
||||||
type: String,
|
type: String,
|
||||||
search_val: String? = null,
|
searchVal: String? = null,
|
||||||
genres: ArrayList<String>? = null,
|
genres: ArrayList<String>? = null,
|
||||||
sort: String = Anilist.sortBy[1],
|
sort: String = Anilist.sortBy[1],
|
||||||
onList: Boolean = true,
|
onList: Boolean = true,
|
||||||
|
@ -239,7 +239,7 @@ class AnilistMangaViewModel : ViewModel() {
|
||||||
mangaPopular.postValue(
|
mangaPopular.postValue(
|
||||||
Anilist.query.search(
|
Anilist.query.search(
|
||||||
type,
|
type,
|
||||||
search = search_val,
|
search = searchVal,
|
||||||
onList = if (onList) null else false,
|
onList = if (onList) null else false,
|
||||||
sort = sort,
|
sort = sort,
|
||||||
genres = genres
|
genres = genres
|
||||||
|
|
|
@ -20,14 +20,14 @@ object Discord {
|
||||||
var avatar: String? = null
|
var avatar: String? = null
|
||||||
|
|
||||||
|
|
||||||
fun getSavedToken(context: Context): Boolean {
|
fun getSavedToken(): Boolean {
|
||||||
token = PrefManager.getVal(
|
token = PrefManager.getVal(
|
||||||
PrefName.DiscordToken, null as String?
|
PrefName.DiscordToken, null as String?
|
||||||
)
|
)
|
||||||
return token != null
|
return token != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveToken(context: Context, token: String) {
|
fun saveToken(token: String) {
|
||||||
PrefManager.setVal(PrefName.DiscordToken, token)
|
PrefManager.setVal(PrefName.DiscordToken, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,16 +5,12 @@ import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.MediaStore
|
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
@ -37,7 +33,6 @@ import okhttp3.Response
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStreamWriter
|
|
||||||
|
|
||||||
class DiscordService : Service() {
|
class DiscordService : Service() {
|
||||||
private var heartbeat: Int = 0
|
private var heartbeat: Int = 0
|
||||||
|
@ -162,8 +157,8 @@ class DiscordService : Service() {
|
||||||
|
|
||||||
inner class DiscordWebSocketListener : WebSocketListener() {
|
inner class DiscordWebSocketListener : WebSocketListener() {
|
||||||
|
|
||||||
var retryAttempts = 0
|
private var retryAttempts = 0
|
||||||
val maxRetryAttempts = 10
|
private val maxRetryAttempts = 10
|
||||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
super.onOpen(webSocket, response)
|
super.onOpen(webSocket, response)
|
||||||
this@DiscordService.webSocket = webSocket
|
this@DiscordService.webSocket = webSocket
|
||||||
|
@ -232,7 +227,7 @@ class DiscordService : Service() {
|
||||||
resume()
|
resume()
|
||||||
resume = false
|
resume = false
|
||||||
} else {
|
} else {
|
||||||
identify(webSocket, baseContext)
|
identify(webSocket)
|
||||||
log("WebSocket: Identified")
|
log("WebSocket: Identified")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,13 +240,13 @@ class DiscordService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun identify(webSocket: WebSocket, context: Context) {
|
private fun identify(webSocket: WebSocket) {
|
||||||
val properties = JsonObject()
|
val properties = JsonObject()
|
||||||
properties.addProperty("os", "linux")
|
properties.addProperty("os", "linux")
|
||||||
properties.addProperty("browser", "unknown")
|
properties.addProperty("browser", "unknown")
|
||||||
properties.addProperty("device", "unknown")
|
properties.addProperty("device", "unknown")
|
||||||
val d = JsonObject()
|
val d = JsonObject()
|
||||||
d.addProperty("token", getToken(context))
|
d.addProperty("token", getToken())
|
||||||
d.addProperty("intents", 0)
|
d.addProperty("intents", 0)
|
||||||
d.add("properties", properties)
|
d.add("properties", properties)
|
||||||
val payload = JsonObject()
|
val payload = JsonObject()
|
||||||
|
@ -311,7 +306,7 @@ class DiscordService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getToken(context: Context): String {
|
fun getToken(): String {
|
||||||
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
||||||
return if (token == null) {
|
return if (token == null) {
|
||||||
log("WebSocket: Token not found")
|
log("WebSocket: Token not found")
|
||||||
|
@ -375,10 +370,10 @@ class DiscordService : Service() {
|
||||||
log("WebSocket: Simple Test Presence Saved")
|
log("WebSocket: Simple Test Presence Saved")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPresence(String: String) {
|
fun setPresence(string: String) {
|
||||||
log("WebSocket: Sending Presence payload")
|
log("WebSocket: Sending Presence payload")
|
||||||
log(String)
|
log(string)
|
||||||
webSocket.send(String)
|
webSocket.send(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun log(string: String) {
|
fun log(string: String) {
|
||||||
|
@ -388,7 +383,7 @@ class DiscordService : Service() {
|
||||||
fun resume() {
|
fun resume() {
|
||||||
log("Sending Resume payload")
|
log("Sending Resume payload")
|
||||||
val d = JsonObject()
|
val d = JsonObject()
|
||||||
d.addProperty("token", getToken(baseContext))
|
d.addProperty("token", getToken())
|
||||||
d.addProperty("session_id", sessionId)
|
d.addProperty("session_id", sessionId)
|
||||||
d.addProperty("seq", sequence)
|
d.addProperty("seq", sequence)
|
||||||
val json = JsonObject()
|
val json = JsonObject()
|
||||||
|
@ -404,8 +399,7 @@ class DiscordService : Service() {
|
||||||
Thread.sleep(heartbeat.toLong())
|
Thread.sleep(heartbeat.toLong())
|
||||||
heartbeatSend(webSocket, sequence)
|
heartbeatSend(webSocket, sequence)
|
||||||
log("WebSocket: Heartbeat Sent")
|
log("WebSocket: Heartbeat Sent")
|
||||||
} catch (e: InterruptedException) {
|
} catch (ignored: InterruptedException) { }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ class Login : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
|
||||||
finish()
|
finish()
|
||||||
saveToken(this, token)
|
saveToken(token)
|
||||||
startMainActivity(this@Login)
|
startMainActivity(this@Login)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.client
|
import ani.dantotsu.client
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
|
@ -64,7 +63,7 @@ object MAL {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun getSavedToken(context: FragmentActivity): Boolean {
|
suspend fun getSavedToken(): Boolean {
|
||||||
return tryWithSuspend(false) {
|
return tryWithSuspend(false) {
|
||||||
var res: ResponseToken =
|
var res: ResponseToken =
|
||||||
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
||||||
|
@ -77,7 +76,7 @@ object MAL {
|
||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeSavedToken(context: Context) {
|
fun removeSavedToken() {
|
||||||
token = null
|
token = null
|
||||||
username = null
|
username = null
|
||||||
userid = null
|
userid = null
|
||||||
|
|
|
@ -3,6 +3,7 @@ package ani.dantotsu.download
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
@ -15,11 +16,11 @@ class DownloadsManager(private val context: Context) {
|
||||||
private val downloadsList = loadDownloads().toMutableList()
|
private val downloadsList = loadDownloads().toMutableList()
|
||||||
|
|
||||||
val mangaDownloadedTypes: List<DownloadedType>
|
val mangaDownloadedTypes: List<DownloadedType>
|
||||||
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
|
get() = downloadsList.filter { it.type == MediaType.MANGA }
|
||||||
val animeDownloadedTypes: List<DownloadedType>
|
val animeDownloadedTypes: List<DownloadedType>
|
||||||
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
|
get() = downloadsList.filter { it.type == MediaType.ANIME }
|
||||||
val novelDownloadedTypes: List<DownloadedType>
|
val novelDownloadedTypes: List<DownloadedType>
|
||||||
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
|
get() = downloadsList.filter { it.type == MediaType.NOVEL }
|
||||||
|
|
||||||
private fun saveDownloads() {
|
private fun saveDownloads() {
|
||||||
val jsonString = gson.toJson(downloadsList)
|
val jsonString = gson.toJson(downloadsList)
|
||||||
|
@ -47,14 +48,8 @@ class DownloadsManager(private val context: Context) {
|
||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMedia(title: String, type: DownloadedType.Type) {
|
fun removeMedia(title: String, type: MediaType) {
|
||||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
val subDirectory = type.asText()
|
||||||
"Manga"
|
|
||||||
} else if (type == DownloadedType.Type.ANIME) {
|
|
||||||
"Anime"
|
|
||||||
} else {
|
|
||||||
"Novel"
|
|
||||||
}
|
|
||||||
val directory = File(
|
val directory = File(
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"Dantotsu/$subDirectory/$title"
|
"Dantotsu/$subDirectory/$title"
|
||||||
|
@ -71,53 +66,45 @@ class DownloadsManager(private val context: Context) {
|
||||||
cleanDownloads()
|
cleanDownloads()
|
||||||
}
|
}
|
||||||
when (type) {
|
when (type) {
|
||||||
DownloadedType.Type.MANGA -> {
|
MediaType.MANGA -> {
|
||||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
|
downloadsList.removeAll { it.title == title && it.type == MediaType.MANGA }
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadedType.Type.ANIME -> {
|
MediaType.ANIME -> {
|
||||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
|
downloadsList.removeAll { it.title == title && it.type == MediaType.ANIME }
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadedType.Type.NOVEL -> {
|
MediaType.NOVEL -> {
|
||||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
|
downloadsList.removeAll { it.title == title && it.type == MediaType.NOVEL }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
saveDownloads()
|
saveDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanDownloads() {
|
private fun cleanDownloads() {
|
||||||
cleanDownload(DownloadedType.Type.MANGA)
|
cleanDownload(MediaType.MANGA)
|
||||||
cleanDownload(DownloadedType.Type.ANIME)
|
cleanDownload(MediaType.ANIME)
|
||||||
cleanDownload(DownloadedType.Type.NOVEL)
|
cleanDownload(MediaType.NOVEL)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanDownload(type: DownloadedType.Type) {
|
private fun cleanDownload(type: MediaType) {
|
||||||
// remove all folders that are not in the downloads list
|
// remove all folders that are not in the downloads list
|
||||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
val subDirectory = type.asText()
|
||||||
"Manga"
|
|
||||||
} else if (type == DownloadedType.Type.ANIME) {
|
|
||||||
"Anime"
|
|
||||||
} else {
|
|
||||||
"Novel"
|
|
||||||
}
|
|
||||||
val directory = File(
|
val directory = File(
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"Dantotsu/$subDirectory"
|
"Dantotsu/$subDirectory"
|
||||||
)
|
)
|
||||||
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
|
val downloadsSubLists = when (type) {
|
||||||
mangaDownloadedTypes
|
MediaType.MANGA -> mangaDownloadedTypes
|
||||||
} else if (type == DownloadedType.Type.ANIME) {
|
MediaType.ANIME -> animeDownloadedTypes
|
||||||
animeDownloadedTypes
|
else -> novelDownloadedTypes
|
||||||
} else {
|
|
||||||
novelDownloadedTypes
|
|
||||||
}
|
}
|
||||||
if (directory.exists()) {
|
if (directory.exists()) {
|
||||||
val files = directory.listFiles()
|
val files = directory.listFiles()
|
||||||
if (files != null) {
|
if (files != null) {
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||||
val deleted = file.deleteRecursively()
|
file.deleteRecursively()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,7 +140,7 @@ class DownloadsManager(private val context: Context) {
|
||||||
return downloadsList.contains(downloadedType)
|
return downloadsList.contains(downloadedType)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
|
fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean {
|
||||||
return if (type == null) {
|
return if (type == null) {
|
||||||
downloadsList.any { it.title == title && it.chapter == chapter }
|
downloadsList.any { it.title == title && it.chapter == chapter }
|
||||||
} else {
|
} else {
|
||||||
|
@ -162,21 +149,25 @@ class DownloadsManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeDirectory(downloadedType: DownloadedType) {
|
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
val directory = when (downloadedType.type) {
|
||||||
File(
|
MediaType.MANGA -> {
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
File(
|
||||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
)
|
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
)
|
||||||
File(
|
}
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
MediaType.ANIME -> {
|
||||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
File(
|
||||||
)
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
} else {
|
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
File(
|
)
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
}
|
||||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
else -> {
|
||||||
)
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the directory exists and delete it recursively
|
// Check if the directory exists and delete it recursively
|
||||||
|
@ -193,21 +184,25 @@ class DownloadsManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
||||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
val directory = when (downloadedType.type) {
|
||||||
File(
|
MediaType.MANGA -> {
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
File(
|
||||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
)
|
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
)
|
||||||
File(
|
}
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
MediaType.ANIME -> {
|
||||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
File(
|
||||||
)
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
} else {
|
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
File(
|
)
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
}
|
||||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
else -> {
|
||||||
)
|
File(
|
||||||
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val destination = File(
|
val destination = File(
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
@ -225,13 +220,17 @@ class DownloadsManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun purgeDownloads(type: DownloadedType.Type) {
|
fun purgeDownloads(type: MediaType) {
|
||||||
val directory = if (type == DownloadedType.Type.MANGA) {
|
val directory = when (type) {
|
||||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
MediaType.MANGA -> {
|
||||||
} else if (type == DownloadedType.Type.ANIME) {
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
}
|
||||||
} else {
|
MediaType.ANIME -> {
|
||||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (directory.exists()) {
|
if (directory.exists()) {
|
||||||
val deleted = directory.deleteRecursively()
|
val deleted = directory.deleteRecursively()
|
||||||
|
@ -255,56 +254,53 @@ class DownloadsManager(private val context: Context) {
|
||||||
|
|
||||||
fun getDirectory(
|
fun getDirectory(
|
||||||
context: Context,
|
context: Context,
|
||||||
type: DownloadedType.Type,
|
type: MediaType,
|
||||||
title: String,
|
title: String,
|
||||||
chapter: String? = null
|
chapter: String? = null
|
||||||
): File {
|
): File {
|
||||||
return if (type == DownloadedType.Type.MANGA) {
|
return when (type) {
|
||||||
if (chapter != null) {
|
MediaType.MANGA -> {
|
||||||
File(
|
if (chapter != null) {
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
File(
|
||||||
"$mangaLocation/$title/$chapter"
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
)
|
"$mangaLocation/$title/$chapter"
|
||||||
} else {
|
)
|
||||||
File(
|
} else {
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
File(
|
||||||
"$mangaLocation/$title"
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
)
|
"$mangaLocation/$title"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (type == DownloadedType.Type.ANIME) {
|
MediaType.ANIME -> {
|
||||||
if (chapter != null) {
|
if (chapter != null) {
|
||||||
File(
|
File(
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"$animeLocation/$title/$chapter"
|
"$animeLocation/$title/$chapter"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
File(
|
File(
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"$animeLocation/$title"
|
"$animeLocation/$title"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
else -> {
|
||||||
if (chapter != null) {
|
if (chapter != null) {
|
||||||
File(
|
File(
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"$novelLocation/$title/$chapter"
|
"$novelLocation/$title/$chapter"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
File(
|
File(
|
||||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"$novelLocation/$title"
|
"$novelLocation/$title"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
|
data class DownloadedType(val title: String, val chapter: String, val type: MediaType) : Serializable
|
||||||
enum class Type {
|
|
||||||
MANGA,
|
|
||||||
ANIME,
|
|
||||||
NOVEL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,14 +27,15 @@ import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||||
import ani.dantotsu.download.video.Helper
|
import ani.dantotsu.download.video.Helper
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.SubtitleDownloader
|
import ani.dantotsu.media.SubtitleDownloader
|
||||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||||
import ani.dantotsu.parsers.Subtitle
|
import ani.dantotsu.parsers.Subtitle
|
||||||
import ani.dantotsu.parsers.Video
|
import ani.dantotsu.parsers.Video
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
@ -242,7 +243,7 @@ class AnimeDownloaderService : Service() {
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.episode,
|
task.episode,
|
||||||
DownloadedType.Type.ANIME,
|
MediaType.ANIME,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -273,7 +274,7 @@ class AnimeDownloaderService : Service() {
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.episode,
|
task.episode,
|
||||||
DownloadedType.Type.ANIME,
|
MediaType.ANIME,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Injekt.get<CrashlyticsInterface>().logException(
|
Injekt.get<CrashlyticsInterface>().logException(
|
||||||
|
@ -302,7 +303,7 @@ class AnimeDownloaderService : Service() {
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.episode,
|
task.episode,
|
||||||
DownloadedType.Type.ANIME,
|
MediaType.ANIME,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||||
|
|
|
@ -34,15 +34,16 @@ import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
@ -188,8 +189,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||||
// Get the OfflineAnimeModel that was clicked
|
// Get the OfflineAnimeModel that was clicked
|
||||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||||
val type: DownloadedType.Type =
|
val type: MediaType = MediaType.ANIME
|
||||||
DownloadedType.Type.ANIME
|
|
||||||
|
|
||||||
// Alert dialog to confirm deletion
|
// Alert dialog to confirm deletion
|
||||||
val builder =
|
val builder =
|
||||||
|
@ -293,11 +293,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||||
val type = when (downloadedType.type) {
|
val type = downloadedType.type.asText()
|
||||||
DownloadedType.Type.MANGA -> "Manga"
|
|
||||||
DownloadedType.Type.ANIME -> "Anime"
|
|
||||||
else -> "Novel"
|
|
||||||
}
|
|
||||||
val directory = File(
|
val directory = File(
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"Dantotsu/$type/${downloadedType.title}"
|
"Dantotsu/$type/${downloadedType.title}"
|
||||||
|
@ -327,11 +323,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||||
val type = when (downloadedType.type) {
|
val type = downloadedType.type.asText()
|
||||||
DownloadedType.Type.MANGA -> "Manga"
|
|
||||||
DownloadedType.Type.ANIME -> "Anime"
|
|
||||||
else -> "Novel"
|
|
||||||
}
|
|
||||||
val directory = File(
|
val directory = File(
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"Dantotsu/$type/${downloadedType.title}"
|
"Dantotsu/$type/${downloadedType.title}"
|
||||||
|
|
|
@ -21,8 +21,8 @@ import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.manga.ImageData
|
import ani.dantotsu.media.manga.ImageData
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
||||||
|
@ -30,6 +30,7 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROG
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
||||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||||
|
@ -211,8 +212,7 @@ class MangaDownloaderService : Service() {
|
||||||
while (bitmap == null && retryCount < task.retries) {
|
while (bitmap == null && retryCount < task.retries) {
|
||||||
bitmap = image.fetchAndProcessImage(
|
bitmap = image.fetchAndProcessImage(
|
||||||
image.page,
|
image.page,
|
||||||
image.source,
|
image.source
|
||||||
this@MangaDownloaderService
|
|
||||||
)
|
)
|
||||||
retryCount++
|
retryCount++
|
||||||
}
|
}
|
||||||
|
@ -246,7 +246,7 @@ class MangaDownloaderService : Service() {
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.chapter,
|
task.chapter,
|
||||||
DownloadedType.Type.MANGA
|
MediaType.MANGA
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
broadcastDownloadFinished(task.chapter)
|
broadcastDownloadFinished(task.chapter)
|
||||||
|
|
|
@ -31,15 +31,16 @@ import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.settings.SettingsDialogFragment
|
import ani.dantotsu.settings.SettingsDialogFragment
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
@ -179,11 +180,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||||
// Get the OfflineMangaModel that was clicked
|
// Get the OfflineMangaModel that was clicked
|
||||||
val item = adapter.getItem(position) as OfflineMangaModel
|
val item = adapter.getItem(position) as OfflineMangaModel
|
||||||
val type: DownloadedType.Type =
|
val type: MediaType =
|
||||||
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
||||||
DownloadedType.Type.MANGA
|
MediaType.MANGA
|
||||||
} else {
|
} else {
|
||||||
DownloadedType.Type.NOVEL
|
MediaType.NOVEL
|
||||||
}
|
}
|
||||||
// Alert dialog to confirm deletion
|
// Alert dialog to confirm deletion
|
||||||
val builder =
|
val builder =
|
||||||
|
@ -289,11 +290,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||||
val type = when (downloadedType.type) {
|
val type = downloadedType.type.asText()
|
||||||
DownloadedType.Type.MANGA -> "Manga"
|
|
||||||
DownloadedType.Type.ANIME -> "Anime"
|
|
||||||
else -> "Novel"
|
|
||||||
}
|
|
||||||
val directory = File(
|
val directory = File(
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"Dantotsu/$type/${downloadedType.title}"
|
"Dantotsu/$type/${downloadedType.title}"
|
||||||
|
@ -317,11 +314,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||||
val type = when (downloadedType.type) {
|
val type = downloadedType.type.asText()
|
||||||
DownloadedType.Type.MANGA -> "Manga"
|
|
||||||
DownloadedType.Type.ANIME -> "Anime"
|
|
||||||
else -> "Novel"
|
|
||||||
}
|
|
||||||
val directory = File(
|
val directory = File(
|
||||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||||
"Dantotsu/$type/${downloadedType.title}"
|
"Dantotsu/$type/${downloadedType.title}"
|
||||||
|
|
|
@ -20,10 +20,11 @@ import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.novel.NovelReadFragment
|
import ani.dantotsu.media.novel.NovelReadFragment
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.InstanceCreator
|
import com.google.gson.InstanceCreator
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
@ -335,7 +336,7 @@ class NovelDownloaderService : Service() {
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
task.title,
|
task.title,
|
||||||
task.chapter,
|
task.chapter,
|
||||||
DownloadedType.Type.NOVEL
|
MediaType.NOVEL
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
broadcastDownloadFinished(task.originalLink)
|
broadcastDownloadFinished(task.originalLink)
|
||||||
|
|
|
@ -9,7 +9,6 @@ import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -37,6 +36,7 @@ import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||||
import ani.dantotsu.logError
|
import ani.dantotsu.logError
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.okHttpClient
|
import ani.dantotsu.okHttpClient
|
||||||
import ani.dantotsu.parsers.Subtitle
|
import ani.dantotsu.parsers.Subtitle
|
||||||
import ani.dantotsu.parsers.SubtitleType
|
import ani.dantotsu.parsers.SubtitleType
|
||||||
|
@ -49,13 +49,14 @@ import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.*
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
object Helper {
|
object Helper {
|
||||||
|
|
||||||
|
|
||||||
private var simpleCache: SimpleCache? = null
|
private var simpleCache: SimpleCache? = null
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||||
val dataSourceFactory = DataSource.Factory {
|
val dataSourceFactory = DataSource.Factory {
|
||||||
val dataSource: HttpDataSource =
|
val dataSource: HttpDataSource =
|
||||||
|
@ -157,16 +158,14 @@ object Helper {
|
||||||
download: Download,
|
download: Download,
|
||||||
finalException: Exception?
|
finalException: Exception?
|
||||||
) {
|
) {
|
||||||
if (download.state == Download.STATE_COMPLETED) {
|
when (download.state) {
|
||||||
Logger.log("Download Completed")
|
Download.STATE_COMPLETED -> Logger.log("Download Completed")
|
||||||
} else if (download.state == Download.STATE_FAILED) {
|
Download.STATE_FAILED -> Logger.log("Download Failed")
|
||||||
Logger.log("Download Failed")
|
Download.STATE_STOPPED -> Logger.log("Download Stopped")
|
||||||
} else if (download.state == Download.STATE_STOPPED) {
|
Download.STATE_QUEUED -> Logger.log("Download Queued")
|
||||||
Logger.log("Download Stopped")
|
Download.STATE_DOWNLOADING -> Logger.log("Download Downloading")
|
||||||
} else if (download.state == Download.STATE_QUEUED) {
|
Download.STATE_REMOVING -> Logger.log("Download Removing")
|
||||||
Logger.log("Download Queued")
|
Download.STATE_RESTARTING -> Logger.log("Download Restarting")
|
||||||
} else if (download.state == Download.STATE_DOWNLOADING) {
|
|
||||||
Logger.log("Download Downloading")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,7 +219,7 @@ object Helper {
|
||||||
|
|
||||||
val downloadsManger = Injekt.get<DownloadsManager>()
|
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||||
val downloadCheck = downloadsManger
|
val downloadCheck = downloadsManger
|
||||||
.queryDownload(title, episode, DownloadedType.Type.ANIME)
|
.queryDownload(title, episode, MediaType.ANIME)
|
||||||
|
|
||||||
if (downloadCheck) {
|
if (downloadCheck) {
|
||||||
AlertDialog.Builder(context, R.style.MyPopup)
|
AlertDialog.Builder(context, R.style.MyPopup)
|
||||||
|
@ -243,7 +242,7 @@ object Helper {
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
title,
|
title,
|
||||||
episode,
|
episode,
|
||||||
DownloadedType.Type.ANIME
|
MediaType.ANIME
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||||
|
|
|
@ -167,8 +167,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||||
|
|
||||||
trendHandler = Handler(Looper.getMainLooper())
|
trendHandler = Handler(Looper.getMainLooper())
|
||||||
trendRun = Runnable {
|
trendRun = Runnable {
|
||||||
binding.animeTrendingViewPager.currentItem =
|
binding.animeTrendingViewPager.currentItem += 1
|
||||||
binding.animeTrendingViewPager.currentItem + 1
|
|
||||||
}
|
}
|
||||||
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
||||||
object : ViewPager2.OnPageChangeCallback() {
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
|
|
|
@ -66,8 +66,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||||
val color = typedValue.data
|
val color = typedValue.data
|
||||||
|
|
||||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||||
|
|
||||||
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -16,8 +15,8 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityListBinding
|
import ani.dantotsu.databinding.ActivityListBinding
|
||||||
|
import ani.dantotsu.hideSystemBarsExtendView
|
||||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||||
import ani.dantotsu.navBarHeight
|
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
@ -73,10 +72,7 @@ class CalendarActivity : AppCompatActivity() {
|
||||||
} else {
|
} else {
|
||||||
binding.root.fitsSystemWindows = false
|
binding.root.fitsSystemWindows = false
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
window.setFlags(
|
hideSystemBarsExtendView()
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
|
||||||
)
|
|
||||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
topMargin = statusBarHeight
|
topMargin = statusBarHeight
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package ani.dantotsu.media
|
||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
@ -13,9 +12,7 @@ import android.view.GestureDetector
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
@ -243,13 +240,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||||
@SuppressLint("ResourceType")
|
@SuppressLint("ResourceType")
|
||||||
fun total() {
|
fun total() {
|
||||||
val text = SpannableStringBuilder().apply {
|
val text = SpannableStringBuilder().apply {
|
||||||
val typedValue = TypedValue()
|
val mediaTypedValue = TypedValue()
|
||||||
this@MediaDetailsActivity.theme.resolveAttribute(
|
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||||
com.google.android.material.R.attr.colorOnBackground,
|
com.google.android.material.R.attr.colorOnBackground,
|
||||||
typedValue,
|
mediaTypedValue,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
val white = typedValue.data
|
val white = mediaTypedValue.data
|
||||||
if (media.userStatus != null) {
|
if (media.userStatus != null) {
|
||||||
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
||||||
val typedValue = TypedValue()
|
val typedValue = TypedValue()
|
||||||
|
|
|
@ -52,12 +52,16 @@ class MediaDetailsViewModel : ViewModel() {
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
if (isDownload) {
|
if (isDownload) {
|
||||||
data.sourceIndex = if (media.anime != null) {
|
data.sourceIndex = when {
|
||||||
AnimeSources.list.size - 1
|
media.anime != null -> {
|
||||||
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
|
AnimeSources.list.size - 1
|
||||||
MangaSources.list.size - 1
|
}
|
||||||
} else {
|
media.format == "MANGA" || media.format == "ONE_SHOT" -> {
|
||||||
NovelSources.list.size - 1
|
MangaSources.list.size - 1
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
NovelSources.list.size - 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
@ -152,10 +156,10 @@ class MediaDetailsViewModel : ViewModel() {
|
||||||
watchSources?.get(i)?.apply {
|
watchSources?.get(i)?.apply {
|
||||||
if (!post && !allowsPreloading) return@apply
|
if (!post && !allowsPreloading) return@apply
|
||||||
ep.sEpisode?.let {
|
ep.sEpisode?.let {
|
||||||
loadByVideoServers(link, ep.extra, it) {
|
loadByVideoServers(link, ep.extra, it) { extractor ->
|
||||||
if (it.videos.isNotEmpty()) {
|
if (extractor.videos.isNotEmpty()) {
|
||||||
list.add(it)
|
list.add(extractor)
|
||||||
ep.extractorCallback?.invoke(it)
|
ep.extractorCallback?.invoke(extractor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
26
app/src/main/java/ani/dantotsu/media/MediaType.kt
Normal file
26
app/src/main/java/ani/dantotsu/media/MediaType.kt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package ani.dantotsu.media
|
||||||
|
|
||||||
|
enum class MediaType {
|
||||||
|
ANIME,
|
||||||
|
MANGA,
|
||||||
|
NOVEL;
|
||||||
|
|
||||||
|
fun asText(): String {
|
||||||
|
return when (this) {
|
||||||
|
ANIME -> "Anime"
|
||||||
|
MANGA -> "Manga"
|
||||||
|
NOVEL -> "Novel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromText(string : String): MediaType {
|
||||||
|
return when (string) {
|
||||||
|
"Anime" -> ANIME
|
||||||
|
"Manga" -> MANGA
|
||||||
|
"Novel" -> NOVEL
|
||||||
|
else -> { ANIME }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,7 +65,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
|
||||||
i = media!!.selected!!.sourceIndex
|
i = media!!.selected!!.sourceIndex
|
||||||
|
|
||||||
val source = if (media!!.anime != null) {
|
val source = if (media!!.anime != null) {
|
||||||
(if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!]
|
(if (media!!.isAdult) HAnimeSources else AnimeSources)[i!!]
|
||||||
} else {
|
} else {
|
||||||
anime = false
|
anime = false
|
||||||
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
|
||||||
|
|
|
@ -17,7 +17,7 @@ class SubtitleDownloader {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
//doesn't really download the subtitles -\_(o_o)_/-
|
//doesn't really download the subtitles -\_(o_o)_/-
|
||||||
suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
|
suspend fun loadSubtitleType(url: String): SubtitleType =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
|
||||||
val networkHelper = Injekt.get<NetworkHelper>()
|
val networkHelper = Injekt.get<NetworkHelper>()
|
||||||
|
@ -60,7 +60,7 @@ class SubtitleDownloader {
|
||||||
if (!directory.exists()) { //just in case
|
if (!directory.exists()) { //just in case
|
||||||
directory.mkdirs()
|
directory.mkdirs()
|
||||||
}
|
}
|
||||||
val type = loadSubtitleType(context, url)
|
val type = loadSubtitleType(url)
|
||||||
val subtiteFile = File(directory, "subtitle.${type}")
|
val subtiteFile = File(directory, "subtitle.${type}")
|
||||||
if (subtiteFile.exists()) {
|
if (subtiteFile.exists()) {
|
||||||
subtiteFile.delete()
|
subtiteFile.delete()
|
||||||
|
|
|
@ -7,7 +7,7 @@ import java.util.regex.Pattern
|
||||||
class AnimeNameAdapter {
|
class AnimeNameAdapter {
|
||||||
companion object {
|
companion object {
|
||||||
const val episodeRegex =
|
const val episodeRegex =
|
||||||
"(episode|episodio|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
"(episode|episodio|ep|e)[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||||
const val failedEpisodeNumberRegex =
|
const val failedEpisodeNumberRegex =
|
||||||
"(?<!part\\s)\\b(\\d+)\\b"
|
"(?<!part\\s)\\b(\\d+)\\b"
|
||||||
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||||
|
@ -114,7 +114,7 @@ class AnimeNameAdapter {
|
||||||
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
|
||||||
val removedNumber = text.replace(regexPattern, "")
|
val removedNumber = text.replace(regexPattern, "")
|
||||||
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||||
val failedEpisodeNumberPattern: Regex =
|
val failedEpisodeNumberPattern =
|
||||||
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
|
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
|
||||||
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||||
mr.value.replaceFirst(mr.groupValues[1], "")
|
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||||
|
|
|
@ -29,19 +29,24 @@ import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.FileUrl
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||||
|
import ani.dantotsu.dp
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.others.LanguageMapper
|
import ani.dantotsu.others.LanguageMapper
|
||||||
import ani.dantotsu.parsers.AnimeParser
|
import ani.dantotsu.parsers.AnimeParser
|
||||||
import ani.dantotsu.parsers.AnimeSources
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
import ani.dantotsu.parsers.HAnimeSources
|
import ani.dantotsu.parsers.HAnimeSources
|
||||||
|
import ani.dantotsu.setNavigationTheme
|
||||||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
@ -433,7 +438,7 @@ class AnimeWatchFragment : Fragment() {
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
media.mainName(),
|
media.mainName(),
|
||||||
i,
|
i,
|
||||||
DownloadedType.Type.ANIME
|
MediaType.ANIME
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
episodeAdapter.purgeDownload(i)
|
episodeAdapter.purgeDownload(i)
|
||||||
|
@ -445,7 +450,7 @@ class AnimeWatchFragment : Fragment() {
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
media.mainName(),
|
media.mainName(),
|
||||||
i,
|
i,
|
||||||
DownloadedType.Type.ANIME
|
MediaType.ANIME
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
|
||||||
|
|
|
@ -13,14 +13,16 @@ import androidx.lifecycle.coroutineScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.offline.DownloadIndex
|
import androidx.media3.exoplayer.offline.DownloadIndex
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.updateProgress
|
import ani.dantotsu.connections.updateProgress
|
||||||
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||||
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
||||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||||
import ani.dantotsu.download.video.Helper
|
import ani.dantotsu.download.video.Helper
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
|
import ani.dantotsu.setAnimation
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
import com.bumptech.glide.load.model.GlideUrl
|
||||||
|
@ -427,7 +429,7 @@ class EpisodeAdapter(
|
||||||
if (bytes < 0) return null
|
if (bytes < 0) return null
|
||||||
val unit = 1000
|
val unit = 1000
|
||||||
if (bytes < unit) return "$bytes B"
|
if (bytes < unit) return "$bytes B"
|
||||||
val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
||||||
val pre = ("KMGTPE")[exp - 1]
|
val pre = ("KMGTPE")[exp - 1]
|
||||||
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
|
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,10 @@ import android.graphics.Color
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.hardware.SensorManager
|
import android.hardware.SensorManager
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.AudioManager.*
|
import android.media.AudioManager.AUDIOFOCUS_GAIN
|
||||||
|
import android.media.AudioManager.AUDIOFOCUS_LOSS
|
||||||
|
import android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
|
||||||
|
import android.media.AudioManager.STREAM_MUSIC
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -27,8 +30,18 @@ import android.provider.Settings.System
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.Rational
|
import android.util.Rational
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.*
|
import android.view.GestureDetector
|
||||||
import android.view.KeyEvent.*
|
import android.view.KeyEvent
|
||||||
|
import android.view.KeyEvent.ACTION_UP
|
||||||
|
import android.view.KeyEvent.KEYCODE_B
|
||||||
|
import android.view.KeyEvent.KEYCODE_DPAD_LEFT
|
||||||
|
import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
|
||||||
|
import android.view.KeyEvent.KEYCODE_N
|
||||||
|
import android.view.KeyEvent.KEYCODE_SPACE
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.OrientationEventListener
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
|
@ -46,27 +59,43 @@ import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.cast.CastPlayer
|
import androidx.media3.cast.CastPlayer
|
||||||
import androidx.media3.cast.SessionAvailabilityListener
|
import androidx.media3.cast.SessionAvailabilityListener
|
||||||
import androidx.media3.common.*
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE
|
import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE
|
||||||
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.MimeTypes
|
||||||
|
import androidx.media3.common.PlaybackException
|
||||||
|
import androidx.media3.common.PlaybackParameters
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.TrackSelectionOverride
|
||||||
|
import androidx.media3.common.Tracks
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.common.util.Util
|
|
||||||
import androidx.media3.datasource.DataSource
|
import androidx.media3.datasource.DataSource
|
||||||
import androidx.media3.datasource.DefaultDataSourceFactory
|
import androidx.media3.datasource.DefaultDataSource
|
||||||
import androidx.media3.datasource.HttpDataSource
|
import androidx.media3.datasource.HttpDataSource
|
||||||
import androidx.media3.datasource.cache.CacheDataSource
|
import androidx.media3.datasource.cache.CacheDataSource
|
||||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
|
import androidx.media3.exoplayer.DefaultLoadControl
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||||
import androidx.media3.exoplayer.util.EventLogger
|
import androidx.media3.exoplayer.util.EventLogger
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import androidx.media3.ui.*
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
import androidx.media3.ui.CaptionStyleCompat.*
|
import androidx.media3.ui.CaptionStyleCompat
|
||||||
import androidx.media3.exoplayer.DefaultLoadControl
|
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_DEPRESSED
|
||||||
|
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW
|
||||||
|
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_NONE
|
||||||
|
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_OUTLINE
|
||||||
|
import androidx.media3.ui.DefaultTimeBar
|
||||||
|
import androidx.media3.ui.PlayerView
|
||||||
|
import androidx.media3.ui.SubtitleView
|
||||||
import androidx.mediarouter.app.MediaRouteButton
|
import androidx.mediarouter.app.MediaRouteButton
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.GesturesListener
|
||||||
|
import ani.dantotsu.NoPaddingArrayAdapter
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.brightnessConverter
|
||||||
|
import ani.dantotsu.circularReveal
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.connections.discord.Discord
|
import ani.dantotsu.connections.discord.Discord
|
||||||
|
@ -75,19 +104,38 @@ import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
|
||||||
import ani.dantotsu.connections.discord.RPC
|
import ani.dantotsu.connections.discord.RPC
|
||||||
import ani.dantotsu.connections.updateProgress
|
import ani.dantotsu.connections.updateProgress
|
||||||
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
import ani.dantotsu.databinding.ActivityExoplayerBinding
|
||||||
|
import ani.dantotsu.defaultHeaders
|
||||||
import ani.dantotsu.download.video.Helper
|
import ani.dantotsu.download.video.Helper
|
||||||
|
import ani.dantotsu.dp
|
||||||
|
import ani.dantotsu.getCurrentBrightnessValue
|
||||||
|
import ani.dantotsu.hideSystemBars
|
||||||
|
import ani.dantotsu.hideSystemBarsExtendView
|
||||||
|
import ani.dantotsu.isOnline
|
||||||
|
import ani.dantotsu.logError
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
import ani.dantotsu.media.SubtitleDownloader
|
import ani.dantotsu.media.SubtitleDownloader
|
||||||
|
import ani.dantotsu.okHttpClient
|
||||||
import ani.dantotsu.others.AniSkip
|
import ani.dantotsu.others.AniSkip
|
||||||
import ani.dantotsu.others.AniSkip.getType
|
import ani.dantotsu.others.AniSkip.getType
|
||||||
import ani.dantotsu.others.ResettableTimer
|
import ani.dantotsu.others.ResettableTimer
|
||||||
import ani.dantotsu.others.getSerialized
|
import ani.dantotsu.others.getSerialized
|
||||||
import ani.dantotsu.parsers.*
|
import ani.dantotsu.parsers.AnimeSources
|
||||||
|
import ani.dantotsu.parsers.HAnimeSources
|
||||||
|
import ani.dantotsu.parsers.Subtitle
|
||||||
|
import ani.dantotsu.parsers.SubtitleType
|
||||||
|
import ani.dantotsu.parsers.Video
|
||||||
|
import ani.dantotsu.parsers.VideoExtractor
|
||||||
|
import ani.dantotsu.parsers.VideoType
|
||||||
|
import ani.dantotsu.px
|
||||||
import ani.dantotsu.settings.PlayerSettingsActivity
|
import ani.dantotsu.settings.PlayerSettingsActivity
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.startMainActivity
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
import ani.dantotsu.toast
|
||||||
|
import ani.dantotsu.tryWithSuspend
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.gms.cast.framework.CastButtonFactory
|
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||||
|
@ -103,8 +151,11 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
import java.util.concurrent.*
|
import java.util.Timer
|
||||||
|
import java.util.TimerTask
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
@ -344,15 +395,14 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
isCastApiAvailable = GoogleApiAvailability.getInstance()
|
isCastApiAvailable = GoogleApiAvailability.getInstance()
|
||||||
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||||
try {
|
try {
|
||||||
castContext = CastContext.getSharedInstance(this)
|
castContext = CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor()).result
|
||||||
castPlayer = CastPlayer(castContext!!)
|
castPlayer = CastPlayer(castContext!!)
|
||||||
castPlayer!!.setSessionAvailabilityListener(this)
|
castPlayer!!.setSessionAvailabilityListener(this)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
isCastApiAvailable = false
|
isCastApiAvailable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
hideSystemBarsExtendView()
|
||||||
hideSystemBars()
|
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(this) {
|
onBackPressedDispatcher.addCallback(this) {
|
||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
|
@ -397,21 +447,25 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
orientationListener =
|
orientationListener =
|
||||||
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
||||||
override fun onOrientationChanged(orientation: Int) {
|
override fun onOrientationChanged(orientation: Int) {
|
||||||
if (orientation in 45..135) {
|
when (orientation) {
|
||||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
|
in 45..135 -> {
|
||||||
exoRotate.visibility = View.VISIBLE
|
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
|
||||||
|
exoRotate.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
}
|
}
|
||||||
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
in 225..315 -> {
|
||||||
} else if (orientation in 225..315) {
|
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
|
||||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
|
exoRotate.visibility = View.VISIBLE
|
||||||
exoRotate.visibility = View.VISIBLE
|
}
|
||||||
|
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
}
|
}
|
||||||
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
in 315..360, in 0..45 -> {
|
||||||
} else if (orientation in 315..360 || orientation in 0..45) {
|
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
|
||||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
|
exoRotate.visibility = View.VISIBLE
|
||||||
exoRotate.visibility = View.VISIBLE
|
}
|
||||||
|
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
}
|
}
|
||||||
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -943,7 +997,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
episodeArr = episodes.keys.toList()
|
episodeArr = episodes.keys.toList()
|
||||||
currentEpisodeIndex = episodeArr.indexOf(media.anime!!.selectedEpisode!!)
|
currentEpisodeIndex = episodeArr.indexOf(media.anime!!.selectedEpisode!!)
|
||||||
|
|
||||||
episodeTitleArr = arrayListOf<String>()
|
episodeTitleArr = arrayListOf()
|
||||||
episodes.forEach {
|
episodes.forEach {
|
||||||
val episode = it.value
|
val episode = it.value
|
||||||
val cleanedTitle = AnimeNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
|
val cleanedTitle = AnimeNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
|
||||||
|
@ -1054,7 +1108,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
|
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
|
||||||
RPC.Link(
|
RPC.Link(
|
||||||
"Stream on Dantotsu",
|
"Stream on Dantotsu",
|
||||||
"https://github.com/rebelonion/Dantotsu/"
|
getString(R.string.github)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -1266,6 +1320,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
media.anime!!.selectedEpisode!!
|
media.anime!!.selectedEpisode!!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val list = (PrefManager.getNullableCustomVal("continueAnimeList", listOf<Int>(), List::class.java) as List<Int>).toMutableList()
|
val list = (PrefManager.getNullableCustomVal("continueAnimeList", listOf<Int>(), List::class.java) as List<Int>).toMutableList()
|
||||||
if (list.contains(media.id)) list.remove(media.id)
|
if (list.contains(media.id)) list.remove(media.id)
|
||||||
list.add(media.id)
|
list.add(media.id)
|
||||||
|
@ -1303,9 +1358,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
if (subtitle != null) {
|
if (subtitle != null) {
|
||||||
//var localFile: String? = null
|
//var localFile: String? = null
|
||||||
if (subtitle?.type == SubtitleType.UNKNOWN) {
|
if (subtitle?.type == SubtitleType.UNKNOWN) {
|
||||||
val context = this
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val type = SubtitleDownloader.loadSubtitleType(context, subtitle!!.file.url)
|
val type = SubtitleDownloader.loadSubtitleType(subtitle!!.file.url)
|
||||||
val fileUri = Uri.parse(subtitle!!.file.url)
|
val fileUri = Uri.parse(subtitle!!.file.url)
|
||||||
sub = MediaItem.SubtitleConfiguration
|
sub = MediaItem.SubtitleConfiguration
|
||||||
.Builder(fileUri)
|
.Builder(fileUri)
|
||||||
|
@ -1360,8 +1414,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
}
|
}
|
||||||
dataSource
|
dataSource
|
||||||
}
|
}
|
||||||
val dafuckDataSourceFactory =
|
val dafuckDataSourceFactory = DefaultDataSource.Factory(this)
|
||||||
DefaultDataSourceFactory(this, Util.getUserAgent(this, R.string.app_name.toString()))
|
|
||||||
cacheFactory = CacheDataSource.Factory().apply {
|
cacheFactory = CacheDataSource.Factory().apply {
|
||||||
setCache(Helper.getSimpleCache(this@ExoplayerView))
|
setCache(Helper.getSimpleCache(this@ExoplayerView))
|
||||||
if (ext.server.offline) {
|
if (ext.server.offline) {
|
||||||
|
@ -1737,28 +1790,26 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||||
timer = null
|
timer = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (timer == null) {
|
timer = object : CountDownTimer(5000, 1000) {
|
||||||
timer = object : CountDownTimer(5000, 1000) {
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
override fun onTick(millisUntilFinished: Long) {
|
if (new == null) {
|
||||||
if (new == null){
|
|
||||||
skipTimeButton.visibility = View.GONE
|
|
||||||
exoSkip.visibility = View.VISIBLE
|
|
||||||
disappeared = false
|
|
||||||
functionstarted = false
|
|
||||||
cancelTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinish() {
|
|
||||||
skipTimeButton.visibility = View.GONE
|
skipTimeButton.visibility = View.GONE
|
||||||
exoSkip.visibility = View.VISIBLE
|
exoSkip.visibility = View.VISIBLE
|
||||||
disappeared = true
|
disappeared = false
|
||||||
functionstarted = false
|
functionstarted = false
|
||||||
cancelTimer()
|
cancelTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timer?.start()
|
|
||||||
|
override fun onFinish() {
|
||||||
|
skipTimeButton.visibility = View.GONE
|
||||||
|
exoSkip.visibility = View.VISIBLE
|
||||||
|
disappeared = true
|
||||||
|
functionstarted = false
|
||||||
|
cancelTimer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
timer?.start()
|
||||||
|
|
||||||
}
|
}
|
||||||
if (PrefManager.getVal(PrefName.ShowTimeStampButton)) {
|
if (PrefManager.getVal(PrefName.ShowTimeStampButton)) {
|
||||||
|
|
|
@ -29,6 +29,7 @@ import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
@ -258,6 +259,7 @@ class CommentItem(val comment: Comment,
|
||||||
|
|
||||||
private fun removeSubCommentIds(){
|
private fun removeSubCommentIds(){
|
||||||
subCommentIds.forEach { id ->
|
subCommentIds.forEach { id ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
|
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
|
||||||
val commentToRemove = parentComments.find { it.comment.commentId == id }
|
val commentToRemove = parentComments.find { it.comment.commentId == id }
|
||||||
commentToRemove?.let {
|
commentToRemove?.let {
|
||||||
|
@ -290,7 +292,7 @@ class CommentItem(val comment: Comment,
|
||||||
@SuppressLint("SimpleDateFormat")
|
@SuppressLint("SimpleDateFormat")
|
||||||
private fun formatTimestamp(timestamp: String): String {
|
private fun formatTimestamp(timestamp: String): String {
|
||||||
return try {
|
return try {
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
|
||||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
val parsedDate = dateFormat.parse(timestamp)
|
val parsedDate = dateFormat.parse(timestamp)
|
||||||
val currentDate = Date()
|
val currentDate = Date()
|
||||||
|
@ -315,7 +317,7 @@ class CommentItem(val comment: Comment,
|
||||||
companion object {
|
companion object {
|
||||||
@SuppressLint("SimpleDateFormat")
|
@SuppressLint("SimpleDateFormat")
|
||||||
fun timestampToMillis(timestamp: String): Long {
|
fun timestampToMillis(timestamp: String): Long {
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
|
||||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
val parsedDate = dateFormat.parse(timestamp)
|
val parsedDate = dateFormat.parse(timestamp)
|
||||||
return parsedDate?.time ?: 0
|
return parsedDate?.time ?: 0
|
||||||
|
|
|
@ -2,7 +2,6 @@ package ani.dantotsu.media.manga
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -10,8 +9,8 @@ import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -25,8 +24,7 @@ data class ImageData(
|
||||||
) {
|
) {
|
||||||
suspend fun fetchAndProcessImage(
|
suspend fun fetchAndProcessImage(
|
||||||
page: Page,
|
page: Page,
|
||||||
httpSource: HttpSource,
|
httpSource: HttpSource
|
||||||
context: Context
|
|
||||||
): Bitmap? {
|
): Bitmap? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -5,8 +5,8 @@ import java.util.regex.Pattern
|
||||||
|
|
||||||
class MangaNameAdapter {
|
class MangaNameAdapter {
|
||||||
companion object {
|
companion object {
|
||||||
const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*"
|
private const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
|
||||||
const val filedChapterNumberRegex = "(?<!part\\s)\\b(\\d+)\\b"
|
private const val filedChapterNumberRegex = "(?<!part\\s)\\b(\\d+)\\b"
|
||||||
fun findChapterNumber(text: String): Float? {
|
fun findChapterNumber(text: String): Float? {
|
||||||
val pattern: Pattern = Pattern.compile(chapterRegex, Pattern.CASE_INSENSITIVE)
|
val pattern: Pattern = Pattern.compile(chapterRegex, Pattern.CASE_INSENSITIVE)
|
||||||
val matcher: Matcher = pattern.matcher(text)
|
val matcher: Matcher = pattern.matcher(text)
|
||||||
|
|
|
@ -30,21 +30,25 @@ import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||||
import ani.dantotsu.download.DownloadedType
|
import ani.dantotsu.download.DownloadedType
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
import ani.dantotsu.download.manga.MangaDownloaderService
|
import ani.dantotsu.download.manga.MangaDownloaderService
|
||||||
import ani.dantotsu.download.manga.MangaServiceDataSingleton
|
import ani.dantotsu.download.manga.MangaServiceDataSingleton
|
||||||
|
import ani.dantotsu.dp
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsActivity
|
import ani.dantotsu.media.MediaDetailsActivity
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
|
||||||
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.others.LanguageMapper
|
import ani.dantotsu.others.LanguageMapper
|
||||||
import ani.dantotsu.parsers.DynamicMangaParser
|
import ani.dantotsu.parsers.DynamicMangaParser
|
||||||
import ani.dantotsu.parsers.HMangaSources
|
import ani.dantotsu.parsers.HMangaSources
|
||||||
import ani.dantotsu.parsers.MangaParser
|
import ani.dantotsu.parsers.MangaParser
|
||||||
import ani.dantotsu.parsers.MangaSources
|
import ani.dantotsu.parsers.MangaSources
|
||||||
|
import ani.dantotsu.setNavigationTheme
|
||||||
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
@ -492,7 +496,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
media.mainName(),
|
media.mainName(),
|
||||||
i,
|
i,
|
||||||
DownloadedType.Type.MANGA
|
MediaType.MANGA
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
chapterAdapter.deleteDownload(i)
|
chapterAdapter.deleteDownload(i)
|
||||||
|
@ -510,7 +514,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
media.mainName(),
|
media.mainName(),
|
||||||
i,
|
i,
|
||||||
DownloadedType.Type.MANGA
|
MediaType.MANGA
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
chapterAdapter.purgeDownload(i)
|
chapterAdapter.purgeDownload(i)
|
||||||
|
|
|
@ -13,10 +13,14 @@ import androidx.core.view.GestureDetectorCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.FileUrl
|
||||||
|
import ani.dantotsu.GesturesListener
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.media.manga.MangaCache
|
import ani.dantotsu.media.manga.MangaCache
|
||||||
import ani.dantotsu.media.manga.MangaChapter
|
import ani.dantotsu.media.manga.MangaChapter
|
||||||
|
import ani.dantotsu.px
|
||||||
import ani.dantotsu.settings.CurrentReaderSettings
|
import ani.dantotsu.settings.CurrentReaderSettings
|
||||||
|
import ani.dantotsu.tryWithSuspend
|
||||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
@ -118,13 +122,13 @@ abstract class BaseImageAdapter(
|
||||||
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
abstract suspend fun loadImage(position: Int, parent: View): Boolean
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
suspend fun Context.loadBitmap_old(
|
suspend fun Context.loadBitmapOld(
|
||||||
link: FileUrl,
|
link: FileUrl,
|
||||||
transforms: List<BitmapTransformation>
|
transforms: List<BitmapTransformation>
|
||||||
): Bitmap? { //still used in some places
|
): Bitmap? { //still used in some places
|
||||||
return tryWithSuspend {
|
return tryWithSuspend {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Glide.with(this@loadBitmap_old)
|
Glide.with(this@loadBitmapOld)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.let {
|
.let {
|
||||||
if (link.url.startsWith("file://")) {
|
if (link.url.startsWith("file://")) {
|
||||||
|
@ -168,8 +172,7 @@ abstract class BaseImageAdapter(
|
||||||
mangaCache.get(link.url)?.let { imageData ->
|
mangaCache.get(link.url)?.let { imageData ->
|
||||||
val bitmap = imageData.fetchAndProcessImage(
|
val bitmap = imageData.fetchAndProcessImage(
|
||||||
imageData.page,
|
imageData.page,
|
||||||
imageData.source,
|
imageData.source
|
||||||
context = this@loadBitmap
|
|
||||||
)
|
)
|
||||||
it.load(bitmap)
|
it.load(bitmap)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
|
|
|
@ -10,8 +10,19 @@ import android.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.KeyEvent.*
|
import android.view.KeyEvent
|
||||||
|
import android.view.KeyEvent.ACTION_DOWN
|
||||||
|
import android.view.KeyEvent.KEYCODE_DPAD_DOWN
|
||||||
|
import android.view.KeyEvent.KEYCODE_DPAD_UP
|
||||||
|
import android.view.KeyEvent.KEYCODE_PAGE_DOWN
|
||||||
|
import android.view.KeyEvent.KEYCODE_PAGE_UP
|
||||||
|
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
|
||||||
|
import android.view.KeyEvent.KEYCODE_VOLUME_UP
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.WindowManager
|
||||||
import android.view.animation.OvershootInterpolator
|
import android.view.animation.OvershootInterpolator
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.CheckBox
|
import android.widget.CheckBox
|
||||||
|
@ -27,7 +38,9 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.PagerSnapHelper
|
import androidx.recyclerview.widget.PagerSnapHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import ani.dantotsu.*
|
import ani.dantotsu.GesturesListener
|
||||||
|
import ani.dantotsu.NoPaddingArrayAdapter
|
||||||
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.connections.discord.Discord
|
import ani.dantotsu.connections.discord.Discord
|
||||||
|
@ -35,7 +48,12 @@ import ani.dantotsu.connections.discord.DiscordService
|
||||||
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
|
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
|
||||||
import ani.dantotsu.connections.discord.RPC
|
import ani.dantotsu.connections.discord.RPC
|
||||||
import ani.dantotsu.connections.updateProgress
|
import ani.dantotsu.connections.updateProgress
|
||||||
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.databinding.ActivityMangaReaderBinding
|
import ani.dantotsu.databinding.ActivityMangaReaderBinding
|
||||||
|
import ani.dantotsu.dp
|
||||||
|
import ani.dantotsu.hideSystemBarsExtendView
|
||||||
|
import ani.dantotsu.isOnline
|
||||||
|
import ani.dantotsu.logError
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
import ani.dantotsu.media.MediaSingleton
|
import ani.dantotsu.media.MediaSingleton
|
||||||
|
@ -46,14 +64,25 @@ import ani.dantotsu.others.ImageViewDialog
|
||||||
import ani.dantotsu.parsers.HMangaSources
|
import ani.dantotsu.parsers.HMangaSources
|
||||||
import ani.dantotsu.parsers.MangaImage
|
import ani.dantotsu.parsers.MangaImage
|
||||||
import ani.dantotsu.parsers.MangaSources
|
import ani.dantotsu.parsers.MangaSources
|
||||||
|
import ani.dantotsu.px
|
||||||
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.settings.CurrentReaderSettings
|
import ani.dantotsu.settings.CurrentReaderSettings
|
||||||
import ani.dantotsu.settings.CurrentReaderSettings.Companion.applyWebtoon
|
import ani.dantotsu.settings.CurrentReaderSettings.Companion.applyWebtoon
|
||||||
import ani.dantotsu.settings.CurrentReaderSettings.Directions.*
|
import ani.dantotsu.settings.CurrentReaderSettings.Directions.BOTTOM_TO_TOP
|
||||||
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.*
|
import ani.dantotsu.settings.CurrentReaderSettings.Directions.LEFT_TO_RIGHT
|
||||||
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
|
import ani.dantotsu.settings.CurrentReaderSettings.Directions.RIGHT_TO_LEFT
|
||||||
|
import ani.dantotsu.settings.CurrentReaderSettings.Directions.TOP_TO_BOTTOM
|
||||||
|
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.Automatic
|
||||||
|
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.Force
|
||||||
|
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.No
|
||||||
|
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.CONTINUOUS_PAGED
|
||||||
|
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.PAGED
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
|
import ani.dantotsu.showSystemBarsRetractView
|
||||||
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
|
import ani.dantotsu.tryWith
|
||||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
|
@ -66,7 +95,8 @@ import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.ObjectInputStream
|
import java.io.ObjectInputStream
|
||||||
import java.io.ObjectOutputStream
|
import java.io.ObjectOutputStream
|
||||||
import java.util.*
|
import java.util.Timer
|
||||||
|
import java.util.TimerTask
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
@ -88,7 +118,6 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private var isContVisible = false
|
private var isContVisible = false
|
||||||
private var showProgressDialog = true
|
private var showProgressDialog = true
|
||||||
private var hidescrollbar = false
|
|
||||||
|
|
||||||
private var maxChapterPage = 0L
|
private var maxChapterPage = 0L
|
||||||
private var currentChapterPage = 0L
|
private var currentChapterPage = 0L
|
||||||
|
@ -123,7 +152,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideSystemBars() {
|
private fun hideSystemBars() {
|
||||||
if (PrefManager.getVal<Boolean>(PrefName.ShowSystemBars))
|
if (PrefManager.getVal(PrefName.ShowSystemBars))
|
||||||
showSystemBarsRetractView()
|
showSystemBarsRetractView()
|
||||||
else
|
else
|
||||||
hideSystemBarsExtendView()
|
hideSystemBarsExtendView()
|
||||||
|
@ -368,7 +397,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||||
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
|
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
|
||||||
RPC.Link(
|
RPC.Link(
|
||||||
"Stream on Dantotsu",
|
"Stream on Dantotsu",
|
||||||
"https://github.com/rebelonion/Dantotsu/"
|
getString(R.string.github)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -740,12 +769,12 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||||
goneTimer.schedule(timerTask, controllerDuration)
|
goneTimer.schedule(timerTask, controllerDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class pressPos {
|
enum class PressPos {
|
||||||
LEFT, RIGHT, CENTER
|
LEFT, RIGHT, CENTER
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleController(shouldShow: Boolean? = null, event: MotionEvent? = null) {
|
fun handleController(shouldShow: Boolean? = null, event: MotionEvent? = null) {
|
||||||
var pressLocation = pressPos.CENTER
|
var pressLocation = PressPos.CENTER
|
||||||
if (!sliding) {
|
if (!sliding) {
|
||||||
if (event != null && defaultSettings.layout == PAGED) {
|
if (event != null && defaultSettings.layout == PAGED) {
|
||||||
if (event.action != MotionEvent.ACTION_UP) return
|
if (event.action != MotionEvent.ACTION_UP) return
|
||||||
|
@ -755,23 +784,23 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||||
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
|
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
|
||||||
if (screenWidth / 5 in x + 1..<y) {
|
if (screenWidth / 5 in x + 1..<y) {
|
||||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||||
pressPos.RIGHT
|
PressPos.RIGHT
|
||||||
} else {
|
} else {
|
||||||
pressPos.LEFT
|
PressPos.LEFT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right
|
//if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right
|
||||||
else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) {
|
else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) {
|
||||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||||
pressPos.LEFT
|
PressPos.LEFT
|
||||||
} else {
|
} else {
|
||||||
pressPos.RIGHT
|
PressPos.RIGHT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if pressLocation is left or right go to previous or next page (paged mode only)
|
// if pressLocation is left or right go to previous or next page (paged mode only)
|
||||||
if (pressLocation == pressPos.LEFT) {
|
if (pressLocation == PressPos.LEFT) {
|
||||||
|
|
||||||
if (binding.mangaReaderPager.currentItem > 0) {
|
if (binding.mangaReaderPager.currentItem > 0) {
|
||||||
//if the current images zoomed in, go back to normal before going to previous page
|
//if the current images zoomed in, go back to normal before going to previous page
|
||||||
|
@ -782,7 +811,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (pressLocation == pressPos.RIGHT) {
|
} else if (pressLocation == PressPos.RIGHT) {
|
||||||
if (binding.mangaReaderPager.currentItem < maxChapterPage - 1) {
|
if (binding.mangaReaderPager.currentItem < maxChapterPage - 1) {
|
||||||
//if the current images zoomed in, go back to normal before going to next page
|
//if the current images zoomed in, go back to normal before going to next page
|
||||||
if (imageAdapter?.isZoomed() == true) {
|
if (imageAdapter?.isZoomed() == true) {
|
||||||
|
@ -960,7 +989,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||||
if (!incognito && PrefManager.getCustomVal(
|
if (!incognito && PrefManager.getCustomVal(
|
||||||
"${media.id}_save_progress",
|
"${media.id}_save_progress",
|
||||||
true
|
true
|
||||||
) && if (media.isAdult) PrefManager.getVal<Boolean>(PrefName.UpdateForHReader) else true
|
) && if (media.isAdult) PrefManager.getVal(PrefName.UpdateForHReader) else true
|
||||||
)
|
)
|
||||||
updateProgress(
|
updateProgress(
|
||||||
media,
|
media,
|
||||||
|
|
|
@ -9,7 +9,6 @@ import android.os.Environment
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -28,6 +27,7 @@ import ani.dantotsu.download.novel.NovelDownloaderService
|
||||||
import ani.dantotsu.download.novel.NovelServiceDataSingleton
|
import ani.dantotsu.download.novel.NovelServiceDataSingleton
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaDetailsViewModel
|
import ani.dantotsu.media.MediaDetailsViewModel
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.novel.novelreader.NovelReaderActivity
|
import ani.dantotsu.media.novel.novelreader.NovelReaderActivity
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.parsers.ShowResponse
|
import ani.dantotsu.parsers.ShowResponse
|
||||||
|
@ -90,7 +90,7 @@ class NovelReadFragment : Fragment(),
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
media.mainName(),
|
media.mainName(),
|
||||||
novel.name,
|
novel.name,
|
||||||
DownloadedType.Type.NOVEL
|
MediaType.NOVEL
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -122,7 +122,7 @@ class NovelReadFragment : Fragment(),
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
media.mainName(),
|
media.mainName(),
|
||||||
novel.name,
|
novel.name,
|
||||||
DownloadedType.Type.NOVEL
|
MediaType.NOVEL
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -133,7 +133,7 @@ class NovelReadFragment : Fragment(),
|
||||||
DownloadedType(
|
DownloadedType(
|
||||||
media.mainName(),
|
media.mainName(),
|
||||||
novel.name,
|
novel.name,
|
||||||
DownloadedType.Type.NOVEL
|
MediaType.NOVEL
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
|
@ -17,7 +16,7 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.Refresh
|
import ani.dantotsu.Refresh
|
||||||
import ani.dantotsu.databinding.ActivityListBinding
|
import ani.dantotsu.databinding.ActivityListBinding
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.hideSystemBarsExtendView
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
|
@ -71,10 +70,7 @@ class ListActivity : AppCompatActivity() {
|
||||||
} else {
|
} else {
|
||||||
binding.root.fitsSystemWindows = false
|
binding.root.fitsSystemWindows = false
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
window.setFlags(
|
hideSystemBarsExtendView()
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
|
||||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
|
||||||
)
|
|
||||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
topMargin = statusBarHeight
|
topMargin = statusBarHeight
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ class AndroidBug5497Workaround private constructor(activity: Activity, private v
|
||||||
private val frameLayoutParams: FrameLayout.LayoutParams
|
private val frameLayoutParams: FrameLayout.LayoutParams
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val content = activity.findViewById(android.R.id.content) as FrameLayout
|
val content: FrameLayout = activity.findViewById(android.R.id.content)
|
||||||
mChildOfContent = content.getChildAt(0)
|
mChildOfContent = content.getChildAt(0)
|
||||||
mChildOfContent.viewTreeObserver.addOnGlobalLayoutListener { possiblyResizeChildOfContent() }
|
mChildOfContent.viewTreeObserver.addOnGlobalLayoutListener { possiblyResizeChildOfContent() }
|
||||||
frameLayoutParams = mChildOfContent.layoutParams as FrameLayout.LayoutParams
|
frameLayoutParams = mChildOfContent.layoutParams as FrameLayout.LayoutParams
|
||||||
|
|
|
@ -14,7 +14,7 @@ import ani.dantotsu.R
|
||||||
import ani.dantotsu.databinding.BottomSheetImageBinding
|
import ani.dantotsu.databinding.BottomSheetImageBinding
|
||||||
import ani.dantotsu.downloadsPermission
|
import ani.dantotsu.downloadsPermission
|
||||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap
|
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap
|
||||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap_old
|
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmapOld
|
||||||
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
|
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
|
||||||
import ani.dantotsu.openLinkInBrowser
|
import ani.dantotsu.openLinkInBrowser
|
||||||
import ani.dantotsu.saveImageToDownloads
|
import ani.dantotsu.saveImageToDownloads
|
||||||
|
@ -84,9 +84,9 @@ class ImageViewDialog : BottomSheetDialogFragment() {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val binding = _binding ?: return@launch
|
val binding = _binding ?: return@launch
|
||||||
|
|
||||||
var bitmap = context.loadBitmap_old(image, trans1 ?: listOf())
|
var bitmap = context.loadBitmapOld(image, trans1 ?: listOf())
|
||||||
var bitmap2 =
|
var bitmap2 =
|
||||||
if (image2 != null) context.loadBitmap_old(image2, trans2 ?: listOf()) else null
|
if (image2 != null) context.loadBitmapOld(image2, trans2 ?: listOf()) else null
|
||||||
if (bitmap == null) {
|
if (bitmap == null) {
|
||||||
bitmap = context.loadBitmap(image, trans1 ?: listOf())
|
bitmap = context.loadBitmap(image, trans1 ?: listOf())
|
||||||
bitmap2 =
|
bitmap2 =
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package ani.dantotsu.others
|
package ani.dantotsu.others
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
|
||||||
|
@ -54,14 +56,14 @@ class OutlineTextView : AppCompatTextView {
|
||||||
setStrokeWidth(strokeWidth)
|
setStrokeWidth(strokeWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val Float.toPx get() = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics
|
||||||
|
)
|
||||||
|
|
||||||
private fun setStrokeWidth(width: Float) {
|
private fun setStrokeWidth(width: Float) {
|
||||||
strokeWidth = width.toPx(context)
|
strokeWidth = width.toPx
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Float.toPx(context: Context) =
|
|
||||||
(this * context.resources.displayMetrics.scaledDensity + 0.5F)
|
|
||||||
|
|
||||||
override fun invalidate() {
|
override fun invalidate() {
|
||||||
if (isDrawing) return
|
if (isDrawing) return
|
||||||
super.invalidate()
|
super.invalidate()
|
||||||
|
|
|
@ -12,19 +12,19 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class CookieCatcher : AppCompatActivity() {
|
class CookieCatcher : AppCompatActivity() {
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
|
|
||||||
//get url from intent
|
//get url from intent
|
||||||
val url = intent.getStringExtra("url") ?: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
val url = intent.getStringExtra("url") ?: getString(R.string.cursed_yt)
|
||||||
val headers: Map<String, String> = intent.getSerializableExtra("headers") as? Map<String, String> ?: emptyMap()
|
val headers: Map<String, String> = intent.getSerializableExtraCompat("headers") as? Map<String, String> ?: emptyMap()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
val process = Application.getProcessName()
|
val process = Application.getProcessName()
|
||||||
|
|
|
@ -11,11 +11,11 @@ import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import ani.dantotsu.FileUrl
|
import ani.dantotsu.FileUrl
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.media.anime.AnimeNameAdapter
|
import ani.dantotsu.media.anime.AnimeNameAdapter
|
||||||
import ani.dantotsu.media.manga.ImageData
|
import ani.dantotsu.media.manga.ImageData
|
||||||
import ani.dantotsu.media.manga.MangaCache
|
import ani.dantotsu.media.manga.MangaCache
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
|
@ -59,8 +59,6 @@ class AniyomiAdapter {
|
||||||
fun aniyomiToAnimeParser(extension: AnimeExtension.Installed): DynamicAnimeParser {
|
fun aniyomiToAnimeParser(extension: AnimeExtension.Installed): DynamicAnimeParser {
|
||||||
return DynamicAnimeParser(extension)
|
return DynamicAnimeParser(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
|
@ -192,7 +190,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
// Group by season, sort within each season, and then renumber while keeping episode number 0 as is
|
// Group by season, sort within each season, and then renumber while keeping episode number 0 as is
|
||||||
val seasonGroups =
|
val seasonGroups =
|
||||||
res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
|
res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
|
||||||
seasonGroups.keys.sortedBy { it.toInt() }
|
seasonGroups.keys.sortedBy { it }
|
||||||
.flatMap { season ->
|
.flatMap { season ->
|
||||||
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
|
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
|
||||||
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
|
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
|
||||||
|
@ -209,7 +207,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sortedEpisodes.map { SEpisodeToEpisode(it) }
|
return sortedEpisodes.map { sEpisodeToEpisode(it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Exception: $e")
|
Logger.log("Exception: $e")
|
||||||
}
|
}
|
||||||
|
@ -244,7 +242,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val videos = source.getVideoList(sEpisode)
|
val videos = source.getVideoList(sEpisode)
|
||||||
videos.map { VideoToVideoServer(it) }
|
videos.map { videoToVideoServer(it) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log("Exception occurred: ${e.message}")
|
Logger.log("Exception occurred: ${e.message}")
|
||||||
emptyList()
|
emptyList()
|
||||||
|
@ -296,7 +294,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SEpisodeToEpisode(sEpisode: SEpisode): Episode {
|
private fun sEpisodeToEpisode(sEpisode: SEpisode): Episode {
|
||||||
//if the float episode number is a whole number, convert it to an int
|
//if the float episode number is a whole number, convert it to an int
|
||||||
val episodeNumberInt =
|
val episodeNumberInt =
|
||||||
if (sEpisode.episode_number % 1 == 0f) {
|
if (sEpisode.episode_number % 1 == 0f) {
|
||||||
|
@ -324,7 +322,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun VideoToVideoServer(video: Video): VideoServer {
|
private fun videoToVideoServer(video: Video): VideoServer {
|
||||||
return VideoServer(
|
return VideoServer(
|
||||||
video.quality,
|
video.quality,
|
||||||
video.url,
|
video.url,
|
||||||
|
@ -363,7 +361,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
return try {
|
return try {
|
||||||
val res = source.getChapterList(sManga)
|
val res = source.getChapterList(sManga)
|
||||||
val reversedRes = res.reversed()
|
val reversedRes = res.reversed()
|
||||||
val chapterList = reversedRes.map { SChapterToMangaChapter(it) }
|
val chapterList = reversedRes.map { sChapterToMangaChapter(it) }
|
||||||
Logger.log("chapterList size: ${chapterList.size}")
|
Logger.log("chapterList size: ${chapterList.size}")
|
||||||
Logger.log("chapterList: ${chapterList[1].title}")
|
Logger.log("chapterList: ${chapterList[1].title}")
|
||||||
Logger.log("chapterList: ${chapterList[1].description}")
|
Logger.log("chapterList: ${chapterList[1].description}")
|
||||||
|
@ -382,7 +380,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
sourceLanguage = 0
|
sourceLanguage = 0
|
||||||
extension.sources[sourceLanguage]
|
extension.sources[sourceLanguage]
|
||||||
} as? HttpSource ?: return emptyList()
|
} as? HttpSource ?: return emptyList()
|
||||||
var imageDataList: List<ImageData> = listOf()
|
val imageDataList: MutableList<ImageData> = mutableListOf()
|
||||||
val ret = coroutineScope {
|
val ret = coroutineScope {
|
||||||
try {
|
try {
|
||||||
Logger.log("source.name " + source.name)
|
Logger.log("source.name " + source.name)
|
||||||
|
@ -632,7 +630,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter {
|
private fun sChapterToMangaChapter(sChapter: SChapter): MangaChapter {
|
||||||
return MangaChapter(
|
return MangaChapter(
|
||||||
sChapter.name,
|
sChapter.name,
|
||||||
sChapter.url,
|
sChapter.url,
|
||||||
|
@ -676,8 +674,8 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||||
get() = videoServer
|
get() = videoServer
|
||||||
|
|
||||||
override suspend fun extract(): VideoContainer {
|
override suspend fun extract(): VideoContainer {
|
||||||
val vidList = listOfNotNull(videoServer.video?.let { AniVideoToSaiVideo(it) })
|
val vidList = listOfNotNull(videoServer.video?.let { aniVideoToSaiVideo(it) })
|
||||||
val subList = videoServer.video?.subtitleTracks?.map { TrackToSubtitle(it) } ?: emptyList()
|
val subList = videoServer.video?.subtitleTracks?.map { trackToSubtitle(it) } ?: emptyList()
|
||||||
|
|
||||||
return if (vidList.isNotEmpty()) {
|
return if (vidList.isNotEmpty()) {
|
||||||
VideoContainer(vidList, subList)
|
VideoContainer(vidList, subList)
|
||||||
|
@ -686,7 +684,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun AniVideoToSaiVideo(aniVideo: Video): ani.dantotsu.parsers.Video {
|
private fun aniVideoToSaiVideo(aniVideo: Video): ani.dantotsu.parsers.Video {
|
||||||
// Find the number value from the .quality string
|
// Find the number value from the .quality string
|
||||||
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
|
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
|
||||||
|
|
||||||
|
@ -789,9 +787,9 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TrackToSubtitle(track: Track): Subtitle {
|
private fun trackToSubtitle(track: Track): Subtitle {
|
||||||
//use Dispatchers.IO to make a HTTP request to determine the subtitle type
|
//use Dispatchers.IO to make a HTTP request to determine the subtitle type
|
||||||
var type: SubtitleType? = null
|
var type: SubtitleType?
|
||||||
runBlocking {
|
runBlocking {
|
||||||
type = findSubtitleType(track.url)
|
type = findSubtitleType(track.url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import ani.dantotsu.currContext
|
import ani.dantotsu.currContext
|
||||||
import ani.dantotsu.download.DownloadsManager
|
import ani.dantotsu.download.DownloadsManager
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.anime.AnimeNameAdapter
|
import ani.dantotsu.media.anime.AnimeNameAdapter
|
||||||
import ani.dantotsu.tryWithSuspend
|
import ani.dantotsu.tryWithSuspend
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
|
@ -132,16 +133,16 @@ class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() {
|
||||||
currContext()?.let {
|
currContext()?.let {
|
||||||
DownloadsManager.getDirectory(
|
DownloadsManager.getDirectory(
|
||||||
it,
|
it,
|
||||||
ani.dantotsu.download.DownloadedType.Type.ANIME,
|
MediaType.ANIME,
|
||||||
title,
|
title,
|
||||||
episode
|
episode
|
||||||
).listFiles()?.forEach {
|
).listFiles()?.forEach { file ->
|
||||||
if (it.name.contains("subtitle")) {
|
if (file.name.contains("subtitle")) {
|
||||||
return listOf(
|
return listOf(
|
||||||
Subtitle(
|
Subtitle(
|
||||||
"Downloaded Subtitle",
|
"Downloaded Subtitle",
|
||||||
Uri.fromFile(it).toString(),
|
Uri.fromFile(file).toString(),
|
||||||
determineSubtitletype(it.absolutePath)
|
determineSubtitletype(file.absolutePath)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package ani.dantotsu.parsers.novel
|
package ani.dantotsu.parsers.novel
|
||||||
|
|
||||||
import android.os.FileObserver
|
import android.os.FileObserver
|
||||||
import android.util.Log
|
|
||||||
import ani.dantotsu.parsers.novel.FileObserver.fileObserver
|
import ani.dantotsu.parsers.novel.FileObserver.fileObserver
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
import java.io.File
|
import java.io.File
|
|
@ -10,7 +10,6 @@ import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
@ -63,7 +62,7 @@ internal class NovelExtensionInstaller(private val context: Context) {
|
||||||
* @param url The url of the apk.
|
* @param url The url of the apk.
|
||||||
* @param extension The extension to install.
|
* @param extension The extension to install.
|
||||||
*/
|
*/
|
||||||
fun downloadAndInstall(url: String, extension: NovelExtension) = Observable.defer {
|
fun downloadAndInstall(url: String, extension: NovelExtension): Observable<InstallStep> = Observable.defer {
|
||||||
val pkgName = extension.pkgName
|
val pkgName = extension.pkgName
|
||||||
|
|
||||||
val oldDownload = activeDownloads[pkgName]
|
val oldDownload = activeDownloads[pkgName]
|
||||||
|
|
|
@ -5,11 +5,10 @@ import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager.GET_SIGNATURES
|
import android.content.pm.PackageManager.GET_SIGNATURES
|
||||||
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import ani.dantotsu.parsers.NovelInterface
|
import ani.dantotsu.parsers.NovelInterface
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
import dalvik.system.PathClassLoader
|
import dalvik.system.PathClassLoader
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -134,10 +133,10 @@ internal object NovelExtensionLoader {
|
||||||
}
|
}
|
||||||
Logger.log("isFileWritable: ${file.canWrite()}")
|
Logger.log("isFileWritable: ${file.canWrite()}")
|
||||||
val classLoader = PathClassLoader(file.absolutePath, null, context.classLoader)
|
val classLoader = PathClassLoader(file.absolutePath, null, context.classLoader)
|
||||||
val className =
|
val extensionClassName =
|
||||||
"some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className"
|
"some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className"
|
||||||
val loadedClass = classLoader.loadClass(className)
|
val loadedClass = classLoader.loadClass(extensionClassName)
|
||||||
val instance = loadedClass.newInstance()
|
val instance = loadedClass.getDeclaredConstructor().newInstance()
|
||||||
val novelInterfaceInstance = instance as? NovelInterface
|
val novelInterfaceInstance = instance as? NovelInterface
|
||||||
listOfNotNull(novelInterfaceInstance)
|
listOfNotNull(novelInterfaceInstance)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ import ani.dantotsu.setSlideIn
|
||||||
import ani.dantotsu.setSlideUp
|
import ani.dantotsu.setSlideUp
|
||||||
import ani.dantotsu.util.AniMarkdown.Companion.getFullAniHTML
|
import ani.dantotsu.util.AniMarkdown.Companion.getFullAniHTML
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
|
import eu.kanade.tachiyomi.util.system.getSerializableCompat
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ class ProfileFragment : Fragment() {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
activity = requireActivity() as ProfileActivity
|
activity = requireActivity() as ProfileActivity
|
||||||
|
|
||||||
user = arguments?.getSerializable("user") as Query.UserProfile
|
user = arguments?.getSerializableCompat<Query.UserProfile>("user") as Query.UserProfile
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
model.setData(user.id)
|
model.setData(user.id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import ani.dantotsu.profile.ChartBuilder.Companion.StatType
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import com.github.aachartmodel.aainfographics.aachartcreator.AAChartType
|
import com.github.aachartmodel.aainfographics.aachartcreator.AAChartType
|
||||||
import com.xwray.groupie.GroupieAdapter
|
import com.xwray.groupie.GroupieAdapter
|
||||||
|
import eu.kanade.tachiyomi.util.system.getSerializableCompat
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -48,7 +49,7 @@ class StatsFragment :
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
activity = requireActivity() as ProfileActivity
|
activity = requireActivity() as ProfileActivity
|
||||||
user = arguments?.getSerializable("user") as Query.UserProfile
|
user = arguments?.getSerializableCompat<Query.UserProfile>("user") as Query.UserProfile
|
||||||
|
|
||||||
binding.statisticList.adapter = adapter
|
binding.statisticList.adapter = adapter
|
||||||
binding.statisticList.recycledViewPool.setMaxRecycledViews(0, 0)
|
binding.statisticList.recycledViewPool.setMaxRecycledViews(0, 0)
|
||||||
|
@ -95,7 +96,7 @@ class StatsFragment :
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stats.removeAll(
|
stats.removeAll(
|
||||||
stats.filter { it?.id == Anilist.userid }
|
stats.filter { it?.id == Anilist.userid }.toSet()
|
||||||
)
|
)
|
||||||
loadStats(type == MediaType.ANIME)
|
loadStats(type == MediaType.ANIME)
|
||||||
}
|
}
|
||||||
|
@ -445,6 +446,7 @@ class StatsFragment :
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
chartPackets.clear()
|
chartPackets.clear()
|
||||||
chartPackets.addAll(standardizedPackets)
|
chartPackets.addAll(standardizedPackets)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val genreChart = ChartBuilder.buildChart(
|
val genreChart = ChartBuilder.buildChart(
|
||||||
activity,
|
activity,
|
||||||
ChartType.TwoDimensional,
|
ChartType.TwoDimensional,
|
||||||
|
@ -499,6 +501,7 @@ class StatsFragment :
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
chartPackets.clear()
|
chartPackets.clear()
|
||||||
chartPackets.addAll(standardizedPackets)
|
chartPackets.addAll(standardizedPackets)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val tagChart = ChartBuilder.buildChart(
|
val tagChart = ChartBuilder.buildChart(
|
||||||
activity,
|
activity,
|
||||||
ChartType.TwoDimensional,
|
ChartType.TwoDimensional,
|
||||||
|
@ -553,6 +556,7 @@ class StatsFragment :
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
chartPackets.clear()
|
chartPackets.clear()
|
||||||
chartPackets.addAll(standardizedPackets)
|
chartPackets.addAll(standardizedPackets)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val countryChart = ChartBuilder.buildChart(
|
val countryChart = ChartBuilder.buildChart(
|
||||||
activity,
|
activity,
|
||||||
ChartType.OneDimensional,
|
ChartType.OneDimensional,
|
||||||
|
@ -609,6 +613,7 @@ class StatsFragment :
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
chartPackets.clear()
|
chartPackets.clear()
|
||||||
chartPackets.addAll(standardizedPackets)
|
chartPackets.addAll(standardizedPackets)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val voiceActorsChart = ChartBuilder.buildChart(
|
val voiceActorsChart = ChartBuilder.buildChart(
|
||||||
activity,
|
activity,
|
||||||
ChartType.TwoDimensional,
|
ChartType.TwoDimensional,
|
||||||
|
@ -663,6 +668,7 @@ class StatsFragment :
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
chartPackets.clear()
|
chartPackets.clear()
|
||||||
chartPackets.addAll(standardizedPackets)
|
chartPackets.addAll(standardizedPackets)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val studioChart = ChartBuilder.buildChart(
|
val studioChart = ChartBuilder.buildChart(
|
||||||
activity,
|
activity,
|
||||||
ChartType.TwoDimensional,
|
ChartType.TwoDimensional,
|
||||||
|
@ -720,6 +726,7 @@ class StatsFragment :
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
chartPackets.clear()
|
chartPackets.clear()
|
||||||
chartPackets.addAll(standardizedPackets)
|
chartPackets.addAll(standardizedPackets)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val staffChart = ChartBuilder.buildChart(
|
val staffChart = ChartBuilder.buildChart(
|
||||||
activity,
|
activity,
|
||||||
ChartType.TwoDimensional,
|
ChartType.TwoDimensional,
|
||||||
|
|
|
@ -61,7 +61,7 @@ class FeedActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
binding.listBack.setOnClickListener {
|
binding.listBack.setOnClickListener {
|
||||||
onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ class NotificationActivity : AppCompatActivity() {
|
||||||
binding.followerGrid.visibility = ViewGroup.GONE
|
binding.followerGrid.visibility = ViewGroup.GONE
|
||||||
binding.followerList.visibility = ViewGroup.GONE
|
binding.followerList.visibility = ViewGroup.GONE
|
||||||
binding.listBack.setOnClickListener {
|
binding.listBack.setOnClickListener {
|
||||||
onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
binding.listProgressBar.visibility = ViewGroup.VISIBLE
|
binding.listProgressBar.visibility = ViewGroup.VISIBLE
|
||||||
val activityId = intent.getIntExtra("activityId", -1)
|
val activityId = intent.getIntExtra("activityId", -1)
|
||||||
|
|
|
@ -46,7 +46,6 @@ import ani.dantotsu.databinding.ActivitySettingsAboutBinding
|
||||||
import ani.dantotsu.databinding.ActivitySettingsAccountsBinding
|
import ani.dantotsu.databinding.ActivitySettingsAccountsBinding
|
||||||
import ani.dantotsu.databinding.ActivitySettingsAnimeBinding
|
import ani.dantotsu.databinding.ActivitySettingsAnimeBinding
|
||||||
import ani.dantotsu.databinding.ActivitySettingsBinding
|
import ani.dantotsu.databinding.ActivitySettingsBinding
|
||||||
import ani.dantotsu.download.DownloadedType
|
|
||||||
import ani.dantotsu.databinding.ActivitySettingsCommonBinding
|
import ani.dantotsu.databinding.ActivitySettingsCommonBinding
|
||||||
import ani.dantotsu.databinding.ActivitySettingsExtensionsBinding
|
import ani.dantotsu.databinding.ActivitySettingsExtensionsBinding
|
||||||
import ani.dantotsu.databinding.ActivitySettingsMangaBinding
|
import ani.dantotsu.databinding.ActivitySettingsMangaBinding
|
||||||
|
@ -57,7 +56,7 @@ import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||||
import ani.dantotsu.downloadsPermission
|
import ani.dantotsu.downloadsPermission
|
||||||
import ani.dantotsu.initActivity
|
import ani.dantotsu.initActivity
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.navBarHeight
|
import ani.dantotsu.navBarHeight
|
||||||
import ani.dantotsu.notifications.TaskScheduler
|
import ani.dantotsu.notifications.TaskScheduler
|
||||||
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
|
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
|
||||||
|
@ -81,6 +80,7 @@ import ani.dantotsu.startMainActivity
|
||||||
import ani.dantotsu.statusBarHeight
|
import ani.dantotsu.statusBarHeight
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import ani.dantotsu.toast
|
import ani.dantotsu.toast
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import eltos.simpledialogfragment.SimpleDialog
|
import eltos.simpledialogfragment.SimpleDialog
|
||||||
|
@ -151,13 +151,13 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
salt
|
salt
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
toast("Incorrect password")
|
toast(getString(R.string.incorrect_password))
|
||||||
return@passwordAlertDialog
|
return@passwordAlertDialog
|
||||||
}
|
}
|
||||||
if (PreferencePackager.unpack(decryptedJson))
|
if (PreferencePackager.unpack(decryptedJson))
|
||||||
restartApp()
|
restartApp()
|
||||||
} else {
|
} else {
|
||||||
toast("Password cannot be empty")
|
toast(getString(R.string.password_cannot_be_empty))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (name.endsWith(".ani")) {
|
} else if (name.endsWith(".ani")) {
|
||||||
|
@ -165,11 +165,11 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
if (PreferencePackager.unpack(decryptedJson))
|
if (PreferencePackager.unpack(decryptedJson))
|
||||||
restartApp()
|
restartApp()
|
||||||
} else {
|
} else {
|
||||||
toast("Unknown file type")
|
toast(getString(R.string.unknown_file_type))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
toast("Error importing settings")
|
toast(getString(R.string.error_importing_settings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -254,7 +254,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
}
|
}
|
||||||
|
|
||||||
val tag = "colorPicker"
|
val tag = "colorPicker"
|
||||||
CustomColorDialog().title("Custom Theme")
|
CustomColorDialog().title(R.string.custom_theme)
|
||||||
.colorPreset(originalColor)
|
.colorPreset(originalColor)
|
||||||
.colors(this, SimpleColorDialog.MATERIAL_COLOR_PALLET)
|
.colors(this, SimpleColorDialog.MATERIAL_COLOR_PALLET)
|
||||||
.allowCustom(true)
|
.allowCustom(true)
|
||||||
|
@ -271,7 +271,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
|
|
||||||
val managers = arrayOf("Default", "1DM", "ADM")
|
val managers = arrayOf("Default", "1DM", "ADM")
|
||||||
val downloadManagerDialog =
|
val downloadManagerDialog =
|
||||||
AlertDialog.Builder(this, R.style.MyPopup).setTitle("Download Manager")
|
AlertDialog.Builder(this, R.style.MyPopup).setTitle(R.string.download_manager)
|
||||||
var downloadManager: Int = PrefManager.getVal(PrefName.DownloadManager)
|
var downloadManager: Int = PrefManager.getVal(PrefName.DownloadManager)
|
||||||
bindingCommon.settingsDownloadManager.setOnClickListener {
|
bindingCommon.settingsDownloadManager.setOnClickListener {
|
||||||
val dialog = downloadManagerDialog.setSingleChoiceItems(
|
val dialog = downloadManagerDialog.setSingleChoiceItems(
|
||||||
|
@ -291,7 +291,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
val filteredLocations = Location.entries.filter { it.exportable }
|
val filteredLocations = Location.entries.filter { it.exportable }
|
||||||
selectedArray.addAll(List(filteredLocations.size - 1) { false })
|
selectedArray.addAll(List(filteredLocations.size - 1) { false })
|
||||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||||
.setTitle("Import/Export Settings")
|
.setTitle(R.string.import_export_settings)
|
||||||
.setMultiChoiceItems(
|
.setMultiChoiceItems(
|
||||||
filteredLocations.map { it.name }.toTypedArray(),
|
filteredLocations.map { it.name }.toTypedArray(),
|
||||||
selectedArray.toBooleanArray()
|
selectedArray.toBooleanArray()
|
||||||
|
@ -346,7 +346,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
.setMessage(getString(R.string.purge_confirm, getString(R.string.anime)))
|
.setMessage(getString(R.string.purge_confirm, getString(R.string.anime)))
|
||||||
.setPositiveButton(R.string.yes) { dialog, _ ->
|
.setPositiveButton(R.string.yes) { dialog, _ ->
|
||||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||||
downloadsManager.purgeDownloads(DownloadedType.Type.ANIME)
|
downloadsManager.purgeDownloads(MediaType.ANIME)
|
||||||
DownloadService.sendRemoveAllDownloads(
|
DownloadService.sendRemoveAllDownloads(
|
||||||
this,
|
this,
|
||||||
ExoplayerDownloadService::class.java,
|
ExoplayerDownloadService::class.java,
|
||||||
|
@ -354,7 +354,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
)
|
)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton("No") { dialog, _ ->
|
.setNegativeButton(R.string.no) { dialog, _ ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.create()
|
.create()
|
||||||
|
@ -368,10 +368,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
.setMessage(getString(R.string.purge_confirm, getString(R.string.manga)))
|
.setMessage(getString(R.string.purge_confirm, getString(R.string.manga)))
|
||||||
.setPositiveButton(R.string.yes) { dialog, _ ->
|
.setPositiveButton(R.string.yes) { dialog, _ ->
|
||||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||||
downloadsManager.purgeDownloads(DownloadedType.Type.MANGA)
|
downloadsManager.purgeDownloads(MediaType.MANGA)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton("No") { dialog, _ ->
|
.setNegativeButton(R.string.no) { dialog, _ ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.create()
|
.create()
|
||||||
|
@ -385,10 +385,10 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
.setMessage(getString(R.string.purge_confirm, getString(R.string.novels)))
|
.setMessage(getString(R.string.purge_confirm, getString(R.string.novels)))
|
||||||
.setPositiveButton(R.string.yes) { dialog, _ ->
|
.setPositiveButton(R.string.yes) { dialog, _ ->
|
||||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||||
downloadsManager.purgeDownloads(DownloadedType.Type.NOVEL)
|
downloadsManager.purgeDownloads(MediaType.NOVEL)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton("No") { dialog, _ ->
|
.setNegativeButton(R.string.no) { dialog, _ ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.create()
|
.create()
|
||||||
|
@ -423,16 +423,16 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
|
val alertDialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||||
.setTitle("User Agent")
|
.setTitle("User Agent")
|
||||||
.setView(dialogView)
|
.setView(dialogView)
|
||||||
.setPositiveButton("OK") { dialog, _ ->
|
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
|
||||||
PrefManager.setVal(PrefName.DefaultUserAgent, editText.text.toString())
|
PrefManager.setVal(PrefName.DefaultUserAgent, editText.text.toString())
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.setNeutralButton("Reset") { dialog, _ ->
|
.setNeutralButton(getString(R.string.reset)) { dialog, _ ->
|
||||||
PrefManager.removeVal(PrefName.DefaultUserAgent)
|
PrefManager.removeVal(PrefName.DefaultUserAgent)
|
||||||
editText.setText("")
|
editText.setText("")
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton("Cancel") { dialog, _ ->
|
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.create()
|
.create()
|
||||||
|
@ -613,7 +613,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
it.pop()
|
it.pop()
|
||||||
}
|
}
|
||||||
openLinkInBrowser("https://www.buymeacoffee.com/rebelonion")
|
openLinkInBrowser(getString(R.string.coffee))
|
||||||
}
|
}
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
binding.settingBuyMeCoffee.pop()
|
binding.settingBuyMeCoffee.pop()
|
||||||
|
@ -905,7 +905,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
if (MAL.token != null) {
|
if (MAL.token != null) {
|
||||||
bindingAccounts.settingsMALLogin.setText(R.string.logout)
|
bindingAccounts.settingsMALLogin.setText(R.string.logout)
|
||||||
bindingAccounts.settingsMALLogin.setOnClickListener {
|
bindingAccounts.settingsMALLogin.setOnClickListener {
|
||||||
MAL.removeSavedToken(it.context)
|
MAL.removeSavedToken()
|
||||||
restartMainActivity.isEnabled = true
|
restartMainActivity.isEnabled = true
|
||||||
reload()
|
reload()
|
||||||
}
|
}
|
||||||
|
@ -1060,7 +1060,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
box?.setSingleLine()
|
box?.setSingleLine()
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||||
.setTitle("Enter Password")
|
.setTitle(getString(R.string.enter_password))
|
||||||
.setView(dialogView)
|
.setView(dialogView)
|
||||||
.setPositiveButton("OK", null)
|
.setPositiveButton("OK", null)
|
||||||
.setNegativeButton("Cancel") { dialog, _ ->
|
.setNegativeButton("Cancel") { dialog, _ ->
|
||||||
|
@ -1076,7 +1076,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
callback(password)
|
callback(password)
|
||||||
} else {
|
} else {
|
||||||
toast("Password cannot be empty")
|
toast(getString(R.string.password_cannot_be_empty))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
box?.setOnEditorActionListener { _, actionId, _ ->
|
box?.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
@ -1090,7 +1090,7 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
|
||||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
||||||
subtitleTextView?.visibility = View.VISIBLE
|
subtitleTextView?.visibility = View.VISIBLE
|
||||||
if (!isExporting)
|
if (!isExporting)
|
||||||
subtitleTextView?.text = "Enter your password to decrypt the file"
|
subtitleTextView?.text = getString(R.string.enter_password_to_decrypt_file)
|
||||||
|
|
||||||
|
|
||||||
dialog.window?.setDimAmount(0.8f)
|
dialog.window?.setDimAmount(0.8f)
|
||||||
|
|
|
@ -12,7 +12,6 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import ani.dantotsu.BottomSheetDialogFragment
|
import ani.dantotsu.BottomSheetDialogFragment
|
||||||
import ani.dantotsu.MainActivity
|
import ani.dantotsu.MainActivity
|
||||||
import ani.dantotsu.profile.ProfileActivity
|
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
import ani.dantotsu.connections.anilist.Anilist
|
import ani.dantotsu.connections.anilist.Anilist
|
||||||
import ani.dantotsu.databinding.BottomSheetSettingsBinding
|
import ani.dantotsu.databinding.BottomSheetSettingsBinding
|
||||||
|
@ -25,13 +24,15 @@ import ani.dantotsu.home.MangaFragment
|
||||||
import ani.dantotsu.home.NoInternet
|
import ani.dantotsu.home.NoInternet
|
||||||
import ani.dantotsu.incognitoNotification
|
import ani.dantotsu.incognitoNotification
|
||||||
import ani.dantotsu.loadImage
|
import ani.dantotsu.loadImage
|
||||||
import ani.dantotsu.profile.activity.NotificationActivity
|
|
||||||
import ani.dantotsu.offline.OfflineFragment
|
import ani.dantotsu.offline.OfflineFragment
|
||||||
|
import ani.dantotsu.profile.ProfileActivity
|
||||||
import ani.dantotsu.profile.activity.FeedActivity
|
import ani.dantotsu.profile.activity.FeedActivity
|
||||||
|
import ani.dantotsu.profile.activity.NotificationActivity
|
||||||
import ani.dantotsu.setSafeOnClickListener
|
import ani.dantotsu.setSafeOnClickListener
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.settings.saving.PrefName
|
import ani.dantotsu.settings.saving.PrefName
|
||||||
import ani.dantotsu.startMainActivity
|
import ani.dantotsu.startMainActivity
|
||||||
|
import eu.kanade.tachiyomi.util.system.getSerializableCompat
|
||||||
import java.util.Timer
|
import java.util.Timer
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
|
@ -42,7 +43,7 @@ class SettingsDialogFragment : BottomSheetDialogFragment() {
|
||||||
private lateinit var pageType: PageType
|
private lateinit var pageType: PageType
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
pageType = arguments?.getSerializable("pageType") as? PageType ?: PageType.HOME
|
pageType = arguments?.getSerializableCompat("pageType") as? PageType ?: PageType.HOME
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
|
|
@ -41,13 +41,14 @@ import kotlinx.coroutines.withContext
|
||||||
class NovelExtensionsViewModelFactory(
|
class NovelExtensionsViewModelFactory(
|
||||||
private val novelExtensionManager: NovelExtensionManager
|
private val novelExtensionManager: NovelExtensionManager
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
return NovelExtensionsViewModel(novelExtensionManager) as T
|
return NovelExtensionsViewModel(novelExtensionManager) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NovelExtensionsViewModel(
|
class NovelExtensionsViewModel(
|
||||||
private val novelExtensionManager: NovelExtensionManager
|
novelExtensionManager: NovelExtensionManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val searchQuery = MutableStateFlow("")
|
private val searchQuery = MutableStateFlow("")
|
||||||
private var currentPagingSource: NovelExtensionPagingSource? = null
|
private var currentPagingSource: NovelExtensionPagingSource? = null
|
||||||
|
@ -102,21 +103,20 @@ class NovelExtensionPagingSource(
|
||||||
} else {
|
} else {
|
||||||
availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
|
availableExtensions.filter { it.name.contains(query, ignoreCase = true) }
|
||||||
}
|
}
|
||||||
val filternfsw = filteredExtensions
|
|
||||||
/*val filternfsw = if(isNsfwEnabled) { currently not implemented
|
/*val filternfsw = if(isNsfwEnabled) { currently not implemented
|
||||||
filteredExtensions
|
filteredExtensions
|
||||||
} else {
|
} else {
|
||||||
filteredExtensions.filterNot { it.isNsfw }
|
filteredExtensions.filterNot { it.isNsfw }
|
||||||
}*/
|
}*/
|
||||||
return try {
|
return try {
|
||||||
val sublist = filternfsw.subList(
|
val sublist = filteredExtensions.subList(
|
||||||
fromIndex = position,
|
fromIndex = position,
|
||||||
toIndex = (position + params.loadSize).coerceAtMost(filternfsw.size)
|
toIndex = (position + params.loadSize).coerceAtMost(filteredExtensions.size)
|
||||||
)
|
)
|
||||||
LoadResult.Page(
|
LoadResult.Page(
|
||||||
data = sublist,
|
data = sublist,
|
||||||
prevKey = if (position == 0) null else position - params.loadSize,
|
prevKey = if (position == 0) null else position - params.loadSize,
|
||||||
nextKey = if (position + params.loadSize >= filternfsw.size) null else position + params.loadSize
|
nextKey = if (position + params.loadSize >= filteredExtensions.size) null else position + params.loadSize
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LoadResult.Error(e)
|
LoadResult.Error(e)
|
||||||
|
|
|
@ -6,7 +6,7 @@ import ani.dantotsu.util.ColorEditor.Companion.toHexColor
|
||||||
class AniMarkdown { //istg anilist has the worst api
|
class AniMarkdown { //istg anilist has the worst api
|
||||||
companion object {
|
companion object {
|
||||||
private fun convertNestedImageToHtml(markdown: String): String {
|
private fun convertNestedImageToHtml(markdown: String): String {
|
||||||
val regex = """\[\!\[(.*?)\]\((.*?)\)\]\((.*?)\)""".toRegex()
|
val regex = """\[!\[(.*?)]\((.*?)\)]\((.*?)\)""".toRegex()
|
||||||
return regex.replace(markdown) { matchResult ->
|
return regex.replace(markdown) { matchResult ->
|
||||||
val altText = matchResult.groupValues[1]
|
val altText = matchResult.groupValues[1]
|
||||||
val imageUrl = matchResult.groupValues[2]
|
val imageUrl = matchResult.groupValues[2]
|
||||||
|
@ -16,7 +16,7 @@ class AniMarkdown { //istg anilist has the worst api
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun convertImageToHtml(markdown: String): String {
|
private fun convertImageToHtml(markdown: String): String {
|
||||||
val regex = """\!\[(.*?)\]\((.*?)\)""".toRegex()
|
val regex = """!\[(.*?)]\((.*?)\)""".toRegex()
|
||||||
return regex.replace(markdown) { matchResult ->
|
return regex.replace(markdown) { matchResult ->
|
||||||
val altText = matchResult.groupValues[1]
|
val altText = matchResult.groupValues[1]
|
||||||
val imageUrl = matchResult.groupValues[2]
|
val imageUrl = matchResult.groupValues[2]
|
||||||
|
@ -25,7 +25,7 @@ class AniMarkdown { //istg anilist has the worst api
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun convertLinkToHtml(markdown: String): String {
|
private fun convertLinkToHtml(markdown: String): String {
|
||||||
val regex = """\[(.*?)\]\((.*?)\)""".toRegex()
|
val regex = """\[(.*?)]\((.*?)\)""".toRegex()
|
||||||
return regex.replace(markdown) { matchResult ->
|
return regex.replace(markdown) { matchResult ->
|
||||||
val linkText = matchResult.groupValues[1]
|
val linkText = matchResult.groupValues[1]
|
||||||
val linkUrl = matchResult.groupValues[2]
|
val linkUrl = matchResult.groupValues[2]
|
||||||
|
@ -50,7 +50,7 @@ class AniMarkdown { //istg anilist has the worst api
|
||||||
private fun underlineToHtml(html: String): String {
|
private fun underlineToHtml(html: String): String {
|
||||||
return html.replace("(?s)___(.*?)___".toRegex(), "<br><em><strong>$1</strong></em><br>")
|
return html.replace("(?s)___(.*?)___".toRegex(), "<br><em><strong>$1</strong></em><br>")
|
||||||
.replace("(?s)__(.*?)__".toRegex(), "<br><strong>$1</strong><br>")
|
.replace("(?s)__(.*?)__".toRegex(), "<br><strong>$1</strong><br>")
|
||||||
.replace("(?s)[\\s]+_([^_]+)_[\\s]+".toRegex(), "<em>$1</em>")
|
.replace("(?s)\\s+_([^_]+)_\\s+".toRegex(), "<em>$1</em>")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBasicAniHTML(html: String): String {
|
fun getBasicAniHTML(html: String): String {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package ani.dantotsu.widgets
|
package ani.dantotsu.widgets
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
@ -12,7 +11,7 @@ import java.io.InputStream
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
class CurrentlyAiringRemoteViewsFactory(private val context: Context, intent: Intent) :
|
class CurrentlyAiringRemoteViewsFactory(private val context: Context) :
|
||||||
RemoteViewsService.RemoteViewsFactory {
|
RemoteViewsService.RemoteViewsFactory {
|
||||||
private var widgetItems = mutableListOf<WidgetItem>()
|
private var widgetItems = mutableListOf<WidgetItem>()
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,6 @@ import ani.dantotsu.util.Logger
|
||||||
class CurrentlyAiringRemoteViewsService : RemoteViewsService() {
|
class CurrentlyAiringRemoteViewsService : RemoteViewsService() {
|
||||||
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
|
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
|
||||||
Logger.log("CurrentlyAiringRemoteViewsFactory onGetViewFactory")
|
Logger.log("CurrentlyAiringRemoteViewsFactory onGetViewFactory")
|
||||||
return CurrentlyAiringRemoteViewsFactory(applicationContext, intent)
|
return CurrentlyAiringRemoteViewsFactory(applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ class ExtensionInstallerPreference(
|
||||||
|
|
||||||
|
|
||||||
val entries
|
val entries
|
||||||
get() = ExtensionInstaller.values().run {
|
get() = ExtensionInstaller.entries.toTypedArray().run {
|
||||||
if (context.hasMiuiPackageInstaller) {
|
if (context.hasMiuiPackageInstaller) {
|
||||||
filter { it != ExtensionInstaller.PACKAGEINSTALLER }
|
filter { it != ExtensionInstaller.PACKAGEINSTALLER }
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -58,8 +58,7 @@ interface AnimeSource {
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
suspend fun getVideoList(episode: SEpisode): List<Video> {
|
suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||||
val list = fetchVideoList(episode).awaitSingle()
|
return fetchVideoList(episode).awaitSingle()
|
||||||
return list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
|
|
|
@ -10,10 +10,11 @@ import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
|
import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
|
||||||
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallReceiver
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||||
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||||
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
@ -51,7 +52,7 @@ class AnimeExtensionManager(
|
||||||
/**
|
/**
|
||||||
* The installer which installs, updates and uninstalls the anime extensions.
|
* The installer which installs, updates and uninstalls the anime extensions.
|
||||||
*/
|
*/
|
||||||
private val installer by lazy { AnimeExtensionInstaller(context) }
|
private val installer by lazy { ExtensionInstaller(context) }
|
||||||
|
|
||||||
private val iconMap = mutableMapOf<String, Drawable>()
|
private val iconMap = mutableMapOf<String, Drawable>()
|
||||||
|
|
||||||
|
@ -92,14 +93,14 @@ class AnimeExtensionManager(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initAnimeExtensions()
|
initAnimeExtensions()
|
||||||
AnimeExtensionInstallReceiver(AnimeInstallationListener()).register(context)
|
ExtensionInstallReceiver().setAnimeListener(InstallationListener()).register(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads and registers the installed animeextensions.
|
* Loads and registers the installed animeextensions.
|
||||||
*/
|
*/
|
||||||
private fun initAnimeExtensions() {
|
private fun initAnimeExtensions() {
|
||||||
val animeextensions = AnimeExtensionLoader.loadExtensions(context)
|
val animeextensions = ExtensionLoader.loadAnimeExtensions(context)
|
||||||
|
|
||||||
_installedAnimeExtensionsFlow.value = animeextensions
|
_installedAnimeExtensionsFlow.value = animeextensions
|
||||||
.filterIsInstance<AnimeLoadResult.Success>()
|
.filterIsInstance<AnimeLoadResult.Success>()
|
||||||
|
@ -254,12 +255,13 @@ class AnimeExtensionManager(
|
||||||
*
|
*
|
||||||
* @param signature The signature to whitelist.
|
* @param signature The signature to whitelist.
|
||||||
*/
|
*/
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
fun trustSignature(signature: String) {
|
fun trustSignature(signature: String) {
|
||||||
val untrustedSignatures =
|
val untrustedSignatures =
|
||||||
_untrustedAnimeExtensionsFlow.value.map { it.signatureHash }.toSet()
|
_untrustedAnimeExtensionsFlow.value.map { it.signatureHash }.toSet()
|
||||||
if (signature !in untrustedSignatures) return
|
if (signature !in untrustedSignatures) return
|
||||||
|
|
||||||
AnimeExtensionLoader.trustedSignatures += signature
|
ExtensionLoader.trustedSignaturesAnime += signature
|
||||||
preferences.trustedSignatures() += signature
|
preferences.trustedSignatures() += signature
|
||||||
|
|
||||||
val nowTrustedAnimeExtensions =
|
val nowTrustedAnimeExtensions =
|
||||||
|
@ -271,7 +273,7 @@ class AnimeExtensionManager(
|
||||||
nowTrustedAnimeExtensions
|
nowTrustedAnimeExtensions
|
||||||
.map { animeextension ->
|
.map { animeextension ->
|
||||||
async {
|
async {
|
||||||
AnimeExtensionLoader.loadExtensionFromPkgName(
|
ExtensionLoader.loadAnimeExtensionFromPkgName(
|
||||||
ctx,
|
ctx,
|
||||||
animeextension.pkgName
|
animeextension.pkgName
|
||||||
)
|
)
|
||||||
|
@ -333,7 +335,7 @@ class AnimeExtensionManager(
|
||||||
/**
|
/**
|
||||||
* Listener which receives events of the anime extensions being installed, updated or removed.
|
* Listener which receives events of the anime extensions being installed, updated or removed.
|
||||||
*/
|
*/
|
||||||
private inner class AnimeInstallationListener : AnimeExtensionInstallReceiver.Listener {
|
private inner class InstallationListener : ExtensionInstallReceiver.AnimeListener {
|
||||||
|
|
||||||
override fun onExtensionInstalled(extension: AnimeExtension.Installed) {
|
override fun onExtensionInstalled(extension: AnimeExtension.Installed) {
|
||||||
registerNewExtension(extension.withUpdateCheck())
|
registerNewExtension(extension.withUpdateCheck())
|
||||||
|
|
|
@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
|
import eu.kanade.tachiyomi.extension.anime.model.AvailableAnimeSources
|
||||||
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
@ -87,7 +87,7 @@ internal class AnimeExtensionGithubApi {
|
||||||
findExtensions().also { lastExtCheck.set(Date().time) }
|
findExtensions().also { lastExtCheck.set(Date().time) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val installedExtensions = AnimeExtensionLoader.loadExtensions(context)
|
val installedExtensions = ExtensionLoader.loadAnimeExtensions(context)
|
||||||
.filterIsInstance<AnimeLoadResult.Success>()
|
.filterIsInstance<AnimeLoadResult.Success>()
|
||||||
.map { it.extension }
|
.map { it.extension }
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ internal class AnimeExtensionGithubApi {
|
||||||
return this
|
return this
|
||||||
.filter {
|
.filter {
|
||||||
val libVersion = it.extractLibVersion()
|
val libVersion = it.extractLibVersion()
|
||||||
libVersion >= AnimeExtensionLoader.LIB_VERSION_MIN && libVersion <= AnimeExtensionLoader.LIB_VERSION_MAX
|
libVersion >= ExtensionLoader.ANIME_LIB_VERSION_MIN && libVersion <= ExtensionLoader.ANIME_LIB_VERSION_MAX
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
AnimeExtension.Available(
|
AnimeExtension.Available(
|
||||||
|
|
|
@ -3,5 +3,5 @@ package eu.kanade.tachiyomi.extension.anime.model
|
||||||
sealed class AnimeLoadResult {
|
sealed class AnimeLoadResult {
|
||||||
class Success(val extension: AnimeExtension.Installed) : AnimeLoadResult()
|
class Success(val extension: AnimeExtension.Installed) : AnimeLoadResult()
|
||||||
class Untrusted(val extension: AnimeExtension.Untrusted) : AnimeLoadResult()
|
class Untrusted(val extension: AnimeExtension.Untrusted) : AnimeLoadResult()
|
||||||
object Error : AnimeLoadResult()
|
data object Error : AnimeLoadResult()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.anime.util
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import ani.dantotsu.themes.ThemeManager
|
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
|
||||||
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity used to install extensions, because we can only receive the result of the installation
|
|
||||||
* with [startActivityForResult], which we need to update the UI.
|
|
||||||
*/
|
|
||||||
class AnimeExtensionInstallActivity : Activity() {
|
|
||||||
|
|
||||||
// MIUI package installer bug workaround
|
|
||||||
private var ignoreUntil = 0L
|
|
||||||
private var ignoreResult = false
|
|
||||||
private var hasIgnoredResult = false
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
ThemeManager(this).applyTheme()
|
|
||||||
|
|
||||||
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
|
||||||
.setDataAndType(intent.data, intent.type)
|
|
||||||
.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
|
||||||
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
|
|
||||||
if (hasMiuiPackageInstaller) {
|
|
||||||
ignoreResult = true
|
|
||||||
ignoreUntil = System.nanoTime() + 1.seconds.inWholeNanoseconds
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
startActivityForResult(installIntent, INSTALL_REQUEST_CODE)
|
|
||||||
} catch (error: Exception) {
|
|
||||||
// Either install package can't be found (probably bots) or there's a security exception
|
|
||||||
// with the download manager. Nothing we can workaround.
|
|
||||||
toast(error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
if (ignoreResult && System.nanoTime() < ignoreUntil) {
|
|
||||||
hasIgnoredResult = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (requestCode == INSTALL_REQUEST_CODE) {
|
|
||||||
checkInstallationResult(resultCode)
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
if (hasIgnoredResult) {
|
|
||||||
checkInstallationResult(RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkInstallationResult(resultCode: Int) {
|
|
||||||
val downloadId = intent.extras!!.getLong(AnimeExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
|
||||||
val extensionManager = Injekt.get<AnimeExtensionManager>()
|
|
||||||
val newStep = when (resultCode) {
|
|
||||||
RESULT_OK -> InstallStep.Installed
|
|
||||||
RESULT_CANCELED -> InstallStep.Idle
|
|
||||||
else -> InstallStep.Error
|
|
||||||
}
|
|
||||||
extensionManager.updateInstallStep(downloadId, newStep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val INSTALL_REQUEST_CODE = 500
|
|
|
@ -1,132 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.anime.util
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
|
||||||
import kotlinx.coroutines.CoroutineStart
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import tachiyomi.core.util.lang.launchNow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
|
|
||||||
* notifies the given [listener] when the package is an extension.
|
|
||||||
*
|
|
||||||
* @param listener The listener that should be notified of extension installation events.
|
|
||||||
*/
|
|
||||||
internal class AnimeExtensionInstallReceiver(private val listener: Listener) :
|
|
||||||
BroadcastReceiver() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers this broadcast receiver
|
|
||||||
*/
|
|
||||||
fun register(context: Context) {
|
|
||||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the intent filter this receiver should subscribe to.
|
|
||||||
*/
|
|
||||||
private val filter
|
|
||||||
get() = IntentFilter().apply {
|
|
||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
|
||||||
addDataScheme("package")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when one of the events of the [filter] is received. When the package is an extension,
|
|
||||||
* it's loaded in background and it notifies the [listener] when finished.
|
|
||||||
*/
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
|
||||||
if (intent == null) return
|
|
||||||
|
|
||||||
when (intent.action) {
|
|
||||||
Intent.ACTION_PACKAGE_ADDED -> {
|
|
||||||
if (isReplacing(intent)) return
|
|
||||||
|
|
||||||
launchNow {
|
|
||||||
when (val result = getExtensionFromIntent(context, intent)) {
|
|
||||||
is AnimeLoadResult.Success -> listener.onExtensionInstalled(result.extension)
|
|
||||||
|
|
||||||
is AnimeLoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
|
||||||
launchNow {
|
|
||||||
when (val result = getExtensionFromIntent(context, intent)) {
|
|
||||||
is AnimeLoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
|
||||||
// Not needed as a package can't be upgraded if the signature is different
|
|
||||||
// is LoadResult.Untrusted -> {}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
|
||||||
if (isReplacing(intent)) return
|
|
||||||
|
|
||||||
val pkgName = getPackageNameFromIntent(intent)
|
|
||||||
if (pkgName != null) {
|
|
||||||
listener.onPackageUninstalled(pkgName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this package is performing an update.
|
|
||||||
*
|
|
||||||
* @param intent The intent that triggered the event.
|
|
||||||
*/
|
|
||||||
private fun isReplacing(intent: Intent): Boolean {
|
|
||||||
return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the extension triggered by the given intent.
|
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
* @param intent The intent containing the package name of the extension.
|
|
||||||
*/
|
|
||||||
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): AnimeLoadResult {
|
|
||||||
val pkgName = getPackageNameFromIntent(intent)
|
|
||||||
if (pkgName == null) {
|
|
||||||
Logger.log("Package name not found")
|
|
||||||
return AnimeLoadResult.Error
|
|
||||||
}
|
|
||||||
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
|
|
||||||
AnimeExtensionLoader.loadExtensionFromPkgName(
|
|
||||||
context,
|
|
||||||
pkgName,
|
|
||||||
)
|
|
||||||
}.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the package name of the installed, updated or removed application.
|
|
||||||
*/
|
|
||||||
private fun getPackageNameFromIntent(intent: Intent?): String? {
|
|
||||||
return intent?.data?.encodedSchemeSpecificPart ?: return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener that receives extension installation events.
|
|
||||||
*/
|
|
||||||
interface Listener {
|
|
||||||
fun onExtensionInstalled(extension: AnimeExtension.Installed)
|
|
||||||
fun onExtensionUpdated(extension: AnimeExtension.Installed)
|
|
||||||
fun onExtensionUntrusted(extension: AnimeExtension.Untrusted)
|
|
||||||
fun onPackageUninstalled(pkgName: String)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.anime.util
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.ServiceInfo
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
import ani.dantotsu.R
|
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.installer.PackageInstallerInstallerAnime
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
|
||||||
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
|
||||||
|
|
||||||
class AnimeExtensionInstallService : Service() {
|
|
||||||
|
|
||||||
private var installer: InstallerAnime? = null
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
|
|
||||||
setSmallIcon(R.drawable.ic_download_24)
|
|
||||||
setAutoCancel(false)
|
|
||||||
setOngoing(true)
|
|
||||||
setShowWhen(false)
|
|
||||||
setContentTitle("Installing Anime Extension...")
|
|
||||||
setProgress(100, 100, true)
|
|
||||||
}.build()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
startForeground(
|
|
||||||
Notifications.ID_EXTENSION_INSTALLER,
|
|
||||||
notification,
|
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
val uri = intent?.data
|
|
||||||
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
|
|
||||||
val installerUsed = intent?.getSerializableExtraCompat<BasePreferences.ExtensionInstaller>(
|
|
||||||
EXTRA_INSTALLER,
|
|
||||||
)
|
|
||||||
if (uri == null || id == null || installerUsed == null) {
|
|
||||||
stopSelf()
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
if (installer == null) {
|
|
||||||
installer = when (installerUsed) {
|
|
||||||
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstallerAnime(
|
|
||||||
this
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Logger.log("Not implemented for installer $installerUsed")
|
|
||||||
stopSelf()
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
installer!!.addToQueue(id, uri)
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
installer?.onDestroy()
|
|
||||||
installer = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(i: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val EXTRA_INSTALLER = "EXTRA_INSTALLER"
|
|
||||||
|
|
||||||
fun getIntent(
|
|
||||||
context: Context,
|
|
||||||
downloadId: Long,
|
|
||||||
uri: Uri,
|
|
||||||
installer: BasePreferences.ExtensionInstaller,
|
|
||||||
): Intent {
|
|
||||||
return Intent(context, AnimeExtensionInstallService::class.java)
|
|
||||||
.setDataAndType(uri, AnimeExtensionInstaller.APK_MIME)
|
|
||||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
|
||||||
.putExtra(EXTRA_INSTALLER, installer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,273 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.anime.util
|
|
||||||
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Environment
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.installer.InstallerAnime
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|
||||||
import rx.Observable
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The installer which installs, updates and uninstalls the extensions.
|
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
*/
|
|
||||||
internal class AnimeExtensionInstaller(private val context: Context) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The system's download manager
|
|
||||||
*/
|
|
||||||
private val downloadManager = context.getSystemService<DownloadManager>()!!
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The broadcast receiver which listens to download completion events.
|
|
||||||
*/
|
|
||||||
private val downloadReceiver = DownloadCompletionReceiver()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The currently requested downloads, with the package name (unique id) as key, and the id
|
|
||||||
* returned by the download manager.
|
|
||||||
*/
|
|
||||||
private val activeDownloads = hashMapOf<String, Long>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Relay used to notify the installation step of every download.
|
|
||||||
*/
|
|
||||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
|
||||||
|
|
||||||
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the given extension to the downloads queue and returns an observable containing its
|
|
||||||
* step in the installation process.
|
|
||||||
*
|
|
||||||
* @param url The url of the apk.
|
|
||||||
* @param extension The extension to install.
|
|
||||||
*/
|
|
||||||
fun downloadAndInstall(url: String, extension: AnimeExtension) = Observable.defer {
|
|
||||||
val pkgName = extension.pkgName
|
|
||||||
|
|
||||||
val oldDownload = activeDownloads[pkgName]
|
|
||||||
if (oldDownload != null) {
|
|
||||||
deleteDownload(pkgName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the receiver after removing (and unregistering) the previous download
|
|
||||||
downloadReceiver.register()
|
|
||||||
|
|
||||||
val downloadUri = url.toUri()
|
|
||||||
val request = DownloadManager.Request(downloadUri)
|
|
||||||
.setTitle(extension.name)
|
|
||||||
.setMimeType(APK_MIME)
|
|
||||||
.setDestinationInExternalFilesDir(
|
|
||||||
context,
|
|
||||||
Environment.DIRECTORY_DOWNLOADS,
|
|
||||||
downloadUri.lastPathSegment
|
|
||||||
)
|
|
||||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
|
||||||
|
|
||||||
val id = downloadManager.enqueue(request)
|
|
||||||
activeDownloads[pkgName] = id
|
|
||||||
|
|
||||||
downloadsRelay.filter { it.first == id }
|
|
||||||
.map { it.second }
|
|
||||||
// Poll download status
|
|
||||||
.mergeWith(pollStatus(id))
|
|
||||||
// Stop when the application is installed or errors
|
|
||||||
.takeUntil { it.isCompleted() }
|
|
||||||
// Always notify on main thread
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
// Always remove the download when unsubscribed
|
|
||||||
.doOnUnsubscribe { deleteDownload(pkgName) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable that polls the given download id for its status every second, as the
|
|
||||||
* manager doesn't have any notification system. It'll stop once the download finishes.
|
|
||||||
*
|
|
||||||
* @param id The id of the download to poll.
|
|
||||||
*/
|
|
||||||
private fun pollStatus(id: Long): Observable<InstallStep> {
|
|
||||||
val query = DownloadManager.Query().setFilterById(id)
|
|
||||||
|
|
||||||
return Observable.interval(0, 1, TimeUnit.SECONDS)
|
|
||||||
// Get the current download status
|
|
||||||
.map {
|
|
||||||
downloadManager.query(query).use { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
|
|
||||||
} else {
|
|
||||||
DownloadManager.STATUS_FAILED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ignore duplicate results
|
|
||||||
.distinctUntilChanged()
|
|
||||||
// Stop polling when the download fails or finishes
|
|
||||||
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
|
|
||||||
// Map to our model
|
|
||||||
.flatMap { status ->
|
|
||||||
when (status) {
|
|
||||||
DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
|
|
||||||
DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading)
|
|
||||||
else -> Observable.empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts an intent to install the extension at the given uri.
|
|
||||||
*
|
|
||||||
* @param uri The uri of the extension to install.
|
|
||||||
*/
|
|
||||||
fun installApk(downloadId: Long, uri: Uri) {
|
|
||||||
when (val installer = extensionInstaller.get()) {
|
|
||||||
BasePreferences.ExtensionInstaller.LEGACY -> {
|
|
||||||
val intent = Intent(context, AnimeExtensionInstallActivity::class.java)
|
|
||||||
.setDataAndType(uri, APK_MIME)
|
|
||||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val intent =
|
|
||||||
AnimeExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels extension install and remove from download manager and installer.
|
|
||||||
*/
|
|
||||||
fun cancelInstall(pkgName: String) {
|
|
||||||
val downloadId = activeDownloads.remove(pkgName) ?: return
|
|
||||||
downloadManager.remove(downloadId)
|
|
||||||
InstallerAnime.cancelInstallQueue(context, downloadId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts an intent to uninstall the extension by the given package name.
|
|
||||||
*
|
|
||||||
* @param pkgName The package name of the extension to uninstall
|
|
||||||
*/
|
|
||||||
fun uninstallApk(pkgName: String) {
|
|
||||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the step of the installation of an extension.
|
|
||||||
*
|
|
||||||
* @param downloadId The id of the download.
|
|
||||||
* @param step New install step.
|
|
||||||
*/
|
|
||||||
fun updateInstallStep(downloadId: Long, step: InstallStep) {
|
|
||||||
downloadsRelay.call(downloadId to step)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the download for the given package name.
|
|
||||||
*
|
|
||||||
* @param pkgName The package name of the download to delete.
|
|
||||||
*/
|
|
||||||
private fun deleteDownload(pkgName: String) {
|
|
||||||
val downloadId = activeDownloads.remove(pkgName)
|
|
||||||
if (downloadId != null) {
|
|
||||||
downloadManager.remove(downloadId)
|
|
||||||
}
|
|
||||||
if (activeDownloads.isEmpty()) {
|
|
||||||
downloadReceiver.unregister()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Receiver that listens to download status events.
|
|
||||||
*/
|
|
||||||
private inner class DownloadCompletionReceiver : BroadcastReceiver() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this receiver is currently registered.
|
|
||||||
*/
|
|
||||||
private var isRegistered = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers this receiver if it's not already.
|
|
||||||
*/
|
|
||||||
fun register() {
|
|
||||||
if (isRegistered) return
|
|
||||||
isRegistered = true
|
|
||||||
|
|
||||||
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
|
||||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregisters this receiver if it's not already.
|
|
||||||
*/
|
|
||||||
fun unregister() {
|
|
||||||
if (!isRegistered) return
|
|
||||||
isRegistered = false
|
|
||||||
|
|
||||||
context.unregisterReceiver(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a download event is received. It looks for the download in the current active
|
|
||||||
* downloads and notifies its installation step.
|
|
||||||
*/
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
|
||||||
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return
|
|
||||||
|
|
||||||
// Avoid events for downloads we didn't request
|
|
||||||
if (id !in activeDownloads.values) return
|
|
||||||
|
|
||||||
val uri = downloadManager.getUriForDownloadedFile(id)
|
|
||||||
|
|
||||||
// Set next installation step
|
|
||||||
if (uri == null) {
|
|
||||||
Logger.log("Couldn't locate downloaded APK")
|
|
||||||
downloadsRelay.call(id to InstallStep.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val query = DownloadManager.Query().setFilterById(id)
|
|
||||||
downloadManager.query(query).use { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
val localUri = cursor.getString(
|
|
||||||
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
|
|
||||||
).removePrefix(FILE_SCHEME)
|
|
||||||
|
|
||||||
installApk(id, File(localUri).getUriCompat(context))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val APK_MIME = "application/vnd.android.package-archive"
|
|
||||||
const val EXTRA_DOWNLOAD_ID = "AnimeExtensionInstaller.extra.DOWNLOAD_ID"
|
|
||||||
const val FILE_SCHEME = "file://"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,244 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.anime.util
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import dalvik.system.PathClassLoader
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
|
||||||
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
|
||||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that handles the loading of the extensions installed in the system.
|
|
||||||
*/
|
|
||||||
@SuppressLint("PackageManagerGetSignatures")
|
|
||||||
internal object AnimeExtensionLoader {
|
|
||||||
|
|
||||||
private val preferences: SourcePreferences by injectLazy()
|
|
||||||
private val loadNsfwSource by lazy {
|
|
||||||
preferences.showNsfwSource().get()
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val EXTENSION_FEATURE = "tachiyomi.animeextension"
|
|
||||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
|
|
||||||
private const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
|
|
||||||
private const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
|
|
||||||
private const val METADATA_HAS_README = "tachiyomi.animeextension.hasReadme"
|
|
||||||
private const val METADATA_HAS_CHANGELOG = "tachiyomi.animeextension.hasChangelog"
|
|
||||||
const val LIB_VERSION_MIN = 12
|
|
||||||
const val LIB_VERSION_MAX = 15
|
|
||||||
|
|
||||||
private const val PACKAGE_FLAGS =
|
|
||||||
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
|
||||||
|
|
||||||
// jmir1's key
|
|
||||||
private const val officialSignature =
|
|
||||||
"50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of the trusted signatures.
|
|
||||||
*/
|
|
||||||
var trustedSignatures =
|
|
||||||
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a list of all the installed extensions initialized concurrently.
|
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
*/
|
|
||||||
fun loadExtensions(context: Context): List<AnimeLoadResult> {
|
|
||||||
val pkgManager = context.packageManager
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
|
|
||||||
} else {
|
|
||||||
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
|
||||||
}
|
|
||||||
|
|
||||||
val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
|
|
||||||
|
|
||||||
if (extPkgs.isEmpty()) return emptyList()
|
|
||||||
|
|
||||||
// Load each extension concurrently and wait for completion
|
|
||||||
return runBlocking {
|
|
||||||
val deferred = extPkgs.map {
|
|
||||||
async { loadExtension(context, it.packageName, it) }
|
|
||||||
}
|
|
||||||
deferred.map { it.await() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to load an extension from the given package name. It checks if the extension
|
|
||||||
* contains the required feature flag before trying to load it.
|
|
||||||
*/
|
|
||||||
fun loadExtensionFromPkgName(context: Context, pkgName: String): AnimeLoadResult {
|
|
||||||
val pkgInfo = try {
|
|
||||||
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
|
||||||
} catch (error: PackageManager.NameNotFoundException) {
|
|
||||||
// Unlikely, but the package may have been uninstalled at this point
|
|
||||||
Logger.log(error)
|
|
||||||
return AnimeLoadResult.Error
|
|
||||||
}
|
|
||||||
if (!isPackageAnExtension(pkgInfo)) {
|
|
||||||
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
|
|
||||||
return AnimeLoadResult.Error
|
|
||||||
}
|
|
||||||
return loadExtension(context, pkgName, pkgInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an extension given its package name.
|
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
* @param pkgName The package name of the extension to load.
|
|
||||||
* @param pkgInfo The package info of the extension.
|
|
||||||
*/
|
|
||||||
private fun loadExtension(
|
|
||||||
context: Context,
|
|
||||||
pkgName: String,
|
|
||||||
pkgInfo: PackageInfo
|
|
||||||
): AnimeLoadResult {
|
|
||||||
val pkgManager = context.packageManager
|
|
||||||
|
|
||||||
val appInfo = try {
|
|
||||||
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
|
||||||
} catch (error: PackageManager.NameNotFoundException) {
|
|
||||||
// Unlikely, but the package may have been uninstalled at this point
|
|
||||||
Logger.log(error)
|
|
||||||
return AnimeLoadResult.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Aniyomi: ")
|
|
||||||
val versionName = pkgInfo.versionName
|
|
||||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
|
||||||
|
|
||||||
if (versionName.isNullOrEmpty()) {
|
|
||||||
Logger.log("Missing versionName for extension $extName")
|
|
||||||
return AnimeLoadResult.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate lib version
|
|
||||||
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
|
|
||||||
if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
|
||||||
Logger.log("Lib version is $libVersion, while only versions " +
|
|
||||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
|
||||||
)
|
|
||||||
return AnimeLoadResult.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
val signatureHash = getSignatureHash(pkgInfo)
|
|
||||||
|
|
||||||
if (signatureHash == null) {
|
|
||||||
Logger.log("Package $pkgName isn't signed")
|
|
||||||
return AnimeLoadResult.Error
|
|
||||||
} else if (signatureHash !in trustedSignatures) {
|
|
||||||
val extension = AnimeExtension.Untrusted(
|
|
||||||
extName,
|
|
||||||
pkgName,
|
|
||||||
versionName,
|
|
||||||
versionCode,
|
|
||||||
libVersion,
|
|
||||||
signatureHash
|
|
||||||
)
|
|
||||||
Logger.log("Extension $pkgName isn't trusted")
|
|
||||||
return AnimeLoadResult.Untrusted(extension)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
|
||||||
if (!loadNsfwSource && isNsfw) {
|
|
||||||
Logger.log("NSFW extension $pkgName not allowed")
|
|
||||||
return AnimeLoadResult.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
|
|
||||||
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
|
|
||||||
|
|
||||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
|
||||||
|
|
||||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
|
||||||
.split(";")
|
|
||||||
.map {
|
|
||||||
val sourceClass = it.trim()
|
|
||||||
if (sourceClass.startsWith(".")) {
|
|
||||||
pkgInfo.packageName + sourceClass
|
|
||||||
} else {
|
|
||||||
sourceClass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.flatMap {
|
|
||||||
try {
|
|
||||||
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
|
||||||
is AnimeSource -> listOf(obj)
|
|
||||||
is AnimeSourceFactory -> obj.createSources()
|
|
||||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.log("Extension load error: $extName ($it)")
|
|
||||||
return AnimeLoadResult.Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val langs = sources.filterIsInstance<AnimeCatalogueSource>()
|
|
||||||
.map { it.lang }
|
|
||||||
.toSet()
|
|
||||||
val lang = when (langs.size) {
|
|
||||||
0 -> ""
|
|
||||||
1 -> langs.first()
|
|
||||||
else -> "all"
|
|
||||||
}
|
|
||||||
|
|
||||||
val extension = AnimeExtension.Installed(
|
|
||||||
name = extName,
|
|
||||||
pkgName = pkgName,
|
|
||||||
versionName = versionName,
|
|
||||||
versionCode = versionCode,
|
|
||||||
libVersion = libVersion,
|
|
||||||
lang = lang,
|
|
||||||
isNsfw = isNsfw,
|
|
||||||
hasReadme = hasReadme,
|
|
||||||
hasChangelog = hasChangelog,
|
|
||||||
sources = sources,
|
|
||||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
|
||||||
isUnofficial = signatureHash != officialSignature,
|
|
||||||
icon = context.getApplicationIcon(pkgName),
|
|
||||||
)
|
|
||||||
return AnimeLoadResult.Success(extension)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given package is an extension.
|
|
||||||
*
|
|
||||||
* @param pkgInfo The package info of the application.
|
|
||||||
*/
|
|
||||||
private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
|
|
||||||
return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the signature hash of the package or null if it's not signed.
|
|
||||||
*
|
|
||||||
* @param pkgInfo The package info of the application.
|
|
||||||
*/
|
|
||||||
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
|
||||||
val signatures = pkgInfo.signatures
|
|
||||||
return if (signatures != null && signatures.isNotEmpty()) {
|
|
||||||
Hash.sha256(signatures.first().toByteArray())
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.anime.installer
|
package eu.kanade.tachiyomi.extension.installer
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
|
@ -8,8 +8,11 @@ import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
|
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
@ -17,9 +20,11 @@ import java.util.concurrent.atomic.AtomicReference
|
||||||
/**
|
/**
|
||||||
* Base implementation class for extension installer. To be used inside a foreground [Service].
|
* Base implementation class for extension installer. To be used inside a foreground [Service].
|
||||||
*/
|
*/
|
||||||
abstract class InstallerAnime(private val service: Service) {
|
abstract class Installer(private val service: Service) {
|
||||||
|
|
||||||
private val extensionManager: AnimeExtensionManager by injectLazy()
|
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
|
||||||
|
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
|
||||||
|
private val novelExtensionManager: NovelExtensionManager by injectLazy()
|
||||||
|
|
||||||
private var waitingInstall = AtomicReference<Entry>(null)
|
private var waitingInstall = AtomicReference<Entry>(null)
|
||||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||||
|
@ -44,8 +49,8 @@ abstract class InstallerAnime(private val service: Service) {
|
||||||
* @param downloadId Download ID as known by [ExtensionManager]
|
* @param downloadId Download ID as known by [ExtensionManager]
|
||||||
* @param uri Uri of APK to install
|
* @param uri Uri of APK to install
|
||||||
*/
|
*/
|
||||||
fun addToQueue(downloadId: Long, uri: Uri) {
|
fun addToQueue(type: MediaType, downloadId: Long, uri: Uri) {
|
||||||
queue.add(Entry(downloadId, uri))
|
queue.add(Entry(type, downloadId, uri))
|
||||||
checkQueue()
|
checkQueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +63,11 @@ abstract class InstallerAnime(private val service: Service) {
|
||||||
*/
|
*/
|
||||||
@CallSuper
|
@CallSuper
|
||||||
open fun processEntry(entry: Entry) {
|
open fun processEntry(entry: Entry) {
|
||||||
extensionManager.setInstalling(entry.downloadId)
|
when (entry.type) {
|
||||||
|
MediaType.ANIME -> animeExtensionManager.setInstalling(entry.downloadId)
|
||||||
|
MediaType.MANGA -> mangaExtensionManager.setInstalling(entry.downloadId)
|
||||||
|
MediaType.NOVEL -> novelExtensionManager.setInstalling(entry.downloadId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,7 +90,19 @@ abstract class InstallerAnime(private val service: Service) {
|
||||||
fun continueQueue(resultStep: InstallStep) {
|
fun continueQueue(resultStep: InstallStep) {
|
||||||
val completedEntry = waitingInstall.getAndSet(null)
|
val completedEntry = waitingInstall.getAndSet(null)
|
||||||
if (completedEntry != null) {
|
if (completedEntry != null) {
|
||||||
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
when (completedEntry.type) {
|
||||||
|
MediaType.ANIME -> {
|
||||||
|
animeExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaType.MANGA -> {
|
||||||
|
mangaExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaType.NOVEL -> {
|
||||||
|
novelExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||||
|
}
|
||||||
|
}
|
||||||
checkQueue()
|
checkQueue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,7 +134,19 @@ abstract class InstallerAnime(private val service: Service) {
|
||||||
@CallSuper
|
@CallSuper
|
||||||
open fun onDestroy() {
|
open fun onDestroy() {
|
||||||
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
||||||
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
|
queue.forEach {
|
||||||
|
when (it.type) {
|
||||||
|
MediaType.ANIME -> {
|
||||||
|
animeExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
|
||||||
|
}
|
||||||
|
MediaType.MANGA -> {
|
||||||
|
mangaExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
|
||||||
|
}
|
||||||
|
MediaType.NOVEL -> {
|
||||||
|
novelExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
queue.clear()
|
queue.clear()
|
||||||
waitingInstall.set(null)
|
waitingInstall.set(null)
|
||||||
}
|
}
|
||||||
|
@ -135,7 +168,17 @@ abstract class InstallerAnime(private val service: Service) {
|
||||||
this.waitingInstall.set(null)
|
this.waitingInstall.set(null)
|
||||||
checkQueue()
|
checkQueue()
|
||||||
}
|
}
|
||||||
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
when (toCancel.type) {
|
||||||
|
MediaType.ANIME -> {
|
||||||
|
animeExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||||
|
}
|
||||||
|
MediaType.MANGA -> {
|
||||||
|
mangaExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||||
|
}
|
||||||
|
MediaType.NOVEL -> {
|
||||||
|
novelExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +188,7 @@ abstract class InstallerAnime(private val service: Service) {
|
||||||
* @param downloadId Download ID as known by [ExtensionManager]
|
* @param downloadId Download ID as known by [ExtensionManager]
|
||||||
* @param uri Uri of APK to install
|
* @param uri Uri of APK to install
|
||||||
*/
|
*/
|
||||||
data class Entry(val downloadId: Long, val uri: Uri)
|
data class Entry(val type: MediaType, val downloadId: Long, val uri: Uri)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
|
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
|
||||||
|
@ -153,8 +196,8 @@ abstract class InstallerAnime(private val service: Service) {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ACTION_CANCEL_QUEUE = "InstallerAnime.action.CANCEL_QUEUE"
|
private const val ACTION_CANCEL_QUEUE = "Installer.action.CANCEL_QUEUE"
|
||||||
private const val EXTRA_DOWNLOAD_ID = "InstallerAnime.extra.DOWNLOAD_ID"
|
private const val EXTRA_DOWNLOAD_ID = "Installer.extra.DOWNLOAD_ID"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to cancel the installation entry for the provided download ID.
|
* Attempts to cancel the installation entry for the provided download ID.
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.anime.installer
|
package eu.kanade.tachiyomi.extension.installer
|
||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
@ -9,6 +9,7 @@ import android.content.IntentFilter
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.IntentSanitizer
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
|
@ -16,7 +17,7 @@ import eu.kanade.tachiyomi.util.lang.use
|
||||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||||
|
|
||||||
class PackageInstallerInstallerAnime(private val service: Service) : InstallerAnime(service) {
|
class PackageInstallerInstaller(private val service: Service) : Installer(service) {
|
||||||
|
|
||||||
private val packageInstaller = service.packageManager.packageInstaller
|
private val packageInstaller = service.packageManager.packageInstaller
|
||||||
|
|
||||||
|
@ -27,7 +28,18 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
|
||||||
PackageInstaller.STATUS_FAILURE
|
PackageInstaller.STATUS_FAILURE
|
||||||
)) {
|
)) {
|
||||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
|
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)?.run {
|
||||||
|
IntentSanitizer.Builder()
|
||||||
|
.allowAction(this.action!!)
|
||||||
|
.allowExtra(PackageInstaller.EXTRA_SESSION_ID) { id -> id == activeSession?.second }
|
||||||
|
.allowAnyComponent()
|
||||||
|
.allowPackage {
|
||||||
|
// There is no way to check the actual installer name so allow all.
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
.sanitizeByFiltering(this)
|
||||||
|
}
|
||||||
if (userAction == null) {
|
if (userAction == null) {
|
||||||
Logger.log("Fatal error for $intent")
|
Logger.log("Fatal error for $intent")
|
||||||
continueQueue(InstallStep.Error)
|
continueQueue(InstallStep.Error)
|
||||||
|
@ -78,7 +90,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
|
||||||
val intentSender = PendingIntent.getBroadcast(
|
val intentSender = PendingIntent.getBroadcast(
|
||||||
service,
|
service,
|
||||||
activeSession!!.second,
|
activeSession!!.second,
|
||||||
Intent(INSTALL_ACTION),
|
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
@ -88,8 +100,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
|
||||||
session.commit(intentSender)
|
session.commit(intentSender)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.log(e)
|
Logger.log("Failed to install extension ${entry.downloadId} ${entry.uri}\n$e")
|
||||||
Logger.log("Failed to install extension ${entry.downloadId} ${entry.uri}")
|
|
||||||
snackString("Failed to install extension ${entry.downloadId} ${entry.uri}")
|
snackString("Failed to install extension ${entry.downloadId} ${entry.uri}")
|
||||||
activeSession?.let { (_, sessionId) ->
|
activeSession?.let { (_, sessionId) ->
|
||||||
packageInstaller.abandonSession(sessionId)
|
packageInstaller.abandonSession(sessionId)
|
||||||
|
@ -118,7 +129,7 @@ class PackageInstallerInstallerAnime(private val service: Service) : InstallerAn
|
||||||
service,
|
service,
|
||||||
packageActionReceiver,
|
packageActionReceiver,
|
||||||
IntentFilter(INSTALL_ACTION),
|
IntentFilter(INSTALL_ACTION),
|
||||||
ContextCompat.RECEIVER_EXPORTED
|
ContextCompat.RECEIVER_EXPORTED,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,9 +10,9 @@ import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
|
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||||
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallReceiver
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||||
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||||
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.util.preference.plusAssign
|
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -51,7 +51,7 @@ class MangaExtensionManager(
|
||||||
/**
|
/**
|
||||||
* The installer which installs, updates and uninstalls the extensions.
|
* The installer which installs, updates and uninstalls the extensions.
|
||||||
*/
|
*/
|
||||||
private val installer by lazy { MangaExtensionInstaller(context) }
|
private val installer by lazy { ExtensionInstaller(context) }
|
||||||
|
|
||||||
private val iconMap = mutableMapOf<String, Drawable>()
|
private val iconMap = mutableMapOf<String, Drawable>()
|
||||||
|
|
||||||
|
@ -89,14 +89,14 @@ class MangaExtensionManager(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initExtensions()
|
initExtensions()
|
||||||
MangaExtensionInstallReceiver(InstallationListener()).register(context)
|
ExtensionInstallReceiver().setMangaListener(InstallationListener()).register(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads and registers the installed extensions.
|
* Loads and registers the installed extensions.
|
||||||
*/
|
*/
|
||||||
private fun initExtensions() {
|
private fun initExtensions() {
|
||||||
val extensions = MangaExtensionLoader.loadMangaExtensions(context)
|
val extensions = ExtensionLoader.loadMangaExtensions(context)
|
||||||
|
|
||||||
_installedExtensionsFlow.value = extensions
|
_installedExtensionsFlow.value = extensions
|
||||||
.filterIsInstance<MangaLoadResult.Success>()
|
.filterIsInstance<MangaLoadResult.Success>()
|
||||||
|
@ -254,7 +254,7 @@ class MangaExtensionManager(
|
||||||
val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
|
val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
|
||||||
if (signature !in untrustedSignatures) return
|
if (signature !in untrustedSignatures) return
|
||||||
|
|
||||||
MangaExtensionLoader.trustedSignatures += signature
|
ExtensionLoader.trustedSignaturesManga += signature
|
||||||
preferences.trustedSignatures() += signature
|
preferences.trustedSignatures() += signature
|
||||||
|
|
||||||
val nowTrustedExtensions =
|
val nowTrustedExtensions =
|
||||||
|
@ -266,7 +266,7 @@ class MangaExtensionManager(
|
||||||
nowTrustedExtensions
|
nowTrustedExtensions
|
||||||
.map { extension ->
|
.map { extension ->
|
||||||
async {
|
async {
|
||||||
MangaExtensionLoader.loadMangaExtensionFromPkgName(
|
ExtensionLoader.loadMangaExtensionFromPkgName(
|
||||||
ctx,
|
ctx,
|
||||||
extension.pkgName
|
extension.pkgName
|
||||||
)
|
)
|
||||||
|
@ -326,7 +326,7 @@ class MangaExtensionManager(
|
||||||
/**
|
/**
|
||||||
* Listener which receives events of the extensions being installed, updated or removed.
|
* Listener which receives events of the extensions being installed, updated or removed.
|
||||||
*/
|
*/
|
||||||
private inner class InstallationListener : MangaExtensionInstallReceiver.Listener {
|
private inner class InstallationListener : ExtensionInstallReceiver.MangaListener {
|
||||||
|
|
||||||
override fun onExtensionInstalled(extension: MangaExtension.Installed) {
|
override fun onExtensionInstalled(extension: MangaExtension.Installed) {
|
||||||
registerNewExtension(extension.withUpdateCheck())
|
registerNewExtension(extension.withUpdateCheck())
|
||||||
|
|
|
@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
|
import eu.kanade.tachiyomi.extension.manga.model.AvailableMangaSources
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||||
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
@ -87,7 +87,7 @@ internal class MangaExtensionGithubApi {
|
||||||
findExtensions().also { lastExtCheck.set(Date().time) }
|
findExtensions().also { lastExtCheck.set(Date().time) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val installedExtensions = MangaExtensionLoader.loadMangaExtensions(context)
|
val installedExtensions = ExtensionLoader.loadMangaExtensions(context)
|
||||||
.filterIsInstance<MangaLoadResult.Success>()
|
.filterIsInstance<MangaLoadResult.Success>()
|
||||||
.map { it.extension }
|
.map { it.extension }
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ internal class MangaExtensionGithubApi {
|
||||||
return this
|
return this
|
||||||
.filter {
|
.filter {
|
||||||
val libVersion = it.extractLibVersion()
|
val libVersion = it.extractLibVersion()
|
||||||
libVersion >= MangaExtensionLoader.LIB_VERSION_MIN && libVersion <= MangaExtensionLoader.LIB_VERSION_MAX
|
libVersion >= ExtensionLoader.MANGA_LIB_VERSION_MIN && libVersion <= ExtensionLoader.MANGA_LIB_VERSION_MAX
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
MangaExtension.Available(
|
MangaExtension.Available(
|
||||||
|
|
|
@ -1,170 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.manga.installer
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.util.Collections
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base implementation class for extension installer. To be used inside a foreground [Service].
|
|
||||||
*/
|
|
||||||
abstract class InstallerManga(private val service: Service) {
|
|
||||||
|
|
||||||
private val extensionManager: MangaExtensionManager by injectLazy()
|
|
||||||
|
|
||||||
private var waitingInstall = AtomicReference<Entry>(null)
|
|
||||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
|
||||||
|
|
||||||
private val cancelReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
val downloadId = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1).takeIf { it >= 0 } ?: return
|
|
||||||
cancelQueue(downloadId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Installer readiness. If false, queue check will not run.
|
|
||||||
*
|
|
||||||
* @see checkQueue
|
|
||||||
*/
|
|
||||||
abstract var ready: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an item to install queue.
|
|
||||||
*
|
|
||||||
* @param downloadId Download ID as known by [MangaExtensionManager]
|
|
||||||
* @param uri Uri of APK to install
|
|
||||||
*/
|
|
||||||
fun addToQueue(downloadId: Long, uri: Uri) {
|
|
||||||
queue.add(Entry(downloadId, uri))
|
|
||||||
checkQueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proceeds to install the APK of this entry inside this method. Call [continueQueue]
|
|
||||||
* when the install process for this entry is finished to continue the queue.
|
|
||||||
*
|
|
||||||
* @param entry The [Entry] of item to process
|
|
||||||
* @see continueQueue
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
open fun processEntry(entry: Entry) {
|
|
||||||
extensionManager.setInstalling(entry.downloadId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before queue continues. Override this to handle when the removed entry is
|
|
||||||
* currently being processed.
|
|
||||||
*
|
|
||||||
* @return true if this entry can be removed from queue.
|
|
||||||
*/
|
|
||||||
open fun cancelEntry(entry: Entry): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the queue to continue processing the next entry and updates the install step
|
|
||||||
* of the completed entry ([waitingInstall]) to [MangaExtensionManager].
|
|
||||||
*
|
|
||||||
* @param resultStep new install step for the processed entry.
|
|
||||||
* @see waitingInstall
|
|
||||||
*/
|
|
||||||
fun continueQueue(resultStep: InstallStep) {
|
|
||||||
val completedEntry = waitingInstall.getAndSet(null)
|
|
||||||
if (completedEntry != null) {
|
|
||||||
extensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
|
||||||
checkQueue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the queue. The provided service will be stopped if the queue is empty.
|
|
||||||
* Will not be run when not ready.
|
|
||||||
*
|
|
||||||
* @see ready
|
|
||||||
*/
|
|
||||||
fun checkQueue() {
|
|
||||||
if (!ready) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (queue.isEmpty()) {
|
|
||||||
service.stopSelf()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val nextEntry = queue.first()
|
|
||||||
if (waitingInstall.compareAndSet(null, nextEntry)) {
|
|
||||||
queue.removeFirst()
|
|
||||||
processEntry(nextEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call this method when the provided service is destroyed.
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
open fun onDestroy() {
|
|
||||||
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
|
||||||
queue.forEach { extensionManager.updateInstallStep(it.downloadId, InstallStep.Error) }
|
|
||||||
queue.clear()
|
|
||||||
waitingInstall.set(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun getActiveEntry(): Entry? = waitingInstall.get()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels queue for the provided download ID if exists.
|
|
||||||
*
|
|
||||||
* @param downloadId Download ID as known by [MangaExtensionManager]
|
|
||||||
*/
|
|
||||||
private fun cancelQueue(downloadId: Long) {
|
|
||||||
val waitingInstall = this.waitingInstall.get()
|
|
||||||
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
|
||||||
if (cancelEntry(toCancel)) {
|
|
||||||
queue.remove(toCancel)
|
|
||||||
if (waitingInstall == toCancel) {
|
|
||||||
// Currently processing removed entry, continue queue
|
|
||||||
this.waitingInstall.set(null)
|
|
||||||
checkQueue()
|
|
||||||
}
|
|
||||||
extensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install item to queue.
|
|
||||||
*
|
|
||||||
* @param downloadId Download ID as known by [MangaExtensionManager]
|
|
||||||
* @param uri Uri of APK to install
|
|
||||||
*/
|
|
||||||
data class Entry(val downloadId: Long, val uri: Uri)
|
|
||||||
|
|
||||||
init {
|
|
||||||
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
|
|
||||||
LocalBroadcastManager.getInstance(service).registerReceiver(cancelReceiver, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ACTION_CANCEL_QUEUE = "Installer.action.CANCEL_QUEUE"
|
|
||||||
private const val EXTRA_DOWNLOAD_ID = "Installer.extra.DOWNLOAD_ID"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to cancel the installation entry for the provided download ID.
|
|
||||||
*
|
|
||||||
* @param downloadId Download ID as known by [MangaExtensionManager]
|
|
||||||
*/
|
|
||||||
fun cancelInstallQueue(context: Context, downloadId: Long) {
|
|
||||||
val intent = Intent(ACTION_CANCEL_QUEUE)
|
|
||||||
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
|
||||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.manga.installer
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import ani.dantotsu.snackString
|
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
|
||||||
import eu.kanade.tachiyomi.util.lang.use
|
|
||||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
|
||||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
|
||||||
|
|
||||||
class PackageInstallerInstallerManga(private val service: Service) : InstallerManga(service) {
|
|
||||||
|
|
||||||
private val packageInstaller = service.packageManager.packageInstaller
|
|
||||||
|
|
||||||
private val packageActionReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
when (intent.getIntExtra(
|
|
||||||
PackageInstaller.EXTRA_STATUS,
|
|
||||||
PackageInstaller.STATUS_FAILURE
|
|
||||||
)) {
|
|
||||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
|
||||||
val userAction = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
|
|
||||||
if (userAction == null) {
|
|
||||||
Logger.log("Fatal error for $intent")
|
|
||||||
continueQueue(InstallStep.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userAction.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
service.startActivity(userAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
|
||||||
continueQueue(InstallStep.Idle)
|
|
||||||
}
|
|
||||||
|
|
||||||
PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed)
|
|
||||||
else -> continueQueue(InstallStep.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var activeSession: Pair<Entry, Int>? = null
|
|
||||||
|
|
||||||
// Always ready
|
|
||||||
override var ready = true
|
|
||||||
|
|
||||||
override fun processEntry(entry: Entry) {
|
|
||||||
super.processEntry(entry)
|
|
||||||
activeSession = null
|
|
||||||
try {
|
|
||||||
val installParams =
|
|
||||||
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
|
|
||||||
}
|
|
||||||
activeSession = entry to packageInstaller.createSession(installParams)
|
|
||||||
val fileSize = service.getUriSize(entry.uri) ?: throw IllegalStateException()
|
|
||||||
installParams.setSize(fileSize)
|
|
||||||
|
|
||||||
val inputStream =
|
|
||||||
service.contentResolver.openInputStream(entry.uri) ?: throw IllegalStateException()
|
|
||||||
val session = packageInstaller.openSession(activeSession!!.second)
|
|
||||||
val outputStream = session.openWrite(entry.downloadId.toString(), 0, fileSize)
|
|
||||||
session.use {
|
|
||||||
arrayOf(inputStream, outputStream).use {
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
session.fsync(outputStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
val intentSender = PendingIntent.getBroadcast(
|
|
||||||
service,
|
|
||||||
activeSession!!.second,
|
|
||||||
Intent(INSTALL_ACTION),
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
|
|
||||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
PendingIntent.FLAG_MUTABLE
|
|
||||||
} else 0
|
|
||||||
).intentSender
|
|
||||||
session.commit(intentSender)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Logger.log("Failed to install extension ${entry.downloadId} ${entry.uri}")
|
|
||||||
Logger.log(e)
|
|
||||||
snackString("Failed to install extension ${entry.downloadId} ${entry.uri}")
|
|
||||||
activeSession?.let { (_, sessionId) ->
|
|
||||||
packageInstaller.abandonSession(sessionId)
|
|
||||||
}
|
|
||||||
continueQueue(InstallStep.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancelEntry(entry: Entry): Boolean {
|
|
||||||
activeSession?.let { (activeEntry, sessionId) ->
|
|
||||||
if (activeEntry == entry) {
|
|
||||||
packageInstaller.abandonSession(sessionId)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
service.unregisterReceiver(packageActionReceiver)
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
ContextCompat.registerReceiver(
|
|
||||||
service,
|
|
||||||
packageActionReceiver,
|
|
||||||
IntentFilter(INSTALL_ACTION),
|
|
||||||
ContextCompat.RECEIVER_EXPORTED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val INSTALL_ACTION = "PackageInstallerInstaller.INSTALL_ACTION"
|
|
|
@ -3,5 +3,5 @@ package eu.kanade.tachiyomi.extension.manga.model
|
||||||
sealed class MangaLoadResult {
|
sealed class MangaLoadResult {
|
||||||
class Success(val extension: MangaExtension.Installed) : MangaLoadResult()
|
class Success(val extension: MangaExtension.Installed) : MangaLoadResult()
|
||||||
class Untrusted(val extension: MangaExtension.Untrusted) : MangaLoadResult()
|
class Untrusted(val extension: MangaExtension.Untrusted) : MangaLoadResult()
|
||||||
object Error : MangaLoadResult()
|
data object Error : MangaLoadResult()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,132 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.manga.util
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
|
||||||
import kotlinx.coroutines.CoroutineStart
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import tachiyomi.core.util.lang.launchNow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
|
|
||||||
* notifies the given [listener] when the package is an extension.
|
|
||||||
*
|
|
||||||
* @param listener The listener that should be notified of extension installation events.
|
|
||||||
*/
|
|
||||||
internal class MangaExtensionInstallReceiver(private val listener: Listener) :
|
|
||||||
BroadcastReceiver() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers this broadcast receiver
|
|
||||||
*/
|
|
||||||
fun register(context: Context) {
|
|
||||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the intent filter this receiver should subscribe to.
|
|
||||||
*/
|
|
||||||
private val filter
|
|
||||||
get() = IntentFilter().apply {
|
|
||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
|
||||||
addDataScheme("package")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when one of the events of the [filter] is received. When the package is an extension,
|
|
||||||
* it's loaded in background and it notifies the [listener] when finished.
|
|
||||||
*/
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
|
||||||
if (intent == null) return
|
|
||||||
|
|
||||||
when (intent.action) {
|
|
||||||
Intent.ACTION_PACKAGE_ADDED -> {
|
|
||||||
if (isReplacing(intent)) return
|
|
||||||
|
|
||||||
launchNow {
|
|
||||||
when (val result = getExtensionFromIntent(context, intent)) {
|
|
||||||
is MangaLoadResult.Success -> listener.onExtensionInstalled(result.extension)
|
|
||||||
|
|
||||||
is MangaLoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
|
||||||
launchNow {
|
|
||||||
when (val result = getExtensionFromIntent(context, intent)) {
|
|
||||||
is MangaLoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
|
||||||
// Not needed as a package can't be upgraded if the signature is different
|
|
||||||
// is LoadResult.Untrusted -> {}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
|
||||||
if (isReplacing(intent)) return
|
|
||||||
|
|
||||||
val pkgName = getPackageNameFromIntent(intent)
|
|
||||||
if (pkgName != null) {
|
|
||||||
listener.onPackageUninstalled(pkgName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this package is performing an update.
|
|
||||||
*
|
|
||||||
* @param intent The intent that triggered the event.
|
|
||||||
*/
|
|
||||||
private fun isReplacing(intent: Intent): Boolean {
|
|
||||||
return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the extension triggered by the given intent.
|
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
* @param intent The intent containing the package name of the extension.
|
|
||||||
*/
|
|
||||||
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): MangaLoadResult {
|
|
||||||
val pkgName = getPackageNameFromIntent(intent)
|
|
||||||
if (pkgName == null) {
|
|
||||||
Logger.log("Package name not found")
|
|
||||||
return MangaLoadResult.Error
|
|
||||||
}
|
|
||||||
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
|
|
||||||
MangaExtensionLoader.loadMangaExtensionFromPkgName(
|
|
||||||
context,
|
|
||||||
pkgName,
|
|
||||||
)
|
|
||||||
}.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the package name of the installed, updated or removed application.
|
|
||||||
*/
|
|
||||||
private fun getPackageNameFromIntent(intent: Intent?): String? {
|
|
||||||
return intent?.data?.encodedSchemeSpecificPart ?: return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener that receives extension installation events.
|
|
||||||
*/
|
|
||||||
interface Listener {
|
|
||||||
fun onExtensionInstalled(extension: MangaExtension.Installed)
|
|
||||||
fun onExtensionUpdated(extension: MangaExtension.Installed)
|
|
||||||
fun onExtensionUntrusted(extension: MangaExtension.Untrusted)
|
|
||||||
fun onPackageUninstalled(pkgName: String)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,240 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.manga.util
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
|
||||||
import ani.dantotsu.util.Logger
|
|
||||||
import dalvik.system.PathClassLoader
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
|
||||||
import eu.kanade.tachiyomi.source.MangaSource
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
|
||||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that handles the loading of the extensions installed in the system.
|
|
||||||
*/
|
|
||||||
@SuppressLint("PackageManagerGetSignatures")
|
|
||||||
internal object MangaExtensionLoader {
|
|
||||||
|
|
||||||
private val preferences: SourcePreferences by injectLazy()
|
|
||||||
private val loadNsfwSource by lazy {
|
|
||||||
preferences.showNsfwSource().get()
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
|
||||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
|
||||||
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
|
||||||
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
|
||||||
private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme"
|
|
||||||
private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog"
|
|
||||||
const val LIB_VERSION_MIN = 1.2
|
|
||||||
const val LIB_VERSION_MAX = 1.5
|
|
||||||
|
|
||||||
private const val PACKAGE_FLAGS =
|
|
||||||
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
|
||||||
|
|
||||||
// inorichi's key
|
|
||||||
private const val officialSignature =
|
|
||||||
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of the trusted signatures.
|
|
||||||
*/
|
|
||||||
var trustedSignatures =
|
|
||||||
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a list of all the installed extensions initialized concurrently.
|
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
*/
|
|
||||||
fun loadMangaExtensions(context: Context): List<MangaLoadResult> {
|
|
||||||
val pkgManager = context.packageManager
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
|
|
||||||
} else {
|
|
||||||
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
|
||||||
}
|
|
||||||
|
|
||||||
val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
|
|
||||||
|
|
||||||
if (extPkgs.isEmpty()) return emptyList()
|
|
||||||
|
|
||||||
// Load each extension concurrently and wait for completion
|
|
||||||
return runBlocking {
|
|
||||||
val deferred = extPkgs.map {
|
|
||||||
async { loadMangaExtension(context, it.packageName, it) }
|
|
||||||
}
|
|
||||||
deferred.map { it.await() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to load an extension from the given package name. It checks if the extension
|
|
||||||
* contains the required feature flag before trying to load it.
|
|
||||||
*/
|
|
||||||
fun loadMangaExtensionFromPkgName(context: Context, pkgName: String): MangaLoadResult {
|
|
||||||
val pkgInfo = try {
|
|
||||||
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
|
||||||
} catch (error: PackageManager.NameNotFoundException) {
|
|
||||||
// Unlikely, but the package may have been uninstalled at this point
|
|
||||||
Logger.log(error)
|
|
||||||
return MangaLoadResult.Error
|
|
||||||
}
|
|
||||||
if (!isPackageAnExtension(pkgInfo)) {
|
|
||||||
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
|
|
||||||
return MangaLoadResult.Error
|
|
||||||
}
|
|
||||||
return loadMangaExtension(context, pkgName, pkgInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an extension given its package name.
|
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
* @param pkgName The package name of the extension to load.
|
|
||||||
* @param pkgInfo The package info of the extension.
|
|
||||||
*/
|
|
||||||
private fun loadMangaExtension(
|
|
||||||
context: Context,
|
|
||||||
pkgName: String,
|
|
||||||
pkgInfo: PackageInfo
|
|
||||||
): MangaLoadResult {
|
|
||||||
val pkgManager = context.packageManager
|
|
||||||
|
|
||||||
val appInfo = try {
|
|
||||||
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
|
||||||
} catch (error: PackageManager.NameNotFoundException) {
|
|
||||||
// Unlikely, but the package may have been uninstalled at this point
|
|
||||||
Logger.log(error)
|
|
||||||
return MangaLoadResult.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
val extName =
|
|
||||||
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
|
||||||
val versionName = pkgInfo.versionName
|
|
||||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
|
||||||
|
|
||||||
if (versionName.isNullOrEmpty()) {
|
|
||||||
Logger.log("Missing versionName for extension $extName")
|
|
||||||
return MangaLoadResult.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate lib version
|
|
||||||
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
|
|
||||||
if (libVersion == null || libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
|
||||||
Logger.log("Lib version is $libVersion, while only versions " +
|
|
||||||
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
|
||||||
)
|
|
||||||
return MangaLoadResult.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
val signatureHash = getSignatureHash(pkgInfo)
|
|
||||||
|
|
||||||
/* temporarily disabling signature check TODO: remove?
|
|
||||||
if (signatureHash == null) {
|
|
||||||
Logger.log("Package $pkgName isn't signed")
|
|
||||||
return MangaLoadResult.Error
|
|
||||||
} else if (signatureHash !in trustedSignatures) {
|
|
||||||
val extension = MangaExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
|
|
||||||
Logger.log("Extension $pkgName isn't trusted")
|
|
||||||
return MangaLoadResult.Untrusted(extension)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
|
||||||
if (!loadNsfwSource && isNsfw) {
|
|
||||||
Logger.log("NSFW extension $pkgName not allowed")
|
|
||||||
return MangaLoadResult.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
|
|
||||||
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
|
|
||||||
|
|
||||||
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
|
||||||
|
|
||||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
|
||||||
.split(";")
|
|
||||||
.map {
|
|
||||||
val sourceClass = it.trim()
|
|
||||||
if (sourceClass.startsWith(".")) {
|
|
||||||
pkgInfo.packageName + sourceClass
|
|
||||||
} else {
|
|
||||||
sourceClass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.flatMap {
|
|
||||||
try {
|
|
||||||
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
|
||||||
is MangaSource -> listOf(obj)
|
|
||||||
is SourceFactory -> obj.createSources()
|
|
||||||
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.log("Extension load error: $extName ($it)")
|
|
||||||
return MangaLoadResult.Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val langs = sources.filterIsInstance<CatalogueSource>()
|
|
||||||
.map { it.lang }
|
|
||||||
.toSet()
|
|
||||||
val lang = when (langs.size) {
|
|
||||||
0 -> ""
|
|
||||||
1 -> langs.first()
|
|
||||||
else -> "all"
|
|
||||||
}
|
|
||||||
|
|
||||||
val extension = MangaExtension.Installed(
|
|
||||||
name = extName,
|
|
||||||
pkgName = pkgName,
|
|
||||||
versionName = versionName,
|
|
||||||
versionCode = versionCode,
|
|
||||||
libVersion = libVersion,
|
|
||||||
lang = lang,
|
|
||||||
isNsfw = isNsfw,
|
|
||||||
hasReadme = hasReadme,
|
|
||||||
hasChangelog = hasChangelog,
|
|
||||||
sources = sources,
|
|
||||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
|
||||||
isUnofficial = signatureHash != officialSignature,
|
|
||||||
icon = context.getApplicationIcon(pkgName),
|
|
||||||
)
|
|
||||||
return MangaLoadResult.Success(extension)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given package is an extension.
|
|
||||||
*
|
|
||||||
* @param pkgInfo The package info of the application.
|
|
||||||
*/
|
|
||||||
private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
|
|
||||||
return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the signature hash of the package or null if it's not signed.
|
|
||||||
*
|
|
||||||
* @param pkgInfo The package info of the application.
|
|
||||||
*/
|
|
||||||
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
|
||||||
val signatures = pkgInfo.signatures
|
|
||||||
return if (signatures != null && signatures.isNotEmpty()) {
|
|
||||||
Hash.sha256(signatures.first().toByteArray())
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,17 @@
|
||||||
package eu.kanade.tachiyomi.extension.manga.util
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
|
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||||
import ani.dantotsu.themes.ThemeManager
|
import ani.dantotsu.themes.ThemeManager
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
||||||
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -16,18 +22,24 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
* Activity used to install extensions, because we can only receive the result of the installation
|
* Activity used to install extensions, because we can only receive the result of the installation
|
||||||
* with [startActivityForResult], which we need to update the UI.
|
* with [startActivityForResult], which we need to update the UI.
|
||||||
*/
|
*/
|
||||||
class MangaExtensionInstallActivity : Activity() {
|
class ExtensionInstallActivity : AppCompatActivity() {
|
||||||
|
|
||||||
// MIUI package installer bug workaround
|
// MIUI package installer bug workaround
|
||||||
private var ignoreUntil = 0L
|
private var ignoreUntil = 0L
|
||||||
private var ignoreResult = false
|
private var ignoreResult = false
|
||||||
private var hasIgnoredResult = false
|
private var hasIgnoredResult = false
|
||||||
|
|
||||||
|
private var type: MediaType? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
ThemeManager(this).applyTheme()
|
ThemeManager(this).applyTheme()
|
||||||
|
|
||||||
|
if (intent.hasExtra(ExtensionInstaller.EXTRA_EXTENSION_TYPE))
|
||||||
|
type = intent.getSerializableExtraCompat<MediaType>(ExtensionInstaller.EXTRA_EXTENSION_TYPE)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
||||||
.setDataAndType(intent.data, intent.type)
|
.setDataAndType(intent.data, intent.type)
|
||||||
.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||||
|
@ -38,8 +50,19 @@ class MangaExtensionInstallActivity : Activity() {
|
||||||
ignoreUntil = System.nanoTime() + 1.seconds.inWholeNanoseconds
|
ignoreUntil = System.nanoTime() + 1.seconds.inWholeNanoseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val onInstallResult = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result: ActivityResult ->
|
||||||
|
if (ignoreResult && System.nanoTime() < ignoreUntil) {
|
||||||
|
hasIgnoredResult = true
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
checkInstallationResult(result.resultCode)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startActivityForResult(installIntent, INSTALL_REQUEST_CODE)
|
onInstallResult.launch(installIntent)
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
// Either install package can't be found (probably bots) or there's a security exception
|
// Either install package can't be found (probably bots) or there's a security exception
|
||||||
// with the download manager. Nothing we can workaround.
|
// with the download manager. Nothing we can workaround.
|
||||||
|
@ -47,17 +70,6 @@ class MangaExtensionInstallActivity : Activity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
if (ignoreResult && System.nanoTime() < ignoreUntil) {
|
|
||||||
hasIgnoredResult = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (requestCode == INSTALL_REQUEST_CODE) {
|
|
||||||
checkInstallationResult(resultCode)
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
if (hasIgnoredResult) {
|
if (hasIgnoredResult) {
|
||||||
|
@ -67,15 +79,23 @@ class MangaExtensionInstallActivity : Activity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkInstallationResult(resultCode: Int) {
|
private fun checkInstallationResult(resultCode: Int) {
|
||||||
val downloadId = intent.extras!!.getLong(MangaExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||||
val extensionManager = Injekt.get<MangaExtensionManager>()
|
|
||||||
val newStep = when (resultCode) {
|
val newStep = when (resultCode) {
|
||||||
RESULT_OK -> InstallStep.Installed
|
RESULT_OK -> InstallStep.Installed
|
||||||
RESULT_CANCELED -> InstallStep.Idle
|
RESULT_CANCELED -> InstallStep.Idle
|
||||||
else -> InstallStep.Error
|
else -> InstallStep.Error
|
||||||
}
|
}
|
||||||
extensionManager.updateInstallStep(downloadId, newStep)
|
when (type) {
|
||||||
|
MediaType.ANIME -> {
|
||||||
|
Injekt.get<AnimeExtensionManager>().updateInstallStep(downloadId, newStep)
|
||||||
|
}
|
||||||
|
MediaType.MANGA -> {
|
||||||
|
Injekt.get<MangaExtensionManager>().updateInstallStep(downloadId, newStep)
|
||||||
|
}
|
||||||
|
MediaType.NOVEL -> {
|
||||||
|
Injekt.get<NovelExtensionManager>().updateInstallStep(downloadId, newStep)
|
||||||
|
}
|
||||||
|
null -> { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val INSTALL_REQUEST_CODE = 500
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import tachiyomi.core.util.lang.launchNow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only
|
||||||
|
* notifies the given [listener] when the package is an extension.
|
||||||
|
*
|
||||||
|
* @param listener The listener that should be notified of extension installation events.
|
||||||
|
*/
|
||||||
|
internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
private var animeListener: AnimeListener? = null
|
||||||
|
private var mangaListener: MangaListener? = null
|
||||||
|
private var type: MediaType? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers this broadcast receiver
|
||||||
|
*/
|
||||||
|
fun register(context: Context) {
|
||||||
|
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAnimeListener(listener: AnimeListener) : ExtensionInstallReceiver {
|
||||||
|
this.type = MediaType.ANIME
|
||||||
|
animeListener = listener
|
||||||
|
this.animeListener
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMangaListener(listener: MangaListener) : ExtensionInstallReceiver {
|
||||||
|
this.type = MediaType.MANGA
|
||||||
|
mangaListener = listener
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the intent filter this receiver should subscribe to.
|
||||||
|
*/
|
||||||
|
private val filter
|
||||||
|
get() = IntentFilter().apply {
|
||||||
|
priority = 100
|
||||||
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
|
addDataScheme("package")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when one of the events of the [filter] is received. When the package is an extension,
|
||||||
|
* it's loaded in background and it notifies the [listener] when finished.
|
||||||
|
*/
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
if (intent == null) return
|
||||||
|
|
||||||
|
when (intent.action) {
|
||||||
|
Intent.ACTION_PACKAGE_ADDED -> {
|
||||||
|
if (isReplacing(intent)) return
|
||||||
|
|
||||||
|
launchNow {
|
||||||
|
when (type) {
|
||||||
|
MediaType.ANIME -> {
|
||||||
|
when (val result = getAnimeExtensionFromIntent(context, intent)) {
|
||||||
|
is AnimeLoadResult.Success -> animeListener?.onExtensionInstalled(result.extension)
|
||||||
|
|
||||||
|
is AnimeLoadResult.Untrusted -> animeListener?.onExtensionUntrusted(result.extension)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MediaType.MANGA -> {
|
||||||
|
when (val result = getMangaExtensionFromIntent(context, intent)) {
|
||||||
|
is MangaLoadResult.Success -> mangaListener?.onExtensionInstalled(result.extension)
|
||||||
|
|
||||||
|
is MangaLoadResult.Untrusted -> mangaListener?.onExtensionUntrusted(result.extension)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||||
|
launchNow {
|
||||||
|
when (type) {
|
||||||
|
MediaType.ANIME -> {
|
||||||
|
when (val result = getAnimeExtensionFromIntent(context, intent)) {
|
||||||
|
is AnimeLoadResult.Success -> animeListener?.onExtensionUpdated(result.extension)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MediaType.MANGA -> {
|
||||||
|
when (val result = getMangaExtensionFromIntent(context, intent)) {
|
||||||
|
is MangaLoadResult.Success -> mangaListener?.onExtensionUpdated(result.extension)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||||
|
if (isReplacing(intent)) return
|
||||||
|
|
||||||
|
val pkgName = getPackageNameFromIntent(intent)
|
||||||
|
if (pkgName != null) {
|
||||||
|
when (type) {
|
||||||
|
MediaType.ANIME -> {
|
||||||
|
animeListener?.onPackageUninstalled(pkgName)
|
||||||
|
}
|
||||||
|
MediaType.MANGA -> {
|
||||||
|
mangaListener?.onPackageUninstalled(pkgName)
|
||||||
|
}
|
||||||
|
else -> { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this package is performing an update.
|
||||||
|
*
|
||||||
|
* @param intent The intent that triggered the event.
|
||||||
|
*/
|
||||||
|
private fun isReplacing(intent: Intent): Boolean {
|
||||||
|
return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the extension triggered by the given intent.
|
||||||
|
*
|
||||||
|
* @param context The application context.
|
||||||
|
* @param intent The intent containing the package name of the extension.
|
||||||
|
*/
|
||||||
|
private suspend fun getAnimeExtensionFromIntent(context: Context, intent: Intent?): AnimeLoadResult {
|
||||||
|
val pkgName = getPackageNameFromIntent(intent)
|
||||||
|
if (pkgName == null) {
|
||||||
|
Logger.log("Package name not found")
|
||||||
|
return AnimeLoadResult.Error
|
||||||
|
}
|
||||||
|
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
|
||||||
|
ExtensionLoader.loadAnimeExtensionFromPkgName(
|
||||||
|
context,
|
||||||
|
pkgName,
|
||||||
|
)
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getMangaExtensionFromIntent(context: Context, intent: Intent?): MangaLoadResult {
|
||||||
|
val pkgName = getPackageNameFromIntent(intent)
|
||||||
|
if (pkgName == null) {
|
||||||
|
Logger.log("Package name not found")
|
||||||
|
return MangaLoadResult.Error
|
||||||
|
}
|
||||||
|
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) {
|
||||||
|
ExtensionLoader.loadMangaExtensionFromPkgName(
|
||||||
|
context,
|
||||||
|
pkgName,
|
||||||
|
)
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the package name of the installed, updated or removed application.
|
||||||
|
*/
|
||||||
|
private fun getPackageNameFromIntent(intent: Intent?): String? {
|
||||||
|
return intent?.data?.encodedSchemeSpecificPart ?: return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener that receives extension installation events.
|
||||||
|
*/
|
||||||
|
interface AnimeListener {
|
||||||
|
fun onExtensionInstalled(extension: AnimeExtension.Installed)
|
||||||
|
fun onExtensionUpdated(extension: AnimeExtension.Installed)
|
||||||
|
fun onExtensionUntrusted(extension: AnimeExtension.Untrusted)
|
||||||
|
fun onPackageUninstalled(pkgName: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MangaListener {
|
||||||
|
fun onExtensionInstalled(extension: MangaExtension.Installed)
|
||||||
|
fun onExtensionUpdated(extension: MangaExtension.Installed)
|
||||||
|
fun onExtensionUntrusted(extension: MangaExtension.Untrusted)
|
||||||
|
fun onPackageUninstalled(pkgName: String)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.manga.util
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -8,18 +8,20 @@ import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import ani.dantotsu.R
|
import ani.dantotsu.R
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
|
import eu.kanade.tachiyomi.extension.installer.PackageInstallerInstaller
|
||||||
import eu.kanade.tachiyomi.extension.manga.installer.PackageInstallerInstallerManga
|
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||||
import eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
||||||
|
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_EXTENSION_TYPE
|
||||||
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
|
|
||||||
class MangaExtensionInstallService : Service() {
|
class ExtensionInstallService : Service() {
|
||||||
|
|
||||||
private var installer: InstallerManga? = null
|
private var installer: Installer? = null
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
|
val notification = notificationBuilder(Notifications.CHANNEL_EXTENSIONS_UPDATE) {
|
||||||
|
@ -43,18 +45,19 @@ class MangaExtensionInstallService : Service() {
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
val uri = intent?.data
|
val uri = intent?.data
|
||||||
|
val type = intent?.getSerializableExtraCompat<MediaType>(EXTRA_EXTENSION_TYPE)
|
||||||
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
|
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
|
||||||
val installerUsed = intent?.getSerializableExtraCompat<BasePreferences.ExtensionInstaller>(
|
val installerUsed = intent?.getSerializableExtraCompat<BasePreferences.ExtensionInstaller>(
|
||||||
EXTRA_INSTALLER,
|
EXTRA_INSTALLER
|
||||||
)
|
)
|
||||||
if (uri == null || id == null || installerUsed == null) {
|
if (uri == null || type == null || id == null || installerUsed == null) {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installer == null) {
|
if (installer == null) {
|
||||||
installer = when (installerUsed) {
|
installer = when (installerUsed) {
|
||||||
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstallerManga(
|
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(
|
||||||
this
|
this
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,7 +68,7 @@ class MangaExtensionInstallService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
installer!!.addToQueue(id, uri)
|
installer!!.addToQueue(type, id, uri)
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,13 +84,15 @@ class MangaExtensionInstallService : Service() {
|
||||||
|
|
||||||
fun getIntent(
|
fun getIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
type: MediaType,
|
||||||
downloadId: Long,
|
downloadId: Long,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
installer: BasePreferences.ExtensionInstaller,
|
installer: BasePreferences.ExtensionInstaller,
|
||||||
): Intent {
|
): Intent {
|
||||||
return Intent(context, MangaExtensionInstallService::class.java)
|
return Intent(context, ExtensionInstallService::class.java)
|
||||||
.setDataAndType(uri, MangaExtensionInstaller.APK_MIME)
|
.setDataAndType(uri, ExtensionInstaller.APK_MIME)
|
||||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||||
|
.putExtra(EXTRA_EXTENSION_TYPE, type)
|
||||||
.putExtra(EXTRA_INSTALLER, installer)
|
.putExtra(EXTRA_INSTALLER, installer)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.extension.manga.util
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
|
@ -6,15 +6,19 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
|
import ani.dantotsu.parsers.novel.NovelExtension
|
||||||
import ani.dantotsu.util.Logger
|
import ani.dantotsu.util.Logger
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
import eu.kanade.tachiyomi.extension.manga.installer.InstallerManga
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
|
import eu.kanade.tachiyomi.extension.installer.Installer
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
@ -29,7 +33,7 @@ import java.util.concurrent.TimeUnit
|
||||||
*
|
*
|
||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
*/
|
*/
|
||||||
internal class MangaExtensionInstaller(private val context: Context) {
|
internal class ExtensionInstaller(private val context: Context) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The system's download manager
|
* The system's download manager
|
||||||
|
@ -61,7 +65,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
* @param url The url of the apk.
|
* @param url The url of the apk.
|
||||||
* @param extension The extension to install.
|
* @param extension The extension to install.
|
||||||
*/
|
*/
|
||||||
fun downloadAndInstall(url: String, extension: MangaExtension) = Observable.defer {
|
fun downloadAndInstall(url: String, extension: AnimeExtension): Observable<InstallStep> = Observable.defer {
|
||||||
val pkgName = extension.pkgName
|
val pkgName = extension.pkgName
|
||||||
|
|
||||||
val oldDownload = activeDownloads[pkgName]
|
val oldDownload = activeDownloads[pkgName]
|
||||||
|
@ -81,6 +85,83 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
Environment.DIRECTORY_DOWNLOADS,
|
Environment.DIRECTORY_DOWNLOADS,
|
||||||
downloadUri.lastPathSegment
|
downloadUri.lastPathSegment
|
||||||
)
|
)
|
||||||
|
.setDescription(MediaType.ANIME.asText())
|
||||||
|
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
|
|
||||||
|
val id = downloadManager.enqueue(request)
|
||||||
|
activeDownloads[pkgName] = id
|
||||||
|
|
||||||
|
downloadsRelay.filter { it.first == id }
|
||||||
|
.map { it.second }
|
||||||
|
// Poll download status
|
||||||
|
.mergeWith(pollStatus(id))
|
||||||
|
// Stop when the application is installed or errors
|
||||||
|
.takeUntil { it.isCompleted() }
|
||||||
|
// Always notify on main thread
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
// Always remove the download when unsubscribed
|
||||||
|
.doOnUnsubscribe { deleteDownload(pkgName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadAndInstall(url: String, extension: MangaExtension): Observable<InstallStep> = Observable.defer {
|
||||||
|
val pkgName = extension.pkgName
|
||||||
|
|
||||||
|
val oldDownload = activeDownloads[pkgName]
|
||||||
|
if (oldDownload != null) {
|
||||||
|
deleteDownload(pkgName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the receiver after removing (and unregistering) the previous download
|
||||||
|
downloadReceiver.register()
|
||||||
|
|
||||||
|
val downloadUri = url.toUri()
|
||||||
|
val request = DownloadManager.Request(downloadUri)
|
||||||
|
.setTitle(extension.name)
|
||||||
|
.setMimeType(APK_MIME)
|
||||||
|
.setDestinationInExternalFilesDir(
|
||||||
|
context,
|
||||||
|
Environment.DIRECTORY_DOWNLOADS,
|
||||||
|
downloadUri.lastPathSegment
|
||||||
|
)
|
||||||
|
.setDescription(MediaType.MANGA.asText())
|
||||||
|
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
|
|
||||||
|
val id = downloadManager.enqueue(request)
|
||||||
|
activeDownloads[pkgName] = id
|
||||||
|
|
||||||
|
downloadsRelay.filter { it.first == id }
|
||||||
|
.map { it.second }
|
||||||
|
// Poll download status
|
||||||
|
.mergeWith(pollStatus(id))
|
||||||
|
// Stop when the application is installed or errors
|
||||||
|
.takeUntil { it.isCompleted() }
|
||||||
|
// Always notify on main thread
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
// Always remove the download when unsubscribed
|
||||||
|
.doOnUnsubscribe { deleteDownload(pkgName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadAndInstall(url: String, extension: NovelExtension) = Observable.defer {
|
||||||
|
val pkgName = extension.pkgName
|
||||||
|
|
||||||
|
val oldDownload = activeDownloads[pkgName]
|
||||||
|
if (oldDownload != null) {
|
||||||
|
deleteDownload(pkgName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the receiver after removing (and unregistering) the previous download
|
||||||
|
downloadReceiver.register()
|
||||||
|
|
||||||
|
val downloadUri = url.toUri()
|
||||||
|
val request = DownloadManager.Request(downloadUri)
|
||||||
|
.setTitle(extension.name)
|
||||||
|
.setMimeType(APK_MIME)
|
||||||
|
.setDestinationInExternalFilesDir(
|
||||||
|
context,
|
||||||
|
Environment.DIRECTORY_DOWNLOADS,
|
||||||
|
downloadUri.lastPathSegment
|
||||||
|
)
|
||||||
|
.setDescription(MediaType.MANGA.asText())
|
||||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
|
|
||||||
val id = downloadManager.enqueue(request)
|
val id = downloadManager.enqueue(request)
|
||||||
|
@ -134,11 +215,12 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
*
|
*
|
||||||
* @param uri The uri of the extension to install.
|
* @param uri The uri of the extension to install.
|
||||||
*/
|
*/
|
||||||
fun installApk(downloadId: Long, uri: Uri) {
|
fun installApk(type: MediaType, downloadId: Long, uri: Uri) {
|
||||||
when (val installer = extensionInstaller.get()) {
|
when (val installer = extensionInstaller.get()) {
|
||||||
BasePreferences.ExtensionInstaller.LEGACY -> {
|
BasePreferences.ExtensionInstaller.LEGACY -> {
|
||||||
val intent = Intent(context, MangaExtensionInstallActivity::class.java)
|
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
||||||
.setDataAndType(uri, APK_MIME)
|
.setDataAndType(uri, APK_MIME)
|
||||||
|
.putExtra(EXTRA_EXTENSION_TYPE, type)
|
||||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
|
||||||
|
@ -147,7 +229,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val intent =
|
val intent =
|
||||||
MangaExtensionInstallService.getIntent(context, downloadId, uri, installer)
|
ExtensionInstallService.getIntent(context, type, downloadId, uri, installer)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,7 +241,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
fun cancelInstall(pkgName: String) {
|
fun cancelInstall(pkgName: String) {
|
||||||
val downloadId = activeDownloads.remove(pkgName) ?: return
|
val downloadId = activeDownloads.remove(pkgName) ?: return
|
||||||
downloadManager.remove(downloadId)
|
downloadManager.remove(downloadId)
|
||||||
InstallerManga.cancelInstallQueue(context, downloadId)
|
Installer.cancelInstallQueue(context, downloadId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -168,10 +250,13 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
* @param pkgName The package name of the extension to uninstall
|
* @param pkgName The package name of the extension to uninstall
|
||||||
*/
|
*/
|
||||||
fun uninstallApk(pkgName: String) {
|
fun uninstallApk(pkgName: String) {
|
||||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
Intent(Intent.ACTION_DELETE).setData("package:$pkgName".toUri())
|
||||||
|
else
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
|
||||||
|
|
||||||
context.startActivity(intent)
|
context.startActivity(intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -255,8 +340,11 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
val localUri = cursor.getString(
|
val localUri = cursor.getString(
|
||||||
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
|
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI),
|
||||||
).removePrefix(FILE_SCHEME)
|
).removePrefix(FILE_SCHEME)
|
||||||
|
val type = MediaType.fromText(cursor.getString(
|
||||||
|
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION),
|
||||||
|
))
|
||||||
|
|
||||||
installApk(id, File(localUri).getUriCompat(context))
|
installApk(type, id, File(localUri).getUriCompat(context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,6 +353,7 @@ internal class MangaExtensionInstaller(private val context: Context) {
|
||||||
companion object {
|
companion object {
|
||||||
const val APK_MIME = "application/vnd.android.package-archive"
|
const val APK_MIME = "application/vnd.android.package-archive"
|
||||||
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
|
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
|
||||||
|
const val EXTRA_EXTENSION_TYPE = "ExtensionInstaller.extra.EXTENSION_TYPE"
|
||||||
const val FILE_SCHEME = "file://"
|
const val FILE_SCHEME = "file://"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,427 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
|
import ani.dantotsu.media.MediaType
|
||||||
|
import ani.dantotsu.util.Logger
|
||||||
|
import dalvik.system.PathClassLoader
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||||
|
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
|
import eu.kanade.tachiyomi.extension.manga.model.MangaLoadResult
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.MangaSource
|
||||||
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
|
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that handles the loading of the extensions. Supports two kinds of extensions:
|
||||||
|
*
|
||||||
|
* 1. Shared extension: This extension is installed to the system with package
|
||||||
|
* installer, so other variants of Tachiyomi and its forks can also use this extension.
|
||||||
|
*
|
||||||
|
* 2. Private extension: This extension is put inside private data directory of the
|
||||||
|
* running app, so this extension can only be used by the running app and not shared
|
||||||
|
* with other apps.
|
||||||
|
*
|
||||||
|
* When both kinds of extensions are installed with a same package name, shared
|
||||||
|
* extension will be used unless the version codes are different. In that case the
|
||||||
|
* one with higher version code will be used.
|
||||||
|
*/
|
||||||
|
internal object ExtensionLoader {
|
||||||
|
|
||||||
|
private val preferences: SourcePreferences by injectLazy()
|
||||||
|
private val loadNsfwSource by lazy {
|
||||||
|
preferences.showNsfwSource().get()
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val ANIME_PACKAGE = "tachiyomi.animeextension"
|
||||||
|
private const val MANGA_PACKAGE = "tachiyomi.extension"
|
||||||
|
|
||||||
|
private const val XX_METADATA_SOURCE_CLASS = ".class"
|
||||||
|
private const val XX_METADATA_SOURCE_FACTORY = ".factory"
|
||||||
|
private const val XX_METADATA_NSFW = "n.nsfw"
|
||||||
|
private const val XX_METADATA_HAS_README = ".hasReadme"
|
||||||
|
private const val XX_METADATA_HAS_CHANGELOG = ".hasChangelog"
|
||||||
|
const val ANIME_LIB_VERSION_MIN = 12
|
||||||
|
const val ANIME_LIB_VERSION_MAX = 15
|
||||||
|
|
||||||
|
const val MANGA_LIB_VERSION_MIN = 1.2
|
||||||
|
const val MANGA_LIB_VERSION_MAX = 1.5
|
||||||
|
|
||||||
|
private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or
|
||||||
|
PackageManager.GET_META_DATA or
|
||||||
|
@Suppress ("DEPRECATION") PackageManager.GET_SIGNATURES or
|
||||||
|
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||||
|
PackageManager.GET_SIGNING_CERTIFICATES else 0)
|
||||||
|
|
||||||
|
// jmir1's key
|
||||||
|
private const val officialSignatureAnime =
|
||||||
|
"50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c"
|
||||||
|
|
||||||
|
var trustedSignaturesAnime =
|
||||||
|
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignatureAnime
|
||||||
|
|
||||||
|
// inorichi's key
|
||||||
|
private const val officialSignatureManga =
|
||||||
|
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of the trusted signatures.
|
||||||
|
*/
|
||||||
|
var trustedSignaturesManga =
|
||||||
|
mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignatureManga
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of all the installed extensions initialized concurrently.
|
||||||
|
*
|
||||||
|
* @param context The application context.
|
||||||
|
*/
|
||||||
|
fun loadAnimeExtensions(context: Context): List<AnimeLoadResult> {
|
||||||
|
val pkgManager = context.packageManager
|
||||||
|
|
||||||
|
|
||||||
|
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
|
||||||
|
} else {
|
||||||
|
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
||||||
|
}
|
||||||
|
|
||||||
|
val extPkgs = installedPkgs.filter { isPackageAnExtension(MediaType.ANIME, it) }
|
||||||
|
|
||||||
|
if (extPkgs.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
// Load each extension concurrently and wait for completion
|
||||||
|
return runBlocking {
|
||||||
|
val deferred = extPkgs.map {
|
||||||
|
async { loadAnimeExtension(context, it.packageName, it) }
|
||||||
|
}
|
||||||
|
deferred.map { it.await() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMangaExtensions(context: Context): List<MangaLoadResult> {
|
||||||
|
val pkgManager = context.packageManager
|
||||||
|
|
||||||
|
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong()))
|
||||||
|
} else {
|
||||||
|
pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
||||||
|
}
|
||||||
|
|
||||||
|
val extPkgs = installedPkgs.filter { isPackageAnExtension(MediaType.MANGA, it) }
|
||||||
|
|
||||||
|
if (extPkgs.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
// Load each extension concurrently and wait for completion
|
||||||
|
return runBlocking {
|
||||||
|
val deferred = extPkgs.map {
|
||||||
|
async { loadMangaExtension(context, it.packageName, it) }
|
||||||
|
}
|
||||||
|
deferred.map { it.await() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to load an extension from the given package name. It checks if the extension
|
||||||
|
* contains the required feature flag before trying to load it.
|
||||||
|
*/
|
||||||
|
fun loadAnimeExtensionFromPkgName(context: Context, pkgName: String): AnimeLoadResult {
|
||||||
|
val pkgInfo = try {
|
||||||
|
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
||||||
|
} catch (error: PackageManager.NameNotFoundException) {
|
||||||
|
// Unlikely, but the package may have been uninstalled at this point
|
||||||
|
Logger.log(error)
|
||||||
|
return AnimeLoadResult.Error
|
||||||
|
}
|
||||||
|
if (!isPackageAnExtension(MediaType.ANIME,pkgInfo)) {
|
||||||
|
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
|
||||||
|
return AnimeLoadResult.Error
|
||||||
|
}
|
||||||
|
return loadAnimeExtension(context, pkgName, pkgInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMangaExtensionFromPkgName(context: Context, pkgName: String): MangaLoadResult {
|
||||||
|
val pkgInfo = try {
|
||||||
|
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
||||||
|
} catch (error: PackageManager.NameNotFoundException) {
|
||||||
|
// Unlikely, but the package may have been uninstalled at this point
|
||||||
|
Logger.log(error)
|
||||||
|
return MangaLoadResult.Error
|
||||||
|
}
|
||||||
|
if (!isPackageAnExtension(MediaType.MANGA, pkgInfo)) {
|
||||||
|
Logger.log("Tried to load a package that wasn't a extension ($pkgName)")
|
||||||
|
return MangaLoadResult.Error
|
||||||
|
}
|
||||||
|
return loadMangaExtension(context, pkgName, pkgInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an extension given its package name.
|
||||||
|
*
|
||||||
|
* @param context The application context.
|
||||||
|
* @param pkgName The package name of the extension to load.
|
||||||
|
* @param pkgInfo The package info of the extension.
|
||||||
|
*/
|
||||||
|
private fun loadAnimeExtension(
|
||||||
|
context: Context,
|
||||||
|
pkgName: String,
|
||||||
|
pkgInfo: PackageInfo
|
||||||
|
): AnimeLoadResult {
|
||||||
|
val pkgManager = context.packageManager
|
||||||
|
|
||||||
|
val appInfo = try {
|
||||||
|
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
||||||
|
} catch (error: PackageManager.NameNotFoundException) {
|
||||||
|
// Unlikely, but the package may have been uninstalled at this point
|
||||||
|
Logger.log(error)
|
||||||
|
return AnimeLoadResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Aniyomi: ")
|
||||||
|
val versionName = pkgInfo.versionName
|
||||||
|
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||||
|
|
||||||
|
if (versionName.isNullOrEmpty()) {
|
||||||
|
Logger.log("Missing versionName for extension $extName")
|
||||||
|
return AnimeLoadResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate lib version
|
||||||
|
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
|
||||||
|
if (libVersion == null || libVersion < ANIME_LIB_VERSION_MIN || libVersion > ANIME_LIB_VERSION_MAX) {
|
||||||
|
Logger.log("Lib version is $libVersion, while only versions " +
|
||||||
|
"$ANIME_LIB_VERSION_MIN to $ANIME_LIB_VERSION_MAX are allowed"
|
||||||
|
)
|
||||||
|
return AnimeLoadResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
val signatureHash = getSignatureHash(pkgInfo)
|
||||||
|
|
||||||
|
if (signatureHash == null) {
|
||||||
|
Logger.log("Package $pkgName isn't signed")
|
||||||
|
return AnimeLoadResult.Error
|
||||||
|
} else if (signatureHash !in trustedSignaturesAnime) {
|
||||||
|
val extension = AnimeExtension.Untrusted(
|
||||||
|
extName,
|
||||||
|
pkgName,
|
||||||
|
versionName,
|
||||||
|
versionCode,
|
||||||
|
libVersion,
|
||||||
|
signatureHash
|
||||||
|
)
|
||||||
|
Logger.log("Extension $pkgName isn't trusted")
|
||||||
|
return AnimeLoadResult.Untrusted(extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isNsfw = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_NSFW") == 1
|
||||||
|
if (!loadNsfwSource && isNsfw) {
|
||||||
|
Logger.log("NSFW extension $pkgName not allowed")
|
||||||
|
return AnimeLoadResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasReadme = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_HAS_README", 0) == 1
|
||||||
|
val hasChangelog = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_HAS_CHANGELOG", 0) == 1
|
||||||
|
|
||||||
|
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||||
|
|
||||||
|
val sources = appInfo.metaData.getString("$ANIME_PACKAGE$XX_METADATA_SOURCE_CLASS")!!
|
||||||
|
.split(";")
|
||||||
|
.map {
|
||||||
|
val sourceClass = it.trim()
|
||||||
|
if (sourceClass.startsWith(".")) {
|
||||||
|
pkgInfo.packageName + sourceClass
|
||||||
|
} else {
|
||||||
|
sourceClass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatMap {
|
||||||
|
try {
|
||||||
|
when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) {
|
||||||
|
is AnimeSource -> listOf(obj)
|
||||||
|
is AnimeSourceFactory -> obj.createSources()
|
||||||
|
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.log("Extension load error: $extName ($it)")
|
||||||
|
return AnimeLoadResult.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val langs = sources.filterIsInstance<AnimeCatalogueSource>()
|
||||||
|
.map { it.lang }
|
||||||
|
.toSet()
|
||||||
|
val lang = when (langs.size) {
|
||||||
|
0 -> ""
|
||||||
|
1 -> langs.first()
|
||||||
|
else -> "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
val extension = AnimeExtension.Installed(
|
||||||
|
name = extName,
|
||||||
|
pkgName = pkgName,
|
||||||
|
versionName = versionName,
|
||||||
|
versionCode = versionCode,
|
||||||
|
libVersion = libVersion,
|
||||||
|
lang = lang,
|
||||||
|
isNsfw = isNsfw,
|
||||||
|
hasReadme = hasReadme,
|
||||||
|
hasChangelog = hasChangelog,
|
||||||
|
sources = sources,
|
||||||
|
pkgFactory = appInfo.metaData.getString("$ANIME_PACKAGE$XX_METADATA_SOURCE_FACTORY"),
|
||||||
|
isUnofficial = signatureHash != officialSignatureAnime,
|
||||||
|
icon = context.getApplicationIcon(pkgName),
|
||||||
|
)
|
||||||
|
return AnimeLoadResult.Success(extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadMangaExtension(
|
||||||
|
context: Context,
|
||||||
|
pkgName: String,
|
||||||
|
pkgInfo: PackageInfo
|
||||||
|
): MangaLoadResult {
|
||||||
|
val pkgManager = context.packageManager
|
||||||
|
|
||||||
|
val appInfo = try {
|
||||||
|
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
||||||
|
} catch (error: PackageManager.NameNotFoundException) {
|
||||||
|
// Unlikely, but the package may have been uninstalled at this point
|
||||||
|
Logger.log(error)
|
||||||
|
return MangaLoadResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
val extName =
|
||||||
|
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
||||||
|
val versionName = pkgInfo.versionName
|
||||||
|
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||||
|
|
||||||
|
if (versionName.isNullOrEmpty()) {
|
||||||
|
Logger.log("Missing versionName for extension $extName")
|
||||||
|
return MangaLoadResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate lib version
|
||||||
|
val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull()
|
||||||
|
if (libVersion == null || libVersion < MANGA_LIB_VERSION_MIN || libVersion > MANGA_LIB_VERSION_MAX) {
|
||||||
|
Logger.log("Lib version is $libVersion, while only versions " +
|
||||||
|
"$MANGA_LIB_VERSION_MIN to $MANGA_LIB_VERSION_MAX are allowed"
|
||||||
|
)
|
||||||
|
return MangaLoadResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
val signatureHash = getSignatureHash(pkgInfo)
|
||||||
|
|
||||||
|
/* temporarily disabling signature check TODO: remove?
|
||||||
|
if (signatureHash == null) {
|
||||||
|
Logger.log("Package $pkgName isn't signed")
|
||||||
|
return MangaLoadResult.Error
|
||||||
|
} else if (signatureHash !in trustedSignatures) {
|
||||||
|
val extension = MangaExtension.Untrusted(extName, pkgName, versionName, versionCode, libVersion, signatureHash)
|
||||||
|
Logger.log("Extension $pkgName isn't trusted")
|
||||||
|
return MangaLoadResult.Untrusted(extension)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
val isNsfw = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_NSFW") == 1
|
||||||
|
if (!loadNsfwSource && isNsfw) {
|
||||||
|
Logger.log("NSFW extension $pkgName not allowed")
|
||||||
|
return MangaLoadResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasReadme = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_HAS_README", 0) == 1
|
||||||
|
val hasChangelog = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_HAS_CHANGELOG", 0) == 1
|
||||||
|
|
||||||
|
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||||
|
|
||||||
|
val sources = appInfo.metaData.getString("$MANGA_PACKAGE$XX_METADATA_SOURCE_CLASS")!!
|
||||||
|
.split(";")
|
||||||
|
.map {
|
||||||
|
val sourceClass = it.trim()
|
||||||
|
if (sourceClass.startsWith(".")) {
|
||||||
|
pkgInfo.packageName + sourceClass
|
||||||
|
} else {
|
||||||
|
sourceClass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatMap {
|
||||||
|
try {
|
||||||
|
when (val obj = Class.forName(it, false, classLoader)
|
||||||
|
.getDeclaredConstructor().newInstance()) {
|
||||||
|
is MangaSource -> listOf(obj)
|
||||||
|
is SourceFactory -> obj.createSources()
|
||||||
|
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.log("Extension load error: $extName ($it)")
|
||||||
|
return MangaLoadResult.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val langs = sources.filterIsInstance<CatalogueSource>()
|
||||||
|
.map { it.lang }
|
||||||
|
.toSet()
|
||||||
|
val lang = when (langs.size) {
|
||||||
|
0 -> ""
|
||||||
|
1 -> langs.first()
|
||||||
|
else -> "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
val extension = MangaExtension.Installed(
|
||||||
|
name = extName,
|
||||||
|
pkgName = pkgName,
|
||||||
|
versionName = versionName,
|
||||||
|
versionCode = versionCode,
|
||||||
|
libVersion = libVersion,
|
||||||
|
lang = lang,
|
||||||
|
isNsfw = isNsfw,
|
||||||
|
hasReadme = hasReadme,
|
||||||
|
hasChangelog = hasChangelog,
|
||||||
|
sources = sources,
|
||||||
|
pkgFactory = appInfo.metaData.getString("$MANGA_PACKAGE$XX_METADATA_SOURCE_FACTORY"),
|
||||||
|
isUnofficial = signatureHash != officialSignatureManga,
|
||||||
|
icon = context.getApplicationIcon(pkgName),
|
||||||
|
)
|
||||||
|
return MangaLoadResult.Success(extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given package is an extension.
|
||||||
|
*
|
||||||
|
* @param pkgInfo The package info of the application.
|
||||||
|
*/
|
||||||
|
private fun isPackageAnExtension(type: MediaType, pkgInfo: PackageInfo): Boolean {
|
||||||
|
return pkgInfo.reqFeatures.orEmpty().any { it.name == when (type) {
|
||||||
|
MediaType.ANIME -> ANIME_PACKAGE
|
||||||
|
MediaType.MANGA -> MANGA_PACKAGE
|
||||||
|
else -> ""
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the signature hash of the package or null if it's not signed.
|
||||||
|
*
|
||||||
|
* @param pkgInfo The package info of the application.
|
||||||
|
*/
|
||||||
|
private fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
||||||
|
val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||||
|
pkgInfo.signingInfo.signingCertificateHistory
|
||||||
|
else
|
||||||
|
@Suppress ("DEPRECATION") pkgInfo.signatures
|
||||||
|
return if (signatures != null && signatures.isNotEmpty()) {
|
||||||
|
Hash.sha256(signatures.first().toByteArray())
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,8 +19,7 @@ fun GET(
|
||||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||||
): Request {
|
): Request {
|
||||||
val nUrl = url.toHttpUrl()
|
val nUrl = url.toHttpUrl()
|
||||||
val g = GET(nUrl, headers, cache)
|
return GET(nUrl, headers, cache)
|
||||||
return g
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -82,7 +82,7 @@ class AndroidAnimeSourceManager(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createStubSource(id: Long): StubAnimeSource {
|
private fun createStubSource(id: Long): StubAnimeSource {
|
||||||
return StubAnimeSource(AnimeSourceData(id, "", ""))
|
return StubAnimeSource(AnimeSourceData(id, "", ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ class AndroidMangaSourceManager(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createStubSource(id: Long): StubMangaSource {
|
private fun createStubSource(id: Long): StubMangaSource {
|
||||||
return StubMangaSource(MangaSourceData(id, "", ""))
|
return StubMangaSource(MangaSourceData(id, "", ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
package eu.kanade.tachiyomi.util.system
|
package eu.kanade.tachiyomi.util.system
|
||||||
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.core.content.IntentCompat
|
import androidx.core.content.IntentCompat
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
fun Uri.toShareIntent(context: Context, type: String = "image/*", message: String? = null): Intent {
|
fun Uri.toShareIntent(type: String = "image/*", message: String? = null): Intent {
|
||||||
val uri = this
|
val uri = this
|
||||||
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
@ -36,6 +36,15 @@ inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
|
||||||
return IntentCompat.getParcelableExtra(this, name, T::class.java)
|
return IntentCompat.getParcelableExtra(this, name, T::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Serializable> Bundle.getSerializableCompat(name: String): T? {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
getSerializable(name, T::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
getSerializable(name) as? T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(name: String): T? {
|
inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(name: String): T? {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
getSerializableExtra(name, T::class.java)
|
getSerializableExtra(name, T::class.java)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.util.system
|
package eu.kanade.tachiyomi.util.system
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -17,7 +16,7 @@ object LocaleHelper {
|
||||||
/**
|
/**
|
||||||
* Returns display name of a string language code.
|
* Returns display name of a string language code.
|
||||||
*/
|
*/
|
||||||
fun getSourceDisplayName(lang: String?, context: Context): String {
|
fun getSourceDisplayName(lang: String?): String {
|
||||||
return when (lang) {
|
return when (lang) {
|
||||||
LAST_USED_KEY -> "Last used"
|
LAST_USED_KEY -> "Last used"
|
||||||
PINNED_KEY -> "Pinned"
|
PINNED_KEY -> "Pinned"
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package tachiyomi.domain.source.anime.model
|
package tachiyomi.domain.source.anime.model
|
||||||
|
|
||||||
sealed class Pin(val code: Int) {
|
sealed class Pin(val code: Int) {
|
||||||
object Unpinned : Pin(0b00)
|
data object Unpinned : Pin(0b00)
|
||||||
object Pinned : Pin(0b01)
|
data object Pinned : Pin(0b01)
|
||||||
object Actual : Pin(0b10)
|
data object Actual : Pin(0b10)
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins {
|
inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins {
|
||||||
|
|
|
@ -21,8 +21,8 @@ class LocalAnimeSource(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
) : AnimeCatalogueSource, UnmeteredSource {
|
) : AnimeCatalogueSource, UnmeteredSource {
|
||||||
|
|
||||||
private val POPULAR_FILTERS = AnimeFilterList(AnimeOrderBy.Popular(context))
|
private val POPULAR_FILTERS = AnimeFilterList(AnimeOrderBy.Popular())
|
||||||
private val LATEST_FILTERS = AnimeFilterList(AnimeOrderBy.Latest(context))
|
private val LATEST_FILTERS = AnimeFilterList(AnimeOrderBy.Latest())
|
||||||
|
|
||||||
override val name = "Local anime source"
|
override val name = "Local anime source"
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ class LocalAnimeSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
override fun getFilterList() = AnimeFilterList(AnimeOrderBy.Popular(context))
|
override fun getFilterList() = AnimeFilterList(AnimeOrderBy.Popular())
|
||||||
|
|
||||||
// Unused stuff
|
// Unused stuff
|
||||||
override suspend fun getVideoList(episode: SEpisode) =
|
override suspend fun getVideoList(episode: SEpisode) =
|
||||||
|
|
|
@ -19,8 +19,8 @@ class LocalMangaSource(
|
||||||
) : CatalogueSource, UnmeteredSource {
|
) : CatalogueSource, UnmeteredSource {
|
||||||
|
|
||||||
|
|
||||||
private val POPULAR_FILTERS = FilterList(MangaOrderBy.Popular(context))
|
private val POPULAR_FILTERS = FilterList(MangaOrderBy.Popular())
|
||||||
private val LATEST_FILTERS = FilterList(MangaOrderBy.Latest(context))
|
private val LATEST_FILTERS = FilterList(MangaOrderBy.Latest())
|
||||||
|
|
||||||
override val name: String = "Local manga source"
|
override val name: String = "Local manga source"
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ class LocalMangaSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
override fun getFilterList() = FilterList(MangaOrderBy.Popular(context))
|
override fun getFilterList() = FilterList(MangaOrderBy.Popular())
|
||||||
|
|
||||||
// Unused stuff
|
// Unused stuff
|
||||||
override suspend fun getPageList(chapter: SChapter) =
|
override suspend fun getPageList(chapter: SChapter) =
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
package tachiyomi.source.local.filter.anime
|
package tachiyomi.source.local.filter.anime
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||||
|
|
||||||
sealed class AnimeOrderBy(context: Context, selection: Selection) : AnimeFilter.Sort(
|
sealed class AnimeOrderBy(selection: Selection) : AnimeFilter.Sort(
|
||||||
|
|
||||||
"Order by",
|
"Order by",
|
||||||
arrayOf("Title", "Date"),
|
arrayOf("Title", "Date"),
|
||||||
selection,
|
selection,
|
||||||
) {
|
) {
|
||||||
class Popular(context: Context) : AnimeOrderBy(context, Selection(0, true))
|
class Popular : AnimeOrderBy(Selection(0, true))
|
||||||
class Latest(context: Context) : AnimeOrderBy(context, Selection(1, false))
|
class Latest : AnimeOrderBy(Selection(1, false))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
package tachiyomi.source.local.filter.manga
|
package tachiyomi.source.local.filter.manga
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
sealed class MangaOrderBy(context: Context, selection: Selection) : Filter.Sort(
|
sealed class MangaOrderBy(selection: Selection) : Filter.Sort(
|
||||||
"Order by",
|
"Order by",
|
||||||
arrayOf("Title", "Date"),
|
arrayOf("Title", "Date"),
|
||||||
selection,
|
selection,
|
||||||
) {
|
) {
|
||||||
class Popular(context: Context) : MangaOrderBy(context, Selection(0, true))
|
class Popular : MangaOrderBy(Selection(0, true))
|
||||||
class Latest(context: Context) : MangaOrderBy(context, Selection(1, false))
|
class Latest : MangaOrderBy(Selection(1, false))
|
||||||
}
|
}
|
||||||
|
|
|
@ -452,7 +452,7 @@
|
||||||
android:minHeight="64dp"
|
android:minHeight="64dp"
|
||||||
android:paddingStart="32dp"
|
android:paddingStart="32dp"
|
||||||
android:paddingEnd="32dp"
|
android:paddingEnd="32dp"
|
||||||
android:text="Auto Hide Time Stamps"
|
android:text="@string/auto_hide_time_stamps"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
android:textColor="@color/bg_opp"
|
android:textColor="@color/bg_opp"
|
||||||
app:cornerRadius="0dp"
|
app:cornerRadius="0dp"
|
||||||
|
@ -1175,7 +1175,7 @@
|
||||||
android:minHeight="64dp"
|
android:minHeight="64dp"
|
||||||
android:paddingStart="32dp"
|
android:paddingStart="32dp"
|
||||||
android:paddingEnd="32dp"
|
android:paddingEnd="32dp"
|
||||||
android:text="Show Rotate Button"
|
android:text="@string/show_rotate_button"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
android:textColor="@color/bg_opp"
|
android:textColor="@color/bg_opp"
|
||||||
app:cornerRadius="0dp"
|
app:cornerRadius="0dp"
|
||||||
|
|
|
@ -86,7 +86,7 @@
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:paddingStart="32dp"
|
android:paddingStart="32dp"
|
||||||
android:paddingEnd="32dp"
|
android:paddingEnd="32dp"
|
||||||
android:text="Default Manga Settings"
|
android:text="@string/default_manga_settings"
|
||||||
android:textColor="?attr/colorSecondary"
|
android:textColor="?attr/colorSecondary"
|
||||||
app:drawableEndCompat="@drawable/ic_round_arrow_drop_down_24"
|
app:drawableEndCompat="@drawable/ic_round_arrow_drop_down_24"
|
||||||
tools:ignore="TextContrastCheck" />
|
tools:ignore="TextContrastCheck" />
|
||||||
|
@ -1134,7 +1134,7 @@
|
||||||
android:minHeight="64dp"
|
android:minHeight="64dp"
|
||||||
android:paddingStart="32dp"
|
android:paddingStart="32dp"
|
||||||
android:paddingEnd="32dp"
|
android:paddingEnd="32dp"
|
||||||
android:text="Use Dark Theme"
|
android:text="@string/use_dark_theme"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
android:textColor="?attr/colorOnBackground"
|
android:textColor="?attr/colorOnBackground"
|
||||||
app:cornerRadius="0dp"
|
app:cornerRadius="0dp"
|
||||||
|
@ -1155,7 +1155,7 @@
|
||||||
android:minHeight="64dp"
|
android:minHeight="64dp"
|
||||||
android:paddingStart="32dp"
|
android:paddingStart="32dp"
|
||||||
android:paddingEnd="32dp"
|
android:paddingEnd="32dp"
|
||||||
android:text="Use OLED Theme"
|
android:text="@string/use_oled_theme"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
android:textColor="?attr/colorOnBackground"
|
android:textColor="?attr/colorOnBackground"
|
||||||
app:cornerRadius="0dp"
|
app:cornerRadius="0dp"
|
||||||
|
|
|
@ -504,7 +504,7 @@
|
||||||
android:elegantTextHeight="true"
|
android:elegantTextHeight="true"
|
||||||
android:fontFamily="@font/poppins_bold"
|
android:fontFamily="@font/poppins_bold"
|
||||||
android:minHeight="64dp"
|
android:minHeight="64dp"
|
||||||
android:text="Use Dark Theme"
|
android:text="@string/use_dark_theme"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
android:textColor="?attr/colorOnBackground"
|
android:textColor="?attr/colorOnBackground"
|
||||||
app:cornerRadius="0dp"
|
app:cornerRadius="0dp"
|
||||||
|
@ -523,7 +523,7 @@
|
||||||
android:elegantTextHeight="true"
|
android:elegantTextHeight="true"
|
||||||
android:fontFamily="@font/poppins_bold"
|
android:fontFamily="@font/poppins_bold"
|
||||||
android:minHeight="64dp"
|
android:minHeight="64dp"
|
||||||
android:text="Use OLED Theme"
|
android:text="@string/use_oled_theme"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
android:textColor="?attr/colorOnBackground"
|
android:textColor="?attr/colorOnBackground"
|
||||||
app:cornerRadius="0dp"
|
app:cornerRadius="0dp"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:theme="@style/Theme.Dantotsu.AppWidgetContainer">
|
android:theme="@style/Theme.Dantotsu.AppWidgetContainer">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
@ -8,7 +9,8 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/gradient_background" />
|
android:src="@drawable/gradient_background"
|
||||||
|
tools:ignore="ContentDescription"/>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/widgetContainer"
|
android:id="@+id/widgetContainer"
|
||||||
|
@ -23,7 +25,8 @@
|
||||||
android:layout_alignParentStart="true"
|
android:layout_alignParentStart="true"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
android:foregroundGravity="center_vertical"
|
android:foregroundGravity="center_vertical"
|
||||||
android:src="@drawable/ic_dantotsu_round" />
|
android:src="@drawable/ic_dantotsu_round"
|
||||||
|
tools:ignore="ContentDescription"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/widgetTitle"
|
android:id="@+id/widgetTitle"
|
||||||
|
@ -34,7 +37,7 @@
|
||||||
android:layout_marginStart="0dp"
|
android:layout_marginStart="0dp"
|
||||||
android:layout_toEndOf="@+id/logoView"
|
android:layout_toEndOf="@+id/logoView"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:text="Currently Airing"
|
android:text="@string/currently_airing"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
@ -44,12 +47,12 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_below="@id/widgetTitle" />
|
android:layout_below="@id/widgetTitle" />
|
||||||
|
|
||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
<TextView
|
||||||
android:id="@+id/empty_view"
|
android:id="@+id/empty_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="No shows to display"
|
android:text="@string/no_shows_to_display"
|
||||||
android:textColor="#ffffff"
|
android:textColor="#ffffff"
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<!-- custom_dialog_layout.xml -->
|
<!-- custom_dialog_layout.xml -->
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
android:fontFamily="@font/poppins_bold"
|
android:fontFamily="@font/poppins_bold"
|
||||||
android:gravity="center|start"
|
android:gravity="center|start"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="Scanlators"
|
android:text="@string/scanlators"
|
||||||
android:textColor="?attr/colorOnBackground"
|
android:textColor="?attr/colorOnBackground"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
@ -40,7 +41,8 @@
|
||||||
android:layout_marginEnd="5dp"
|
android:layout_marginEnd="5dp"
|
||||||
android:background="@null"
|
android:background="@null"
|
||||||
android:src="@drawable/untick_all_boxes"
|
android:src="@drawable/untick_all_boxes"
|
||||||
app:tint="?attr/colorPrimary" />
|
app:tint="?attr/colorPrimary"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
android:inputType="number"
|
android:inputType="number"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:maxLength="4"
|
android:maxLength="4"
|
||||||
android:autofillHints="e.g. 1" />
|
android:autofillHints="e.g. 1"
|
||||||
|
tools:ignore="LabelFor" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -120,7 +120,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:alpha="0.58"
|
android:alpha="0.58"
|
||||||
android:fontFamily="@font/poppins_bold"
|
android:fontFamily="@font/poppins_bold"
|
||||||
android:text="Sort" />
|
android:text="@string/sort" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/sortText"
|
android:id="@+id/sortText"
|
||||||
|
@ -169,7 +169,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:alpha="0.58"
|
android:alpha="0.58"
|
||||||
android:fontFamily="@font/poppins_bold"
|
android:fontFamily="@font/poppins_bold"
|
||||||
android:text="Download" />
|
android:text="@string/download" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/downloadNo"
|
android:id="@+id/downloadNo"
|
||||||
|
@ -215,7 +215,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:alpha="0.58"
|
android:alpha="0.58"
|
||||||
android:fontFamily="@font/poppins_bold"
|
android:fontFamily="@font/poppins_bold"
|
||||||
android:text="Scanlator" />
|
android:text="@string/scanlator" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/scanlatorNo"
|
android:id="@+id/scanlatorNo"
|
||||||
|
@ -263,13 +263,13 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:alpha="0.58"
|
android:alpha="0.58"
|
||||||
android:fontFamily="@font/poppins_bold"
|
android:fontFamily="@font/poppins_bold"
|
||||||
android:text="Set Cookies" />
|
android:text="@string/set_cookies" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:fontFamily="@font/poppins_bold"
|
android:fontFamily="@font/poppins_bold"
|
||||||
android:text="Open Website"
|
android:text="@string/open_website"
|
||||||
android:textColor="?attr/colorSecondary"
|
android:textColor="?attr/colorSecondary"
|
||||||
tools:ignore="TextContrastCheck" />
|
tools:ignore="TextContrastCheck" />
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:fontFamily="@font/poppins"
|
android:fontFamily="@font/poppins"
|
||||||
android:text="Exporting credentials requires a password for encryption."
|
android:text="@string/exporting_requires_encryption"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,8 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="32dp">
|
android:padding="32dp"
|
||||||
|
android:baselineAligned="false">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
|
@ -22,12 +22,12 @@
|
||||||
<com.google.android.material.tabs.TabItem
|
<com.google.android.material.tabs.TabItem
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Anime Queue(WIP)" />
|
android:text="@string/anime_queue" />
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabItem
|
<com.google.android.material.tabs.TabItem
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Manga Queue(WIP)" />
|
android:text="@string/manga_queue" />
|
||||||
</com.google.android.material.tabs.TabLayout>
|
</com.google.android.material.tabs.TabLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue