diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 9670e55c..68444cd8 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 - uses: actions/upload-artifact@v4.3.1 + - name: Upload a Build Artifact + uses: actions/upload-artifact@v4 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' }} @@ -109,31 +104,12 @@ jobs: commit_messages="${commit_messages:0:$max_length}... (truncated)" 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 }} - + curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-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 "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \ + -F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \ https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument env: @@ -141,23 +117,13 @@ jobs: VERSION: ${{ env.VERSION }} - name: Upload Current SHA as Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: last-sha path: last_sha.txt - name: Upload Commit log as Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: commit-log path: commit_log.txt - - - name: Delete Old Pre-Releases - id: delete-pre-releases - uses: sgpublic/delete-release-action@master - with: - pre-release-drop: true - pre-release-keep-count: 3 - pre-release-drop-tag: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 6cc9cdea..2ccc322f 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ output.json #other scripts/ + +#crowdin +crowdin.yml \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 53cb1299..117a1cc9 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" @@ -142,13 +135,13 @@ dependencies { // Markwon ext.markwon_version = '4.6.2' - implementation "io.noties.markwon:core:$markwon_version" - implementation "io.noties.markwon:editor:$markwon_version" - implementation "io.noties.markwon:ext-strikethrough:$markwon_version" - implementation "io.noties.markwon:ext-tables:$markwon_version" - implementation "io.noties.markwon:ext-tasklist:$markwon_version" - implementation "io.noties.markwon:html:$markwon_version" - implementation "io.noties.markwon:image-glide:$markwon_version" + implementation "io.noties.markwon:core:$markwon_version" + implementation "io.noties.markwon:editor:$markwon_version" + implementation "io.noties.markwon:ext-strikethrough:$markwon_version" + implementation "io.noties.markwon:ext-tables:$markwon_version" + implementation "io.noties.markwon:ext-tasklist:$markwon_version" + implementation "io.noties.markwon:html:$markwon_version" + implementation "io.noties.markwon:image-glide:$markwon_version" // Groupie ext.groupie_version = '2.10.1' @@ -158,9 +151,6 @@ 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 'com.github.yausername.youtubedl-android:library:0.15.0' - // Aniyomi implementation 'io.reactivex:rxjava:1.3.8' implementation 'io.reactivex:rxandroid:1.2.1' diff --git a/app/src/google/java/ani/dantotsu/connections/crashlytics/FirebaseCrashlytics.kt b/app/src/google/java/ani/dantotsu/connections/crashlytics/FirebaseCrashlytics.kt index 64ab524f..105bcbf0 100644 --- a/app/src/google/java/ani/dantotsu/connections/crashlytics/FirebaseCrashlytics.kt +++ b/app/src/google/java/ani/dantotsu/connections/crashlytics/FirebaseCrashlytics.kt @@ -5,12 +5,12 @@ import com.google.firebase.FirebaseApp import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase -import com.google.firebase.ktx.app class FirebaseCrashlytics : CrashlyticsInterface { override fun initialize(context: Context) { FirebaseApp.initializeApp(context) } + override fun logException(e: Throwable) { FirebaseCrashlytics.getInstance().recordException(e) } diff --git a/app/src/google/java/ani/dantotsu/others/AppUpdater.kt b/app/src/google/java/ani/dantotsu/others/AppUpdater.kt index 27bd5263..67b7ce43 100644 --- a/app/src/google/java/ani/dantotsu/others/AppUpdater.kt +++ b/app/src/google/java/ani/dantotsu/others/AppUpdater.kt @@ -8,7 +8,6 @@ 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 android.widget.TextView import androidx.core.content.ContextCompat @@ -88,11 +87,12 @@ object AppUpdater { 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() + .parsed().assets?.filter { + it.browserDownloadURL.endsWith( + ".apk" + ) + } + val apkToDownload = apks?.first() apkToDownload?.browserDownloadURL.apply { if (this != null) activity.downloadUpdate(version, this) else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version") @@ -115,16 +115,6 @@ object AppUpdater { } } - /** - * 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(version: String): Boolean { return when (BuildConfig.BUILD_TYPE) { "debug" -> BuildConfig.VERSION_NAME != version diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f752bc76..203d7307 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + @@ -42,8 +44,10 @@ - + + @@ -88,7 +92,7 @@ - + @@ -147,6 +151,9 @@ + @@ -164,7 +171,7 @@ android:name=".widgets.statistics.ProfileStatsConfigure" android:exported="true"> - + @@ -427,6 +434,11 @@ android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService" android:exported="true" android:permission="android.permission.BIND_JOB_SERVICE" /> + = Build.VERSION_CODES.P && statusBarHeight == 0 - && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT + ) { window.decorView.rootWindowInsets?.displayCutout?.apply { if (boundingRects.size > 0) { statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height()) @@ -296,7 +294,12 @@ fun ViewGroup.setBaseline(navBar: AnimatedBottomBar, overlayView: View) { navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) overlayView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) clipToPadding = false - setPadding(paddingLeft, paddingTop, paddingRight, navBarHeight + navBar.measuredHeight + overlayView.measuredHeight) + setPadding( + paddingLeft, + paddingTop, + paddingRight, + navBarHeight + navBar.measuredHeight + overlayView.measuredHeight + ) } fun Activity.reloadActivity() { @@ -306,23 +309,19 @@ fun Activity.reloadActivity() { initActivity(this) } -fun Context.restartApp(view: View) { +fun Activity.restartApp() { val mainIntent = Intent.makeRestartActivityTask( packageManager.getLaunchIntentForPackage(this.packageName)!!.component ) - val component = ComponentName(this@restartApp.packageName, this@restartApp::class.qualifiedName!!) - Snackbar.make(view, R.string.restart_app, Snackbar.LENGTH_INDEFINITE).apply { - setAction(R.string.do_it) { - this.dismiss() - try { - startActivity(Intent().setComponent(component)) - } catch (anything: Exception) { - startActivity(mainIntent) - } - Runtime.getRuntime().exit(0) - } - show() + val component = + ComponentName(this@restartApp.packageName, this@restartApp::class.qualifiedName!!) + try { + startActivity(Intent().setComponent(component)) + } catch (e: Exception) { + startActivity(mainIntent) } + finishAndRemoveTask() + PrefManager.setCustomVal("reload", true) } open class BottomSheetDialogFragment : BottomSheetDialogFragment() { @@ -469,7 +468,7 @@ class InputFilterMinMax( } -class ZoomOutPageTransformer() : +class ZoomOutPageTransformer : ViewPager2.PageTransformer { override fun transformPage(view: View, position: Float) { if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) { @@ -637,6 +636,23 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) { } } +fun ImageView.loadImage(file: FileUrl?, width: Int = 0, height: Int = 0) { + file?.url = PrefManager.getVal(PrefName.ImageUrl).ifEmpty { file?.url ?: "" } + if (file?.url?.isNotEmpty() == true) { + tryWith { + if (file.url.startsWith("content://")) { + Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade()) + .override(width, height).into(this) + } else { + val glideUrl = GlideUrl(file.url) { file.headers } + Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(width, height) + .into(this) + } + } + } +} + + fun ImageView.loadLocalImage(file: File?, size: Int = 0) { if (file?.exists() == true) { tryWith { @@ -958,7 +974,8 @@ fun copyToClipboard(string: String, toast: Boolean = true) { fun countDown(media: Media, view: ViewGroup) { if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null - && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) { + && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong() + ) { val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false) view.addView(v.root, 0) v.mediaCountdownText.text = @@ -1030,7 +1047,7 @@ fun displayTimer(media: Media, view: ViewGroup) { when { media.anime != null -> countDown(media, view) media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view) - else -> { } // No timer yet + else -> {} // No timer yet } } diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt index e7b33c68..4a7bdc5e 100644 --- a/app/src/main/java/ani/dantotsu/MainActivity.kt +++ b/app/src/main/java/ani/dantotsu/MainActivity.kt @@ -3,7 +3,6 @@ package ani.dantotsu import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.AlertDialog -import android.content.Context import android.content.Intent import android.content.res.Configuration import android.graphics.drawable.Animatable @@ -20,7 +19,6 @@ import android.view.ViewGroup import android.view.animation.AnticipateInterpolator import android.widget.TextView import androidx.activity.addCallback -import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.annotation.OptIn import androidx.appcompat.app.AppCompatActivity @@ -35,14 +33,15 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.offline.Download import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.work.OneTimeWorkRequest +import ani.dantotsu.addons.torrent.ServerService +import ani.dantotsu.addons.torrent.TorrentAddonManager import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.AnilistHomeViewModel import ani.dantotsu.databinding.ActivityMainBinding +import ani.dantotsu.databinding.DialogUserAgentBinding import ani.dantotsu.databinding.SplashScreenBinding -import ani.dantotsu.download.video.Helper import ani.dantotsu.home.AnimeFragment import ani.dantotsu.home.HomeFragment import ani.dantotsu.home.LoginFragment @@ -70,11 +69,13 @@ import com.google.android.material.textfield.TextInputEditText import eu.kanade.domain.source.service.SourcePreferences import io.noties.markwon.Markwon import io.noties.markwon.SoftBreakAddsNewLinePlugin +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import nl.joery.animatedbottombar.AnimatedBottomBar +import tachiyomi.core.util.lang.launchIO import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.Serializable @@ -87,6 +88,7 @@ class MainActivity : AppCompatActivity() { private var load = false + @kotlin.OptIn(DelicateCoroutinesApi::class) @SuppressLint("InternalInsetResource", "DiscouragedApi") @OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) { @@ -453,16 +455,26 @@ class MainActivity : AppCompatActivity() { } } } - /*lifecycleScope.launch(Dispatchers.IO) { //simple cleanup - val index = Helper.downloadManager(this@MainActivity).downloadIndex - val downloadCursor = index.getDownloads() - while (downloadCursor.moveToNext()) { - val download = downloadCursor.download - if (download.state == Download.STATE_FAILED) { - Helper.downloadManager(this@MainActivity).removeDownload(download.request.id) + + val torrentManager = Injekt.get() + 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 +485,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) } @@ -482,16 +494,14 @@ class MainActivity : AppCompatActivity() { val password = CharArray(16).apply { fill('0') } // Inflate the dialog layout - val dialogView = - LayoutInflater.from(this).inflate(R.layout.dialog_user_agent, null) - dialogView.findViewById(R.id.userAgentTextBox)?.hint = "Password" - val subtitleTextView = dialogView.findViewById(R.id.subtitle) - subtitleTextView?.visibility = View.VISIBLE - subtitleTextView?.text = getString(R.string.enter_password_to_decrypt_file) + val dialogView = DialogUserAgentBinding.inflate(layoutInflater) + dialogView.userAgentTextBox.hint = "Password" + dialogView.subtitle.visibility = View.VISIBLE + dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file) val dialog = AlertDialog.Builder(this, R.style.MyPopup) .setTitle("Enter Password") - .setView(dialogView) + .setView(dialogView.root) .setPositiveButton("OK", null) .setNegativeButton("Cancel") { dialog, _ -> password.fill('0') 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 00000000..e7b1a1d7 --- /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 00000000..76f077c1 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt @@ -0,0 +1,128 @@ +package ani.dantotsu.addons + +import android.app.Activity +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import ani.dantotsu.Mapper +import ani.dantotsu.R +import ani.dantotsu.client +import ani.dantotsu.logError +import ani.dantotsu.openLinkInBrowser +import ani.dantotsu.others.AppUpdater +import ani.dantotsu.settings.InstallerSteps +import ani.dantotsu.toast +import ani.dantotsu.util.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.decodeFromJsonElement +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()) + .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/AddonInstallReceiver.kt b/app/src/main/java/ani/dantotsu/addons/AddonInstallReceiver.kt new file mode 100644 index 00000000..decc4d30 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/AddonInstallReceiver.kt @@ -0,0 +1,131 @@ +package ani.dantotsu.addons + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat +import ani.dantotsu.addons.download.DownloadAddonManager +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 -> { + 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/AddonListener.kt b/app/src/main/java/ani/dantotsu/addons/AddonListener.kt new file mode 100644 index 00000000..7fdd4786 --- /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 00000000..a6da3011 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt @@ -0,0 +1,143 @@ +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.TorrentAddon +import ani.dantotsu.addons.torrent.TorrentAddonApi +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 00000000..951c2f0c --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/AddonManager.kt @@ -0,0 +1,46 @@ +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 00000000..e59288c8 --- /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/DownloadAddon.kt b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddon.kt new file mode 100644 index 00000000..4409206d --- /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 00000000..c786387e --- /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 00000000..6c704f83 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonManager.kt @@ -0,0 +1,134 @@ +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.AddonInstallReceiver +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 00000000..da8e7cdd --- /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 00000000..ebb369f6 --- /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 00000000..956e5c21 --- /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 00000000..1dc7bfe5 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentAddonManager.kt @@ -0,0 +1,137 @@ +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.AddonInstallReceiver +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 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 00000000..dfba2009 --- /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 00000000..414df069 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt @@ -0,0 +1,168 @@ +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 5a58cea6..0f92966b 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,6 +6,8 @@ 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.addons.torrent.TorrentAddonManager import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.download.DownloadsManager import ani.dantotsu.media.manga.MangaCache @@ -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/connections/anilist/Anilist.kt b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt index 65fbb3cd..9810b8c5 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt @@ -3,7 +3,6 @@ package ani.dantotsu.connections.anilist import android.content.ActivityNotFoundException import android.content.Context import android.net.Uri -import android.util.Log import androidx.browser.customtabs.CustomTabsIntent import ani.dantotsu.R import ani.dantotsu.client @@ -14,7 +13,6 @@ import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.snackString import ani.dantotsu.toast -import ani.dantotsu.tryWithSuspend import ani.dantotsu.util.Logger import java.util.Calendar @@ -201,7 +199,9 @@ object Anilist { toast("Rate limited. Try after $retry seconds") throw Exception("Rate limited after $retry seconds") } - if (!json.text.startsWith("{")) {throw Exception(currContext()?.getString(R.string.anilist_down))} + if (!json.text.startsWith("{")) { + throw Exception(currContext()?.getString(R.string.anilist_down)) + } if (show) Logger.log("Anilist Response: ${json.text}") json.parsed() } else null diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt index d6638cff..b39aeebc 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -1061,21 +1061,32 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: } return null } - private val onListAnime = (if(PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace("\"", "") - private val isAdult = (if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "") - private fun recentAnimeUpdates(page: Int): String{ + + private val onListAnime = + (if (PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace( + "\"", + "" + ) + private val isAdult = + (if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "") + + private fun recentAnimeUpdates(page: Int): String { return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${System.currentTimeMillis() / 1000 - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}""" } - private fun trendingMovies(page: Int): String{ + + private fun trendingMovies(page: Int): String { return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: ANIME, format: MOVIE, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}""" } - private fun topRatedAnime(page: Int): String{ + + private fun topRatedAnime(page: Int): String { return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}""" } - private fun mostFavAnime(page: Int): String{ + + private fun mostFavAnime(page: Int): String { return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}""" } - suspend fun loadAnimeList(): Map>{ + + suspend fun loadAnimeList(): Map> { val list = mutableMapOf>() fun query(): String { return """{ @@ -1136,26 +1147,37 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: } return list } - private val onListManga = (if(PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace("\"", "") - private fun trendingManga(page: Int): String{ + + private val onListManga = + (if (PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace( + "\"", + "" + ) + + private fun trendingManga(page: Int): String { return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA,countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}""" } - private fun trendingManhwa(page: Int): String{ + + private fun trendingManhwa(page: Int): String { return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, countryOfOrigin:KR, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}""" } - private fun trendingNovel(page: Int): String{ + + private fun trendingNovel(page: Int): String { return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, format: NOVEL, countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}""" } - private fun topRatedManga(page: Int): String{ + + private fun topRatedManga(page: Int): String { return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}""" } - private fun mostFavManga(page: Int): String{ + + private fun mostFavManga(page: Int): String { return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}""" } - suspend fun loadMangaList(): Map>{ + + suspend fun loadMangaList(): Map> { val list = mutableMapOf>() - fun query(): String{ - return """{ + fun query(): String { + return """{ trendingManga:${trendingManga(1)} trendingManga2:${trendingManga(2)} trendingManhwa:${trendingManhwa(1)} @@ -1169,7 +1191,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: }""".trimIndent() } - executeQuery(query() , force = true)?.data?.apply { + executeQuery(query(), force = true)?.data?.apply { list["trendingManga"] = trendingManga?.media?.map { Media(it) } as ArrayList list["trendingManhwa"] = trendingManhwa?.media?.map { Media(it) } as ArrayList list["trendingNovel"] = trendingNovel?.media?.map { Media(it) } as ArrayList @@ -1185,10 +1207,11 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: return list } + suspend fun recentlyUpdated( greater: Long = 0, lesser: Long = System.currentTimeMillis() / 1000 - 10000 - ): MutableList? { + ): MutableList { suspend fun execute(page: Int = 1): Page? { val query = """{ Page(page:$page,perPage:50) { @@ -1235,25 +1258,26 @@ Page(page:$page,perPage:50) { }""".replace("\n", " ").replace(""" """, "") return executeQuery(query, force = true)?.data?.page } - var i = 1 - val list = mutableListOf() - var res: Page? = null - suspend fun next() { - res = execute(i) - list.addAll(res?.airingSchedules?.mapNotNull { j -> - j.media?.let { - if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) { - Media(it).apply { relation = "${j.episode},${j.airingAt}" } - } else null - } - } ?: listOf()) - } + + var i = 1 + val list = mutableListOf() + var res: Page? = null + suspend fun next() { + res = execute(i) + list.addAll(res?.airingSchedules?.mapNotNull { j -> + j.media?.let { + if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) { + Media(it).apply { relation = "${j.episode},${j.airingAt}" } + } else null + } + } ?: listOf()) + } + next() + while (res?.pageInfo?.hasNextPage == true) { next() - while (res?.pageInfo?.hasNextPage == true) { - next() - i++ - } - return list.reversed().toMutableList() + i++ + } + return list.reversed().toMutableList() } suspend fun getCharacterDetails(character: Character): Character { @@ -1468,7 +1492,8 @@ Page(page:$page,perPage:50) { val characters = arrayListOf() while (hasNextPage) { page++ - val query = executeQuery(query(page), force = true + val query = executeQuery( + query(page), force = true )?.data?.author hasNextPage = query?.staffMedia?.let { it.edges?.forEach { i -> @@ -1487,7 +1512,16 @@ Page(page:$page,perPage:50) { } ?: false query?.characters?.let { it.nodes?.forEach { i -> - characters.add(Character(i.id, i.name?.userPreferred, i.image?.large, i.image?.medium, "", false)) + characters.add( + Character( + i.id, + i.name?.userPreferred, + i.image?.large, + i.image?.medium, + "", + false + ) + ) } } } @@ -1501,6 +1535,7 @@ Page(page:$page,perPage:50) { author.yearMedia = yearMedia return author } + suspend fun toggleFollow(id: Int): Query.ToggleFollow? { return executeQuery( """mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}""" @@ -1568,7 +1603,11 @@ Page(page:$page,perPage:50) { } - suspend fun getNotifications(id: Int, page: Int = 1, resetNotification: Boolean = true): NotificationResponse? { + suspend fun getNotifications( + id: Int, + page: Int = 1, + resetNotification: Boolean = true + ): NotificationResponse? { val reset = if (resetNotification) "true" else "false" val res = executeQuery( """{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){pageInfo{currentPage,hasNextPage}notifications(resetNotificationCount:$reset){__typename...on AiringNotification{id,type,animeId,episode,contexts,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}},}...on FollowingNotification{id,userId,type,context,createdAt,user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMessageNotification{id,userId,type,activityId,context,createdAt,message{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMentionNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplySubscribedNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentMentionNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentReplyNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentSubscribedNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentLikeNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadLikeNotification{id,userId,type,threadId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on RelatedMediaAdditionNotification{id,type,context,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDataChangeNotification{id,type,mediaId,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaMergeNotification{id,type,mediaId,deletedMediaTitles,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDeletionNotification{id,type,deletedMediaTitle,context,reason,createdAt,}}}}""", @@ -1583,7 +1622,12 @@ Page(page:$page,perPage:50) { return res } - suspend fun getFeed(userId: Int?, global: Boolean = false, page: Int = 1, activityId: Int? = null): FeedResponse? { + suspend fun getFeed( + userId: Int?, + global: Boolean = false, + page: Int = 1, + activityId: Int? = null + ): FeedResponse? { val filter = if (activityId != null) "id:$activityId," else if (userId != null) "userId:$userId," else if (global) "isFollowing:false,hasRepliesOrTypeText:true," @@ -1612,14 +1656,26 @@ Page(page:$page,perPage:50) { .filter { it.timeUntilAiring != null } } - suspend fun isUserFav(favType: AnilistMutations.FavType, id: Int): Boolean { //anilist isFavourite is broken, so we need to check it manually - val res = getUserProfile(Anilist.userid?: return false) + suspend fun isUserFav( + favType: AnilistMutations.FavType, + id: Int + ): Boolean { //anilist isFavourite is broken, so we need to check it manually + val res = getUserProfile(Anilist.userid ?: return false) return when (favType) { - AnilistMutations.FavType.ANIME -> res?.data?.user?.favourites?.anime?.nodes?.any { it.id == id } ?: false - AnilistMutations.FavType.MANGA -> res?.data?.user?.favourites?.manga?.nodes?.any { it.id == id } ?: false - AnilistMutations.FavType.CHARACTER -> res?.data?.user?.favourites?.characters?.nodes?.any { it.id == id } ?: false - AnilistMutations.FavType.STAFF -> res?.data?.user?.favourites?.staff?.nodes?.any { it.id == id } ?: false - AnilistMutations.FavType.STUDIO -> res?.data?.user?.favourites?.studios?.nodes?.any { it.id == id } ?: false + AnilistMutations.FavType.ANIME -> res?.data?.user?.favourites?.anime?.nodes?.any { it.id == id } + ?: false + + AnilistMutations.FavType.MANGA -> res?.data?.user?.favourites?.manga?.nodes?.any { it.id == id } + ?: false + + AnilistMutations.FavType.CHARACTER -> res?.data?.user?.favourites?.characters?.nodes?.any { it.id == id } + ?: false + + AnilistMutations.FavType.STAFF -> res?.data?.user?.favourites?.staff?.nodes?.any { it.id == id } + ?: false + + AnilistMutations.FavType.STUDIO -> res?.data?.user?.favourites?.studios?.nodes?.any { it.id == id } + ?: false } } diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt index 7c05586f..11d5cda1 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt @@ -5,9 +5,6 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.webkit.internal.ApiFeature.M -import androidx.webkit.internal.ApiFeature.P -import androidx.webkit.internal.StartupApiFeature import ani.dantotsu.BuildConfig import ani.dantotsu.R import ani.dantotsu.connections.discord.Discord @@ -190,21 +187,25 @@ class AnilistAnimeViewModel : ViewModel() { var loaded: Boolean = false private val updated: MutableLiveData> = MutableLiveData>(null) + fun getUpdated(): LiveData> = updated private val popularMovies: MutableLiveData> = MutableLiveData>(null) + fun getMovies(): LiveData> = popularMovies private val topRatedAnime: MutableLiveData> = MutableLiveData>(null) + fun getTopRated(): LiveData> = topRatedAnime private val mostFavAnime: MutableLiveData> = MutableLiveData>(null) + fun getMostFav(): LiveData> = mostFavAnime suspend fun loadAll() { - val list= Anilist.query.loadAnimeList() + val list = Anilist.query.loadAnimeList() updated.postValue(list["recentUpdates"]) popularMovies.postValue(list["trendingMovies"]) topRatedAnime.postValue(list["topRated"]) @@ -283,22 +284,27 @@ class AnilistMangaViewModel : ViewModel() { private val popularManga: MutableLiveData> = MutableLiveData>(null) + fun getPopularManga(): LiveData> = popularManga private val popularManhwa: MutableLiveData> = MutableLiveData>(null) + fun getPopularManhwa(): LiveData> = popularManhwa private val popularNovel: MutableLiveData> = MutableLiveData>(null) + fun getPopularNovel(): LiveData> = popularNovel private val topRatedManga: MutableLiveData> = MutableLiveData>(null) + fun getTopRated(): LiveData> = topRatedManga private val mostFavManga: MutableLiveData> = MutableLiveData>(null) + fun getMostFav(): LiveData> = mostFavManga suspend fun loadAll() { val list = Anilist.query.loadMangaList() diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/Login.kt b/app/src/main/java/ani/dantotsu/connections/anilist/Login.kt index 0ec87efb..c9e148a2 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/Login.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/Login.kt @@ -4,7 +4,6 @@ import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import ani.dantotsu.logError -import ani.dantotsu.util.Logger import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.startMainActivity diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt b/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt index 8458cc7e..6c2ca504 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt @@ -11,7 +11,7 @@ data class SearchResults( var onList: Boolean? = null, var perPage: Int? = null, var search: String? = null, - var countryOfOrigin :String? = null, + var countryOfOrigin: String? = null, var sort: String? = null, var genres: MutableList? = null, var excludedGenres: MutableList? = null, diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt index 499d4c30..d45fdc79 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt @@ -149,8 +149,10 @@ class Query { @Serializable data class Data( @SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?, - @SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?) + @SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User? + ) } + @Serializable data class AnimeList( @SerialName("data") @@ -168,6 +170,7 @@ class Query { @SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?, ) } + @Serializable data class MangaList( @SerialName("data") @@ -187,6 +190,7 @@ class Query { @SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?, ) } + @Serializable data class ToggleFollow( @SerialName("data") @@ -317,13 +321,13 @@ class Query { val statistics: NNUserStatisticTypes, @SerialName("siteUrl") val siteUrl: String, - ): java.io.Serializable + ) : java.io.Serializable @Serializable data class NNUserStatisticTypes( @SerialName("anime") var anime: NNUserStatistics, @SerialName("manga") var manga: NNUserStatistics - ): java.io.Serializable + ) : java.io.Serializable @Serializable data class NNUserStatistics( @@ -334,7 +338,7 @@ class Query { @SerialName("episodesWatched") var episodesWatched: Int, @SerialName("chaptersRead") var chaptersRead: Int, @SerialName("volumesRead") var volumesRead: Int, - ): java.io.Serializable + ) : java.io.Serializable @Serializable data class UserFavourites( @@ -348,13 +352,13 @@ class Query { val staff: UserStaffFavouritesCollection, @SerialName("studios") val studios: UserStudioFavouritesCollection, - ): java.io.Serializable + ) : java.io.Serializable @Serializable data class UserMediaFavouritesCollection( @SerialName("nodes") val nodes: List, - ): java.io.Serializable + ) : java.io.Serializable @Serializable data class UserMediaImageFavorite( @@ -362,13 +366,13 @@ class Query { val id: Int, @SerialName("coverImage") val coverImage: MediaCoverImage - ): java.io.Serializable + ) : java.io.Serializable @Serializable data class UserCharacterFavouritesCollection( @SerialName("nodes") val nodes: List, - ): java.io.Serializable + ) : java.io.Serializable @Serializable data class UserCharacterImageFavorite( @@ -380,19 +384,19 @@ class Query { val image: CharacterImage, @SerialName("isFavourite") val isFavourite: Boolean - ): java.io.Serializable + ) : java.io.Serializable @Serializable data class UserStaffFavouritesCollection( @SerialName("nodes") val nodes: List, //downstream it's the same as character - ): java.io.Serializable + ) : java.io.Serializable @Serializable data class UserStudioFavouritesCollection( @SerialName("nodes") val nodes: List, - ): java.io.Serializable + ) : java.io.Serializable @Serializable data class UserStudioFavorite( @@ -400,7 +404,7 @@ class Query { val id: Int, @SerialName("name") val name: String, - ): java.io.Serializable + ) : java.io.Serializable //---------------------------------------- // Statistics @@ -409,12 +413,12 @@ class Query { data class StatisticsResponse( @SerialName("data") val data: Data - ): java.io.Serializable { + ) : java.io.Serializable { @Serializable data class Data( @SerialName("User") val user: StatisticsUser? - ): java.io.Serializable + ) : java.io.Serializable } @Serializable diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Notification.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Notification.kt index b7d1cf0c..a5c59ce1 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Notification.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Notification.kt @@ -21,6 +21,7 @@ enum class NotificationType(val value: String) { MEDIA_DATA_CHANGE("MEDIA_DATA_CHANGE"), MEDIA_MERGE("MEDIA_MERGE"), MEDIA_DELETION("MEDIA_DELETION"), + //custom COMMENT_REPLY("COMMENT_REPLY"), } @@ -84,9 +85,9 @@ data class Notification( @SerialName("createdAt") val createdAt: Int, @SerialName("media") - val media: ani.dantotsu.connections.anilist.api.Media? = null, + val media: Media? = null, @SerialName("user") - val user: ani.dantotsu.connections.anilist.api.User? = null, + val user: User? = null, @SerialName("message") val message: MessageActivity? = null, @SerialName("activity") diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Staff.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Staff.kt index 7b56f693..7c9cafe1 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Staff.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Staff.kt @@ -93,6 +93,7 @@ data class StaffConnection( // The pagination information // @SerialName("pageInfo") var pageInfo: PageInfo?, ) + @Serializable data class StaffImage( // The character's image of media at its largest size @@ -101,6 +102,7 @@ data class StaffImage( // The character's image of media at medium size @SerialName("medium") var medium: String?, ) : java.io.Serializable + @Serializable data class StaffEdge( var role: String?, diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt index b1ec8862..f6668fc4 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt @@ -111,7 +111,7 @@ data class UserAvatar( // The avatar of user at medium size @SerialName("medium") var medium: String?, -): java.io.Serializable +) : java.io.Serializable @Serializable data class UserStatisticTypes( diff --git a/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt b/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt index 426abc51..ec66cb7c 100644 --- a/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt +++ b/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt @@ -24,7 +24,7 @@ class MangaUpdates { private val apiUrl = "https://api.mangaupdates.com/v1/releases/search" - suspend fun search(title: String, startDate: FuzzyDate?) : MangaUpdatesResponse.Results? { + suspend fun search(title: String, startDate: FuzzyDate?): MangaUpdatesResponse.Results? { return tryWithSuspend { val query = JSONObject().apply { try { @@ -96,6 +96,7 @@ class MangaUpdates { @SerialName("release_date") val releaseDate: String ) + @Serializable data class MetaData( val series: Series diff --git a/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt b/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt index 9c6d473c..ea62d728 100644 --- a/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt +++ b/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt @@ -32,7 +32,12 @@ object CommentsAPI { var isMod: Boolean = false var totalVotes: Int = 0 - suspend fun getCommentsForId(id: Int, page: Int = 1, tag: Int?, sort: String?): CommentResponse? { + suspend fun getCommentsForId( + id: Int, + page: Int = 1, + tag: Int?, + sort: String? + ): CommentResponse? { var url = "$ADDRESS/comments/$id/$page" val request = requestBuilder() tag?.let { @@ -399,7 +404,7 @@ object CommentsAPI { null } val message = parsed?.message ?: reason ?: error - val fullMessage = if(code == 500) message else "$code: $message" + val fullMessage = if (code == 500) message else "$code: $message" toast(fullMessage) } diff --git a/app/src/main/java/ani/dantotsu/connections/crashlytics/CrashlyticsStub.kt b/app/src/main/java/ani/dantotsu/connections/crashlytics/CrashlyticsStub.kt index 6c4e988a..76c072e1 100644 --- a/app/src/main/java/ani/dantotsu/connections/crashlytics/CrashlyticsStub.kt +++ b/app/src/main/java/ani/dantotsu/connections/crashlytics/CrashlyticsStub.kt @@ -7,6 +7,7 @@ class CrashlyticsStub : CrashlyticsInterface { override fun initialize(context: Context) { //no-op } + override fun logException(e: Throwable) { Logger.log(e) } diff --git a/app/src/main/java/ani/dantotsu/connections/discord/DiscordService.kt b/app/src/main/java/ani/dantotsu/connections/discord/DiscordService.kt index b8c52766..9b6d19a1 100644 --- a/app/src/main/java/ani/dantotsu/connections/discord/DiscordService.kt +++ b/app/src/main/java/ani/dantotsu/connections/discord/DiscordService.kt @@ -63,7 +63,7 @@ class DiscordService : Service() { PowerManager.PARTIAL_WAKE_LOCK, "discordRPC:backgroundPresence" ) - wakeLock.acquire(30*60*1000L /*30 minutes*/) + wakeLock.acquire(30 * 60 * 1000L /*30 minutes*/) log("WakeLock Acquired") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val serviceChannel = NotificationChannel( @@ -402,7 +402,8 @@ class DiscordService : Service() { Thread.sleep(heartbeat.toLong()) heartbeatSend(webSocket, sequence) log("WebSocket: Heartbeat Sent") - } catch (ignored: InterruptedException) { } + } catch (ignored: InterruptedException) { + } } } diff --git a/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt b/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt index ee2d3b6a..f46dd63d 100644 --- a/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt +++ b/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt @@ -71,7 +71,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) { assets = Activity.Assets( largeImage = data.largeImage?.url?.discordUrl(), largeText = data.largeImage?.label, - smallImage = if (PrefManager.getVal(PrefName.ShowAniListIcon)) Discord.small_Image_AniList.discordUrl() else Discord.small_Image.discordUrl(), + smallImage = if (PrefManager.getVal(PrefName.ShowAniListIcon)) Discord.small_Image_AniList.discordUrl() else Discord.small_Image.discordUrl(), smallText = if (PrefManager.getVal(PrefName.ShowAniListIcon)) "Anilist" else "Dantotsu", ), buttons = data.buttons.map { it.label }, diff --git a/app/src/main/java/ani/dantotsu/connections/github/Forks.kt b/app/src/main/java/ani/dantotsu/connections/github/Forks.kt index d620742e..d9ae6424 100644 --- a/app/src/main/java/ani/dantotsu/connections/github/Forks.kt +++ b/app/src/main/java/ani/dantotsu/connections/github/Forks.kt @@ -1,9 +1,7 @@ package ani.dantotsu.connections.github import ani.dantotsu.Mapper -import ani.dantotsu.R import ani.dantotsu.client -import ani.dantotsu.getAppString import ani.dantotsu.settings.Developer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -17,10 +15,11 @@ class Forks { fun getForks(): Array { var forks = arrayOf() runBlocking(Dispatchers.IO) { - val res = client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks?sort=stargazers") - .parsed().map { - Mapper.json.decodeFromJsonElement(it) - } + val res = + client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks?sort=stargazers") + .parsed().map { + Mapper.json.decodeFromJsonElement(it) + } res.forEach { forks = forks.plus( Developer( diff --git a/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt b/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt new file mode 100644 index 00000000..a003bb64 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt @@ -0,0 +1,381 @@ +package ani.dantotsu.download + +import android.content.Context +import android.net.Uri +import android.os.Environment +import android.widget.Toast +import ani.dantotsu.R +import ani.dantotsu.connections.crashlytics.CrashlyticsInterface +import ani.dantotsu.currActivity +import ani.dantotsu.currContext +import ani.dantotsu.download.anime.OfflineAnimeModel +import ani.dantotsu.download.manga.OfflineMangaModel +import ani.dantotsu.media.Media +import ani.dantotsu.media.MediaNameAdapter +import ani.dantotsu.media.MediaType +import ani.dantotsu.parsers.Episode +import ani.dantotsu.parsers.MangaChapter +import ani.dantotsu.parsers.MangaImage +import ani.dantotsu.parsers.Subtitle +import ani.dantotsu.parsers.SubtitleType +import ani.dantotsu.util.Logger +import com.google.gson.GsonBuilder +import com.google.gson.InstanceCreator +import eu.kanade.tachiyomi.animesource.model.SAnime +import eu.kanade.tachiyomi.animesource.model.SAnimeImpl +import eu.kanade.tachiyomi.animesource.model.SEpisode +import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SChapterImpl +import eu.kanade.tachiyomi.source.model.SManga +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.File +import java.util.Locale + +@Deprecated("external storage is deprecated, use SAF instead") +class DownloadCompat { + companion object { + @Deprecated("external storage is deprecated, use SAF instead") + fun loadMediaCompat(downloadedType: DownloadedType): Media? { + val type = when (downloadedType.type) { + MediaType.MANGA -> "Manga" + MediaType.ANIME -> "Anime" + else -> "Novel" + } + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/$type/${downloadedType.titleName}" + ) + //load media.json and convert to media class with gson + return try { + val gson = GsonBuilder() + .registerTypeAdapter(SChapter::class.java, InstanceCreator { + SChapterImpl() // Provide an instance of SChapterImpl + }) + .registerTypeAdapter(SAnime::class.java, InstanceCreator { + SAnimeImpl() // Provide an instance of SAnimeImpl + }) + .registerTypeAdapter(SEpisode::class.java, InstanceCreator { + SEpisodeImpl() // Provide an instance of SEpisodeImpl + }) + .create() + val media = File(directory, "media.json") + val mediaJson = media.readText() + gson.fromJson(mediaJson, Media::class.java) + } catch (e: Exception) { + Logger.log("Error loading media.json: ${e.message}") + Logger.log(e) + Injekt.get().logException(e) + null + } + } + + @Deprecated("external storage is deprecated, use SAF instead") + fun loadOfflineAnimeModelCompat(downloadedType: DownloadedType): OfflineAnimeModel { + val type = when (downloadedType.type) { + MediaType.MANGA -> "Manga" + MediaType.ANIME -> "Anime" + else -> "Novel" + } + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/$type/${downloadedType.titleName}" + ) + //load media.json and convert to media class with gson + try { + val mediaModel = loadMediaCompat(downloadedType)!! + val cover = File(directory, "cover.jpg") + val coverUri: Uri? = if (cover.exists()) { + Uri.fromFile(cover) + } else null + val banner = File(directory, "banner.jpg") + val bannerUri: Uri? = if (banner.exists()) { + Uri.fromFile(banner) + } else null + val title = mediaModel.mainName() + val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore + ?: 0) else mediaModel.userScore) / 10.0).toString() + val isOngoing = + mediaModel.status == currActivity()!!.getString(R.string.status_releasing) + val isUserScored = mediaModel.userScore != 0 + val watchedEpisodes = (mediaModel.userProgress ?: "~").toString() + val totalEpisode = + if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString() + " | " + (mediaModel.anime.totalEpisodes + ?: "~").toString()) else (mediaModel.anime?.totalEpisodes ?: "~").toString() + val chapters = " Chapters" + val totalEpisodesList = + if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString()) else (mediaModel.anime?.totalEpisodes + ?: "~").toString() + return OfflineAnimeModel( + title, + score, + totalEpisode, + totalEpisodesList, + watchedEpisodes, + type, + chapters, + isOngoing, + isUserScored, + coverUri, + bannerUri + ) + } catch (e: Exception) { + Logger.log("Error loading media.json: ${e.message}") + Logger.log(e) + Injekt.get().logException(e) + return OfflineAnimeModel( + "unknown", + "0", + "??", + "??", + "??", + "movie", + "hmm", + isOngoing = false, + isUserScored = false, + null, + null + ) + } + } + + @Deprecated("external storage is deprecated, use SAF instead") + fun loadOfflineMangaModelCompat(downloadedType: DownloadedType): OfflineMangaModel { + val type = when (downloadedType.type) { + MediaType.MANGA -> "Manga" + MediaType.ANIME -> "Anime" + else -> "Novel" + } + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/$type/${downloadedType.titleName}" + ) + //load media.json and convert to media class with gson + try { + val mediaModel = loadMediaCompat(downloadedType)!! + val cover = File(directory, "cover.jpg") + val coverUri: Uri? = if (cover.exists()) { + Uri.fromFile(cover) + } else null + val banner = File(directory, "banner.jpg") + val bannerUri: Uri? = if (banner.exists()) { + Uri.fromFile(banner) + } else null + val title = mediaModel.mainName() + val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore + ?: 0) else mediaModel.userScore) / 10.0).toString() + val isOngoing = + mediaModel.status == currActivity()!!.getString(R.string.status_releasing) + val isUserScored = mediaModel.userScore != 0 + val readchapter = (mediaModel.userProgress ?: "~").toString() + val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}" + val chapters = " Chapters" + return OfflineMangaModel( + title, + score, + totalchapter, + readchapter, + type, + chapters, + isOngoing, + isUserScored, + coverUri, + bannerUri + ) + } catch (e: Exception) { + Logger.log("Error loading media.json: ${e.message}") + Logger.log(e) + Injekt.get().logException(e) + return OfflineMangaModel( + "unknown", + "0", + "??", + "??", + "movie", + "hmm", + isOngoing = false, + isUserScored = false, + null, + null + ) + } + } + + @Deprecated("external storage is deprecated, use SAF instead") + suspend fun loadEpisodesCompat( + animeLink: String, + extra: Map?, + sAnime: SAnime + ): List { + + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "${animeLocation}/$animeLink" + ) + //get all of the folder names and add them to the list + val episodes = mutableListOf() + if (directory.exists()) { + directory.listFiles()?.forEach { + //put the title and episdode number in the extra data + val extraData = mutableMapOf() + extraData["title"] = animeLink + extraData["episode"] = it.name + if (it.isDirectory) { + val episode = Episode( + it.name, + "$animeLink - ${it.name}", + it.name, + null, + null, + extra = extraData, + sEpisode = SEpisodeImpl() + ) + episodes.add(episode) + } + } + episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) } + return episodes + } + return emptyList() + } + + @Deprecated("external storage is deprecated, use SAF instead") + suspend fun loadChaptersCompat( + mangaLink: String, + extra: Map?, + sManga: SManga + ): List { + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Manga/$mangaLink" + ) + //get all of the folder names and add them to the list + val chapters = mutableListOf() + if (directory.exists()) { + directory.listFiles()?.forEach { + if (it.isDirectory) { + val chapter = MangaChapter( + it.name, + "$mangaLink/${it.name}", + it.name, + null, + null, + SChapter.create() + ) + chapters.add(chapter) + } + } + chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) } + return chapters + } + return emptyList() + } + + @Deprecated("external storage is deprecated, use SAF instead") + suspend fun loadImagesCompat(chapterLink: String, sChapter: SChapter): List { + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Manga/$chapterLink" + ) + val images = mutableListOf() + val imageNumberRegex = Regex("""(\d+)\.jpg$""") + if (directory.exists()) { + directory.listFiles()?.forEach { + if (it.isFile) { + val image = MangaImage(it.absolutePath, false, null) + images.add(image) + } + } + images.sortBy { image -> + val matchResult = imageNumberRegex.find(image.url.url) + matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE + } + for (image in images) { + Logger.log("imageNumber: ${image.url.url}") + } + return images + } + return emptyList() + } + + @Deprecated("external storage is deprecated, use SAF instead") + fun loadSubtitleCompat(title: String, episode: String): List? { + currContext()?.let { + File( + it.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "$animeLocation/$title/$episode" + ).listFiles()?.forEach { + if (it.name.contains("subtitle")) { + return listOf( + Subtitle( + "Downloaded Subtitle", + Uri.fromFile(it).toString(), + determineSubtitletype(it.absolutePath) + ) + ) + } + } + } + return null + } + + private fun determineSubtitletype(url: String): SubtitleType { + return when { + url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS + url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT + else -> SubtitleType.SRT + } + } + + @Deprecated("external storage is deprecated, use SAF instead") + fun removeMediaCompat(context: Context, title: String, type: MediaType) { + val subDirectory = if (type == MediaType.MANGA) { + "Manga" + } else if (type == MediaType.ANIME) { + "Anime" + } else { + "Novel" + } + val directory = File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/$subDirectory/$title" + ) + if (directory.exists()) { + directory.deleteRecursively() + } + } + + @Deprecated("external storage is deprecated, use SAF instead") + fun removeDownloadCompat(context: Context, downloadedType: DownloadedType) { + val directory = if (downloadedType.type == MediaType.MANGA) { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Manga/${downloadedType.titleName}/${downloadedType.chapterName}" + ) + } else if (downloadedType.type == MediaType.ANIME) { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Anime/${downloadedType.titleName}/${downloadedType.chapterName}" + ) + } else { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Novel/${downloadedType.titleName}/${downloadedType.chapterName}" + ) + } + + // Check if the directory exists and delete it recursively + if (directory.exists()) { + val deleted = directory.deleteRecursively() + if (deleted) { + Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show() + } + } + } + + private val animeLocation = "Dantotsu/Anime" + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt index 75c6626c..b97c3b34 100644 --- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt +++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt @@ -3,6 +3,8 @@ package ani.dantotsu.download import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile +import ani.dantotsu.download.DownloadCompat.Companion.removeDownloadCompat +import ani.dantotsu.download.DownloadCompat.Companion.removeMediaCompat import ani.dantotsu.media.Media import ani.dantotsu.media.MediaType import ani.dantotsu.settings.saving.PrefManager @@ -53,7 +55,12 @@ class DownloadsManager(private val context: Context) { saveDownloads() } - fun removeDownload(downloadedType: DownloadedType, toast: Boolean = true, onFinished: () -> Unit) { + fun removeDownload( + downloadedType: DownloadedType, + toast: Boolean = true, + onFinished: () -> Unit + ) { + removeDownloadCompat(context, downloadedType) downloadsList.remove(downloadedType) CoroutineScope(Dispatchers.IO).launch { removeDirectory(downloadedType, toast) @@ -65,6 +72,7 @@ class DownloadsManager(private val context: Context) { } fun removeMedia(title: String, type: MediaType) { + removeMediaCompat(context, title, type) val baseDirectory = getBaseDirectory(context, type) val directory = baseDirectory?.findFolder(title) if (directory?.exists() == true) { @@ -80,15 +88,15 @@ class DownloadsManager(private val context: Context) { } when (type) { MediaType.MANGA -> { - downloadsList.removeAll { it.title == title && it.type == MediaType.MANGA } + downloadsList.removeAll { it.titleName == title && it.type == MediaType.MANGA } } MediaType.ANIME -> { - downloadsList.removeAll { it.title == title && it.type == MediaType.ANIME } + downloadsList.removeAll { it.titleName == title && it.type == MediaType.ANIME } } MediaType.NOVEL -> { - downloadsList.removeAll { it.title == title && it.type == MediaType.NOVEL } + downloadsList.removeAll { it.titleName == title && it.type == MediaType.NOVEL } } } saveDownloads() @@ -111,7 +119,7 @@ class DownloadsManager(private val context: Context) { if (directory?.exists() == true && directory.isDirectory) { val files = directory.listFiles() for (file in files) { - if (!downloadsSubLists.any { it.title == file.name }) { + if (!downloadsSubLists.any { it.titleName == file.name }) { file.deleteRecursively(context, false) } } @@ -120,8 +128,8 @@ class DownloadsManager(private val context: Context) { val iterator = downloadsList.iterator() while (iterator.hasNext()) { val download = iterator.next() - val downloadDir = directory?.findFolder(download.title) - if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) { + val downloadDir = directory?.findFolder(download.titleName) + if ((downloadDir?.exists() == false && download.type == type) || download.titleName.isBlank()) { iterator.remove() } } @@ -207,16 +215,17 @@ class DownloadsManager(private val context: Context) { fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean { return if (type == null) { - downloadsList.any { it.title == title && it.chapter == chapter } + downloadsList.any { it.titleName == title && it.chapterName == chapter } } else { - downloadsList.any { it.title == title && it.chapter == chapter && it.type == type } + downloadsList.any { it.titleName == title && it.chapterName == chapter && it.type == type } } } private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) { val baseDirectory = getBaseDirectory(context, downloadedType.type) val directory = - baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter) + baseDirectory?.findFolder(downloadedType.titleName) + ?.findFolder(downloadedType.chapterName) downloadsList.remove(downloadedType) // Check if the directory exists and delete it recursively if (directory?.exists() == true) { @@ -360,15 +369,21 @@ class DownloadsManager(private val context: Context) { } private const val RESERVED_CHARS = "|\\?*<\":>+[]/'" -private fun String?.findValidName(): String { +fun String?.findValidName(): String { return this?.filterNot { RESERVED_CHARS.contains(it) } ?: "" } data class DownloadedType( - val pTitle: String, val pChapter: String, val type: MediaType + private val pTitle: String?, + private val pChapter: String?, + val type: MediaType, + @Deprecated("use pTitle instead") + private val title: String? = null, + @Deprecated("use pChapter instead") + private val chapter: String? = null ) : Serializable { - val title: String - get() = pTitle.findValidName() - val chapter: String - get() = pChapter.findValidName() + val titleName: String + get() = title?:pTitle.findValidName() + val chapterName: String + get() = chapter?:pChapter.findValidName() } 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 38801e4d..dd8c326b 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 ) @@ -241,50 +244,32 @@ class AnimeDownloaderService : Service() { headersStringBuilder.append("\"").append("User-Agent: ") .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() - } - }) + val probeRequest = + "-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\"" + 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 +285,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 +298,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 +333,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/anime/OfflineAnimeFragment.kt b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt index 6bffde6e..ea63bcd8 100644 --- a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt @@ -31,9 +31,12 @@ import ani.dantotsu.bottomBar import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.currActivity import ani.dantotsu.currContext +import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat +import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.compareName +import ani.dantotsu.download.findValidName import ani.dantotsu.initActivity import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity @@ -175,7 +178,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { // Get the OfflineAnimeModel that was clicked val item = adapter.getItem(position) as OfflineAnimeModel val media = - downloadManager.animeDownloadedTypes.firstOrNull { it.title.compareName(item.title) } + downloadManager.animeDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) } media?.let { lifecycleScope.launch { val mediaModel = getMedia(it) @@ -287,10 +290,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { } downloadsJob = Job() CoroutineScope(Dispatchers.IO + downloadsJob).launch { - val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct() + val animeTitles = downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct() val newAnimeDownloads = mutableListOf() for (title in animeTitles) { - val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title } + val tDownloads = downloadManager.animeDownloadedTypes.filter { it.titleName == title } val download = tDownloads.first() val offlineAnimeModel = loadOfflineAnimeModel(download) newAnimeDownloads += offlineAnimeModel @@ -313,7 +316,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { return try { val directory = DownloadsManager.getSubDirectory( context ?: currContext()!!, downloadedType.type, - false, downloadedType.title + false, downloadedType.titleName ) val gson = GsonBuilder() .registerTypeAdapter(SChapter::class.java, InstanceCreator { @@ -327,7 +330,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { }) .create() val media = directory?.findFile("media.json") - ?: return null + ?: return loadMediaCompat(downloadedType) val mediaJson = media.openInputStream(context ?: currContext()!!)?.bufferedReader().use { it?.readText() @@ -352,7 +355,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { try { val directory = DownloadsManager.getSubDirectory( context ?: currContext()!!, downloadedType.type, - false, downloadedType.title + false, downloadedType.titleName ) val mediaModel = getMedia(downloadedType)!! val cover = directory?.findFile("cover.jpg") @@ -363,6 +366,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { val bannerUri: Uri? = if (banner?.exists() == true) { banner.uri } else null + if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat") val title = mediaModel.mainName() val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore ?: 0) else mediaModel.userScore) / 10.0).toString() @@ -391,22 +395,26 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { bannerUri ) } catch (e: Exception) { - Logger.log("Error loading media.json: ${e.message}") - Logger.log(e) - Injekt.get().logException(e) - return OfflineAnimeModel( - "unknown", - "0", - "??", - "??", - "??", - "movie", - "hmm", - isOngoing = false, - isUserScored = false, - null, - null - ) + return try { + loadOfflineAnimeModelCompat(downloadedType) + } catch (e: Exception) { + Logger.log("Error loading media.json: ${e.message}") + Logger.log(e) + Injekt.get().logException(e) + OfflineAnimeModel( + "unknown", + "0", + "??", + "??", + "??", + "movie", + "hmm", + isOngoing = false, + isUserScored = false, + null, + null + ) + } } } } diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt index de93087b..3ee64ee4 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt @@ -28,10 +28,13 @@ import ani.dantotsu.bottomBar import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.currActivity import ani.dantotsu.currContext +import ani.dantotsu.download.DownloadCompat +import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineMangaModelCompat import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.compareName import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory +import ani.dantotsu.download.findValidName import ani.dantotsu.initActivity import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity @@ -169,8 +172,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { // Get the OfflineMangaModel that was clicked val item = adapter.getItem(position) as OfflineMangaModel val media = - downloadManager.mangaDownloadedTypes.firstOrNull { it.title.compareName(item.title) } - ?: downloadManager.novelDownloadedTypes.firstOrNull { it.title.compareName(item.title) } + downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) } + ?: downloadManager.novelDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) } media?.let { lifecycleScope.launch { ContextCompat.startActivity( @@ -190,7 +193,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { // Get the OfflineMangaModel that was clicked val item = adapter.getItem(position) as OfflineMangaModel val type: MediaType = - if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) { + if (downloadManager.mangaDownloadedTypes.any { it.titleName == item.title }) { MediaType.MANGA } else { MediaType.NOVEL @@ -278,19 +281,19 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { downloads = listOf() downloadsJob = Job() CoroutineScope(Dispatchers.IO + downloadsJob).launch { - val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct() + val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct() val newMangaDownloads = mutableListOf() for (title in mangaTitles) { - val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title } + val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName == title } val download = tDownloads.first() val offlineMangaModel = loadOfflineMangaModel(download) newMangaDownloads += offlineMangaModel } downloads = newMangaDownloads - val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct() + val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct() val newNovelDownloads = mutableListOf() for (title in novelTitles) { - val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title } + val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName == title } val download = tDownloads.first() val offlineMangaModel = loadOfflineMangaModel(download) newNovelDownloads += offlineMangaModel @@ -315,7 +318,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { return try { val directory = getSubDirectory( context ?: currContext()!!, downloadedType.type, - false, downloadedType.title + false, downloadedType.titleName ) val gson = GsonBuilder() .registerTypeAdapter(SChapter::class.java, InstanceCreator { @@ -323,7 +326,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { }) .create() val media = directory?.findFile("media.json") - ?: return null + ?: return DownloadCompat.loadMediaCompat(downloadedType) val mediaJson = media.openInputStream(context ?: currContext()!!)?.bufferedReader().use { it?.readText() @@ -343,7 +346,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { try { val directory = getSubDirectory( context ?: currContext()!!, downloadedType.type, - false, downloadedType.title + false, downloadedType.titleName ) val mediaModel = getMedia(downloadedType)!! val cover = directory?.findFile("cover.jpg") @@ -354,6 +357,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { val bannerUri: Uri? = if (banner?.exists() == true) { banner.uri } else null + if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat") val title = mediaModel.mainName() val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore ?: 0) else mediaModel.userScore) / 10.0).toString() @@ -376,21 +380,25 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { bannerUri ) } catch (e: Exception) { - Logger.log("Error loading media.json: ${e.message}") - Logger.log(e) - Injekt.get().logException(e) - return OfflineMangaModel( - "unknown", - "0", - "??", - "??", - "movie", - "hmm", - isOngoing = false, - isUserScored = false, - null, - null - ) + return try { + loadOfflineMangaModelCompat(downloadedType) + } catch (e: Exception) { + Logger.log("Error loading media.json: ${e.message}") + Logger.log(e) + Injekt.get().logException(e) + return OfflineMangaModel( + "unknown", + "0", + "??", + "??", + "movie", + "hmm", + isOngoing = false, + isUserScored = false, + null, + null + ) + } } } } 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 123a54c1..bc1d8e31 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 @@ -270,7 +267,9 @@ class NovelDownloaderService : Service() { task.coverUrl?.let { file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") } } - val outputStream = this@NovelDownloaderService.contentResolver.openOutputStream(file.uri) ?: throw Exception("Could not open OutputStream") + val outputStream = + this@NovelDownloaderService.contentResolver.openOutputStream(file.uri) + ?: throw Exception("Could not open OutputStream") val sink = outputStream.sink().buffer() val responseBody = response.body @@ -358,7 +357,7 @@ class NovelDownloaderService : Service() { private fun saveMediaInfo(task: DownloadTask) { launchIO { val directory = - DownloadsManager.getSubDirectory( + getSubDirectory( this@NovelDownloaderService, MediaType.NOVEL, false, 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 ed146b90..463cfa91 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -7,14 +7,10 @@ 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 @@ -22,11 +18,8 @@ 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 @@ -34,21 +27,16 @@ 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") @@ -130,4 +118,92 @@ object Helper { } return true } + + @Synchronized + @UnstableApi + @Deprecated("exoplayer download manager is no longer used") + fun downloadManager(context: Context): DownloadManager { + return download ?: let { + val database = Injekt.get() + val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) + val dataSourceFactory = DataSource.Factory { + //val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() + val networkHelper = Injekt.get() + val okHttpClient = networkHelper.client + val dataSource: HttpDataSource = + OkHttpDataSource.Factory(okHttpClient).createDataSource() + defaultHeaders.forEach { + dataSource.setRequestProperty(it.key, it.value) + } + dataSource + } + val threadPoolSize = Runtime.getRuntime().availableProcessors() + val executorService = Executors.newFixedThreadPool(threadPoolSize) + val downloadManager = DownloadManager( + context, + database, + getSimpleCache(context), + dataSourceFactory, + executorService + ).apply { + requirements = + Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW) + maxParallelDownloads = 3 + } + downloadManager.addListener( //for testing + object : DownloadManager.Listener { + override fun onDownloadChanged( + downloadManager: DownloadManager, + download: Download, + finalException: Exception? + ) { + if (download.state == Download.STATE_COMPLETED) { + Logger.log("Download Completed") + } else if (download.state == Download.STATE_FAILED) { + Logger.log("Download Failed") + } else if (download.state == Download.STATE_STOPPED) { + Logger.log("Download Stopped") + } else if (download.state == Download.STATE_QUEUED) { + Logger.log("Download Queued") + } else if (download.state == Download.STATE_DOWNLOADING) { + Logger.log("Download Downloading") + } + } + } + ) + + downloadManager + } + } + @Deprecated("exoplayer download manager is no longer used") + @OptIn(UnstableApi::class) + fun getSimpleCache(context: Context): SimpleCache { + return if (simpleCache == null) { + val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) + val database = Injekt.get() + simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database) + simpleCache!! + } else { + simpleCache!! + } + } + @Synchronized + @Deprecated("exoplayer download manager is no longer used") + private fun getDownloadDirectory(context: Context): File { + if (downloadDirectory == null) { + downloadDirectory = context.getExternalFilesDir(null) + if (downloadDirectory == null) { + downloadDirectory = context.filesDir + } + } + return downloadDirectory!! + } + @Deprecated("exoplayer download manager is no longer used") + private var download: DownloadManager? = null + @Deprecated("exoplayer download manager is no longer used") + private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads" + @Deprecated("exoplayer download manager is no longer used") + private var simpleCache: SimpleCache? = null + @Deprecated("exoplayer download manager is no longer used") + private var downloadDirectory: File? = null } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt index 9861113a..862c08c3 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt @@ -101,7 +101,7 @@ class AnimePageAdapter : RecyclerView.Adapter 0) View.VISIBLE else View.GONE + trendingBinding.notificationCount.visibility = + if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() listOf( @@ -167,7 +168,8 @@ class AnimePageAdapter : RecyclerView.Adapter 0 binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString() @@ -128,7 +131,7 @@ class HomeFragment : Fragment() { it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) ContextCompat.startActivity( requireContext(), Intent(requireContext(), ProfileActivity::class.java) - .putExtra("userId", Anilist.userid),null + .putExtra("userId", Anilist.userid), null ) false } @@ -376,6 +379,7 @@ class HomeFragment : Fragment() { } } } + override fun onResume() { if (!model.loaded) Refresh.activity[1]!!.postValue(true) if (_binding != null) { diff --git a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt index 17c4dec8..e2801817 100644 --- a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt @@ -172,7 +172,13 @@ class MangaFragment : Fragment() { } model.getPopularManhwa().observe(viewLifecycleOwner) { if (it != null) { - mangaPageAdapter.updateTrendingManhwa(MediaAdaptor(0, it, requireActivity())) + mangaPageAdapter.updateTrendingManhwa( + MediaAdaptor( + 0, + it, + requireActivity() + ) + ) } } model.getTopRated().observe(viewLifecycleOwner) { diff --git a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt index 27f4c8ea..4fe604d6 100644 --- a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt @@ -101,7 +101,7 @@ class MangaPageAdapter : RecyclerView.Adapter currActivity()!!.getString(R.string.male) - currActivity()!!.getString(R.string.female) -> currActivity()!!.getString(R.string.female) - else -> character.gender - } else "") + "\n" + character.description + currActivity()!!.getString(R.string.male) -> currActivity()!!.getString( + R.string.male + ) + + currActivity()!!.getString(R.string.female) -> currActivity()!!.getString( + R.string.female + ) + + else -> character.gender + } else "") + "\n" + character.description binding.characterDesc.isTextSelectable val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()) diff --git a/app/src/main/java/ani/dantotsu/media/Media.kt b/app/src/main/java/ani/dantotsu/media/Media.kt index 7f2150c9..92847073 100644 --- a/app/src/main/java/ani/dantotsu/media/Media.kt +++ b/app/src/main/java/ani/dantotsu/media/Media.kt @@ -100,7 +100,7 @@ data class Media( startDate = apiMedia.startDate, endDate = apiMedia.endDate, favourites = apiMedia.favourites, - timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000}, + timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000 }, anime = if (apiMedia.type == MediaType.ANIME) Anime( totalEpisodes = apiMedia.episodes, nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1) @@ -115,7 +115,8 @@ data class Media( this.userScore = mediaList.score?.toInt() ?: 0 this.userStatus = mediaList.status?.toString() this.userUpdatedAt = mediaList.updatedAt?.toLong() - this.genres = mediaList.media?.genres?.toMutableList() as? ArrayList? ?: arrayListOf() + this.genres = + mediaList.media?.genres?.toMutableList() as? ArrayList? ?: arrayListOf() } constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) { diff --git a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt index 8fa4e1fa..d31473c6 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt @@ -149,14 +149,22 @@ class MediaAdaptor( (if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score) ) if (media.anime != null) { - val itemTotal = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(R.string.episode_singular) + val itemTotal = " " + if ((media.anime.totalEpisodes + ?: 0) != 1 + ) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString( + R.string.episode_singular + ) b.itemTotal.text = itemTotal b.itemCompactTotal.text = if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes ?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString() } else if (media.manga != null) { - val itemTotal = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(R.string.chapter_singular) + val itemTotal = " " + if ((media.manga.totalChapters + ?: 0) != 1 + ) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString( + R.string.chapter_singular + ) b.itemTotal.text = itemTotal b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}" } @@ -183,7 +191,10 @@ class MediaAdaptor( AccelerateDecelerateInterpolator() ) ) - blurImage(if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen , media.banner ?: media.cover) + blurImage( + if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen, + media.banner ?: media.cover + ) b.itemCompactOngoing.isVisible = media.status == currActivity()!!.getString(R.string.status_releasing) b.itemCompactTitle.text = media.userPreferredName @@ -232,7 +243,10 @@ class MediaAdaptor( AccelerateDecelerateInterpolator() ) ) - blurImage(if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen , media.banner ?: media.cover) + blurImage( + if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen, + media.banner ?: media.cover + ) b.itemCompactOngoing.isVisible = media.status == currActivity()!!.getString(R.string.status_releasing) b.itemCompactTitle.text = media.userPreferredName diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt index 4f93cdde..a490721d 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt @@ -109,7 +109,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi // Ui init initActivity(this) - binding.mediaViewPager.updateLayoutParams { bottomMargin = navBarHeight } + binding.mediaViewPager.updateLayoutParams { + bottomMargin = navBarHeight + } val oldMargin = binding.mediaViewPager.marginBottom AndroidBug5497Workaround.assistActivity(this) { if (it) { @@ -125,9 +127,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi } } val navBarRightMargin = if (resources.configuration.orientation == - Configuration.ORIENTATION_LANDSCAPE) navBarHeight else 0 + Configuration.ORIENTATION_LANDSCAPE + ) navBarHeight else 0 val navBarBottomMargin = if (resources.configuration.orientation == - Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight + Configuration.ORIENTATION_LANDSCAPE + ) 0 else navBarHeight navBar.updateLayoutParams { rightMargin = navBarRightMargin bottomMargin = navBarBottomMargin @@ -345,7 +349,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi adult = media.isAdult if (media.anime != null) { viewPager.adapter = - ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME, media, intent.getIntExtra("commentId", -1)) + ViewPagerAdapter( + supportFragmentManager, + lifecycle, + SupportedMedia.ANIME, + media, + intent.getIntExtra("commentId", -1) + ) } else if (media.manga != null) { viewPager.adapter = ViewPagerAdapter( supportFragmentManager, @@ -368,7 +378,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi } else { navBar.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read) } - val commentTab = navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment) + val commentTab = + navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment) navBar.addTab(infoTab) navBar.addTab(watchTab) navBar.addTab(commentTab) @@ -412,10 +423,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) val rightMargin = if (resources.configuration.orientation == - Configuration.ORIENTATION_LANDSCAPE) navBarHeight else 0 + Configuration.ORIENTATION_LANDSCAPE + ) navBarHeight else 0 val bottomMargin = if (resources.configuration.orientation == - Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight - val params : ViewGroup.MarginLayoutParams = + Configuration.ORIENTATION_LANDSCAPE + ) 0 else navBarHeight + val params: ViewGroup.MarginLayoutParams = navBar.layoutParams as ViewGroup.MarginLayoutParams params.updateMargins(right = rightMargin, bottom = bottomMargin) } @@ -448,6 +461,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi SupportedMedia.MANGA -> MangaReadFragment() SupportedMedia.NOVEL -> NovelReadFragment() } + 2 -> { val fragment = CommentsFragment() val bundle = Bundle() diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt index 184f32bd..cfd9c736 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt @@ -56,9 +56,11 @@ class MediaDetailsViewModel : ViewModel() { media.anime != null -> { AnimeSources.list.size - 1 } + media.format == "MANGA" || media.format == "ONE_SHOT" -> { MangaSources.list.size - 1 } + else -> { NovelSources.list.size - 1 } diff --git a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt index c9f365be..1a4a9050 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt @@ -145,7 +145,8 @@ class MediaInfoFragment : Fragment() { } binding.mediaInfoDurationContainer.visibility = View.VISIBLE binding.mediaInfoSeasonContainer.visibility = View.VISIBLE - val seasonInfo = "${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}" + val seasonInfo = + "${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}" binding.mediaInfoSeason.text = seasonInfo if (media.anime.mainStudio != null) { @@ -182,9 +183,9 @@ class MediaInfoFragment : Fragment() { } binding.mediaInfoTotalTitle.setText(R.string.total_eps) val infoTotal = if (media.anime.nextAiringEpisode != null) - "${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}" + "${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}" else - (media.anime.totalEpisodes ?: "~").toString() + (media.anime.totalEpisodes ?: "~").toString() binding.mediaInfoTotal.text = infoTotal } else if (media.manga != null) { @@ -213,7 +214,8 @@ class MediaInfoFragment : Fragment() { (media.description ?: "null").replace("\\n", "
").replace("\\\"", "\""), HtmlCompat.FROM_HTML_MODE_LEGACY ) - val infoDesc = tripleTab + if (desc.toString() != "null") desc else getString(R.string.no_description_available) + val infoDesc = + tripleTab + if (desc.toString() != "null") desc else getString(R.string.no_description_available) binding.mediaInfoDescription.text = infoDesc binding.mediaInfoDescription.setOnClickListener { @@ -570,7 +572,7 @@ class MediaInfoFragment : Fragment() { parent.addView(root) } } - if(!media.users.isNullOrEmpty() && !offline){ + if (!media.users.isNullOrEmpty() && !offline) { ItemTitleRecyclerBinding.inflate( LayoutInflater.from(context), parent, diff --git a/app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt b/app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt index 07d151a0..a28bc018 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt @@ -70,7 +70,7 @@ object MediaNameAdapter { return if (seasonMatcher.find()) { seasonMatcher.group(2)?.toInt() } else { - null + text.toIntOrNull() } } @@ -93,7 +93,7 @@ object MediaNameAdapter { } } } else { - null + text.toFloatOrNull() } } @@ -139,7 +139,7 @@ object MediaNameAdapter { if (failedChapterNumberMatcher.find()) { failedChapterNumberMatcher.group(1)?.toFloat() } else { - null + text.toFloatOrNull() } } } diff --git a/app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt b/app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt index e8be5019..a32aa2ea 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt @@ -1,8 +1,6 @@ package ani.dantotsu.media import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context import android.content.Intent import android.view.LayoutInflater import android.view.View diff --git a/app/src/main/java/ani/dantotsu/media/MediaType.kt b/app/src/main/java/ani/dantotsu/media/MediaType.kt index 6762d98e..61cb2239 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,38 @@ 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/SearchActivity.kt b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt index fb685fe9..358c9700 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt @@ -78,8 +78,10 @@ class SearchActivity : AppCompatActivity() { source = intent.getStringExtra("source"), countryOfOrigin = intent.getStringExtra("country"), season = intent.getStringExtra("season"), - seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")?.toIntOrNull() else null, - startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")?.toIntOrNull() else null, + seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear") + ?.toIntOrNull() else null, + startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear") + ?.toIntOrNull() else null, results = mutableListOf(), hasNextPage = false ) diff --git a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt index f04cee00..a47a495b 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt @@ -13,7 +13,6 @@ import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager -import android.widget.ImageView import android.widget.PopupMenu import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources @@ -60,6 +59,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri } binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0) } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder { val binding = ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) @@ -129,36 +129,42 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri activity.search() updateFilterTextViewDrawable() } + R.id.sort_by_popular -> { activity.result.sort = Anilist.sortBy[1] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } + R.id.sort_by_trending -> { activity.result.sort = Anilist.sortBy[2] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } + R.id.sort_by_recent -> { activity.result.sort = Anilist.sortBy[3] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } + R.id.sort_by_a_z -> { activity.result.sort = Anilist.sortBy[4] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } + R.id.sort_by_z_a -> { activity.result.sort = Anilist.sortBy[5] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } + R.id.sort_by_pure_pain -> { activity.result.sort = Anilist.sortBy[6] activity.updateChips.invoke() @@ -299,14 +305,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri private fun fadeInAnimation(): Animation { return AlphaAnimation(0f, 1f).apply { duration = 150 - fillAfter = true } } private fun fadeOutAnimation(): Animation { return AlphaAnimation(1f, 0f).apply { duration = 150 - fillAfter = true } } @@ -325,7 +329,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri } - class SearchChipAdapter(val activity: SearchActivity, private val searchAdapter: SearchAdapter) : + class SearchChipAdapter( + val activity: SearchActivity, + private val searchAdapter: SearchAdapter + ) : RecyclerView.Adapter() { private var chips = activity.result.toChipList() diff --git a/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt b/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt index 53ee263f..6658532c 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt @@ -105,7 +105,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { setSortByFilterImage() binding.resetSearchFilter.setOnClickListener { - val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f) + val rotateAnimation = + ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f) rotateAnimation.duration = 500 rotateAnimation.interpolator = AccelerateDecelerateInterpolator() rotateAnimation.start() @@ -113,7 +114,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { } binding.resetSearchFilter.setOnLongClickListener { - val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f) + val rotateAnimation = + ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f) rotateAnimation.duration = 500 rotateAnimation.interpolator = AccelerateDecelerateInterpolator() rotateAnimation.start() @@ -125,8 +127,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { CoroutineScope(Dispatchers.Main).launch { activity.result.apply { - status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null } - source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null } + status = + binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null } + source = + binding.searchSource.text.toString().replace(" ", "_").ifBlank { null } format = binding.searchFormat.text.toString().ifBlank { null } season = binding.searchSeason.text.toString().ifBlank { null } startYear = binding.searchYear.text.toString().toIntOrNull() @@ -206,21 +210,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts) startBounceZoomAnimation(binding.countryFilter) } + R.id.country_china -> { activity.result.countryOfOrigin = "CN" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts) startBounceZoomAnimation(binding.countryFilter) } + R.id.country_south_korea -> { activity.result.countryOfOrigin = "KR" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts) startBounceZoomAnimation(binding.countryFilter) } + R.id.country_japan -> { activity.result.countryOfOrigin = "JP" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts) startBounceZoomAnimation(binding.countryFilter) } + R.id.country_taiwan -> { activity.result.countryOfOrigin = "TW" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts) @@ -257,7 +265,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { binding.searchFilterCancel.setOnClickListener { dismiss() } - val format = if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus + val format = + if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus binding.searchStatus.setText(activity.result.status?.replace("_", " ")) binding.searchStatus.setAdapter( ArrayAdapter( diff --git a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt index 38affa0d..503f1732 100644 --- a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt +++ b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.withContext import okhttp3.Request import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File class SubtitleDownloader { @@ -56,8 +55,8 @@ class SubtitleDownloader { context, downloadedType.type, false, - downloadedType.title, - downloadedType.chapter + downloadedType.titleName, + downloadedType.chapterName ) ?: throw Exception("Could not create directory") val type = loadSubtitleType(url) directory.findFile("subtitle.${type}")?.delete() diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt index d5d11b9e..f3fc4fe7 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt @@ -330,6 +330,7 @@ class AnimeWatchAdapter( 0 ) } + val chipText = "${names[limit * (position)]} - ${names[last - 1]}" chip.text = chipText chip.setTextColor( @@ -412,10 +413,12 @@ class AnimeWatchAdapter( if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE binding.animeSourceContinueText.text = - currActivity()!!.getString(R.string.continue_episode, ep.number, if (ep.filler) - currActivity()!!.getString(R.string.filler_tag) - else - "", cleanedTitle) + currActivity()!!.getString( + R.string.continue_episode, ep.number, if (ep.filler) + currActivity()!!.getString(R.string.filler_tag) + else + "", cleanedTitle + ) binding.animeSourceContinue.setOnClickListener { fragment.onEpisodeClick(continueEp) } @@ -441,11 +444,14 @@ class AnimeWatchAdapter( if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) { if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) { val nextIndex = media.selected!!.sourceIndex + 1 - binding.animeSource.setText(binding.animeSource.adapter - .getItem(nextIndex).toString(), false) + binding.animeSource.setText( + binding.animeSource.adapter + .getItem(nextIndex).toString(), false + ) fragment.onSourceChange(nextIndex).apply { binding.animeSourceTitle.text = showUserText - showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } + showUserTextListener = + { MainScope().launch { binding.animeSourceTitle.text = it } } binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately() setLanguageList(0, nextIndex) diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt index bbe0f908..7a3d993e 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt @@ -199,7 +199,8 @@ class AnimeWatchFragment : Fragment() { ConcatAdapter(headerAdapter, episodeAdapter) lifecycleScope.launch(Dispatchers.IO) { - val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode) + val offline = + !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode) if (offline) { media.selected!!.sourceIndex = model.watchSources!!.list.lastIndex } else { @@ -552,8 +553,8 @@ class AnimeWatchFragment : Fragment() { episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView)) episodeAdapter.notifyItemRangeInserted(0, arr.size) for (download in downloadManager.animeDownloadedTypes) { - if (media.compareName(download.title)) { - episodeAdapter.stopDownload(download.chapter) + if (media.compareName(download.titleName)) { + episodeAdapter.stopDownload(download.chapterName) } } } 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 86e8af61..27bd887a 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/ExoplayerView.kt b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt index a350f465..f3b8ffb6 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -111,6 +111,7 @@ import ani.dantotsu.connections.updateProgress import ani.dantotsu.databinding.ActivityExoplayerBinding import ani.dantotsu.defaultHeaders import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory +import ani.dantotsu.download.video.Helper import ani.dantotsu.dp import ani.dantotsu.getCurrentBrightnessValue import ani.dantotsu.hideSystemBars @@ -486,12 +487,14 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL } rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE } + in 225..315 -> { if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) { exoRotate.visibility = View.VISIBLE } rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE } + in 315..360, in 0..45 -> { if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) { exoRotate.visibility = View.VISIBLE @@ -1396,7 +1399,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL subClick() } } - val sub: MutableList = emptyList().toMutableList() + val sub: MutableList = + emptyList().toMutableList() ext.subtitles.forEach { subtitle -> val subtitleUrl = if (!hasExtSubtitles) video!!.file.url else subtitle.file.url //var localFile: String? = null @@ -1478,26 +1482,38 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL val downloadedMediaItem = if (ext.server.offline) { val titleName = ext.server.name.split("/").first() val episodeName = ext.server.name.split("/").last() + downloadId = PrefManager.getAnimeDownloadPreferences() + .getString("$titleName - $episodeName", null) ?: + PrefManager.getAnimeDownloadPreferences() + .getString(ext.server.name, null) + val exoItem = if (downloadId != null) { + Helper.downloadManager(this) + .downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem() + } else null + if (exoItem != null) { + exoItem + } else { - val directory = getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName) - if (directory != null) { - val files = directory.listFiles() - println(files) - val docFile = directory.listFiles().firstOrNull { - it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true - } - if (docFile != null) { - val uri = docFile.uri - MediaItem.Builder().setUri(uri).setMimeType(mimeType).build() + val directory = + getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName) + if (directory != null) { + val files = directory.listFiles() + println(files) + val docFile = directory.listFiles().firstOrNull { + it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true + } + if (docFile != null) { + val uri = docFile.uri + MediaItem.Builder().setUri(uri).setMimeType(mimeType).build() + } else { + snackString("File not found") + null + } } else { - snackString("File not found") + snackString("Directory not found") null } - } else { - snackString("Directory not found") - null } - } else null mediaItem = if (downloadedMediaItem == null) { @@ -1570,7 +1586,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL } }.show() dialog.window?.setDimAmount(0.8f) - } else buildExoplayer() + } + if (!this::exoPlayer.isInitialized) buildExoplayer() val isDisabled = (subtitle == null && hasExtSubtitles) exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters @@ -1864,7 +1881,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL exoPlayer.seekTo((new.interval.endTime * 1000).toLong()) skippedTimeStamps.add(new) } - if (PrefManager.getVal(PrefName.AutoSkipRecap) && new.skipType == "recap" && !skippedTimeStamps.contains(new)) { + if (PrefManager.getVal(PrefName.AutoSkipRecap) && new.skipType == "recap" && !skippedTimeStamps.contains( + new + ) + ) { exoPlayer.seekTo((new.interval.endTime * 1000).toLong()) skippedTimeStamps.add(new) } @@ -1909,12 +1929,15 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL val audioTracks: ArrayList = arrayListOf() val subTracks: ArrayList = arrayListOf(dummyTrack) tracks.groups.forEach { - println("Track__: $it\nTrack__: ${it.length}\nTrack__: ${it.isSelected}\n" + - "Track__: ${it.type}\nTrack__: ${it.mediaTrackGroup.id}") + println( + "Track__: $it\nTrack__: ${it.length}\nTrack__: ${it.isSelected}\n" + + "Track__: ${it.type}\nTrack__: ${it.mediaTrackGroup.id}" + ) when (it.type) { TRACK_TYPE_AUDIO -> { if (it.isSupported(true)) audioTracks.add(it) } + TRACK_TYPE_TEXT -> { if (!hasExtSubtitles) { if (it.isSupported(true)) subTracks.add(it) @@ -1950,7 +1973,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL onSetTrackGroupOverride(dummyTrack, TRACK_TYPE_TEXT) } } - else -> { } + + else -> {} } } } @@ -1965,6 +1989,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL isPlayerPlaying = true sourceClick() } + else -> { toast("Player Error ${error.errorCode} (${error.errorCodeName}) : ${error.message}") Injekt.get().logException(error) 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 e203a77f..737fc898 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt @@ -24,6 +24,8 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.R +import ani.dantotsu.addons.download.DownloadAddonManager +import ani.dantotsu.addons.torrent.TorrentAddonManager import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.copyToClipboard import ani.dantotsu.currActivity @@ -47,12 +49,16 @@ import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.snackString +import ani.dantotsu.toast 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.runBlocking 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 @@ -230,11 +236,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } private val externalPlayerResult = registerForActivityResult( - ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult -> Logger.log(result.data.toString()) } - private fun exportMagnetIntent(episode: Episode, video: Video) : Intent { + private fun exportMagnetIntent(episode: Episode, video: Video): Intent { val amnis = "com.amnis" return Intent(Intent.ACTION_VIEW).apply { component = ComponentName(amnis, "$amnis.gui.player.PlayerActivity") @@ -252,32 +259,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 (url.startsWith("magnet:") || 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 (url.contains("index=")) { + url.substringAfter("index=").toIntOrNull() ?: 0 + } else 0 + Logger.log("Sending: ${url}, ${video.quality}, $index") + val currentTorrent = extension.addTorrent( + 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 +330,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } } + dismiss() if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) { stopAddingToList() val intent = Intent(activity, ExoplayerView::class.java) @@ -403,7 +449,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { SubtitleDownloader.downloadSubtitle( requireContext(), subtitleToDownload!!.file.url, - DownloadedType(media!!.mainName(), media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number, MediaType.ANIME) + DownloadedType( + media!!.mainName(), + media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number, + MediaType.ANIME + ) ) } } @@ -430,12 +480,45 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { media!!.userPreferredName ) } else { + val downloadAddonManager: DownloadAddonManager = Injekt.get() + if (!downloadAddonManager.isAvailable()){ + toast("Download Extension not available") + return@setSafeOnClickListener + } val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!! val selectedVideo = if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null val subtitleNames = subtitles.map { it.language } var subtitleToDownload: Subtitle? = null - val activity = currActivity()?:requireActivity() + val activity = currActivity() ?: requireActivity() + selectedVideo?.file?.url?.let { url -> + if (url.startsWith("magnet:") || url.endsWith(".torrent")) { + val torrentExtension = Injekt.get() + if (!torrentExtension.isAvailable()) { + toast("Torrent Extension not available") + return@setSafeOnClickListener + } + runBlocking { + withContext(Dispatchers.IO) { + val extension = torrentExtension.extension!!.extension + torrentExtension.torrentHash?.let { + extension.removeTorrent(it) + } + val index = if (url.contains("index=")) { + url.substringAfter("index=").toIntOrNull() ?: 0 + } else 0 + Logger.log("Sending: ${url}, ${selectedVideo.quality}, $index") + val currentTorrent = extension.addTorrent( + url, selectedVideo.quality.toString(), "", "", false + ) + torrentExtension.torrentHash = currentTorrent.hash + selectedVideo.file.url = + extension.getLink(currentTorrent, index) + Logger.log("Received: ${selectedVideo.file.url}") + } + } + } + } if (subtitles.isNotEmpty()) { val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) .setTitle("Download Subtitle") @@ -509,9 +592,13 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { if (video.format == VideoType.CONTAINER) { binding.urlSize.isVisible = video.size != null // if video size is null or 0, show "Unknown Size" else show the size in MB - val sizeText = getString(R.string.mb_size, "${if (video.extraNote != null) " : " else ""}${ - if (video.size == 0.0) getString(R.string.size_unknown) else DecimalFormat("#.##").format(video.size ?: 0) - }") + val sizeText = getString( + R.string.mb_size, "${if (video.extraNote != null) " : " else ""}${ + if (video.size == 0.0) getString(R.string.size_unknown) else DecimalFormat("#.##").format( + video.size ?: 0 + ) + }" + ) binding.urlSize.text = sizeText } binding.urlNote.visibility = View.VISIBLE diff --git a/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt index a268237e..c17b286c 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt @@ -67,7 +67,11 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() { binding.subtitleTitle.setText(R.string.none) model.getMedia().observe(viewLifecycleOwner) { media -> val mediaID: Int = media.id - val selSubs = PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java) + val selSubs = PrefManager.getNullableCustomVal( + "subLang_${mediaID}", + null, + String::class.java + ) if (episode.selectedSubtitle != null && selSubs != "None") { binding.root.setCardBackgroundColor(TRANSPARENT) } @@ -107,7 +111,11 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() { model.getMedia().observe(viewLifecycleOwner) { media -> val mediaID: Int = media.id val selSubs: String? = - PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java) + PrefManager.getNullableCustomVal( + "subLang_${mediaID}", + null, + String::class.java + ) if (episode.selectedSubtitle != position - 1 && selSubs != subtitles[position - 1].language) { binding.root.setCardBackgroundColor(TRANSPARENT) } diff --git a/app/src/main/java/ani/dantotsu/media/anime/TrackGroupDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/TrackGroupDialogFragment.kt index c3aa729a..3ea86097 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/TrackGroupDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/TrackGroupDialogFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.OptIn -import androidx.core.view.isVisible import androidx.media3.common.C.TRACK_TYPE_AUDIO import androidx.media3.common.C.TrackType import androidx.media3.common.Tracks @@ -20,7 +19,7 @@ import java.util.Locale @OptIn(UnstableApi::class) class TrackGroupDialogFragment( - instance: ExoplayerView, trackGroups: ArrayList, type : @TrackType Int + instance: ExoplayerView, trackGroups: ArrayList, type: @TrackType Int ) : BottomSheetDialogFragment() { private var _binding: BottomSheetSubtitlesBinding? = null private val binding get() = _binding!! @@ -70,21 +69,28 @@ class TrackGroupDialogFragment( trackGroups[position].let { trackGroup -> when (val language = trackGroup.getTrackFormat(0).language?.lowercase()) { null -> { - binding.subtitleTitle.text = getString(R.string.unknown_track, "Track $position") + binding.subtitleTitle.text = + getString(R.string.unknown_track, "Track $position") } + "none" -> { binding.subtitleTitle.text = getString(R.string.disabled_track) } + else -> { val locale = if (language.contains("-")) { val parts = language.split("-") try { Locale(parts[0], parts[1]) - } catch (ignored: Exception) { null } + } catch (ignored: Exception) { + null + } } else { try { Locale(language) - } catch (ignored: Exception) { null } + } catch (ignored: Exception) { + null + } } binding.subtitleTitle.text = locale?.let { "[${it.language}] ${it.displayName}" diff --git a/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt b/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt index 5e4596ed..eea9a6b6 100644 --- a/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt +++ b/app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt @@ -60,7 +60,8 @@ class CommentItem( override fun bind(viewBinding: ItemCommentsBinding, position: Int) { binding = viewBinding setAnimation(binding.root.context, binding.root) - viewBinding.commentRepliesList.layoutManager = LinearLayoutManager(commentsFragment.activity) + viewBinding.commentRepliesList.layoutManager = + LinearLayoutManager(commentsFragment.activity) viewBinding.commentRepliesList.adapter = adapter val isUserComment = CommentsAPI.userId == comment.userId val levelColor = getAvatarColor(comment.totalVotes, backgroundColor) @@ -112,16 +113,20 @@ class CommentItem( viewBinding.commentUserName.setOnClickListener { ContextCompat.startActivity( - commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java) + commentsFragment.activity, + Intent(commentsFragment.activity, ProfileActivity::class.java) .putExtra("userId", comment.userId.toInt()) - .putExtra("userLVL","[${levelColor.second}]"), null + .putExtra("userLVL", "[${levelColor.second}]"), + null ) } viewBinding.commentUserAvatar.setOnClickListener { ContextCompat.startActivity( - commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java) + commentsFragment.activity, + Intent(commentsFragment.activity, ProfileActivity::class.java) .putExtra("userId", comment.userId.toInt()) - .putExtra("userLVL","[${levelColor.second}]"), null + .putExtra("userLVL", "[${levelColor.second}]"), + null ) } viewBinding.commentText.setOnLongClickListener { @@ -143,8 +148,10 @@ class CommentItem( viewBinding.commentInfo.setOnClickListener { val popup = PopupMenu(commentsFragment.requireContext(), viewBinding.commentInfo) popup.menuInflater.inflate(R.menu.profile_details_menu, popup.menu) - popup.menu.findItem(R.id.commentDelete)?.isVisible = isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod - popup.menu.findItem(R.id.commentBanUser)?.isVisible = (CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment + popup.menu.findItem(R.id.commentDelete)?.isVisible = + isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod + popup.menu.findItem(R.id.commentBanUser)?.isVisible = + (CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment popup.menu.findItem(R.id.commentReport)?.isVisible = !isUserComment popup.setOnMenuItemClickListener { item -> when (item.itemId) { @@ -273,12 +280,16 @@ class CommentItem( } fun replying(isReplying: Boolean) { - binding.commentReply.text = if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply" + binding.commentReply.text = + if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply" this.isReplying = isReplying } fun editing(isEditing: Boolean) { - binding.commentEdit.text = if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(R.string.edit) + binding.commentEdit.text = + if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString( + R.string.edit + ) this.isEditing = isEditing } @@ -286,7 +297,7 @@ class CommentItem( subCommentIds.add(id) } - private fun removeSubCommentIds(){ + private fun removeSubCommentIds() { subCommentIds.forEach { id -> @Suppress("UNCHECKED_CAST") val parentComments = parentSection.groups as? List ?: emptyList() @@ -306,11 +317,13 @@ class CommentItem( viewBinding.commentUpVote.alpha = 1f viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) } + -1 -> { viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24) viewBinding.commentDownVote.alpha = 1f } + else -> { viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) @@ -355,7 +368,8 @@ class CommentItem( private fun getAvatarColor(voteCount: Int, backgroundColor: Int): Pair { val level = if (voteCount < 0) 0 else sqrt(abs(voteCount.toDouble()) / 0.8).toInt() - val colorString = if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level] + val colorString = + if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level] var color = Color.parseColor(colorString) val ratio = getContrastRatio(color, backgroundColor) if (ratio < 4.5) { @@ -373,16 +387,17 @@ class CommentItem( * @param callback the callback to call when the user clicks yes */ private fun dialogBuilder(title: String, message: String, callback: () -> Unit) { - val alertDialog = android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup) - .setTitle(title) - .setMessage(message) - .setPositiveButton("Yes") { dialog, _ -> - callback() - dialog.dismiss() - } - .setNegativeButton("No") { dialog, _ -> - dialog.dismiss() - } + val alertDialog = + android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup) + .setTitle(title) + .setMessage(message) + .setPositiveButton("Yes") { dialog, _ -> + callback() + dialog.dismiss() + } + .setNegativeButton("No") { dialog, _ -> + dialog.dismiss() + } val dialog = alertDialog.show() dialog?.window?.setDimAmount(0.8f) } diff --git a/app/src/main/java/ani/dantotsu/media/comments/CommentsFragment.kt b/app/src/main/java/ani/dantotsu/media/comments/CommentsFragment.kt index fec27104..ff861dbe 100644 --- a/app/src/main/java/ani/dantotsu/media/comments/CommentsFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/comments/CommentsFragment.kt @@ -75,7 +75,10 @@ class CommentsFragment : Fragment() { super.onViewCreated(view, savedInstanceState) activity = requireActivity() as MediaDetailsActivity - binding.commentsListContainer.setBaseline(activity.navBar, activity.binding.commentInputLayout) + binding.commentsListContainer.setBaseline( + activity.navBar, + activity.binding.commentInputLayout + ) //get the media id from the intent val mediaId = arguments?.getInt("mediaId") ?: -1 @@ -118,7 +121,6 @@ class CommentsFragment : Fragment() { } } } else { - toast("Not logged in") activity.binding.commentMessageContainer.visibility = View.GONE } @@ -301,7 +303,7 @@ class CommentsFragment : Fragment() { activity.binding.commentLabel.setOnClickListener { //alert dialog to enter a number, with a cancel and ok button - val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup) + val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup) .setTitle("Enter a chapter/episode number tag") .setView(R.layout.dialog_edittext) .setPositiveButton("OK") { dialog, _ -> @@ -577,7 +579,7 @@ class CommentsFragment : Fragment() { * Called when the user tries to comment for the first time */ private fun showCommentRulesDialog() { - val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup) + val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup) .setTitle("Commenting Rules") .setMessage( "I WILL BAN YOU WITHOUT HESITATION\n" + diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt index 393d87b9..b4eee30e 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt @@ -343,6 +343,7 @@ class MangaChapterAdapter( fun updateType(t: Int) { type = t } + private fun formatDate(timestamp: Long?): String { timestamp ?: return "" // Return empty string if timestamp is null @@ -366,6 +367,7 @@ class MangaChapterAdapter( else -> "Just now" } } + 1L -> "1 day ago" in 2..6 -> "$daysDifference days ago" else -> SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(targetDate) diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt index 353bc1c2..74cd38d5 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt @@ -229,7 +229,7 @@ class MangaReadAdapter( refresh = true val intent = Intent(fragment.requireContext(), CookieCatcher::class.java) .putExtra("url", url) - ContextCompat.startActivity(fragment.requireContext(), intent, null) + startActivity(fragment.requireContext(), intent, null) } } } @@ -258,8 +258,10 @@ class MangaReadAdapter( dialogBinding.animeScanlatorContainer.isVisible = options.count() > 1 dialogBinding.scanlatorNo.text = "${options.count()}" dialogBinding.animeScanlatorTop.setOnClickListener { - val dialogView2 = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null) - val checkboxContainer = dialogView2.findViewById(R.id.checkboxContainer) + val dialogView2 = + LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null) + val checkboxContainer = + dialogView2.findViewById(R.id.checkboxContainer) val tickAllButton = dialogView2.findViewById(R.id.toggleButton) // Function to get the right image resource for the toggle button @@ -441,7 +443,11 @@ class MangaReadAdapter( if (media.manga?.chapters != null) { val chapters = media.manga.chapters!!.keys.toTypedArray() val anilistEp = (media.userProgress ?: 0).plus(1) - val appEp = PrefManager.getNullableCustomVal("${media.id}_current_chp", null, String::class.java) + val appEp = PrefManager.getNullableCustomVal( + "${media.id}_current_chp", + null, + String::class.java + ) ?.toIntOrNull() ?: 1 var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString() val filteredChapters = chapters.filter { chapterKey -> @@ -470,7 +476,11 @@ class MangaReadAdapter( val ep = media.manga.chapters!![continueEp]!! binding.itemEpisodeImage.loadImage(media.banner ?: media.cover) binding.animeSourceContinueText.text = - currActivity()!!.getString(R.string.continue_chapter, ep.number, if (!ep.title.isNullOrEmpty()) ep.title else "") + currActivity()!!.getString( + R.string.continue_chapter, + ep.number, + if (!ep.title.isNullOrEmpty()) ep.title else "" + ) binding.animeSourceContinue.setOnClickListener { fragment.onMangaChapterClick(continueEp) } @@ -491,11 +501,14 @@ class MangaReadAdapter( if (!sourceFound && PrefManager.getVal(PrefName.SearchSources)) { if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) { val nextIndex = media.selected!!.sourceIndex + 1 - binding.animeSource.setText(binding.animeSource.adapter - .getItem(nextIndex).toString(), false) + binding.animeSource.setText( + binding.animeSource.adapter + .getItem(nextIndex).toString(), false + ) fragment.onSourceChange(nextIndex).apply { binding.animeSourceTitle.text = showUserText - showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } + showUserTextListener = + { MainScope().launch { binding.animeSourceTitle.text = it } } setLanguageList(0, nextIndex) } subscribeButton(false) diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt index 9228a3f9..b86a3654 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -42,8 +42,8 @@ import ani.dantotsu.isOnline import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsViewModel -import ani.dantotsu.media.MediaType import ani.dantotsu.media.MediaNameAdapter +import ani.dantotsu.media.MediaType import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog import ani.dantotsu.navBarHeight import ani.dantotsu.notifications.subscription.SubscriptionHelper @@ -194,8 +194,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { ) for (download in downloadManager.mangaDownloadedTypes) { - if (media.compareName(download.title)) { - chapterAdapter.stopDownload(download.chapter) + if (media.compareName(download.titleName)) { + chapterAdapter.stopDownload(download.chapterName) } } @@ -203,8 +203,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { ConcatAdapter(headerAdapter, chapterAdapter) lifecycleScope.launch(Dispatchers.IO) { - val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode) - if (offline) media.selected!!.sourceIndex = model.mangaReadSources!!.list.lastIndex + val offline = + !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode) + if (offline) media.selected!!.sourceIndex = + model.mangaReadSources!!.list.lastIndex model.loadMangaChapters(media, media.selected!!.sourceIndex) } loaded = true diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt index 7b91d6f7..cd5fa2b8 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/BaseImageAdapter.kt @@ -42,7 +42,8 @@ abstract class BaseImageAdapter( override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { images = if (settings.layout == CurrentReaderSettings.Layouts.PAGED - && settings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP) { + && settings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP + ) { chapterImages.reversed() } else { chapterImages diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt index 5cd84736..af664426 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/MangaReaderActivity.kt @@ -56,8 +56,8 @@ import ani.dantotsu.isOnline import ani.dantotsu.logError import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsViewModel -import ani.dantotsu.media.MediaSingleton import ani.dantotsu.media.MediaNameAdapter +import ani.dantotsu.media.MediaSingleton import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.others.ImageViewDialog @@ -129,10 +129,12 @@ class MangaReaderActivity : AppCompatActivity() { var sliding = false var isAnimating = false - private val directionRLBT get() = defaultSettings.direction == RIGHT_TO_LEFT - || defaultSettings.direction == BOTTOM_TO_TOP - private val directionPagedBT get() = defaultSettings.layout == CurrentReaderSettings.Layouts.PAGED - && defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP + private val directionRLBT + get() = defaultSettings.direction == RIGHT_TO_LEFT + || defaultSettings.direction == BOTTOM_TO_TOP + private val directionPagedBT + get() = defaultSettings.layout == CurrentReaderSettings.Layouts.PAGED + && defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP override fun onAttachedToWindow() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !PrefManager.getVal(PrefName.ShowSystemBars)) { @@ -229,7 +231,7 @@ class MangaReaderActivity : AppCompatActivity() { binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 } ?: 1)) else - if (defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP ) { + if (defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP) { binding.mangaReaderPager.currentItem = (maxChapterPage.toInt() - value.toInt()) / (dualPage { 2 } ?: 1) } else { @@ -345,7 +347,11 @@ class MangaReaderActivity : AppCompatActivity() { if (currentChapterIndex > 0) change(currentChapterIndex - 1) else snackString(getString(R.string.first_chapter)) } else { - if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) } + if (chaptersArr.size > currentChapterIndex + 1) progress { + change( + currentChapterIndex + 1 + ) + } else snackString(getString(R.string.next_chapter_not_found)) } } @@ -355,7 +361,11 @@ class MangaReaderActivity : AppCompatActivity() { } binding.mangaReaderPreviousChapter.setOnClickListener { if (directionRLBT) { - if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) } + if (chaptersArr.size > currentChapterIndex + 1) progress { + change( + currentChapterIndex + 1 + ) + } else snackString(getString(R.string.next_chapter_not_found)) } else { if (currentChapterIndex > 0) change(currentChapterIndex - 1) @@ -372,11 +382,15 @@ class MangaReaderActivity : AppCompatActivity() { currentChapterIndex = chaptersArr.indexOf(chap.number) binding.mangaReaderChapterSelect.setSelection(currentChapterIndex) if (directionRLBT) { - binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: "" - binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: "" + binding.mangaReaderNextChap.text = + chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: "" + binding.mangaReaderPrevChap.text = + chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: "" } else { - binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: "" - binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: "" + binding.mangaReaderNextChap.text = + chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: "" + binding.mangaReaderPrevChap.text = + chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: "" } applySettings() val context = this @@ -389,10 +403,12 @@ class MangaReaderActivity : AppCompatActivity() { "nothing" -> mutableListOf( RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""), ) + "dantotsu" -> mutableListOf( RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""), RPC.Link("Read on Dantotsu", getString(R.string.dantotsu)) ) + "anilist" -> { val userId = PrefManager.getVal(PrefName.AnilistUserId) val anilistLink = "https://anilist.co/user/$userId/" @@ -401,6 +417,7 @@ class MangaReaderActivity : AppCompatActivity() { RPC.Link("View My AniList", anilistLink) ) } + else -> mutableListOf() } val presence = RPC.createPresence( @@ -411,7 +428,12 @@ class MangaReaderActivity : AppCompatActivity() { details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number), state = "${chap.number}/${media.manga?.totalChapters ?: "??"}", - largeImage = media.cover?.let { cover -> RPC.Link(media.userPreferredName, cover) }, + largeImage = media.cover?.let { cover -> + RPC.Link( + media.userPreferredName, + cover + ) + }, buttons = buttons ) ) @@ -918,7 +940,12 @@ class MangaReaderActivity : AppCompatActivity() { isAnimating = true ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f) .setDuration(controllerDuration).start() - ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 0f, 128f) + ObjectAnimator.ofFloat( + binding.mangaReaderBottomLayout, + "translationY", + 0f, + 128f + ) .apply { interpolator = overshoot;duration = controllerDuration;start() } ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f) .apply { interpolator = overshoot;duration = controllerDuration;start() } 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 eaa553ab..c95328f6 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, @@ -116,8 +113,14 @@ class NovelReadFragment : Fragment(), ) { try { val directory = - DownloadsManager.getSubDirectory(context?:currContext()!!, MediaType.NOVEL, false, novel.name) - val file = directory?.findFile(novel.name) + DownloadsManager.getSubDirectory( + context ?: currContext()!!, + MediaType.NOVEL, + false, + media.mainName(), + novel.name + ) + val file = directory?.findFile("0.epub") if (file?.exists() == false) return false val fileUri = file?.uri ?: return false val intent = Intent(context, NovelReaderActivity::class.java).apply { diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt index 6633d75c..bd723319 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt @@ -8,6 +8,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.R import ani.dantotsu.databinding.ItemNovelResponseBinding +import ani.dantotsu.loadImage import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.setAnimation import ani.dantotsu.snackString @@ -37,10 +38,7 @@ class NovelResponseAdapter( val binding = holder.binding val novel = list[position] setAnimation(fragment.requireContext(), holder.binding.root) - - val cover = GlideUrl(novel.coverUrl.url) { novel.coverUrl.headers } - Glide.with(binding.itemEpisodeImage).load(cover).override(400, 0) - .into(binding.itemEpisodeImage) + binding.itemEpisodeImage.loadImage(novel.coverUrl, 400, 0) val typedValue = TypedValue() fragment.requireContext().theme?.resolveAttribute( @@ -181,7 +179,7 @@ class NovelResponseAdapter( if (position != -1) { list[position].extra?.remove("0") list[position].extra?.set("0", "Downloading: $progress%") - Logger.log( "updateDownloadProgress: $progress, position: $position") + Logger.log("updateDownloadProgress: $progress, position: $position") notifyItemChanged(position) } } diff --git a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt index 7e21e14d..170980bc 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderActivity.kt @@ -292,7 +292,11 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener { applySettings() } - val cfi = PrefManager.getNullableCustomVal("${sanitizedBookId}_progress", null, String::class.java) + val cfi = PrefManager.getNullableCustomVal( + "${sanitizedBookId}_progress", + null, + String::class.java + ) cfi?.let { binding.bookReader.goto(it) } binding.progress.visibility = View.GONE diff --git a/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt b/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt index 0d07d8ac..3fdae952 100644 --- a/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt @@ -70,8 +70,10 @@ class ListActivity : AppCompatActivity() { setContentView(binding.root) val anime = intent.getBooleanExtra("anime", true) - binding.listTitle.text = getString(R.string.user_list, intent.getStringExtra("username"), - if (anime) getString(R.string.anime) else getString(R.string.manga)) + binding.listTitle.text = getString( + R.string.user_list, intent.getStringExtra("username"), + if (anime) getString(R.string.anime) else getString(R.string.manga) + ) binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { this@ListActivity.selectedTabIdx = tab?.position ?: 0 @@ -158,7 +160,8 @@ class ListActivity : AppCompatActivity() { } binding.filter.setOnClickListener { - val genres = PrefManager.getVal>(PrefName.GenresList).toMutableSet().sorted() + val genres = + PrefManager.getVal>(PrefName.GenresList).toMutableSet().sorted() val popup = PopupMenu(this, it) popup.menu.add("All") genres.forEach { genre -> diff --git a/app/src/main/java/ani/dantotsu/notifications/AlarmManagerScheduler.kt b/app/src/main/java/ani/dantotsu/notifications/AlarmManagerScheduler.kt index 3cc6b110..f5ed216c 100644 --- a/app/src/main/java/ani/dantotsu/notifications/AlarmManagerScheduler.kt +++ b/app/src/main/java/ani/dantotsu/notifications/AlarmManagerScheduler.kt @@ -5,9 +5,9 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build +import ani.dantotsu.notifications.TaskScheduler.TaskType import ani.dantotsu.notifications.anilist.AnilistNotificationReceiver import ani.dantotsu.notifications.comment.CommentNotificationReceiver -import ani.dantotsu.notifications.TaskScheduler.TaskType import ani.dantotsu.notifications.subscription.SubscriptionNotificationReceiver import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName diff --git a/app/src/main/java/ani/dantotsu/notifications/BootCompletedReceiver.kt b/app/src/main/java/ani/dantotsu/notifications/BootCompletedReceiver.kt index 8cfc0a1c..66c1f294 100644 --- a/app/src/main/java/ani/dantotsu/notifications/BootCompletedReceiver.kt +++ b/app/src/main/java/ani/dantotsu/notifications/BootCompletedReceiver.kt @@ -5,9 +5,9 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build +import ani.dantotsu.notifications.TaskScheduler.TaskType import ani.dantotsu.notifications.anilist.AnilistNotificationWorker import ani.dantotsu.notifications.comment.CommentNotificationWorker -import ani.dantotsu.notifications.TaskScheduler.TaskType import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.util.Logger diff --git a/app/src/main/java/ani/dantotsu/notifications/TaskScheduler.kt b/app/src/main/java/ani/dantotsu/notifications/TaskScheduler.kt index 04e431db..77ab1316 100644 --- a/app/src/main/java/ani/dantotsu/notifications/TaskScheduler.kt +++ b/app/src/main/java/ani/dantotsu/notifications/TaskScheduler.kt @@ -21,11 +21,16 @@ interface TaskScheduler { for (taskType in TaskType.entries) { val interval = when (taskType) { TaskType.COMMENT_NOTIFICATION -> CommentNotificationWorker.checkIntervals[PrefManager.getVal( - PrefName.CommentNotificationInterval)] + PrefName.CommentNotificationInterval + )] + TaskType.ANILIST_NOTIFICATION -> AnilistNotificationWorker.checkIntervals[PrefManager.getVal( - PrefName.AnilistNotificationInterval)] + PrefName.AnilistNotificationInterval + )] + TaskType.SUBSCRIPTION_NOTIFICATION -> SubscriptionNotificationWorker.checkIntervals[PrefManager.getVal( - PrefName.SubscriptionNotificationInterval)] + PrefName.SubscriptionNotificationInterval + )] } scheduleRepeatingTask(taskType, interval) } @@ -62,6 +67,7 @@ interface TaskScheduler { } } } + enum class TaskType { COMMENT_NOTIFICATION, ANILIST_NOTIFICATION, diff --git a/app/src/main/java/ani/dantotsu/notifications/comment/MediaNameFetch.kt b/app/src/main/java/ani/dantotsu/notifications/comment/MediaNameFetch.kt index 15e0e7e6..96e006bb 100644 --- a/app/src/main/java/ani/dantotsu/notifications/comment/MediaNameFetch.kt +++ b/app/src/main/java/ani/dantotsu/notifications/comment/MediaNameFetch.kt @@ -66,6 +66,7 @@ class MediaNameFetch { val type = object : TypeToken() {}.type return gson.fromJson(response, type) } + data class ReturnedData(val title: String, val coverImage: String, val color: String) data class MediaResponse(val data: Map) diff --git a/app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionHelper.kt b/app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionHelper.kt index 1e2f3db3..9905cb86 100644 --- a/app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionHelper.kt +++ b/app/src/main/java/ani/dantotsu/notifications/subscription/SubscriptionHelper.kt @@ -32,7 +32,7 @@ class SubscriptionHelper { return data } - private fun saveSelected( mediaId: Int, data: Selected) { + private fun saveSelected(mediaId: Int, data: Selected) { PrefManager.setCustomVal("${mediaId}-select", data) } diff --git a/app/src/main/java/ani/dantotsu/others/AndroidBug5497Workaround.kt b/app/src/main/java/ani/dantotsu/others/AndroidBug5497Workaround.kt index b25d10e2..dbe4e7d7 100644 --- a/app/src/main/java/ani/dantotsu/others/AndroidBug5497Workaround.kt +++ b/app/src/main/java/ani/dantotsu/others/AndroidBug5497Workaround.kt @@ -5,7 +5,10 @@ import android.graphics.Rect import android.view.View import android.widget.FrameLayout -class AndroidBug5497Workaround private constructor(activity: Activity, private val callback: (Boolean) -> Unit) { +class AndroidBug5497Workaround private constructor( + activity: Activity, + private val callback: (Boolean) -> Unit +) { private val mChildOfContent: View private var usableHeightPrevious = 0 private val frameLayoutParams: FrameLayout.LayoutParams diff --git a/app/src/main/java/ani/dantotsu/others/Download.kt b/app/src/main/java/ani/dantotsu/others/Download.kt index b80c941d..c25c12aa 100644 --- a/app/src/main/java/ani/dantotsu/others/Download.kt +++ b/app/src/main/java/ani/dantotsu/others/Download.kt @@ -1,6 +1,5 @@ package ani.dantotsu.others -import android.app.DownloadManager import android.content.ComponentName import android.content.Context import android.content.Intent @@ -8,7 +7,6 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle import android.os.Environment -import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import ani.dantotsu.FileUrl import ani.dantotsu.R @@ -19,9 +17,6 @@ import ani.dantotsu.parsers.Book import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.toast -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import java.io.File object Download { diff --git a/app/src/main/java/ani/dantotsu/others/Kitsu.kt b/app/src/main/java/ani/dantotsu/others/Kitsu.kt index f548fc31..958c1b46 100644 --- a/app/src/main/java/ani/dantotsu/others/Kitsu.kt +++ b/app/src/main/java/ani/dantotsu/others/Kitsu.kt @@ -2,10 +2,10 @@ package ani.dantotsu.others import ani.dantotsu.FileUrl import ani.dantotsu.client -import ani.dantotsu.util.Logger import ani.dantotsu.media.Media import ani.dantotsu.media.anime.Episode import ani.dantotsu.tryWithSuspend +import ani.dantotsu.util.Logger import com.google.gson.Gson import com.lagradost.nicehttp.NiceResponse import kotlinx.serialization.SerialName diff --git a/app/src/main/java/ani/dantotsu/others/OutlineTextView.kt b/app/src/main/java/ani/dantotsu/others/OutlineTextView.kt index 7a0cf925..105f34a7 100644 --- a/app/src/main/java/ani/dantotsu/others/OutlineTextView.kt +++ b/app/src/main/java/ani/dantotsu/others/OutlineTextView.kt @@ -56,9 +56,10 @@ class OutlineTextView : AppCompatTextView { setStrokeWidth(strokeWidth) } - private val Float.toPx get() = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics - ) + private val Float.toPx + get() = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics + ) private fun setStrokeWidth(width: Float) { strokeWidth = width.toPx diff --git a/app/src/main/java/ani/dantotsu/others/Xpandable.kt b/app/src/main/java/ani/dantotsu/others/Xpandable.kt index 53518b40..1c4fe3bf 100644 --- a/app/src/main/java/ani/dantotsu/others/Xpandable.kt +++ b/app/src/main/java/ani/dantotsu/others/Xpandable.kt @@ -50,7 +50,7 @@ class Xpandable @JvmOverloads constructor( } } postDelayed({ - listeners.forEach{ + listeners.forEach { it.onRetract() } }, 300) @@ -66,7 +66,7 @@ class Xpandable @JvmOverloads constructor( } } postDelayed({ - listeners.forEach{ + listeners.forEach { it.onExpand() } }, 300) diff --git a/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt b/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt index e9e171f8..5dffd4fa 100644 --- a/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt +++ b/app/src/main/java/ani/dantotsu/others/webview/CookieCatcher.kt @@ -24,7 +24,8 @@ class CookieCatcher : AppCompatActivity() { //get url from intent val url = intent.getStringExtra("url") ?: getString(R.string.cursed_yt) - val headers: Map = intent.getSerializableExtraCompat("headers") as? Map ?: emptyMap() + val headers: Map = + intent.getSerializableExtraCompat("headers") as? Map ?: emptyMap() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val process = Application.getProcessName() diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index 0b59fe14..95edf008 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/OfflineAnimeParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt index 08f62446..85d1af90 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt @@ -2,11 +2,13 @@ package ani.dantotsu.parsers import android.app.Application import ani.dantotsu.currContext +import ani.dantotsu.download.DownloadCompat.Companion.loadEpisodesCompat +import ani.dantotsu.download.DownloadCompat.Companion.loadSubtitleCompat import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName -import ani.dantotsu.media.MediaType import ani.dantotsu.media.MediaNameAdapter +import ani.dantotsu.media.MediaType import ani.dantotsu.tryWithSuspend import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.animesource.model.SAnime @@ -43,7 +45,7 @@ class OfflineAnimeParser : AnimeParser() { if (it.isDirectory) { val episode = Episode( it.name!!, - getTaskName(animeLink,it.name!!), + getTaskName(animeLink, it.name!!), it.name, null, null, @@ -53,8 +55,11 @@ class OfflineAnimeParser : AnimeParser() { episodes.add(episode) } } - episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) } - return episodes + //episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) } + episodes.addAll(loadEpisodesCompat(animeLink, extra, sAnime)) + //filter those with the same name + return episodes.distinctBy { it.number } + .sortedBy { MediaNameAdapter.findEpisodeNumber(it.number) } } return emptyList() } @@ -75,14 +80,16 @@ class OfflineAnimeParser : AnimeParser() { override suspend fun search(query: String): List { - val titles = downloadManager.animeDownloadedTypes.map { it.title }.distinct() - val returnTitles: MutableList = mutableListOf() + val titles = downloadManager.animeDownloadedTypes.map { it.titleName }.distinct() + val returnTitlesPair: MutableList> = mutableListOf() for (title in titles) { Logger.log("Comparing $title to $query") - if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { - returnTitles.add(title) + val score = FuzzySearch.ratio(title.lowercase(), query.lowercase()) + if (score > 80) { + returnTitlesPair.add(Pair(title, score)) } } + val returnTitles = returnTitlesPair.sortedByDescending { it.second }.map { it.first } val returnList: MutableList = mutableListOf() for (title in returnTitles) { returnList.add(ShowResponse(title, title, title)) @@ -148,6 +155,7 @@ class OfflineVideoExtractor(private val videoServer: VideoServer) : VideoExtract ) } } + loadSubtitleCompat(title, episode)?.let { return it } } return null } diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt index a3a239a8..f0c1237f 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt @@ -1,8 +1,8 @@ package ani.dantotsu.parsers import android.app.Application -import android.os.Environment -import ani.dantotsu.currContext +import ani.dantotsu.download.DownloadCompat.Companion.loadChaptersCompat +import ani.dantotsu.download.DownloadCompat.Companion.loadImagesCompat import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory import ani.dantotsu.media.MediaNameAdapter @@ -13,7 +13,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() @@ -44,8 +43,9 @@ class OfflineMangaParser : MangaParser() { chapters.add(chapter) } } - chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) } - return chapters + chapters.addAll(loadChaptersCompat(mangaLink, extra, sManga)) + return chapters.distinctBy { it.number } + .sortedBy { MediaNameAdapter.findChapterNumber(it.number) } } return emptyList() } @@ -63,26 +63,32 @@ class OfflineMangaParser : MangaParser() { images.add(image) } } - images.sortBy { image -> - val matchResult = imageNumberRegex.find(image.url.url) - matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE - } for (image in images) { Logger.log("imageNumber: ${image.url.url}") } - return images + return if (images.isNotEmpty()) { + images.sortBy { image -> + val matchResult = imageNumberRegex.find(image.url.url) + matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE + } + images + } else { + loadImagesCompat(chapterLink, sChapter) + } } return emptyList() } override suspend fun search(query: String): List { - val titles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct() - val returnTitles: MutableList = mutableListOf() + val titles = downloadManager.mangaDownloadedTypes.map { it.titleName }.distinct() + val returnTitlesPair: MutableList> = mutableListOf() for (title in titles) { - if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { - returnTitles.add(title) + val score = FuzzySearch.ratio(title.lowercase(), query.lowercase()) + if (score > 80) { + returnTitlesPair.add(Pair(title, score)) } } + val returnTitles = returnTitlesPair.sortedByDescending { it.second }.map { it.first } val returnList: MutableList = mutableListOf() for (title in returnTitles) { returnList.add(ShowResponse(title, title, title)) diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt index 2ae88200..7a3535e0 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt @@ -1,16 +1,14 @@ 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 import ani.dantotsu.media.MediaType +import ani.dantotsu.util.Logger 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() @@ -31,7 +29,7 @@ class OfflineNovelParser : NovelParser() { directory.listFiles().forEach { if (it.isDirectory) { val chapter = Book( - it.name?:"Unknown", + it.name ?: "Unknown", it.uri.toString(), null, listOf(it.uri.toString()) @@ -51,13 +49,16 @@ class OfflineNovelParser : NovelParser() { } override suspend fun search(query: String): List { - val titles = downloadManager.novelDownloadedTypes.map { it.title }.distinct() - val returnTitles: MutableList = mutableListOf() + val titles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct() + val returnTitlesPair: MutableList> = mutableListOf() for (title in titles) { - if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { - returnTitles.add(title) + Logger.log("Comparing $title to $query") + val score = FuzzySearch.ratio(title.lowercase(), query.lowercase()) + if (score > 80) { + returnTitlesPair.add(Pair(title, score)) } } + val returnTitles = returnTitlesPair.sortedByDescending { it.second }.map { it.first } val returnList: MutableList = mutableListOf() for (title in returnTitles) { //need to search the subdirectories for the ShowResponses @@ -66,13 +67,13 @@ class OfflineNovelParser : NovelParser() { if (directory?.exists() == true) { directory.listFiles().forEach { if (it.isDirectory) { - names.add(it.name?: "Unknown") + names.add(it.name ?: "Unknown") } } } val cover = directory?.findFile("cover.jpg")?.uri.toString() names.forEach { - returnList.add(ShowResponse(it, it, cover)) + returnList.add(ShowResponse(it, query, cover)) } } return returnList diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionFileObserver.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionFileObserver.kt deleted file mode 100644 index 30ad7548..00000000 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionFileObserver.kt +++ /dev/null @@ -1,57 +0,0 @@ -package ani.dantotsu.parsers.novel - -import android.os.FileObserver -import ani.dantotsu.parsers.novel.FileObserver.fileObserver -import ani.dantotsu.util.Logger -import java.io.File - - -class NovelExtensionFileObserver(private val listener: Listener, private val path: String) : - FileObserver(path, CREATE or DELETE or MOVED_FROM or MOVED_TO or MODIFY) { - - init { - fileObserver = this - } - - /** - * Starts observing the file changes in the directory. - */ - fun register() { - startWatching() - } - - - override fun onEvent(event: Int, file: String?) { - Logger.log("Event: $event") - if (file == null) return - - val fullPath = File(path, file) - - when (event) { - CREATE -> { - Logger.log("File created: $fullPath") - listener.onExtensionFileCreated(fullPath) - } - - DELETE -> { - Logger.log("File deleted: $fullPath") - listener.onExtensionFileDeleted(fullPath) - } - - MODIFY -> { - Logger.log("File modified: $fullPath") - listener.onExtensionFileModified(fullPath) - } - } - } - - interface Listener { - fun onExtensionFileCreated(file: File) - fun onExtensionFileDeleted(file: File) - fun onExtensionFileModified(file: File) - } -} - -object FileObserver { - var fileObserver: FileObserver? = null -} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt index a209881e..0a225c9d 100644 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt @@ -8,6 +8,7 @@ import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult +import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.awaitSuccess @@ -87,7 +88,7 @@ class NovelExtensionGithubApi { } } - val installedExtensions = NovelExtensionLoader.loadExtensions(context) + val installedExtensions = ExtensionLoader.loadNovelExtensions(context) .filterIsInstance() .map { it.extension } diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionInstaller.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionInstaller.kt deleted file mode 100644 index 809f4a5f..00000000 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionInstaller.kt +++ /dev/null @@ -1,377 +0,0 @@ -package ani.dantotsu.parsers.novel - -import android.app.DownloadManager -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.database.Cursor -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.MediaStore -import androidx.core.content.ContextCompat -import androidx.core.content.getSystemService -import androidx.core.net.toUri -import ani.dantotsu.snackString -import ani.dantotsu.util.Logger -import com.jakewharton.rxrelay.PublishRelay -import eu.kanade.tachiyomi.extension.InstallStep -import eu.kanade.tachiyomi.util.storage.getUriCompat -import rx.Observable -import rx.android.schedulers.AndroidSchedulers -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.nio.channels.FileChannel -import java.nio.file.Files -import java.util.concurrent.TimeUnit - -/** - * The installer which installs, updates and uninstalls the extensions. - * - * @param context The application context. - */ -internal class NovelExtensionInstaller(private val context: Context) { - - /** - * The system's download manager - */ - private val downloadManager = context.getSystemService()!! - - /** - * The broadcast receiver which listens to download completion events. - */ - private val downloadReceiver = DownloadCompletionReceiver() - - /** - * The currently requested downloads, with the package name (unique id) as key, and the id - * returned by the download manager. - */ - private val activeDownloads = hashMapOf() - - /** - * Relay used to notify the installation step of every download. - */ - private val downloadsRelay = PublishRelay.create>() - - /** - * Adds the given extension to the downloads queue and returns an observable containing its - * step in the installation process. - * - * @param url The url of the apk. - * @param extension The extension to install. - */ - fun downloadAndInstall(url: String, extension: NovelExtension): Observable = Observable.defer { - val pkgName = extension.pkgName - - val oldDownload = activeDownloads[pkgName] - if (oldDownload != null) { - deleteDownload(pkgName) - } - - val sourcePath = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath - //if the file is already downloaded, remove it - val fileToDelete = File("$sourcePath/${url.toUri().lastPathSegment}") - if (fileToDelete.exists()) { - if (fileToDelete.delete()) { - Logger.log("APK file deleted successfully.") - } else { - Logger.log("Failed to delete APK file.") - } - } else { - Logger.log("APK file not found.") - } - - // Register the receiver after removing (and unregistering) the previous download - downloadReceiver.register() - - val downloadUri = url.toUri() - val request = DownloadManager.Request(downloadUri) - .setTitle(extension.name) - .setMimeType(APK_MIME) - .setDestinationInExternalFilesDir( - context, - Environment.DIRECTORY_DOWNLOADS, - downloadUri.lastPathSegment - ) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - val id = downloadManager.enqueue(request) - activeDownloads[pkgName] = id - - downloadsRelay.filter { it.first == id } - .map { it.second } - // Poll download status - .mergeWith(pollStatus(id)) - // Stop when the application is installed or errors - .takeUntil { it.isCompleted() } - // Always notify on main thread - .observeOn(AndroidSchedulers.mainThread()) - // Always remove the download when unsubscribed - .doOnUnsubscribe { deleteDownload(pkgName) } - } - - /** - * Returns an observable that polls the given download id for its status every second, as the - * manager doesn't have any notification system. It'll stop once the download finishes. - * - * @param id The id of the download to poll. - */ - private fun pollStatus(id: Long): Observable { - val query = DownloadManager.Query().setFilterById(id) - - return Observable.interval(0, 1, TimeUnit.SECONDS) - // Get the current download status - .map { - downloadManager.query(query).use { cursor -> - if (cursor.moveToFirst()) { - cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) - } else { - DownloadManager.STATUS_FAILED - } - } - } - // Ignore duplicate results - .distinctUntilChanged() - // Stop polling when the download fails or finishes - .takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED } - // Map to our model - .flatMap { status -> - when (status) { - DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending) - DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading) - DownloadManager.STATUS_SUCCESSFUL -> Observable.just(InstallStep.Installing) - else -> Observable.empty() - } - } - } - - fun installApk(downloadId: Long, uri: Uri, context: Context, pkgName: String): InstallStep { - val sourcePath = - context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/" + uri.lastPathSegment - val destinationPath = - context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk" - - // Check if source path is obtained correctly - if (!sourcePath.startsWith(FILE_SCHEME)) { - Logger.log("Source APK path not found.") - downloadsRelay.call(downloadId to InstallStep.Error) - return InstallStep.Error - } - - // Create the destination directory if it doesn't exist - val destinationDir = File(destinationPath).parentFile - if (destinationDir?.exists() == false) { - destinationDir.mkdirs() - } - if (destinationDir?.setWritable(true) == false) { - Logger.log("Failed to set destinationDir to writable.") - downloadsRelay.call(downloadId to InstallStep.Error) - return InstallStep.Error - } - - // Copy the file to the new location - copyFileToInternalStorage(sourcePath, destinationPath) - Logger.log("APK moved to $destinationPath") - downloadsRelay.call(downloadId to InstallStep.Installed) - return InstallStep.Installed - } - - /** - * Cancels extension install and remove from download manager and installer. - */ - fun cancelInstall(pkgName: String) { - val downloadId = activeDownloads.remove(pkgName) ?: return - downloadManager.remove(downloadId) - } - - fun uninstallApk(pkgName: String, context: Context) { - val apkPath = - context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk" - val fileToDelete = File(apkPath) - //give write permission to the file - if (fileToDelete.exists() && !fileToDelete.canWrite()) { - Logger.log("File is not writable. Giving write permission.") - val a = fileToDelete.setWritable(true) - Logger.log("Success: $a") - } - //set the directory to writable - val destinationDir = File(apkPath).parentFile - if (destinationDir?.exists() == false) { - destinationDir.mkdirs() - } - val s = destinationDir?.setWritable(true) - Logger.log("Success destinationDir: $s") - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - Files.delete(fileToDelete.toPath()) - } catch (e: Exception) { - Logger.log("Failed to delete APK file.") - Logger.log(e) - snackString("Failed to delete APK file.") - } - } else { - if (fileToDelete.exists()) { - if (fileToDelete.delete()) { - Logger.log("APK file deleted successfully.") - snackString("APK file deleted successfully.") - } else { - Logger.log("Failed to delete APK file.") - snackString("Failed to delete APK file.") - } - } else { - Logger.log("APK file not found.") - snackString("APK file not found.") - } - } - } - - private fun copyFileToInternalStorage(sourcePath: String, destinationPath: String) { - val source = File(sourcePath) - val destination = File(destinationPath) - destination.setWritable(true) - - //delete the file if it already exists - if (destination.exists()) { - if (destination.delete()) { - Logger.log("File deleted successfully.") - } else { - Logger.log("Failed to delete file.") - } - } - - var inputChannel: FileChannel? = null - var outputChannel: FileChannel? = null - try { - inputChannel = FileInputStream(source).channel - outputChannel = FileOutputStream(destination).channel - inputChannel.transferTo(0, inputChannel.size(), outputChannel) - destination.setWritable(false) - } catch (e: Exception) { - e.printStackTrace() - } finally { - inputChannel?.close() - outputChannel?.close() - } - - Logger.log("File copied to internal storage.") - } - - @Suppress("unused") - private fun getRealPathFromURI(context: Context, contentUri: Uri): String? { - var cursor: Cursor? = null - try { - val proj = arrayOf(MediaStore.Images.Media.DATA) - cursor = context.contentResolver.query(contentUri, proj, null, null, null) - val columnIndex = cursor?.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) - if (cursor != null && cursor.moveToFirst() && columnIndex != null) { - return cursor.getString(columnIndex) - } - } finally { - cursor?.close() - } - return null - } - - /** - * Sets the step of the installation of an extension. - * - * @param downloadId The id of the download. - * @param step New install step. - */ - fun updateInstallStep(downloadId: Long, step: InstallStep) { - downloadsRelay.call(downloadId to step) - } - - /** - * Deletes the download for the given package name. - * - * @param pkgName The package name of the download to delete. - */ - private fun deleteDownload(pkgName: String) { - val downloadId = activeDownloads.remove(pkgName) - if (downloadId != null) { - downloadManager.remove(downloadId) - } - if (activeDownloads.isEmpty()) { - downloadReceiver.unregister() - } - } - - /** - * Receiver that listens to download status events. - */ - private inner class DownloadCompletionReceiver : BroadcastReceiver() { - - /** - * Whether this receiver is currently registered. - */ - private var isRegistered = false - - /** - * Registers this receiver if it's not already. - */ - fun register() { - if (isRegistered) return - isRegistered = true - - val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) - ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED) - } - - /** - * Unregisters this receiver if it's not already. - */ - fun unregister() { - if (!isRegistered) return - isRegistered = false - - context.unregisterReceiver(this) - } - - /** - * Called when a download event is received. It looks for the download in the current active - * downloads and notifies its installation step. - */ - override fun onReceive(context: Context, intent: Intent?) { - val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return - - // Avoid events for downloads we didn't request - if (id !in activeDownloads.values) return - - val uri = downloadManager.getUriForDownloadedFile(id) - - // Set next installation step - if (uri == null) { - Logger.log("Couldn't locate downloaded APK") - downloadsRelay.call(id to InstallStep.Error) - return - } - - val query = DownloadManager.Query().setFilterById(id) - downloadManager.query(query).use { cursor -> - if (cursor.moveToFirst()) { - val localUri = cursor.getString( - cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI), - ).removePrefix(FILE_SCHEME) - val pkgName = extractPkgNameFromUri(localUri) - installApk(id, File(localUri).getUriCompat(context), context, pkgName) - } - } - } - - private fun extractPkgNameFromUri(localUri: String): String { - val uri = Uri.parse(localUri) - val path = uri.path - val pkgName = path?.substring(path.lastIndexOf('/') + 1)?.removeSuffix(".apk") - Logger.log("Package name: $pkgName") - return pkgName ?: "" - } - } - - companion object { - const val APK_MIME = "application/vnd.android.package-archive" - const val FILE_SCHEME = "file://" - } -} diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt deleted file mode 100644 index 7c814880..00000000 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt +++ /dev/null @@ -1,154 +0,0 @@ -package ani.dantotsu.parsers.novel - -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager.GET_SIGNATURES -import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES -import android.os.Build -import ani.dantotsu.connections.crashlytics.CrashlyticsInterface -import ani.dantotsu.parsers.NovelInterface -import ani.dantotsu.snackString -import ani.dantotsu.util.Logger -import dalvik.system.PathClassLoader -import eu.kanade.tachiyomi.util.lang.Hash -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.File -import java.util.Locale - -internal object NovelExtensionLoader { - - private const val officialSignature = - "a3061edb369278749b8e8de810d440d38e96417bbd67bbdfc5d9d9ed475ce4a5" //dan's key - - fun loadExtensions(context: Context): List { - val installDir = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/" - val results = mutableListOf() - //the number of files - Logger.log("Loading extensions from $installDir") - Logger.log( - "Loading extensions from ${File(installDir).listFiles()?.size}" - ) - File(installDir).setWritable(false) - File(installDir).listFiles()?.forEach { - //set the file to read only - it.setWritable(false) - Logger.log("Loading extension ${it.name}") - val extension = loadExtension(context, it) - if (extension is NovelLoadResult.Success) { - results.add(extension) - } else { - Logger.log("Failed to load extension ${it.name}") - } - } - return results - } - - /** - * Attempts to load an extension from the given package name. It checks if the extension - * contains the required feature flag before trying to load it. - */ - @Suppress("unused") - fun loadExtensionFromPkgName(context: Context, pkgName: String): NovelLoadResult { - val path = - context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/$pkgName.apk" - //make /extensions/novel read only - context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/".let { - File(it).setWritable(false) - File(it).setReadable(true) - } - try { - context.packageManager.getPackageArchiveInfo(path, 0) - } catch (error: Exception) { - // Unlikely, but the package may have been uninstalled at this point - Logger.log("Failed to load extension $pkgName") - return NovelLoadResult.Error(Exception("Failed to load extension")) - } - return loadExtension(context, File(path)) - } - - @Suppress("DEPRECATION") - fun loadExtension(context: Context, file: File): NovelLoadResult { - val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - context.packageManager.getPackageArchiveInfo( - file.absolutePath, - GET_SIGNATURES or GET_SIGNING_CERTIFICATES - ) - ?: return NovelLoadResult.Error(Exception("Failed to load extension")) - } else { - context.packageManager.getPackageArchiveInfo(file.absolutePath, GET_SIGNATURES) - ?: return NovelLoadResult.Error(Exception("Failed to load extension")) - } - val appInfo = packageInfo.applicationInfo - ?: return NovelLoadResult.Error(Exception("Failed to load Extension Info")) - appInfo.sourceDir = file.absolutePath - appInfo.publicSourceDir = file.absolutePath - - val signatureHash = getSignatureHash(packageInfo) - - if ((signatureHash == null) || !signatureHash.contains(officialSignature)) { - Logger.log("Package ${packageInfo.packageName} isn't signed") - Logger.log("signatureHash: $signatureHash") - snackString("Package ${packageInfo.packageName} isn't signed") - //return NovelLoadResult.Error(Exception("Extension not signed")) - } - - val extension = NovelExtension.Installed( - packageInfo.applicationInfo?.loadLabel(context.packageManager)?.toString() - ?: return NovelLoadResult.Error(Exception("Failed to load Extension Info")), - packageInfo.packageName - ?: return NovelLoadResult.Error(Exception("Failed to load Extension Info")), - packageInfo.versionName ?: "", - packageInfo.versionCode.toLong(), - loadSources( - context, file, - packageInfo.applicationInfo?.loadLabel(context.packageManager)?.toString()!! - ), - packageInfo.applicationInfo?.loadIcon(context.packageManager) - ) - - return NovelLoadResult.Success(extension) - } - - @Suppress("DEPRECATION") - private fun getSignatureHash(pkgInfo: PackageInfo): List? { - val signatures = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && pkgInfo.signingInfo != null) { - pkgInfo.signingInfo.apkContentsSigners - } else { - pkgInfo.signatures - } - return if (!signatures.isNullOrEmpty()) { - signatures.map { Hash.sha256(it.toByteArray()) } - } else { - null - } - } - - private fun loadSources(context: Context, file: File, className: String): List { - return try { - Logger.log("isFileWritable: ${file.canWrite()}") - if (file.canWrite()) { - val a = file.setWritable(false) - Logger.log("success: $a") - } - Logger.log("isFileWritable: ${file.canWrite()}") - val classLoader = PathClassLoader(file.absolutePath, null, context.classLoader) - val extensionClassName = - "some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className" - val loadedClass = classLoader.loadClass(extensionClassName) - val instance = loadedClass.getDeclaredConstructor().newInstance() - val novelInterfaceInstance = instance as? NovelInterface - listOfNotNull(novelInterfaceInstance) - } catch (e: Exception) { - e.printStackTrace() - Injekt.get().logException(e) - emptyList() - } - } -} - -sealed class NovelLoadResult { - data class Success(val extension: NovelExtension.Installed) : NovelLoadResult() - data class Error(val error: Exception) : NovelLoadResult() -} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt index e55b238c..8ea2f620 100644 --- a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt @@ -2,14 +2,17 @@ package ani.dantotsu.parsers.novel 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.tachiyomi.extension.InstallStep +import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver +import eu.kanade.tachiyomi.extension.util.ExtensionInstaller +import eu.kanade.tachiyomi.extension.util.ExtensionLoader import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import rx.Observable import tachiyomi.core.util.lang.withUIContext -import java.io.File class NovelExtensionManager(private val context: Context) { var isInitialized = false @@ -24,7 +27,7 @@ class NovelExtensionManager(private val context: Context) { /** * The installer which installs, updates and uninstalls the Novel extensions. */ - private val installer by lazy { NovelExtensionInstaller(context) } + private val installer by lazy { ExtensionInstaller(context) } private val iconMap = mutableMapOf() @@ -49,12 +52,11 @@ class NovelExtensionManager(private val context: Context) { init { initNovelExtensions() - val path = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/" - NovelExtensionFileObserver(NovelInstallationListener(), path).register() + ExtensionInstallReceiver().setNovelListener(NovelInstallationListener()).register(context) } private fun initNovelExtensions() { - val novelExtensions = NovelExtensionLoader.loadExtensions(context) + val novelExtensions = ExtensionLoader.loadNovelExtensions(context) _installedNovelExtensionsFlow.value = novelExtensions .filterIsInstance() @@ -117,7 +119,8 @@ class NovelExtensionManager(private val context: Context) { * @param extension The anime extension to be installed. */ fun installExtension(extension: NovelExtension.Available): Observable { - return installer.downloadAndInstall(api.getApkUrl(extension), extension) + return installer.downloadAndInstall(api.getApkUrl(extension), extension.pkgName, + extension.name, MediaType.NOVEL) } /** @@ -157,7 +160,7 @@ class NovelExtensionManager(private val context: Context) { * @param pkgName The package name of the application to uninstall. */ fun uninstallExtension(pkgName: String, context: Context) { - installer.uninstallApk(pkgName, context) + installer.uninstallApk(pkgName) } /** @@ -202,28 +205,18 @@ class NovelExtensionManager(private val context: Context) { /** * Listener which receives events of the novel extensions being installed, updated or removed. */ - private inner class NovelInstallationListener : NovelExtensionFileObserver.Listener { - - override fun onExtensionFileCreated(file: File) { - NovelExtensionLoader.loadExtension(context, file).let { - if (it is NovelLoadResult.Success) { - registerNewExtension(it.extension.withUpdateCheck()) - } - } + private inner class NovelInstallationListener : ExtensionInstallReceiver.NovelListener { + override fun onExtensionInstalled(extension: NovelExtension.Installed) { + registerNewExtension(extension.withUpdateCheck()) } - override fun onExtensionFileDeleted(file: File) { - val pkgName = file.nameWithoutExtension + override fun onExtensionUpdated(extension: NovelExtension.Installed) { + registerUpdatedExtension(extension.withUpdateCheck()) + } + + override fun onPackageUninstalled(pkgName: String) { unregisterNovelExtension(pkgName) } - - override fun onExtensionFileModified(file: File) { - NovelExtensionLoader.loadExtension(context, file).let { - if (it is NovelLoadResult.Success) { - registerUpdatedExtension(it.extension.withUpdateCheck()) - } - } - } } /** diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelLoadResult.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelLoadResult.kt new file mode 100644 index 00000000..59ca96a5 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelLoadResult.kt @@ -0,0 +1,7 @@ +package ani.dantotsu.parsers.novel + + +sealed class NovelLoadResult { + data class Success(val extension: NovelExtension.Installed) : NovelLoadResult() + data class Error(val error: Exception) : NovelLoadResult() +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/profile/ChartItem.kt b/app/src/main/java/ani/dantotsu/profile/ChartItem.kt index 98b2889e..c7d6be0c 100644 --- a/app/src/main/java/ani/dantotsu/profile/ChartItem.kt +++ b/app/src/main/java/ani/dantotsu/profile/ChartItem.kt @@ -15,7 +15,8 @@ import com.xwray.groupie.viewbinding.GroupieViewHolder class ChartItem( private val title: String, private val aaOptions: AAOptions, - private val activity: ProfileActivity): BindableItem() { + private val activity: ProfileActivity +) : BindableItem() { private lateinit var binding: ItemChartBinding override fun bind(viewBinding: ItemChartBinding, position: Int) { binding = viewBinding @@ -78,6 +79,7 @@ class ChartItem( viewHolder.setIsRecyclable(false) super.bind(viewHolder, position, payloads, onItemClickListener, onItemLongClickListener) } + override fun getViewType(): Int { return 0 } diff --git a/app/src/main/java/ani/dantotsu/profile/FollowActivity.kt b/app/src/main/java/ani/dantotsu/profile/FollowActivity.kt index 70368ea3..30d99b50 100644 --- a/app/src/main/java/ani/dantotsu/profile/FollowActivity.kt +++ b/app/src/main/java/ani/dantotsu/profile/FollowActivity.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class FollowActivity : AppCompatActivity(){ +class FollowActivity : AppCompatActivity() { private lateinit var binding: ActivityFollowBinding val adapter = GroupieAdapter() var users: List? = null @@ -37,7 +37,9 @@ class FollowActivity : AppCompatActivity(){ initActivity(this) binding = ActivityFollowBinding.inflate(layoutInflater) binding.listToolbar.updateLayoutParams { topMargin = statusBarHeight } - binding.listFrameLayout.updateLayoutParams { bottomMargin = navBarHeight } + binding.listFrameLayout.updateLayoutParams { + bottomMargin = navBarHeight + } setContentView(binding.root) val layoutType = PrefManager.getVal(PrefName.FollowerLayout) selected = getSelected(layoutType) @@ -54,7 +56,7 @@ class FollowActivity : AppCompatActivity(){ binding.listBack.setOnClickListener { finish() } val title = intent.getStringExtra("title") - val userID= intent.getIntExtra("userId", 0) + val userID = intent.getIntExtra("userId", 0) binding.listTitle.text = title lifecycleScope.launch(Dispatchers.IO) { @@ -93,9 +95,20 @@ class FollowActivity : AppCompatActivity(){ } users?.forEach { user -> if (getLayoutType(selected) == 0) { - adapter.add(FollowerItem(user.id, user.name ?: "Unknown", user.avatar?.medium, user.bannerImage ?: user.avatar?.medium ) { onUserClick(it) }) + adapter.add( + FollowerItem( + user.id, + user.name ?: "Unknown", + user.avatar?.medium, + user.bannerImage ?: user.avatar?.medium + ) { onUserClick(it) }) } else { - adapter.add(GridFollowerItem(user.id, user.name ?: "Unknown", user.avatar?.medium) { onUserClick(it) }) + adapter.add( + GridFollowerItem( + user.id, + user.name ?: "Unknown", + user.avatar?.medium + ) { onUserClick(it) }) } } } diff --git a/app/src/main/java/ani/dantotsu/profile/FollowerItem.kt b/app/src/main/java/ani/dantotsu/profile/FollowerItem.kt index 6312e3c5..9513c29e 100644 --- a/app/src/main/java/ani/dantotsu/profile/FollowerItem.kt +++ b/app/src/main/java/ani/dantotsu/profile/FollowerItem.kt @@ -14,7 +14,7 @@ class FollowerItem( private val avatar: String?, private val banner: String?, val clickCallback: (Int) -> Unit -): BindableItem() { +) : BindableItem() { private lateinit var binding: ItemFollowerBinding override fun bind(viewBinding: ItemFollowerBinding, position: Int) { diff --git a/app/src/main/java/ani/dantotsu/profile/GridFollowerItem.kt b/app/src/main/java/ani/dantotsu/profile/GridFollowerItem.kt index f6a61fa3..33d7e66b 100644 --- a/app/src/main/java/ani/dantotsu/profile/GridFollowerItem.kt +++ b/app/src/main/java/ani/dantotsu/profile/GridFollowerItem.kt @@ -6,12 +6,12 @@ import ani.dantotsu.databinding.ItemFollowerGridBinding import ani.dantotsu.loadImage import com.xwray.groupie.viewbinding.BindableItem -class GridFollowerItem ( +class GridFollowerItem( private val id: Int, private val name: String, private val avatar: String?, val clickCallback: (Int) -> Unit -): BindableItem() { +) : BindableItem() { private lateinit var binding: ItemFollowerGridBinding override fun bind(viewBinding: ItemFollowerGridBinding, position: Int) { diff --git a/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt b/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt index 571bc2a5..9e19a5f3 100644 --- a/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt +++ b/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt @@ -59,9 +59,11 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene screenWidth = resources.displayMetrics.widthPixels.toFloat() navBar = binding.profileNavBar val navBarRightMargin = if (resources.configuration.orientation == - Configuration.ORIENTATION_LANDSCAPE) navBarHeight else 0 + Configuration.ORIENTATION_LANDSCAPE + ) navBarHeight else 0 val navBarBottomMargin = if (resources.configuration.orientation == - Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight + Configuration.ORIENTATION_LANDSCAPE + ) 0 else navBarHeight navBar.updateLayoutParams { rightMargin = navBarRightMargin bottomMargin = navBarBottomMargin @@ -284,7 +286,7 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange val percentage = abs(i) * 100 / mMaxScrollSize - with (bindingProfileAppBar) { + with(bindingProfileAppBar) { profileUserAvatarContainer.visibility = if (profileUserAvatarContainer.scaleX == 0f) View.GONE else View.VISIBLE val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong() @@ -315,10 +317,12 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) val rightMargin = if (resources.configuration.orientation == - Configuration.ORIENTATION_LANDSCAPE) navBarHeight else 0 + Configuration.ORIENTATION_LANDSCAPE + ) navBarHeight else 0 val bottomMargin = if (resources.configuration.orientation == - Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight - val params : ViewGroup.MarginLayoutParams = + Configuration.ORIENTATION_LANDSCAPE + ) 0 else navBarHeight + val params: ViewGroup.MarginLayoutParams = navBar.layoutParams as ViewGroup.MarginLayoutParams params.updateMargins(right = rightMargin, bottom = bottomMargin) } diff --git a/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt b/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt index f7a04944..1d4b8f72 100644 --- a/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt +++ b/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt @@ -1,8 +1,6 @@ package ani.dantotsu.profile -import android.content.Intent import android.os.Bundle -import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -22,19 +20,16 @@ import ani.dantotsu.R import ani.dantotsu.connections.anilist.ProfileViewModel import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.databinding.FragmentProfileBinding -import ani.dantotsu.loadImage import ani.dantotsu.media.Author import ani.dantotsu.media.AuthorAdapter import ani.dantotsu.media.Character import ani.dantotsu.media.CharacterAdapter import ani.dantotsu.media.Media import ani.dantotsu.media.MediaAdaptor -import ani.dantotsu.media.user.ListActivity import ani.dantotsu.setBaseline import ani.dantotsu.setSlideIn import ani.dantotsu.setSlideUp import ani.dantotsu.util.AniMarkdown.Companion.getFullAniHTML -import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.util.system.getSerializableCompat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -137,7 +132,7 @@ class ProfileFragment : Fragment() { } user.favourites?.staff?.nodes?.forEach { i -> - favStaff.add(Author(i.id, i.name.full, i.image.large , "" )) + favStaff.add(Author(i.id, i.name.full, i.image.large, "")) } setFavPeople() @@ -159,7 +154,8 @@ class ProfileFragment : Fragment() { binding.profileFavStaffRecycler.layoutManager = LinearLayoutManager( activity, LinearLayoutManager.HORIZONTAL, false ) - binding.profileFavStaffRecycler.layoutAnimation = LayoutAnimationController(setSlideIn(), 0.25f) + binding.profileFavStaffRecycler.layoutAnimation = + LayoutAnimationController(setSlideIn(), 0.25f) } if (favCharacter.isEmpty()) { @@ -169,7 +165,8 @@ class ProfileFragment : Fragment() { binding.profileFavCharactersRecycler.layoutManager = LinearLayoutManager( activity, LinearLayoutManager.HORIZONTAL, false ) - binding.profileFavCharactersRecycler.layoutAnimation = LayoutAnimationController(setSlideIn(), 0.25f) + binding.profileFavCharactersRecycler.layoutAnimation = + LayoutAnimationController(setSlideIn(), 0.25f) } } @@ -189,7 +186,7 @@ class ProfileFragment : Fragment() { recyclerView.visibility = View.GONE if (it != null) { if (it.isNotEmpty()) { - recyclerView.adapter = MediaAdaptor(0, it, activity, fav=true) + recyclerView.adapter = MediaAdaptor(0, it, activity, fav = true) recyclerView.layoutManager = LinearLayoutManager( activity, LinearLayoutManager.HORIZONTAL, diff --git a/app/src/main/java/ani/dantotsu/profile/SingleStatActivity.kt b/app/src/main/java/ani/dantotsu/profile/SingleStatActivity.kt index 337d2c86..c038ec2c 100644 --- a/app/src/main/java/ani/dantotsu/profile/SingleStatActivity.kt +++ b/app/src/main/java/ani/dantotsu/profile/SingleStatActivity.kt @@ -10,8 +10,7 @@ import ani.dantotsu.themes.ThemeManager import ani.dantotsu.toast import com.github.aachartmodel.aainfographics.aachartcreator.AAOptions -class SingleStatActivity : AppCompatActivity() -{ +class SingleStatActivity : AppCompatActivity() { private lateinit var binding: ActivitySingleStatBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt b/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt index aaf0ed6f..52ba668b 100644 --- a/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt +++ b/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt @@ -58,10 +58,13 @@ class StatsFragment : binding.statisticList.adapter = adapter binding.statisticList.recycledViewPool.setMaxRecycledViews(0, 0) binding.statisticList.isNestedScrollingEnabled = true - binding.statisticList.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) + binding.statisticList.layoutManager = + LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) binding.statisticProgressBar.visibility = View.VISIBLE binding.compare.visibility = if (user.id == Anilist.userid) View.GONE else View.VISIBLE - binding.filterContainer.updateLayoutParams { topMargin = statusBarHeight } + binding.filterContainer.updateLayoutParams { + topMargin = statusBarHeight + } binding.sourceType.setAdapter( ArrayAdapter( diff --git a/app/src/main/java/ani/dantotsu/profile/User.kt b/app/src/main/java/ani/dantotsu/profile/User.kt index 15814f44..a16c3329 100644 --- a/app/src/main/java/ani/dantotsu/profile/User.kt +++ b/app/src/main/java/ani/dantotsu/profile/User.kt @@ -12,8 +12,8 @@ data class User( val status: String? = null, val score: Float? = null, val progress: Int? = null, - val totalEpisodes : Int? = null, - val nextAiringEpisode : Int? = null, + val totalEpisodes: Int? = null, + val nextAiringEpisode: Int? = null, ) : java.io.Serializable { companion object { private const val serialVersionUID: Long = 1 diff --git a/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt b/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt index 9b4d23a1..90363e76 100644 --- a/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt +++ b/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt @@ -5,13 +5,13 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView -import ani.dantotsu.blurImage import ani.dantotsu.databinding.ItemFollowerBinding import ani.dantotsu.loadImage import ani.dantotsu.setAnimation -class UsersAdapter(private val user: ArrayList) : RecyclerView.Adapter() { +class UsersAdapter(private val user: ArrayList) : + RecyclerView.Adapter() { inner class UsersViewHolder(val binding: ItemFollowerBinding) : RecyclerView.ViewHolder(binding.root) { diff --git a/app/src/main/java/ani/dantotsu/profile/UsersDialogFragment.kt b/app/src/main/java/ani/dantotsu/profile/UsersDialogFragment.kt index f9e17321..01284d1f 100644 --- a/app/src/main/java/ani/dantotsu/profile/UsersDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/profile/UsersDialogFragment.kt @@ -14,9 +14,10 @@ class UsersDialogFragment : BottomSheetDialogFragment() { private val binding get() = _binding!! private var userList = arrayListOf() - fun userList(user: ArrayList){ + fun userList(user: ArrayList) { userList = user } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -25,6 +26,7 @@ class UsersDialogFragment : BottomSheetDialogFragment() { _binding = BottomSheetUsersBinding.inflate(inflater, container, false) return binding.root } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/ani/dantotsu/profile/activity/ActivityItem.kt b/app/src/main/java/ani/dantotsu/profile/activity/ActivityItem.kt index 0f9ed865..983fdca8 100644 --- a/app/src/main/java/ani/dantotsu/profile/activity/ActivityItem.kt +++ b/app/src/main/java/ani/dantotsu/profile/activity/ActivityItem.kt @@ -1,6 +1,5 @@ package ani.dantotsu.profile.activity -import android.annotation.SuppressLint import android.view.View import androidx.core.content.ContextCompat import androidx.core.view.isVisible @@ -18,13 +17,8 @@ import ani.dantotsu.profile.UsersDialogFragment import ani.dantotsu.setAnimation import ani.dantotsu.snackString import ani.dantotsu.util.AniMarkdown.Companion.getBasicAniHTML -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.model.GlideUrl -import com.bumptech.glide.request.RequestOptions import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.viewbinding.BindableItem -import jp.wasabeef.glide.transformations.BlurTransformation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -108,11 +102,12 @@ class ActivityItem( } val context = binding.root.context val userList = arrayListOf() - activity.likes?.forEach{ i -> + activity.likes?.forEach { i -> userList.add(User(i.id, i.name.toString(), i.avatar?.medium, i.bannerImage)) } - binding.activityLike.setOnLongClickListener{ - UsersDialogFragment().apply { userList(userList) + binding.activityLike.setOnLongClickListener { + UsersDialogFragment().apply { + userList(userList) show(fragActivity.supportFragmentManager, "dialog") } true @@ -126,8 +121,10 @@ class ActivityItem( binding.activityContent.visibility = View.GONE binding.activityBannerContainer.visibility = View.VISIBLE binding.activityMediaName.text = activity.media?.title?.userPreferred - val activityText = "${activity.user!!.name} ${activity.status} ${activity.progress - ?: activity.media?.title?.userPreferred}" + val activityText = "${activity.user!!.name} ${activity.status} ${ + activity.progress + ?: activity.media?.title?.userPreferred + }" binding.activityText.text = activityText binding.activityCover.loadImage(cover) blurImage(binding.activityBannerImage, banner ?: cover) diff --git a/app/src/main/java/ani/dantotsu/profile/activity/ActivityItemBuilder.kt b/app/src/main/java/ani/dantotsu/profile/activity/ActivityItemBuilder.kt index ef50027e..3e2289a2 100644 --- a/app/src/main/java/ani/dantotsu/profile/activity/ActivityItemBuilder.kt +++ b/app/src/main/java/ani/dantotsu/profile/activity/ActivityItemBuilder.kt @@ -5,6 +5,7 @@ import ani.dantotsu.connections.anilist.api.NotificationType import java.text.SimpleDateFormat import java.util.Date import java.util.Locale + class ActivityItemBuilder { companion object { @@ -109,6 +110,7 @@ class ActivityItemBuilder { else -> "Just now" } } + 1L -> "1 day ago" in 2..6 -> "$daysDifference days ago" else -> SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(targetDate) diff --git a/app/src/main/java/ani/dantotsu/profile/activity/FeedActivity.kt b/app/src/main/java/ani/dantotsu/profile/activity/FeedActivity.kt index 42ee6973..df86fe92 100644 --- a/app/src/main/java/ani/dantotsu/profile/activity/FeedActivity.kt +++ b/app/src/main/java/ani/dantotsu/profile/activity/FeedActivity.kt @@ -31,7 +31,8 @@ class FeedActivity : AppCompatActivity() { setContentView(binding.root) navBar = binding.feedNavBar val navBarMargin = if (resources.configuration.orientation == - Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight + Configuration.ORIENTATION_LANDSCAPE + ) 0 else navBarHeight navBar.updateLayoutParams { bottomMargin = navBarMargin } val personalTab = navBar.createTab(R.drawable.ic_round_person_24, "Following") val globalTab = navBar.createTab(R.drawable.ic_globe_24, "Global") @@ -67,10 +68,12 @@ class FeedActivity : AppCompatActivity() { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight - val params : ViewGroup.MarginLayoutParams = + val margin = + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight + val params: ViewGroup.MarginLayoutParams = binding.feedViewPager.layoutParams as ViewGroup.MarginLayoutParams - val paramsNav : ViewGroup.MarginLayoutParams = navBar.layoutParams as ViewGroup.MarginLayoutParams + val paramsNav: ViewGroup.MarginLayoutParams = + navBar.layoutParams as ViewGroup.MarginLayoutParams params.updateMargins(bottom = margin) paramsNav.updateMargins(bottom = margin) } diff --git a/app/src/main/java/ani/dantotsu/profile/activity/FeedFragment.kt b/app/src/main/java/ani/dantotsu/profile/activity/FeedFragment.kt index be624b29..0da64157 100644 --- a/app/src/main/java/ani/dantotsu/profile/activity/FeedFragment.kt +++ b/app/src/main/java/ani/dantotsu/profile/activity/FeedFragment.kt @@ -19,7 +19,6 @@ import ani.dantotsu.databinding.FragmentFeedBinding import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.setBaseline -import ani.dantotsu.snackString import ani.dantotsu.util.Logger import com.xwray.groupie.GroupieAdapter import kotlinx.coroutines.Dispatchers @@ -57,7 +56,7 @@ class FeedFragment : Fragment() { val navBar = if (userId != null) { (activity as ProfileActivity).navBar - }else{ + } else { (activity as FeedActivity).navBar } binding.listRecyclerView.setBaseline(navBar) @@ -74,7 +73,7 @@ class FeedFragment : Fragment() { binding.root.requestLayout() val navBar = if (userId != null) { (activity as ProfileActivity).navBar - }else{ + } else { (activity as FeedActivity).navBar } binding.listRecyclerView.setBaseline(navBar) @@ -85,10 +84,17 @@ class FeedFragment : Fragment() { withContext(Dispatchers.Main) { res?.data?.page?.activities?.let { activities -> activityList = activities - val filtered = activityList.filterNot { //filter out messages that are not directed to the user - it.recipient?.id != null && it.recipient.id != Anilist.userid - } - adapter.update(filtered.map { ActivityItem(it, ::onActivityClick,requireActivity()) }) + val filtered = + activityList.filterNot { //filter out messages that are not directed to the user + it.recipient?.id != null && it.recipient.id != Anilist.userid + } + adapter.update(filtered.map { + ActivityItem( + it, + ::onActivityClick, + requireActivity() + ) + }) } binding.listProgressBar.visibility = ViewGroup.GONE val scrollView = binding.listRecyclerView @@ -134,7 +140,13 @@ class FeedFragment : Fragment() { val filtered = activities.filterNot { it.recipient?.id != null && it.recipient.id != Anilist.userid } - adapter.addAll(filtered.map { ActivityItem(it, ::onActivityClick,requireActivity()) }) + adapter.addAll(filtered.map { + ActivityItem( + it, + ::onActivityClick, + requireActivity() + ) + }) } binding.feedSwipeRefresh.isRefreshing = false onFinish() @@ -150,6 +162,7 @@ class FeedFragment : Fragment() { .putExtra("userId", id), null ) } + "MEDIA" -> { ContextCompat.startActivity( activity, Intent(activity, MediaDetailsActivity::class.java) diff --git a/app/src/main/java/ani/dantotsu/profile/activity/NotificationActivity.kt b/app/src/main/java/ani/dantotsu/profile/activity/NotificationActivity.kt index a8214004..a07ee3b3 100644 --- a/app/src/main/java/ani/dantotsu/profile/activity/NotificationActivity.kt +++ b/app/src/main/java/ani/dantotsu/profile/activity/NotificationActivity.kt @@ -22,7 +22,6 @@ import ani.dantotsu.notifications.comment.CommentStore import ani.dantotsu.profile.ProfileActivity 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.util.Logger diff --git a/app/src/main/java/ani/dantotsu/profile/activity/NotificationItem.kt b/app/src/main/java/ani/dantotsu/profile/activity/NotificationItem.kt index 0276e3f7..59edae2f 100644 --- a/app/src/main/java/ani/dantotsu/profile/activity/NotificationItem.kt +++ b/app/src/main/java/ani/dantotsu/profile/activity/NotificationItem.kt @@ -1,9 +1,7 @@ package ani.dantotsu.profile.activity -import android.util.TypedValue import android.view.View import android.view.ViewGroup -import androidx.core.view.updateLayoutParams import ani.dantotsu.R import ani.dantotsu.blurImage import ani.dantotsu.connections.anilist.api.Notification @@ -60,7 +58,8 @@ class NotificationItem( } binding.notificationBannerImage.layoutParams.height = userHeight binding.notificationGradiant.layoutParams.height = userHeight - (binding.notificationTextContainer.layoutParams as ViewGroup.MarginLayoutParams).marginStart = userHeight + (binding.notificationTextContainer.layoutParams as ViewGroup.MarginLayoutParams).marginStart = + userHeight } else { binding.notificationCover.visibility = View.VISIBLE binding.notificationCoverUser.visibility = View.VISIBLE @@ -68,7 +67,8 @@ class NotificationItem( binding.notificationCover.loadImage(notification.media?.coverImage?.large) binding.notificationBannerImage.layoutParams.height = defaultHeight binding.notificationGradiant.layoutParams.height = defaultHeight - (binding.notificationTextContainer.layoutParams as ViewGroup.MarginLayoutParams).marginStart = textMarginStart + (binding.notificationTextContainer.layoutParams as ViewGroup.MarginLayoutParams).marginStart = + textMarginStart } } @@ -308,7 +308,9 @@ class NotificationItem( if (notification.commentId != null && notification.mediaId != null) { binding.notificationBannerImage.setOnClickListener { clickCallback( - notification.mediaId, notification.commentId, NotificationClickType.COMMENT + notification.mediaId, + notification.commentId, + NotificationClickType.COMMENT ) } } diff --git a/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt index fe7b81d2..3f07c7c0 100644 --- a/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt @@ -6,20 +6,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.app.NotificationCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import ani.dantotsu.R -import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.databinding.FragmentExtensionsBinding import ani.dantotsu.settings.paging.AnimeExtensionAdapter import ani.dantotsu.settings.paging.AnimeExtensionsViewModel import ani.dantotsu.settings.paging.AnimeExtensionsViewModelFactory import ani.dantotsu.settings.paging.OnAnimeInstallClickListener -import ani.dantotsu.snackString -import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import kotlinx.coroutines.flow.collectLatest @@ -80,48 +75,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/DiscordDialogFragment.kt b/app/src/main/java/ani/dantotsu/settings/DiscordDialogFragment.kt index 1c546b6f..5254dcd2 100644 --- a/app/src/main/java/ani/dantotsu/settings/DiscordDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/DiscordDialogFragment.kt @@ -10,7 +10,7 @@ import ani.dantotsu.databinding.BottomSheetDiscordRpcBinding import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName -class DiscordDialogFragment: BottomSheetDialogFragment() { +class DiscordDialogFragment : BottomSheetDialogFragment() { private var _binding: BottomSheetDiscordRpcBinding? = null private val binding get() = _binding!! @@ -22,11 +22,12 @@ class DiscordDialogFragment: BottomSheetDialogFragment() { _binding = BottomSheetDiscordRpcBinding.inflate(inflater, container, false) return binding.root } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) when (PrefManager.getCustomVal("discord_mode", "dantotsu")) { - "nothing" -> binding.radioNothing.isChecked= true + "nothing" -> binding.radioNothing.isChecked = true "dantotsu" -> binding.radioDantotsu.isChecked = true "anilist" -> binding.radioAnilist.isChecked = true else -> binding.radioAnilist.isChecked = true @@ -35,7 +36,8 @@ class DiscordDialogFragment: BottomSheetDialogFragment() { binding.showIcon.setOnCheckedChangeListener { _, isChecked -> PrefManager.setVal(PrefName.ShowAniListIcon, isChecked) } - binding.anilistLinkPreview.text = getString(R.string.anilist_link, PrefManager.getVal(PrefName.AnilistUserName)) + binding.anilistLinkPreview.text = + getString(R.string.anilist_link, PrefManager.getVal(PrefName.AnilistUserName)) binding.radioGroup.setOnCheckedChangeListener { _, checkedId -> val mode = when (checkedId) { diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt index fbae5b70..0125c3df 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 @@ -215,9 +213,17 @@ class ExtensionsActivity : AppCompatActivity() { private fun getSavedRepositories(repoInventory: ViewGroup, type: MediaType) { repoInventory.removeAllViews() val prefName: PrefName? = when (type) { - MediaType.ANIME -> { PrefName.AnimeExtensionRepos } - MediaType.MANGA -> { PrefName.MangaExtensionRepos } - else -> { null } + MediaType.ANIME -> { + PrefName.AnimeExtensionRepos + } + + MediaType.MANGA -> { + PrefName.MangaExtensionRepos + } + + else -> { + null + } } prefName?.let { repoList -> PrefManager.getVal>(repoList).forEach { item -> @@ -235,9 +241,15 @@ class ExtensionsActivity : AppCompatActivity() { repoInventory.removeView(view.root) CoroutineScope(Dispatchers.IO).launch { when (type) { - MediaType.ANIME -> { animeExtensionManager.findAvailableExtensions() } - MediaType.MANGA -> { mangaExtensionManager.findAvailableExtensions() } - else -> { } + MediaType.ANIME -> { + animeExtensionManager.findAvailableExtensions() + } + + MediaType.MANGA -> { + mangaExtensionManager.findAvailableExtensions() + } + + else -> {} } } dialog.dismiss() @@ -276,9 +288,17 @@ class ExtensionsActivity : AppCompatActivity() { private fun generateRepositoryButton(type: MediaType) { val hintResource: Int? = when (type) { - MediaType.ANIME -> { R.string.anime_add_repository } - MediaType.MANGA -> { R.string.manga_add_repository } - else -> { null } + MediaType.ANIME -> { + R.string.anime_add_repository + } + + MediaType.MANGA -> { + R.string.manga_add_repository + } + + else -> { + null + } } hintResource?.let { res -> binding.openSettingsButton.setOnClickListener { 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 00000000..39b91b3e --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/InstallerSteps.kt @@ -0,0 +1,57 @@ +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 c5118096..d198793f 100644 --- a/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt @@ -6,20 +6,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.app.NotificationCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import ani.dantotsu.R -import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.databinding.FragmentExtensionsBinding import ani.dantotsu.settings.paging.MangaExtensionAdapter import ani.dantotsu.settings.paging.MangaExtensionsViewModel import ani.dantotsu.settings.paging.MangaExtensionsViewModelFactory import ani.dantotsu.settings.paging.OnMangaInstallClickListener -import ani.dantotsu.snackString -import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import kotlinx.coroutines.flow.collectLatest @@ -81,48 +76,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/PlayerSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt index dd6f0c71..9006228a 100644 --- a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt @@ -24,7 +24,6 @@ import ani.dantotsu.others.getSerialized import ani.dantotsu.parsers.Subtitle import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName -import ani.dantotsu.settings.saving.internal.Pref import ani.dantotsu.snackString import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager @@ -40,9 +39,10 @@ class PlayerSettingsActivity : AppCompatActivity() { var media: Media? = null var subtitle: Subtitle? = null - private val Int.toSP get() = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_SP, this.toFloat(), Resources.getSystem().displayMetrics - ) + private val Int.toSP + get() = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, this.toFloat(), Resources.getSystem().displayMetrics + ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -151,8 +151,10 @@ class PlayerSettingsActivity : AppCompatActivity() { binding.playerSettingsTimeStampsAutoHide.isEnabled = isChecked } - binding.playerSettingsTimeStampsAutoHide.isChecked = PrefManager.getVal(PrefName.AutoHideTimeStamps) - binding.playerSettingsTimeStampsAutoHide.isEnabled = binding.playerSettingsShowTimeStamp.isChecked + binding.playerSettingsTimeStampsAutoHide.isChecked = + PrefManager.getVal(PrefName.AutoHideTimeStamps) + binding.playerSettingsTimeStampsAutoHide.isEnabled = + binding.playerSettingsShowTimeStamp.isChecked binding.playerSettingsTimeStampsAutoHide.setOnCheckedChangeListener { _, isChecked -> PrefManager.setVal(PrefName.AutoHideTimeStamps, isChecked) } @@ -484,10 +486,11 @@ class PlayerSettingsActivity : AppCompatActivity() { updateSubPreview() } } - binding.subtitleTest.addOnChangeListener(object: Xpandable.OnChangeListener { + binding.subtitleTest.addOnChangeListener(object : Xpandable.OnChangeListener { override fun onExpand() { updateSubPreview() } + override fun onRetract() {} }) updateSubPreview() @@ -496,24 +499,26 @@ class PlayerSettingsActivity : AppCompatActivity() { private fun updateSubPreview() { binding.subtitleTestWindow.run { alpha = PrefManager.getVal(PrefName.SubAlpha) - setBackgroundColor(when (PrefManager.getVal(PrefName.SubWindow)) { - 0 -> Color.TRANSPARENT - 1 -> Color.BLACK - 2 -> Color.DKGRAY - 3 -> Color.GRAY - 4 -> Color.LTGRAY - 5 -> Color.WHITE - 6 -> Color.RED - 7 -> Color.YELLOW - 8 -> Color.GREEN - 9 -> Color.CYAN - 10 -> Color.BLUE - 11 -> Color.MAGENTA - else -> Color.TRANSPARENT - }) + setBackgroundColor( + when (PrefManager.getVal(PrefName.SubWindow)) { + 0 -> Color.TRANSPARENT + 1 -> Color.BLACK + 2 -> Color.DKGRAY + 3 -> Color.GRAY + 4 -> Color.LTGRAY + 5 -> Color.WHITE + 6 -> Color.RED + 7 -> Color.YELLOW + 8 -> Color.GREEN + 9 -> Color.CYAN + 10 -> Color.BLUE + 11 -> Color.MAGENTA + else -> Color.TRANSPARENT + } + ) } binding.subtitleTestText.run { - textSize = PrefManager.getVal(PrefName.FontSize).toSP + textSize = PrefManager.getVal(PrefName.FontSize).toSP typeface = when (PrefManager.getVal(PrefName.Font)) { 0 -> ResourcesCompat.getFont(this.context, R.font.poppins_semi_bold) 1 -> ResourcesCompat.getFont(this.context, R.font.poppins_bold) @@ -524,36 +529,40 @@ class PlayerSettingsActivity : AppCompatActivity() { 6 -> ResourcesCompat.getFont(this.context, R.font.blocky) else -> ResourcesCompat.getFont(this.context, R.font.poppins_semi_bold) } - setTextColor(when (PrefManager.getVal(PrefName.PrimaryColor)) { - 0 -> Color.BLACK - 1 -> Color.DKGRAY - 2 -> Color.GRAY - 3 -> Color.LTGRAY - 4 -> Color.WHITE - 5 -> Color.RED - 6 -> Color.YELLOW - 7 -> Color.GREEN - 8 -> Color.CYAN - 9 -> Color.BLUE - 10 -> Color.MAGENTA - 11 -> Color.TRANSPARENT - else -> Color.WHITE - }) - setBackgroundColor(when (PrefManager.getVal(PrefName.SubBackground)) { - 0 -> Color.TRANSPARENT - 1 -> Color.BLACK - 2 -> Color.DKGRAY - 3 -> Color.GRAY - 4 -> Color.LTGRAY - 5 -> Color.WHITE - 6 -> Color.RED - 7 -> Color.YELLOW - 8 -> Color.GREEN - 9 -> Color.CYAN - 10 -> Color.BLUE - 11 -> Color.MAGENTA - else -> Color.TRANSPARENT - }) + setTextColor( + when (PrefManager.getVal(PrefName.PrimaryColor)) { + 0 -> Color.BLACK + 1 -> Color.DKGRAY + 2 -> Color.GRAY + 3 -> Color.LTGRAY + 4 -> Color.WHITE + 5 -> Color.RED + 6 -> Color.YELLOW + 7 -> Color.GREEN + 8 -> Color.CYAN + 9 -> Color.BLUE + 10 -> Color.MAGENTA + 11 -> Color.TRANSPARENT + else -> Color.WHITE + } + ) + setBackgroundColor( + when (PrefManager.getVal(PrefName.SubBackground)) { + 0 -> Color.TRANSPARENT + 1 -> Color.BLACK + 2 -> Color.DKGRAY + 3 -> Color.GRAY + 4 -> Color.LTGRAY + 5 -> Color.WHITE + 6 -> Color.RED + 7 -> Color.YELLOW + 8 -> Color.GREEN + 9 -> Color.CYAN + 10 -> Color.BLUE + 11 -> Color.MAGENTA + else -> Color.TRANSPARENT + } + ) } } } diff --git a/app/src/main/java/ani/dantotsu/settings/Settings.kt b/app/src/main/java/ani/dantotsu/settings/Settings.kt index 86b8c6f0..a8d48e6c 100644 --- a/app/src/main/java/ani/dantotsu/settings/Settings.kt +++ b/app/src/main/java/ani/dantotsu/settings/Settings.kt @@ -2,17 +2,18 @@ package ani.dantotsu.settings import ani.dantotsu.databinding.ItemSettingsBinding import ani.dantotsu.databinding.ItemSettingsSwitchBinding -import java.lang.reflect.Array data class Settings( val type: Int, - val name : String, + val name: String, val desc: String, - val icon : Int, + val icon: Int, val onClick: ((ItemSettingsBinding) -> Unit)? = null, val onLongClick: (() -> Unit)? = null, - var isChecked : Boolean = false, - val switch: ((isChecked:Boolean , view: ItemSettingsSwitchBinding ) -> 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 + val isActivity: Boolean = false, + var isChecked: Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsAboutActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsAboutActivity.kt index 2a3617fa..d3183be4 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 @@ -18,6 +19,7 @@ import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager +import ani.dantotsu.util.Logger class SettingsAboutActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsAboutBinding @@ -42,7 +44,7 @@ class SettingsAboutActivity : AppCompatActivity() { Settings( type = 1, name = getString(R.string.faq), - desc = getString(R.string.faq), + desc = getString(R.string.faq_desc), icon = R.drawable.ic_round_help_24, onClick = { startActivity(Intent(context, FAQActivity::class.java)) @@ -52,7 +54,7 @@ class SettingsAboutActivity : AppCompatActivity() { Settings( type = 2, name = getString(R.string.check_app_updates), - desc = getString(R.string.check_app_updates), + desc = getString(R.string.check_app_updates_desc), icon = R.drawable.ic_round_new_releases_24, isChecked = PrefManager.getVal(PrefName.CheckUpdate), switch = { isChecked, _ -> @@ -63,7 +65,7 @@ class SettingsAboutActivity : AppCompatActivity() { Settings( type = 2, name = getString(R.string.share_username_in_crash_reports), - desc = getString(R.string.share_username_in_crash_reports), + desc = getString(R.string.share_username_in_crash_reports_desc), icon = R.drawable.ic_round_search_24, isChecked = PrefManager.getVal(PrefName.SharedUserID), switch = { isChecked, _ -> @@ -79,13 +81,21 @@ class SettingsAboutActivity : AppCompatActivity() { isChecked = PrefManager.getVal(PrefName.LogToFile), switch = { isChecked, _ -> PrefManager.setVal(PrefName.LogToFile, isChecked) - restartApp(binding.root) + restartApp() }, + attachToSwitch = { + it.settingsExtraIcon.visibility = View.VISIBLE + it.settingsExtraIcon.setImageResource(R.drawable.ic_round_share_24) + it.settingsExtraIcon.setOnClickListener { + Logger.shareLog(context) + } + + } ), Settings( type = 1, name = getString(R.string.devs), - desc= getString(R.string.devs), + desc = getString(R.string.devs_desc), icon = R.drawable.ic_round_accessible_forward_24, onClick = { DevelopersDialogFragment().show(supportFragmentManager, "dialog") @@ -94,7 +104,7 @@ class SettingsAboutActivity : AppCompatActivity() { Settings( type = 1, name = getString(R.string.forks), - desc = getString(R.string.forks), + desc = getString(R.string.forks_desc), icon = R.drawable.ic_round_restaurant_24, onClick = { ForksDialogFragment().show(supportFragmentManager, "dialog") @@ -103,7 +113,7 @@ class SettingsAboutActivity : AppCompatActivity() { Settings( type = 1, name = getString(R.string.disclaimer), - desc = getString(R.string.disclaimer), + desc = getString(R.string.disclaimer_desc), icon = R.drawable.ic_round_info_24, onClick = { val text = TextView(context) @@ -121,7 +131,8 @@ class SettingsAboutActivity : AppCompatActivity() { ), ) ) - binding.settingsRecyclerView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + binding.settingsRecyclerView.layoutManager = + LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) } } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsAccountActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsAccountActivity.kt index fd3300a4..362afcbb 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsAccountActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsAccountActivity.kt @@ -13,7 +13,6 @@ import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.discord.Discord import ani.dantotsu.connections.mal.MAL -import ani.dantotsu.databinding.ActivitySettingsAboutBinding import ani.dantotsu.databinding.ActivitySettingsAccountsBinding import ani.dantotsu.initActivity import ani.dantotsu.loadImage @@ -33,6 +32,7 @@ class SettingsAccountActivity : AppCompatActivity() { private val restartMainActivity = object : OnBackPressedCallback(false) { override fun handleOnBackPressed() = startMainActivity(this@SettingsAccountActivity) } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ThemeManager(this).applyTheme() diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt index 7185b04e..9f7bc7f8 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt @@ -11,7 +11,7 @@ import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.view.ViewGroup import android.widget.TextView -import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope @@ -28,6 +28,7 @@ import ani.dantotsu.others.AppUpdater import ani.dantotsu.others.CustomBottomDialog import ani.dantotsu.pop import ani.dantotsu.setSafeOnClickListener +import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.snackString import ani.dantotsu.startMainActivity import ani.dantotsu.statusBarHeight @@ -40,9 +41,6 @@ import kotlin.random.Random class SettingsActivity : AppCompatActivity() { - private val restartMainActivity = object : OnBackPressedCallback(false) { - override fun handleOnBackPressed() = startMainActivity(this@SettingsActivity) - } lateinit var binding: ActivitySettingsBinding private var cursedCounter = 0 @@ -70,7 +68,14 @@ class SettingsActivity : AppCompatActivity() { bottomMargin = navBarHeight } - onBackPressedDispatcher.addCallback(context, restartMainActivity) + onBackPressedDispatcher.addCallback(context) { + if (PrefManager.getCustomVal("reload", false)) { + startMainActivity(context) + PrefManager.setCustomVal("reload", false) + } else { + finish() + } + } settingsBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() @@ -138,6 +143,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), @@ -262,4 +277,9 @@ class SettingsActivity : AppCompatActivity() { ?: "Unknown Architecture" } } + + override fun onResume() { + ThemeManager(this).applyTheme() + super.onResume() + } } diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsAdapter.kt b/app/src/main/java/ani/dantotsu/settings/SettingsAdapter.kt index add55912..1be2557c 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsAdapter.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsAdapter.kt @@ -12,10 +12,10 @@ import ani.dantotsu.setAnimation class SettingsAdapter(private val settings: ArrayList) : RecyclerView.Adapter() { inner class SettingsViewHolder(val binding: ItemSettingsBinding) : - RecyclerView.ViewHolder(binding.root) {} + RecyclerView.ViewHolder(binding.root) inner class SettingsSwitchViewHolder(val binding: ItemSettingsSwitchBinding) : - RecyclerView.ViewHolder(binding.root) {} + RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { @@ -63,6 +63,8 @@ class SettingsAdapter(private val settings: ArrayList) : b.settingsLayout.visibility = if (settings.isVisible) View.VISIBLE else View.GONE b.settingsIconRight.visibility = if (settings.isActivity) View.VISIBLE else View.GONE + b.attachView.visibility = if (settings.attach != null) View.VISIBLE else View.GONE + settings.attach?.invoke(b) } 2 -> { @@ -80,11 +82,12 @@ class SettingsAdapter(private val settings: ArrayList) : b.settingsButton.setOnCheckedChangeListener { _, isChecked -> settings.switch?.invoke(isChecked, b) } - b.settingsLayout.setOnLongClickListener() { + b.settingsLayout.setOnLongClickListener { settings.onLongClick?.invoke() 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 00000000..fbb3e1bd --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/SettingsAddonActivity.kt @@ -0,0 +1,255 @@ +package ani.dantotsu.settings + +import android.content.Context +import android.os.Bundle +import android.view.ViewGroup +import android.view.animation.LinearInterpolator +import androidx.appcompat.app.AppCompatActivity +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.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.util.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +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() + + 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.ic_download_24, + 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 + } 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.ic_round_magnet_24, + 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_desc), + icon = R.drawable.ic_round_dns_24, + isChecked = PrefManager.getVal(PrefName.TorrentEnabled), + switch = { isChecked, it -> + if (isChecked && !torrentAddonManager.isAvailable()) { + snackString(getString(R.string.install_torrent_addon)) + it.settingsButton.isChecked = false + PrefManager.setVal(PrefName.TorrentEnabled, false) + return@Settings + } + 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/SettingsAnimeActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsAnimeActivity.kt index 7e853ed5..2a5c6aee 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsAnimeActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsAnimeActivity.kt @@ -22,7 +22,7 @@ import ani.dantotsu.themes.ThemeManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class SettingsAnimeActivity: AppCompatActivity(){ +class SettingsAnimeActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsAnimeBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -44,7 +44,7 @@ class SettingsAnimeActivity: AppCompatActivity(){ Settings( type = 1, name = getString(R.string.player_settings), - desc = getString(R.string.player_settings), + desc = getString(R.string.player_settings_desc), icon = R.drawable.ic_round_video_settings_24, onClick = { startActivity(Intent(context, PlayerSettingsActivity::class.java)) @@ -54,12 +54,17 @@ class SettingsAnimeActivity: AppCompatActivity(){ Settings( type = 1, name = getString(R.string.purge_anime_downloads), - desc = getString(R.string.purge_anime_downloads), + desc = getString(R.string.purge_anime_downloads_desc), icon = R.drawable.ic_round_delete_24, onClick = { val dialog = AlertDialog.Builder(context, R.style.MyPopup) .setTitle(R.string.purge_anime_downloads) - .setMessage(getString(R.string.purge_confirm, getString(R.string.anime))) + .setMessage( + getString( + R.string.purge_confirm, + getString(R.string.anime) + ) + ) .setPositiveButton(R.string.yes) { dialog, _ -> val downloadsManager = Injekt.get() downloadsManager.purgeDownloads(MediaType.ANIME) @@ -75,7 +80,7 @@ class SettingsAnimeActivity: AppCompatActivity(){ Settings( type = 2, name = getString(R.string.prefer_dub), - desc = getString(R.string.prefer_dub), + desc = getString(R.string.prefer_dub_desc), icon = R.drawable.ic_round_audiotrack_24, isChecked = PrefManager.getVal(PrefName.SettingsPreferDub), switch = { isChecked, _ -> @@ -85,7 +90,7 @@ class SettingsAnimeActivity: AppCompatActivity(){ Settings( type = 2, name = getString(R.string.show_yt), - desc = getString(R.string.show_yt), + desc = getString(R.string.show_yt_desc), icon = R.drawable.ic_round_play_circle_24, isChecked = PrefManager.getVal(PrefName.ShowYtButton), switch = { isChecked, _ -> @@ -95,12 +100,12 @@ class SettingsAnimeActivity: AppCompatActivity(){ Settings( type = 2, name = getString(R.string.include_list), - desc = getString(R.string.include_list), + desc = getString(R.string.include_list_anime_desc), icon = R.drawable.view_list_24, isChecked = PrefManager.getVal(PrefName.IncludeAnimeList), switch = { isChecked, _ -> PrefManager.setVal(PrefName.IncludeAnimeList, isChecked) - restartApp(binding.root) + restartApp() } ), ) diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt index 5e1fc7ae..4f8b1558 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsCommonActivity.kt @@ -18,6 +18,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.databinding.ActivitySettingsCommonBinding +import ani.dantotsu.databinding.DialogUserAgentBinding import ani.dantotsu.download.DownloadsManager import ani.dantotsu.initActivity import ani.dantotsu.navBarHeight @@ -33,7 +34,6 @@ import ani.dantotsu.themes.ThemeManager import ani.dantotsu.toast import ani.dantotsu.util.LauncherWrapper import ani.dantotsu.util.StoragePermissions -import com.google.android.material.textfield.TextInputEditText import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -41,9 +41,10 @@ import kotlinx.coroutines.launch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class SettingsCommonActivity: AppCompatActivity(){ +class SettingsCommonActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsCommonBinding private lateinit var launcher: LauncherWrapper + @OptIn(DelicateCoroutinesApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -73,14 +74,14 @@ class SettingsCommonActivity: AppCompatActivity(){ toast(getString(R.string.incorrect_password)) return@passwordAlertDialog } - if (PreferencePackager.unpack(decryptedJson)) restartApp(binding.root) + if (PreferencePackager.unpack(decryptedJson)) restartApp() } else { toast(getString(R.string.password_cannot_be_empty)) } } } else if (name.endsWith(".ani")) { val decryptedJson = jsonString.toString(Charsets.UTF_8) - if (PreferencePackager.unpack(decryptedJson)) restartApp(binding.root) + if (PreferencePackager.unpack(decryptedJson)) restartApp() } else { toast(getString(R.string.unknown_file_type)) } @@ -127,7 +128,7 @@ class SettingsCommonActivity: AppCompatActivity(){ settingsExtensionDns.setOnItemClickListener { _, _, i, _ -> PrefManager.setVal(PrefName.DohProvider, i) settingsExtensionDns.clearFocus() - restartApp(binding.root) + restartApp() } settingsRecyclerView.adapter = SettingsAdapter( @@ -135,22 +136,28 @@ class SettingsCommonActivity: AppCompatActivity(){ Settings( type = 1, name = getString(R.string.ui_settings), - desc = getString(R.string.ui_settings), + desc = getString(R.string.ui_settings_desc), icon = R.drawable.ic_round_auto_awesome_24, onClick = { - startActivity(Intent(context, UserInterfaceSettingsActivity::class.java)) + startActivity( + Intent( + context, + UserInterfaceSettingsActivity::class.java + ) + ) }, isActivity = true ), Settings( type = 1, name = getString(R.string.download_manager_select), - desc = getString(R.string.download_manager_select), + desc = getString(R.string.download_manager_select_desc), icon = R.drawable.ic_download_24, onClick = { val managers = arrayOf("Default", "1DM", "ADM") val downloadManagerDialog = - AlertDialog.Builder(context, R.style.MyPopup).setTitle(R.string.download_manager) + AlertDialog.Builder(context, R.style.MyPopup) + .setTitle(R.string.download_manager) var downloadManager: Int = PrefManager.getVal(PrefName.DownloadManager) val dialog = downloadManagerDialog.setSingleChoiceItems( managers, downloadManager @@ -165,7 +172,7 @@ class SettingsCommonActivity: AppCompatActivity(){ Settings( type = 1, name = getString(R.string.backup_restore), - desc = getString(R.string.backup_restore), + desc = getString(R.string.backup_restore_desc), icon = R.drawable.backup_restore, onClick = { StoragePermissions.downloadsPermission(context) @@ -220,7 +227,7 @@ class SettingsCommonActivity: AppCompatActivity(){ Settings( type = 1, name = getString(R.string.change_download_location), - desc = getString(R.string.change_download_location), + desc = getString(R.string.change_download_location_desc), icon = R.drawable.ic_round_source_24, onClick = { val dialog = AlertDialog.Builder(context, R.style.MyPopup) @@ -231,7 +238,8 @@ class SettingsCommonActivity: AppCompatActivity(){ launcher.registerForCallback { success -> if (success) { toast(getString(R.string.please_wait)) - val newUri = PrefManager.getVal(PrefName.DownloadsDir) + val newUri = + PrefManager.getVal(PrefName.DownloadsDir) GlobalScope.launch(Dispatchers.IO) { Injekt.get().moveDownloadsDir( context, Uri.parse(oldUri), Uri.parse(newUri) @@ -259,42 +267,42 @@ class SettingsCommonActivity: AppCompatActivity(){ Settings( type = 2, name = getString(R.string.always_continue_content), - desc = getString(R.string.always_continue_content), + desc = getString(R.string.always_continue_content_desc), icon = R.drawable.ic_round_delete_24, isChecked = PrefManager.getVal(PrefName.ContinueMedia), - switch = {isChecked, _ -> + switch = { isChecked, _ -> PrefManager.setVal(PrefName.ContinueMedia, isChecked) } ), Settings( type = 2, name = getString(R.string.search_source_list), - desc = getString(R.string.search_source_list), + desc = getString(R.string.search_source_list_desc), icon = R.drawable.ic_round_search_sources_24, isChecked = PrefManager.getVal(PrefName.SearchSources), - switch = {isChecked, _ -> + switch = { isChecked, _ -> PrefManager.setVal(PrefName.SearchSources, isChecked) } ), Settings( type = 2, name = getString(R.string.recentlyListOnly), - desc = getString(R.string.recentlyListOnly), + desc = getString(R.string.recentlyListOnly_desc), icon = R.drawable.ic_round_new_releases_24, isChecked = PrefManager.getVal(PrefName.RecentlyListOnly), - switch = {isChecked, _ -> + switch = { isChecked, _ -> PrefManager.setVal(PrefName.RecentlyListOnly, isChecked) } ), Settings( type = 2, name = getString(R.string.adult_only_content), - desc = getString(R.string.adult_only_content), + desc = getString(R.string.adult_only_content_desc), icon = R.drawable.ic_round_nsfw_24, isChecked = PrefManager.getVal(PrefName.AdultOnly), - switch = {isChecked, _ -> + switch = { isChecked, _ -> PrefManager.setVal(PrefName.AdultOnly, isChecked) - restartApp(binding.root) + restartApp() }, isVisible = Anilist.adult @@ -334,18 +342,20 @@ class SettingsCommonActivity: AppCompatActivity(){ } } + private fun passwordAlertDialog(isExporting: Boolean, callback: (CharArray?) -> Unit) { val password = CharArray(16).apply { fill('0') } // Inflate the dialog layout - val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_user_agent, null) - val box = dialogView.findViewById(R.id.userAgentTextBox) - box?.hint = getString(R.string.password) - box?.setSingleLine() + val dialogView = DialogUserAgentBinding.inflate(layoutInflater) + val box = dialogView.userAgentTextBox + box.hint = getString(R.string.password) + box.setSingleLine() val dialog = AlertDialog.Builder(this, R.style.MyPopup).setTitle(getString(R.string.enter_password)) - .setView(dialogView).setPositiveButton(R.string.ok, null) + .setView(dialogView.root) + .setPositiveButton(R.string.ok, null) .setNegativeButton(R.string.cancel) { dialog, _ -> password.fill('0') dialog.dismiss() @@ -353,8 +363,8 @@ class SettingsCommonActivity: AppCompatActivity(){ }.create() fun handleOkAction() { - val editText = dialog.findViewById(R.id.userAgentTextBox) - if (editText?.text?.isNotBlank() == true) { + val editText = dialogView.userAgentTextBox + if (editText.text?.isNotBlank() == true) { editText.text?.toString()?.trim()?.toCharArray(password) dialog.dismiss() callback(password) @@ -362,7 +372,7 @@ class SettingsCommonActivity: AppCompatActivity(){ toast(getString(R.string.password_cannot_be_empty)) } } - box?.setOnEditorActionListener { _, actionId, _ -> + box.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { handleOkAction() true @@ -370,9 +380,8 @@ class SettingsCommonActivity: AppCompatActivity(){ false } } - val subtitleTextView = dialogView.findViewById(R.id.subtitle) - subtitleTextView?.visibility = View.VISIBLE - if (!isExporting) subtitleTextView?.text = + dialogView.subtitle.visibility = View.VISIBLE + if (!isExporting) dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file) diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt b/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt index 535b7273..078c179e 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt @@ -74,15 +74,15 @@ class SettingsDialogFragment : BottomSheetDialogFragment() { binding.settingsLogin.setText(R.string.logout) binding.settingsLogin.setOnClickListener { val alertDialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) - .setTitle("Logout") - .setMessage("Are you sure you want to logout?") - .setPositiveButton("Yes") { _, _ -> - Anilist.removeSavedToken() - dismiss() - startMainActivity(requireActivity()) - } - .setNegativeButton("No") { _, _ -> } - .create() + .setTitle("Logout") + .setMessage("Are you sure you want to logout?") + .setPositiveButton("Yes") { _, _ -> + Anilist.removeSavedToken() + dismiss() + startMainActivity(requireActivity()) + } + .setNegativeButton("No") { _, _ -> } + .create() alertDialog.window?.setDimAmount(0.8f) alertDialog.show() } @@ -98,7 +98,7 @@ class SettingsDialogFragment : BottomSheetDialogFragment() { } binding.settingsNotificationCount.isVisible = Anilist.unreadNotificationCount > 0 binding.settingsNotificationCount.text = Anilist.unreadNotificationCount.toString() - binding.settingsUserAvatar.setOnClickListener{ + binding.settingsUserAvatar.setOnClickListener { ContextCompat.startActivity( requireContext(), Intent(requireContext(), ProfileActivity::class.java) .putExtra("userId", Anilist.userid), null diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt index 8bb190e0..1f38a491 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsExtensionsActivity.kt @@ -24,7 +24,6 @@ import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager -import com.google.android.material.textfield.TextInputEditText import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager @@ -35,7 +34,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy -class SettingsExtensionsActivity: AppCompatActivity() { +class SettingsExtensionsActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsExtensionsBinding private val extensionInstaller = Injekt.get().extensionInstaller() private val animeExtensionManager: AnimeExtensionManager by injectLazy() @@ -48,7 +47,13 @@ class SettingsExtensionsActivity: AppCompatActivity() { binding = ActivitySettingsExtensionsBinding.inflate(layoutInflater) setContentView(binding.root) binding.apply { - + settingsExtensionsLayout.updateLayoutParams { + topMargin = statusBarHeight + bottomMargin = navBarHeight + } + extensionSettingsBack.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } fun setExtensionOutput(repoInventory: ViewGroup, type: MediaType) { repoInventory.removeAllViews() val prefName: PrefName? = when (type) { @@ -70,7 +75,7 @@ class SettingsExtensionsActivity: AppCompatActivity() { LayoutInflater.from(repoInventory.context), repoInventory, true ) view.repositoryItem.text = - item.removePrefix("https://raw.githubusercontent.com") + item.removePrefix("https://raw.githubusercontent.com/") view.repositoryItem.setOnClickListener { AlertDialog.Builder(context, R.style.MyPopup) .setTitle(R.string.rem_repository).setMessage(item) @@ -107,7 +112,7 @@ class SettingsExtensionsActivity: AppCompatActivity() { } } - fun processUserInput(input: String, mediaType: MediaType) { + fun processUserInput(input: String, mediaType: MediaType, view: ViewGroup) { val entry = if (input.endsWith("/") || input.endsWith("index.min.json")) input.substring( 0, @@ -120,7 +125,7 @@ class SettingsExtensionsActivity: AppCompatActivity() { CoroutineScope(Dispatchers.IO).launch { animeExtensionManager.findAvailableExtensions() } - setExtensionOutput(animeRepoInventory, MediaType.ANIME) + setExtensionOutput(view, MediaType.ANIME) } if (mediaType == MediaType.MANGA) { val manga = @@ -129,17 +134,22 @@ class SettingsExtensionsActivity: AppCompatActivity() { CoroutineScope(Dispatchers.IO).launch { mangaExtensionManager.findAvailableExtensions() } - setExtensionOutput(mangaRepoInventory, MediaType.MANGA) + setExtensionOutput(view, MediaType.MANGA) } } - fun processEditorAction(dialog: AlertDialog, editText: EditText, mediaType: MediaType) { + fun processEditorAction( + dialog: AlertDialog, + editText: EditText, + mediaType: MediaType, + view: ViewGroup + ) { editText.setOnEditorActionListener { textView, action, keyEvent -> if (action == EditorInfo.IME_ACTION_SEARCH || action == EditorInfo.IME_ACTION_DONE || (keyEvent?.action == KeyEvent.ACTION_UP && keyEvent.keyCode == KeyEvent.KEYCODE_ENTER)) { return@setOnEditorActionListener if (textView.text.isNullOrBlank()) { false } else { - processUserInput(textView.text.toString(), mediaType) + processUserInput(textView.text.toString(), mediaType, view) dialog.dismiss() true } @@ -147,131 +157,152 @@ class SettingsExtensionsActivity: AppCompatActivity() { false } } - settingsExtensionsLayout.updateLayoutParams { - topMargin = statusBarHeight - bottomMargin = navBarHeight - } - extensionSettingsBack.setOnClickListener{ - onBackPressedDispatcher.onBackPressed() - } - setExtensionOutput(animeRepoInventory, MediaType.ANIME) - setExtensionOutput(mangaRepoInventory, MediaType.MANGA) - animeAddRepository.setOnClickListener { - val dialogView = layoutInflater.inflate(R.layout.dialog_user_agent, null) - val editText = - dialogView.findViewById(R.id.userAgentTextBox).apply { - hint = getString(R.string.anime_add_repository) - } - val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) - .setTitle(R.string.anime_add_repository).setView(dialogView) - .setPositiveButton(getString(R.string.ok)) { dialog, _ -> - if (!editText.text.isNullOrBlank()) processUserInput( - editText.text.toString(), - MediaType.ANIME - ) - dialog.dismiss() - }.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> - dialog.dismiss() - }.create() + settingsRecyclerView.adapter = SettingsAdapter( + arrayListOf( + Settings( + type = 1, + name = getString(R.string.anime_add_repository), + desc = getString(R.string.anime_add_repository_desc), + icon = R.drawable.ic_github, + onClick = { + val dialogView = DialogUserAgentBinding.inflate(layoutInflater) + val editText = dialogView.userAgentTextBox.apply { + hint = getString(R.string.anime_add_repository) + } + val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) + .setTitle(R.string.anime_add_repository).setView(dialogView.root) + .setPositiveButton(getString(R.string.ok)) { dialog, _ -> + if (!editText.text.isNullOrBlank()) processUserInput( + editText.text.toString(), + MediaType.ANIME, + it.attachView + ) + dialog.dismiss() + }.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> + dialog.dismiss() + }.create() - processEditorAction(alertDialog, editText, MediaType.ANIME) - alertDialog.show() - alertDialog.window?.setDimAmount(0.8f) - } - - mangaAddRepository.setOnClickListener { - val dialogView = layoutInflater.inflate(R.layout.dialog_user_agent, null) - val editText = - dialogView.findViewById(R.id.userAgentTextBox).apply { - hint = getString(R.string.manga_add_repository) - } - val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) - .setTitle(R.string.manga_add_repository).setView(dialogView) - .setPositiveButton(getString(R.string.ok)) { dialog, _ -> - if (!editText.text.isNullOrBlank()) processUserInput( - editText.text.toString(), - MediaType.MANGA - ) - dialog.dismiss() - }.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> - dialog.dismiss() - }.create() - - processEditorAction(alertDialog, editText, MediaType.MANGA) - alertDialog.show() - alertDialog.window?.setDimAmount(0.8f) - } - } - - binding.settingsRecyclerView.adapter = SettingsAdapter( - arrayListOf( - Settings( - type = 1, - name = getString(R.string.user_agent), - desc = getString(R.string.NSFWExtention), - icon = R.drawable.ic_round_video_settings_24, - onClick = { - val dialogView = DialogUserAgentBinding.inflate(layoutInflater) - val editText = dialogView.userAgentTextBox - editText.setText(PrefManager.getVal(PrefName.DefaultUserAgent)) - val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) - .setTitle(R.string.user_agent).setView(dialogView.root) - .setPositiveButton(getString(R.string.ok)) { dialog, _ -> - PrefManager.setVal(PrefName.DefaultUserAgent, editText.text.toString()) - dialog.dismiss() - }.setNeutralButton(getString(R.string.reset)) { dialog, _ -> - PrefManager.removeVal(PrefName.DefaultUserAgent) - editText.setText("") - dialog.dismiss() - }.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> - dialog.dismiss() - }.create() - - alertDialog.show() - alertDialog.window?.setDimAmount(0.8f) - } - ), - Settings( - type = 2, - name = getString(R.string.force_legacy_installer), - desc = getString(R.string.force_legacy_installer), - icon = R.drawable.ic_round_new_releases_24, - isChecked = extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY, - switch = { isChecked, _ -> - if (isChecked) { - extensionInstaller.set(BasePreferences.ExtensionInstaller.LEGACY) - } else { - extensionInstaller.set(BasePreferences.ExtensionInstaller.PACKAGEINSTALLER) + processEditorAction( + alertDialog, + editText, + MediaType.ANIME, + it.attachView + ) + alertDialog.show() + alertDialog.window?.setDimAmount(0.8f) + }, + attach = { + setExtensionOutput(it.attachView, MediaType.ANIME) } - } + ), + Settings( + type = 1, + name = getString(R.string.manga_add_repository), + desc = getString(R.string.manga_add_repository_desc), + icon = R.drawable.ic_github, + onClick = { + val dialogView = DialogUserAgentBinding.inflate(layoutInflater) + val editText = dialogView.userAgentTextBox.apply { + hint = getString(R.string.manga_add_repository) + } + val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) + .setTitle(R.string.manga_add_repository).setView(dialogView.root) + .setPositiveButton(getString(R.string.ok)) { dialog, _ -> + if (!editText.text.isNullOrBlank()) processUserInput( + editText.text.toString(), + MediaType.MANGA, + it.attachView + ) + dialog.dismiss() + }.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> + dialog.dismiss() + }.create() - ), - Settings( - type = 2, - name = getString(R.string.skip_loading_extension_icons), - desc = getString(R.string.skip_loading_extension_icons), - icon = R.drawable.ic_round_no_icon_24, - isChecked = PrefManager.getVal(PrefName.SkipExtensionIcons), - switch = { isChecked, _ -> - PrefManager.setVal(PrefName.SkipExtensionIcons, isChecked) - } - ), - Settings( - type = 2, - name = getString(R.string.NSFWExtention), - desc = getString(R.string.NSFWExtention), - icon = R.drawable.ic_round_nsfw_24, - isChecked = PrefManager.getVal(PrefName.NSFWExtension), - switch = { isChecked, _ -> - PrefManager.setVal(PrefName.NSFWExtension, isChecked) - } + processEditorAction( + alertDialog, + editText, + MediaType.MANGA, + it.attachView + ) + alertDialog.show() + alertDialog.window?.setDimAmount(0.8f) + }, + attach = { + setExtensionOutput(it.attachView, MediaType.MANGA) + } + ), + Settings( + type = 1, + name = getString(R.string.user_agent), + desc = getString(R.string.user_agent_desc), + icon = R.drawable.ic_round_video_settings_24, + onClick = { + val dialogView = DialogUserAgentBinding.inflate(layoutInflater) + val editText = dialogView.userAgentTextBox + editText.setText(PrefManager.getVal(PrefName.DefaultUserAgent)) + val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) + .setTitle(R.string.user_agent).setView(dialogView.root) + .setPositiveButton(getString(R.string.ok)) { dialog, _ -> + PrefManager.setVal( + PrefName.DefaultUserAgent, + editText.text.toString() + ) + dialog.dismiss() + }.setNeutralButton(getString(R.string.reset)) { dialog, _ -> + PrefManager.removeVal(PrefName.DefaultUserAgent) + editText.setText("") + dialog.dismiss() + }.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> + dialog.dismiss() + }.create() + alertDialog.show() + alertDialog.window?.setDimAmount(0.8f) + } + ), + Settings( + type = 2, + name = getString(R.string.force_legacy_installer), + desc = getString(R.string.force_legacy_installer_desc), + icon = R.drawable.ic_round_new_releases_24, + isChecked = extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY, + switch = { isChecked, _ -> + if (isChecked) { + extensionInstaller.set(BasePreferences.ExtensionInstaller.LEGACY) + } else { + extensionInstaller.set(BasePreferences.ExtensionInstaller.PACKAGEINSTALLER) + } + } + + ), + Settings( + type = 2, + name = getString(R.string.skip_loading_extension_icons), + desc = getString(R.string.skip_loading_extension_icons_desc), + icon = R.drawable.ic_round_no_icon_24, + isChecked = PrefManager.getVal(PrefName.SkipExtensionIcons), + switch = { isChecked, _ -> + PrefManager.setVal(PrefName.SkipExtensionIcons, isChecked) + } + ), + Settings( + type = 2, + name = getString(R.string.NSFWExtention), + desc = getString(R.string.NSFWExtention_desc), + icon = R.drawable.ic_round_nsfw_24, + isChecked = PrefManager.getVal(PrefName.NSFWExtension), + switch = { isChecked, _ -> + PrefManager.setVal(PrefName.NSFWExtension, isChecked) + } + + ) ) ) - ) - binding.settingsRecyclerView.apply { - layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) - setHasFixedSize(true) + settingsRecyclerView.apply { + layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + setHasFixedSize(true) + } } + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsMangaActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsMangaActivity.kt index afa56eb8..7322f3ef 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsMangaActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsMangaActivity.kt @@ -22,7 +22,7 @@ import ani.dantotsu.themes.ThemeManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class SettingsMangaActivity: AppCompatActivity(){ +class SettingsMangaActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsMangaBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -67,7 +67,7 @@ class SettingsMangaActivity: AppCompatActivity(){ Settings( type = 1, name = getString(R.string.reader_settings), - desc = getString(R.string.reader_settings), + desc = getString(R.string.reader_settings_desc), icon = R.drawable.ic_round_reader_settings, onClick = { startActivity(Intent(context, ReaderSettingsActivity::class.java)) @@ -77,12 +77,17 @@ class SettingsMangaActivity: AppCompatActivity(){ Settings( type = 1, name = getString(R.string.purge_manga_downloads), - desc = getString(R.string.purge_manga_downloads), + desc = getString(R.string.purge_manga_downloads_desc), icon = R.drawable.ic_round_delete_24, onClick = { val dialog = AlertDialog.Builder(context, R.style.MyPopup) .setTitle(R.string.purge_manga_downloads) - .setMessage(getString(R.string.purge_confirm, getString(R.string.manga))) + .setMessage( + getString( + R.string.purge_confirm, + getString(R.string.manga) + ) + ) .setPositiveButton(R.string.yes) { dialog, _ -> val downloadsManager = Injekt.get() downloadsManager.purgeDownloads(MediaType.MANGA) @@ -98,12 +103,17 @@ class SettingsMangaActivity: AppCompatActivity(){ Settings( type = 1, name = getString(R.string.purge_novel_downloads), - desc = getString(R.string.purge_novel_downloads), + desc = getString(R.string.purge_novel_downloads_desc), icon = R.drawable.ic_round_delete_24, onClick = { val dialog = AlertDialog.Builder(context, R.style.MyPopup) .setTitle(R.string.purge_novel_downloads) - .setMessage(getString(R.string.purge_confirm, getString(R.string.novels))) + .setMessage( + getString( + R.string.purge_confirm, + getString(R.string.novels) + ) + ) .setPositiveButton(R.string.yes) { dialog, _ -> val downloadsManager = Injekt.get() downloadsManager.purgeDownloads(MediaType.NOVEL) @@ -118,12 +128,12 @@ class SettingsMangaActivity: AppCompatActivity(){ Settings( type = 2, name = getString(R.string.include_list), - desc = getString(R.string.include_list), + desc = getString(R.string.include_list_desc), icon = R.drawable.view_list_24, isChecked = PrefManager.getVal(PrefName.IncludeMangaList), - switch = {isChecked, _ -> + switch = { isChecked, _ -> PrefManager.setVal(PrefName.IncludeMangaList, isChecked) - restartApp(binding.root) + restartApp() } ), ) diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt index 2d47a32e..75659b99 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsNotificationActivity.kt @@ -13,24 +13,19 @@ 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(){ +class SettingsNotificationActivity : AppCompatActivity() { private lateinit var binding: ActivitySettingsNotificationsBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -73,22 +68,32 @@ class SettingsNotificationActivity: AppCompatActivity(){ arrayListOf( Settings( type = 1, - name = getString(R.string.subscriptions_checking_time_s, timeNames[curTime]), + name = getString( + R.string.subscriptions_checking_time_s, + timeNames[curTime] + ), desc = getString(R.string.subscriptions_info), icon = R.drawable.ic_round_notifications_none_24, onClick = { val speedDialog = AlertDialog.Builder(context, R.style.MyPopup) .setTitle(R.string.subscriptions_checking_time) - val dialog = speedDialog.setSingleChoiceItems(timeNames, curTime) { dialog, i -> - curTime = i - it.settingsTitle.text= - getString(R.string.subscriptions_checking_time_s, timeNames[i]) - PrefManager.setVal(PrefName.SubscriptionNotificationInterval, curTime) - dialog.dismiss() - TaskScheduler.create( - context, PrefManager.getVal(PrefName.UseAlarmManager) - ).scheduleAllTasks(context) - }.show() + val dialog = + speedDialog.setSingleChoiceItems(timeNames, curTime) { dialog, i -> + curTime = i + it.settingsTitle.text = + getString( + R.string.subscriptions_checking_time_s, + timeNames[i] + ) + PrefManager.setVal( + PrefName.SubscriptionNotificationInterval, + curTime + ) + dialog.dismiss() + TaskScheduler.create( + context, PrefManager.getVal(PrefName.UseAlarmManager) + ).scheduleAllTasks(context) + }.show() dialog.window?.setDimAmount(0.8f) }, onLongClick = { @@ -100,16 +105,20 @@ class SettingsNotificationActivity: AppCompatActivity(){ Settings( type = 1, name = getString(R.string.anilist_notification_filters), - desc = getString(R.string.anilist_notification_filters), + desc = getString(R.string.anilist_notification_filters_desc), icon = R.drawable.ic_anilist, onClick = { val types = NotificationType.entries.map { it.name } val filteredTypes = - PrefManager.getVal>(PrefName.AnilistFilteredTypes).toMutableSet() + PrefManager.getVal>(PrefName.AnilistFilteredTypes) + .toMutableSet() val selected = types.map { filteredTypes.contains(it) }.toBooleanArray() val dialog = AlertDialog.Builder(context, R.style.MyPopup) .setTitle(R.string.anilist_notification_filters) - .setMultiChoiceItems(types.toTypedArray(), selected) { _, which, isChecked -> + .setMultiChoiceItems( + types.toTypedArray(), + selected + ) { _, which, isChecked -> val type = types[which] if (isChecked) { filteredTypes.add(type) @@ -129,19 +138,23 @@ class SettingsNotificationActivity: AppCompatActivity(){ R.string.anilist_notifications_checking_time, aItems[PrefManager.getVal(PrefName.AnilistNotificationInterval)] ), - desc = getString( - R.string.anilist_notifications_checking_time, - aItems[PrefManager.getVal(PrefName.AnilistNotificationInterval)] - ), + desc = getString(R.string.anilist_notifications_checking_time_desc), icon = R.drawable.ic_round_notifications_none_24, onClick = { - val selected = PrefManager.getVal(PrefName.AnilistNotificationInterval) + val selected = + PrefManager.getVal(PrefName.AnilistNotificationInterval) val dialog = AlertDialog.Builder(context, R.style.MyPopup) .setTitle(R.string.subscriptions_checking_time) - .setSingleChoiceItems(aItems.toTypedArray(), selected) { dialog, i -> + .setSingleChoiceItems( + aItems.toTypedArray(), + selected + ) { dialog, i -> PrefManager.setVal(PrefName.AnilistNotificationInterval, i) it.settingsTitle.text = - getString(R.string.anilist_notifications_checking_time, aItems[i]) + getString( + R.string.anilist_notifications_checking_time, + aItems[i] + ) dialog.dismiss() TaskScheduler.create( context, PrefManager.getVal(PrefName.UseAlarmManager) @@ -157,19 +170,23 @@ class SettingsNotificationActivity: AppCompatActivity(){ R.string.comment_notification_checking_time, cItems[PrefManager.getVal(PrefName.CommentNotificationInterval)] ), - desc = getString( - R.string.comment_notification_checking_time, - cItems[PrefManager.getVal(PrefName.CommentNotificationInterval)] - ), + desc = getString(R.string.comment_notification_checking_time_desc), icon = R.drawable.ic_round_notifications_none_24, onClick = { - val selected = PrefManager.getVal(PrefName.CommentNotificationInterval) + val selected = + PrefManager.getVal(PrefName.CommentNotificationInterval) val dialog = AlertDialog.Builder(context, R.style.MyPopup) .setTitle(R.string.subscriptions_checking_time) - .setSingleChoiceItems(cItems.toTypedArray(), selected) { dialog, i -> + .setSingleChoiceItems( + cItems.toTypedArray(), + selected + ) { dialog, i -> PrefManager.setVal(PrefName.CommentNotificationInterval, i) it.settingsTitle.text = - getString(R.string.comment_notification_checking_time, cItems[i]) + getString( + R.string.comment_notification_checking_time, + cItems[i] + ) dialog.dismiss() TaskScheduler.create( context, PrefManager.getVal(PrefName.UseAlarmManager) @@ -182,11 +199,14 @@ class SettingsNotificationActivity: AppCompatActivity(){ Settings( type = 2, name = getString(R.string.notification_for_checking_subscriptions), - desc = getString(R.string.notification_for_checking_subscriptions), + desc = getString(R.string.notification_for_checking_subscriptions_desc), icon = R.drawable.ic_round_smart_button_24, isChecked = PrefManager.getVal(PrefName.SubscriptionCheckingNotifications), switch = { isChecked, _ -> - PrefManager.setVal(PrefName.SubscriptionCheckingNotifications, isChecked) + PrefManager.setVal( + PrefName.SubscriptionCheckingNotifications, + isChecked + ) }, onLongClick = { openSettings(context, null) @@ -195,10 +215,10 @@ class SettingsNotificationActivity: AppCompatActivity(){ Settings( type = 2, name = getString(R.string.use_alarm_manager_reliable), - desc = getString(R.string.use_alarm_manager_reliable), + desc = getString(R.string.use_alarm_manager_reliable_desc), icon = R.drawable.ic_anilist, isChecked = PrefManager.getVal(PrefName.UseAlarmManager), - switch = {isChecked, view -> + switch = { isChecked, view -> if (isChecked) { val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) .setTitle(R.string.use_alarm_manager) @@ -217,7 +237,7 @@ class SettingsNotificationActivity: AppCompatActivity(){ }.setNegativeButton(R.string.cancel) { dialog, _ -> view.settingsButton.isChecked = false PrefManager.setVal(PrefName.UseAlarmManager, false) - + dialog.dismiss() }.create() alertDialog.window?.setDimAmount(0.8f) diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsThemeActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsThemeActivity.kt index e1fec176..5e526e28 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsThemeActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsThemeActivity.kt @@ -1,10 +1,13 @@ package ani.dantotsu.settings +import android.content.ComponentName +import android.content.Intent import android.os.Build import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter +import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.LinearLayoutManager @@ -24,7 +27,7 @@ import eltos.simpledialogfragment.color.SimpleColorDialog class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListener { private lateinit var binding: ActivitySettingsThemeBinding - + private var reload = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ThemeManager(this).applyTheme() @@ -37,8 +40,27 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi topMargin = statusBarHeight bottomMargin = navBarHeight } - themeSettingsBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() } - + onBackPressedDispatcher.addCallback(context) { + if (reload) { + val packageName = context.packageName + val mainIntent = Intent.makeRestartActivityTask( + packageManager.getLaunchIntentForPackage(packageName)!!.component + ) + val component = ComponentName(packageName, SettingsActivity::class.qualifiedName!!) + try { + startActivity(Intent().setComponent(component)) + } catch (e: Exception) { + startActivity(mainIntent) + } + finishAndRemoveTask() + reload = false + } else { + finish() + } + } + themeSettingsBack.setOnClickListener { + onBackPressedDispatcher.onBackPressed() + } var previous: View = when (PrefManager.getVal(PrefName.DarkMode)) { 0 -> settingsUiAuto 1 -> settingsUiLight @@ -87,7 +109,7 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi ThemeManager.Companion.Theme.entries[i].theme ) clearFocus() - restartApp(binding.root) + reload() } } @@ -96,67 +118,69 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi Settings( type = 2, name = getString(R.string.oled_theme_variant), - desc = getString(R.string.oled_theme_variant), + desc = getString(R.string.oled_theme_variant_desc), icon = R.drawable.ic_round_brightness_4_24, isChecked = PrefManager.getVal(PrefName.UseOLED), switch = { isChecked, _ -> PrefManager.setVal(PrefName.UseOLED, isChecked) - restartApp(binding.root) + reload() } ), Settings( type = 2, name = getString(R.string.use_material_you), - desc = getString(R.string.use_material_you), + desc = getString(R.string.use_material_you_desc), icon = R.drawable.ic_round_new_releases_24, isChecked = PrefManager.getVal(PrefName.UseMaterialYou), switch = { isChecked, _ -> PrefManager.setVal(PrefName.UseMaterialYou, isChecked) if (isChecked) PrefManager.setVal(PrefName.UseCustomTheme, false) - restartApp(binding.root) + reload() }, isVisible = Build.VERSION.SDK_INT > Build.VERSION_CODES.R ), Settings( type = 2, name = getString(R.string.use_unique_theme_for_each_item), - desc = getString(R.string.use_unique_theme_for_each_item), + desc = getString(R.string.use_unique_theme_for_each_item_desc), icon = R.drawable.ic_palette, isChecked = PrefManager.getVal(PrefName.UseSourceTheme), switch = { isChecked, _ -> PrefManager.setVal(PrefName.UseSourceTheme, isChecked) - restartApp(binding.root) }, isVisible = Build.VERSION.SDK_INT > Build.VERSION_CODES.R ), Settings( type = 2, name = getString(R.string.use_custom_theme), - desc = getString(R.string.use_custom_theme), + desc = getString(R.string.use_custom_theme_desc), icon = R.drawable.ic_palette, isChecked = PrefManager.getVal(PrefName.UseCustomTheme), switch = { isChecked, _ -> PrefManager.setVal(PrefName.UseCustomTheme, isChecked) if (isChecked) PrefManager.setVal(PrefName.UseMaterialYou, false) - restartApp(binding.root) + reload() }, isVisible = Build.VERSION.SDK_INT > Build.VERSION_CODES.R ), Settings( type = 1, name = getString(R.string.color_picker), - desc = getString(R.string.color_picker), + desc = getString(R.string.color_picker_desc), icon = R.drawable.ic_palette, onClick = { val originalColor: Int = PrefManager.getVal(PrefName.CustomThemeInt) + class CustomColorDialog : SimpleColorDialog() { override fun onPositiveButtonClick() { - restartApp(binding.root) + reload() super.onPositiveButtonClick() } } + val tag = "colorPicker" - CustomColorDialog().title(R.string.custom_theme).colorPreset(originalColor) + CustomColorDialog().title(R.string.custom_theme) + .colorPreset(originalColor) .colors(context, SimpleColorDialog.MATERIAL_COLOR_PALLET) .allowCustom(true).showOutline(0x46000000).gridNumColumn(5) .choiceMode(SimpleColorDialog.SINGLE_CHOICE).neg() @@ -183,4 +207,11 @@ class SettingsThemeActivity : AppCompatActivity(), SimpleDialog.OnDialogResultLi } return true } + + fun reload() { + PrefManager.setCustomVal("reload", true) + restartApp() + reload = true + } + } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt index b803fb1b..78cab787 100644 --- a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt @@ -48,7 +48,7 @@ class UserInterfaceSettingsActivity : AppCompatActivity() { } setPositiveButton("Done") { _, _ -> PrefManager.setVal(PrefName.HomeLayoutShow, set) - restartApp(binding.root) + restartApp() } }.show() dialog.window?.setDimAmount(0.8f) @@ -57,24 +57,24 @@ class UserInterfaceSettingsActivity : AppCompatActivity() { binding.uiSettingsSmallView.isChecked = PrefManager.getVal(PrefName.SmallView) binding.uiSettingsSmallView.setOnCheckedChangeListener { _, isChecked -> PrefManager.setVal(PrefName.SmallView, isChecked) - restartApp(binding.root) + restartApp() } binding.uiSettingsImmersive.isChecked = PrefManager.getVal(PrefName.ImmersiveMode) binding.uiSettingsImmersive.setOnCheckedChangeListener { _, isChecked -> PrefManager.setVal(PrefName.ImmersiveMode, isChecked) - restartApp(binding.root) + restartApp() } binding.uiSettingsBannerAnimation.isChecked = PrefManager.getVal(PrefName.BannerAnimations) binding.uiSettingsBannerAnimation.setOnCheckedChangeListener { _, isChecked -> PrefManager.setVal(PrefName.BannerAnimations, isChecked) - restartApp(binding.root) + restartApp() } binding.uiSettingsLayoutAnimation.isChecked = PrefManager.getVal(PrefName.LayoutAnimations) binding.uiSettingsLayoutAnimation.setOnCheckedChangeListener { _, isChecked -> PrefManager.setVal(PrefName.LayoutAnimations, isChecked) - restartApp(binding.root) + restartApp() } binding.uiSettingsTrendingScroller.isChecked = PrefManager.getVal(PrefName.TrendingScroller) @@ -98,22 +98,22 @@ class UserInterfaceSettingsActivity : AppCompatActivity() { mapReverse[PrefManager.getVal(PrefName.AnimationSpeed)] ?: 1f binding.uiSettingsAnimationSpeed.addOnChangeListener { _, value, _ -> PrefManager.setVal(PrefName.AnimationSpeed, map[value] ?: 1f) - restartApp(binding.root) + restartApp() } binding.uiSettingsBlurBanners.isChecked = PrefManager.getVal(PrefName.BlurBanners) binding.uiSettingsBlurBanners.setOnCheckedChangeListener { _, isChecked -> PrefManager.setVal(PrefName.BlurBanners, isChecked) - restartApp(binding.root) + restartApp() } - binding.uiSettingsBlurRadius.value = (PrefManager.getVal(PrefName.BlurRadius) as Float) + binding.uiSettingsBlurRadius.value = (PrefManager.getVal(PrefName.BlurRadius) as Float) binding.uiSettingsBlurRadius.addOnChangeListener { _, value, _ -> PrefManager.setVal(PrefName.BlurRadius, value) - restartApp(binding.root) + restartApp() } binding.uiSettingsBlurSampling.value = (PrefManager.getVal(PrefName.BlurSampling) as Float) binding.uiSettingsBlurSampling.addOnChangeListener { _, value, _ -> PrefManager.setVal(PrefName.BlurSampling, value) - restartApp(binding.root) + restartApp() } } } 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 9bc61d27..6adb2b11 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt @@ -102,7 +102,7 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files UseProxyForTimeStamps(Pref(Location.Player, Boolean::class, false)), ShowTimeStampButton(Pref(Location.Player, Boolean::class, true)), AutoSkipOPED(Pref(Location.Player, Boolean::class, false)), - AutoSkipRecap(Pref(Location.Player, Boolean::class, false )), + AutoSkipRecap(Pref(Location.Player, Boolean::class, false)), AutoPlay(Pref(Location.Player, Boolean::class, true)), AutoSkipFiller(Pref(Location.Player, Boolean::class, false)), AskIndividualPlayer(Pref(Location.Player, Boolean::class, true)), @@ -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/ani/dantotsu/settings/saving/SharedPreferenceLiveData.kt b/app/src/main/java/ani/dantotsu/settings/saving/SharedPreferenceLiveData.kt index 365cb4bd..562abbc6 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/SharedPreferenceLiveData.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/SharedPreferenceLiveData.kt @@ -82,6 +82,7 @@ class SharedPreferenceStringSetLiveData( fun SharedPreferences.intLiveData(key: String, defValue: Int): SharedPreferenceLiveData { return SharedPreferenceIntLiveData(this, key, defValue) } + @Suppress("unused") fun SharedPreferences.stringLiveData( diff --git a/app/src/main/java/ani/dantotsu/util/AniMarkdown.kt b/app/src/main/java/ani/dantotsu/util/AniMarkdown.kt index 54cd0dff..0228256c 100644 --- a/app/src/main/java/ani/dantotsu/util/AniMarkdown.kt +++ b/app/src/main/java/ani/dantotsu/util/AniMarkdown.kt @@ -1,7 +1,6 @@ package ani.dantotsu.util import ani.dantotsu.util.ColorEditor.Companion.toCssColor -import ani.dantotsu.util.ColorEditor.Companion.toHexColor class AniMarkdown { //istg anilist has the worst api companion object { diff --git a/app/src/main/java/ani/dantotsu/util/Logger.kt b/app/src/main/java/ani/dantotsu/util/Logger.kt index 5820abd8..3714aa01 100644 --- a/app/src/main/java/ani/dantotsu/util/Logger.kt +++ b/app/src/main/java/ani/dantotsu/util/Logger.kt @@ -133,7 +133,11 @@ object Logger { shareIntent.type = "text/plain" shareIntent.putExtra( Intent.EXTRA_STREAM, - FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", fileToUse!!) + FileProvider.getUriForFile( + context, + "${BuildConfig.APPLICATION_ID}.provider", + fileToUse!! + ) ) shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Log file") shareIntent.putExtra(Intent.EXTRA_TEXT, "Log file") diff --git a/app/src/main/java/ani/dantotsu/util/StoragePermissions.kt b/app/src/main/java/ani/dantotsu/util/StoragePermissions.kt index 4ef3bf28..8291c567 100644 --- a/app/src/main/java/ani/dantotsu/util/StoragePermissions.kt +++ b/app/src/main/java/ani/dantotsu/util/StoragePermissions.kt @@ -62,9 +62,10 @@ class StoragePermissions { return hasDirAccess(context, path) } - fun AppCompatActivity.accessAlertDialog(launcher: LauncherWrapper, - force: Boolean = false, - complete: (Boolean) -> Unit + fun AppCompatActivity.accessAlertDialog( + launcher: LauncherWrapper, + force: Boolean = false, + complete: (Boolean) -> Unit ) { if (hasDirAccess(this) && !force) { complete(true) @@ -97,11 +98,12 @@ class StoragePermissions { class LauncherWrapper( activity: AppCompatActivity, - contract: ActivityResultContracts.OpenDocumentTree) -{ + contract: ActivityResultContracts.OpenDocumentTree +) { private var launcher: ActivityResultLauncher var complete: (Boolean) -> Unit = {} - init{ + + init { launcher = activity.registerForActivityResult(contract) { uri -> if (uri != null) { activity.contentResolver.takePersistableUriPermission( diff --git a/app/src/main/java/ani/dantotsu/widgets/statistics/ProfileStatsConfigure.kt b/app/src/main/java/ani/dantotsu/widgets/statistics/ProfileStatsConfigure.kt index 38509477..8fa27687 100644 --- a/app/src/main/java/ani/dantotsu/widgets/statistics/ProfileStatsConfigure.kt +++ b/app/src/main/java/ani/dantotsu/widgets/statistics/ProfileStatsConfigure.kt @@ -1,6 +1,5 @@ package ani.dantotsu.widgets.statistics -import android.app.Activity import android.appwidget.AppWidgetManager import android.content.Context import android.content.Intent @@ -12,9 +11,7 @@ import android.view.View import androidx.appcompat.app.AppCompatActivity import ani.dantotsu.R import ani.dantotsu.databinding.StatisticsWidgetConfigureBinding - import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.widgets.upcoming.UpcomingWidget import com.google.android.material.button.MaterialButton import eltos.simpledialogfragment.SimpleDialog import eltos.simpledialogfragment.color.SimpleColorDialog @@ -64,9 +61,12 @@ class ProfileStatsConfigure : AppCompatActivity(), AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID ) - val prefs = getSharedPreferences(ProfileStatsWidget.getPrefsName(appWidgetId), Context.MODE_PRIVATE) - val topBackground = prefs.getInt(ProfileStatsWidget.PREF_BACKGROUND_COLOR, Color.parseColor("#80000000")) - (binding.topBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(topBackground) + val prefs = + getSharedPreferences(ProfileStatsWidget.getPrefsName(appWidgetId), Context.MODE_PRIVATE) + val topBackground = + prefs.getInt(ProfileStatsWidget.PREF_BACKGROUND_COLOR, Color.parseColor("#80000000")) + (binding.topBackgroundButton as MaterialButton).iconTint = + ColorStateList.valueOf(topBackground) binding.topBackgroundButton.setOnClickListener { val tag = ProfileStatsWidget.PREF_BACKGROUND_COLOR SimpleColorDialog().title(R.string.custom_theme) @@ -83,8 +83,10 @@ class ProfileStatsConfigure : AppCompatActivity(), .neg() .show(this@ProfileStatsConfigure, tag) } - val bottomBackground = prefs.getInt(ProfileStatsWidget.PREF_BACKGROUND_FADE, Color.parseColor("#00000000")) - (binding.bottomBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(bottomBackground) + val bottomBackground = + prefs.getInt(ProfileStatsWidget.PREF_BACKGROUND_FADE, Color.parseColor("#00000000")) + (binding.bottomBackgroundButton as MaterialButton).iconTint = + ColorStateList.valueOf(bottomBackground) binding.bottomBackgroundButton.setOnClickListener { val tag = ProfileStatsWidget.PREF_BACKGROUND_FADE SimpleColorDialog().title(R.string.custom_theme) @@ -196,7 +198,10 @@ class ProfileStatsConfigure : AppCompatActivity(), ) val subTextColor = typedValueOutline.data - getSharedPreferences(ProfileStatsWidget.getPrefsName(appWidgetId), Context.MODE_PRIVATE).edit().apply { + getSharedPreferences( + ProfileStatsWidget.getPrefsName(appWidgetId), + Context.MODE_PRIVATE + ).edit().apply { putInt(ProfileStatsWidget.PREF_BACKGROUND_COLOR, backgroundColor) putInt(ProfileStatsWidget.PREF_BACKGROUND_FADE, backgroundColor) putInt(ProfileStatsWidget.PREF_TITLE_TEXT_COLOR, textColor) diff --git a/app/src/main/java/ani/dantotsu/widgets/statistics/ProfileStatsWidget.kt b/app/src/main/java/ani/dantotsu/widgets/statistics/ProfileStatsWidget.kt index cc319524..35f41bd0 100644 --- a/app/src/main/java/ani/dantotsu/widgets/statistics/ProfileStatsWidget.kt +++ b/app/src/main/java/ani/dantotsu/widgets/statistics/ProfileStatsWidget.kt @@ -59,7 +59,8 @@ class ProfileStatsWidget : AppWidgetProvider() { appWidgetId: Int ) { - val prefs = context.getSharedPreferences(getPrefsName(appWidgetId), Context.MODE_PRIVATE) + val prefs = + context.getSharedPreferences(getPrefsName(appWidgetId), Context.MODE_PRIVATE) val backgroundColor = prefs.getInt(PREF_BACKGROUND_COLOR, Color.parseColor("#80000000")) val backgroundFade = prefs.getInt(PREF_BACKGROUND_FADE, Color.parseColor("#00000000")) @@ -87,88 +88,95 @@ class ProfileStatsWidget : AppWidgetProvider() { val respond = Anilist.query.getUserProfile(userPref.toInt()) respond?.data?.user?.let { user -> withContext(Dispatchers.Main) { - val views = RemoteViews(context.packageName, R.layout.statistics_widget).apply { - setImageViewBitmap( - R.id.backgroundView, - gradientDrawable.toBitmap( - width, - height + val views = + RemoteViews(context.packageName, R.layout.statistics_widget).apply { + setImageViewBitmap( + R.id.backgroundView, + gradientDrawable.toBitmap( + width, + height + ) ) - ) - setOnClickPendingIntent( - R.id.userAvatar, - PendingIntent.getActivity( - context, - 1, - Intent(context, ProfileStatsConfigure::class.java).apply { - putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) - data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) - }, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + setOnClickPendingIntent( + R.id.userAvatar, + PendingIntent.getActivity( + context, + 1, + Intent( + context, + ProfileStatsConfigure::class.java + ).apply { + putExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + appWidgetId + ) + data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) ) - ) - setTextColor(R.id.userLabel, titleTextColor) - setTextColor(R.id.topLeftItem, titleTextColor) - setTextColor(R.id.topLeftLabel, statsTextColor) - setTextColor(R.id.topRightItem, titleTextColor) - setTextColor(R.id.topRightLabel, statsTextColor) - setTextColor(R.id.bottomLeftItem, titleTextColor) - setTextColor(R.id.bottomLeftLabel, statsTextColor) - setTextColor(R.id.bottomRightItem, titleTextColor) - setTextColor(R.id.bottomRightLabel, statsTextColor) + setTextColor(R.id.userLabel, titleTextColor) + setTextColor(R.id.topLeftItem, titleTextColor) + setTextColor(R.id.topLeftLabel, statsTextColor) + setTextColor(R.id.topRightItem, titleTextColor) + setTextColor(R.id.topRightLabel, statsTextColor) + setTextColor(R.id.bottomLeftItem, titleTextColor) + setTextColor(R.id.bottomLeftLabel, statsTextColor) + setTextColor(R.id.bottomRightItem, titleTextColor) + setTextColor(R.id.bottomRightLabel, statsTextColor) - setImageViewBitmap( - R.id.userAvatar, - user.avatar?.medium?.let { it1 -> downloadImageAsBitmap(it1) } - ) - setTextViewText( - R.id.userLabel, - context.getString(R.string.user_stats, user.name) - ) + setImageViewBitmap( + R.id.userAvatar, + user.avatar?.medium?.let { it1 -> downloadImageAsBitmap(it1) } + ) + setTextViewText( + R.id.userLabel, + context.getString(R.string.user_stats, user.name) + ) - setTextViewText( - R.id.topLeftItem, - user.statistics.anime.count.toString() - ) - setTextViewText( - R.id.topLeftLabel, - context.getString(R.string.anime_watched) - ) + setTextViewText( + R.id.topLeftItem, + user.statistics.anime.count.toString() + ) + setTextViewText( + R.id.topLeftLabel, + context.getString(R.string.anime_watched) + ) - setTextViewText( - R.id.topRightItem, - user.statistics.anime.episodesWatched.toString() - ) - setTextViewText( - R.id.topRightLabel, - context.getString(R.string.episodes_watched_n) - ) + setTextViewText( + R.id.topRightItem, + user.statistics.anime.episodesWatched.toString() + ) + setTextViewText( + R.id.topRightLabel, + context.getString(R.string.episodes_watched_n) + ) - setTextViewText( - R.id.bottomLeftItem, - user.statistics.manga.count.toString() - ) - setTextViewText( - R.id.bottomLeftLabel, - context.getString(R.string.manga_read) - ) + setTextViewText( + R.id.bottomLeftItem, + user.statistics.manga.count.toString() + ) + setTextViewText( + R.id.bottomLeftLabel, + context.getString(R.string.manga_read) + ) - setTextViewText( - R.id.bottomRightItem, - user.statistics.manga.chaptersRead.toString() - ) - setTextViewText( - R.id.bottomRightLabel, - context.getString(R.string.chapters_read_n) - ) + setTextViewText( + R.id.bottomRightItem, + user.statistics.manga.chaptersRead.toString() + ) + setTextViewText( + R.id.bottomRightLabel, + context.getString(R.string.chapters_read_n) + ) - val intent = Intent(context, ProfileActivity::class.java) - .putExtra("userId", userPref.toInt()) - val pendingIntent = PendingIntent.getActivity( - context, 0, intent, PendingIntent.FLAG_IMMUTABLE - ) - setOnClickPendingIntent(R.id.widgetContainer, pendingIntent) - } + val intent = Intent(context, ProfileActivity::class.java) + .putExtra("userId", userPref.toInt()) + val pendingIntent = PendingIntent.getActivity( + context, 0, intent, PendingIntent.FLAG_IMMUTABLE + ) + setOnClickPendingIntent(R.id.widgetContainer, pendingIntent) + } // Instruct the widget manager to update the widget appWidgetManager.updateAppWidget(appWidgetId, views) } @@ -221,6 +229,7 @@ class ProfileStatsWidget : AppWidgetProvider() { fun getPrefsName(appWidgetId: Int): String { return "ani.dantotsu.widgets.Statistics.${appWidgetId}" } + const val PREF_BACKGROUND_COLOR = "background_color" const val PREF_BACKGROUND_FADE = "background_fade" const val PREF_TITLE_TEXT_COLOR = "title_text_color" diff --git a/app/src/main/java/ani/dantotsu/widgets/upcoming/UpcomingWidgetConfigure.kt b/app/src/main/java/ani/dantotsu/widgets/upcoming/UpcomingWidgetConfigure.kt index a465a48a..7b237598 100644 --- a/app/src/main/java/ani/dantotsu/widgets/upcoming/UpcomingWidgetConfigure.kt +++ b/app/src/main/java/ani/dantotsu/widgets/upcoming/UpcomingWidgetConfigure.kt @@ -48,8 +48,10 @@ class UpcomingWidgetConfigure : AppCompatActivity(), binding = UpcomingWidgetConfigureBinding.inflate(layoutInflater) setContentView(binding.root) val prefs = getSharedPreferences(UpcomingWidget.PREFS_NAME, Context.MODE_PRIVATE) - val topBackground = prefs.getInt(UpcomingWidget.PREF_BACKGROUND_COLOR, Color.parseColor("#80000000")) - (binding.topBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(topBackground) + val topBackground = + prefs.getInt(UpcomingWidget.PREF_BACKGROUND_COLOR, Color.parseColor("#80000000")) + (binding.topBackgroundButton as MaterialButton).iconTint = + ColorStateList.valueOf(topBackground) binding.topBackgroundButton.setOnClickListener { val tag = UpcomingWidget.PREF_BACKGROUND_COLOR SimpleColorDialog().title(R.string.custom_theme) @@ -66,8 +68,10 @@ class UpcomingWidgetConfigure : AppCompatActivity(), .neg() .show(this@UpcomingWidgetConfigure, tag) } - val bottomBackground = prefs.getInt(UpcomingWidget.PREF_BACKGROUND_FADE, Color.parseColor("#00000000")) - (binding.bottomBackgroundButton as MaterialButton).iconTint = ColorStateList.valueOf(bottomBackground) + val bottomBackground = + prefs.getInt(UpcomingWidget.PREF_BACKGROUND_FADE, Color.parseColor("#00000000")) + (binding.bottomBackgroundButton as MaterialButton).iconTint = + ColorStateList.valueOf(bottomBackground) binding.bottomBackgroundButton.setOnClickListener { val tag = UpcomingWidget.PREF_BACKGROUND_FADE SimpleColorDialog().title(R.string.custom_theme) @@ -85,7 +89,8 @@ class UpcomingWidgetConfigure : AppCompatActivity(), .show(this@UpcomingWidgetConfigure, tag) } val titleTextColor = prefs.getInt(UpcomingWidget.PREF_TITLE_TEXT_COLOR, Color.WHITE) - (binding.titleColorButton as MaterialButton).iconTint = ColorStateList.valueOf(titleTextColor) + (binding.titleColorButton as MaterialButton).iconTint = + ColorStateList.valueOf(titleTextColor) binding.titleColorButton.setOnClickListener { val tag = UpcomingWidget.PREF_TITLE_TEXT_COLOR SimpleColorDialog().title(R.string.custom_theme) @@ -102,7 +107,8 @@ class UpcomingWidgetConfigure : AppCompatActivity(), .show(this@UpcomingWidgetConfigure, tag) } val countdownTextColor = prefs.getInt(UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR, Color.WHITE) - (binding.countdownColorButton as MaterialButton).iconTint = ColorStateList.valueOf(countdownTextColor) + (binding.countdownColorButton as MaterialButton).iconTint = + ColorStateList.valueOf(countdownTextColor) binding.countdownColorButton.setOnClickListener { val tag = UpcomingWidget.PREF_COUNTDOWN_TEXT_COLOR SimpleColorDialog().title(R.string.custom_theme) @@ -152,15 +158,27 @@ class UpcomingWidgetConfigure : AppCompatActivity(), private fun themeColors() { val typedValueSurface = TypedValue() - theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValueSurface, true) + theme.resolveAttribute( + com.google.android.material.R.attr.colorSurface, + typedValueSurface, + true + ) val backgroundColor = typedValueSurface.data val typedValuePrimary = TypedValue() - theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValuePrimary, true) + theme.resolveAttribute( + com.google.android.material.R.attr.colorPrimary, + typedValuePrimary, + true + ) val textColor = typedValuePrimary.data val typedValueOutline = TypedValue() - theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValueOutline, true) + theme.resolveAttribute( + com.google.android.material.R.attr.colorOutline, + typedValueOutline, + true + ) val subTextColor = typedValueOutline.data getSharedPreferences(UpcomingWidget.PREFS_NAME, Context.MODE_PRIVATE).edit().apply { diff --git a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt index b34758c4..6d09285a 100644 --- a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt @@ -17,7 +17,7 @@ class BasePreferences( fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false) fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore) - + fun deviceHasPip() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && context.packageManager.hasSystemFeature( PackageManager.FEATURE_PICTURE_IN_PICTURE diff --git a/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt index 61e8f5db..ed1ed07d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/animesource/online/AnimeHttpSource.kt @@ -23,7 +23,6 @@ import uy.kohesive.injekt.injectLazy import java.net.URI import java.net.URISyntaxException import java.security.MessageDigest -import java.util.concurrent.TimeUnit /** * A simple implementation for sources from a website. @@ -89,14 +88,15 @@ abstract class AnimeHttpSource : AnimeCatalogueSource { protected fun generateId(name: String, lang: String, versionId: Int): Long { val key = "${name.lowercase()}/$lang/$versionId" val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) - return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE + return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) } + .reduce(Long::or) and Long.MAX_VALUE } /** * Headers builder for requests. Implementations can override this method for custom headers. */ protected open fun headersBuilder() = Headers.Builder().apply { - add("User-Agent", NetworkHelper.defaultUserAgentProvider()) + add("User-Agent", defaultUserAgentProvider()) } /** @@ -148,7 +148,11 @@ abstract class AnimeHttpSource : AnimeCatalogueSource { "Use the non-RxJava API instead", ReplaceWith("getSearchAnime"), ) - override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + override fun fetchSearchAnime( + page: Int, + query: String, + filters: AnimeFilterList + ): Observable { return Observable.defer { try { client.newCall(searchAnimeRequest(page, query, filters)).asObservableSuccess() @@ -170,7 +174,11 @@ abstract class AnimeHttpSource : AnimeCatalogueSource { * @param query the search query. * @param filters the list of filters to apply. */ - protected abstract fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request + protected abstract fun searchAnimeRequest( + page: Int, + query: String, + filters: AnimeFilterList + ): Request /** * Parses the response from the site and returns a [AnimesPage] object. @@ -403,7 +411,8 @@ abstract class AnimeHttpSource : AnimeCatalogueSource { video: Video, tries: Int, ): Long { - val headers = Headers.Builder().addAll(video.headers ?: headers).add("Range", "bytes=0-1").build() + val headers = + Headers.Builder().addAll(video.headers ?: headers).add("Range", "bytes=0-1").build() val request = GET(video.videoUrl!!, headers) val response = client.newCall(request).execute() // parse the response headers to get the size of the video, in particular the content-range header 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 0e77ed37..2e254c52 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 00000000..e1445dd1 --- /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 9bb07824..07809c60 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,10 @@ 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 c3f13e27..32504b89 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) + MediaType.NOVEL -> novelExtensionManager.updateInstallStep( + completedEntry.downloadId, + resultStep + ) + } + } else { + when (completedEntry.type) { + AddonType.TORRENT -> torrentAddonManager.updateInstallStep( + completedEntry.downloadId, + resultStep + ) + + AddonType.DOWNLOAD -> downloadAddonManager.updateInstallStep( + completedEntry.downloadId, + resultStep + ) } } checkQueue() @@ -113,7 +143,7 @@ abstract class Installer(private val service: Service) { * * @see ready */ - fun checkQueue() { + private fun checkQueue() { if (!ready) { return } @@ -135,15 +165,35 @@ abstract class Installer(private val service: Service) { open fun onDestroy() { LocalBroadcastManager.getInstance(service).unregisterReceiver(cancelReceiver) queue.forEach { - when (it.type) { - MediaType.ANIME -> { - animeExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error) + + if (it.type is MediaType) { + when (it.type) { + MediaType.ANIME -> animeExtensionManager.updateInstallStep( + it.downloadId, + InstallStep.Error + ) + + MediaType.MANGA -> mangaExtensionManager.updateInstallStep( + it.downloadId, + InstallStep.Error + ) + + MediaType.NOVEL -> novelExtensionManager.updateInstallStep( + it.downloadId, + InstallStep.Error + ) } - MediaType.MANGA -> { - mangaExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error) - } - MediaType.NOVEL -> { - novelExtensionManager.updateInstallStep(it.downloadId, InstallStep.Error) + } else { + when (it.type) { + AddonType.TORRENT -> torrentAddonManager.updateInstallStep( + it.downloadId, + InstallStep.Error + ) + + AddonType.DOWNLOAD -> downloadAddonManager.updateInstallStep( + it.downloadId, + InstallStep.Error + ) } } } @@ -168,15 +218,34 @@ abstract class Installer(private val service: Service) { this.waitingInstall.set(null) checkQueue() } - when (toCancel.type) { - MediaType.ANIME -> { - animeExtensionManager.updateInstallStep(downloadId, InstallStep.Idle) + if (toCancel.type is MediaType) { + when (toCancel.type) { + MediaType.ANIME -> animeExtensionManager.updateInstallStep( + downloadId, + InstallStep.Idle + ) + + MediaType.MANGA -> mangaExtensionManager.updateInstallStep( + downloadId, + InstallStep.Idle + ) + + MediaType.NOVEL -> novelExtensionManager.updateInstallStep( + downloadId, + InstallStep.Idle + ) } - MediaType.MANGA -> { - mangaExtensionManager.updateInstallStep(downloadId, InstallStep.Idle) - } - MediaType.NOVEL -> { - novelExtensionManager.updateInstallStep(downloadId, InstallStep.Idle) + } else { + when (toCancel.type) { + AddonType.TORRENT -> torrentAddonManager.updateInstallStep( + downloadId, + InstallStep.Idle + ) + + AddonType.DOWNLOAD -> downloadAddonManager.updateInstallStep( + downloadId, + InstallStep.Idle + ) } } } @@ -188,7 +257,7 @@ abstract class Installer(private val service: Service) { * @param downloadId Download ID as known by [ExtensionManager] * @param uri Uri of APK to install */ - data class Entry(val type: MediaType, val downloadId: Long, val uri: Uri) + data class Entry(val type: Type, val downloadId: Long, val uri: Uri) init { val filter = IntentFilter(ACTION_CANCEL_QUEUE) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt index 7e96782e..a76b68d9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/PackageInstallerInstaller.kt @@ -10,7 +10,9 @@ import android.content.pm.PackageInstaller import android.os.Build import androidx.core.content.ContextCompat import androidx.core.content.IntentSanitizer +import ani.dantotsu.R import ani.dantotsu.snackString +import ani.dantotsu.toast import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.util.lang.use @@ -28,18 +30,19 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic PackageInstaller.STATUS_FAILURE )) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val userAction = intent.getParcelableExtraCompat(Intent.EXTRA_INTENT)?.run { - IntentSanitizer.Builder() - .allowAction(this.action!!) - .allowExtra(PackageInstaller.EXTRA_SESSION_ID) { id -> id == activeSession?.second } - .allowAnyComponent() - .allowPackage { - // There is no way to check the actual installer name so allow all. - true - } - .build() - .sanitizeByFiltering(this) - } + val userAction = + intent.getParcelableExtraCompat(Intent.EXTRA_INTENT)?.run { + IntentSanitizer.Builder() + .allowAction(this.action!!) + .allowExtra(PackageInstaller.EXTRA_SESSION_ID) { id -> id == activeSession?.second } + .allowAnyComponent() + .allowPackage { + // There is no way to check the actual installer name so allow all. + true + } + .build() + .sanitizeByFiltering(this) + } if (userAction == null) { Logger.log("Fatal error for $intent") continueQueue(InstallStep.Error) @@ -54,7 +57,16 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic } PackageInstaller.STATUS_SUCCESS -> continueQueue(InstallStep.Installed) - else -> continueQueue(InstallStep.Error) + PackageInstaller.STATUS_FAILURE_CONFLICT -> { + Logger.log("Failed to install extension due to conflict") + toast(context.getString(R.string.failed_ext_install_conflict)) + continueQueue(InstallStep.Error) + } + else -> { + Logger.log("Fatal error for $intent") + Logger.log("Status: ${intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)}") + continueQueue(InstallStep.Error) + } } } } 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 3dcb4cf8..1ae39724 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,10 @@ 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 778f8f9f..673ef920 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,11 @@ 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 +93,34 @@ class ExtensionInstallActivity : AppCompatActivity() { RESULT_CANCELED -> InstallStep.Idle else -> InstallStep.Error } - when (type) { - MediaType.ANIME -> { - 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.MANGA -> { - Injekt.get().updateInstallStep(downloadId, newStep) + } else { + when (addonType) { + AddonType.TORRENT -> { + Injekt.get().updateInstallStep(downloadId, newStep) + } + + AddonType.DOWNLOAD -> { + Injekt.get().updateInstallStep(downloadId, newStep) + } + + null -> {} } - MediaType.NOVEL -> { - Injekt.get().updateInstallStep(downloadId, newStep) - } - 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 987cfead..f0bbe7c6 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 @@ -6,6 +6,8 @@ import android.content.Intent import android.content.IntentFilter import androidx.core.content.ContextCompat import ani.dantotsu.media.MediaType +import ani.dantotsu.parsers.novel.NovelExtension +import ani.dantotsu.parsers.novel.NovelLoadResult import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeLoadResult @@ -28,6 +30,7 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { private var animeListener: AnimeListener? = null private var mangaListener: MangaListener? = null + private var novelListener: NovelListener? = null private var type: MediaType? = null /** @@ -37,30 +40,24 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED) } - fun setAnimeListener(listener: AnimeListener) : ExtensionInstallReceiver { + fun setAnimeListener(listener: AnimeListener): ExtensionInstallReceiver { this.type = MediaType.ANIME animeListener = listener this.animeListener return this } - fun setMangaListener(listener: MangaListener) : ExtensionInstallReceiver { + fun setMangaListener(listener: MangaListener): ExtensionInstallReceiver { this.type = MediaType.MANGA mangaListener = listener return this } - /** - * Returns the intent filter this receiver should subscribe to. - */ - private val filter - get() = IntentFilter().apply { - priority = 100 - addAction(Intent.ACTION_PACKAGE_ADDED) - addAction(Intent.ACTION_PACKAGE_REPLACED) - addAction(Intent.ACTION_PACKAGE_REMOVED) - addDataScheme("package") - } + fun setNovelListener(listener: NovelListener): ExtensionInstallReceiver { + this.type = MediaType.NOVEL + novelListener = listener + return this + } /** * Called when one of the events of the [filter] is received. When the package is an extension, @@ -78,21 +75,43 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { when (type) { MediaType.ANIME -> { when (val result = getAnimeExtensionFromIntent(context, intent)) { - is AnimeLoadResult.Success -> animeListener?.onExtensionInstalled(result.extension) + is AnimeLoadResult.Success -> animeListener?.onExtensionInstalled( + result.extension + ) + + is AnimeLoadResult.Untrusted -> animeListener?.onExtensionUntrusted( + result.extension + ) - is AnimeLoadResult.Untrusted -> animeListener?.onExtensionUntrusted(result.extension) else -> {} } } + MediaType.MANGA -> { when (val result = getMangaExtensionFromIntent(context, intent)) { - is MangaLoadResult.Success -> mangaListener?.onExtensionInstalled(result.extension) + is MangaLoadResult.Success -> mangaListener?.onExtensionInstalled( + result.extension + ) + + is MangaLoadResult.Untrusted -> mangaListener?.onExtensionUntrusted( + result.extension + ) - is MangaLoadResult.Untrusted -> mangaListener?.onExtensionUntrusted(result.extension) else -> {} } } - else -> { } + + MediaType.NOVEL -> { + when (val result = getNovelExtensionFromIntent(context, intent)) { + is NovelLoadResult.Success -> novelListener?.onExtensionInstalled( + result.extension + ) + + else -> {} + } + } + + else -> {} } } } @@ -102,17 +121,35 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { when (type) { MediaType.ANIME -> { when (val result = getAnimeExtensionFromIntent(context, intent)) { - is AnimeLoadResult.Success -> animeListener?.onExtensionUpdated(result.extension) + is AnimeLoadResult.Success -> animeListener?.onExtensionUpdated( + result.extension + ) + else -> {} } } + MediaType.MANGA -> { when (val result = getMangaExtensionFromIntent(context, intent)) { - is MangaLoadResult.Success -> mangaListener?.onExtensionUpdated(result.extension) + is MangaLoadResult.Success -> mangaListener?.onExtensionUpdated( + result.extension + ) + else -> {} } } - else -> { } + + MediaType.NOVEL -> { + when (val result = getNovelExtensionFromIntent(context, intent)) { + is NovelLoadResult.Success -> novelListener?.onExtensionUpdated( + result.extension + ) + + else -> {} + } + } + + else -> {} } } } @@ -126,32 +163,33 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { MediaType.ANIME -> { animeListener?.onPackageUninstalled(pkgName) } + MediaType.MANGA -> { mangaListener?.onPackageUninstalled(pkgName) } - else -> { } + + MediaType.NOVEL -> { + novelListener?.onPackageUninstalled(pkgName) + } + + else -> {} } } } } } - /** - * Returns true if this package is performing an update. - * - * @param intent The intent that triggered the event. - */ - private fun isReplacing(intent: Intent): Boolean { - return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) - } - /** * Returns the extension triggered by the given intent. * * @param context The application context. * @param intent The intent containing the package name of the extension. */ - private suspend fun getAnimeExtensionFromIntent(context: Context, intent: Intent?): AnimeLoadResult { + @OptIn(DelicateCoroutinesApi::class) + private suspend fun getAnimeExtensionFromIntent( + context: Context, + intent: Intent? + ): AnimeLoadResult { val pkgName = getPackageNameFromIntent(intent) if (pkgName == null) { Logger.log("Package name not found") @@ -166,7 +204,10 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { } @OptIn(DelicateCoroutinesApi::class) - private suspend fun getMangaExtensionFromIntent(context: Context, intent: Intent?): MangaLoadResult { + private suspend fun getMangaExtensionFromIntent( + context: Context, + intent: Intent? + ): MangaLoadResult { val pkgName = getPackageNameFromIntent(intent) if (pkgName == null) { Logger.log("Package name not found") @@ -180,11 +221,22 @@ 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 + @OptIn(DelicateCoroutinesApi::class) + private suspend fun getNovelExtensionFromIntent( + context: Context, + intent: Intent? + ): NovelLoadResult { + val pkgName = getPackageNameFromIntent(intent) + if (pkgName == null) { + Logger.log("Package name not found") + return NovelLoadResult.Error(Exception("Package name not found")) + } + return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { + ExtensionLoader.loadNovelExtensionFromPkgName( + context, + pkgName, + ) + }.await() } /** @@ -203,4 +255,42 @@ internal class ExtensionInstallReceiver : BroadcastReceiver() { fun onExtensionUntrusted(extension: MangaExtension.Untrusted) fun onPackageUninstalled(pkgName: String) } + + interface NovelListener { + fun onExtensionInstalled(extension: NovelExtension.Installed) + fun onExtensionUpdated(extension: NovelExtension.Installed) + 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 5c726c6c..3b8c6571 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,12 +8,14 @@ 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 -import eu.kanade.tachiyomi.extension.installer.PackageInstallerInstaller import eu.kanade.tachiyomi.extension.installer.Installer +import eu.kanade.tachiyomi.extension.installer.PackageInstallerInstaller import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_EXTENSION_TYPE import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat @@ -45,12 +47,14 @@ 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 +72,7 @@ class ExtensionInstallService : Service() { } } } - installer!!.addToQueue(type, id, uri) + installer!!.addToQueue(mediaType ?: addonType!!, id, uri) return START_NOT_STICKY } @@ -84,16 +88,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 27ddd610..4d7a0fed 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,15 +11,14 @@ 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.parsers.novel.NovelExtension +import ani.dantotsu.media.Type import ani.dantotsu.util.Logger import com.jakewharton.rxrelay.PublishRelay import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.extension.InstallStep -import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.installer.Installer -import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.util.storage.getUriCompat import rx.Observable import rx.android.schedulers.AndroidSchedulers @@ -33,7 +32,7 @@ import java.util.concurrent.TimeUnit * * @param context The application context. */ -internal class ExtensionInstaller(private val context: Context) { +class ExtensionInstaller(private val context: Context) { /** * The system's download manager @@ -65,27 +64,31 @@ 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 - + 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.ANIME.asText()) + .setDescription(type.asText()) + .setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE) + .setAllowedOverRoaming(true) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) val id = downloadManager.enqueue(request) @@ -93,91 +96,12 @@ internal class ExtensionInstaller(private val context: Context) { downloadsRelay.filter { it.first == id } .map { it.second } - // Poll download status .mergeWith(pollStatus(id)) - // Stop when the application is installed or errors .takeUntil { it.isCompleted() } - // Always notify on main thread .observeOn(AndroidSchedulers.mainThread()) - // Always remove the download when unsubscribed .doOnUnsubscribe { deleteDownload(pkgName) } } - fun downloadAndInstall(url: String, extension: MangaExtension): Observable = Observable.defer { - val pkgName = extension.pkgName - - val oldDownload = activeDownloads[pkgName] - if (oldDownload != null) { - deleteDownload(pkgName) - } - - // Register the receiver after removing (and unregistering) the previous download - downloadReceiver.register() - - val downloadUri = url.toUri() - val request = DownloadManager.Request(downloadUri) - .setTitle(extension.name) - .setMimeType(APK_MIME) - .setDestinationInExternalFilesDir( - context, - Environment.DIRECTORY_DOWNLOADS, - downloadUri.lastPathSegment - ) - .setDescription(MediaType.MANGA.asText()) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - - val id = downloadManager.enqueue(request) - activeDownloads[pkgName] = id - - downloadsRelay.filter { it.first == id } - .map { it.second } - // Poll download status - .mergeWith(pollStatus(id)) - // Stop when the application is installed or errors - .takeUntil { it.isCompleted() } - // Always notify on main thread - .observeOn(AndroidSchedulers.mainThread()) - // Always remove the download when unsubscribed - .doOnUnsubscribe { deleteDownload(pkgName) } - } - - fun downloadAndInstall(url: String, extension: NovelExtension) = Observable.defer { - val pkgName = extension.pkgName - - val oldDownload = activeDownloads[pkgName] - if (oldDownload != null) { - deleteDownload(pkgName) - } - - // Register the receiver after removing (and unregistering) the previous download - downloadReceiver.register() - - val downloadUri = url.toUri() - val request = DownloadManager.Request(downloadUri) - .setTitle(extension.name) - .setMimeType(APK_MIME) - .setDestinationInExternalFilesDir( - context, - Environment.DIRECTORY_DOWNLOADS, - downloadUri.lastPathSegment - ) - .setDescription(MediaType.MANGA.asText()) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - - val id = downloadManager.enqueue(request) - activeDownloads[pkgName] = id - - downloadsRelay.filter { it.first == id } - .map { it.second } - // Poll download status - .mergeWith(pollStatus(id)) - // Stop when the application is installed or errors - .takeUntil { it.isCompleted() } - // Always notify on main thread - .observeOn(AndroidSchedulers.mainThread()) - // Always remove the download when unsubscribed - .doOnUnsubscribe { deleteDownload(pkgName) } - } /** * Returns an observable that polls the given download id for its status every second, as the @@ -215,14 +139,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) } @@ -340,9 +268,15 @@ internal class ExtensionInstaller(private val context: Context) { val localUri = cursor.getString( cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI), ).removePrefix(FILE_SCHEME) - val type = MediaType.fromText(cursor.getString( - cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION), - )) + 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 +288,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 59f35ef5..d7b454a1 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 @@ -6,6 +6,9 @@ import android.content.pm.PackageManager import android.os.Build import androidx.core.content.pm.PackageInfoCompat import ani.dantotsu.media.MediaType +import ani.dantotsu.parsers.NovelInterface +import ani.dantotsu.parsers.novel.NovelExtension +import ani.dantotsu.parsers.novel.NovelLoadResult import ani.dantotsu.util.Logger import dalvik.system.PathClassLoader import eu.kanade.domain.source.service.SourcePreferences @@ -24,6 +27,7 @@ import eu.kanade.tachiyomi.util.system.getApplicationIcon import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking import uy.kohesive.injekt.injectLazy +import java.util.Locale /** * Class that handles the loading of the extensions. Supports two kinds of extensions: @@ -60,9 +64,9 @@ 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 + @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES or (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) PackageManager.GET_SIGNING_CERTIFICATES else 0) @@ -77,6 +81,10 @@ internal object ExtensionLoader { private const val officialSignatureManga = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" + //dan's key + private const val officialSignature = + "a3061edb369278749b8e8de810d440d38e96417bbd67bbdfc5d9d9ed475ce4a5" + /** * List of the trusted signatures. */ @@ -133,6 +141,28 @@ internal object ExtensionLoader { } } + fun loadNovelExtensions(context: Context): List { + val pkgManager = context.packageManager + + val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(PACKAGE_FLAGS.toLong())) + } else { + pkgManager.getInstalledPackages(PACKAGE_FLAGS) + } + + val extPkgs = installedPkgs.filter { isPackageAnExtension(MediaType.NOVEL, it) } + + if (extPkgs.isEmpty()) return emptyList() + + // Load each extension concurrently and wait for completion + return runBlocking { + val deferred = extPkgs.map { + async { loadNovelExtension(context, it.packageName, it) } + } + deferred.map { it.await() } + } + } + /** * Attempts to load an extension from the given package name. It checks if the extension * contains the required feature flag before trying to load it. @@ -145,7 +175,7 @@ internal object ExtensionLoader { Logger.log(error) return AnimeLoadResult.Error } - if (!isPackageAnExtension(MediaType.ANIME,pkgInfo)) { + if (!isPackageAnExtension(MediaType.ANIME, pkgInfo)) { Logger.log("Tried to load a package that wasn't a extension ($pkgName)") return AnimeLoadResult.Error } @@ -167,6 +197,21 @@ internal object ExtensionLoader { return loadMangaExtension(context, pkgName, pkgInfo) } + fun loadNovelExtensionFromPkgName(context: Context, pkgName: String): NovelLoadResult { + val pkgInfo = try { + context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS) + } catch (error: PackageManager.NameNotFoundException) { + // Unlikely, but the package may have been uninstalled at this point + Logger.log(error) + return NovelLoadResult.Error(error) + } + if (!isPackageAnExtension(MediaType.NOVEL, pkgInfo)) { + Logger.log("Tried to load a package that wasn't a extension ($pkgName)") + return NovelLoadResult.Error(Exception("Tried to load a package that wasn't a extension ($pkgName)")) + } + return loadNovelExtension(context, pkgName, pkgInfo) + } + /** * Loads an extension given its package name. * @@ -201,8 +246,9 @@ internal object ExtensionLoader { // Validate lib version val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull() if (libVersion == null || libVersion < ANIME_LIB_VERSION_MIN || libVersion > ANIME_LIB_VERSION_MAX) { - Logger.log("Lib version is $libVersion, while only versions " + - "$ANIME_LIB_VERSION_MIN to $ANIME_LIB_VERSION_MAX are allowed" + Logger.log( + "Lib version is $libVersion, while only versions " + + "$ANIME_LIB_VERSION_MIN to $ANIME_LIB_VERSION_MAX are allowed" ) return AnimeLoadResult.Error } @@ -232,7 +278,8 @@ internal object ExtensionLoader { } val hasReadme = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_HAS_README", 0) == 1 - val hasChangelog = appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_HAS_CHANGELOG", 0) == 1 + val hasChangelog = + appInfo.metaData.getInt("$ANIME_PACKAGE$XX_METADATA_HAS_CHANGELOG", 0) == 1 val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader) @@ -248,7 +295,8 @@ internal object ExtensionLoader { } .flatMap { try { - when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor().newInstance()) { + when (val obj = Class.forName(it, false, classLoader).getDeclaredConstructor() + .newInstance()) { is AnimeSource -> listOf(obj) is AnimeSourceFactory -> obj.createSources() else -> throw Exception("Unknown source class type! ${obj.javaClass}") @@ -314,8 +362,9 @@ internal object ExtensionLoader { // Validate lib version val libVersion = versionName.substringBeforeLast('.').toDoubleOrNull() if (libVersion == null || libVersion < MANGA_LIB_VERSION_MIN || libVersion > MANGA_LIB_VERSION_MAX) { - Logger.log("Lib version is $libVersion, while only versions " + - "$MANGA_LIB_VERSION_MIN to $MANGA_LIB_VERSION_MAX are allowed" + Logger.log( + "Lib version is $libVersion, while only versions " + + "$MANGA_LIB_VERSION_MIN to $MANGA_LIB_VERSION_MAX are allowed" ) return MangaLoadResult.Error } @@ -340,7 +389,8 @@ internal object ExtensionLoader { } val hasReadme = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_HAS_README", 0) == 1 - val hasChangelog = appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_HAS_CHANGELOG", 0) == 1 + val hasChangelog = + appInfo.metaData.getInt("$MANGA_PACKAGE$XX_METADATA_HAS_CHANGELOG", 0) == 1 val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader) @@ -395,17 +445,77 @@ internal object ExtensionLoader { return MangaLoadResult.Success(extension) } + private fun loadNovelExtension( + context: Context, + pkgName: String, + pkgInfo: PackageInfo + ): NovelLoadResult { + val pkgManager = context.packageManager + + val appInfo = try { + pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA) + } catch (error: PackageManager.NameNotFoundException) { + // Unlikely, but the package may have been uninstalled at this point + Logger.log(error) + return NovelLoadResult.Error(error) + } + + val extName = + pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ") + val versionName = pkgInfo.versionName + val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo) + + if (versionName.isNullOrEmpty()) { + Logger.log("Missing versionName for extension $extName") + return NovelLoadResult.Error(Exception("Missing versionName for extension $extName")) + } + + val signatureHash = getSignatureHash(pkgInfo) + + val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader) + val novelInterfaceInstance = try { + val className = appInfo.loadLabel(context.packageManager).toString() + val extensionClassName = + "some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className" + val loadedClass = classLoader.loadClass(extensionClassName) + val instance = loadedClass.getDeclaredConstructor().newInstance() + instance as? NovelInterface + } catch (e: Throwable) { + Logger.log("Extension load error: $extName") + return NovelLoadResult.Error(e as Exception) + } + + val extension = NovelExtension.Installed( + name = extName, + pkgName = pkgName, + versionName = versionName, + versionCode = versionCode, + sources = listOfNotNull(novelInterfaceInstance), + isUnofficial = signatureHash != officialSignatureManga, + icon = context.getApplicationIcon(pkgName), + ) + return NovelLoadResult.Success(extension) + } + + /** * Returns true if the given package is an extension. * * @param pkgInfo The package info of the application. */ private fun isPackageAnExtension(type: MediaType, pkgInfo: PackageInfo): Boolean { - return pkgInfo.reqFeatures.orEmpty().any { it.name == when (type) { - MediaType.ANIME -> ANIME_PACKAGE - MediaType.MANGA -> MANGA_PACKAGE - else -> "" - } } + + return if (type == MediaType.NOVEL) { + pkgInfo.packageName.startsWith("some.random") + } else { + pkgInfo.reqFeatures.orEmpty().any { + it.name == when (type) { + MediaType.ANIME -> ANIME_PACKAGE + MediaType.MANGA -> MANGA_PACKAGE + else -> "" + } + } + } } /** @@ -417,7 +527,7 @@ internal object ExtensionLoader { val signatures = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) pkgInfo.signingInfo.signingCertificateHistory else - @Suppress ("DEPRECATION") pkgInfo.signatures + @Suppress("DEPRECATION") pkgInfo.signatures return if (signatures != null && signatures.isNotEmpty()) { Hash.sha256(signatures.first().toByteArray()) } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index ead9e3e2..53149638 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -11,8 +11,8 @@ import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import okhttp3.Cache import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import okhttp3.brotli.BrotliInterceptor +import okhttp3.logging.HttpLoggingInterceptor import java.io.File import java.util.concurrent.TimeUnit diff --git a/app/src/main/java/tachiyomi/source/local/entries/anime/LocalAnimeSource.kt b/app/src/main/java/tachiyomi/source/local/entries/anime/LocalAnimeSource.kt index 73a20c60..8327ecdc 100644 --- a/app/src/main/java/tachiyomi/source/local/entries/anime/LocalAnimeSource.kt +++ b/app/src/main/java/tachiyomi/source/local/entries/anime/LocalAnimeSource.kt @@ -47,7 +47,11 @@ class LocalAnimeSource( override fun fetchLatestUpdates(page: Int) = fetchSearchAnime(page, "", LATEST_FILTERS) @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchAnime")) - override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable { + override fun fetchSearchAnime( + page: Int, + query: String, + filters: AnimeFilterList + ): Observable { return runBlocking { Observable.just(getSearchAnime(page, query, filters)) } diff --git a/app/src/main/res/anim/bounce_zoom.xml b/app/src/main/res/anim/bounce_zoom.xml index c19927cf..30827e89 100644 --- a/app/src/main/res/anim/bounce_zoom.xml +++ b/app/src/main/res/anim/bounce_zoom.xml @@ -1,9 +1,9 @@ \ No newline at end of file + android:toXScale="1.0" + android:toYScale="1.0" /> \ No newline at end of file diff --git a/app/src/main/res/drawable-night/widget_stats_rounded.xml b/app/src/main/res/drawable-night/widget_stats_rounded.xml index 933eab8b..c6cd5d5a 100644 --- a/app/src/main/res/drawable-night/widget_stats_rounded.xml +++ b/app/src/main/res/drawable-night/widget_stats_rounded.xml @@ -1,25 +1,27 @@ - + + android:bottomRightRadius="0dp" + android:topLeftRadius="28dp" + android:topRightRadius="28dp" /> + android:right="-3dp"> - + + android:bottomRightRadius="0dp" + android:topLeftRadius="28dp" + android:topRightRadius="28dp" /> diff --git a/app/src/main/res/drawable/adjust.xml b/app/src/main/res/drawable/adjust.xml index 6736f14f..155a57dd 100644 --- a/app/src/main/res/drawable/adjust.xml +++ b/app/src/main/res/drawable/adjust.xml @@ -1,10 +1,10 @@ - + android:viewportHeight="960"> + diff --git a/app/src/main/res/drawable/backup_restore.xml b/app/src/main/res/drawable/backup_restore.xml index 2c551ca4..0348cbe3 100644 --- a/app/src/main/res/drawable/backup_restore.xml +++ b/app/src/main/res/drawable/backup_restore.xml @@ -1,11 +1,11 @@ - + android:viewportWidth="960" + android:viewportHeight="960"> + diff --git a/app/src/main/res/drawable/blur_on.xml b/app/src/main/res/drawable/blur_on.xml index 11630fa4..fba75c2b 100644 --- a/app/src/main/res/drawable/blur_on.xml +++ b/app/src/main/res/drawable/blur_on.xml @@ -1,10 +1,10 @@ - + android:viewportHeight="960"> + diff --git a/app/src/main/res/drawable/bottom_sheet_background.xml b/app/src/main/res/drawable/bottom_sheet_background.xml index 77a184e8..4ecbb367 100644 --- a/app/src/main/res/drawable/bottom_sheet_background.xml +++ b/app/src/main/res/drawable/bottom_sheet_background.xml @@ -2,8 +2,8 @@ android:shape="rectangle"> + android:bottomRightRadius="0dp" + android:topLeftRadius="16dp" + android:topRightRadius="16dp" /> diff --git a/app/src/main/res/drawable/cast_warning.xml b/app/src/main/res/drawable/cast_warning.xml index 15f35a55..525cfb5b 100644 --- a/app/src/main/res/drawable/cast_warning.xml +++ b/app/src/main/res/drawable/cast_warning.xml @@ -1,10 +1,10 @@ - + android:viewportHeight="960"> + diff --git a/app/src/main/res/drawable/discord_status_dnd.xml b/app/src/main/res/drawable/discord_status_dnd.xml index db415306..b37a12a7 100644 --- a/app/src/main/res/drawable/discord_status_dnd.xml +++ b/app/src/main/res/drawable/discord_status_dnd.xml @@ -1,5 +1,12 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/discord_status_idle.xml b/app/src/main/res/drawable/discord_status_idle.xml index ed0e0880..3a64bb60 100644 --- a/app/src/main/res/drawable/discord_status_idle.xml +++ b/app/src/main/res/drawable/discord_status_idle.xml @@ -1,5 +1,12 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/discord_status_online.xml b/app/src/main/res/drawable/discord_status_online.xml index cc81654f..cdbb698d 100644 --- a/app/src/main/res/drawable/discord_status_online.xml +++ b/app/src/main/res/drawable/discord_status_online.xml @@ -1,5 +1,12 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/gradient_background.xml b/app/src/main/res/drawable/gradient_background.xml index d4b19a1f..6d10feed 100644 --- a/app/src/main/res/drawable/gradient_background.xml +++ b/app/src/main/res/drawable/gradient_background.xml @@ -4,5 +4,5 @@ + android:startColor="@color/theme" /> diff --git a/app/src/main/res/drawable/ic_camera_roll_24.xml b/app/src/main/res/drawable/ic_camera_roll_24.xml index 1c4b99e3..a6a2e756 100644 --- a/app/src/main/res/drawable/ic_camera_roll_24.xml +++ b/app/src/main/res/drawable/ic_camera_roll_24.xml @@ -1,13 +1,12 @@ - - + android:viewportHeight="24"> + - + android:pathData="M14,5c0,-1.1 -0.9,-2 -2,-2h-1L11,2c0,-0.55 -0.45,-1 -1,-1L6,1c-0.55,0 -1,0.45 -1,1v1L4,3c-1.1,0 -2,0.9 -2,2v15c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2h8L22,5h-8zM12,18h-2v-2h2v2zM12,9h-2L10,7h2v2zM16,18h-2v-2h2v2zM16,9h-2L14,7h2v2zM20,18h-2v-2h2v2zM20,9h-2L18,7h2v2z" /> + diff --git a/app/src/main/res/drawable/ic_circle_arrow_left_24.xml b/app/src/main/res/drawable/ic_circle_arrow_left_24.xml index 687a99d4..a9d8b65d 100644 --- a/app/src/main/res/drawable/ic_circle_arrow_left_24.xml +++ b/app/src/main/res/drawable/ic_circle_arrow_left_24.xml @@ -1,17 +1,16 @@ - + android:viewportHeight="24"> + android:rotation="90" + android:translateX="24"> + android:pathData="M12,4c4.41,0 8,3.59 8,8s-3.59,8 -8,8s-8,-3.59 -8,-8S7.59,4 12,4M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2L12,2zM13,12l0,-4h-2l0,4H8l4,4l4,-4H13z" /> diff --git a/app/src/main/res/drawable/ic_globe_24.xml b/app/src/main/res/drawable/ic_globe_24.xml index ff6e71b0..3b7123a0 100644 --- a/app/src/main/res/drawable/ic_globe_24.xml +++ b/app/src/main/res/drawable/ic_globe_24.xml @@ -4,7 +4,7 @@ android:tint="?attr/colorControlNormal" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/ic_label_24.xml b/app/src/main/res/drawable/ic_label_24.xml index ad3f2592..57a9cd6d 100644 --- a/app/src/main/res/drawable/ic_label_24.xml +++ b/app/src/main/res/drawable/ic_label_24.xml @@ -4,7 +4,7 @@ android:tint="?attr/colorControlNormal" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/ic_label_off_24.xml b/app/src/main/res/drawable/ic_label_off_24.xml index d4dbdd00..5579a991 100644 --- a/app/src/main/res/drawable/ic_label_off_24.xml +++ b/app/src/main/res/drawable/ic_label_off_24.xml @@ -4,7 +4,7 @@ android:tint="?attr/colorControlNormal" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/ic_lightbulb_24.xml b/app/src/main/res/drawable/ic_lightbulb_24.xml index 487b9ff8..f3fea31d 100644 --- a/app/src/main/res/drawable/ic_lightbulb_24.xml +++ b/app/src/main/res/drawable/ic_lightbulb_24.xml @@ -1,10 +1,10 @@ - + android:viewportHeight="960"> + diff --git a/app/src/main/res/drawable/ic_open_24.xml b/app/src/main/res/drawable/ic_open_24.xml index 52b21320..345ab07b 100644 --- a/app/src/main/res/drawable/ic_open_24.xml +++ b/app/src/main/res/drawable/ic_open_24.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/ic_round_comment_24.xml b/app/src/main/res/drawable/ic_round_comment_24.xml index b39f06fa..d7a8f7de 100644 --- a/app/src/main/res/drawable/ic_round_comment_24.xml +++ b/app/src/main/res/drawable/ic_round_comment_24.xml @@ -4,7 +4,7 @@ android:tint="?attr/colorControlNormal" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/ic_round_globe_china_googlefonts.xml b/app/src/main/res/drawable/ic_round_globe_china_googlefonts.xml index 1a36e8f6..cae3bc7e 100644 --- a/app/src/main/res/drawable/ic_round_globe_china_googlefonts.xml +++ b/app/src/main/res/drawable/ic_round_globe_china_googlefonts.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/ic_round_globe_japan_googlefonts.xml b/app/src/main/res/drawable/ic_round_globe_japan_googlefonts.xml index 24803edd..d2624768 100644 --- a/app/src/main/res/drawable/ic_round_globe_japan_googlefonts.xml +++ b/app/src/main/res/drawable/ic_round_globe_japan_googlefonts.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/ic_round_globe_search_googlefonts.xml b/app/src/main/res/drawable/ic_round_globe_search_googlefonts.xml index 97b82b0e..09ec477f 100644 --- a/app/src/main/res/drawable/ic_round_globe_search_googlefonts.xml +++ b/app/src/main/res/drawable/ic_round_globe_search_googlefonts.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/ic_round_globe_south_korea_googlefonts.xml b/app/src/main/res/drawable/ic_round_globe_south_korea_googlefonts.xml index dff31e26..2c86d094 100644 --- a/app/src/main/res/drawable/ic_round_globe_south_korea_googlefonts.xml +++ b/app/src/main/res/drawable/ic_round_globe_south_korea_googlefonts.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/ic_round_globe_taiwan_googlefonts.xml b/app/src/main/res/drawable/ic_round_globe_taiwan_googlefonts.xml index 52052789..a6a37cd4 100644 --- a/app/src/main/res/drawable/ic_round_globe_taiwan_googlefonts.xml +++ b/app/src/main/res/drawable/ic_round_globe_taiwan_googlefonts.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/ic_round_help_outline.xml b/app/src/main/res/drawable/ic_round_help_outline.xml index 7f116ea4..fab6e157 100644 --- a/app/src/main/res/drawable/ic_round_help_outline.xml +++ b/app/src/main/res/drawable/ic_round_help_outline.xml @@ -1,11 +1,11 @@ - + android:viewportWidth="960" + android:viewportHeight="960"> + diff --git a/app/src/main/res/drawable/ic_round_magnet_24.xml b/app/src/main/res/drawable/ic_round_magnet_24.xml new file mode 100644 index 00000000..e3423f18 --- /dev/null +++ b/app/src/main/res/drawable/ic_round_magnet_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_round_no_scroll_bar.xml b/app/src/main/res/drawable/ic_round_no_scroll_bar.xml index c11b94ee..f425dc93 100644 --- a/app/src/main/res/drawable/ic_round_no_scroll_bar.xml +++ b/app/src/main/res/drawable/ic_round_no_scroll_bar.xml @@ -1,5 +1,12 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/ic_round_reply_24.xml b/app/src/main/res/drawable/ic_round_reply_24.xml index d7e5a744..7ac5e159 100644 --- a/app/src/main/res/drawable/ic_round_reply_24.xml +++ b/app/src/main/res/drawable/ic_round_reply_24.xml @@ -1,10 +1,11 @@ - + android:height="24dp" + android:alpha="0.9" + android:tint="?attr/colorControlNormal" + android:viewportWidth="960" + android:viewportHeight="960"> + android:pathData="M760,760v-160q0,-50 -35,-85t-85,-35L273,480l144,144 -57,56 -240,-240 240,-240 57,56 -144,144h367q83,0 141.5,58.5T840,600v160h-80Z" /> diff --git a/app/src/main/res/drawable/ic_round_search_sources_24.xml b/app/src/main/res/drawable/ic_round_search_sources_24.xml index 48e31b13..7691464c 100644 --- a/app/src/main/res/drawable/ic_round_search_sources_24.xml +++ b/app/src/main/res/drawable/ic_round_search_sources_24.xml @@ -1,17 +1,16 @@ - - + android:viewportHeight="24"> + - + android:pathData="M11.5,3.5m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" /> + - + android:pathData="M12.13,7.12c-0.17,-0.35 -0.44,-0.65 -0.8,-0.85C10.72,5.91 9.99,5.93 9.4,6.24l0,-0.01L4,9.3V14h2v-3.54l1.5,-0.85C7.18,10.71 7,11.85 7,13v5.33L4.4,21.8L6,23l3,-4l0.22,-3.54L11,18v5h2v-6.5l-1.97,-2.81c-0.04,-0.52 -0.14,-1.76 0.45,-3.4c0.75,1.14 1.88,1.98 3.2,2.41L20.63,23l0.87,-0.5L16.02,13H17v-2c-0.49,0 -2.88,0.17 -4.08,-2.21" /> + diff --git a/app/src/main/res/drawable/ic_round_upvote_active_24.xml b/app/src/main/res/drawable/ic_round_upvote_active_24.xml index 683aae88..4670fe7e 100644 --- a/app/src/main/res/drawable/ic_round_upvote_active_24.xml +++ b/app/src/main/res/drawable/ic_round_upvote_active_24.xml @@ -1,12 +1,11 @@ - + android:height="24dp" + android:alpha="0.9" + android:tint="?attr/colorControlNormal" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M4,14h4v7a1,1 0,0 0,1 1h6a1,1 0,0 0,1 -1v-7h4a1.001,1.001 0,0 0,0.781 -1.625l-8,-10c-0.381,-0.475 -1.181,-0.475 -1.562,0l-8,10A1.001,1.001 0,0 0,4 14z" /> diff --git a/app/src/main/res/drawable/ic_round_upvote_inactive_24.xml b/app/src/main/res/drawable/ic_round_upvote_inactive_24.xml index dceee11c..01c0c3fb 100644 --- a/app/src/main/res/drawable/ic_round_upvote_inactive_24.xml +++ b/app/src/main/res/drawable/ic_round_upvote_inactive_24.xml @@ -1,11 +1,11 @@ - + android:height="24dp" + android:alpha="0.9" + android:tint="?attr/colorControlNormal" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M12.781,2.375c-0.381,-0.475 -1.181,-0.475 -1.562,0l-8,10A1.001,1.001 0,0 0,4 14h4v7a1,1 0,0 0,1 1h6a1,1 0,0 0,1 -1v-7h4a1.001,1.001 0,0 0,0.781 -1.625l-8,-10zM15,12h-1v8h-4v-8H6.081L12,4.601 17.919,12H15z" /> diff --git a/app/src/main/res/drawable/ic_stats_24.xml b/app/src/main/res/drawable/ic_stats_24.xml index d0734d7b..1b9af2af 100644 --- a/app/src/main/res/drawable/ic_stats_24.xml +++ b/app/src/main/res/drawable/ic_stats_24.xml @@ -4,7 +4,7 @@ android:tint="?attr/colorControlNormal" android:viewportWidth="960" android:viewportHeight="960"> - + diff --git a/app/src/main/res/drawable/inbox_empty.xml b/app/src/main/res/drawable/inbox_empty.xml index bb6540aa..306c2176 100644 --- a/app/src/main/res/drawable/inbox_empty.xml +++ b/app/src/main/res/drawable/inbox_empty.xml @@ -1,5 +1,12 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/inbox_filled.xml b/app/src/main/res/drawable/inbox_filled.xml index 08ad4bf1..8dbb33de 100644 --- a/app/src/main/res/drawable/inbox_filled.xml +++ b/app/src/main/res/drawable/inbox_filled.xml @@ -1,5 +1,12 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/invert_all_boxes.xml b/app/src/main/res/drawable/invert_all_boxes.xml index 2a5d5763..6525bc2b 100644 --- a/app/src/main/res/drawable/invert_all_boxes.xml +++ b/app/src/main/res/drawable/invert_all_boxes.xml @@ -1,7 +1,18 @@ - - - - - - + + + + + + diff --git a/app/src/main/res/drawable/notification_circle.xml b/app/src/main/res/drawable/notification_circle.xml index d864d738..2e511007 100644 --- a/app/src/main/res/drawable/notification_circle.xml +++ b/app/src/main/res/drawable/notification_circle.xml @@ -1,8 +1,8 @@ - + + android:height="100dp" /> diff --git a/app/src/main/res/drawable/notification_icon.xml b/app/src/main/res/drawable/notification_icon.xml index 29e3dd6d..547667af 100644 --- a/app/src/main/res/drawable/notification_icon.xml +++ b/app/src/main/res/drawable/notification_icon.xml @@ -1,10 +1,17 @@ - + - - - + + + diff --git a/app/src/main/res/drawable/stacks.xml b/app/src/main/res/drawable/stacks.xml index 29383b41..a77bff77 100644 --- a/app/src/main/res/drawable/stacks.xml +++ b/app/src/main/res/drawable/stacks.xml @@ -1,10 +1,10 @@ - + android:viewportHeight="960"> + diff --git a/app/src/main/res/drawable/tick_all_boxes.xml b/app/src/main/res/drawable/tick_all_boxes.xml index 59eddd41..c76b8d16 100644 --- a/app/src/main/res/drawable/tick_all_boxes.xml +++ b/app/src/main/res/drawable/tick_all_boxes.xml @@ -1,5 +1,12 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/trail_length_short.xml b/app/src/main/res/drawable/trail_length_short.xml index 81166f10..eb6ab76a 100644 --- a/app/src/main/res/drawable/trail_length_short.xml +++ b/app/src/main/res/drawable/trail_length_short.xml @@ -1,10 +1,10 @@ - + android:viewportHeight="960"> + diff --git a/app/src/main/res/drawable/untick_all_boxes.xml b/app/src/main/res/drawable/untick_all_boxes.xml index 4e86043d..7a7bf1bc 100644 --- a/app/src/main/res/drawable/untick_all_boxes.xml +++ b/app/src/main/res/drawable/untick_all_boxes.xml @@ -1,5 +1,12 @@ - - - - + + + + diff --git a/app/src/main/res/drawable/view_list_24.xml b/app/src/main/res/drawable/view_list_24.xml index f7378f12..450a12bc 100644 --- a/app/src/main/res/drawable/view_list_24.xml +++ b/app/src/main/res/drawable/view_list_24.xml @@ -1,11 +1,11 @@ - + android:viewportWidth="960" + android:viewportHeight="960"> + diff --git a/app/src/main/res/drawable/widget_stats_rounded.xml b/app/src/main/res/drawable/widget_stats_rounded.xml index 2b32dd96..b5f192d5 100644 --- a/app/src/main/res/drawable/widget_stats_rounded.xml +++ b/app/src/main/res/drawable/widget_stats_rounded.xml @@ -1,25 +1,27 @@ - + + android:bottomRightRadius="0dp" + android:topLeftRadius="28dp" + android:topRightRadius="28dp" /> + android:right="-3dp"> - + + android:bottomRightRadius="0dp" + android:topLeftRadius="28dp" + android:topRightRadius="28dp" /> diff --git a/app/src/main/res/layout-land/activity_media.xml b/app/src/main/res/layout-land/activity_media.xml index d4a133f3..08942377 100644 --- a/app/src/main/res/layout-land/activity_media.xml +++ b/app/src/main/res/layout-land/activity_media.xml @@ -307,7 +307,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:paddingTop="16dp" - android:visibility="gone"/> + android:visibility="gone" /> + app:itemTextColor="@color/tab_layout_icon" /> diff --git a/app/src/main/res/layout-land/activity_profile.xml b/app/src/main/res/layout-land/activity_profile.xml index b4844201..76eca50c 100644 --- a/app/src/main/res/layout-land/activity_profile.xml +++ b/app/src/main/res/layout-land/activity_profile.xml @@ -46,13 +46,14 @@ android:id="@+id/profileNavBar" android:layout_width="wrap_content" android:layout_height="match_parent" - android:layout_weight="0" android:layout_gravity="center_horizontal|bottom" + android:layout_weight="0" android:background="?attr/colorSurface" android:padding="0dp" app:abb_animationInterpolator="@anim/over_shoot" app:abb_indicatorAppearance="round" app:abb_indicatorLocation="top" + app:abb_isVerticalBar="true" app:abb_selectedTabType="text" app:abb_textAppearance="@style/NavBarText" app:itemActiveIndicatorStyle="@style/BottomNavBar" @@ -60,7 +61,6 @@ app:itemRippleColor="#00000000" app:itemTextAppearanceActive="@style/NavBarText" app:itemTextAppearanceInactive="@style/NavBarText" - app:itemTextColor="@color/tab_layout_icon" - app:abb_isVerticalBar="true"/> + app:itemTextColor="@color/tab_layout_icon" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_character.xml b/app/src/main/res/layout/activity_character.xml index dcc8d70e..ccc2af4d 100644 --- a/app/src/main/res/layout/activity_character.xml +++ b/app/src/main/res/layout/activity_character.xml @@ -57,11 +57,11 @@ android:id="@+id/characterTitle" android:layout_width="0dp" android:layout_height="match_parent" - android:layout_weight="1" + android:layout_marginStart="24dp" + android:layout_weight="1" android:ellipsize="marquee" android:focusable="true" - android:layout_marginStart="24dp" android:focusableInTouchMode="true" android:fontFamily="@font/poppins_bold" android:gravity="center_vertical" diff --git a/app/src/main/res/layout/activity_extensions.xml b/app/src/main/res/layout/activity_extensions.xml index 4600cf50..cc6b4da7 100644 --- a/app/src/main/res/layout/activity_extensions.xml +++ b/app/src/main/res/layout/activity_extensions.xml @@ -86,17 +86,16 @@ android:layout_height="match_parent" android:layout_gravity="bottom" android:layout_weight="1" - android:visibility="gone"> - + android:visibility="gone"> - + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_follow.xml b/app/src/main/res/layout/activity_follow.xml index 7b8e8ac5..3ff66743 100644 --- a/app/src/main/res/layout/activity_follow.xml +++ b/app/src/main/res/layout/activity_follow.xml @@ -112,8 +112,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:nestedScrollingEnabled="true" - tools:listitem="@layout/item_follower" - android:requiresFadingEdge="vertical" /> + android:requiresFadingEdge="vertical" + tools:listitem="@layout/item_follower" /> diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index c00db1c6..33bc4e09 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 8565d548..902db405 100644 --- a/app/src/main/res/layout/activity_settings_anime.xml +++ b/app/src/main/res/layout/activity_settings_anime.xml @@ -62,8 +62,8 @@ @@ -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 d59e42f4..5cfd39a1 100644 --- a/app/src/main/res/layout/activity_settings_common.xml +++ b/app/src/main/res/layout/activity_settings_common.xml @@ -63,7 +63,7 @@ @@ -135,7 +135,7 @@ @@ -144,7 +144,7 @@ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginHorizontal="32dp" + android:layout_marginHorizontal="16dp" android:layout_marginBottom="8dp" app:boxCornerRadiusBottomEnd="8dp" app:boxCornerRadiusBottomStart="8dp" @@ -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 0e8e51fb..b681645d 100644 --- a/app/src/main/res/layout/activity_settings_extensions.xml +++ b/app/src/main/res/layout/activity_settings_extensions.xml @@ -51,85 +51,20 @@ -