Addons (#368)
* feat: (wip) torrent credit to kuukiyomi * fix: extensions -> addons * fix: unified loader * feat: (wip) modularity * fix: addon ui * feat: addon install/uninstall --------- Co-authored-by: aayush262 <aayushthakur262006@gmail.com>
This commit is contained in:
parent
3d1040b280
commit
670d16bd8e
66 changed files with 1923 additions and 427 deletions
32
.github/workflows/beta.yml
vendored
32
.github/workflows/beta.yml
vendored
|
@ -84,18 +84,13 @@ jobs:
|
|||
- name: Build with Gradle
|
||||
run: ./gradlew assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Upload Build Artifacts
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: Dantotsu-Split
|
||||
name: Dantotsu
|
||||
retention-days: 5
|
||||
compression-level: 9
|
||||
path: |
|
||||
app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk
|
||||
app/build/outputs/apk/google/alpha/app-google-armeabi-v7a-alpha.apk
|
||||
app/build/outputs/apk/google/alpha/app-google-arm64-v8a-alpha.apk
|
||||
app/build/outputs/apk/google/alpha/app-google-x86-alpha.apk
|
||||
app/build/outputs/apk/google/alpha/app-google-x86_64-alpha.apk
|
||||
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
|
||||
|
||||
- name: Upload APK to Discord and Telegram
|
||||
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
|
||||
|
@ -110,30 +105,11 @@ jobs:
|
|||
fi
|
||||
contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
||||
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-armeabi-v7a-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-arm64-v8a-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-x86-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-x86_64-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
#Telegram
|
||||
curl -X POST \
|
||||
-d chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }} \
|
||||
-d text="Alpha-Build: ${VERSION}: ${commit_messages}" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-armeabi-v7a-alpha.apk" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-arm64-v8a-alpha.apk" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-x86-alpha.apk" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-x86_64-alpha.apk" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk" \
|
||||
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
|
||||
env:
|
||||
|
|
|
@ -21,14 +21,7 @@ android {
|
|||
versionName "3.0.0"
|
||||
versionCode 300000000
|
||||
signingConfig signingConfigs.debug
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
flavorDimensions += "store"
|
||||
|
@ -158,7 +151,7 @@ dependencies {
|
|||
// String Matching
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
|
||||
implementation group: 'com.arthenica', name: 'ffmpeg-kit-full-gpl', version: '6.0-2.LTS'
|
||||
//implementation group: 'com.arthenica', name: 'ffmpeg-kit-full-gpl', version: '6.0-2.LTS'
|
||||
//implementation 'com.github.yausername.youtubedl-android:library:0.15.0'
|
||||
|
||||
// Aniyomi
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="go.server.gojni" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
@ -147,6 +147,9 @@
|
|||
<activity
|
||||
android:name=".settings.SettingsExtensionsActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsAddonActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsMangaActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
|
@ -427,6 +430,10 @@
|
|||
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service android:name=".addons.torrent.ServerService"
|
||||
android:exported="false"
|
||||
android:stopWithTask="true"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
|
|
|
@ -6,10 +6,12 @@ import android.content.Context
|
|||
import android.os.Bundle
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
||||
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.notifications.TaskScheduler
|
||||
import ani.dantotsu.others.DisabledReports
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
|
@ -41,6 +43,9 @@ class App : MultiDexApplication() {
|
|||
private lateinit var animeExtensionManager: AnimeExtensionManager
|
||||
private lateinit var mangaExtensionManager: MangaExtensionManager
|
||||
private lateinit var novelExtensionManager: NovelExtensionManager
|
||||
private lateinit var torrentAddonManager: TorrentAddonManager
|
||||
private lateinit var downloadAddonManager: DownloadAddonManager
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
MultiDex.install(this)
|
||||
|
@ -96,6 +101,8 @@ class App : MultiDexApplication() {
|
|||
animeExtensionManager = Injekt.get()
|
||||
mangaExtensionManager = Injekt.get()
|
||||
novelExtensionManager = Injekt.get()
|
||||
torrentAddonManager = Injekt.get()
|
||||
downloadAddonManager = Injekt.get()
|
||||
|
||||
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||
animeScope.launch {
|
||||
|
@ -115,6 +122,11 @@ class App : MultiDexApplication() {
|
|||
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
val addonScope = CoroutineScope(Dispatchers.Default)
|
||||
addonScope.launch {
|
||||
torrentAddonManager.init()
|
||||
downloadAddonManager.init()
|
||||
}
|
||||
val commentsScope = CoroutineScope(Dispatchers.Default)
|
||||
commentsScope.launch {
|
||||
CommentsAPI.fetchAuthToken()
|
||||
|
|
|
@ -3,7 +3,6 @@ package ani.dantotsu
|
|||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.Animatable
|
||||
|
@ -20,7 +19,6 @@ import android.view.ViewGroup
|
|||
import android.view.animation.AnticipateInterpolator
|
||||
import android.widget.TextView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
@ -35,14 +33,14 @@ import androidx.fragment.app.FragmentManager
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.databinding.ActivityMainBinding
|
||||
import ani.dantotsu.databinding.SplashScreenBinding
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.addons.torrent.ServerService
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.home.AnimeFragment
|
||||
import ani.dantotsu.home.HomeFragment
|
||||
import ani.dantotsu.home.LoginFragment
|
||||
|
@ -70,11 +68,13 @@ import com.google.android.material.textfield.TextInputEditText
|
|||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.Serializable
|
||||
|
@ -87,6 +87,7 @@ class MainActivity : AppCompatActivity() {
|
|||
private var load = false
|
||||
|
||||
|
||||
@kotlin.OptIn(DelicateCoroutinesApi::class)
|
||||
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -453,16 +454,26 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
/*lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
|
||||
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||
val downloadCursor = index.getDownloads()
|
||||
while (downloadCursor.moveToNext()) {
|
||||
val download = downloadCursor.download
|
||||
if (download.state == Download.STATE_FAILED) {
|
||||
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
||||
|
||||
val torrentManager = Injekt.get<TorrentAddonManager>()
|
||||
fun startTorrent() {
|
||||
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
|
||||
launchIO {
|
||||
if (!ServerService.isRunning()) {
|
||||
ServerService.start()
|
||||
}
|
||||
}
|
||||
}*/ //TODO: remove this
|
||||
}
|
||||
}
|
||||
if (torrentManager.isInitialized.value == false) {
|
||||
torrentManager.isInitialized.observe(this) {
|
||||
if (it) {
|
||||
startTorrent()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startTorrent()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestart() {
|
||||
|
|
15
app/src/main/java/ani/dantotsu/addons/Addon.kt
Normal file
15
app/src/main/java/ani/dantotsu/addons/Addon.kt
Normal file
|
@ -0,0 +1,15 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
abstract class Addon {
|
||||
abstract val name: String
|
||||
abstract val pkgName: String
|
||||
abstract val versionName: String
|
||||
abstract val versionCode: Long
|
||||
|
||||
abstract class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
) : Addon()
|
||||
}
|
145
app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt
Normal file
145
app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt
Normal file
|
@ -0,0 +1,145 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.DownloadManager
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import ani.dantotsu.BuildConfig
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.settings.InstallerSteps
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
|
||||
class AddonDownloader {
|
||||
companion object {
|
||||
private suspend fun check(repo: String): Pair<String, String> {
|
||||
return try {
|
||||
val res = client.get("https://api.github.com/repos/$repo/releases")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<AppUpdater.GithubResponse>(it)
|
||||
}
|
||||
val r = res.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v", "")
|
||||
val md = r.body ?: ""
|
||||
val version = v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||
|
||||
Logger.log("Git Version : $version")
|
||||
Pair(md, version)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error checking for update")
|
||||
Logger.log(e)
|
||||
Pair("", "")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun hasUpdate(repo: String, currentVersion: String): Boolean {
|
||||
val (_, version) = check(repo)
|
||||
return compareVersion(version, currentVersion)
|
||||
}
|
||||
|
||||
suspend fun update(
|
||||
activity: Activity,
|
||||
manager: AddonManager<*>,
|
||||
repo: String,
|
||||
currentVersion: String
|
||||
) {
|
||||
val (_, version) = check(repo)
|
||||
if (!compareVersion(version, currentVersion)) {
|
||||
toast(activity.getString(R.string.no_update_found))
|
||||
return
|
||||
}
|
||||
MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
val apks =
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<AppUpdater.GithubResponse>().assets?.filter {
|
||||
it.browserDownloadURL.endsWith(
|
||||
".apk"
|
||||
)
|
||||
}
|
||||
val apkToDownload =
|
||||
apks?.find { it.browserDownloadURL.contains(getCurrentABI()) }
|
||||
?: apks?.find { it.browserDownloadURL.contains("universal") }
|
||||
?: apks?.first()
|
||||
apkToDownload?.browserDownloadURL.apply {
|
||||
if (this != null) {
|
||||
val notificationManager =
|
||||
activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val installerSteps = InstallerSteps(notificationManager, activity)
|
||||
manager.install(this)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep -> installerSteps.onInstallStep(installStep) {} },
|
||||
{ error -> installerSteps.onError(error) {} },
|
||||
{ installerSteps.onComplete {} }
|
||||
)
|
||||
}
|
||||
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ABI that the app is most likely running on.
|
||||
* @return The primary ABI for the device.
|
||||
*/
|
||||
private fun getCurrentABI(): String {
|
||||
return if (Build.SUPPORTED_ABIS.isNotEmpty()) {
|
||||
Build.SUPPORTED_ABIS[0]
|
||||
} else "Unknown"
|
||||
}
|
||||
|
||||
private fun compareVersion(newVersion: String, oldVersion: String): Boolean {
|
||||
fun toDouble(list: List<String>): Double {
|
||||
return try {
|
||||
list.mapIndexed { i: Int, s: String ->
|
||||
when (i) {
|
||||
0 -> s.toDouble() * 100
|
||||
1 -> s.toDouble() * 10
|
||||
2 -> s.toDouble()
|
||||
else -> s.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
}.sum()
|
||||
} catch (e: NumberFormatException) {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
val new = toDouble(newVersion.split("."))
|
||||
val curr = toDouble(oldVersion.split("."))
|
||||
return new > curr
|
||||
}
|
||||
|
||||
}
|
||||
}
|
11
app/src/main/java/ani/dantotsu/addons/AddonListener.kt
Normal file
11
app/src/main/java/ani/dantotsu/addons/AddonListener.kt
Normal file
|
@ -0,0 +1,11 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
interface AddonListener {
|
||||
fun onAddonInstalled(result: LoadResult?)
|
||||
fun onAddonUpdated(result: LoadResult?)
|
||||
fun onAddonUninstalled(pkgName: String)
|
||||
|
||||
enum class ListenerAction {
|
||||
INSTALL, UPDATE, UNINSTALL
|
||||
}
|
||||
}
|
137
app/src/main/java/ani/dantotsu/addons/AddonLoader.kt
Normal file
137
app/src/main/java/ani/dantotsu/addons/AddonLoader.kt
Normal file
|
@ -0,0 +1,137 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
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.addons.download.DownloadAddon
|
||||
import ani.dantotsu.addons.download.DownloadAddonApi
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.download.DownloadLoadResult
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonApi
|
||||
import ani.dantotsu.addons.torrent.TorrentAddon
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentLoadResult
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||
|
||||
class AddonLoader {
|
||||
companion object {
|
||||
fun loadExtension(
|
||||
context: Context,
|
||||
packageName: String,
|
||||
className: String,
|
||||
type: AddonType
|
||||
): LoadResult? {
|
||||
val pkgManager = context.packageManager
|
||||
|
||||
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(ExtensionLoader.PACKAGE_FLAGS.toLong()))
|
||||
} else {
|
||||
pkgManager.getInstalledPackages(ExtensionLoader.PACKAGE_FLAGS)
|
||||
}
|
||||
|
||||
val extPkgs = installedPkgs.filter {
|
||||
isPackageAnExtension(
|
||||
packageName,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
if (extPkgs.isEmpty()) return null
|
||||
if (extPkgs.size > 1) throw IllegalStateException("Multiple extensions with the same package name found")
|
||||
|
||||
val pkgName = extPkgs.first().packageName
|
||||
val pkgInfo = extPkgs.first()
|
||||
|
||||
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)
|
||||
throw error
|
||||
}
|
||||
|
||||
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Dantotsu: ")
|
||||
val versionName = pkgInfo.versionName
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||
|
||||
if (versionName.isNullOrEmpty()) {
|
||||
Logger.log("Missing versionName for extension $extName")
|
||||
throw IllegalStateException("Missing versionName for extension $extName")
|
||||
}
|
||||
val classLoader = PathClassLoader(appInfo.sourceDir, appInfo.nativeLibraryDir, context.classLoader)
|
||||
val loadedClass = try {
|
||||
Class.forName(className, false, classLoader)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
}catch (e: Exception) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
}
|
||||
val instance = loadedClass.getDeclaredConstructor().newInstance()
|
||||
|
||||
return when (type) {
|
||||
AddonType.TORRENT -> {
|
||||
val extension = instance as? TorrentAddonApi ?: throw IllegalStateException("Extension is not a TorrentAddonApi")
|
||||
TorrentLoadResult.Success(
|
||||
TorrentAddon.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
extension = extension,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
)
|
||||
}
|
||||
AddonType.DOWNLOAD -> {
|
||||
val extension = instance as? DownloadAddonApi ?: throw IllegalStateException("Extension is not a DownloadAddonApi")
|
||||
DownloadLoadResult.Success(
|
||||
DownloadAddon.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
extension = extension,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? {
|
||||
return when (type) {
|
||||
AddonType.TORRENT -> loadExtension(
|
||||
context,
|
||||
packageName,
|
||||
TorrentAddonManager.TORRENT_CLASS,
|
||||
type
|
||||
)
|
||||
AddonType.DOWNLOAD -> loadExtension(
|
||||
context,
|
||||
packageName,
|
||||
DownloadAddonManager.DOWNLOAD_CLASS,
|
||||
type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPackageAnExtension(type: String, pkgInfo: PackageInfo): Boolean {
|
||||
return pkgInfo.packageName.equals(type)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
44
app/src/main/java/ani/dantotsu/addons/AddonManager.kt
Normal file
44
app/src/main/java/ani/dantotsu/addons/AddonManager.kt
Normal file
|
@ -0,0 +1,44 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.media.AddonType
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import rx.Observable
|
||||
|
||||
abstract class AddonManager<T: Addon.Installed>(
|
||||
private val context: Context
|
||||
) {
|
||||
abstract var extension: T?
|
||||
abstract var name: String
|
||||
abstract var type: AddonType
|
||||
protected val installer by lazy { ExtensionInstaller(context) }
|
||||
var hasUpdate: Boolean = false
|
||||
protected set
|
||||
|
||||
protected var onListenerAction: ((AddonListener.ListenerAction) -> Unit)? = null
|
||||
|
||||
abstract suspend fun init()
|
||||
abstract fun isAvailable(): Boolean
|
||||
abstract fun getVersion(): String?
|
||||
abstract fun getPackageName(): String?
|
||||
abstract fun hadError(context: Context): String?
|
||||
abstract fun updateInstallStep(id: Long, step: InstallStep)
|
||||
abstract fun setInstalling(id: Long)
|
||||
|
||||
fun uninstall() {
|
||||
getPackageName()?.let {
|
||||
installer.uninstallApk(it)
|
||||
}
|
||||
}
|
||||
fun addListenerAction(action: (AddonListener.ListenerAction) -> Unit) {
|
||||
onListenerAction = action
|
||||
}
|
||||
fun removeListenerAction() {
|
||||
onListenerAction = null
|
||||
}
|
||||
|
||||
fun install(url: String): Observable<InstallStep> {
|
||||
return installer.downloadAndInstall(url, getPackageName()?: "", name, type)
|
||||
}
|
||||
}
|
8
app/src/main/java/ani/dantotsu/addons/LoadResult.kt
Normal file
8
app/src/main/java/ani/dantotsu/addons/LoadResult.kt
Normal file
|
@ -0,0 +1,8 @@
|
|||
package ani.dantotsu.addons
|
||||
|
||||
abstract class LoadResult {
|
||||
|
||||
abstract class Success : LoadResult()
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.media.AddonType
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.filter
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.getPackageNameFromIntent
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import tachiyomi.core.util.lang.launchNow
|
||||
|
||||
internal class AddonInstallReceiver : BroadcastReceiver() {
|
||||
private var listener: AddonListener? = null
|
||||
private var type: AddonType? = null
|
||||
|
||||
/**
|
||||
* Registers this broadcast receiver
|
||||
*/
|
||||
fun register(context: Context) {
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
fun setListener(listener: AddonListener, type: AddonType) : AddonInstallReceiver {
|
||||
this.listener = listener
|
||||
this.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
launchNow {
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||
listener?.onAddonInstalled(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.DOWNLOAD
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||
listener?.onAddonInstalled(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.TORRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
launchNow {
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||
listener?.onAddonUpdated(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.DOWNLOAD
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||
listener?.onAddonUpdated(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.TORRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return
|
||||
listener?.onAddonUninstalled(packageName)
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return
|
||||
listener?.onAddonUninstalled(packageName)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.addons.Addon
|
||||
|
||||
sealed class DownloadAddon : Addon() {
|
||||
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
val extension: DownloadAddonApi,
|
||||
val icon: Drawable?,
|
||||
val hasUpdate: Boolean = false,
|
||||
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
interface DownloadAddonApi {
|
||||
|
||||
fun cancelDownload(sessionId: Long)
|
||||
|
||||
fun setDownloadPath(context: Context, uri: Uri): String
|
||||
|
||||
suspend fun executeFFProbe(request: String, logCallback: (String) -> Unit)
|
||||
|
||||
suspend fun executeFFMpeg(request: String, statCallback: (Double) -> Unit): Long
|
||||
|
||||
fun getState(sessionId: Long): String
|
||||
|
||||
fun getStackTrace(sessionId: Long): String?
|
||||
|
||||
fun hadError(sessionId: Long): Boolean
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DownloadAddonManager(
|
||||
private val context: Context
|
||||
) : AddonManager<DownloadAddon.Installed>(context) {
|
||||
|
||||
override var extension: DownloadAddon.Installed? = null
|
||||
override var name: String = "Download Addon"
|
||||
override var type = AddonType.DOWNLOAD
|
||||
|
||||
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||
|
||||
private var error: String? = null
|
||||
|
||||
override suspend fun init() {
|
||||
extension = null
|
||||
error = null
|
||||
hasUpdate = false
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = false
|
||||
}
|
||||
|
||||
AddonInstallReceiver()
|
||||
.setListener(InstallationListener(), type)
|
||||
.register(context)
|
||||
try {
|
||||
val result = AddonLoader.loadExtension(
|
||||
context,
|
||||
DOWNLOAD_PACKAGE,
|
||||
DOWNLOAD_CLASS,
|
||||
AddonType.DOWNLOAD
|
||||
) as? DownloadLoadResult
|
||||
result?.let {
|
||||
if (it is DownloadLoadResult.Success) {
|
||||
extension = it.extension
|
||||
hasUpdate = AddonDownloader.hasUpdate(REPO, it.extension.versionName)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error initializing Download extension")
|
||||
Logger.log(e)
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return extension?.extension != null
|
||||
}
|
||||
|
||||
override fun getVersion(): String? {
|
||||
return extension?.versionName
|
||||
}
|
||||
|
||||
override fun getPackageName(): String? {
|
||||
return extension?.pkgName
|
||||
}
|
||||
|
||||
override fun hadError(context: Context): String? {
|
||||
return if (isInitialized.value == true) {
|
||||
if (error != null) {
|
||||
error
|
||||
} else if (extension != null) {
|
||||
context.getString(R.string.loaded_successfully)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class InstallationListener : AddonListener {
|
||||
override fun onAddonInstalled(result: LoadResult?) {
|
||||
if (result is DownloadLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUpdated(result: LoadResult?) {
|
||||
if (result is DownloadLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUninstalled(pkgName: String) {
|
||||
if (extension?.pkgName == pkgName) {
|
||||
extension = null
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||
installer.updateInstallStep(id, step)
|
||||
}
|
||||
|
||||
override fun setInstalling(id: Long) {
|
||||
installer.updateInstallStep(id, InstallStep.Installing)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
const val DOWNLOAD_PACKAGE = "dantotsu.downloadAddon"
|
||||
const val DOWNLOAD_CLASS = "ani.dantotsu.downloadAddon.DownloadAddon"
|
||||
const val REPO = "rebelonion/Dantotsu-Download-Addon"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package ani.dantotsu.addons.download
|
||||
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
|
||||
open class DownloadLoadResult: LoadResult() {
|
||||
class Success(val extension: DownloadAddon.Installed) : DownloadLoadResult()
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.addons.Addon
|
||||
|
||||
sealed class TorrentAddon : Addon() {
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
val extension: TorrentAddonApi,
|
||||
val icon: Drawable?,
|
||||
val hasUpdate: Boolean = false,
|
||||
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import eu.kanade.tachiyomi.data.torrentServer.model.Torrent
|
||||
|
||||
interface TorrentAddonApi {
|
||||
|
||||
fun startServer(path: String)
|
||||
|
||||
fun stopServer()
|
||||
|
||||
fun echo(): String
|
||||
|
||||
fun removeTorrent(torrent: String)
|
||||
|
||||
fun addTorrent(
|
||||
link: String,
|
||||
title: String,
|
||||
poster: String,
|
||||
data: String,
|
||||
save: Boolean,
|
||||
): Torrent
|
||||
|
||||
fun getLink(torrent: Torrent, index: Int): String
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.addons.download.AddonInstallReceiver
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class TorrentAddonManager(
|
||||
private val context: Context
|
||||
) : AddonManager<TorrentAddon.Installed>(context) {
|
||||
override var extension: TorrentAddon.Installed? = null
|
||||
override var name: String = "Torrent Addon"
|
||||
override var type: AddonType = AddonType.TORRENT
|
||||
var torrentHash: String? = null
|
||||
|
||||
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||
|
||||
private var error: String? = null
|
||||
|
||||
override suspend fun init() {
|
||||
extension = null
|
||||
error = null
|
||||
hasUpdate = false
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = false
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
Logger.log("Torrent extension is not supported on this device.")
|
||||
error = context.getString(R.string.torrent_extension_not_supported)
|
||||
return
|
||||
}
|
||||
|
||||
AddonInstallReceiver()
|
||||
.setListener(InstallationListener(), type)
|
||||
.register(context)
|
||||
try {
|
||||
val result = AddonLoader.loadExtension(
|
||||
context,
|
||||
TORRENT_PACKAGE,
|
||||
TORRENT_CLASS,
|
||||
type
|
||||
) as TorrentLoadResult?
|
||||
result?.let {
|
||||
if (it is TorrentLoadResult.Success) {
|
||||
extension = it.extension
|
||||
hasUpdate = hasUpdate(REPO, it.extension.versionName)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error initializing torrent extension")
|
||||
Logger.log(e)
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return extension?.extension != null
|
||||
}
|
||||
|
||||
override fun getVersion(): String? {
|
||||
return extension?.versionName
|
||||
}
|
||||
|
||||
override fun getPackageName(): String? {
|
||||
return extension?.pkgName
|
||||
}
|
||||
|
||||
override fun hadError(context: Context): String? {
|
||||
return if (isInitialized.value == true) {
|
||||
if (error != null) {
|
||||
error
|
||||
} else if (extension != null) {
|
||||
context.getString(R.string.loaded_successfully)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class InstallationListener : AddonListener {
|
||||
override fun onAddonInstalled(result: LoadResult?) {
|
||||
if (result is TorrentLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUpdated(result: LoadResult?) {
|
||||
if (result is TorrentLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUninstalled(pkgName: String) {
|
||||
if (pkgName == TORRENT_PACKAGE) {
|
||||
extension = null
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||
installer.updateInstallStep(id, step)
|
||||
}
|
||||
|
||||
override fun setInstalling(id: Long) {
|
||||
installer.updateInstallStep(id, InstallStep.Installing)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TORRENT_PACKAGE = "dantotsu.torrentAddon"
|
||||
const val TORRENT_CLASS = "ani.dantotsu.torrentAddon.TorrentAddon"
|
||||
const val REPO = "rebelonion/Dantotsu-Torrent-Addon"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
|
||||
open class TorrentLoadResult: LoadResult() {
|
||||
class Success(val extension: TorrentAddon.Installed) : TorrentLoadResult()
|
||||
}
|
167
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
167
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
|
@ -0,0 +1,167 @@
|
|||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_TORRENT_SERVER
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.ID_TORRENT_SERVER
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
|
||||
class ServerService: Service() {
|
||||
private val serviceScope = CoroutineScope(EmptyCoroutineContext)
|
||||
private val applicationContext = Injekt.get<Application>()
|
||||
private val extension = Injekt.get<TorrentAddonManager>().extension!!.extension
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
): Int {
|
||||
intent?.let {
|
||||
if (it.action != null) {
|
||||
when (it.action) {
|
||||
ACTION_START -> {
|
||||
startServer()
|
||||
notification(applicationContext)
|
||||
return START_STICKY
|
||||
}
|
||||
ACTION_STOP -> {
|
||||
stopServer()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun startServer() {
|
||||
serviceScope.launch {
|
||||
val echo = extension.echo()
|
||||
if (echo == "") {
|
||||
extension.startServer(filesDir.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopServer() {
|
||||
serviceScope.launch {
|
||||
extension.stopServer()
|
||||
applicationContext.cancelNotification(ID_TORRENT_SERVER)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun notification(context: Context) {
|
||||
val exitPendingIntent =
|
||||
PendingIntent.getService(
|
||||
applicationContext,
|
||||
0,
|
||||
Intent(applicationContext, ServerService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val builder = context.notificationBuilder(CHANNEL_TORRENT_SERVER) {
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setContentText("Torrent Server")
|
||||
setContentTitle("Server is running…")
|
||||
setAutoCancel(false)
|
||||
setOngoing(true)
|
||||
setUsesChronometer(true)
|
||||
addAction(
|
||||
R.drawable.ic_circle_cancel,
|
||||
"Stop",
|
||||
exitPendingIntent,
|
||||
)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
ID_TORRENT_SERVER,
|
||||
builder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
)
|
||||
} else {
|
||||
startForeground(ID_TORRENT_SERVER, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_START = "start_torrent_server"
|
||||
const val ACTION_STOP = "stop_torrent_server"
|
||||
|
||||
fun isRunning(): Boolean {
|
||||
with (Injekt.get<Application>().getSystemService(ACTIVITY_SERVICE) as ActivityManager) {
|
||||
@Suppress("DEPRECATION") // We only need our services
|
||||
getRunningServices(Int.MAX_VALUE).forEach {
|
||||
if (ServerService::class.java.name.equals(it.service.className)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun start() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||
action = ACTION_START
|
||||
}
|
||||
Injekt.get<Application>().startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun stop() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
}
|
||||
Injekt.get<Application>().startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun wait(timeout: Int = -1): Boolean {
|
||||
var count = 0
|
||||
if (timeout < 0) {
|
||||
count = -20
|
||||
}
|
||||
var echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||
while (echo == "") {
|
||||
Thread.sleep(1000)
|
||||
count++
|
||||
if (count > timeout) {
|
||||
return false
|
||||
}
|
||||
echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||
}
|
||||
Logger.log("ServerService: Server started: $echo")
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -6,8 +6,10 @@ import androidx.annotation.OptIn
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
|
@ -38,10 +40,13 @@ class AppModule(val app: Application) : InjektModule {
|
|||
addSingletonFactory { DownloadsManager(app) }
|
||||
|
||||
addSingletonFactory { NetworkHelper(app) }
|
||||
addSingletonFactory { NetworkHelper(app).client }
|
||||
|
||||
addSingletonFactory { AnimeExtensionManager(app) }
|
||||
addSingletonFactory { MangaExtensionManager(app) }
|
||||
addSingletonFactory { NovelExtensionManager(app) }
|
||||
addSingletonFactory { TorrentAddonManager(app) }
|
||||
addSingletonFactory { DownloadAddonManager(app) }
|
||||
|
||||
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
||||
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
||||
|
|
|
@ -19,6 +19,7 @@ import androidx.documentfile.provider.DocumentFile
|
|||
import androidx.media3.common.util.UnstableApi
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
|
@ -37,10 +38,6 @@ import ani.dantotsu.toast
|
|||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.forceDelete
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
||||
import com.arthenica.ffmpegkit.FFprobeKit
|
||||
import com.arthenica.ffmpegkit.SessionState
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
|
@ -76,6 +73,7 @@ class AnimeDownloaderService : Service() {
|
|||
private val mutex = Mutex()
|
||||
private var isCurrentlyProcessing = false
|
||||
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
|
||||
private val ffExtension = Injekt.get<DownloadAddonManager>().extension?.extension
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// This is only required for bound services.
|
||||
|
@ -84,6 +82,11 @@ class AnimeDownloaderService : Service() {
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (ffExtension == null) {
|
||||
toast(getString(R.string.download_addon_not_found))
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
builder =
|
||||
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
|
@ -165,7 +168,7 @@ class AnimeDownloaderService : Service() {
|
|||
.map { it.sessionId }.toMutableList()
|
||||
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
|
||||
sessionIds.forEach {
|
||||
FFmpegKit.cancel(it)
|
||||
ffExtension!!.cancelDownload(it)
|
||||
}
|
||||
currentTasks.removeAll { it.getTaskName() == taskName }
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
|
@ -229,7 +232,7 @@ class AnimeDownloaderService : Service() {
|
|||
|
||||
var percent = 0
|
||||
var totalLength = 0.0
|
||||
val path = FFmpegKitConfig.getSafParameterForWrite(
|
||||
val path = ffExtension!!.setDownloadPath(
|
||||
this@AnimeDownloaderService,
|
||||
outputFile.uri
|
||||
)
|
||||
|
@ -242,49 +245,30 @@ class AnimeDownloaderService : Service() {
|
|||
.append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'")
|
||||
}
|
||||
val probeRequest = "-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\""
|
||||
FFprobeKit.executeAsync(
|
||||
probeRequest,
|
||||
{
|
||||
Logger.log("FFprobeKit: $it")
|
||||
}, {
|
||||
if (it.message.toDoubleOrNull() != null) {
|
||||
totalLength = it.message.toDouble()
|
||||
ffExtension.executeFFProbe(
|
||||
probeRequest
|
||||
) {
|
||||
if (it.toDoubleOrNull() != null) {
|
||||
totalLength = it.toDouble()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val headers = headersStringBuilder.toString()
|
||||
var request = "-headers $headers "
|
||||
request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
|
||||
Logger.log("Request: $request")
|
||||
val ffTask =
|
||||
FFmpegKit.executeAsync(request,
|
||||
{ session ->
|
||||
val state: SessionState = session.state
|
||||
val returnCode = session.returnCode
|
||||
// CALLED WHEN SESSION IS EXECUTED
|
||||
Logger.log(
|
||||
java.lang.String.format(
|
||||
"FFmpeg process exited with state %s and rc %s.%s",
|
||||
state,
|
||||
returnCode,
|
||||
session.failStackTrace
|
||||
)
|
||||
)
|
||||
|
||||
}, {
|
||||
// CALLED WHEN SESSION PRINTS LOGS
|
||||
Logger.log(it.message)
|
||||
}) {
|
||||
ffExtension.executeFFMpeg(request) {
|
||||
// CALLED WHEN SESSION GENERATES STATISTICS
|
||||
val timeInMilliseconds = it.time
|
||||
val timeInMilliseconds = it
|
||||
if (timeInMilliseconds > 0 && totalLength > 0) {
|
||||
percent = ((it.time / 1000) / totalLength * 100).toInt()
|
||||
percent = ((it / 1000) / totalLength * 100).toInt()
|
||||
}
|
||||
Logger.log("Statistics: $it")
|
||||
}
|
||||
task.sessionId = ffTask.sessionId
|
||||
task.sessionId = ffTask
|
||||
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
|
||||
ffTask.sessionId
|
||||
ffTask
|
||||
|
||||
saveMediaInfo(task)
|
||||
task.subtitle?.let {
|
||||
|
@ -300,8 +284,8 @@ class AnimeDownloaderService : Service() {
|
|||
}
|
||||
|
||||
// periodically check if the download is complete
|
||||
while (ffTask.state != SessionState.COMPLETED) {
|
||||
if (ffTask.state == SessionState.FAILED) {
|
||||
while (ffExtension.getState(ffTask) != "COMPLETED") {
|
||||
if (ffExtension.getState(ffTask) == "FAILED") {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
|
@ -313,7 +297,7 @@ class AnimeDownloaderService : Service() {
|
|||
)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
toast("${getTaskName(task.title, task.episode)} Download failed")
|
||||
Logger.log("Download failed: ${ffTask.failStackTrace}")
|
||||
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
|
@ -348,8 +332,8 @@ class AnimeDownloaderService : Service() {
|
|||
}
|
||||
kotlinx.coroutines.delay(2000)
|
||||
}
|
||||
if (ffTask.state == SessionState.COMPLETED) {
|
||||
if (ffTask.returnCode.isValueError) {
|
||||
if (ffExtension.getState(ffTask) == "COMPLETED") {
|
||||
if (ffExtension.hadError(ffTask)) {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.content.IntentFilter
|
|||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
|
@ -50,8 +49,6 @@ import okio.sink
|
|||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
|
|
@ -7,49 +7,23 @@ import android.app.AlertDialog
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadHelper
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.media3.exoplayer.scheduler.Requirements
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.okHttpClient
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
object Helper {
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package ani.dantotsu.media
|
||||
|
||||
enum class MediaType {
|
||||
interface Type {
|
||||
fun asText(): String
|
||||
}
|
||||
|
||||
enum class MediaType: Type {
|
||||
ANIME,
|
||||
MANGA,
|
||||
NOVEL;
|
||||
|
||||
fun asText(): String {
|
||||
override fun asText(): String {
|
||||
return when (this) {
|
||||
ANIME -> "Anime"
|
||||
MANGA -> "Manga"
|
||||
|
@ -14,12 +18,34 @@ enum class MediaType {
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun fromText(string : String): MediaType {
|
||||
fun fromText(string : String): MediaType? {
|
||||
return when (string) {
|
||||
"Anime" -> ANIME
|
||||
"Manga" -> MANGA
|
||||
"Novel" -> NOVEL
|
||||
else -> { ANIME }
|
||||
else -> { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class AddonType: Type {
|
||||
TORRENT,
|
||||
DOWNLOAD;
|
||||
|
||||
override fun asText(): String {
|
||||
return when (this) {
|
||||
TORRENT -> "Torrent"
|
||||
DOWNLOAD -> "Download"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromText(string : String): AddonType? {
|
||||
return when (string) {
|
||||
"Torrent" -> TORRENT
|
||||
"Download" -> DOWNLOAD
|
||||
else -> { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,7 @@ import ani.dantotsu.currContext
|
|||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getDirSize
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
|
|
|
@ -32,6 +32,7 @@ import ani.dantotsu.databinding.ItemStreamBinding
|
|||
import ani.dantotsu.databinding.ItemUrlBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.hideSystemBars
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
|
@ -50,9 +51,11 @@ import ani.dantotsu.snackString
|
|||
import ani.dantotsu.tryWith
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.DecimalFormat
|
||||
|
@ -252,32 +255,70 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun startExoplayer(media: Media) {
|
||||
prevEpisode = null
|
||||
|
||||
dismiss()
|
||||
|
||||
episode?.let { ep ->
|
||||
val video = ep.extractors?.find {
|
||||
it.server.name == ep.selectedExtractor
|
||||
}?.videos?.getOrNull(ep.selectedVideo)
|
||||
video?.file?.url?.let { url ->
|
||||
if (url.startsWith("magnet:")) {
|
||||
if (video.file.url.startsWith("magnet:") || video.file.url.endsWith(".torrent")) {
|
||||
val torrentExtension = Injekt.get<TorrentAddonManager>()
|
||||
if (torrentExtension.isAvailable()) {
|
||||
val activity = currActivity() ?: requireActivity()
|
||||
launchIO {
|
||||
val extension = torrentExtension.extension!!.extension
|
||||
torrentExtension.torrentHash?.let {
|
||||
extension.removeTorrent(it)
|
||||
}
|
||||
val index = if (video.file.url.contains("index=")) {
|
||||
video.file.url.substringAfter("index=").toIntOrNull() ?: 0
|
||||
} else 0
|
||||
Logger.log("Sending: ${video.file.url}, ${video.quality}, $index")
|
||||
val currentTorrent = extension.addTorrent(
|
||||
video.file.url, video.quality.toString(), "", "", false
|
||||
)
|
||||
torrentExtension.torrentHash = currentTorrent.hash
|
||||
video.file.url = extension.getLink(currentTorrent, index)
|
||||
Logger.log("Received: ${video.file.url}")
|
||||
if (launch == true) {
|
||||
Intent(activity, ExoplayerView::class.java).apply {
|
||||
ExoplayerView.media = media
|
||||
ExoplayerView.initialized = true
|
||||
startActivity(this)
|
||||
}
|
||||
} else {
|
||||
model.setEpisode(
|
||||
media.anime!!.episodes!![media.anime.selectedEpisode!!]!!,
|
||||
"startExo no launch"
|
||||
)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
externalPlayerResult.launch(exportMagnetIntent(ep, video))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val amnis = "com.amnis"
|
||||
try {
|
||||
startActivity(Intent(
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("market://details?id=$amnis"))
|
||||
Uri.parse("market://details?id=$amnis")
|
||||
)
|
||||
)
|
||||
dismiss()
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
startActivity(Intent(
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://play.google.com/store/apps/details?id=$amnis")
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
|
@ -285,6 +326,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
dismiss()
|
||||
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
|
||||
stopAddingToList()
|
||||
val intent = Intent(activity, ExoplayerView::class.java)
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Parcelable
|
||||
|
@ -13,7 +12,6 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
|
@ -44,7 +42,6 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class NovelReadFragment : Fragment(),
|
||||
DownloadTriggerCallback,
|
||||
|
|
|
@ -560,7 +560,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
|
|||
format = VideoType.CONTAINER
|
||||
}
|
||||
} catch (malformed: MalformedURLException) {
|
||||
if (videoUrl.startsWith("magnet:"))
|
||||
if (videoUrl.startsWith("magnet:") || videoUrl.endsWith(".torrent"))
|
||||
format = VideoType.CONTAINER
|
||||
else
|
||||
throw malformed
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Environment
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
|
@ -13,7 +11,6 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class OfflineMangaParser : MangaParser() {
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package ani.dantotsu.parsers
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Environment
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
|
@ -10,7 +8,6 @@ import ani.dantotsu.media.MediaType
|
|||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
||||
class OfflineNovelParser : NovelParser() {
|
||||
private val downloadManager = Injekt.get<DownloadsManager>()
|
||||
|
|
|
@ -80,48 +80,14 @@ class AnimeExtensionsFragment : Fragment(),
|
|||
if (isAdded) {
|
||||
val notificationManager =
|
||||
requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
val installerSteps = InstallerSteps(notificationManager, context)
|
||||
// Start the installation process
|
||||
animeExtensionManager.installExtension(pkg)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep ->
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||
.setContentTitle(getString(R.string.installing_extension))
|
||||
.setContentText(getString(R.string.install_step, installStep))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{ error ->
|
||||
Injekt.get<CrashlyticsInterface>().logException(error)
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||
.setContentTitle(getString(R.string.installation_failed, error.message))
|
||||
.setContentText(getString(R.string.error_message, error.message))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
notificationManager.notify(1, builder.build())
|
||||
snackString(getString(R.string.installation_failed, error.message))
|
||||
},
|
||||
{
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_download_24)
|
||||
.setContentTitle(getString(R.string.installation_complete))
|
||||
.setContentText(getString(R.string.extension_has_been_installed))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
viewModel.invalidatePager()
|
||||
snackString(getString(R.string.extension_installed))
|
||||
}
|
||||
{ installStep -> installerSteps.onInstallStep(installStep) {} },
|
||||
{ error -> installerSteps.onError(error) {} },
|
||||
{ installerSteps.onComplete { viewModel.invalidatePager() } }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package ani.dantotsu.settings
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
|
@ -31,7 +30,6 @@ import ani.dantotsu.others.AndroidBug5497Workaround
|
|||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
|
|
54
app/src/main/java/ani/dantotsu/settings/InstallerSteps.kt
Normal file
54
app/src/main/java/ani/dantotsu/settings/InstallerSteps.kt
Normal file
|
@ -0,0 +1,54 @@
|
|||
package ani.dantotsu.settings
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.snackString
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class InstallerSteps(private val notificationManager: NotificationManager, private val context: Context) {
|
||||
|
||||
fun onInstallStep(installStep: InstallStep, extra: () -> Unit) {
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||
.setContentTitle(context.getString(R.string.installing_extension))
|
||||
.setContentText(context.getString(R.string.install_step, installStep))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
}
|
||||
|
||||
fun onError(error: Throwable, extra: () -> Unit) {
|
||||
Injekt.get<CrashlyticsInterface>().logException(error)
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||
.setContentTitle(context.getString(R.string.installation_failed, error.message))
|
||||
.setContentText(context.getString(R.string.error_message, error.message))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
notificationManager.notify(1, builder.build())
|
||||
snackString(context.getString(R.string.installation_failed, error.message))
|
||||
}
|
||||
|
||||
fun onComplete(extra: () -> Unit) {
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_download_24)
|
||||
.setContentTitle(context.getString(R.string.installation_complete))
|
||||
.setContentText(context.getString(R.string.extension_has_been_installed))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
snackString(context.getString(R.string.extension_installed))
|
||||
}
|
||||
}
|
|
@ -81,48 +81,15 @@ class MangaExtensionsFragment : Fragment(),
|
|||
val context = requireContext()
|
||||
val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val installerSteps = InstallerSteps(notificationManager, context)
|
||||
|
||||
// Start the installation process
|
||||
mangaExtensionManager.installExtension(pkg)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep ->
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_sync_24)
|
||||
.setContentTitle(getString(R.string.installing_extension))
|
||||
.setContentText(getString(R.string.install_step, installStep))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
},
|
||||
{ error ->
|
||||
Injekt.get<CrashlyticsInterface>().logException(error)
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_round_info_24)
|
||||
.setContentTitle(getString(R.string.installation_failed, error.message))
|
||||
.setContentText(getString(R.string.error_message, error.message))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
notificationManager.notify(1, builder.build())
|
||||
snackString(getString(R.string.installation_failed, error.message))
|
||||
},
|
||||
{
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_download_24)
|
||||
.setContentTitle(getString(R.string.installation_complete))
|
||||
.setContentText(getString(R.string.extension_has_been_installed))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
notificationManager.notify(1, builder.build())
|
||||
viewModel.invalidatePager()
|
||||
snackString(getString(R.string.extension_installed))
|
||||
}
|
||||
{ installStep -> installerSteps.onInstallStep(installStep) {} },
|
||||
{ error -> installerSteps.onError(error) {} },
|
||||
{ installerSteps.onComplete { viewModel.invalidatePager() } }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package ani.dantotsu.settings
|
||||
|
||||
import android.view.ViewGroup
|
||||
import ani.dantotsu.databinding.ItemSettingsBinding
|
||||
import ani.dantotsu.databinding.ItemSettingsSwitchBinding
|
||||
|
||||
|
@ -13,6 +12,7 @@ data class Settings(
|
|||
val onLongClick: (() -> Unit)? = null,
|
||||
val switch: ((isChecked:Boolean , view: ItemSettingsSwitchBinding ) -> Unit)? = null,
|
||||
val attach:((ItemSettingsBinding) -> Unit)? = null,
|
||||
val attachToSwitch : ((ItemSettingsSwitchBinding) -> Unit)? = null,
|
||||
val isVisible: Boolean = true,
|
||||
val isActivity: Boolean = false,
|
||||
var isChecked : Boolean = false,
|
||||
|
|
|
@ -2,6 +2,7 @@ package ani.dantotsu.settings
|
|||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
@ -82,11 +83,13 @@ class SettingsAboutActivity : AppCompatActivity() {
|
|||
PrefManager.setVal(PrefName.LogToFile, isChecked)
|
||||
restartApp()
|
||||
},
|
||||
attach = {
|
||||
it.settingsDesc.setOnLongClickListener {
|
||||
attachToSwitch = {
|
||||
it.settingsExtraIcon.visibility = View.VISIBLE
|
||||
it.settingsExtraIcon.setImageResource(R.drawable.ic_round_share_24)
|
||||
it.settingsExtraIcon.setOnClickListener {
|
||||
Logger.shareLog(context)
|
||||
true
|
||||
}
|
||||
|
||||
}
|
||||
),
|
||||
Settings(
|
||||
|
|
|
@ -144,6 +144,16 @@ class SettingsActivity : AppCompatActivity() {
|
|||
},
|
||||
isActivity = true
|
||||
),
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.addons),
|
||||
desc = getString(R.string.addons_desc),
|
||||
icon = R.drawable.ic_round_restaurant_24,
|
||||
onClick = {
|
||||
startActivity(Intent(context, SettingsAddonActivity::class.java))
|
||||
},
|
||||
isActivity = true
|
||||
),
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.notifications),
|
||||
|
|
|
@ -87,6 +87,7 @@ class SettingsAdapter(private val settings: ArrayList<Settings>) :
|
|||
true
|
||||
}
|
||||
b.settingsLayout.visibility = if (settings.isVisible) View.VISIBLE else View.GONE
|
||||
settings.attachToSwitch?.invoke(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
259
app/src/main/java/ani/dantotsu/settings/SettingsAddonActivity.kt
Normal file
259
app/src/main/java/ani/dantotsu/settings/SettingsAddonActivity.kt
Normal file
|
@ -0,0 +1,259 @@
|
|||
package ani.dantotsu.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.torrent.ServerService
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.databinding.ActivitySettingsAddonsBinding
|
||||
import ani.dantotsu.databinding.ItemSettingsBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SettingsAddonActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivitySettingsAddonsBinding
|
||||
private val downloadAddonManager: DownloadAddonManager = Injekt.get()
|
||||
private val torrentAddonManager: TorrentAddonManager = Injekt.get()
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
initActivity(this)
|
||||
val context = this
|
||||
binding = ActivitySettingsAddonsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.apply {
|
||||
settingsAddonsLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
|
||||
binding.addonSettingsBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() }
|
||||
|
||||
binding.settingsRecyclerView.adapter = SettingsAdapter(
|
||||
arrayListOf(
|
||||
Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.anime_downloader_addon),
|
||||
desc = getString(R.string.not_installed),
|
||||
icon = R.drawable.anim_play_to_pause,
|
||||
isActivity = true,
|
||||
attach = {
|
||||
setStatus(
|
||||
view = it,
|
||||
context = context,
|
||||
status = downloadAddonManager.hadError(context),
|
||||
hasUpdate = downloadAddonManager.hasUpdate
|
||||
)
|
||||
var job = Job()
|
||||
downloadAddonManager.addListenerAction { _ ->
|
||||
job.cancel()
|
||||
it.settingsIconRight.animate().cancel()
|
||||
it.settingsIconRight.rotation = 0f
|
||||
setStatus(
|
||||
view = it,
|
||||
context = context,
|
||||
status = downloadAddonManager.hadError(context),
|
||||
hasUpdate = false
|
||||
)
|
||||
}
|
||||
it.settingsIconRight.setOnClickListener { _ ->
|
||||
if (it.settingsDesc.text == getString(R.string.installed)) {
|
||||
downloadAddonManager.uninstall()
|
||||
return@setOnClickListener //uninstall logic here
|
||||
} else {
|
||||
job = Job()
|
||||
val scope = CoroutineScope(Dispatchers.Main + job)
|
||||
it.settingsIconRight.setImageResource(R.drawable.ic_sync)
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
withContext(Dispatchers.Main) {
|
||||
it.settingsIconRight.animate()
|
||||
.rotationBy(360f)
|
||||
.setDuration(1000)
|
||||
.setInterpolator(LinearInterpolator())
|
||||
.start()
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
snackString(getString(R.string.downloading))
|
||||
lifecycleScope.launchIO {
|
||||
AddonDownloader.update(
|
||||
activity = context,
|
||||
downloadAddonManager,
|
||||
repo = DownloadAddonManager.REPO,
|
||||
currentVersion = downloadAddonManager.getVersion() ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
), Settings(
|
||||
type = 1,
|
||||
name = getString(R.string.torrent_addon),
|
||||
desc = getString(R.string.not_installed),
|
||||
icon = R.drawable.anim_play_to_pause,
|
||||
isActivity = true,
|
||||
attach = {
|
||||
setStatus(
|
||||
view = it,
|
||||
context = context,
|
||||
status = torrentAddonManager.hadError(context),
|
||||
hasUpdate = torrentAddonManager.hasUpdate
|
||||
)
|
||||
var job = Job()
|
||||
torrentAddonManager.addListenerAction { _ ->
|
||||
job.cancel()
|
||||
it.settingsIconRight.animate().cancel()
|
||||
it.settingsIconRight.rotation = 0f
|
||||
setStatus(
|
||||
view = it,
|
||||
context = context,
|
||||
status = torrentAddonManager.hadError(context),
|
||||
hasUpdate = false
|
||||
)
|
||||
}
|
||||
it.settingsIconRight.setOnClickListener { _ ->
|
||||
if (it.settingsDesc.text == getString(R.string.installed)) {
|
||||
ServerService.stop()
|
||||
torrentAddonManager.uninstall()
|
||||
return@setOnClickListener
|
||||
} else {
|
||||
job = Job()
|
||||
val scope = CoroutineScope(Dispatchers.Main + job)
|
||||
it.settingsIconRight.setImageResource(R.drawable.ic_sync)
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
withContext(Dispatchers.Main) {
|
||||
it.settingsIconRight.animate()
|
||||
.rotationBy(360f)
|
||||
.setDuration(1000)
|
||||
.setInterpolator(LinearInterpolator())
|
||||
.start()
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
snackString(getString(R.string.downloading))
|
||||
lifecycleScope.launchIO {
|
||||
AddonDownloader.update(
|
||||
activity = context,
|
||||
torrentAddonManager,
|
||||
repo = TorrentAddonManager.REPO,
|
||||
currentVersion = torrentAddonManager.getVersion() ?: "",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
Settings(
|
||||
type = 2,
|
||||
name = getString(R.string.enable_torrent),
|
||||
desc = getString(R.string.enable_torrent),
|
||||
icon = R.drawable.ic_round_dns_24,
|
||||
isChecked = PrefManager.getVal(PrefName.TorrentEnabled),
|
||||
switch = { isChecked, _ ->
|
||||
PrefManager.setVal(PrefName.TorrentEnabled, isChecked)
|
||||
Injekt.get<TorrentAddonManager>().extension?.let {
|
||||
if (isChecked) {
|
||||
lifecycleScope.launchIO {
|
||||
if (!ServerService.isRunning()) {
|
||||
ServerService.start()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lifecycleScope.launchIO {
|
||||
if (ServerService.isRunning()) {
|
||||
ServerService.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
binding.settingsRecyclerView.layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
torrentAddonManager.removeListenerAction()
|
||||
downloadAddonManager.removeListenerAction()
|
||||
}
|
||||
|
||||
private fun setStatus(
|
||||
view: ItemSettingsBinding,
|
||||
context: Context,
|
||||
status: String?,
|
||||
hasUpdate: Boolean
|
||||
) {
|
||||
try {
|
||||
when (status) {
|
||||
context.getString(R.string.loaded_successfully) -> {
|
||||
view.settingsIconRight.setImageResource(R.drawable.ic_round_delete_24)
|
||||
view.settingsIconRight.rotation = 0f
|
||||
view.settingsDesc.text = context.getString(R.string.installed)
|
||||
}
|
||||
|
||||
null -> {
|
||||
view.settingsIconRight.setImageResource(R.drawable.ic_download_24)
|
||||
view.settingsIconRight.rotation = 0f
|
||||
view.settingsDesc.text = context.getString(R.string.not_installed)
|
||||
}
|
||||
|
||||
else -> {
|
||||
view.settingsIconRight.setImageResource(R.drawable.ic_round_new_releases_24)
|
||||
view.settingsIconRight.rotation = 0f
|
||||
view.settingsDesc.text = context.getString(R.string.error_msg, status)
|
||||
}
|
||||
}
|
||||
if (hasUpdate) {
|
||||
view.settingsIconRight.setImageResource(R.drawable.ic_round_sync_24)
|
||||
view.settingsDesc.text = context.getString(R.string.update_addon)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,22 +13,17 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.api.NotificationType
|
||||
import ani.dantotsu.databinding.ActivitySettingsNotificationsBinding
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.notifications.TaskScheduler
|
||||
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
|
||||
import ani.dantotsu.notifications.comment.CommentNotificationWorker
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionNotificationWorker
|
||||
import ani.dantotsu.openSettings
|
||||
import ani.dantotsu.restartApp
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SettingsNotificationActivity: AppCompatActivity(){
|
||||
private lateinit var binding: ActivitySettingsNotificationsBinding
|
||||
|
|
|
@ -120,6 +120,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
|
|||
UseInternalCast(Pref(Location.Player, Boolean::class, false)),
|
||||
Pip(Pref(Location.Player, Boolean::class, true)),
|
||||
RotationPlayer(Pref(Location.Player, Boolean::class, true)),
|
||||
TorrentEnabled(Pref(Location.Player, Boolean::class, false)),
|
||||
|
||||
//Reader
|
||||
ShowSource(Pref(Location.Reader, Boolean::class, true)),
|
||||
|
|
|
@ -39,6 +39,12 @@ object Notifications {
|
|||
const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
|
||||
const val GROUP_NEW_EPISODES = "eu.kanade.tachiyomi.NEW_EPISODES"
|
||||
|
||||
/**
|
||||
* Notification channel and ids used by the torrent server.
|
||||
*/
|
||||
const val ID_TORRENT_SERVER = -1100
|
||||
const val CHANNEL_TORRENT_SERVER = "dantotsu_torrent_server"
|
||||
|
||||
/**
|
||||
* Notification channel used for Incognito Mode
|
||||
*/
|
||||
|
@ -154,6 +160,9 @@ object Notifications {
|
|||
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
|
||||
setName("Incognito Mode")
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_TORRENT_SERVER, IMPORTANCE_LOW) {
|
||||
setName("Torrent Server")
|
||||
},
|
||||
buildNotificationChannel(CHANNEL_COMMENTS, IMPORTANCE_HIGH) {
|
||||
setName("Comments")
|
||||
setGroup(GROUP_COMMENTS)
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package eu.kanade.tachiyomi.data.torrentServer.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Torrent(
|
||||
var title: String,
|
||||
var poster: String? = null,
|
||||
var data: String? = null,
|
||||
var timestamp: Long? = null,
|
||||
var name: String? = null,
|
||||
var hash: String? = null,
|
||||
var stat: Int? = null,
|
||||
var stat_string: String? = null,
|
||||
var loaded_size: Long? = null,
|
||||
var torrent_size: Long? = null,
|
||||
var preloaded_bytes: Long? = null,
|
||||
var preload_size: Long? = null,
|
||||
var download_speed: Double? = null,
|
||||
var upload_speed: Double? = null,
|
||||
var total_peers: Int? = null,
|
||||
var pending_peers: Int? = null,
|
||||
var active_peers: Int? = null,
|
||||
var connected_seeders: Int? = null,
|
||||
var half_open_peers: Int? = null,
|
||||
var bytes_written: Long? = null,
|
||||
var bytes_written_data: Long? = null,
|
||||
var bytes_read: Long? = null,
|
||||
var bytes_read_data: Long? = null,
|
||||
var bytes_read_useful_data: Long? = null,
|
||||
var chunks_written: Long? = null,
|
||||
var chunks_read: Long? = null,
|
||||
var chunks_read_useful: Long? = null,
|
||||
var chunks_read_wasted: Long? = null,
|
||||
var pieces_dirtied_good: Long? = null,
|
||||
var pieces_dirtied_bad: Long? = null,
|
||||
var duration_seconds: Double? = null,
|
||||
var bit_rate: String? = null,
|
||||
var file_stats: List<FileStat>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FileStat(
|
||||
var id: Int? = null,
|
||||
var path: String,
|
||||
var length: Long,
|
||||
)
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.anime
|
|||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
|
@ -206,7 +207,8 @@ class AnimeExtensionManager(
|
|||
* @param extension The anime extension to be installed.
|
||||
*/
|
||||
fun installExtension(extension: AnimeExtension.Available): Observable<InstallStep> {
|
||||
return installer.downloadAndInstall(api.getAnimeApkUrl(extension), extension)
|
||||
return installer.downloadAndInstall(api.getAnimeApkUrl(extension), extension.pkgName,
|
||||
extension.name, MediaType.ANIME)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,7 +8,11 @@ import android.content.IntentFilter
|
|||
import android.net.Uri
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.Type
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
|
@ -25,6 +29,8 @@ abstract class Installer(private val service: Service) {
|
|||
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
|
||||
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
|
||||
private val novelExtensionManager: NovelExtensionManager by injectLazy()
|
||||
private val torrentAddonManager: TorrentAddonManager by injectLazy()
|
||||
private val downloadAddonManager: DownloadAddonManager by injectLazy()
|
||||
|
||||
private var waitingInstall = AtomicReference<Entry>(null)
|
||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||
|
@ -49,7 +55,7 @@ abstract class Installer(private val service: Service) {
|
|||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
* @param uri Uri of APK to install
|
||||
*/
|
||||
fun addToQueue(type: MediaType, downloadId: Long, uri: Uri) {
|
||||
fun addToQueue(type: Type, downloadId: Long, uri: Uri) {
|
||||
queue.add(Entry(type, downloadId, uri))
|
||||
checkQueue()
|
||||
}
|
||||
|
@ -63,11 +69,18 @@ abstract class Installer(private val service: Service) {
|
|||
*/
|
||||
@CallSuper
|
||||
open fun processEntry(entry: Entry) {
|
||||
if (entry.type is MediaType) {
|
||||
when (entry.type) {
|
||||
MediaType.ANIME -> animeExtensionManager.setInstalling(entry.downloadId)
|
||||
MediaType.MANGA -> mangaExtensionManager.setInstalling(entry.downloadId)
|
||||
MediaType.NOVEL -> novelExtensionManager.setInstalling(entry.downloadId)
|
||||
}
|
||||
} else {
|
||||
when (entry.type) {
|
||||
AddonType.TORRENT -> torrentAddonManager.setInstalling(entry.downloadId)
|
||||
AddonType.DOWNLOAD -> downloadAddonManager.setInstalling(entry.downloadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,17 +103,34 @@ abstract class Installer(private val service: Service) {
|
|||
fun continueQueue(resultStep: InstallStep) {
|
||||
val completedEntry = waitingInstall.getAndSet(null)
|
||||
if (completedEntry != null) {
|
||||
if (completedEntry.type is MediaType) {
|
||||
when (completedEntry.type) {
|
||||
MediaType.ANIME -> {
|
||||
animeExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||
}
|
||||
MediaType.ANIME -> animeExtensionManager.updateInstallStep(
|
||||
completedEntry.downloadId,
|
||||
resultStep
|
||||
)
|
||||
|
||||
MediaType.MANGA -> {
|
||||
mangaExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||
}
|
||||
MediaType.MANGA -> mangaExtensionManager.updateInstallStep(
|
||||
completedEntry.downloadId,
|
||||
resultStep
|
||||
)
|
||||
|
||||
MediaType.NOVEL -> {
|
||||
novelExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
|
||||
MediaType.NOVEL -> novelExtensionManager.updateInstallStep(
|
||||
completedEntry.downloadId,
|
||||
resultStep
|
||||
)
|
||||
}
|
||||
} else {
|
||||
when (completedEntry.type) {
|
||||
AddonType.TORRENT -> torrentAddonManager.updateInstallStep(
|
||||
completedEntry.downloadId,
|
||||
resultStep
|
||||
)
|
||||
|
||||
AddonType.DOWNLOAD -> downloadAddonManager.updateInstallStep(
|
||||
completedEntry.downloadId,
|
||||
resultStep
|
||||
)
|
||||
}
|
||||
}
|
||||
checkQueue()
|
||||
|
@ -113,7 +143,7 @@ abstract class Installer(private val service: Service) {
|
|||
*
|
||||
* @see ready
|
||||
*/
|
||||
fun checkQueue() {
|
||||
private fun checkQueue() {
|
||||
if (!ready) {
|
||||
return
|
||||
}
|
||||
|
@ -135,15 +165,35 @@ abstract class Installer(private val service: Service) {
|
|||
open fun onDestroy() {
|
||||
LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver)
|
||||
queue.forEach {
|
||||
|
||||
if (it.type is MediaType) {
|
||||
when (it.type) {
|
||||
MediaType.ANIME -> {
|
||||
animeExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
|
||||
MediaType.ANIME -> animeExtensionManager.updateInstallStep(
|
||||
it.downloadId,
|
||||
InstallStep.Error
|
||||
)
|
||||
|
||||
MediaType.MANGA -> mangaExtensionManager.updateInstallStep(
|
||||
it.downloadId,
|
||||
InstallStep.Error
|
||||
)
|
||||
|
||||
MediaType.NOVEL -> novelExtensionManager.updateInstallStep(
|
||||
it.downloadId,
|
||||
InstallStep.Error
|
||||
)
|
||||
}
|
||||
MediaType.MANGA -> {
|
||||
mangaExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
|
||||
}
|
||||
MediaType.NOVEL -> {
|
||||
novelExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
|
||||
} else {
|
||||
when (it.type) {
|
||||
AddonType.TORRENT -> torrentAddonManager.updateInstallStep(
|
||||
it.downloadId,
|
||||
InstallStep.Error
|
||||
)
|
||||
|
||||
AddonType.DOWNLOAD -> downloadAddonManager.updateInstallStep(
|
||||
it.downloadId,
|
||||
InstallStep.Error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,15 +218,34 @@ abstract class Installer(private val service: Service) {
|
|||
this.waitingInstall.set(null)
|
||||
checkQueue()
|
||||
}
|
||||
if (toCancel.type is MediaType) {
|
||||
when (toCancel.type) {
|
||||
MediaType.ANIME -> {
|
||||
animeExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||
MediaType.ANIME -> animeExtensionManager.updateInstallStep(
|
||||
downloadId,
|
||||
InstallStep.Idle
|
||||
)
|
||||
|
||||
MediaType.MANGA -> mangaExtensionManager.updateInstallStep(
|
||||
downloadId,
|
||||
InstallStep.Idle
|
||||
)
|
||||
|
||||
MediaType.NOVEL -> novelExtensionManager.updateInstallStep(
|
||||
downloadId,
|
||||
InstallStep.Idle
|
||||
)
|
||||
}
|
||||
MediaType.MANGA -> {
|
||||
mangaExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||
}
|
||||
MediaType.NOVEL -> {
|
||||
novelExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
|
||||
} else {
|
||||
when (toCancel.type) {
|
||||
AddonType.TORRENT -> torrentAddonManager.updateInstallStep(
|
||||
downloadId,
|
||||
InstallStep.Idle
|
||||
)
|
||||
|
||||
AddonType.DOWNLOAD -> downloadAddonManager.updateInstallStep(
|
||||
downloadId,
|
||||
InstallStep.Idle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -188,7 +257,7 @@ abstract class Installer(private val service: Service) {
|
|||
* @param downloadId Download ID as known by [ExtensionManager]
|
||||
* @param uri Uri of APK to install
|
||||
*/
|
||||
data class Entry(val type: MediaType, val downloadId: Long, val uri: Uri)
|
||||
data class Entry(val type: Type, val downloadId: Long, val uri: Uri)
|
||||
|
||||
init {
|
||||
val filter = IntentFilter(ACTION_CANCEL_QUEUE)
|
||||
|
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.manga
|
|||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
|
@ -203,7 +204,8 @@ class MangaExtensionManager(
|
|||
* @param extension The extension to be installed.
|
||||
*/
|
||||
fun installExtension(extension: MangaExtension.Available): Observable<InstallStep> {
|
||||
return installer.downloadAndInstall(api.getMangaApkUrl(extension), extension)
|
||||
return installer.downloadAndInstall(api.getMangaApkUrl(extension), extension.pkgName,
|
||||
extension.name, MediaType.MANGA)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,9 @@ import android.os.Bundle
|
|||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
@ -29,7 +32,8 @@ class ExtensionInstallActivity : AppCompatActivity() {
|
|||
private var ignoreResult = false
|
||||
private var hasIgnoredResult = false
|
||||
|
||||
private var type: MediaType? = null
|
||||
private var mediaType: MediaType? = null
|
||||
private var addonType: AddonType? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -37,7 +41,9 @@ class ExtensionInstallActivity : AppCompatActivity() {
|
|||
ThemeManager(this).applyTheme()
|
||||
|
||||
if (intent.hasExtra(ExtensionInstaller.EXTRA_EXTENSION_TYPE))
|
||||
type = intent.getSerializableExtraCompat<MediaType>(ExtensionInstaller.EXTRA_EXTENSION_TYPE)
|
||||
mediaType = intent.getSerializableExtraCompat<MediaType>(ExtensionInstaller.EXTRA_EXTENSION_TYPE)
|
||||
if (intent.hasExtra(ExtensionInstaller.EXTRA_ADDON_TYPE))
|
||||
addonType = intent.getSerializableExtraCompat<AddonType>(ExtensionInstaller.EXTRA_ADDON_TYPE)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
||||
|
@ -85,17 +91,34 @@ class ExtensionInstallActivity : AppCompatActivity() {
|
|||
RESULT_CANCELED -> InstallStep.Idle
|
||||
else -> InstallStep.Error
|
||||
}
|
||||
when (type) {
|
||||
if (mediaType != null) {
|
||||
when (mediaType) {
|
||||
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 -> {}
|
||||
}
|
||||
} else {
|
||||
when (addonType) {
|
||||
AddonType.TORRENT -> {
|
||||
Injekt.get<TorrentAddonManager>().updateInstallStep(downloadId, newStep)
|
||||
}
|
||||
|
||||
AddonType.DOWNLOAD -> {
|
||||
Injekt.get<DownloadAddonManager>().updateInstallStep(downloadId, newStep)
|
||||
}
|
||||
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,18 +50,6 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
|||
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.
|
||||
|
@ -136,21 +124,13 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private suspend fun getAnimeExtensionFromIntent(context: Context, intent: Intent?): AnimeLoadResult {
|
||||
val pkgName = getPackageNameFromIntent(intent)
|
||||
if (pkgName == null) {
|
||||
|
@ -180,12 +160,6 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
|||
}.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.
|
||||
|
@ -203,4 +177,36 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() {
|
|||
fun onExtensionUntrusted(extension: MangaExtension.Untrusted)
|
||||
fun onPackageUninstalled(pkgName: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Returns the intent filter this receiver should subscribe to.
|
||||
*/
|
||||
val filter
|
||||
get() = IntentFilter().apply {
|
||||
priority = 100
|
||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addDataScheme("package")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this package is performing an update.
|
||||
*
|
||||
* @param intent The intent that triggered the event.
|
||||
*/
|
||||
fun isReplacing(intent: Intent): Boolean {
|
||||
return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the package name of the installed, updated or removed application.
|
||||
*/
|
||||
fun getPackageNameFromIntent(intent: Intent?): String? {
|
||||
return intent?.data?.encodedSchemeSpecificPart ?: return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.Type
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
|
@ -45,12 +47,13 @@ class ExtensionInstallService : Service() {
|
|||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val uri = intent?.data
|
||||
val type = intent?.getSerializableExtraCompat<MediaType>(EXTRA_EXTENSION_TYPE)
|
||||
val mediaType = intent?.getSerializableExtraCompat<MediaType>(EXTRA_EXTENSION_TYPE)
|
||||
val addonType = intent?.getSerializableExtraCompat<AddonType>(ExtensionInstaller.EXTRA_ADDON_TYPE)
|
||||
val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L }
|
||||
val installerUsed = intent?.getSerializableExtraCompat<BasePreferences.ExtensionInstaller>(
|
||||
EXTRA_INSTALLER
|
||||
)
|
||||
if (uri == null || type == null || id == null || installerUsed == null) {
|
||||
if (uri == null || (mediaType == null && addonType == null) || id == null || installerUsed == null) {
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
@ -68,7 +71,7 @@ class ExtensionInstallService : Service() {
|
|||
}
|
||||
}
|
||||
}
|
||||
installer!!.addToQueue(type, id, uri)
|
||||
installer!!.addToQueue(mediaType ?: addonType!!, id, uri)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
|
@ -84,16 +87,21 @@ class ExtensionInstallService : Service() {
|
|||
|
||||
fun getIntent(
|
||||
context: Context,
|
||||
type: MediaType,
|
||||
type: Type,
|
||||
downloadId: Long,
|
||||
uri: Uri,
|
||||
installer: BasePreferences.ExtensionInstaller,
|
||||
): Intent {
|
||||
return Intent(context, ExtensionInstallService::class.java)
|
||||
val intent = Intent(context, ExtensionInstallService::class.java)
|
||||
.setDataAndType(uri, ExtensionInstaller.APK_MIME)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.putExtra(EXTRA_EXTENSION_TYPE, type)
|
||||
.putExtra(EXTRA_INSTALLER, installer)
|
||||
if (type is MediaType) {
|
||||
intent.putExtra(EXTRA_EXTENSION_TYPE, type)
|
||||
} else if (type is AddonType) {
|
||||
intent.putExtra(ExtensionInstaller.EXTRA_ADDON_TYPE, type)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ import android.os.Environment
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.Type
|
||||
import ani.dantotsu.parsers.novel.NovelExtension
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
|
@ -33,7 +35,7 @@ import java.util.concurrent.TimeUnit
|
|||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
internal class ExtensionInstaller(private val context: Context) {
|
||||
class ExtensionInstaller(private val context: Context) {
|
||||
|
||||
/**
|
||||
* The system's download manager
|
||||
|
@ -65,27 +67,24 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||
* @param url The url of the apk.
|
||||
* @param extension The extension to install.
|
||||
*/
|
||||
fun downloadAndInstall(url: String, extension: AnimeExtension): Observable<InstallStep> = Observable.defer {
|
||||
val pkgName = extension.pkgName
|
||||
|
||||
fun <T : Type> downloadAndInstall(url: String, pkgName: String, name: String, type: T): Observable<InstallStep> = Observable.defer {
|
||||
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)
|
||||
.setTitle(name)
|
||||
.setMimeType(APK_MIME)
|
||||
.setDestinationInExternalFilesDir(
|
||||
context,
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
downloadUri.lastPathSegment
|
||||
)
|
||||
.setDescription(MediaType.ANIME.asText())
|
||||
.setDescription(type.asText())
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
|
||||
val id = downloadManager.enqueue(request)
|
||||
|
@ -93,91 +92,12 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||
|
||||
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)
|
||||
|
||||
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
|
||||
|
@ -215,14 +135,18 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||
*
|
||||
* @param uri The uri of the extension to install.
|
||||
*/
|
||||
fun installApk(type: MediaType, downloadId: Long, uri: Uri) {
|
||||
fun installApk(type: Type, downloadId: Long, uri: Uri) {
|
||||
when (val installer = extensionInstaller.get()) {
|
||||
BasePreferences.ExtensionInstaller.LEGACY -> {
|
||||
val intent = Intent(context, ExtensionInstallActivity::class.java)
|
||||
.setDataAndType(uri, APK_MIME)
|
||||
.putExtra(EXTRA_EXTENSION_TYPE, type)
|
||||
.putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
if (type is MediaType) {
|
||||
intent.putExtra(EXTRA_EXTENSION_TYPE, type)
|
||||
} else if (type is AddonType) {
|
||||
intent.putExtra(EXTRA_ADDON_TYPE, type)
|
||||
}
|
||||
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
@ -342,7 +266,9 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||
).removePrefix(FILE_SCHEME)
|
||||
val type = MediaType.fromText(cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION),
|
||||
))
|
||||
)) ?: AddonType.fromText(cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION),
|
||||
)) ?: return
|
||||
|
||||
installApk(type, id, File(localUri).getUriCompat(context))
|
||||
}
|
||||
|
@ -354,6 +280,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||
const val APK_MIME = "application/vnd.android.package-archive"
|
||||
const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID"
|
||||
const val EXTRA_EXTENSION_TYPE = "ExtensionInstaller.extra.EXTENSION_TYPE"
|
||||
const val EXTRA_ADDON_TYPE = "ExtensionInstaller.extra.ADDON_TYPE"
|
||||
const val FILE_SCHEME = "file://"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ internal object ExtensionLoader {
|
|||
const val MANGA_LIB_VERSION_MIN = 1.2
|
||||
const val MANGA_LIB_VERSION_MAX = 1.5
|
||||
|
||||
private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or
|
||||
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)
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
android:id="@+id/settingsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:itemCount="5"
|
||||
|
|
73
app/src/main/res/layout/activity_settings_addons.xml
Normal file
73
app/src/main/res/layout/activity_settings_addons.xml
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".settings.SettingsAccountActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/settingsAddonsLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="32dp"
|
||||
app:cardBackgroundColor="@color/nav_bg_inv"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/addonSettingsBack"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:background="@color/nav_bg_inv"
|
||||
android:padding="16dp"
|
||||
app:srcCompat="@drawable/ic_round_arrow_back_ios_new_24"
|
||||
app:tint="?attr/colorOnBackground"
|
||||
tools:ignore="ContentDescription,SpeakableTextPresentCheck" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="32dp"
|
||||
android:layout_weight="1"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
android:text="@string/addons"
|
||||
android:textSize="28sp" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:padding="24dp"
|
||||
app:srcCompat="@drawable/ic_round_restaurant_24"
|
||||
app:tint="?attr/colorOnBackground"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ani.dantotsu.FadingEdgeRecyclerView
|
||||
android:id="@+id/settingsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/item_settings" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
|
@ -136,7 +136,7 @@
|
|||
android:id="@+id/settingsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:itemCount="5"
|
||||
|
|
|
@ -173,7 +173,7 @@
|
|||
android:id="@+id/settingsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:itemCount="5"
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
android:id="@+id/settingsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:itemCount="5"
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
android:id="@+id/settingsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:itemCount="5"
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
android:id="@+id/settingsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:itemCount="5"
|
||||
|
|
|
@ -167,7 +167,7 @@
|
|||
android:id="@+id/settingsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:requiresFadingEdge="vertical"
|
||||
tools:itemCount="5"
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_marginTop="8dp"
|
||||
android:id="@+id/settingsDesc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/settingsButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="12dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="false"
|
||||
android:elegantTextHeight="true"
|
||||
android:fontFamily="@font/poppins_bold"
|
||||
|
@ -41,15 +41,34 @@
|
|||
app:showText="false"
|
||||
app:thumbTint="@color/button_switch_track" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/settingsDesc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_weight="1"
|
||||
android:alpha="0.66"
|
||||
android:fontFamily="@font/poppins_semi_bold"
|
||||
android:text="@string/slogan"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/settingsExtraIcon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="top"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
app:srcCompat="@drawable/ic_circle_add"
|
||||
app:tint="?attr/colorPrimary"
|
||||
android:visibility="gone"
|
||||
tools:ignore="ContentDescription" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
<color name="grey_60">#999999</color>
|
||||
<color name="darkest_Black">#000000</color>
|
||||
<color name="yt_red">#CD201F</color>
|
||||
<color name="literally_just_green">#00FF00</color>
|
||||
<color name="chip">#a3a2a2</color>
|
||||
<color name="grey_nav">#F9EDEDED</color>
|
||||
<color name="CustomColor1">#93DB00</color>
|
||||
|
|
|
@ -382,6 +382,8 @@
|
|||
<string name="forks">Versions</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="accounts">Accounts</string>
|
||||
<string name="addons">Addons</string>
|
||||
<string name="addons_desc">Addons are extensions that provide additional functionality.</string>
|
||||
<string name="myanimelist">MyAnimeList</string>
|
||||
<string name="login_with_anilist">Login with Anilist!</string>
|
||||
<string name="anilist">Anilist</string>
|
||||
|
@ -423,6 +425,7 @@
|
|||
<string name="installation_complete">Installation complete</string>
|
||||
<string name="extension_has_been_installed">The extension has been successfully installed.</string>
|
||||
<string name="extension_installed">Extension installed</string>
|
||||
<string name="installed">Installed</string>
|
||||
<string name="error_message">Error: %1$s</string>
|
||||
<string name="install_step">Step: %1$s</string>
|
||||
<string name="review">Review</string>
|
||||
|
@ -909,4 +912,13 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
|
|||
<string name="devs_desc">Dantotsu\'s very own unpaid labours </string>
|
||||
<string name="forks_desc">More like Dantotsu</string>
|
||||
<string name="disclaimer_desc">Something to keep in mind</string>
|
||||
<string name="torrent_addon">Torrent Addon</string>
|
||||
<string name="enable_torrent">Enable torrent</string>
|
||||
<string name="anime_downloader_addon">Anime Downloader Addon</string>
|
||||
<string name="loaded_successfully">Loaded Successfully</string>
|
||||
<string name="not_installed">Not Installed</string>
|
||||
<string name="torrent_extension_not_supported">Torrent extension not supported on this device</string>
|
||||
<string name="update_addon">Update Addon</string>
|
||||
<string name="install_addon">Install Addon</string>
|
||||
<string name="download_addon_not_found">Download addon not found</string>
|
||||
</resources>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue