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