* 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:
rebel onion 2024-04-19 04:08:20 -05:00 committed by GitHub
parent 3d1040b280
commit 670d16bd8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 1923 additions and 427 deletions

View file

@ -77,25 +77,20 @@ jobs:
- name: List files in the directory
run: ls -l
- name: Make gradlew executable
run: chmod +x ./gradlew
- 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:
@ -160,4 +136,4 @@ jobs:
pre-release-keep-count: 3
pre-release-drop-tag: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -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

View file

@ -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"

View file

@ -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()

View file

@ -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() {
@ -473,7 +484,7 @@ class MainActivity : AppCompatActivity() {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
val params : ViewGroup.MarginLayoutParams =
val params: ViewGroup.MarginLayoutParams =
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
params.updateMargins(bottom = margin.toPx)
}

View 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()
}

View 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
}
}
}

View 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
}
}

View 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)
}
}
}

View 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)
}
}

View file

@ -0,0 +1,8 @@
package ani.dantotsu.addons
abstract class LoadResult {
abstract class Success : LoadResult()
}

View file

@ -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 -> {}
}
}
}
}
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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"
}
}

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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"
}
}

View file

@ -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()
}

View 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
}
}
}

View file

@ -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()) }

View file

@ -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(
"${

View file

@ -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

View file

@ -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 {

View file

@ -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 }
}
}
}

View file

@ -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

View file

@ -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:")) {
try {
externalPlayerResult.launch(exportMagnetIntent(ep, video))
} catch (e: ActivityNotFoundException) {
val amnis = "com.amnis"
try {
startActivity(Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$amnis"))
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) {
startActivity(Intent(
Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=$amnis")
))
val amnis = "com.amnis"
try {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$amnis")
)
)
dismiss()
} catch (e: ActivityNotFoundException) {
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)

View file

@ -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,

View file

@ -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

View file

@ -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>()

View file

@ -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>()

View file

@ -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() } }
)
}
}

View file

@ -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

View 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))
}
}

View file

@ -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() } }
)
}
}

View file

@ -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,

View file

@ -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(

View file

@ -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),

View file

@ -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)
}
}
}

View 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)
}
}
}

View file

@ -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

View file

@ -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)),

View file

@ -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)

View file

@ -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,
)

View file

@ -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)
}
/**

View file

@ -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,10 +69,17 @@ abstract class Installer(private val service: Service) {
*/
@CallSuper
open fun processEntry(entry: Entry) {
when (entry.type) {
MediaType.ANIME -> animeExtensionManager.setInstalling(entry.downloadId)
MediaType.MANGA -> mangaExtensionManager.setInstalling(entry.downloadId)
MediaType.NOVEL -> novelExtensionManager.setInstalling(entry.downloadId)
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) {
when (completedEntry.type) {
MediaType.ANIME -> {
animeExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep)
}
if (completedEntry.type is MediaType) {
when (completedEntry.type) {
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 {
when (it.type) {
MediaType.ANIME -> {
animeExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error)
if (it.type is MediaType) {
when (it.type) {
MediaType.ANIME -> animeExtensionManager.updateInstallStep(
it.downloadId,
InstallStep.Error
)
MediaType.MANGA -> mangaExtensionManager.updateInstallStep(
it.downloadId,
InstallStep.Error
)
MediaType.NOVEL -> novelExtensionManager.updateInstallStep(
it.downloadId,
InstallStep.Error
)
}
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()
}
when (toCancel.type) {
MediaType.ANIME -> {
animeExtensionManager.updateInstallStep(downloadId, InstallStep.Idle)
if (toCancel.type is MediaType) {
when (toCancel.type) {
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)

View file

@ -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)
}
/**

View file

@ -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) {
MediaType.ANIME -> {
Injekt.get<AnimeExtensionManager>().updateInstallStep(downloadId, newStep)
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 -> {}
}
MediaType.MANGA -> {
Injekt.get<MangaExtensionManager>().updateInstallStep(downloadId, newStep)
} else {
when (addonType) {
AddonType.TORRENT -> {
Injekt.get<TorrentAddonManager>().updateInstallStep(downloadId, newStep)
}
AddonType.DOWNLOAD -> {
Injekt.get<DownloadAddonManager>().updateInstallStep(downloadId, newStep)
}
null -> {}
}
MediaType.NOVEL -> {
Injekt.get<NovelExtensionManager>().updateInstallStep(downloadId, newStep)
}
null -> { }
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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://"
}
}

View 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)

View file

@ -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"

View file

@ -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"

View 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>

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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" />
<TextView
android:id="@+id/settingsDesc"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:alpha="0.66"
android:fontFamily="@font/poppins_semi_bold"
android:text="@string/slogan"
android:textColor="?attr/colorOnSurfaceVariant" />
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<TextView
android:id="@+id/settingsDesc"
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>

View file

@ -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>

View file

@ -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>