From 670d16bd8e1ffc0b8641b053aee257792e4ed3a8 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Fri, 19 Apr 2024 04:08:20 -0500 Subject: [PATCH] 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 --- .github/workflows/beta.yml | 38 +-- app/build.gradle | 11 +- app/src/main/AndroidManifest.xml | 9 +- app/src/main/java/ani/dantotsu/App.kt | 12 + .../main/java/ani/dantotsu/MainActivity.kt | 37 ++- .../main/java/ani/dantotsu/addons/Addon.kt | 15 + .../ani/dantotsu/addons/AddonDownloader.kt | 145 ++++++++++ .../java/ani/dantotsu/addons/AddonListener.kt | 11 + .../java/ani/dantotsu/addons/AddonLoader.kt | 137 +++++++++ .../java/ani/dantotsu/addons/AddonManager.kt | 44 +++ .../java/ani/dantotsu/addons/LoadResult.kt | 8 + .../addons/download/AddonInstallReceiver.kt | 134 +++++++++ .../dantotsu/addons/download/DownloadAddon.kt | 18 ++ .../addons/download/DownloadAddonApi.kt | 21 ++ .../addons/download/DownloadAddonManager.kt | 133 +++++++++ .../addons/download/DownloadLoadResult.kt | 7 + .../dantotsu/addons/torrent/TorrentAddon.kt | 16 ++ .../addons/torrent/TorrentAddonApi.kt | 24 ++ .../addons/torrent/TorrentAddonManager.kt | 140 ++++++++++ .../addons/torrent/TorrentLoadResult.kt | 7 + .../dantotsu/addons/torrent/TorrentService.kt | 167 +++++++++++ .../aniyomi/anime/custom/InjektModules.kt | 5 + .../download/anime/AnimeDownloaderService.kt | 68 ++--- .../download/novel/NovelDownloaderService.kt | 3 - .../ani/dantotsu/download/video/Helper.kt | 26 -- .../main/java/ani/dantotsu/media/MediaType.kt | 34 ++- .../dantotsu/media/anime/EpisodeAdapters.kt | 2 - .../media/anime/SelectorDialogFragment.kt | 72 ++++- .../dantotsu/media/novel/NovelReadFragment.kt | 3 - .../ani/dantotsu/parsers/AniyomiAdapter.kt | 2 +- .../dantotsu/parsers/OfflineMangaParser.kt | 3 - .../dantotsu/parsers/OfflineNovelParser.kt | 3 - .../settings/AnimeExtensionsFragment.kt | 42 +-- .../dantotsu/settings/ExtensionsActivity.kt | 2 - .../ani/dantotsu/settings/InstallerSteps.kt | 54 ++++ .../settings/MangaExtensionsFragment.kt | 41 +-- .../java/ani/dantotsu/settings/Settings.kt | 2 +- .../settings/SettingsAboutActivity.kt | 9 +- .../ani/dantotsu/settings/SettingsActivity.kt | 10 + .../ani/dantotsu/settings/SettingsAdapter.kt | 1 + .../settings/SettingsAddonActivity.kt | 259 ++++++++++++++++++ .../settings/SettingsNotificationActivity.kt | 5 - .../dantotsu/settings/saving/Preferences.kt | 1 + .../data/notification/Notifications.kt | 9 + .../data/torrentServer/model/Torrent.kt | 47 ++++ .../extension/anime/AnimeExtensionManager.kt | 4 +- .../extension/installer/Installer.kt | 131 ++++++--- .../extension/manga/MangaExtensionManager.kt | 4 +- .../util/ExtensionInstallActivity.kt | 45 ++- .../util/ExtensionInstallReceiver.kt | 60 ++-- .../extension/util/ExtensionInstallService.kt | 20 +- .../extension/util/ExtensionInstaller.kt | 105 ++----- .../extension/util/ExtensionLoader.kt | 2 +- app/src/main/res/layout/activity_settings.xml | 2 +- .../res/layout/activity_settings_about.xml | 2 +- .../res/layout/activity_settings_addons.xml | 73 +++++ .../res/layout/activity_settings_anime.xml | 2 +- .../res/layout/activity_settings_common.xml | 2 +- .../layout/activity_settings_extensions.xml | 2 +- .../res/layout/activity_settings_manga.xml | 2 +- .../activity_settings_notifications.xml | 2 +- .../res/layout/activity_settings_theme.xml | 2 +- app/src/main/res/layout/item_settings.xml | 1 + .../main/res/layout/item_settings_switch.xml | 37 ++- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 12 + 66 files changed, 1922 insertions(+), 426 deletions(-) create mode 100644 app/src/main/java/ani/dantotsu/addons/Addon.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/AddonListener.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/AddonLoader.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/AddonManager.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/LoadResult.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/download/AddonInstallReceiver.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/download/DownloadAddon.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApi.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/download/DownloadAddonManager.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/download/DownloadLoadResult.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddon.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddonApi.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddonManager.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/torrent/TorrentLoadResult.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt create mode 100644 app/src/main/java/ani/dantotsu/settings/InstallerSteps.kt create mode 100644 app/src/main/java/ani/dantotsu/settings/SettingsAddonActivity.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/Torrent.kt create mode 100644 app/src/main/res/layout/activity_settings_addons.xml diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 9670e55ca2c..f763d2de46b 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -77,25 +77,20 @@ jobs: - name: List files in the directory run: ls -l - + - name: Make gradlew executable run: chmod +x ./gradlew - name: Build with Gradle run: ./gradlew assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }} - - name: Upload Build Artifacts + - name: Upload a Build Artifact uses: actions/upload-artifact@v4.3.1 with: - name: Dantotsu-Split + name: Dantotsu retention-days: 5 compression-level: 9 - path: | - app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk - app/build/outputs/apk/google/alpha/app-google-armeabi-v7a-alpha.apk - app/build/outputs/apk/google/alpha/app-google-arm64-v8a-alpha.apk - app/build/outputs/apk/google/alpha/app-google-x86-alpha.apk - app/build/outputs/apk/google/alpha/app-google-x86_64-alpha.apk + path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk" - name: Upload APK to Discord and Telegram if: ${{ github.repository == 'rebelonion/Dantotsu' }} @@ -110,30 +105,11 @@ jobs: fi contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' ) curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }} - curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-armeabi-v7a-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }} - curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-arm64-v8a-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }} - curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-x86-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }} - curl -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-x86_64-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }} - + #Telegram - curl -X POST \ - -d chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }} \ - -d text="Alpha-Build: ${VERSION}: ${commit_messages}" \ - https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage - curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \ - -F "document=@app/build/outputs/apk/google/alpha/app-google-armeabi-v7a-alpha.apk" \ - https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument - curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \ - -F "document=@app/build/outputs/apk/google/alpha/app-google-arm64-v8a-alpha.apk" \ - https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument - curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \ - -F "document=@app/build/outputs/apk/google/alpha/app-google-x86-alpha.apk" \ - https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument - curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \ - -F "document=@app/build/outputs/apk/google/alpha/app-google-x86_64-alpha.apk" \ - https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \ -F "document=@app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk" \ + -F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \ https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument env: @@ -160,4 +136,4 @@ jobs: pre-release-keep-count: 3 pre-release-drop-tag: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 53cb12994a4..89ca14c3e03 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,14 +21,7 @@ android { versionName "3.0.0" versionCode 300000000 signingConfig signingConfigs.debug - splits { - abi { - enable true - reset() - include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' - universalApk true - } - } + } flavorDimensions += "store" @@ -158,7 +151,7 @@ dependencies { // String Matching implementation 'me.xdrop:fuzzywuzzy:1.4.0' - implementation group: 'com.arthenica', name: 'ffmpeg-kit-full-gpl', version: '6.0-2.LTS' + //implementation group: 'com.arthenica', name: 'ffmpeg-kit-full-gpl', version: '6.0-2.LTS' //implementation 'com.github.yausername.youtubedl-android:library:0.15.0' // Aniyomi diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f752bc76b36..385f4fa7313 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ - + @@ -147,6 +147,9 @@ + @@ -427,6 +430,10 @@ android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService" android:exported="true" android:permission="android.permission.BIND_JOB_SERVICE" /> + () + fun startTorrent() { + if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) { + launchIO { + if (!ServerService.isRunning()) { + ServerService.start() + } } } - }*/ //TODO: remove this + } + if (torrentManager.isInitialized.value == false) { + torrentManager.isInitialized.observe(this) { + if (it) { + startTorrent() + } + } + } else { + startTorrent() + } } override fun onRestart() { @@ -473,7 +484,7 @@ class MainActivity : AppCompatActivity() { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32 - val params : ViewGroup.MarginLayoutParams = + val params: ViewGroup.MarginLayoutParams = binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams params.updateMargins(bottom = margin.toPx) } diff --git a/app/src/main/java/ani/dantotsu/addons/Addon.kt b/app/src/main/java/ani/dantotsu/addons/Addon.kt new file mode 100644 index 00000000000..e7b1a1d7b26 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/Addon.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt b/app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt new file mode 100644 index 00000000000..0f679becb16 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt @@ -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 { + return try { + val res = client.get("https://api.github.com/repos/$repo/releases") + .parsed().map { + Mapper.json.decodeFromJsonElement(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().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): 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 + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/AddonListener.kt b/app/src/main/java/ani/dantotsu/addons/AddonListener.kt new file mode 100644 index 00000000000..7fdd47861de --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/AddonListener.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt b/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt new file mode 100644 index 00000000000..c44338d9180 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt @@ -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) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/AddonManager.kt b/app/src/main/java/ani/dantotsu/addons/AddonManager.kt new file mode 100644 index 00000000000..19d9cd26b0d --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/AddonManager.kt @@ -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( + 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 { + return installer.downloadAndInstall(url, getPackageName()?: "", name, type) + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/LoadResult.kt b/app/src/main/java/ani/dantotsu/addons/LoadResult.kt new file mode 100644 index 00000000000..e59288c8228 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/LoadResult.kt @@ -0,0 +1,8 @@ +package ani.dantotsu.addons + +abstract class LoadResult { + + abstract class Success : LoadResult() + + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/download/AddonInstallReceiver.kt b/app/src/main/java/ani/dantotsu/addons/download/AddonInstallReceiver.kt new file mode 100644 index 00000000000..f08f375aedd --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/download/AddonInstallReceiver.kt @@ -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 -> {} + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/download/DownloadAddon.kt b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddon.kt new file mode 100644 index 00000000000..4409206d27f --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddon.kt @@ -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) + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApi.kt b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApi.kt new file mode 100644 index 00000000000..c786387e644 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApi.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonManager.kt b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonManager.kt new file mode 100644 index 00000000000..a136224d79c --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonManager.kt @@ -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(context) { + + override var extension: DownloadAddon.Installed? = null + override var name: String = "Download Addon" + override var type = AddonType.DOWNLOAD + + private val _isInitialized = MutableLiveData().apply { value = false } + val isInitialized: LiveData = _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" + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/download/DownloadLoadResult.kt b/app/src/main/java/ani/dantotsu/addons/download/DownloadLoadResult.kt new file mode 100644 index 00000000000..62038d13f34 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/download/DownloadLoadResult.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddon.kt b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddon.kt new file mode 100644 index 00000000000..ebb369f6edd --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddon.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddonApi.kt b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddonApi.kt new file mode 100644 index 00000000000..956e5c21e95 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddonApi.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddonManager.kt b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddonManager.kt new file mode 100644 index 00000000000..184663c5d24 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddonManager.kt @@ -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(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().apply { value = false } + val isInitialized: LiveData = _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" + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/torrent/TorrentLoadResult.kt b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentLoadResult.kt new file mode 100644 index 00000000000..8cffb9288dc --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentLoadResult.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt new file mode 100644 index 00000000000..6d7f11017fa --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt @@ -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() + private val extension = Injekt.get().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().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(), ServerService::class.java).apply { + action = ACTION_START + } + Injekt.get().startService(intent) + } catch (e: Exception) { + e.printStackTrace() + } + } + + + fun stop() { + try { + val intent = + Intent(Injekt.get(), ServerService::class.java).apply { + action = ACTION_STOP + } + Injekt.get().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().extension?.extension?.echo() + while (echo == "") { + Thread.sleep(1000) + count++ + if (count > timeout) { + return false + } + echo = Injekt.get().extension?.extension?.echo() + } + Logger.log("ServerService: Server started: $echo") + return true + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt index 5a58cea6336..188565b545a 100644 --- a/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt +++ b/app/src/main/java/ani/dantotsu/aniyomi/anime/custom/InjektModules.kt @@ -6,8 +6,10 @@ import androidx.annotation.OptIn import androidx.core.content.ContextCompat import androidx.media3.common.util.UnstableApi import androidx.media3.database.StandaloneDatabaseProvider +import ani.dantotsu.addons.download.DownloadAddonManager import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.download.DownloadsManager +import ani.dantotsu.addons.torrent.TorrentAddonManager import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.parsers.novel.NovelExtensionManager import eu.kanade.domain.base.BasePreferences @@ -38,10 +40,13 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { DownloadsManager(app) } addSingletonFactory { NetworkHelper(app) } + addSingletonFactory { NetworkHelper(app).client } addSingletonFactory { AnimeExtensionManager(app) } addSingletonFactory { MangaExtensionManager(app) } addSingletonFactory { NovelExtensionManager(app) } + addSingletonFactory { TorrentAddonManager(app) } + addSingletonFactory { DownloadAddonManager(app) } addSingletonFactory { AndroidAnimeSourceManager(app, get()) } addSingletonFactory { AndroidMangaSourceManager(app, get()) } diff --git a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt index 38801e4d604..c5120abf617 100644 --- a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt @@ -19,6 +19,7 @@ import androidx.documentfile.provider.DocumentFile import androidx.media3.common.util.UnstableApi import ani.dantotsu.FileUrl import ani.dantotsu.R +import ani.dantotsu.addons.download.DownloadAddonManager import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.defaultHeaders import ani.dantotsu.download.DownloadedType @@ -37,10 +38,6 @@ import ani.dantotsu.toast import ani.dantotsu.util.Logger import com.anggrayudi.storage.file.forceDelete import com.anggrayudi.storage.file.openOutputStream -import com.arthenica.ffmpegkit.FFmpegKit -import com.arthenica.ffmpegkit.FFmpegKitConfig -import com.arthenica.ffmpegkit.FFprobeKit -import com.arthenica.ffmpegkit.SessionState import com.google.gson.GsonBuilder import com.google.gson.InstanceCreator import eu.kanade.tachiyomi.animesource.model.SAnime @@ -76,6 +73,7 @@ class AnimeDownloaderService : Service() { private val mutex = Mutex() private var isCurrentlyProcessing = false private var currentTasks: MutableList = mutableListOf() + private val ffExtension = Injekt.get().extension?.extension override fun onBind(intent: Intent?): IBinder? { // This is only required for bound services. @@ -84,6 +82,11 @@ class AnimeDownloaderService : Service() { override fun onCreate() { super.onCreate() + if (ffExtension == null) { + toast(getString(R.string.download_addon_not_found)) + stopSelf() + return + } notificationManager = NotificationManagerCompat.from(this) builder = NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply { @@ -165,7 +168,7 @@ class AnimeDownloaderService : Service() { .map { it.sessionId }.toMutableList() sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId }) sessionIds.forEach { - FFmpegKit.cancel(it) + ffExtension!!.cancelDownload(it) } currentTasks.removeAll { it.getTaskName() == taskName } CoroutineScope(Dispatchers.Default).launch { @@ -229,7 +232,7 @@ class AnimeDownloaderService : Service() { var percent = 0 var totalLength = 0.0 - val path = FFmpegKitConfig.getSafParameterForWrite( + val path = ffExtension!!.setDownloadPath( this@AnimeDownloaderService, outputFile.uri ) @@ -242,49 +245,30 @@ class AnimeDownloaderService : Service() { .append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'") } val probeRequest = "-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\"" - FFprobeKit.executeAsync( - probeRequest, - { - Logger.log("FFprobeKit: $it") - }, { - if (it.message.toDoubleOrNull() != null) { - totalLength = it.message.toDouble() - } - }) + ffExtension.executeFFProbe( + probeRequest + ) { + if (it.toDoubleOrNull() != null) { + totalLength = it.toDouble() + } + } val headers = headersStringBuilder.toString() var request = "-headers $headers " request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace" Logger.log("Request: $request") val ffTask = - FFmpegKit.executeAsync(request, - { session -> - val state: SessionState = session.state - val returnCode = session.returnCode - // CALLED WHEN SESSION IS EXECUTED - Logger.log( - java.lang.String.format( - "FFmpeg process exited with state %s and rc %s.%s", - state, - returnCode, - session.failStackTrace - ) - ) - - }, { - // CALLED WHEN SESSION PRINTS LOGS - Logger.log(it.message) - }) { + ffExtension.executeFFMpeg(request) { // CALLED WHEN SESSION GENERATES STATISTICS - val timeInMilliseconds = it.time + val timeInMilliseconds = it if (timeInMilliseconds > 0 && totalLength > 0) { - percent = ((it.time / 1000) / totalLength * 100).toInt() + percent = ((it / 1000) / totalLength * 100).toInt() } Logger.log("Statistics: $it") } - task.sessionId = ffTask.sessionId + task.sessionId = ffTask currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId = - ffTask.sessionId + ffTask saveMediaInfo(task) task.subtitle?.let { @@ -300,8 +284,8 @@ class AnimeDownloaderService : Service() { } // periodically check if the download is complete - while (ffTask.state != SessionState.COMPLETED) { - if (ffTask.state == SessionState.FAILED) { + while (ffExtension.getState(ffTask) != "COMPLETED") { + if (ffExtension.getState(ffTask) == "FAILED") { Logger.log("Download failed") builder.setContentText( "${ @@ -313,7 +297,7 @@ class AnimeDownloaderService : Service() { ) notificationManager.notify(NOTIFICATION_ID, builder.build()) toast("${getTaskName(task.title, task.episode)} Download failed") - Logger.log("Download failed: ${ffTask.failStackTrace}") + Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}") downloadsManager.removeDownload( DownloadedType( task.title, @@ -348,8 +332,8 @@ class AnimeDownloaderService : Service() { } kotlinx.coroutines.delay(2000) } - if (ffTask.state == SessionState.COMPLETED) { - if (ffTask.returnCode.isValueError) { + if (ffExtension.getState(ffTask) == "COMPLETED") { + if (ffExtension.hadError(ffTask)) { Logger.log("Download failed") builder.setContentText( "${ diff --git a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt index 123a54c1f91..1e036eab400 100644 --- a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt @@ -9,7 +9,6 @@ import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.ServiceInfo import android.os.Build -import android.os.Environment import android.os.IBinder import android.widget.Toast import androidx.core.app.ActivityCompat @@ -50,8 +49,6 @@ import okio.sink import tachiyomi.core.util.lang.launchIO import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File -import java.io.FileOutputStream import java.io.IOException import java.net.HttpURLConnection import java.net.URL diff --git a/app/src/main/java/ani/dantotsu/download/video/Helper.kt b/app/src/main/java/ani/dantotsu/download/video/Helper.kt index ed146b901d9..67fb9e2f1c7 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -7,49 +7,23 @@ import android.app.AlertDialog import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.net.Uri import android.os.Build import androidx.annotation.OptIn import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import androidx.media3.common.C -import androidx.media3.common.MediaItem -import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi -import androidx.media3.database.StandaloneDatabaseProvider -import androidx.media3.datasource.DataSource -import androidx.media3.datasource.HttpDataSource -import androidx.media3.datasource.cache.NoOpCacheEvictor -import androidx.media3.datasource.cache.SimpleCache -import androidx.media3.datasource.okhttp.OkHttpDataSource -import androidx.media3.exoplayer.DefaultRenderersFactory -import androidx.media3.exoplayer.offline.Download -import androidx.media3.exoplayer.offline.DownloadHelper -import androidx.media3.exoplayer.offline.DownloadManager -import androidx.media3.exoplayer.offline.DownloadService -import androidx.media3.exoplayer.scheduler.Requirements import ani.dantotsu.R -import ani.dantotsu.defaultHeaders import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.anime.AnimeDownloaderService import ani.dantotsu.download.anime.AnimeServiceDataSingleton -import ani.dantotsu.logError import ani.dantotsu.media.Media import ani.dantotsu.media.MediaType -import ani.dantotsu.okHttpClient import ani.dantotsu.parsers.Subtitle -import ani.dantotsu.parsers.SubtitleType import ani.dantotsu.parsers.Video -import ani.dantotsu.parsers.VideoType import ani.dantotsu.settings.saving.PrefManager -import ani.dantotsu.util.Logger -import eu.kanade.tachiyomi.network.NetworkHelper import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File -import java.io.IOException -import java.util.concurrent.Executors @SuppressLint("UnsafeOptInUsageError") object Helper { diff --git a/app/src/main/java/ani/dantotsu/media/MediaType.kt b/app/src/main/java/ani/dantotsu/media/MediaType.kt index 6762d98efa6..dde4e2862c4 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaType.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaType.kt @@ -1,11 +1,15 @@ package ani.dantotsu.media -enum class MediaType { +interface Type { + fun asText(): String +} + +enum class MediaType: Type { ANIME, MANGA, NOVEL; - fun asText(): String { + override fun asText(): String { return when (this) { ANIME -> "Anime" MANGA -> "Manga" @@ -14,12 +18,34 @@ enum class MediaType { } companion object { - fun fromText(string : String): MediaType { + fun fromText(string : String): MediaType? { return when (string) { "Anime" -> ANIME "Manga" -> MANGA "Novel" -> NOVEL - else -> { ANIME } + else -> { null } + } + } + } +} + +enum class AddonType: Type { + TORRENT, + DOWNLOAD; + + override fun asText(): String { + return when (this) { + TORRENT -> "Torrent" + DOWNLOAD -> "Download" + } + } + + companion object { + fun fromText(string : String): AddonType? { + return when (string) { + "Torrent" -> TORRENT + "Download" -> DOWNLOAD + else -> { null } } } } diff --git a/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt b/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt index 86e8af61e11..27bd887a295 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt @@ -17,9 +17,7 @@ import ani.dantotsu.currContext import ani.dantotsu.databinding.ItemEpisodeCompactBinding import ani.dantotsu.databinding.ItemEpisodeGridBinding import ani.dantotsu.databinding.ItemEpisodeListBinding -import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.getDirSize -import ani.dantotsu.download.anime.AnimeDownloaderService import ani.dantotsu.media.Media import ani.dantotsu.media.MediaNameAdapter import ani.dantotsu.media.MediaType diff --git a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt index e203a77f649..59e22c4b476 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt @@ -32,6 +32,7 @@ import ani.dantotsu.databinding.ItemStreamBinding import ani.dantotsu.databinding.ItemUrlBinding import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.video.Helper +import ani.dantotsu.addons.torrent.TorrentAddonManager import ani.dantotsu.hideSystemBars import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsViewModel @@ -50,9 +51,11 @@ import ani.dantotsu.snackString import ani.dantotsu.tryWith import ani.dantotsu.util.Logger import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import tachiyomi.core.util.lang.launchIO import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.text.DecimalFormat @@ -252,32 +255,70 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } } + @OptIn(DelicateCoroutinesApi::class) @SuppressLint("UnsafeOptInUsageError") fun startExoplayer(media: Media) { prevEpisode = null - dismiss() - episode?.let { ep -> val video = ep.extractors?.find { it.server.name == ep.selectedExtractor }?.videos?.getOrNull(ep.selectedVideo) video?.file?.url?.let { url -> - if (url.startsWith("magnet:")) { - try { - externalPlayerResult.launch(exportMagnetIntent(ep, video)) - } catch (e: ActivityNotFoundException) { - val amnis = "com.amnis" - try { - startActivity(Intent( - Intent.ACTION_VIEW, - Uri.parse("market://details?id=$amnis")) + if (video.file.url.startsWith("magnet:") || video.file.url.endsWith(".torrent")) { + val torrentExtension = Injekt.get() + if (torrentExtension.isAvailable()) { + val activity = currActivity() ?: requireActivity() + launchIO { + val extension = torrentExtension.extension!!.extension + torrentExtension.torrentHash?.let { + extension.removeTorrent(it) + } + val index = if (video.file.url.contains("index=")) { + video.file.url.substringAfter("index=").toIntOrNull() ?: 0 + } else 0 + Logger.log("Sending: ${video.file.url}, ${video.quality}, $index") + val currentTorrent = extension.addTorrent( + video.file.url, video.quality.toString(), "", "", false ) + torrentExtension.torrentHash = currentTorrent.hash + video.file.url = extension.getLink(currentTorrent, index) + Logger.log("Received: ${video.file.url}") + if (launch == true) { + Intent(activity, ExoplayerView::class.java).apply { + ExoplayerView.media = media + ExoplayerView.initialized = true + startActivity(this) + } + } else { + model.setEpisode( + media.anime!!.episodes!![media.anime.selectedEpisode!!]!!, + "startExo no launch" + ) + } + dismiss() + } + } else { + try { + externalPlayerResult.launch(exportMagnetIntent(ep, video)) } catch (e: ActivityNotFoundException) { - startActivity(Intent( - Intent.ACTION_VIEW, - Uri.parse("https://play.google.com/store/apps/details?id=$amnis") - )) + val amnis = "com.amnis" + try { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("market://details?id=$amnis") + ) + ) + dismiss() + } catch (e: ActivityNotFoundException) { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=$amnis") + ) + ) + } } } return @@ -285,6 +326,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } } + dismiss() if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) { stopAddingToList() val intent = Intent(activity, ExoplayerView::class.java) diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt index eaa553ab026..e13303bf914 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt @@ -5,7 +5,6 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Bundle -import android.os.Environment import android.os.Handler import android.os.Looper import android.os.Parcelable @@ -13,7 +12,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat -import androidx.core.content.FileProvider import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -44,7 +42,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File class NovelReadFragment : Fragment(), DownloadTriggerCallback, diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index 0b59fe14e0c..95edf008110 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -560,7 +560,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac format = VideoType.CONTAINER } } catch (malformed: MalformedURLException) { - if (videoUrl.startsWith("magnet:")) + if (videoUrl.startsWith("magnet:") || videoUrl.endsWith(".torrent")) format = VideoType.CONTAINER else throw malformed diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt index a3a239a8d8e..4b132d60d48 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt @@ -1,8 +1,6 @@ package ani.dantotsu.parsers import android.app.Application -import android.os.Environment -import ani.dantotsu.currContext import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory import ani.dantotsu.media.MediaNameAdapter @@ -13,7 +11,6 @@ import eu.kanade.tachiyomi.source.model.SManga import me.xdrop.fuzzywuzzy.FuzzySearch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File class OfflineMangaParser : MangaParser() { private val downloadManager = Injekt.get() diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt index 2ae88200662..ff326b33fc9 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt @@ -1,8 +1,6 @@ package ani.dantotsu.parsers import android.app.Application -import android.os.Environment -import ani.dantotsu.currContext import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory import ani.dantotsu.media.MediaNameAdapter @@ -10,7 +8,6 @@ import ani.dantotsu.media.MediaType import me.xdrop.fuzzywuzzy.FuzzySearch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File class OfflineNovelParser : NovelParser() { private val downloadManager = Injekt.get() diff --git a/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt index fe7b81d22fd..d78cc5afddc 100644 --- a/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt @@ -80,48 +80,14 @@ class AnimeExtensionsFragment : Fragment(), if (isAdded) { val notificationManager = requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - + val installerSteps = InstallerSteps(notificationManager, context) // Start the installation process animeExtensionManager.installExtension(pkg) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - { installStep -> - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(R.drawable.ic_round_sync_24) - .setContentTitle(getString(R.string.installing_extension)) - .setContentText(getString(R.string.install_step, installStep)) - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - }, - { error -> - Injekt.get().logException(error) - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_ERROR - ) - .setSmallIcon(R.drawable.ic_round_info_24) - .setContentTitle(getString(R.string.installation_failed, error.message)) - .setContentText(getString(R.string.error_message, error.message)) - .setPriority(NotificationCompat.PRIORITY_HIGH) - notificationManager.notify(1, builder.build()) - snackString(getString(R.string.installation_failed, error.message)) - }, - { - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(R.drawable.ic_download_24) - .setContentTitle(getString(R.string.installation_complete)) - .setContentText(getString(R.string.extension_has_been_installed)) - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - viewModel.invalidatePager() - snackString(getString(R.string.extension_installed)) - } + { installStep -> installerSteps.onInstallStep(installStep) {} }, + { error -> installerSteps.onError(error) {} }, + { installerSteps.onComplete { viewModel.invalidatePager() } } ) } } diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt index fbae5b70099..506a43fb5f6 100644 --- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt @@ -1,7 +1,6 @@ package ani.dantotsu.settings import android.app.AlertDialog -import android.os.Build import android.os.Bundle import android.text.Editable import android.text.TextWatcher @@ -31,7 +30,6 @@ import ani.dantotsu.others.AndroidBug5497Workaround import ani.dantotsu.others.LanguageMapper import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName -import ani.dantotsu.snackString import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager import com.google.android.material.tabs.TabLayout diff --git a/app/src/main/java/ani/dantotsu/settings/InstallerSteps.kt b/app/src/main/java/ani/dantotsu/settings/InstallerSteps.kt new file mode 100644 index 00000000000..13cd42514bd --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/InstallerSteps.kt @@ -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().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)) + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt index c51180964ac..c2d64f37e4c 100644 --- a/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt @@ -81,48 +81,15 @@ class MangaExtensionsFragment : Fragment(), val context = requireContext() val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val installerSteps = InstallerSteps(notificationManager, context) // Start the installation process mangaExtensionManager.installExtension(pkg) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - { installStep -> - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(R.drawable.ic_round_sync_24) - .setContentTitle(getString(R.string.installing_extension)) - .setContentText(getString(R.string.install_step, installStep)) - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - }, - { error -> - Injekt.get().logException(error) - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_ERROR - ) - .setSmallIcon(R.drawable.ic_round_info_24) - .setContentTitle(getString(R.string.installation_failed, error.message)) - .setContentText(getString(R.string.error_message, error.message)) - .setPriority(NotificationCompat.PRIORITY_HIGH) - notificationManager.notify(1, builder.build()) - snackString(getString(R.string.installation_failed, error.message)) - }, - { - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(R.drawable.ic_download_24) - .setContentTitle(getString(R.string.installation_complete)) - .setContentText(getString(R.string.extension_has_been_installed)) - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - viewModel.invalidatePager() - snackString(getString(R.string.extension_installed)) - } + { installStep -> installerSteps.onInstallStep(installStep) {} }, + { error -> installerSteps.onError(error) {} }, + { installerSteps.onComplete { viewModel.invalidatePager() } } ) } } diff --git a/app/src/main/java/ani/dantotsu/settings/Settings.kt b/app/src/main/java/ani/dantotsu/settings/Settings.kt index 8f319aac169..db3f1d22a5a 100644 --- a/app/src/main/java/ani/dantotsu/settings/Settings.kt +++ b/app/src/main/java/ani/dantotsu/settings/Settings.kt @@ -1,6 +1,5 @@ package ani.dantotsu.settings -import android.view.ViewGroup import ani.dantotsu.databinding.ItemSettingsBinding import ani.dantotsu.databinding.ItemSettingsSwitchBinding @@ -13,6 +12,7 @@ data class Settings( val onLongClick: (() -> Unit)? = null, val switch: ((isChecked:Boolean , view: ItemSettingsSwitchBinding ) -> Unit)? = null, val attach:((ItemSettingsBinding) -> Unit)? = null, + val attachToSwitch : ((ItemSettingsSwitchBinding) -> Unit)? = null, val isVisible: Boolean = true, val isActivity: Boolean = false, var isChecked : Boolean = false, diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsAboutActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsAboutActivity.kt index 1c82c194dbc..664e9f911b1 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsAboutActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsAboutActivity.kt @@ -2,6 +2,7 @@ package ani.dantotsu.settings import android.content.Intent import android.os.Bundle +import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AppCompatActivity @@ -82,11 +83,13 @@ class SettingsAboutActivity : AppCompatActivity() { PrefManager.setVal(PrefName.LogToFile, isChecked) restartApp() }, - attach = { - it.settingsDesc.setOnLongClickListener { + attachToSwitch = { + it.settingsExtraIcon.visibility = View.VISIBLE + it.settingsExtraIcon.setImageResource(R.drawable.ic_round_share_24) + it.settingsExtraIcon.setOnClickListener { Logger.shareLog(context) - true } + } ), Settings( diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt index a2420ea1472..60f8dace586 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt @@ -144,6 +144,16 @@ class SettingsActivity : AppCompatActivity() { }, isActivity = true ), + Settings( + type = 1, + name = getString(R.string.addons), + desc = getString(R.string.addons_desc), + icon = R.drawable.ic_round_restaurant_24, + onClick = { + startActivity(Intent(context, SettingsAddonActivity::class.java)) + }, + isActivity = true + ), Settings( type = 1, name = getString(R.string.notifications), diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsAdapter.kt b/app/src/main/java/ani/dantotsu/settings/SettingsAdapter.kt index 37b934b9e8d..7fb944eb898 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsAdapter.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsAdapter.kt @@ -87,6 +87,7 @@ class SettingsAdapter(private val settings: ArrayList) : true } b.settingsLayout.visibility = if (settings.isVisible) View.VISIBLE else View.GONE + settings.attachToSwitch?.invoke(b) } } } diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsAddonActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsAddonActivity.kt new file mode 100644 index 00000000000..3086fada2c3 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/SettingsAddonActivity.kt @@ -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 { + 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().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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt index 2d47a32ec2b..7958ea9ea3d 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt @@ -13,22 +13,17 @@ import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.R import ani.dantotsu.connections.anilist.api.NotificationType import ani.dantotsu.databinding.ActivitySettingsNotificationsBinding -import ani.dantotsu.download.DownloadsManager import ani.dantotsu.initActivity -import ani.dantotsu.media.MediaType import ani.dantotsu.navBarHeight import ani.dantotsu.notifications.TaskScheduler import ani.dantotsu.notifications.anilist.AnilistNotificationWorker import ani.dantotsu.notifications.comment.CommentNotificationWorker import ani.dantotsu.notifications.subscription.SubscriptionNotificationWorker import ani.dantotsu.openSettings -import ani.dantotsu.restartApp import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get class SettingsNotificationActivity: AppCompatActivity(){ private lateinit var binding: ActivitySettingsNotificationsBinding diff --git a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt index 9bc61d27f71..b418915fe76 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt @@ -120,6 +120,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files UseInternalCast(Pref(Location.Player, Boolean::class, false)), Pip(Pref(Location.Player, Boolean::class, true)), RotationPlayer(Pref(Location.Player, Boolean::class, true)), + TorrentEnabled(Pref(Location.Player, Boolean::class, false)), //Reader ShowSource(Pref(Location.Reader, Boolean::class, true)), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt index 0e77ed37052..2e254c52307 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt @@ -39,6 +39,12 @@ object Notifications { const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS" const val GROUP_NEW_EPISODES = "eu.kanade.tachiyomi.NEW_EPISODES" + /** + * Notification channel and ids used by the torrent server. + */ + const val ID_TORRENT_SERVER = -1100 + const val CHANNEL_TORRENT_SERVER = "dantotsu_torrent_server" + /** * Notification channel used for Incognito Mode */ @@ -154,6 +160,9 @@ object Notifications { buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) { setName("Incognito Mode") }, + buildNotificationChannel(CHANNEL_TORRENT_SERVER, IMPORTANCE_LOW) { + setName("Torrent Server") + }, buildNotificationChannel(CHANNEL_COMMENTS, IMPORTANCE_HIGH) { setName("Comments") setGroup(GROUP_COMMENTS) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/Torrent.kt b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/Torrent.kt new file mode 100644 index 00000000000..e1445dd19a6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/torrentServer/model/Torrent.kt @@ -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? = null, +) + +@Serializable +data class FileStat( + var id: Int? = null, + var path: String, + var length: Long, +) \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt index 9bb078240da..e568a1faec5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/anime/AnimeExtensionManager.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.anime import android.content.Context import android.graphics.drawable.Drawable +import ani.dantotsu.media.MediaType import ani.dantotsu.snackString import ani.dantotsu.util.Logger import eu.kanade.domain.source.service.SourcePreferences @@ -206,7 +207,8 @@ class AnimeExtensionManager( * @param extension The anime extension to be installed. */ fun installExtension(extension: AnimeExtension.Available): Observable { - return installer.downloadAndInstall(api.getAnimeApkUrl(extension), extension) + return installer.downloadAndInstall(api.getAnimeApkUrl(extension), extension.pkgName, + extension.name, MediaType.ANIME) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt index c3f13e27205..32504b89095 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/Installer.kt @@ -8,7 +8,11 @@ import android.content.IntentFilter import android.net.Uri import androidx.annotation.CallSuper import androidx.localbroadcastmanager.content.LocalBroadcastManager +import ani.dantotsu.addons.download.DownloadAddonManager +import ani.dantotsu.addons.torrent.TorrentAddonManager +import ani.dantotsu.media.AddonType import ani.dantotsu.media.MediaType +import ani.dantotsu.media.Type import ani.dantotsu.parsers.novel.NovelExtensionManager import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager @@ -25,6 +29,8 @@ abstract class Installer(private val service: Service) { private val animeExtensionManager: AnimeExtensionManager by injectLazy() private val mangaExtensionManager: MangaExtensionManager by injectLazy() private val novelExtensionManager: NovelExtensionManager by injectLazy() + private val torrentAddonManager: TorrentAddonManager by injectLazy() + private val downloadAddonManager: DownloadAddonManager by injectLazy() private var waitingInstall = AtomicReference(null) private val queue = Collections.synchronizedList(mutableListOf()) @@ -49,7 +55,7 @@ abstract class Installer(private val service: Service) { * @param downloadId Download ID as known by [ExtensionManager] * @param uri Uri of APK to install */ - fun addToQueue(type: MediaType, downloadId: Long, uri: Uri) { + fun addToQueue(type: Type, downloadId: Long, uri: Uri) { queue.add(Entry(type, downloadId, uri)) checkQueue() } @@ -63,10 +69,17 @@ abstract class Installer(private val service: Service) { */ @CallSuper open fun processEntry(entry: Entry) { - when (entry.type) { - MediaType.ANIME -> animeExtensionManager.setInstalling(entry.downloadId) - MediaType.MANGA -> mangaExtensionManager.setInstalling(entry.downloadId) - MediaType.NOVEL -> novelExtensionManager.setInstalling(entry.downloadId) + if (entry.type is MediaType) { + when (entry.type) { + MediaType.ANIME -> animeExtensionManager.setInstalling(entry.downloadId) + MediaType.MANGA -> mangaExtensionManager.setInstalling(entry.downloadId) + MediaType.NOVEL -> novelExtensionManager.setInstalling(entry.downloadId) + } + } else { + when (entry.type) { + AddonType.TORRENT -> torrentAddonManager.setInstalling(entry.downloadId) + AddonType.DOWNLOAD -> downloadAddonManager.setInstalling(entry.downloadId) + } } } @@ -90,17 +103,34 @@ abstract class Installer(private val service: Service) { fun continueQueue(resultStep: InstallStep) { val completedEntry = waitingInstall.getAndSet(null) if (completedEntry != null) { - when (completedEntry.type) { - MediaType.ANIME -> { - animeExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep) - } + if (completedEntry.type is MediaType) { + when (completedEntry.type) { + MediaType.ANIME -> animeExtensionManager.updateInstallStep( + completedEntry.downloadId, + resultStep + ) + + MediaType.MANGA -> mangaExtensionManager.updateInstallStep( + completedEntry.downloadId, + resultStep + ) - MediaType.MANGA -> { - mangaExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep) + MediaType.NOVEL -> novelExtensionManager.updateInstallStep( + completedEntry.downloadId, + resultStep + ) } + } else { + when (completedEntry.type) { + AddonType.TORRENT -> torrentAddonManager.updateInstallStep( + completedEntry.downloadId, + resultStep + ) - MediaType.NOVEL -> { - novelExtensionManager.updateInstallStep(completedEntry.downloadId, resultStep) + AddonType.DOWNLOAD -> downloadAddonManager.updateInstallStep( + completedEntry.downloadId, + resultStep + ) } } checkQueue() @@ -113,7 +143,7 @@ abstract class Installer(private val service: Service) { * * @see ready */ - fun checkQueue() { + private fun checkQueue() { if (!ready) { return } @@ -135,15 +165,35 @@ abstract class Installer(private val service: Service) { open fun onDestroy() { LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver) queue.forEach { - when (it.type) { - MediaType.ANIME -> { - animeExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error) - } - MediaType.MANGA -> { - mangaExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error) + + if (it.type is MediaType) { + when (it.type) { + MediaType.ANIME -> animeExtensionManager.updateInstallStep( + it.downloadId, + InstallStep.Error + ) + + MediaType.MANGA -> mangaExtensionManager.updateInstallStep( + it.downloadId, + InstallStep.Error + ) + + MediaType.NOVEL -> novelExtensionManager.updateInstallStep( + it.downloadId, + InstallStep.Error + ) } - MediaType.NOVEL -> { - novelExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error) + } else { + when (it.type) { + AddonType.TORRENT -> torrentAddonManager.updateInstallStep( + it.downloadId, + InstallStep.Error + ) + + AddonType.DOWNLOAD -> downloadAddonManager.updateInstallStep( + it.downloadId, + InstallStep.Error + ) } } } @@ -168,15 +218,34 @@ abstract class Installer(private val service: Service) { this.waitingInstall.set(null) checkQueue() } - when (toCancel.type) { - MediaType.ANIME -> { - animeExtensionManager.updateInstallStep(downloadId, InstallStep.Idle) - } - MediaType.MANGA -> { - mangaExtensionManager.updateInstallStep(downloadId, InstallStep.Idle) + if (toCancel.type is MediaType) { + when (toCancel.type) { + MediaType.ANIME -> animeExtensionManager.updateInstallStep( + downloadId, + InstallStep.Idle + ) + + MediaType.MANGA -> mangaExtensionManager.updateInstallStep( + downloadId, + InstallStep.Idle + ) + + MediaType.NOVEL -> novelExtensionManager.updateInstallStep( + downloadId, + InstallStep.Idle + ) } - MediaType.NOVEL -> { - novelExtensionManager.updateInstallStep(downloadId, InstallStep.Idle) + } else { + when (toCancel.type) { + AddonType.TORRENT -> torrentAddonManager.updateInstallStep( + downloadId, + InstallStep.Idle + ) + + AddonType.DOWNLOAD -> downloadAddonManager.updateInstallStep( + downloadId, + InstallStep.Idle + ) } } } @@ -188,7 +257,7 @@ abstract class Installer(private val service: Service) { * @param downloadId Download ID as known by [ExtensionManager] * @param uri Uri of APK to install */ - data class Entry(val type: MediaType, val downloadId: Long, val uri: Uri) + data class Entry(val type: Type, val downloadId: Long, val uri: Uri) init { val filter = IntentFilter(ACTION_CANCEL_QUEUE) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt index 3dcb4cf8aad..75f139b26bc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/manga/MangaExtensionManager.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.manga import android.content.Context import android.graphics.drawable.Drawable +import ani.dantotsu.media.MediaType import ani.dantotsu.snackString import ani.dantotsu.util.Logger import eu.kanade.domain.source.service.SourcePreferences @@ -203,7 +204,8 @@ class MangaExtensionManager( * @param extension The extension to be installed. */ fun installExtension(extension: MangaExtension.Available): Observable { - return installer.downloadAndInstall(api.getMangaApkUrl(extension), extension) + return installer.downloadAndInstall(api.getMangaApkUrl(extension), extension.pkgName, + extension.name, MediaType.MANGA) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt index 778f8f9f557..dce7d4f88de 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt @@ -5,6 +5,9 @@ import android.os.Bundle import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import ani.dantotsu.addons.download.DownloadAddonManager +import ani.dantotsu.addons.torrent.TorrentAddonManager +import ani.dantotsu.media.AddonType import ani.dantotsu.media.MediaType import ani.dantotsu.parsers.novel.NovelExtensionManager import ani.dantotsu.themes.ThemeManager @@ -29,7 +32,8 @@ class ExtensionInstallActivity : AppCompatActivity() { private var ignoreResult = false private var hasIgnoredResult = false - private var type: MediaType? = null + private var mediaType: MediaType? = null + private var addonType: AddonType? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -37,7 +41,9 @@ class ExtensionInstallActivity : AppCompatActivity() { ThemeManager(this).applyTheme() if (intent.hasExtra(ExtensionInstaller.EXTRA_EXTENSION_TYPE)) - type = intent.getSerializableExtraCompat(ExtensionInstaller.EXTRA_EXTENSION_TYPE) + mediaType = intent.getSerializableExtraCompat(ExtensionInstaller.EXTRA_EXTENSION_TYPE) + if (intent.hasExtra(ExtensionInstaller.EXTRA_ADDON_TYPE)) + addonType = intent.getSerializableExtraCompat(ExtensionInstaller.EXTRA_ADDON_TYPE) @Suppress("DEPRECATION") val installIntent = Intent(Intent.ACTION_INSTALL_PACKAGE) @@ -85,17 +91,34 @@ class ExtensionInstallActivity : AppCompatActivity() { RESULT_CANCELED -> InstallStep.Idle else -> InstallStep.Error } - when (type) { - MediaType.ANIME -> { - Injekt.get().updateInstallStep(downloadId, newStep) - } - MediaType.MANGA -> { - Injekt.get().updateInstallStep(downloadId, newStep) + if (mediaType != null) { + when (mediaType) { + MediaType.ANIME -> { + Injekt.get().updateInstallStep(downloadId, newStep) + } + + MediaType.MANGA -> { + Injekt.get().updateInstallStep(downloadId, newStep) + } + + MediaType.NOVEL -> { + Injekt.get().updateInstallStep(downloadId, newStep) + } + + null -> {} } - MediaType.NOVEL -> { - Injekt.get().updateInstallStep(downloadId, newStep) + } else { + when (addonType) { + AddonType.TORRENT -> { + Injekt.get().updateInstallStep(downloadId, newStep) + } + + AddonType.DOWNLOAD -> { + Injekt.get().updateInstallStep(downloadId, newStep) + } + + null -> {} } - null -> { } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt index 987cfead1d7..d40493e419d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt @@ -50,18 +50,6 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { return this } - /** - * Returns the intent filter this receiver should subscribe to. - */ - private val filter - get() = IntentFilter().apply { - priority = 100 - addAction(Intent.ACTION_PACKAGE_ADDED) - addAction(Intent.ACTION_PACKAGE_REPLACED) - addAction(Intent.ACTION_PACKAGE_REMOVED) - addDataScheme("package") - } - /** * Called when one of the events of the [filter] is received. When the package is an extension, * it's loaded in background and it notifies the [listener] when finished. @@ -136,21 +124,13 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { } } - /** - * Returns true if this package is performing an update. - * - * @param intent The intent that triggered the event. - */ - private fun isReplacing(intent: Intent): Boolean { - return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) - } - /** * Returns the extension triggered by the given intent. * * @param context The application context. * @param intent The intent containing the package name of the extension. */ + @OptIn(DelicateCoroutinesApi::class) private suspend fun getAnimeExtensionFromIntent(context: Context, intent: Intent?): AnimeLoadResult { val pkgName = getPackageNameFromIntent(intent) if (pkgName == null) { @@ -180,12 +160,6 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { }.await() } - /** - * Returns the package name of the installed, updated or removed application. - */ - private fun getPackageNameFromIntent(intent: Intent?): String? { - return intent?.data?.encodedSchemeSpecificPart ?: return null - } /** * Listener that receives extension installation events. @@ -203,4 +177,36 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { fun onExtensionUntrusted(extension: MangaExtension.Untrusted) fun onPackageUninstalled(pkgName: String) } + + companion object { + + /** + * Returns the intent filter this receiver should subscribe to. + */ + val filter + get() = IntentFilter().apply { + priority = 100 + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REPLACED) + addAction(Intent.ACTION_PACKAGE_REMOVED) + addDataScheme("package") + } + + /** + * Returns true if this package is performing an update. + * + * @param intent The intent that triggered the event. + */ + fun isReplacing(intent: Intent): Boolean { + return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) + } + + + /** + * Returns the package name of the installed, updated or removed application. + */ + fun getPackageNameFromIntent(intent: Intent?): String? { + return intent?.data?.encodedSchemeSpecificPart ?: return null + } + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt index 5c726c6cff9..58c698ddaab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallService.kt @@ -8,7 +8,9 @@ import android.net.Uri import android.os.Build import android.os.IBinder import ani.dantotsu.R +import ani.dantotsu.media.AddonType import ani.dantotsu.media.MediaType +import ani.dantotsu.media.Type import ani.dantotsu.util.Logger import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.data.notification.Notifications @@ -45,12 +47,13 @@ class ExtensionInstallService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val uri = intent?.data - val type = intent?.getSerializableExtraCompat(EXTRA_EXTENSION_TYPE) + val mediaType = intent?.getSerializableExtraCompat(EXTRA_EXTENSION_TYPE) + val addonType = intent?.getSerializableExtraCompat(ExtensionInstaller.EXTRA_ADDON_TYPE) val id = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.takeIf { it != -1L } val installerUsed = intent?.getSerializableExtraCompat( EXTRA_INSTALLER ) - if (uri == null || type == null || id == null || installerUsed == null) { + if (uri == null || (mediaType == null && addonType == null) || id == null || installerUsed == null) { stopSelf() return START_NOT_STICKY } @@ -68,7 +71,7 @@ class ExtensionInstallService : Service() { } } } - installer!!.addToQueue(type, id, uri) + installer!!.addToQueue(mediaType ?: addonType!!, id, uri) return START_NOT_STICKY } @@ -84,16 +87,21 @@ class ExtensionInstallService : Service() { fun getIntent( context: Context, - type: MediaType, + type: Type, downloadId: Long, uri: Uri, installer: BasePreferences.ExtensionInstaller, ): Intent { - return Intent(context, ExtensionInstallService::class.java) + val intent = Intent(context, ExtensionInstallService::class.java) .setDataAndType(uri, ExtensionInstaller.APK_MIME) .putExtra(EXTRA_DOWNLOAD_ID, downloadId) - .putExtra(EXTRA_EXTENSION_TYPE, type) .putExtra(EXTRA_INSTALLER, installer) + if (type is MediaType) { + intent.putExtra(EXTRA_EXTENSION_TYPE, type) + } else if (type is AddonType) { + intent.putExtra(ExtensionInstaller.EXTRA_ADDON_TYPE, type) + } + return intent } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index 27ddd610927..1727a6a8527 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -11,7 +11,9 @@ import android.os.Environment import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.net.toUri +import ani.dantotsu.media.AddonType import ani.dantotsu.media.MediaType +import ani.dantotsu.media.Type import ani.dantotsu.parsers.novel.NovelExtension import ani.dantotsu.util.Logger import com.jakewharton.rxrelay.PublishRelay @@ -33,7 +35,7 @@ import java.util.concurrent.TimeUnit * * @param context The application context. */ -internal class ExtensionInstaller(private val context: Context) { +class ExtensionInstaller(private val context: Context) { /** * The system's download manager @@ -65,65 +67,24 @@ internal class ExtensionInstaller(private val context: Context) { * @param url The url of the apk. * @param extension The extension to install. */ - fun downloadAndInstall(url: String, extension: AnimeExtension): Observable = 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.ANIME.asText()) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - - val id = downloadManager.enqueue(request) - activeDownloads[pkgName] = id - - downloadsRelay.filter { it.first == id } - .map { it.second } - // Poll download status - .mergeWith(pollStatus(id)) - // Stop when the application is installed or errors - .takeUntil { it.isCompleted() } - // Always notify on main thread - .observeOn(AndroidSchedulers.mainThread()) - // Always remove the download when unsubscribed - .doOnUnsubscribe { deleteDownload(pkgName) } - } - - fun downloadAndInstall(url: String, extension: MangaExtension): Observable = Observable.defer { - val pkgName = extension.pkgName - + fun downloadAndInstall(url: String, pkgName: String, name: String, type: T): Observable = Observable.defer { val oldDownload = activeDownloads[pkgName] if (oldDownload != null) { deleteDownload(pkgName) } - // Register the receiver after removing (and unregistering) the previous download downloadReceiver.register() val downloadUri = url.toUri() val request = DownloadManager.Request(downloadUri) - .setTitle(extension.name) + .setTitle(name) .setMimeType(APK_MIME) .setDestinationInExternalFilesDir( context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment ) - .setDescription(MediaType.MANGA.asText()) + .setDescription(type.asText()) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) val id = downloadManager.enqueue(request) @@ -131,53 +92,12 @@ internal class ExtensionInstaller(private val context: Context) { downloadsRelay.filter { it.first == id } .map { it.second } - // Poll download status .mergeWith(pollStatus(id)) - // Stop when the application is installed or errors .takeUntil { it.isCompleted() } - // Always notify on main thread .observeOn(AndroidSchedulers.mainThread()) - // Always remove the download when unsubscribed .doOnUnsubscribe { deleteDownload(pkgName) } } - fun downloadAndInstall(url: String, extension: NovelExtension) = Observable.defer { - val pkgName = extension.pkgName - - val oldDownload = activeDownloads[pkgName] - if (oldDownload != null) { - deleteDownload(pkgName) - } - - // Register the receiver after removing (and unregistering) the previous download - downloadReceiver.register() - - val downloadUri = url.toUri() - val request = DownloadManager.Request(downloadUri) - .setTitle(extension.name) - .setMimeType(APK_MIME) - .setDestinationInExternalFilesDir( - context, - Environment.DIRECTORY_DOWNLOADS, - downloadUri.lastPathSegment - ) - .setDescription(MediaType.MANGA.asText()) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - - val id = downloadManager.enqueue(request) - activeDownloads[pkgName] = id - - downloadsRelay.filter { it.first == id } - .map { it.second } - // Poll download status - .mergeWith(pollStatus(id)) - // Stop when the application is installed or errors - .takeUntil { it.isCompleted() } - // Always notify on main thread - .observeOn(AndroidSchedulers.mainThread()) - // Always remove the download when unsubscribed - .doOnUnsubscribe { deleteDownload(pkgName) } - } /** * Returns an observable that polls the given download id for its status every second, as the @@ -215,14 +135,18 @@ internal class ExtensionInstaller(private val context: Context) { * * @param uri The uri of the extension to install. */ - fun installApk(type: MediaType, downloadId: Long, uri: Uri) { + fun installApk(type: Type, downloadId: Long, uri: Uri) { when (val installer = extensionInstaller.get()) { BasePreferences.ExtensionInstaller.LEGACY -> { val intent = Intent(context, ExtensionInstallActivity::class.java) .setDataAndType(uri, APK_MIME) - .putExtra(EXTRA_EXTENSION_TYPE, type) .putExtra(EXTRA_DOWNLOAD_ID, downloadId) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) + if (type is MediaType) { + intent.putExtra(EXTRA_EXTENSION_TYPE, type) + } else if (type is AddonType) { + intent.putExtra(EXTRA_ADDON_TYPE, type) + } context.startActivity(intent) } @@ -342,7 +266,9 @@ internal class ExtensionInstaller(private val context: Context) { ).removePrefix(FILE_SCHEME) val type = MediaType.fromText(cursor.getString( cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION), - )) + )) ?: AddonType.fromText(cursor.getString( + cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION), + )) ?: return installApk(type, id, File(localUri).getUriCompat(context)) } @@ -354,6 +280,7 @@ internal class ExtensionInstaller(private val context: Context) { const val APK_MIME = "application/vnd.android.package-archive" const val EXTRA_DOWNLOAD_ID = "ExtensionInstaller.extra.DOWNLOAD_ID" const val EXTRA_EXTENSION_TYPE = "ExtensionInstaller.extra.EXTENSION_TYPE" + const val EXTRA_ADDON_TYPE = "ExtensionInstaller.extra.ADDON_TYPE" const val FILE_SCHEME = "file://" } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 59f35ef52a8..d68ffbf896a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -60,7 +60,7 @@ internal object ExtensionLoader { const val MANGA_LIB_VERSION_MIN = 1.2 const val MANGA_LIB_VERSION_MAX = 1.5 - private val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or + val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_META_DATA or @Suppress ("DEPRECATION") PackageManager.GET_SIGNATURES or (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index c00db1c62da..33bc4e0936b 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -70,7 +70,7 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings_anime.xml b/app/src/main/res/layout/activity_settings_anime.xml index c9cb208667f..566960eb9d6 100644 --- a/app/src/main/res/layout/activity_settings_anime.xml +++ b/app/src/main/res/layout/activity_settings_anime.xml @@ -136,7 +136,7 @@ android:id="@+id/settingsRecyclerView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginHorizontal="16dp" + android:layout_marginHorizontal="24dp" android:nestedScrollingEnabled="false" android:requiresFadingEdge="vertical" tools:itemCount="5" diff --git a/app/src/main/res/layout/activity_settings_common.xml b/app/src/main/res/layout/activity_settings_common.xml index f668d982f62..5cfd39a16bf 100644 --- a/app/src/main/res/layout/activity_settings_common.xml +++ b/app/src/main/res/layout/activity_settings_common.xml @@ -173,7 +173,7 @@ android:id="@+id/settingsRecyclerView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginHorizontal="16dp" + android:layout_marginHorizontal="24dp" android:nestedScrollingEnabled="false" android:requiresFadingEdge="vertical" tools:itemCount="5" diff --git a/app/src/main/res/layout/activity_settings_extensions.xml b/app/src/main/res/layout/activity_settings_extensions.xml index 07aa599d3cd..67e31348220 100644 --- a/app/src/main/res/layout/activity_settings_extensions.xml +++ b/app/src/main/res/layout/activity_settings_extensions.xml @@ -64,7 +64,7 @@ android:id="@+id/settingsRecyclerView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginHorizontal="16dp" + android:layout_marginHorizontal="24dp" android:nestedScrollingEnabled="false" android:requiresFadingEdge="vertical" tools:itemCount="5" diff --git a/app/src/main/res/layout/activity_settings_manga.xml b/app/src/main/res/layout/activity_settings_manga.xml index ee3e1c1d816..e56eed33e46 100644 --- a/app/src/main/res/layout/activity_settings_manga.xml +++ b/app/src/main/res/layout/activity_settings_manga.xml @@ -118,7 +118,7 @@ android:id="@+id/settingsRecyclerView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginHorizontal="16dp" + android:layout_marginHorizontal="24dp" android:nestedScrollingEnabled="false" android:requiresFadingEdge="vertical" tools:itemCount="5" diff --git a/app/src/main/res/layout/activity_settings_notifications.xml b/app/src/main/res/layout/activity_settings_notifications.xml index 5d32a640fc9..479b7fa1e7e 100644 --- a/app/src/main/res/layout/activity_settings_notifications.xml +++ b/app/src/main/res/layout/activity_settings_notifications.xml @@ -64,7 +64,7 @@ android:id="@+id/settingsRecyclerView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginHorizontal="16dp" + android:layout_marginHorizontal="24dp" android:nestedScrollingEnabled="false" android:requiresFadingEdge="vertical" tools:itemCount="5" diff --git a/app/src/main/res/layout/activity_settings_theme.xml b/app/src/main/res/layout/activity_settings_theme.xml index 3f26f7e8396..bf039a864f7 100644 --- a/app/src/main/res/layout/activity_settings_theme.xml +++ b/app/src/main/res/layout/activity_settings_theme.xml @@ -167,7 +167,7 @@ android:id="@+id/settingsRecyclerView" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginHorizontal="16dp" + android:layout_marginHorizontal="24dp" android:nestedScrollingEnabled="false" android:requiresFadingEdge="vertical" tools:itemCount="5" diff --git a/app/src/main/res/layout/item_settings.xml b/app/src/main/res/layout/item_settings.xml index ae549147bdf..f531c107e25 100644 --- a/app/src/main/res/layout/item_settings.xml +++ b/app/src/main/res/layout/item_settings.xml @@ -36,6 +36,7 @@ android:textSize="16sp" /> - + android:orientation="horizontal" + tools:ignore="UseCompoundDrawables"> + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f7e02089b02..0f5a93b4ae0 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -28,6 +28,7 @@ #999999 #000000 #CD201F + #00FF00 #a3a2a2 #F9EDEDED #93DB00 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13ef23e07d1..efa0502e7c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -382,6 +382,8 @@ Versions FAQ Accounts + Addons + Addons are extensions that provide additional functionality. MyAnimeList Login with Anilist! Anilist @@ -423,6 +425,7 @@ Installation complete The extension has been successfully installed. Extension installed + Installed Error: %1$s Step: %1$s Review @@ -909,4 +912,13 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc Dantotsu\'s very own unpaid labours More like Dantotsu Something to keep in mind + Torrent Addon + Enable torrent + Anime Downloader Addon + Loaded Successfully + Not Installed + Torrent extension not supported on this device + Update Addon + Install Addon + Download addon not found