diff --git a/app/build.gradle b/app/build.gradle index 5caa29de..eb978148 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,18 +21,18 @@ android { minSdk 23 targetSdk 34 versionCode ((System.currentTimeMillis() / 60000).toInteger()) - versionName "1.0.0-beta03i" + versionName "1.0.0-beta03i-2" signingConfig signingConfigs.debug } buildTypes { debug { applicationIdSuffix ".beta" - manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta"] + manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"] debuggable true } release { - manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher"] + manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"] debuggable false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } @@ -64,9 +64,10 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.google.code.gson:gson:2.8.9' - implementation 'com.github.Blatzar:NiceHttp:0.4.3' + implementation 'com.github.Blatzar:NiceHttp:0.4.4' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.webkit:webkit:1.9.0' // Glide ext.glide_version = '4.16.0' @@ -99,6 +100,7 @@ dependencies { implementation 'com.alexvasilkov:gesture-views:2.8.3' implementation 'com.github.VipulOG:ebook-reader:0.1.6' implementation 'androidx.paging:paging-runtime-ktx:3.2.1' + implementation "com.github.skydoves:colorpickerview:2.3.0" // string matching implementation 'me.xdrop:fuzzywuzzy:1.4.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 84d17a8f..a77c0d31 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -52,7 +52,7 @@ android:label="@string/app_name" android:largeHeap="true" android:requestLegacyExternalStorage="true" - android:roundIcon="@mipmap/ic_launcher_round" + android:roundIcon="${icon_placeholder_round}" android:supportsRtl="true" android:theme="@style/Theme.Dantotsu" android:usesCleartextTraffic="true" @@ -276,6 +276,14 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/App.kt b/app/src/main/java/ani/dantotsu/App.kt index 109ced31..89a5d441 100644 --- a/app/src/main/java/ani/dantotsu/App.kt +++ b/app/src/main/java/ani/dantotsu/App.kt @@ -8,14 +8,15 @@ import androidx.multidex.MultiDex import androidx.multidex.MultiDexApplication import ani.dantotsu.aniyomi.anime.custom.AppModule import ani.dantotsu.aniyomi.anime.custom.PreferenceModule -import eu.kanade.tachiyomi.data.notification.Notifications -import tachiyomi.core.util.system.logcat import ani.dantotsu.others.DisabledReports import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.MangaSources +import ani.dantotsu.parsers.NovelSources +import ani.dantotsu.parsers.novel.NovelExtensionManager import com.google.android.material.color.DynamicColors import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase +import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import kotlinx.coroutines.CoroutineScope @@ -25,14 +26,16 @@ import kotlinx.coroutines.launch import logcat.AndroidLogcatLogger import logcat.LogPriority import logcat.LogcatLogger +import tachiyomi.core.util.system.logcat import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.Locale + @SuppressLint("StaticFieldLeak") class App : MultiDexApplication() { private lateinit var animeExtensionManager: AnimeExtensionManager private lateinit var mangaExtensionManager: MangaExtensionManager + private lateinit var novelExtensionManager: NovelExtensionManager override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) MultiDex.install(this) @@ -48,17 +51,19 @@ class App : MultiDexApplication() { super.onCreate() val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false) - if(useMaterialYou) { + if (useMaterialYou) { DynamicColors.applyToActivitiesIfAvailable(this) + //TODO: HarmonizedColors } registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks) Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports) - initializeNetwork(baseContext) Injekt.importModule(AppModule(this)) Injekt.importModule(PreferenceModule(this)) + initializeNetwork(baseContext) + setupNotificationChannels() if (!LogcatLogger.isInstalled) { LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) @@ -66,6 +71,7 @@ class App : MultiDexApplication() { animeExtensionManager = Injekt.get() mangaExtensionManager = Injekt.get() + novelExtensionManager = Injekt.get() val animeScope = CoroutineScope(Dispatchers.Default) animeScope.launch { @@ -79,9 +85,16 @@ class App : MultiDexApplication() { logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}") MangaSources.init(mangaExtensionManager.installedExtensionsFlow) } + val novelScope = CoroutineScope(Dispatchers.Default) + novelScope.launch { + novelExtensionManager.findAvailableExtensions() + logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}") + NovelSources.init(novelExtensionManager.installedExtensionsFlow) + } } + private fun setupNotificationChannels() { try { Notifications.createChannels(this) @@ -109,7 +122,7 @@ class App : MultiDexApplication() { companion object { private var instance: App? = null - var context : Context? = null + var context: Context? = null fun currentContext(): Context? { return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context } diff --git a/app/src/main/java/ani/dantotsu/Functions.kt b/app/src/main/java/ani/dantotsu/Functions.kt index d7b7469a..6a789743 100644 --- a/app/src/main/java/ani/dantotsu/Functions.kt +++ b/app/src/main/java/ani/dantotsu/Functions.kt @@ -132,9 +132,10 @@ fun loadData(fileName: String, context: Context? = null, toast: Boolean = tr fun initActivity(a: Activity) { val window = a.window WindowCompat.setDecorFitsSystemWindows(window, false) - val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { - saveData("ui_settings", this) - } + val uiSettings = loadData("ui_settings", toast = false) + ?: UserInterfaceSettings().apply { + saveData("ui_settings", this) + } uiSettings.darkMode.apply { AppCompatDelegate.setDefaultNightMode( when (this) { @@ -146,9 +147,10 @@ fun initActivity(a: Activity) { } if (uiSettings.immersiveMode) { if (navBarHeight == 0) { - ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))?.apply { - navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom - } + ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content)) + ?.apply { + navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom + } } a.hideStatusBar() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { @@ -160,7 +162,8 @@ fun initActivity(a: Activity) { } } else if (statusBarHeight == 0) { - val windowInsets = ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content)) + val windowInsets = + ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content)) if (windowInsets != null) { val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) statusBarHeight = insets.top @@ -205,7 +208,8 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() { } fun isOnline(context: Context): Boolean { - val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager return tryWith { val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) return@tryWith if (cap != null) { @@ -219,7 +223,7 @@ fun isOnline(context: Context): Boolean { cap.hasTransport(TRANSPORT_WIFI) || cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true - else -> false + else -> false } } else false } ?: false @@ -239,7 +243,8 @@ fun startMainActivity(activity: Activity, bundle: Bundle? = null) { } -class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().getToday()) : DialogFragment(), +class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().getToday()) : + DialogFragment(), DatePickerDialog.OnDateSetListener { var dialog: DatePickerDialog @@ -264,9 +269,20 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g } } -class InputFilterMinMax(private val min: Double, private val max: Double, private val status: AutoCompleteTextView? = null) : +class InputFilterMinMax( + private val min: Double, + private val max: Double, + private val status: AutoCompleteTextView? = null +) : InputFilter { - override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? { + override fun filter( + source: CharSequence, + start: Int, + end: Int, + dest: Spanned, + dstart: Int, + dend: Int + ): CharSequence? { try { val input = (dest.toString() + source.toString()).toDouble() if (isInRange(min, max, input)) return null @@ -289,11 +305,20 @@ class InputFilterMinMax(private val min: Double, private val max: Double, privat } -class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) : ViewPager2.PageTransformer { +class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) : + ViewPager2.PageTransformer { override fun transformPage(view: View, position: Float) { if (position == 0.0f && uiSettings.layoutAnimations) { - setAnimation(view.context, view, uiSettings, 300, floatArrayOf(1.3f, 1f, 1.3f, 1f), 0.5f to 0f) - ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f).setDuration((200 * uiSettings.animationSpeed).toLong()).start() + setAnimation( + view.context, + view, + uiSettings, + 300, + floatArrayOf(1.3f, 1f, 1.3f, 1f), + 0.5f to 0f + ) + ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f) + .setDuration((200 * uiSettings.animationSpeed).toLong()).start() } } } @@ -328,7 +353,11 @@ class FadingEdgeRecyclerView : RecyclerView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) override fun isPaddingOffsetRequired(): Boolean { return !clipToPadding @@ -414,7 +443,7 @@ fun MutableList.sortByTitle(string: String) { } fun String.findBetween(a: String, b: String): String? { - val string = substringAfter(a, "").substringBefore(b,"") + val string = substringAfter(a, "").substringBefore(b, "") return string.ifEmpty { null } } @@ -423,8 +452,7 @@ fun ImageView.loadImage(url: String?, size: Int = 0) { val localFile = File(url) if (localFile.exists()) { loadLocalImage(localFile, size) - } - else { + } else { loadImage(FileUrl(url), size) } } @@ -434,7 +462,8 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) { if (file?.url?.isNotEmpty() == true) { tryWith { val glideUrl = GlideUrl(file.url) { file.headers } - Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size).into(this) + Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size) + .into(this) } } } @@ -442,7 +471,8 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) { fun ImageView.loadLocalImage(file: File?, size: Int = 0) { if (file?.exists() == true) { tryWith { - Glide.with(this.context).load(file).transition(withCrossFade()).override(size).into(this) + Glide.with(this.context).load(file).transition(withCrossFade()).override(size) + .into(this) } } } @@ -500,7 +530,12 @@ abstract class GesturesListener : GestureDetector.SimpleOnGestureListener() { return super.onDoubleTap(e) } - override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean { onScrollYClick(distanceY) onScrollXClick(distanceX) return super.onScroll(e1, e2, distanceX, distanceY) @@ -642,9 +677,15 @@ fun countDown(media: Media, view: ViewGroup) { val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false) view.addView(v.root, 0) v.mediaCountdownText.text = - currActivity()?.getString(R.string.episode_release_countdown, media.anime.nextAiringEpisode!! + 1) + currActivity()?.getString( + R.string.episode_release_countdown, + media.anime.nextAiringEpisode!! + 1 + ) - object : CountDownTimer((media.anime.nextAiringEpisodeTime!! + 10000) * 1000 - System.currentTimeMillis(), 1000) { + object : CountDownTimer( + (media.anime.nextAiringEpisodeTime!! + 10000) * 1000 - System.currentTimeMillis(), + 1000 + ) { override fun onTick(millisUntilFinished: Long) { val a = millisUntilFinished / 1000 v.mediaCountdown.text = currActivity()?.getString( @@ -735,7 +776,8 @@ fun toast(string: String?) { if (string != null) { logger(string) MainScope().launch { - Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT).show() + Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT) + .show() } } } @@ -744,7 +786,11 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul if (s != null) { (activity ?: currActivity())?.apply { runOnUiThread { - val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_SHORT) + val snackBar = Snackbar.make( + window.decorView.findViewById(android.R.id.content), + s, + Snackbar.LENGTH_SHORT + ) snackBar.view.apply { updateLayoutParams { gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) @@ -769,7 +815,8 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul } } -open class NoPaddingArrayAdapter(context: Context, layoutId: Int, items: List) : ArrayAdapter(context, layoutId, items) { +open class NoPaddingArrayAdapter(context: Context, layoutId: Int, items: List) : + ArrayAdapter(context, layoutId, items) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view = super.getView(position, convertView, parent) view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom) @@ -790,16 +837,21 @@ class SpinnerNoSwipe : androidx.appcompat.widget.AppCompatSpinner { setup() } - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { setup() } private fun setup() { - mGestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { - override fun onSingleTapUp(e: MotionEvent): Boolean { - return performClick() - } - }) + mGestureDetector = + GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { + override fun onSingleTapUp(e: MotionEvent): Boolean { + return performClick() + } + }) } override fun onTouchEvent(event: MotionEvent): Boolean { @@ -843,7 +895,11 @@ fun getCurrentBrightnessValue(context: Context): Float { } fun getCur(): Float { - return Settings.System.getInt(context.contentResolver, Settings.System.SCREEN_BRIGHTNESS, 127).toFloat() + return Settings.System.getInt( + context.contentResolver, + Settings.System.SCREEN_BRIGHTNESS, + 127 + ).toFloat() } return brightnessConverter(getCur() / getMax(), true) @@ -865,12 +921,12 @@ fun checkCountry(context: Context): Boolean { tz.equals("Asia/Kolkata", ignoreCase = true) } - TelephonyManager.SIM_STATE_READY -> { + TelephonyManager.SIM_STATE_READY -> { val countryCodeValue = telMgr.networkCountryIso countryCodeValue.equals("in", ignoreCase = true) } - else -> false + else -> false } } diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt index 4cea1e39..b27ee17d 100644 --- a/app/src/main/java/ani/dantotsu/MainActivity.kt +++ b/app/src/main/java/ani/dantotsu/MainActivity.kt @@ -1,15 +1,9 @@ package ani.dantotsu import android.animation.ObjectAnimator -import android.annotation.SuppressLint -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.content.pm.PackageManager -import android.graphics.Color import android.graphics.drawable.Animatable -import android.graphics.drawable.ColorDrawable import android.graphics.drawable.GradientDrawable import android.net.Uri import android.os.Build @@ -17,18 +11,14 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.provider.Settings -import android.util.Log import android.view.View import android.view.ViewGroup import android.view.animation.AnticipateInterpolator -import android.widget.FrameLayout import android.widget.TextView import androidx.activity.addCallback import androidx.activity.viewModels -import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.animation.doOnEnd -import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.view.doOnAttach import androidx.core.view.updateLayoutParams @@ -37,13 +27,10 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.viewpager2.adapter.FragmentStateAdapter -import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.AnilistHomeViewModel import ani.dantotsu.databinding.ActivityMainBinding -import ani.dantotsu.databinding.ItemNavbarBinding import ani.dantotsu.databinding.SplashScreenBinding -import ani.dantotsu.download.manga.OfflineMangaFragment import ani.dantotsu.home.AnimeFragment import ani.dantotsu.home.HomeFragment import ani.dantotsu.home.LoginFragment @@ -51,27 +38,17 @@ import ani.dantotsu.home.MangaFragment import ani.dantotsu.home.NoInternet import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.others.CustomBottomDialog -import ani.dantotsu.parsers.AnimeSources -import ani.dantotsu.parsers.MangaSources -import ani.dantotsu.settings.SettingsActivity +import ani.dantotsu.others.LangSet import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet -import com.google.firebase.crashlytics.FirebaseCrashlytics -import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import io.noties.markwon.Markwon import io.noties.markwon.SoftBreakAddsNewLinePlugin -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import nl.joery.animatedbottombar.AnimatedBottomBar -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy import java.io.Serializable @@ -82,6 +59,7 @@ class MainActivity : AppCompatActivity() { private var uiSettings = UserInterfaceSettings() + override fun onCreate(savedInstanceState: Bundle?) { ThemeManager(this).applyTheme() LangSet.setLocale(this) @@ -266,10 +244,6 @@ class MainActivity : AppCompatActivity() { } - override fun onResume() { - super.onResume() - } - //ViewPager private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : diff --git a/app/src/main/java/ani/dantotsu/Network.kt b/app/src/main/java/ani/dantotsu/Network.kt index 58c9d6fc..dd58849a 100644 --- a/app/src/main/java/ani/dantotsu/Network.kt +++ b/app/src/main/java/ani/dantotsu/Network.kt @@ -8,58 +8,51 @@ import ani.dantotsu.others.webview.WebViewBottomDialog import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser import com.lagradost.nicehttp.addGenericDns -import kotlinx.coroutines.* +import eu.kanade.tachiyomi.network.NetworkHelper import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.serializer -import okhttp3.Cache import okhttp3.OkHttpClient -import java.io.File +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.io.PrintWriter import java.io.Serializable import java.io.StringWriter -import java.util.concurrent.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import kotlin.reflect.KClass import kotlin.reflect.KFunction -val defaultHeaders = mapOf( - "User-Agent" to - "Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36" - .format(Build.VERSION.RELEASE, Build.MODEL) -) -lateinit var cache: Cache +lateinit var defaultHeaders: Map lateinit var okHttpClient: OkHttpClient lateinit var client: Requests fun initializeNetwork(context: Context) { - val dns = loadData("settings_dns") - cache = Cache( - File(context.cacheDir, "http_cache"), - 5 * 1024L * 1024L // 5 MiB + + val networkHelper = Injekt.get() + + defaultHeaders = mapOf( + "User-Agent" to + Injekt.get().defaultUserAgentProvider() + .format(Build.VERSION.RELEASE, Build.MODEL) ) - okHttpClient = OkHttpClient.Builder() - .followRedirects(true) - .followSslRedirects(true) - .cache(cache) - .apply { - when (dns) { - 1 -> addGoogleDns() - 2 -> addCloudFlareDns() - 3 -> addAdGuardDns() - } - } - .build() + + okHttpClient = networkHelper.client client = Requests( - okHttpClient, + networkHelper.client, defaultHeaders, defaultCacheTime = 6, defaultCacheTimeUnit = TimeUnit.HOURS, responseParser = Mapper ) + } object Mapper : ResponseParser { @@ -122,7 +115,11 @@ fun tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T): } } -suspend fun tryWithSuspend(post: Boolean = false, snackbar: Boolean = true, call: suspend () -> T): T? { +suspend fun tryWithSuspend( + post: Boolean = false, + snackbar: Boolean = true, + call: suspend () -> T +): T? { return try { call.invoke() } catch (e: Throwable) { @@ -202,28 +199,29 @@ fun OkHttpClient.Builder.addAdGuardDns() = ( @Suppress("BlockingMethodInNonBlockingContext") suspend fun webViewInterface(webViewDialog: WebViewBottomDialog): Map? { - var map : Map? = null + var map: Map? = null val latch = CountDownLatch(1) webViewDialog.callback = { map = it latch.countDown() } - val fragmentManager = (currContext() as FragmentActivity?)?.supportFragmentManager ?: return null + val fragmentManager = + (currContext() as FragmentActivity?)?.supportFragmentManager ?: return null webViewDialog.show(fragmentManager, "web-view") delay(0) - latch.await(2,TimeUnit.MINUTES) + latch.await(2, TimeUnit.MINUTES) return map } suspend fun webViewInterface(type: String, url: FileUrl): Map? { val webViewDialog: WebViewBottomDialog = when (type) { "Cloudflare" -> CloudFlare.newInstance(url) - else -> return null + else -> return null } return webViewInterface(webViewDialog) } suspend fun webViewInterface(type: String, url: String): Map? { - return webViewInterface(type,FileUrl(url)) + return webViewInterface(type, FileUrl(url)) } \ 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 ef2af1e9..38b9fc4c 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 @@ -4,18 +4,20 @@ package ani.dantotsu.aniyomi.anime.custom import android.app.Application import android.content.Context import androidx.core.content.ContextCompat +import ani.dantotsu.download.DownloadsManager import ani.dantotsu.media.manga.MangaCache -import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager -import tachiyomi.core.preference.PreferenceStore +import ani.dantotsu.parsers.novel.NovelExtensionManager import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore +import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager import kotlinx.serialization.json.Json +import tachiyomi.core.preference.PreferenceStore import tachiyomi.domain.source.anime.service.AnimeSourceManager import tachiyomi.domain.source.manga.service.MangaSourceManager import uy.kohesive.injekt.api.InjektModule @@ -23,7 +25,6 @@ import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.addSingleton import uy.kohesive.injekt.api.addSingletonFactory import uy.kohesive.injekt.api.get -import ani.dantotsu.download.DownloadsManager class AppModule(val app: Application) : InjektModule { override fun InjektRegistrar.registerInjectables() { @@ -35,6 +36,7 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { AnimeExtensionManager(app) } addSingletonFactory { MangaExtensionManager(app) } + addSingletonFactory { NovelExtensionManager(app) } addSingletonFactory { AndroidAnimeSourceManager(app, get()) } addSingletonFactory { AndroidMangaSourceManager(app, get()) } diff --git a/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt b/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt index c930ab67..6917aade 100644 --- a/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt +++ b/app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt @@ -16,7 +16,7 @@ fun updateProgress(media: Media, number: String) { if (Anilist.userid != null) { CoroutineScope(Dispatchers.IO).launch { val a = number.toFloatOrNull()?.roundToInt() - if ((a?:0) > (media.userProgress?:0)) { + if ((a ?: 0) > (media.userProgress ?: 0)) { Anilist.mutation.editList( media.id, a, 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 231a9483..77325d0b 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt @@ -10,7 +10,7 @@ import ani.dantotsu.currContext import ani.dantotsu.openLinkInBrowser import ani.dantotsu.tryWithSuspend import java.io.File -import java.util.* +import java.util.Calendar object Anilist { val query: AnilistQueries = AnilistQueries() @@ -29,7 +29,12 @@ object Anilist { var tags: Map>? = null val sortBy = listOf( - "SCORE_DESC","POPULARITY_DESC","TRENDING_DESC","TITLE_ENGLISH","TITLE_ENGLISH_DESC","SCORE" + "SCORE_DESC", + "POPULARITY_DESC", + "TRENDING_DESC", + "TITLE_ENGLISH", + "TITLE_ENGLISH_DESC", + "SCORE" ) val seasons = listOf( @@ -51,11 +56,11 @@ object Anilist { private val cal: Calendar = Calendar.getInstance() private val currentYear = cal.get(Calendar.YEAR) private val currentSeason: Int = when (cal.get(Calendar.MONTH)) { - 0, 1, 2 -> 0 - 3, 4, 5 -> 1 - 6, 7, 8 -> 2 + 0, 1, 2 -> 0 + 3, 4, 5 -> 1 + 6, 7, 8 -> 2 9, 10, 11 -> 3 - else -> 0 + else -> 0 } private fun getSeason(next: Boolean): Pair { @@ -132,7 +137,12 @@ object Anilist { if (token != null || force) { if (token != null && useToken) headers["Authorization"] = "Bearer $token" - val json = client.post("https://graphql.anilist.co/", headers, data = data, cacheTime = cache ?: 10) + val json = client.post( + "https://graphql.anilist.co/", + headers, + data = data, + cacheTime = cache ?: 10 + ) if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down)) if (show) println("Response : ${json.text}") json.parsed() diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt index da50162c..686381f1 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt @@ -20,7 +20,7 @@ class AnilistMutations { repeat: Int? = null, notes: String? = null, status: String? = null, - private:Boolean? = null, + private: Boolean? = null, startedAt: FuzzyDate? = null, completedAt: FuzzyDate? = null, customList: List? = null @@ -41,7 +41,7 @@ class AnilistMutations { ${if (repeat != null) ""","repeat":$repeat""" else ""} ${if (notes != null) ""","notes":"${notes.replace("\n", "\\n")}"""" else ""} ${if (status != null) ""","status":"$status"""" else ""} - ${if (customList !=null) ""","customLists":[${customList.joinToString { "\"$it\"" }}]""" else ""} + ${if (customList != null) ""","customLists":[${customList.joinToString { "\"$it\"" }}]""" else ""} }""".replace("\n", "").replace(""" """, "") println(variables) executeQuery(query, variables, show = true) 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 21bcccc3..a0c1c270 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -2,13 +2,13 @@ package ani.dantotsu.connections.anilist import android.app.Activity import ani.dantotsu.R +import ani.dantotsu.checkGenreTime +import ani.dantotsu.checkId import ani.dantotsu.connections.anilist.Anilist.authorRoles import ani.dantotsu.connections.anilist.Anilist.executeQuery import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.Page import ani.dantotsu.connections.anilist.api.Query -import ani.dantotsu.checkGenreTime -import ani.dantotsu.checkId import ani.dantotsu.currContext import ani.dantotsu.loadData import ani.dantotsu.logError @@ -113,9 +113,13 @@ class AnilistQueries { name = i.node?.name?.userPreferred, image = i.node?.image?.medium, banner = media.banner ?: media.cover, - role = when (i.role.toString()){ - "MAIN" -> currContext()?.getString(R.string.main_role) ?: "MAIN" - "SUPPORTING" -> currContext()?.getString(R.string.supporting_role) ?: "SUPPORTING" + role = when (i.role.toString()) { + "MAIN" -> currContext()?.getString(R.string.main_role) + ?: "MAIN" + + "SUPPORTING" -> currContext()?.getString(R.string.supporting_role) + ?: "SUPPORTING" + else -> i.role.toString() } ) @@ -129,11 +133,16 @@ class AnilistQueries { val m = Media(mediaEdge) media.relations?.add(m) if (m.relation == "SEQUEL") { - media.sequel = if ((media.sequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.sequel + media.sequel = + if ((media.sequel?.popularity ?: 0) < (m.popularity + ?: 0) + ) m else media.sequel } else if (m.relation == "PREQUEL") { media.prequel = - if ((media.prequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.prequel + if ((media.prequel?.popularity ?: 0) < (m.popularity + ?: 0) + ) m else media.prequel } } media.relations?.sortByDescending { it.popularity } @@ -199,17 +208,19 @@ class AnilistQueries { ) } - media.anime.nextAiringEpisodeTime = fetchedMedia.nextAiringEpisode?.airingAt?.toLong() + media.anime.nextAiringEpisodeTime = + fetchedMedia.nextAiringEpisode?.airingAt?.toLong() fetchedMedia.externalLinks?.forEach { i -> when (i.site.lowercase()) { - "youtube" -> media.anime.youtube = i.url - "crunchyroll" -> media.crunchySlug = i.url?.split("/")?.getOrNull(3) - "vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4) + "youtube" -> media.anime.youtube = i.url + "crunchyroll" -> media.crunchySlug = + i.url?.split("/")?.getOrNull(3) + + "vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4) } } - } - else if (media.manga != null) { + } else if (media.manga != null) { fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let { media.manga.author = Author( it.id.toString(), @@ -241,10 +252,10 @@ class AnilistQueries { return media } - suspend fun continueMedia(type: String,planned:Boolean=false): ArrayList { + suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList { val returnArray = arrayListOf() val map = mutableMapOf() - val statuses = if(!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING") + val statuses = if (!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING") suspend fun repeat(status: String) { val response = executeQuery(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """) @@ -275,21 +286,21 @@ class AnilistQueries { var hasNextPage = true var page = 0 - suspend fun getNextPage(page:Int): List { + suspend fun getNextPage(page: Int): List { val response = executeQuery("""{User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}}""") val favourites = response?.data?.user?.favourites val apiMediaList = if (anime) favourites?.anime else favourites?.manga hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false return apiMediaList?.edges?.mapNotNull { - it.node?.let { i-> + it.node?.let { i -> Media(i).apply { isFav = true } } } ?: return listOf() } val responseArray = arrayListOf() - while(hasNextPage){ + while (hasNextPage) { page++ responseArray.addAll(getNextPage(page)) } @@ -361,7 +372,11 @@ class AnilistQueries { return default } - suspend fun getMediaLists(anime: Boolean, userId: Int, sortOrder: String? = null): MutableMap> { + suspend fun getMediaLists( + anime: Boolean, + userId: Int, + sortOrder: String? = null + ): MutableMap> { val response = executeQuery("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""") val sorted = mutableMapOf>() @@ -388,7 +403,7 @@ class AnilistQueries { if (unsorted.containsKey(it)) sorted[it] = unsorted[it]!! } unsorted.forEach { - if(!sorted.containsKey(it.key)) sorted[it.key] = it.value + if (!sorted.containsKey(it.key)) sorted[it.key] = it.value } sorted["Favourites"] = favMedia(anime) @@ -399,11 +414,18 @@ class AnilistQueries { val sort = sortOrder ?: options?.rowOrder for (i in sorted.keys) { when (sort) { - "score" -> sorted[i]?.sortWith { b, a -> compareValuesBy(a, b, { it.userScore }, { it.meanScore }) } - "title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName }) + "score" -> sorted[i]?.sortWith { b, a -> + compareValuesBy( + a, + b, + { it.userScore }, + { it.meanScore }) + } + + "title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName }) "updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt }) - "release" -> sorted[i]?.sortWith(compareByDescending { it.startDate }) - "id" -> sorted[i]?.sortWith(compareBy { it.id }) + "release" -> sorted[i]?.sortWith(compareByDescending { it.startDate }) + "id" -> sorted[i]?.sortWith(compareBy { it.id }) } } return sorted @@ -559,18 +581,36 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: ${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""} ${if (season != null) ""","season":"$season"""" else ""} ${if (search != null) ""","search":"$search"""" else ""} - ${if (sort!=null) ""","sort":"$sort"""" else ""} + ${if (sort != null) ""","sort":"$sort"""" else ""} ${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""} ${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""} ${ if (excludedGenres?.isNotEmpty() == true) - ""","excludedGenres":[${excludedGenres.joinToString { "\"${it.replace("Not ", "")}\"" }}]""" + ""","excludedGenres":[${ + excludedGenres.joinToString { + "\"${ + it.replace( + "Not ", + "" + ) + }\"" + } + }]""" else "" } ${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""} ${ if (excludedTags?.isNotEmpty() == true) - ""","excludedTags":[${excludedTags.joinToString { "\"${it.replace("Not ", "")}\"" }}]""" + ""","excludedTags":[${ + excludedTags.joinToString { + "\"${ + it.replace( + "Not ", + "" + ) + }\"" + } + }]""" else "" } }""".replace("\n", " ").replace(""" """, "") @@ -622,7 +662,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: greater: Long = 0, lesser: Long = System.currentTimeMillis() / 1000 - 10000 ): MutableList? { - suspend fun execute(page:Int = 1):Page?{ + suspend fun execute(page: Int = 1): Page? { val query = """{ Page(page:$page,perPage:50) { pageInfo { @@ -668,7 +708,7 @@ Page(page:$page,perPage:50) { }""".replace("\n", " ").replace(""" """, "") return executeQuery(query, force = true)?.data?.page } - if(smaller) { + if (smaller) { val response = execute()?.airingSchedules ?: return null val idArr = mutableListOf() val listOnly = loadData("recently_list_only") ?: false @@ -682,11 +722,11 @@ Page(page:$page,perPage:50) { else null } }.toMutableList() - }else{ + } else { var i = 1 val list = mutableListOf() - var res : Page? = null - suspend fun next(){ + var res: Page? = null + suspend fun next() { res = execute(i) list.addAll(res?.airingSchedules?.mapNotNull { j -> j.media?.let { @@ -694,10 +734,10 @@ Page(page:$page,perPage:50) { Media(it).apply { relation = "${j.episode},${j.airingAt}" } } else null } - }?: listOf()) + } ?: listOf()) } next() - while (res?.pageInfo?.hasNextPage == true){ + while (res?.pageInfo?.hasNextPage == true) { next() i++ } @@ -822,19 +862,20 @@ Page(page:$page,perPage:50) { var page = 0 while (hasNextPage) { page++ - hasNextPage = executeQuery(query(page), force = true)?.data?.studio?.media?.let { - it.edges?.forEach { i -> - i.node?.apply { - val status = status.toString() - val year = startDate?.year?.toString() ?: "TBA" - val title = if (status != "CANCELLED") year else status - if (!yearMedia.containsKey(title)) - yearMedia[title] = arrayListOf() - yearMedia[title]?.add(Media(this)) + hasNextPage = + executeQuery(query(page), force = true)?.data?.studio?.media?.let { + it.edges?.forEach { i -> + i.node?.apply { + val status = status.toString() + val year = startDate?.year?.toString() ?: "TBA" + val title = if (status != "CANCELLED") year else status + if (!yearMedia.containsKey(title)) + yearMedia[title] = arrayListOf() + yearMedia[title]?.add(Media(this)) + } } - } - it.pageInfo?.hasNextPage == true - } ?: false + it.pageInfo?.hasNextPage == true + } ?: false } if (yearMedia.contains("CANCELLED")) { val a = yearMedia["CANCELLED"]!! @@ -896,7 +937,10 @@ Page(page:$page,perPage:50) { while (hasNextPage) { page++ - hasNextPage = executeQuery(query(page), force = true)?.data?.author?.staffMedia?.let { + hasNextPage = executeQuery( + query(page), + force = true + )?.data?.author?.staffMedia?.let { it.edges?.forEach { i -> i.node?.apply { val status = status.toString() 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 2cd2e0ce..4cc8451f 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt @@ -7,8 +7,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import ani.dantotsu.R import ani.dantotsu.connections.discord.Discord -import ani.dantotsu.loadData import ani.dantotsu.connections.mal.MAL +import ani.dantotsu.loadData import ani.dantotsu.media.Media import ani.dantotsu.others.AppUpdater import ani.dantotsu.snackString @@ -19,9 +19,16 @@ import kotlinx.coroutines.launch suspend fun getUserId(context: Context, block: () -> Unit) { CoroutineScope(Dispatchers.IO).launch { - if (Discord.userid == null && Discord.token != null) { - if (!Discord.getUserData()) - snackString(context.getString(R.string.error_loading_discord_user_data)) + val sharedPref = context.getSharedPreferences( + context.getString(R.string.preference_file_key), + Context.MODE_PRIVATE + ) + val token = sharedPref.getString("discord_token", null) + val userid = sharedPref.getString("discord_id", null) + if (userid == null && token != null) { + /*if (!Discord.getUserData()) + snackString(context.getString(R.string.error_loading_discord_user_data))*/ + //TODO: Discord.getUserData() } } @@ -38,39 +45,57 @@ suspend fun getUserId(context: Context, block: () -> Unit) { } } else true - if(anilist) block.invoke() + if (anilist) block.invoke() } class AnilistHomeViewModel : ViewModel() { - private val listImages: MutableLiveData> = MutableLiveData>(arrayListOf()) + private val listImages: MutableLiveData> = + MutableLiveData>(arrayListOf()) + fun getListImages(): LiveData> = listImages suspend fun setListImages() = listImages.postValue(Anilist.query.getBannerImages()) - private val animeContinue: MutableLiveData> = MutableLiveData>(null) + private val animeContinue: MutableLiveData> = + MutableLiveData>(null) + fun getAnimeContinue(): LiveData> = animeContinue suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME")) - private val animeFav: MutableLiveData> = MutableLiveData>(null) + private val animeFav: MutableLiveData> = + MutableLiveData>(null) + fun getAnimeFav(): LiveData> = animeFav suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true)) - private val animePlanned: MutableLiveData> = MutableLiveData>(null) - fun getAnimePlanned(): LiveData> = animePlanned - suspend fun setAnimePlanned() = animePlanned.postValue(Anilist.query.continueMedia("ANIME", true)) + private val animePlanned: MutableLiveData> = + MutableLiveData>(null) + + fun getAnimePlanned(): LiveData> = animePlanned + suspend fun setAnimePlanned() = + animePlanned.postValue(Anilist.query.continueMedia("ANIME", true)) + + private val mangaContinue: MutableLiveData> = + MutableLiveData>(null) - private val mangaContinue: MutableLiveData> = MutableLiveData>(null) fun getMangaContinue(): LiveData> = mangaContinue suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA")) - private val mangaFav: MutableLiveData> = MutableLiveData>(null) + private val mangaFav: MutableLiveData> = + MutableLiveData>(null) + fun getMangaFav(): LiveData> = mangaFav suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false)) - private val mangaPlanned: MutableLiveData> = MutableLiveData>(null) - fun getMangaPlanned(): LiveData> = mangaPlanned - suspend fun setMangaPlanned() = mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true)) + private val mangaPlanned: MutableLiveData> = + MutableLiveData>(null) + + fun getMangaPlanned(): LiveData> = mangaPlanned + suspend fun setMangaPlanned() = + mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true)) + + private val recommendation: MutableLiveData> = + MutableLiveData>(null) - private val recommendation: MutableLiveData> = MutableLiveData>(null) fun getRecommendation(): LiveData> = recommendation suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations()) @@ -93,7 +118,9 @@ class AnilistAnimeViewModel : ViewModel() { var notSet = true lateinit var searchResults: SearchResults private val type = "ANIME" - private val trending: MutableLiveData> = MutableLiveData>(null) + private val trending: MutableLiveData> = + MutableLiveData>(null) + fun getTrending(): LiveData> = trending suspend fun loadTrending(i: Int) { val (season, year) = Anilist.currentSeasons[i] @@ -109,7 +136,9 @@ class AnilistAnimeViewModel : ViewModel() { ) } - private val updated: MutableLiveData> = MutableLiveData>(null) + private val updated: MutableLiveData> = + MutableLiveData>(null) + fun getUpdated(): LiveData> = updated suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated()) @@ -157,15 +186,33 @@ class AnilistMangaViewModel : ViewModel() { var notSet = true lateinit var searchResults: SearchResults private val type = "MANGA" - private val trending: MutableLiveData> = MutableLiveData>(null) + private val trending: MutableLiveData> = + MutableLiveData>(null) + fun getTrending(): LiveData> = trending suspend fun loadTrending() = - trending.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], hd = true)?.results) + trending.postValue( + Anilist.query.search( + type, + perPage = 10, + sort = Anilist.sortBy[2], + hd = true + )?.results + ) + + private val updated: MutableLiveData> = + MutableLiveData>(null) - private val updated: MutableLiveData> = MutableLiveData>(null) fun getTrendingNovel(): LiveData> = updated suspend fun loadTrendingNovel() = - updated.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], format = "NOVEL")?.results) + updated.postValue( + Anilist.query.search( + type, + perPage = 10, + sort = Anilist.sortBy[2], + format = "NOVEL" + )?.results + ) private val mangaPopular = MutableLiveData(null) fun getPopular(): LiveData = mangaPopular 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 abeafbe6..fdba6d3a 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/Login.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/Login.kt @@ -6,19 +6,20 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import ani.dantotsu.logError import ani.dantotsu.logger +import ani.dantotsu.others.LangSet import ani.dantotsu.startMainActivity import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet class Login : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() val data: Uri? = intent?.data logger(data.toString()) try { - Anilist.token = Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value + Anilist.token = + Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value val filename = "anilistToken" this.openFileOutput(filename, Context.MODE_PRIVATE).use { it.write(Anilist.token!!.toByteArray()) 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 86b146ca..32bc1757 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt @@ -27,7 +27,15 @@ data class SearchResults( val list = mutableListOf() sort?.let { val c = currContext()!! - list.add(SearchChip("SORT", c.getString(R.string.filter_sort, c.resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]))) + list.add( + SearchChip( + "SORT", + c.getString( + R.string.filter_sort, + c.resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)] + ) + ) + ) } format?.let { list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it))) @@ -42,27 +50,37 @@ data class SearchResults( list.add(SearchChip("GENRE", it)) } excludedGenres?.forEach { - list.add(SearchChip("EXCLUDED_GENRE", currContext()!!.getString(R.string.filter_exclude, it))) + list.add( + SearchChip( + "EXCLUDED_GENRE", + currContext()!!.getString(R.string.filter_exclude, it) + ) + ) } tags?.forEach { list.add(SearchChip("TAG", it)) } excludedTags?.forEach { - list.add(SearchChip("EXCLUDED_TAG", currContext()!!.getString(R.string.filter_exclude, it))) + list.add( + SearchChip( + "EXCLUDED_TAG", + currContext()!!.getString(R.string.filter_exclude, it) + ) + ) } return list } fun removeChip(chip: SearchChip) { when (chip.type) { - "SORT" -> sort = null - "FORMAT" -> format = null - "SEASON" -> season = null - "SEASON_YEAR" -> seasonYear = null - "GENRE" -> genres?.remove(chip.text) + "SORT" -> sort = null + "FORMAT" -> format = null + "SEASON" -> season = null + "SEASON_YEAR" -> seasonYear = null + "GENRE" -> genres?.remove(chip.text) "EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text) - "TAG" -> tags?.remove(chip.text) - "EXCLUDED_TAG" -> excludedTags?.remove(chip.text) + "TAG" -> tags?.remove(chip.text) + "EXCLUDED_TAG" -> excludedTags?.remove(chip.text) } } diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/UrlMedia.kt b/app/src/main/java/ani/dantotsu/connections/anilist/UrlMedia.kt index addaebed..30be1706 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/UrlMedia.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/UrlMedia.kt @@ -5,15 +5,15 @@ import android.net.Uri import android.os.Bundle import androidx.core.os.bundleOf import ani.dantotsu.loadMedia +import ani.dantotsu.others.LangSet import ani.dantotsu.startMainActivity import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet class UrlMedia : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() var id: Int? = intent?.extras?.getInt("media", 0) ?: 0 var isMAL = false var continueMedia = true @@ -23,6 +23,9 @@ ThemeManager(this).applyTheme() isMAL = data?.host != "anilist.co" id = data?.pathSegments?.getOrNull(1)?.toIntOrNull() } else loadMedia = id - startMainActivity(this, bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)) + startMainActivity( + this, + bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia) + ) } } \ No newline at end of file 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 0d0b10e2..5b3a16ea 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 @@ -3,23 +3,24 @@ package ani.dantotsu.connections.anilist.api import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -class Query{ +class Query { @Serializable data class Viewer( @SerialName("data") - val data : Data? - ){ + val data: Data? + ) { @Serializable data class Data( @SerialName("Viewer") val user: ani.dantotsu.connections.anilist.api.User? ) } + @Serializable data class Media( @SerialName("data") - val data : Data? - ){ + val data: Data? + ) { @Serializable data class Data( @SerialName("Media") @@ -30,12 +31,12 @@ class Query{ @Serializable data class Page( @SerialName("data") - val data : Data? - ){ + val data: Data? + ) { @Serializable data class Data( @SerialName("Page") - val page : ani.dantotsu.connections.anilist.api.Page? + val page: ani.dantotsu.connections.anilist.api.Page? ) } // data class AiringSchedule( @@ -49,8 +50,8 @@ class Query{ @Serializable data class Character( @SerialName("data") - val data : Data? - ){ + val data: Data? + ) { @Serializable data class Data( @@ -63,7 +64,7 @@ class Query{ data class Studio( @SerialName("data") val data: Data? - ){ + ) { @Serializable data class Data( @SerialName("Studio") @@ -76,7 +77,7 @@ class Query{ data class Author( @SerialName("data") val data: Data? - ){ + ) { @Serializable data class Data( @SerialName("Staff") @@ -95,8 +96,8 @@ class Query{ @Serializable data class MediaListCollection( @SerialName("data") - val data : Data? - ){ + val data: Data? + ) { @Serializable data class Data( @SerialName("MediaListCollection") @@ -108,7 +109,7 @@ class Query{ data class GenreCollection( @SerialName("data") val data: Data - ){ + ) { @Serializable data class Data( @SerialName("GenreCollection") @@ -120,7 +121,7 @@ class Query{ data class MediaTagCollection( @SerialName("data") val data: Data - ){ + ) { @Serializable data class Data( @SerialName("MediaTagCollection") @@ -132,7 +133,7 @@ class Query{ data class User( @SerialName("data") val data: Data - ){ + ) { @Serializable data class Data( @SerialName("User") diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/FuzzyDate.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/FuzzyDate.kt index 0be30778..9de2c0c4 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/FuzzyDate.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/FuzzyDate.kt @@ -3,7 +3,7 @@ package ani.dantotsu.connections.anilist.api import kotlinx.serialization.SerialName import java.io.Serializable import java.text.DateFormatSymbols -import java.util.* +import java.util.Calendar @kotlinx.serialization.Serializable data class FuzzyDate( @@ -16,9 +16,11 @@ data class FuzzyDate( fun isEmpty(): Boolean { return year == null && month == null && day == null } + override fun toString(): String { - return if ( isEmpty() ) "??" else toStringOrEmpty() + return if (isEmpty()) "??" else toStringOrEmpty() } + fun toStringOrEmpty(): String { return listOfNotNull( day?.toString(), @@ -29,16 +31,21 @@ data class FuzzyDate( fun getToday(): FuzzyDate { val cal = Calendar.getInstance() - return FuzzyDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)) + return FuzzyDate( + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH) + 1, + cal.get(Calendar.DAY_OF_MONTH) + ) } fun toVariableString(): String { return listOfNotNull( - year?.let {"year:$it"}, - month?.let {"month:$it"}, - day?.let {"day:$it"} + year?.let { "year:$it" }, + month?.let { "month:$it" }, + day?.let { "day:$it" } ).joinToString(",", "{", "}") } + fun toMALString(): String { val padding = '0' val values = listOf( @@ -46,7 +53,7 @@ data class FuzzyDate( month?.toString()?.padStart(2, padding), day?.toString()?.padStart(2, padding) ) - return values.takeWhile {it is String}.joinToString("-") + return values.takeWhile { it is String }.joinToString("-") } // fun toInt(): Int { @@ -54,8 +61,8 @@ data class FuzzyDate( // } override fun compareTo(other: FuzzyDate): Int = when { - year != other.year -> (year ?: 0) - (other.year ?: 0) + year != other.year -> (year ?: 0) - (other.year ?: 0) month != other.month -> (month ?: 0) - (other.month ?: 0) - else -> (day ?: 0) - (other.day ?: 0) + else -> (day ?: 0) - (other.day ?: 0) } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt index 311ef02c..491f4df4 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt @@ -116,7 +116,7 @@ data class Media( @SerialName("characters") var characters: CharacterConnection?, // The staff who produced the media - @SerialName("staffPreview") var staff: StaffConnection?, + @SerialName("staffPreview") var staff: StaffConnection?, // The companies who produced the media @SerialName("studios") var studios: StudioConnection?, @@ -292,7 +292,7 @@ data class MediaList( @SerialName("hiddenFromStatusLists") var hiddenFromStatusLists: Boolean?, // Map of booleans for which custom lists the entry are in - @SerialName("customLists") var customLists: Map?, + @SerialName("customLists") var customLists: Map?, // Map of advanced scores with name keys // @SerialName("advancedScores") var advancedScores: Json?, @@ -355,7 +355,7 @@ data class MediaTrailer( @Serializable data class MediaTagCollection( - @SerialName("tags") var tags : List? + @SerialName("tags") var tags: List? ) @Serializable diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Recommendations.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Recommendations.kt index c9e9f321..413a6095 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Recommendations.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Recommendations.kt @@ -2,6 +2,7 @@ package ani.dantotsu.connections.anilist.api import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable + @Serializable data class Recommendation( // The id of the recommendation @@ -22,6 +23,7 @@ data class Recommendation( // The user that first created the recommendation @SerialName("user") var user: User?, ) + @Serializable data class RecommendationConnection( //@SerialName("edges") var edges: List?, 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 b9c6bc80..b4742e5b 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 @@ -9,7 +9,7 @@ data class Staff( @SerialName("id") var id: Int, // The names of the staff member - @SerialName("name") var name: StaffName?, + @SerialName("name") var name: StaffName?, // The primary language of the staff member. Current values: Japanese, English, Korean, Italian, Spanish, Portuguese, French, German, Hebrew, Hungarian, Chinese, Arabic, Filipino, Catalan, Finnish, Turkish, Dutch, Swedish, Thai, Tagalog, Malaysian, Indonesian, Vietnamese, Nepali, Hindi, Urdu @SerialName("languageV2") var languageV2: String?, @@ -80,8 +80,8 @@ data class Staff( ) @Serializable -data class StaffName ( - var userPreferred:String? +data class StaffName( + var userPreferred: String? ) @Serializable @@ -96,6 +96,6 @@ data class StaffConnection( @Serializable data class StaffEdge( - var role:String?, + var role: String?, var node: Staff? ) \ No newline at end of file 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 99a66eb8..dddef0d5 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 @@ -80,10 +80,10 @@ data class UserOptions( @SerialName("displayAdultContent") var displayAdultContent: Boolean?, // Whether the user receives notifications when a show they are watching aires - @SerialName("airingNotifications") var airingNotifications: Boolean?, + @SerialName("airingNotifications") var airingNotifications: Boolean?, // - // Profile highlight color (blue, purple, pink, orange, red, green, gray) - @SerialName("profileColor") var profileColor: String?, + // Profile highlight color (blue, purple, pink, orange, red, green, gray) + @SerialName("profileColor") var profileColor: String?, // // // Notification options // // @SerialName("notificationOptions") var notificationOptions: List?, diff --git a/app/src/main/java/ani/dantotsu/connections/discord/Discord.kt b/app/src/main/java/ani/dantotsu/connections/discord/Discord.kt index f718677f..95450305 100644 --- a/app/src/main/java/ani/dantotsu/connections/discord/Discord.kt +++ b/app/src/main/java/ani/dantotsu/connections/discord/Discord.kt @@ -5,14 +5,11 @@ import android.content.Intent import android.widget.TextView import androidx.core.content.edit import ani.dantotsu.R -import ani.dantotsu.connections.discord.serializers.User import ani.dantotsu.others.CustomBottomDialog import ani.dantotsu.toast import ani.dantotsu.tryWith -import ani.dantotsu.tryWithSuspend import io.noties.markwon.Markwon import io.noties.markwon.SoftBreakAddsNewLinePlugin -import kotlinx.coroutines.Dispatchers import java.io.File object Discord { @@ -21,7 +18,7 @@ object Discord { var userid: String? = null var avatar: String? = null - private const val TOKEN = "discord_token" + const val TOKEN = "discord_token" fun getSavedToken(context: Context): Boolean { val sharedPref = context.getSharedPreferences( @@ -60,17 +57,7 @@ object Discord { } } - private var rpc : RPC? = null - suspend fun getUserData() = tryWithSuspend(true) { - if(rpc==null) { - val rpc = RPC(token!!, Dispatchers.IO).also { rpc = it } - val user: User = rpc.getUserData() - userid = user.username - avatar = user.userAvatar() - rpc.close() - true - } else true - } ?: false + private var rpc: RPC? = null fun warning(context: Context) = CustomBottomDialog().apply { @@ -97,16 +84,21 @@ object Discord { context.startActivity(intent) } - fun defaultRPC(): RPC? { + const val application_Id = "1163925779692912771" + const val small_Image: String = + "mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png" + /*fun defaultRPC(): RPC? { return token?.let { RPC(it, Dispatchers.IO).apply { - applicationId = "1163925779692912771" + applicationId = application_Id smallImage = RPC.Link( "Dantotsu", - "mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png" + small_Image ) buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/")) } } - } + }*/ + + } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/connections/discord/DiscordService.kt b/app/src/main/java/ani/dantotsu/connections/discord/DiscordService.kt new file mode 100644 index 00000000..3506ea25 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/connections/discord/DiscordService.kt @@ -0,0 +1,475 @@ +package ani.dantotsu.connections.discord + +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.os.IBinder +import android.os.PowerManager +import android.provider.MediaStore +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import ani.dantotsu.MainActivity +import ani.dantotsu.R +import ani.dantotsu.connections.discord.serializers.Presence +import ani.dantotsu.connections.discord.serializers.User +import ani.dantotsu.isOnline +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import java.io.File +import java.io.OutputStreamWriter + +class DiscordService : Service() { + private var heartbeat: Int = 0 + private var sequence: Int? = null + private var sessionId: String = "" + private var resume = false + private lateinit var logFile: File + private lateinit var webSocket: WebSocket + private lateinit var heartbeatThread: Thread + private lateinit var client: OkHttpClient + private lateinit var wakeLock: PowerManager.WakeLock + var presenceStore = "" + val json = Json { + encodeDefaults = true + allowStructuredMapKeys = true + ignoreUnknownKeys = true + coerceInputValues = true + } + var log = "" + + override fun onCreate() { + super.onCreate() + + log("Service onCreate()") + val powerManager = baseContext.getSystemService(Context.POWER_SERVICE) as PowerManager + wakeLock = powerManager.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "discordRPC:backgroundPresence" + ) + wakeLock.acquire() + log("WakeLock Acquired") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val serviceChannel = NotificationChannel( + "discordPresence", + "Discord Presence Service Channel", + NotificationManager.IMPORTANCE_LOW + ) + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(serviceChannel) + } + val intent = Intent(this, MainActivity::class.java).apply { + action = Intent.ACTION_MAIN + addCategory(Intent.CATEGORY_LAUNCHER) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + val pendingIntent = + PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val builder = NotificationCompat.Builder(this, "discordPresence") + .setSmallIcon(R.mipmap.ic_launcher_round) + .setContentTitle("Discord Presence") + .setContentText("Running in the background") + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_LOW) + startForeground(1, builder.build()) + log("Foreground service started, notification shown") + client = OkHttpClient() + client.newWebSocket( + Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(), + DiscordWebSocketListener() + ) + client.dispatcher.executorService.shutdown() + SERVICE_RUNNING = true + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + log("Service onStartCommand()") + if (intent != null) { + if (intent.hasExtra("presence")) { + log("Service onStartCommand() setPresence") + val lPresence = intent.getStringExtra("presence") + if (this::webSocket.isInitialized) webSocket.send(lPresence!!) + presenceStore = lPresence!! + } else { + log("Service onStartCommand() no presence") + DiscordServiceRunningSingleton.running = false + //kill the client + client = OkHttpClient() + stopSelf() + } + } + return START_REDELIVER_INTENT + } + + override fun onDestroy() { + log("Service Destroyed") + if (DiscordServiceRunningSingleton.running) { + log("Accidental Service Destruction, restarting service") + val intent = Intent(baseContext, DiscordService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + baseContext.startForegroundService(intent) + } else { + baseContext.startService(intent) + } + } else { + if (this::webSocket.isInitialized) + setPresence( + json.encodeToString( + Presence.Response( + 3, + Presence(status = "offline") + ) + ) + ) + wakeLock.release() + } + SERVICE_RUNNING = false + client = OkHttpClient() + if (this::webSocket.isInitialized) webSocket.close(1000, "Closed by user") + super.onDestroy() + //saveLogToFile() + } + + fun saveProfile(response: String) { + val sharedPref = baseContext.getSharedPreferences( + baseContext.getString(R.string.preference_file_key), + Context.MODE_PRIVATE + ) + val user = json.decodeFromString(response).d.user + log("User data: $user") + with(sharedPref.edit()) { + putString("discord_username", user.username) + putString("discord_id", user.id) + putString("discord_avatar", user.avatar) + apply() + } + + } + + override fun onBind(p0: Intent?): IBinder? = null + + inner class DiscordWebSocketListener : WebSocketListener() { + + var retryAttempts = 0 + val maxRetryAttempts = 10 + override fun onOpen(webSocket: WebSocket, response: Response) { + super.onOpen(webSocket, response) + this@DiscordService.webSocket = webSocket + log("WebSocket: Opened") + } + + override fun onMessage(webSocket: WebSocket, text: String) { + super.onMessage(webSocket, text) + val json = JsonParser.parseString(text).asJsonObject + log("WebSocket: Received op code ${json.get("op")}") + when (json.get("op").asInt) { + 0 -> { + if (json.has("s")) { + log("WebSocket: Sequence ${json.get("s")} Received") + sequence = json.get("s").asInt + } + if (json.get("t").asString != "READY") return + saveProfile(text) + log(text) + sessionId = json.get("d").asJsonObject.get("session_id").asString + log("WebSocket: SessionID ${json.get("d").asJsonObject.get("session_id")} Received") + if (presenceStore.isNotEmpty()) setPresence(presenceStore) + sendBroadcast(Intent("ServiceToConnectButton")) + } + + 1 -> { + log("WebSocket: Received Heartbeat request, sending heartbeat") + heartbeatThread.interrupt() + heartbeatSend(webSocket, sequence) + heartbeatThread = Thread(HeartbeatRunnable()) + heartbeatThread.start() + } + + 7 -> { + resume = true + log("WebSocket: Requested to Restart, restarting") + webSocket.close(1000, "Requested to Restart by the server") + client = OkHttpClient() + client.newWebSocket( + Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json") + .build(), + DiscordWebSocketListener() + ) + client.dispatcher.executorService.shutdown() + } + + 9 -> { + log("WebSocket: Invalid Session, restarting") + webSocket.close(1000, "Invalid Session") + Thread.sleep(5000) + client = OkHttpClient() + client.newWebSocket( + Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json") + .build(), + DiscordWebSocketListener() + ) + client.dispatcher.executorService.shutdown() + } + + 10 -> { + heartbeat = json.get("d").asJsonObject.get("heartbeat_interval").asInt + heartbeatThread = Thread(HeartbeatRunnable()) + heartbeatThread.start() + if (resume) { + log("WebSocket: Resuming because server requested") + resume() + resume = false + } else { + identify(webSocket, baseContext) + log("WebSocket: Identified") + } + } + + 11 -> { + log("WebSocket: Heartbeat ACKed") + heartbeatThread = Thread(HeartbeatRunnable()) + heartbeatThread.start() + } + } + } + + fun identify(webSocket: WebSocket, context: Context) { + val properties = JsonObject() + properties.addProperty("os", "linux") + properties.addProperty("browser", "unknown") + properties.addProperty("device", "unknown") + val d = JsonObject() + d.addProperty("token", getToken(context)) + d.addProperty("intents", 0) + d.add("properties", properties) + val payload = JsonObject() + payload.addProperty("op", 2) + payload.add("d", d) + webSocket.send(payload.toString()) + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + super.onFailure(webSocket, t, response) + if (!isOnline(baseContext)) { + log("WebSocket: Error, onFailure() reason: No Internet") + errorNotification("Could not set the presence", "No Internet") + return + } else { + retryAttempts++ + if (retryAttempts >= maxRetryAttempts) { + log("WebSocket: Error, onFailure() reason: Max Retry Attempts") + errorNotification("Could not set the presence", "Max Retry Attempts") + return + } + } + t.message?.let { Log.d("WebSocket", "onFailure() $it") } + log("WebSocket: Error, onFailure() reason: ${t.message}") + client = OkHttpClient() + client.newWebSocket( + Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(), + DiscordWebSocketListener() + ) + client.dispatcher.executorService.shutdown() + if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) { + heartbeatThread.interrupt() + } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + super.onClosing(webSocket, code, reason) + Log.d("WebSocket", "onClosing() $code $reason") + if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) { + heartbeatThread.interrupt() + } + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + super.onClosed(webSocket, code, reason) + Log.d("WebSocket", "onClosed() $code $reason") + if (code >= 4000) { + log("WebSocket: Error, code: $code reason: $reason") + client = OkHttpClient() + client.newWebSocket( + Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(), + DiscordWebSocketListener() + ) + client.dispatcher.executorService.shutdown() + return + } + } + } + + fun getToken(context: Context): String { + val sharedPref = context.getSharedPreferences( + context.getString(R.string.preference_file_key), + Context.MODE_PRIVATE + ) + val token = sharedPref.getString(Discord.TOKEN, null) + if (token == null) { + log("WebSocket: Token not found") + errorNotification("Could not set the presence", "token not found") + return "" + } else { + return token + } + } + + fun heartbeatSend(webSocket: WebSocket, seq: Int?) { + val json = JsonObject() + json.addProperty("op", 1) + json.addProperty("d", seq) + webSocket.send(json.toString()) + } + + private fun errorNotification(title: String, text: String) { + val intent = Intent(this@DiscordService, MainActivity::class.java).apply { + action = Intent.ACTION_MAIN + addCategory(Intent.CATEGORY_LAUNCHER) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + val pendingIntent = + PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val builder = NotificationCompat.Builder(this@DiscordService, "discordPresence") + .setSmallIcon(R.mipmap.ic_launcher_round) + .setContentTitle(title) + .setContentText(text) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_HIGH) + val notificationManager = NotificationManagerCompat.from(applicationContext) + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + //TODO: Request permission + return + } + notificationManager.notify(2, builder.build()) + log("Error Notified") + } + + fun saveSimpleTestPresence() { + val file = File(baseContext.cacheDir, "payload") + //fill with test payload + val payload = JsonObject() + payload.addProperty("op", 3) + payload.add("d", JsonObject().apply { + addProperty("status", "online") + addProperty("afk", false) + add("activities", JsonArray().apply { + add(JsonObject().apply { + addProperty("name", "Test") + addProperty("type", 0) + }) + }) + }) + file.writeText(payload.toString()) + log("WebSocket: Simple Test Presence Saved") + } + + fun setPresence(String: String) { + log("WebSocket: Sending Presence payload") + log(String) + webSocket.send(String) + } + + fun log(string: String) { + Log.d("WebSocket_Discord", string) + //log += "${SimpleDateFormat("HH:mm:ss").format(Calendar.getInstance().time)} $string\n" + } + + fun saveLogToFile() { + val fileName = "log_${System.currentTimeMillis()}.txt" + + // ContentValues to store file metadata + val values = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + put(MediaStore.MediaColumns.MIME_TYPE, "text/plain") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/") + } + } + + // Inserting the file in the MediaStore + val resolver = baseContext.contentResolver + val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) + } else { + val directory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + val file = File(directory, fileName) + + // Make sure the Downloads directory exists + if (!directory.exists()) { + directory.mkdirs() + } + + // Use FileProvider to get the URI for the file + val authority = + "${baseContext.packageName}.provider" // Adjust with your app's package name + Uri.fromFile(file) + } + + // Writing to the file + uri?.let { + resolver.openOutputStream(it).use { outputStream -> + OutputStreamWriter(outputStream).use { writer -> + writer.write(log) + } + } + } ?: run { + log("Error saving log file") + } + } + + fun resume() { + log("Sending Resume payload") + val d = JsonObject() + d.addProperty("token", getToken(baseContext)) + d.addProperty("session_id", sessionId) + d.addProperty("seq", sequence) + val json = JsonObject() + json.addProperty("op", 6) + json.add("d", d) + log(json.toString()) + webSocket.send(json.toString()) + } + + inner class HeartbeatRunnable : Runnable { + override fun run() { + try { + Thread.sleep(heartbeat.toLong()) + heartbeatSend(webSocket, sequence) + log("WebSocket: Heartbeat Sent") + } catch (e: InterruptedException) { + } + } + } + + companion object { + var SERVICE_RUNNING = false + } +} + +object DiscordServiceRunningSingleton { + var running = false + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/connections/discord/Login.kt b/app/src/main/java/ani/dantotsu/connections/discord/Login.kt index 6554f20b..98048844 100644 --- a/app/src/main/java/ani/dantotsu/connections/discord/Login.kt +++ b/app/src/main/java/ani/dantotsu/connections/discord/Login.kt @@ -7,15 +7,13 @@ import android.os.Bundle import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import androidx.annotation.RequiresApi +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import ani.dantotsu.R import ani.dantotsu.connections.discord.Discord.saveToken -import ani.dantotsu.logger +import ani.dantotsu.others.LangSet import ani.dantotsu.startMainActivity import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet -import ani.dantotsu.snackString class Login : AppCompatActivity() { @@ -39,17 +37,22 @@ class Login : AppCompatActivity() { } WebView.setWebContentsDebuggingEnabled(true) webView.webViewClient = object : WebViewClient() { - override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { // Check if the URL is the one expected after a successful login if (request?.url.toString() != "https://discord.com/login") { // Delay the script execution to ensure the page is fully loaded view?.postDelayed({ - view.evaluateJavascript(""" + view.evaluateJavascript( + """ (function() { const wreq = (webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken(); return wreq; })() - """.trimIndent()) { result -> + """.trimIndent() + ) { result -> login(result.trim('"')) } }, 2000) @@ -66,12 +69,12 @@ class Login : AppCompatActivity() { } private fun login(token: String) { - if (token.isEmpty() || token == "null"){ - snackString("Failed to retrieve token") + if (token.isEmpty() || token == "null") { + Toast.makeText(this, "Failed to retrieve token", Toast.LENGTH_SHORT).show() finish() return } - snackString("Logged in successfully") + Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show() finish() saveToken(this, token) startMainActivity(this@Login) 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 90468921..822218c5 100644 --- a/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt +++ b/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt @@ -1,26 +1,10 @@ package ani.dantotsu.connections.discord -import ani.dantotsu.connections.discord.serializers.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine +import ani.dantotsu.connections.discord.serializers.Activity +import ani.dantotsu.connections.discord.serializers.Presence import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import okhttp3.WebSocket -import okhttp3.WebSocketListener -import java.util.concurrent.TimeUnit.* import kotlin.coroutines.CoroutineContext import ani.dantotsu.client as app @@ -33,203 +17,73 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) { ignoreUnknownKeys = true } - private val client = OkHttpClient.Builder() - .connectTimeout(10, SECONDS) - .readTimeout(10, SECONDS) - .writeTimeout(10, SECONDS) - .build() - - private val request = Request.Builder() - .url("wss://gateway.discord.gg/?encoding=json&v=10") - .build() - - private var webSocket = client.newWebSocket(request, Listener()) - - var applicationId: String? = null - var type: Type? = null - var activityName: String? = null - var details: String? = null - var state: String? = null - var largeImage: Link? = null - var smallImage: Link? = null - var status: String? = null - var startTimestamp: Long? = null - var stopTimestamp: Long? = null - enum class Type { PLAYING, STREAMING, LISTENING, WATCHING, COMPETING } - var buttons = mutableListOf() - data class Link(val label: String, val url: String) - private suspend fun createPresence(): String { - return json.encodeToString(Presence.Response( - 3, - Presence( - activities = listOf( - Activity( - name = activityName, - state = state, - details = details, - type = type?.ordinal, - timestamps = if (startTimestamp != null) - Activity.Timestamps(startTimestamp, stopTimestamp) - else null, - assets = Activity.Assets( - largeImage = largeImage?.url?.discordUrl(), - largeText = largeImage?.label, - smallImage = smallImage?.url?.discordUrl(), - smallText = smallImage?.label - ), - buttons = buttons.map { it.label }, - metadata = Activity.Metadata( - buttonUrls = buttons.map { it.url } - ), - applicationId = applicationId, - ) - ), - afk = true, - since = startTimestamp, - status = status - ) - )) - } - - @Serializable - data class KizzyApi(val id: String) - val api = "https://kizzy-api.vercel.app/image?url=" - private suspend fun String.discordUrl(): String? { - if (startsWith("mp:")) return this - val json = app.get("$api$this").parsedSafe() - return json?.id - } - - private fun sendIdentify() { - val response = Identity.Response( - op = 2, - d = Identity( - token = token, - properties = Identity.Properties( - os = "windows", - browser = "Chrome", - device = "disco" - ), - compress = false, - intents = 0 - ) + companion object { + data class RPCData( + val applicationId: String? = null, + val type: Type? = null, + val activityName: String? = null, + val details: String? = null, + val state: String? = null, + val largeImage: Link? = null, + val smallImage: Link? = null, + val status: String? = null, + val startTimestamp: Long? = null, + val stopTimestamp: Long? = null, + val buttons: MutableList = mutableListOf() ) - webSocket.send(json.encodeToString(response)) - } - fun send(block: RPC.() -> Unit) { - block.invoke(this) - send() - } + @Serializable + data class KizzyApi(val id: String) - var started = false - var whenStarted: ((User) -> Unit)? = null + val api = "https://kizzy-api.vercel.app/image?url=" + private suspend fun String.discordUrl(): String? { + if (startsWith("mp:")) return this + val json = app.get("$api$this").parsedSafe() + return json?.id + } - fun send() { - val send = { - CoroutineScope(coroutineContext).launch { - webSocket.send(createPresence()) + suspend fun createPresence(data: RPCData): String { + val json = Json { + encodeDefaults = true + allowStructuredMapKeys = true + ignoreUnknownKeys = true } - } - if (!started) whenStarted = { - send.invoke() - whenStarted = null - } - else send.invoke() - } - - fun close() { - webSocket.send( - json.encodeToString( - Presence.Response( - 3, - Presence(status = "offline") + return json.encodeToString(Presence.Response( + 3, + Presence( + activities = listOf( + Activity( + name = data.activityName, + state = data.state, + details = data.details, + type = data.type?.ordinal, + timestamps = if (data.startTimestamp != null) + Activity.Timestamps(data.startTimestamp, data.stopTimestamp) + else null, + assets = Activity.Assets( + largeImage = data.largeImage?.url?.discordUrl(), + largeText = data.largeImage?.label, + smallImage = data.smallImage?.url?.discordUrl(), + smallText = data.smallImage?.label + ), + buttons = data.buttons.map { it.label }, + metadata = Activity.Metadata( + buttonUrls = data.buttons.map { it.url } + ), + applicationId = data.applicationId, + ) + ), + afk = true, + since = data.startTimestamp, + status = data.status ) - ) - ) - webSocket.close(4000, "Interrupt") - } - - //I kinda hate this - suspend fun getUserData(): User = suspendCancellableCoroutine { continuation -> - whenStarted = { - continuation.resume(it, onCancellation = null) - whenStarted = null - } - continuation.invokeOnCancellation { - whenStarted = null - } - } - - var onReceiveUserData: ((User) -> Deferred)? = null - - inner class Listener : WebSocketListener() { - private var seq: Int? = null - private var heartbeatInterval: Long? = null - - var scope = CoroutineScope(coroutineContext) - - private fun sendHeartBeat() { - scope.cancel() - scope = CoroutineScope(coroutineContext) - scope.launch { - delay(heartbeatInterval!!) - webSocket.send("{\"op\":1, \"d\":$seq}") - } - } - - override fun onMessage(webSocket: WebSocket, text: String) { - println("Discord Message : $text") - - val map = json.decodeFromString(text) - seq = map.s - - when (map.op) { - 10 -> { - map.d as JsonObject - heartbeatInterval = map.d["heartbeat_interval"]!!.jsonPrimitive.long - sendHeartBeat() - sendIdentify() - } - - 0 -> if (map.t == "READY") { - val user = json.decodeFromString(text).d.user - started = true - whenStarted?.invoke(user) - } - - 1 -> { - if (scope.isActive) scope.cancel() - webSocket.send("{\"op\":1, \"d\":$seq}") - } - - 11 -> sendHeartBeat() - 7 -> webSocket.close(400, "Reconnect") - 9 -> { - sendHeartBeat() - sendIdentify() - } - } - } - - override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { - println("Server Closed : $code $reason") - if (code == 4000) { - scope.cancel() - } - } - - override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { - println("Failure : ${t.message}") - if (t.message != "Interrupt") { - this@RPC.webSocket = client.newWebSocket(request, Listener()) - } + )) } } diff --git a/app/src/main/java/ani/dantotsu/connections/discord/serializers/Activity.kt b/app/src/main/java/ani/dantotsu/connections/discord/serializers/Activity.kt index 2debdfd7..2e6366ac 100644 --- a/app/src/main/java/ani/dantotsu/connections/discord/serializers/Activity.kt +++ b/app/src/main/java/ani/dantotsu/connections/discord/serializers/Activity.kt @@ -2,8 +2,9 @@ package ani.dantotsu.connections.discord.serializers import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable + @Serializable -data class Activity ( +data class Activity( @SerialName("application_id") val applicationId: String? = null, val name: String? = null, diff --git a/app/src/main/java/ani/dantotsu/connections/discord/serializers/Identity.kt b/app/src/main/java/ani/dantotsu/connections/discord/serializers/Identity.kt index 838ba214..1e31a6d8 100644 --- a/app/src/main/java/ani/dantotsu/connections/discord/serializers/Identity.kt +++ b/app/src/main/java/ani/dantotsu/connections/discord/serializers/Identity.kt @@ -12,13 +12,13 @@ data class Identity( ) { @Serializable - data class Response ( + data class Response( val op: Long, val d: Identity ) @Serializable - data class Properties ( + data class Properties( @SerialName("\$os") val os: String, diff --git a/app/src/main/java/ani/dantotsu/connections/discord/serializers/Presence.kt b/app/src/main/java/ani/dantotsu/connections/discord/serializers/Presence.kt index fa78a948..1d06a192 100644 --- a/app/src/main/java/ani/dantotsu/connections/discord/serializers/Presence.kt +++ b/app/src/main/java/ani/dantotsu/connections/discord/serializers/Presence.kt @@ -3,14 +3,14 @@ package ani.dantotsu.connections.discord.serializers import kotlinx.serialization.Serializable @Serializable -data class Presence ( +data class Presence( val activities: List = listOf(), val afk: Boolean = true, val since: Long? = null, val status: String? = null -){ +) { @Serializable - data class Response ( + data class Response( val op: Long, val d: Presence ) diff --git a/app/src/main/java/ani/dantotsu/connections/discord/serializers/User.kt b/app/src/main/java/ani/dantotsu/connections/discord/serializers/User.kt index 225ad73d..6ce3a9ef 100644 --- a/app/src/main/java/ani/dantotsu/connections/discord/serializers/User.kt +++ b/app/src/main/java/ani/dantotsu/connections/discord/serializers/User.kt @@ -1,60 +1,60 @@ package ani.dantotsu.connections.discord.serializers -import kotlinx.serialization.* -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* -import kotlinx.serialization.json.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement + @Serializable -data class User ( - val verified: Boolean, +data class User( + val verified: Boolean? = null, val username: String, @SerialName("purchased_flags") - val purchasedFlags: Long, + val purchasedFlags: Long? = null, @SerialName("public_flags") - val publicFlags: Long, + val publicFlags: Long? = null, - val pronouns: String, + val pronouns: String? = null, @SerialName("premium_type") - val premiumType: Long, + val premiumType: Long? = null, - val premium: Boolean, - val phone: String, + val premium: Boolean? = null, + val phone: String? = null, @SerialName("nsfw_allowed") - val nsfwAllowed: Boolean, + val nsfwAllowed: Boolean? = null, - val mobile: Boolean, + val mobile: Boolean? = null, @SerialName("mfa_enabled") - val mfaEnabled: Boolean, + val mfaEnabled: Boolean? = null, val id: String, @SerialName("global_name") - val globalName: String, + val globalName: String? = null, - val flags: Long, - val email: String, - val discriminator: String, - val desktop: Boolean, - val bio: String, + val flags: Long? = null, + val email: String? = null, + val discriminator: String? = null, + val desktop: Boolean? = null, + val bio: String? = null, @SerialName("banner_color") - val bannerColor: String, + val bannerColor: String? = null, val banner: JsonElement? = null, @SerialName("avatar_decoration") val avatarDecoration: JsonElement? = null, - val avatar: String, + val avatar: String? = null, @SerialName("accent_color") - val accentColor: Long + val accentColor: Long? = null ) { @Serializable data class Response( @@ -70,7 +70,7 @@ data class User ( ) } - fun userAvatar():String{ + fun userAvatar(): String { return "https://cdn.discordapp.com/avatars/$id/$avatar.png" } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/connections/mal/Login.kt b/app/src/main/java/ani/dantotsu/connections/mal/Login.kt index f6249727..9ec2ca5a 100644 --- a/app/src/main/java/ani/dantotsu/connections/mal/Login.kt +++ b/app/src/main/java/ani/dantotsu/connections/mal/Login.kt @@ -4,11 +4,17 @@ import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope -import ani.dantotsu.* +import ani.dantotsu.R +import ani.dantotsu.client import ani.dantotsu.connections.mal.MAL.clientId import ani.dantotsu.connections.mal.MAL.saveResponse -import ani.dantotsu.themes.ThemeManager +import ani.dantotsu.loadData +import ani.dantotsu.logError import ani.dantotsu.others.LangSet +import ani.dantotsu.snackString +import ani.dantotsu.startMainActivity +import ani.dantotsu.themes.ThemeManager +import ani.dantotsu.tryWithSuspend import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -16,7 +22,7 @@ class Login : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() try { val data: Uri = intent?.data ?: throw Exception(getString(R.string.mal_login_uri_not_found)) @@ -46,9 +52,8 @@ ThemeManager(this).applyTheme() } } } - } - catch (e:Exception){ - logError(e,snackbar = false) + } catch (e: Exception) { + logError(e, snackbar = false) startMainActivity(this) } } diff --git a/app/src/main/java/ani/dantotsu/connections/mal/MAL.kt b/app/src/main/java/ani/dantotsu/connections/mal/MAL.kt index 26034cd2..0391cd47 100644 --- a/app/src/main/java/ani/dantotsu/connections/mal/MAL.kt +++ b/app/src/main/java/ani/dantotsu/connections/mal/MAL.kt @@ -6,7 +6,13 @@ import android.net.Uri import android.util.Base64 import androidx.browser.customtabs.CustomTabsIntent import androidx.fragment.app.FragmentActivity -import ani.dantotsu.* +import ani.dantotsu.R +import ani.dantotsu.client +import ani.dantotsu.currContext +import ani.dantotsu.loadData +import ani.dantotsu.openLinkInBrowser +import ani.dantotsu.saveData +import ani.dantotsu.tryWithSuspend import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.io.File @@ -94,6 +100,6 @@ object MAL { @SerialName("expires_in") var expiresIn: Long, @SerialName("access_token") val accessToken: String, @SerialName("refresh_token") val refreshToken: String, - ): java.io.Serializable + ) : java.io.Serializable } diff --git a/app/src/main/java/ani/dantotsu/connections/mal/MALQueries.kt b/app/src/main/java/ani/dantotsu/connections/mal/MALQueries.kt index 28662cf6..c35cdcb9 100644 --- a/app/src/main/java/ani/dantotsu/connections/mal/MALQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/mal/MALQueries.kt @@ -1,7 +1,7 @@ package ani.dantotsu.connections.mal -import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.client +import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.tryWithSuspend import kotlinx.serialization.Serializable @@ -43,18 +43,18 @@ class MALQueries { start: FuzzyDate? = null, end: FuzzyDate? = null ) { - if(idMAL==null) return + if (idMAL == null) return val data = mutableMapOf("status" to convertStatus(isAnime, status)) if (progress != null) data[if (isAnime) "num_watched_episodes" else "num_chapters_read"] = progress.toString() data[if (isAnime) "is_rewatching" else "is_rereading"] = (status == "REPEATING").toString() if (score != null) data["score"] = score.div(10).toString() - if(rewatch!=null) - data[if(isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString() - if(start!=null) + if (rewatch != null) + data[if (isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString() + if (start != null) data["start_date"] = start.toMALString() - if(end!=null) + if (end != null) data["finish_date"] = end.toMALString() tryWithSuspend { client.put( @@ -65,8 +65,8 @@ class MALQueries { } } - suspend fun deleteList(isAnime: Boolean, idMAL: Int?){ - if(idMAL==null) return + suspend fun deleteList(isAnime: Boolean, idMAL: Int?) { + if (idMAL == null) return tryWithSuspend { client.delete( "$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status", diff --git a/app/src/main/java/ani/dantotsu/download/DownloadContainerActivity.kt b/app/src/main/java/ani/dantotsu/download/DownloadContainerActivity.kt index 2fb9df4e..21f0a910 100644 --- a/app/src/main/java/ani/dantotsu/download/DownloadContainerActivity.kt +++ b/app/src/main/java/ani/dantotsu/download/DownloadContainerActivity.kt @@ -4,15 +4,15 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import ani.dantotsu.R -import ani.dantotsu.themes.ThemeManager import ani.dantotsu.others.LangSet +import ani.dantotsu.themes.ThemeManager class DownloadContainerActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() setContentView(R.layout.activity_container) val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME") diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt index a9981088..e3b19ae1 100644 --- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt +++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt @@ -10,7 +10,8 @@ import java.io.File import java.io.Serializable class DownloadsManager(private val context: Context) { - private val prefs: SharedPreferences = context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE) + private val prefs: SharedPreferences = + context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE) private val gson = Gson() private val downloadsList = loadDownloads().toMutableList() @@ -18,6 +19,8 @@ class DownloadsManager(private val context: Context) { get() = downloadsList.filter { it.type == Download.Type.MANGA } val animeDownloads: List get() = downloadsList.filter { it.type == Download.Type.ANIME } + val novelDownloads: List + get() = downloadsList.filter { it.type == Download.Type.NOVEL } private fun saveDownloads() { val jsonString = gson.toJson(downloadsList) @@ -45,11 +48,52 @@ class DownloadsManager(private val context: Context) { saveDownloads() } - private fun removeDirectory(download: Download) { - val directory = if (download.type == Download.Type.MANGA){ - File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}") + fun removeMedia(title: String, type: Download.Type) { + val subDirectory = if (type == Download.Type.MANGA) { + "Manga" + } else if (type == Download.Type.ANIME) { + "Anime" } else { - File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}") + "Novel" + } + val directory = File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/$subDirectory/$title" + ) + 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() + } + } else { + Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show() + } + downloadsList.removeAll { it.title == title } + saveDownloads() + } + + fun queryDownload(download: Download): Boolean { + return downloadsList.contains(download) + } + + private fun removeDirectory(download: Download) { + val directory = if (download.type == Download.Type.MANGA) { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Manga/${download.title}/${download.chapter}" + ) + } else if (download.type == Download.Type.ANIME) { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Anime/${download.title}/${download.chapter}" + ) + } else { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Novel/${download.title}/${download.chapter}" + ) } // Check if the directory exists and delete it recursively @@ -66,12 +110,26 @@ class DownloadsManager(private val context: Context) { } fun exportDownloads(download: Download) { //copies to the downloads folder available to the user - val directory = if (download.type == Download.Type.MANGA){ - File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}") + val directory = if (download.type == Download.Type.MANGA) { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Manga/${download.title}/${download.chapter}" + ) + } else if (download.type == Download.Type.ANIME) { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Anime/${download.title}/${download.chapter}" + ) } else { - File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime/${download.title}/${download.chapter}") + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Novel/${download.title}/${download.chapter}" + ) } - val destination = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/${download.title}/${download.chapter}") + val destination = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/${download.title}/${download.chapter}" + ) if (directory.exists()) { val copied = directory.copyRecursively(destination, true) if (copied) { @@ -84,11 +142,13 @@ class DownloadsManager(private val context: Context) { } } - fun purgeDownloads(type: Download.Type){ - val directory = if (type == Download.Type.MANGA){ + fun purgeDownloads(type: Download.Type) { + val directory = if (type == Download.Type.MANGA) { File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga") - } else { + } else if (type == Download.Type.ANIME) { File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime") + } else { + File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel") } if (directory.exists()) { val deleted = directory.deleteRecursively() @@ -105,11 +165,18 @@ class DownloadsManager(private val context: Context) { saveDownloads() } + companion object { + const val novelLocation = "Dantotsu/Novel" + const val mangaLocation = "Dantotsu/Manga" + const val animeLocation = "Dantotsu/Anime" + } + } data class Download(val title: String, val chapter: String, val type: Type) : Serializable { enum class Type { MANGA, - ANIME + ANIME, + NOVEL } } diff --git a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt index cdcf6b4c..fce89d3e 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt @@ -9,7 +9,6 @@ import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.ServiceInfo import android.graphics.Bitmap -import android.net.Uri import android.os.Build import android.os.Environment import android.os.IBinder @@ -17,24 +16,13 @@ import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat import ani.dantotsu.R import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadsManager +import ani.dantotsu.logger import ani.dantotsu.media.Media import ani.dantotsu.media.manga.ImageData -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.File -import java.io.FileOutputStream -import com.google.gson.Gson -import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS -import java.net.HttpURLConnection -import java.net.URL -import androidx.core.content.ContextCompat import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROGRESS @@ -44,15 +32,27 @@ import ani.dantotsu.snackString import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.gson.GsonBuilder import com.google.gson.InstanceCreator -import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapterImpl import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.File +import java.io.FileOutputStream +import java.net.HttpURLConnection +import java.net.URL import java.util.Queue import java.util.concurrent.ConcurrentLinkedQueue @@ -82,18 +82,27 @@ class MangaDownloaderService : Service() { setProgress(0, 0, false) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - startForeground(NOTIFICATION_ID, builder.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) - }else{ + startForeground( + NOTIFICATION_ID, + builder.build(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } else { startForeground(NOTIFICATION_ID, builder.build()) } - ContextCompat.registerReceiver(this, cancelReceiver, IntentFilter(ACTION_CANCEL_DOWNLOAD), ContextCompat.RECEIVER_EXPORTED) + ContextCompat.registerReceiver( + this, + cancelReceiver, + IntentFilter(ACTION_CANCEL_DOWNLOAD), + ContextCompat.RECEIVER_EXPORTED + ) } override fun onDestroy() { super.onDestroy() - ServiceDataSingleton.downloadQueue.clear() + MangaServiceDataSingleton.downloadQueue.clear() downloadJobs.clear() - ServiceDataSingleton.isServiceRunning = false + MangaServiceDataSingleton.isServiceRunning = false unregisterReceiver(cancelReceiver) } @@ -114,8 +123,8 @@ class MangaDownloaderService : Service() { private fun processQueue() { CoroutineScope(Dispatchers.Default).launch { - while (ServiceDataSingleton.downloadQueue.isNotEmpty()) { - val task = ServiceDataSingleton.downloadQueue.poll() + while (MangaServiceDataSingleton.downloadQueue.isNotEmpty()) { + val task = MangaServiceDataSingleton.downloadQueue.poll() if (task != null) { val job = launch { download(task) } mutex.withLock { @@ -127,7 +136,7 @@ class MangaDownloaderService : Service() { } updateNotification() // Update the notification after each task is completed } - if (ServiceDataSingleton.downloadQueue.isEmpty()) { + if (MangaServiceDataSingleton.downloadQueue.isEmpty()) { withContext(Dispatchers.Main) { stopSelf() // Stop the service when the queue is empty } @@ -141,7 +150,7 @@ class MangaDownloaderService : Service() { mutex.withLock { downloadJobs[chapter]?.cancel() downloadJobs.remove(chapter) - ServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter } + MangaServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter } updateNotification() // Update the notification after cancellation } } @@ -149,7 +158,7 @@ class MangaDownloaderService : Service() { private fun updateNotification() { // Update the notification to reflect the current state of the queue - val pendingDownloads = ServiceDataSingleton.downloadQueue.size + val pendingDownloads = MangaServiceDataSingleton.downloadQueue.size val text = if (pendingDownloads > 0) { "Pending downloads: $pendingDownloads" } else { @@ -167,74 +176,90 @@ class MangaDownloaderService : Service() { } suspend fun download(task: DownloadTask) { - withContext(Dispatchers.Main) { - val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - ContextCompat.checkSelfPermission( - this@MangaDownloaderService, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - } else { - true - } - - val deferredList = mutableListOf>() - builder.setContentText("Downloading ${task.title} - ${task.chapter}") - if (notifi) { - notificationManager.notify(NOTIFICATION_ID, builder.build()) - } - - // Loop through each ImageData object from the task - var farthest = 0 - for ((index, image) in task.imageData.withIndex()) { - // Limit the number of simultaneous downloads from the task - if (deferredList.size >= task.simultaneousDownloads) { - // Wait for all deferred to complete and clear the list - deferredList.awaitAll() - deferredList.clear() + try { + withContext(Dispatchers.Main) { + val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission( + this@MangaDownloaderService, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } else { + true } - // Download the image and add to deferred list - val deferred = async(Dispatchers.IO) { - var bitmap: Bitmap? = null - var retryCount = 0 + val deferredList = mutableListOf>() + builder.setContentText("Downloading ${task.title} - ${task.chapter}") + if (notifi) { + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } - while (bitmap == null && retryCount < task.retries) { - bitmap = image.fetchAndProcessImage( - image.page, - image.source, - this@MangaDownloaderService + // Loop through each ImageData object from the task + var farthest = 0 + for ((index, image) in task.imageData.withIndex()) { + // Limit the number of simultaneous downloads from the task + if (deferredList.size >= task.simultaneousDownloads) { + // Wait for all deferred to complete and clear the list + deferredList.awaitAll() + deferredList.clear() + } + + // Download the image and add to deferred list + val deferred = async(Dispatchers.IO) { + var bitmap: Bitmap? = null + var retryCount = 0 + + while (bitmap == null && retryCount < task.retries) { + bitmap = image.fetchAndProcessImage( + image.page, + image.source, + this@MangaDownloaderService + ) + retryCount++ + } + + // Cache the image if successful + if (bitmap != null) { + saveToDisk("$index.jpg", bitmap, task.title, task.chapter) + } + farthest++ + builder.setProgress(task.imageData.size, farthest, false) + broadcastDownloadProgress( + task.chapter, + farthest * 100 / task.imageData.size ) - retryCount++ + if (notifi) { + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } + + bitmap } - // Cache the image if successful - if (bitmap != null) { - saveToDisk("$index.jpg", bitmap, task.title, task.chapter) - } - farthest++ - builder.setProgress(task.imageData.size, farthest, false) - broadcastDownloadProgress(task.chapter, farthest * 100 / task.imageData.size) - if (notifi) { - notificationManager.notify(NOTIFICATION_ID, builder.build()) - } - - bitmap + deferredList.add(deferred) } - deferredList.add(deferred) + // Wait for any remaining deferred to complete + deferredList.awaitAll() + + builder.setContentText("${task.title} - ${task.chapter} Download complete") + .setProgress(0, 0, false) + notificationManager.notify(NOTIFICATION_ID, builder.build()) + + saveMediaInfo(task) + downloadsManager.addDownload( + Download( + task.title, + task.chapter, + Download.Type.MANGA + ) + ) + broadcastDownloadFinished(task.chapter) + snackString("${task.title} - ${task.chapter} Download finished") } - - // Wait for any remaining deferred to complete - deferredList.awaitAll() - - builder.setContentText("${task.title} - ${task.chapter} Download complete") - .setProgress(0, 0, false) - notificationManager.notify(NOTIFICATION_ID, builder.build()) - - saveMediaInfo(task) - downloadsManager.addDownload(Download(task.title, task.chapter, Download.Type.MANGA)) - broadcastDownloadFinished(task.chapter) - snackString("${task.title} - ${task.chapter} Download finished") + } catch (e: Exception) { + logger("Exception while downloading file: ${e.message}") + snackString("Exception while downloading file: ${e.message}") + FirebaseCrashlytics.getInstance().recordException(e) + broadcastDownloadFailed(task.chapter) } } @@ -296,33 +321,38 @@ class MangaDownloaderService : Service() { } - private suspend fun downloadImage(url: String, directory: File, name: String): String? = withContext(Dispatchers.IO) { - var connection: HttpURLConnection? = null - println("Downloading url $url") - try { - connection = URL(url).openConnection() as HttpURLConnection - connection.connect() - if (connection.responseCode != HttpURLConnection.HTTP_OK) { - throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}") - } - - val file = File(directory, name) - FileOutputStream(file).use { output -> - connection.inputStream.use { input -> - input.copyTo(output) + private suspend fun downloadImage(url: String, directory: File, name: String): String? = + withContext(Dispatchers.IO) { + var connection: HttpURLConnection? = null + println("Downloading url $url") + try { + connection = URL(url).openConnection() as HttpURLConnection + connection.connect() + if (connection.responseCode != HttpURLConnection.HTTP_OK) { + throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}") } + + val file = File(directory, name) + FileOutputStream(file).use { output -> + connection.inputStream.use { input -> + input.copyTo(output) + } + } + return@withContext file.absolutePath + } catch (e: Exception) { + e.printStackTrace() + withContext(Dispatchers.Main) { + Toast.makeText( + this@MangaDownloaderService, + "Exception while saving ${name}: ${e.message}", + Toast.LENGTH_LONG + ).show() + } + null + } finally { + connection?.disconnect() } - return@withContext file.absolutePath - } catch (e: Exception) { - e.printStackTrace() - withContext(Dispatchers.Main) { - Toast.makeText(this@MangaDownloaderService, "Exception while saving ${name}: ${e.message}", Toast.LENGTH_LONG).show() - } - null - } finally { - connection?.disconnect() } - } private fun broadcastDownloadStarted(chapterNumber: String) { val intent = Intent(ACTION_DOWNLOAD_STARTED).apply { @@ -381,10 +411,11 @@ class MangaDownloaderService : Service() { } } -object ServiceDataSingleton { +object MangaServiceDataSingleton { var imageData: List = listOf() var sourceMedia: Media? = null var downloadQueue: Queue = ConcurrentLinkedQueue() + @Volatile var isServiceRunning: Boolean = false } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt index a9933dfa..b91bb55a 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaAdapter.kt @@ -11,8 +11,15 @@ import androidx.cardview.widget.CardView import ani.dantotsu.R -class OfflineMangaAdapter(private val context: Context, private val items: List) : BaseAdapter() { - private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater +class OfflineMangaAdapter( + private val context: Context, + private var items: List, + private val searchListener: OfflineMangaSearchListener +) : BaseAdapter() { + private val inflater: LayoutInflater = + context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + private var originalItems: List = items + override fun getCount(): Int { return items.size } @@ -49,4 +56,22 @@ class OfflineMangaAdapter(private val context: Context, private val items: List< } return view } + + fun onSearchQuery(query: String) { + // Implement the filtering logic here, for example: + items = if (query.isEmpty()) { + // Return the original list if the query is empty + originalItems + } else { + // Filter the list based on the query + originalItems.filter { it.title.contains(query, ignoreCase = true) } + } + notifyDataSetChanged() // Notify the adapter that the data set has changed + } + + fun setItems(items: List) { + this.items = items + this.originalItems = items + notifyDataSetChanged() + } } \ No newline at end of file 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 4167a94e..11d449ff 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt @@ -7,29 +7,25 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment +import android.text.Editable +import android.text.TextWatcher import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.OvershootInterpolator +import android.widget.AutoCompleteTextView import android.widget.GridView import androidx.appcompat.app.AppCompatActivity import androidx.cardview.widget.CardView -import androidx.core.view.updatePaddingRelative import androidx.fragment.app.Fragment -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import ani.dantotsu.R -import ani.dantotsu.Refresh import ani.dantotsu.currContext -import ani.dantotsu.databinding.FragmentMangaBinding import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadsManager import ani.dantotsu.logger import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity -import ani.dantotsu.media.manga.MangaNameAdapter -import ani.dantotsu.navBarHeight -import ani.dantotsu.px import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.settings.SettingsDialogFragment import ani.dantotsu.snackString @@ -38,23 +34,27 @@ import com.google.android.material.card.MaterialCardView import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.textfield.TextInputLayout import com.google.firebase.crashlytics.FirebaseCrashlytics -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.File import com.google.gson.GsonBuilder import com.google.gson.InstanceCreator import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapterImpl +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.File import kotlin.math.max import kotlin.math.min -class OfflineMangaFragment: Fragment() { +class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { private val downloadManager = Injekt.get() private var downloads: List = listOf() private lateinit var gridView: GridView private lateinit var adapter: OfflineMangaAdapter - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { val view = inflater.inflate(R.layout.fragment_manga_offline, container, false) val textInputLayout = view.findViewById(R.id.offlineMangaSearchBar) @@ -67,25 +67,44 @@ class OfflineMangaFragment: Fragment() { requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true) val color = typedValue.data - val animeUserAvatar= view.findViewById(R.id.offlineMangaUserAvatar) + val animeUserAvatar = view.findViewById(R.id.offlineMangaUserAvatar) animeUserAvatar.setSafeOnClickListener { - SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show((it.context as AppCompatActivity).supportFragmentManager, "dialog") + SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show( + (it.context as AppCompatActivity).supportFragmentManager, + "dialog" + ) } - val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean("colorOverflow", false) ?: false + val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + ?.getBoolean("colorOverflow", false) ?: false if (!colorOverflow) { textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt() materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt()) } + val searchView = view.findViewById(R.id.animeSearchBarText) + searchView.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + } + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + onSearchQuery(s.toString()) + } + }) + gridView = view.findViewById(R.id.gridView) getDownloads() - adapter = OfflineMangaAdapter(requireContext(), downloads) + adapter = OfflineMangaAdapter(requireContext(), downloads, this) gridView.adapter = adapter gridView.setOnItemClickListener { parent, view, position, id -> // Get the OfflineMangaModel that was clicked val item = adapter.getItem(position) as OfflineMangaModel - val media = downloadManager.mangaDownloads.filter { it.title == item.title }.firstOrNull() + val media = + downloadManager.mangaDownloads.firstOrNull { it.title == item.title } + ?: downloadManager.novelDownloads.firstOrNull { it.title == item.title } media?.let { startActivity( Intent(requireContext(), MediaDetailsActivity::class.java) @@ -97,9 +116,37 @@ class OfflineMangaFragment: Fragment() { } } + gridView.setOnItemLongClickListener { parent, view, position, id -> + // Get the OfflineMangaModel that was clicked + val item = adapter.getItem(position) as OfflineMangaModel + val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) { + Download.Type.MANGA + } else { + Download.Type.NOVEL + } + // Alert dialog to confirm deletion + val builder = androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup) + builder.setTitle("Delete ${item.title}?") + builder.setMessage("Are you sure you want to delete ${item.title}?") + builder.setPositiveButton("Yes") { _, _ -> + downloadManager.removeMedia(item.title, type) + getDownloads() + adapter.setItems(downloads) + } + builder.setNegativeButton("No") { _, _ -> + // Do nothing + } + builder.show() + true + } + return view } + override fun onSearchQuery(query: String) { + adapter.onSearchQuery(query) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) var height = statusBarHeight @@ -139,9 +186,7 @@ class OfflineMangaFragment: Fragment() { } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - } + override fun onResume() { super.onResume() getDownloads() @@ -162,25 +207,44 @@ class OfflineMangaFragment: Fragment() { super.onStop() downloads = listOf() } + private fun getDownloads() { - val titles = downloadManager.mangaDownloads.map { it.title }.distinct() - val newDownloads = mutableListOf() - for (title in titles) { + downloads = listOf() + val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct() + val newMangaDownloads = mutableListOf() + for (title in mangaTitles) { val _downloads = downloadManager.mangaDownloads.filter { it.title == title } val download = _downloads.first() val offlineMangaModel = loadOfflineMangaModel(download) - newDownloads += offlineMangaModel + newMangaDownloads += offlineMangaModel } - downloads = newDownloads + downloads = newMangaDownloads + val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct() + val newNovelDownloads = mutableListOf() + for (title in novelTitles) { + val _downloads = downloadManager.novelDownloads.filter { it.title == title } + val download = _downloads.first() + val offlineMangaModel = loadOfflineMangaModel(download) + newNovelDownloads += offlineMangaModel + } + downloads += newNovelDownloads + } private fun getMedia(download: Download): Media? { + val type = if (download.type == Download.Type.MANGA) { + "Manga" + } else if (download.type == Download.Type.ANIME) { + "Anime" + } else { + "Novel" + } val directory = File( currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Manga/${download.title}" + "Dantotsu/$type/${download.title}" ) //load media.json and convert to media class with gson - try { + return try { val gson = GsonBuilder() .registerTypeAdapter(SChapter::class.java, InstanceCreator { SChapterImpl() // Provide an instance of SChapterImpl @@ -188,20 +252,26 @@ class OfflineMangaFragment: Fragment() { .create() val media = File(directory, "media.json") val mediaJson = media.readText() - return gson.fromJson(mediaJson, Media::class.java) - } - catch (e: Exception){ + gson.fromJson(mediaJson, Media::class.java) + } catch (e: Exception) { logger("Error loading media.json: ${e.message}") logger(e.printStackTrace()) FirebaseCrashlytics.getInstance().recordException(e) - return null + null } } - private fun loadOfflineMangaModel(download: Download): OfflineMangaModel{ + private fun loadOfflineMangaModel(download: Download): OfflineMangaModel { + val type = if (download.type == Download.Type.MANGA) { + "Manga" + } else if (download.type == Download.Type.ANIME) { + "Anime" + } else { + "Novel" + } val directory = File( currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - "Dantotsu/Manga/${download.title}" + "Dantotsu/$type/${download.title}" ) //load media.json and convert to media class with gson try { @@ -214,18 +284,21 @@ class OfflineMangaFragment: Fragment() { } else { null } - val title = mediaModel.nameMAL?:"unknown" - val score = if (mediaModel.userScore != 0) mediaModel.userScore.toString() else - if (mediaModel.meanScore == null) "?" else mediaModel.meanScore.toString() + val title = mediaModel.nameMAL ?: "unknown" + val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore + ?: 0) else mediaModel.userScore) / 10.0).toString() val isOngoing = false val isUserScored = mediaModel.userScore != 0 return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri) - } - catch (e: Exception){ + } catch (e: Exception) { logger("Error loading media.json: ${e.message}") logger(e.printStackTrace()) FirebaseCrashlytics.getInstance().recordException(e) return OfflineMangaModel("unknown", "0", false, false, null) } } +} + +interface OfflineMangaSearchListener { + fun onSearchQuery(query: String) } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt index 30b97911..568081ee 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaModel.kt @@ -2,5 +2,10 @@ package ani.dantotsu.download.manga import android.net.Uri -data class OfflineMangaModel(val title: String, val score: String, val isOngoing: Boolean, val isUserScored: Boolean, val image: Uri?) { -} \ No newline at end of file +data class OfflineMangaModel( + val title: String, + val score: String, + val isOngoing: Boolean, + val isUserScored: Boolean, + val image: Uri? +) \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt new file mode 100644 index 00000000..e987cd25 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt @@ -0,0 +1,478 @@ +package ani.dantotsu.download.novel + +import android.Manifest +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +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 +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import ani.dantotsu.R +import ani.dantotsu.download.Download +import ani.dantotsu.download.DownloadsManager +import ani.dantotsu.logger +import ani.dantotsu.media.Media +import ani.dantotsu.media.novel.NovelReadFragment +import ani.dantotsu.snackString +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.gson.GsonBuilder +import com.google.gson.InstanceCreator +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SChapterImpl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import okhttp3.Request +import okio.buffer +import okio.sink +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 +import java.util.Queue +import java.util.concurrent.ConcurrentLinkedQueue + +class NovelDownloaderService : Service() { + + private lateinit var notificationManager: NotificationManagerCompat + private lateinit var builder: NotificationCompat.Builder + private val downloadsManager: DownloadsManager = Injekt.get() + + private val downloadJobs = mutableMapOf() + private val mutex = Mutex() + private var isCurrentlyProcessing = false + + val networkHelper = Injekt.get() + + override fun onBind(intent: Intent?): IBinder? { + // This is only required for bound services. + return null + } + + override fun onCreate() { + super.onCreate() + notificationManager = NotificationManagerCompat.from(this) + builder = + NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply { + setContentTitle("Novel Download Progress") + setSmallIcon(R.drawable.ic_round_download_24) + priority = NotificationCompat.PRIORITY_DEFAULT + setOnlyAlertOnce(true) + setProgress(0, 0, false) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + NOTIFICATION_ID, + builder.build(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } else { + startForeground(NOTIFICATION_ID, builder.build()) + } + ContextCompat.registerReceiver( + this, + cancelReceiver, + IntentFilter(ACTION_CANCEL_DOWNLOAD), + ContextCompat.RECEIVER_EXPORTED + ) + } + + override fun onDestroy() { + super.onDestroy() + NovelServiceDataSingleton.downloadQueue.clear() + downloadJobs.clear() + NovelServiceDataSingleton.isServiceRunning = false + unregisterReceiver(cancelReceiver) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + snackString("Download started") + val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + serviceScope.launch { + mutex.withLock { + if (!isCurrentlyProcessing) { + isCurrentlyProcessing = true + processQueue() + isCurrentlyProcessing = false + } + } + } + return START_NOT_STICKY + } + + private fun processQueue() { + CoroutineScope(Dispatchers.Default).launch { + while (NovelServiceDataSingleton.downloadQueue.isNotEmpty()) { + val task = NovelServiceDataSingleton.downloadQueue.poll() + if (task != null) { + val job = launch { download(task) } + mutex.withLock { + downloadJobs[task.chapter] = job + } + job.join() // Wait for the job to complete before continuing to the next task + mutex.withLock { + downloadJobs.remove(task.chapter) + } + updateNotification() // Update the notification after each task is completed + } + if (NovelServiceDataSingleton.downloadQueue.isEmpty()) { + withContext(Dispatchers.Main) { + stopSelf() // Stop the service when the queue is empty + } + } + } + } + } + + fun cancelDownload(chapter: String) { + CoroutineScope(Dispatchers.Default).launch { + mutex.withLock { + downloadJobs[chapter]?.cancel() + downloadJobs.remove(chapter) + NovelServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter } + updateNotification() // Update the notification after cancellation + } + } + } + + private fun updateNotification() { + // Update the notification to reflect the current state of the queue + val pendingDownloads = NovelServiceDataSingleton.downloadQueue.size + val text = if (pendingDownloads > 0) { + "Pending downloads: $pendingDownloads" + } else { + "All downloads completed" + } + builder.setContentText(text) + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } + + private suspend fun isEpubFile(urlString: String): Boolean { + return withContext(Dispatchers.IO) { + try { + val request = Request.Builder() + .url(urlString) + .head() + .build() + + networkHelper.client.newCall(request).execute().use { response -> + val contentType = response.header("Content-Type") + val contentDisposition = response.header("Content-Disposition") + + logger("Content-Type: $contentType") + logger("Content-Disposition: $contentDisposition") + + // Return true if the Content-Type or Content-Disposition indicates an EPUB file + contentType == "application/epub+zip" || + (contentDisposition?.contains(".epub") == true) + } + } catch (e: Exception) { + logger("Error checking file type: ${e.message}") + false + } + } + } + + private fun isAlreadyDownloaded(urlString: String): Boolean { + return urlString.contains("file://") + } + + suspend fun download(task: DownloadTask) { + try { + withContext(Dispatchers.Main) { + val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission( + this@NovelDownloaderService, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } else { + true + } + + broadcastDownloadStarted(task.originalLink) + + if (notifi) { + builder.setContentText("Downloading ${task.title} - ${task.chapter}") + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } + + if (!isEpubFile(task.downloadLink)) { + if (isAlreadyDownloaded(task.originalLink)) { + logger("Already downloaded") + broadcastDownloadFinished(task.originalLink) + snackString("Already downloaded") + return@withContext + } + logger("Download link is not an .epub file") + broadcastDownloadFailed(task.originalLink) + snackString("Download link is not an .epub file") + return@withContext + } + + // Start the download + withContext(Dispatchers.IO) { + try { + val request = Request.Builder() + .url(task.downloadLink) + .build() + + networkHelper.downloadClient.newCall(request).execute().use { response -> + // Ensure the response is successful and has a body + if (!response.isSuccessful || response.body == null) { + throw IOException("Failed to download file: ${response.message}") + } + + val file = File( + this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Novel/${task.title}/${task.chapter}/0.epub" + ) + + // Create directories if they don't exist + file.parentFile?.takeIf { !it.exists() }?.mkdirs() + + // Overwrite existing file + if (file.exists()) file.delete() + + //download cover + task.coverUrl?.let { + file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") } + } + + val sink = file.sink().buffer() + val responseBody = response.body + val totalBytes = responseBody.contentLength() + var downloadedBytes = 0L + + val notificationUpdateInterval = 1024 * 1024 // 1 MB + val broadcastUpdateInterval = 1024 * 256 // 256 KB + var lastNotificationUpdate = 0L + var lastBroadcastUpdate = 0L + + responseBody.source().use { source -> + while (true) { + val read = source.read(sink.buffer, 8192) + if (read == -1L) break + downloadedBytes += read + sink.emit() + + // Update progress at intervals + if (downloadedBytes - lastNotificationUpdate >= notificationUpdateInterval) { + withContext(Dispatchers.Main) { + val progress = + (downloadedBytes * 100 / totalBytes).toInt() + builder.setProgress(100, progress, false) + if (notifi) { + notificationManager.notify( + NOTIFICATION_ID, + builder.build() + ) + } + } + lastNotificationUpdate = downloadedBytes + } + if (downloadedBytes - lastBroadcastUpdate >= broadcastUpdateInterval) { + withContext(Dispatchers.Main) { + val progress = + (downloadedBytes * 100 / totalBytes).toInt() + logger("Download progress: $progress") + broadcastDownloadProgress(task.originalLink, progress) + } + lastBroadcastUpdate = downloadedBytes + } + } + } + + sink.close() + //if the file is smaller than 95% of totalBytes, it means the download was interrupted + if (file.length() < totalBytes * 0.95) { + throw IOException("Failed to download file: ${response.message}") + } + } + } catch (e: Exception) { + logger("Exception while downloading .epub inside request: ${e.message}") + throw e + } + } + + // Update notification for download completion + builder.setContentText("${task.title} - ${task.chapter} Download complete") + .setProgress(0, 0, false) + if (notifi) { + notificationManager.notify(NOTIFICATION_ID, builder.build()) + } + + saveMediaInfo(task) + downloadsManager.addDownload( + Download( + task.title, + task.chapter, + Download.Type.NOVEL + ) + ) + broadcastDownloadFinished(task.originalLink) + snackString("${task.title} - ${task.chapter} Download finished") + } + } catch (e: Exception) { + logger("Exception while downloading .epub: ${e.message}") + snackString("Exception while downloading .epub: ${e.message}") + FirebaseCrashlytics.getInstance().recordException(e) + broadcastDownloadFailed(task.originalLink) + } + } + + private fun saveMediaInfo(task: DownloadTask) { + GlobalScope.launch(Dispatchers.IO) { + val directory = File( + getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Novel/${task.title}" + ) + if (!directory.exists()) directory.mkdirs() + + val file = File(directory, "media.json") + val gson = GsonBuilder() + .registerTypeAdapter(SChapter::class.java, InstanceCreator { + SChapterImpl() // Provide an instance of SChapterImpl + }) + .create() + val mediaJson = gson.toJson(task.sourceMedia) + val media = gson.fromJson(mediaJson, Media::class.java) + if (media != null) { + media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") } + media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") } + + val jsonString = gson.toJson(media) + withContext(Dispatchers.Main) { + file.writeText(jsonString) + } + } + } + } + + + private suspend fun downloadImage(url: String, directory: File, name: String): String? = + withContext( + Dispatchers.IO + ) { + var connection: HttpURLConnection? = null + println("Downloading url $url") + try { + connection = URL(url).openConnection() as HttpURLConnection + connection.connect() + if (connection.responseCode != HttpURLConnection.HTTP_OK) { + throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}") + } + + val file = File(directory, name) + FileOutputStream(file).use { output -> + connection.inputStream.use { input -> + input.copyTo(output) + } + } + return@withContext file.absolutePath + } catch (e: Exception) { + e.printStackTrace() + withContext(Dispatchers.Main) { + Toast.makeText( + this@NovelDownloaderService, + "Exception while saving ${name}: ${e.message}", + Toast.LENGTH_LONG + ).show() + } + null + } finally { + connection?.disconnect() + } + } + + private fun broadcastDownloadStarted(link: String) { + val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_STARTED).apply { + putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link) + } + sendBroadcast(intent) + } + + private fun broadcastDownloadFinished(link: String) { + val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_FINISHED).apply { + putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link) + } + sendBroadcast(intent) + } + + private fun broadcastDownloadFailed(link: String) { + val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_FAILED).apply { + putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link) + } + sendBroadcast(intent) + } + + private fun broadcastDownloadProgress(link: String, progress: Int) { + val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_PROGRESS).apply { + putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link) + putExtra("progress", progress) + } + sendBroadcast(intent) + } + + private val cancelReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == ACTION_CANCEL_DOWNLOAD) { + val chapter = intent.getStringExtra(EXTRA_CHAPTER) + chapter?.let { + cancelDownload(it) + } + } + } + } + + + data class DownloadTask( + val title: String, + val chapter: String, + val downloadLink: String, + val originalLink: String, + val sourceMedia: Media? = null, + val coverUrl: String? = null, + val retries: Int = 2, + ) + + companion object { + private const val NOTIFICATION_ID = 1103 + const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download" + const val EXTRA_CHAPTER = "extra_chapter" + } +} + +object NovelServiceDataSingleton { + var sourceMedia: Media? = null + var downloadQueue: Queue = ConcurrentLinkedQueue() + + @Volatile + var isServiceRunning: Boolean = false +} \ No newline at end of file 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 15cbda59..2dcf31c6 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -38,9 +38,10 @@ import java.util.concurrent.* object Helper { @SuppressLint("UnsafeOptInUsageError") - fun downloadVideo(context : Context, video: Video, subtitle: Subtitle?){ + fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) { val dataSourceFactory = DataSource.Factory { - val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() + val dataSource: HttpDataSource = + OkHttpDataSource.Factory(okHttpClient).createDataSource() defaultHeaders.forEach { dataSource.setRequestProperty(it.key, it.value) } @@ -52,7 +53,7 @@ object Helper { val mimeType = when (video.format) { VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8 VideoType.DASH -> MimeTypes.APPLICATION_MPD - else -> MimeTypes.APPLICATION_MP4 + else -> MimeTypes.APPLICATION_MP4 } val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType) @@ -79,12 +80,13 @@ object Helper { DefaultRenderersFactory(context), dataSourceFactory ) - downloadHelper.prepare(object : DownloadHelper.Callback{ + downloadHelper.prepare(object : DownloadHelper.Callback { override fun onPrepared(helper: DownloadHelper) { - TrackSelectionDialogBuilder(context,"Select thingy",helper.getTracks(0).groups + TrackSelectionDialogBuilder( + context, "Select thingy", helper.getTracks(0).groups ) { _, overrides -> val params = TrackSelectionParameters.Builder(context) - overrides.forEach{ + overrides.forEach { params.addOverride(it.value) } helper.addTrackSelection(0, params.build()) @@ -124,7 +126,8 @@ object Helper { //val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() val networkHelper = Injekt.get() val okHttpClient = networkHelper.client - val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() + val dataSource: HttpDataSource = + OkHttpDataSource.Factory(okHttpClient).createDataSource() defaultHeaders.forEach { dataSource.setRequestProperty(it.key, it.value) } @@ -137,7 +140,8 @@ object Helper { dataSourceFactory, Executor(Runnable::run) ).apply { - requirements = Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW) + requirements = + Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW) maxParallelDownloads = 3 } } diff --git a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt b/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt index 18daad3a..3a26ac1d 100644 --- a/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt +++ b/app/src/main/java/ani/dantotsu/download/video/MyDownloadService.kt @@ -21,7 +21,10 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID) - override fun getForegroundNotification(downloads: MutableList, notMetRequirements: Int): Notification = + override fun getForegroundNotification( + downloads: MutableList, + notMetRequirements: Int + ): Notification = DownloadNotificationHelper(this, "download_service").buildProgressNotification( this, R.drawable.mono, diff --git a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt index 3e80d341..1a08934d 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt @@ -48,7 +48,8 @@ class AnimeFragment : Fragment() { private var _binding: FragmentAnimeBinding? = null private val binding get() = _binding!! - private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings() + private var uiSettings: UserInterfaceSettings = + loadData("ui_settings") ?: UserInterfaceSettings() val model: AnilistAnimeViewModel by activityViewModels() @@ -224,7 +225,8 @@ class AnimeFragment : Fragment() { } } } - binding.animePageScrollTop.translationY = -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() + binding.animePageScrollTop.translationY = + -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() } } diff --git a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt index 8cda9988..63ee9b84 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt @@ -2,7 +2,6 @@ package ani.dantotsu.home import android.content.Context import android.content.Intent -import android.graphics.Color import android.os.Handler import android.os.Looper import android.util.TypedValue @@ -18,7 +17,6 @@ import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 -import ani.dantotsu.media.GenreActivity import ani.dantotsu.MediaPageTransformer import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist @@ -27,6 +25,7 @@ import ani.dantotsu.databinding.ItemAnimePageBinding import ani.dantotsu.loadData import ani.dantotsu.loadImage import ani.dantotsu.media.CalendarActivity +import ani.dantotsu.media.GenreActivity import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.SearchActivity import ani.dantotsu.px @@ -45,10 +44,12 @@ class AnimePageAdapter : RecyclerView.Adapter(R.id.animeUserAvatarContainer) + val materialCardView = + holder.itemView.findViewById(R.id.animeUserAvatarContainer) materialCardView.setCardBackgroundColor(semiTransparentColor) val typedValue = TypedValue() currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true) val color = typedValue.data - val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean("colorOverflow", false) ?: false + val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + ?.getBoolean("colorOverflow", false) ?: false if (!colorOverflow) { textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt() materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt()) @@ -95,7 +98,10 @@ class AnimePageAdapter : RecyclerView.Adapter onIncludeListClick.invoke(isChecked) } @@ -133,9 +140,9 @@ class AnimePageAdapter : RecyclerView.AdapterUnit) - lateinit var onSeasonLongClick : ((Int)->Boolean) - lateinit var onIncludeListClick : ((Boolean)->Unit) + lateinit var onSeasonClick: ((Int) -> Unit) + lateinit var onSeasonLongClick: ((Int) -> Boolean) + lateinit var onIncludeListClick: ((Boolean) -> Unit) override fun getItemCount(): Int = 1 @@ -152,7 +159,8 @@ class AnimePageAdapter : RecyclerView.Adapter { @@ -123,11 +132,13 @@ class HomeFragment : Fragment() { if (!binding.homeScroll.canScrollVertically(1)) { reached = true bottomBar.animate().translationZ(0f).setDuration(duration).start() - ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration).start() + ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration) + .start() } else { if (reached) { bottomBar.animate().translationZ(12f).setDuration(duration).start() - ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration).start() + ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration) + .start() } } } @@ -138,7 +149,13 @@ class HomeFragment : Fragment() { if (displayCutout != null) { if (displayCutout.boundingRects.size > 0) { height = - max(statusBarHeight, min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height())) + max( + statusBarHeight, + min( + displayCutout.boundingRects[0].width(), + displayCutout.boundingRects[0].height() + ) + ) } } } @@ -189,7 +206,8 @@ class HomeFragment : Fragment() { false ) recyclerView.visibility = View.VISIBLE - recyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) + recyclerView.layoutAnimation = + LayoutAnimationController(setSlideIn(uiSettings), 0.25f) } else { empty.visibility = View.VISIBLE @@ -313,7 +331,8 @@ class HomeFragment : Fragment() { live.observe(viewLifecycleOwner) { if (it) { scope.launch { - uiSettings = loadData("ui_settings") ?: UserInterfaceSettings() + uiSettings = + loadData("ui_settings") ?: UserInterfaceSettings() withContext(Dispatchers.IO) { //Get userData First getUserId(requireContext()) { diff --git a/app/src/main/java/ani/dantotsu/home/LoginFragment.kt b/app/src/main/java/ani/dantotsu/home/LoginFragment.kt index 40b97cc1..d3144237 100644 --- a/app/src/main/java/ani/dantotsu/home/LoginFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/LoginFragment.kt @@ -15,7 +15,11 @@ class LoginFragment : Fragment() { private var _binding: FragmentLoginBinding? = null private val binding get() = _binding!! - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = FragmentLoginBinding.inflate(layoutInflater, container, false) return binding.root } diff --git a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt index a9d01448..14870b7f 100644 --- a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt @@ -44,11 +44,16 @@ class MangaFragment : Fragment() { private var _binding: FragmentMangaBinding? = null private val binding get() = _binding!! - private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings() + private var uiSettings: UserInterfaceSettings = + loadData("ui_settings") ?: UserInterfaceSettings() val model: AnilistMangaViewModel by activityViewModels() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = FragmentMangaBinding.inflate(inflater, container, false) return binding.root } @@ -100,7 +105,8 @@ class MangaFragment : Fragment() { } val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity()) val progressAdaptor = ProgressAdapter(searched = model.searched) - binding.mangaPageRecyclerView.adapter = ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor) + binding.mangaPageRecyclerView.adapter = + ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor) val layout = LinearLayoutManager(requireContext()) binding.mangaPageRecyclerView.layoutManager = layout @@ -177,7 +183,8 @@ class MangaFragment : Fragment() { } } } - binding.mangaPageScrollTop.translationY = -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() + binding.mangaPageScrollTop.translationY = + -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() } } diff --git a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt index 5103c72b..7053df8a 100644 --- a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt @@ -17,7 +17,6 @@ import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 -import ani.dantotsu.media.GenreActivity import ani.dantotsu.MediaPageTransformer import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist @@ -25,6 +24,7 @@ import ani.dantotsu.currContext import ani.dantotsu.databinding.ItemMangaPageBinding import ani.dantotsu.loadData import ani.dantotsu.loadImage +import ani.dantotsu.media.GenreActivity import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.SearchActivity import ani.dantotsu.px @@ -43,10 +43,12 @@ class MangaPageAdapter : RecyclerView.Adapter(R.id.mangaUserAvatarContainer) + val materialCardView = + holder.itemView.findViewById(R.id.mangaUserAvatarContainer) materialCardView.setCardBackgroundColor(semiTransparentColor) val typedValue = TypedValue() currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true) val color = typedValue.data - val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.getBoolean("colorOverflow", false) ?: false + val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + ?.getBoolean("colorOverflow", false) ?: false if (!colorOverflow) { textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt() materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt()) @@ -89,7 +93,10 @@ class MangaPageAdapter : RecyclerView.Adapter onIncludeListClick.invoke(isChecked) } @@ -126,7 +134,7 @@ class MangaPageAdapter : RecyclerView.AdapterUnit) + lateinit var onIncludeListClick: ((Boolean) -> Unit) override fun getItemCount(): Int = 1 @@ -142,7 +150,8 @@ class MangaPageAdapter : RecyclerView.Adapter("ui_settings") ?: UserInterfaceSettings() @@ -65,10 +69,13 @@ ThemeManager(this).applyTheme() ContextCompat.getColor(this, R.color.nav_bg_inv) binding.root.fitsSystemWindows = true - }else{ + } else { binding.root.fitsSystemWindows = false requestWindowFeature(Window.FEATURE_NO_TITLE) - window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) + window.setFlags( + WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN + ) } setContentView(binding.root) @@ -79,14 +86,15 @@ ThemeManager(this).applyTheme() override fun onTabSelected(tab: TabLayout.Tab?) { this@CalendarActivity.selectedTabIdx = tab?.position ?: 1 } - override fun onTabUnselected(tab: TabLayout.Tab?) { } - override fun onTabReselected(tab: TabLayout.Tab?) { } + + override fun onTabUnselected(tab: TabLayout.Tab?) {} + override fun onTabReselected(tab: TabLayout.Tab?) {} }) model.getCalendar().observe(this) { if (it != null) { binding.listProgressBar.visibility = View.GONE - binding.listViewPager.adapter = ListViewPagerAdapter(it.size, true,this) + binding.listViewPager.adapter = ListViewPagerAdapter(it.size, true, this) val keys = it.keys.toList() val values = it.values.toList() val savedTab = this.selectedTabIdx diff --git a/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt b/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt index 644402ef..63223023 100644 --- a/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt @@ -21,11 +21,13 @@ class CharacterAdapter( private val characterList: ArrayList ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder { - val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false) return CharacterViewHolder(binding) } - private val uiSettings = loadData("ui_settings") ?: UserInterfaceSettings() + private val uiSettings = + loadData("ui_settings") ?: UserInterfaceSettings() @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) { @@ -38,16 +40,23 @@ class CharacterAdapter( } override fun getItemCount(): Int = characterList.size - inner class CharacterViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) { + inner class CharacterViewHolder(val binding: ItemCharacterBinding) : + RecyclerView.ViewHolder(binding.root) { init { itemView.setOnClickListener { val char = characterList[bindingAdapterPosition] ContextCompat.startActivity( itemView.context, - Intent(itemView.context, CharacterDetailsActivity::class.java).putExtra("character", char as Serializable), + Intent( + itemView.context, + CharacterDetailsActivity::class.java + ).putExtra("character", char as Serializable), ActivityOptionsCompat.makeSceneTransitionAnimation( itemView.context as Activity, - Pair.create(binding.itemCompactImage, ViewCompat.getTransitionName(binding.itemCompactImage)!!), + Pair.create( + binding.itemCompactImage, + ViewCompat.getTransitionName(binding.itemCompactImage)!! + ), ).toBundle() ) } diff --git a/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt index 36439bb3..3ccba6e1 100644 --- a/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt @@ -13,13 +13,20 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager -import ani.dantotsu.* +import ani.dantotsu.R +import ani.dantotsu.Refresh import ani.dantotsu.databinding.ActivityCharacterBinding +import ani.dantotsu.initActivity +import ani.dantotsu.loadData +import ani.dantotsu.loadImage +import ani.dantotsu.navBarHeight import ani.dantotsu.others.ImageViewDialog -import ani.dantotsu.others.getSerialized -import ani.dantotsu.settings.UserInterfaceSettings -import ani.dantotsu.themes.ThemeManager import ani.dantotsu.others.LangSet +import ani.dantotsu.others.getSerialized +import ani.dantotsu.px +import ani.dantotsu.settings.UserInterfaceSettings +import ani.dantotsu.statusBarHeight +import ani.dantotsu.themes.ThemeManager import com.google.android.material.appbar.AppBarLayout import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -36,15 +43,17 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityCharacterBinding.inflate(layoutInflater) setContentView(binding.root) initActivity(this) screenWidth = resources.displayMetrics.run { widthPixels / density } - if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status) + if (uiSettings.immersiveMode) this.window.statusBarColor = + ContextCompat.getColor(this, R.color.status) - val banner = if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen + val banner = + if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen banner.updateLayoutParams { height += statusBarHeight } binding.characterClose.updateLayoutParams { topMargin += statusBarHeight } @@ -61,7 +70,13 @@ ThemeManager(this).applyTheme() binding.characterTitle.text = character.name banner.loadImage(character.banner) binding.characterCoverImage.loadImage(character.image) - binding.characterCoverImage.setOnLongClickListener { ImageViewDialog.newInstance(this, character.name, character.image) } + binding.characterCoverImage.setOnLongClickListener { + ImageViewDialog.newInstance( + this, + character.name, + character.image + ) + } model.getCharacter().observe(this) { if (it != null && !loaded) { @@ -73,14 +88,15 @@ ThemeManager(this).applyTheme() val roles = character.roles if (roles != null) { val mediaAdaptor = MediaAdaptor(0, roles, this, matchParent = true) - val concatAdaptor = ConcatAdapter(CharacterDetailsAdapter(character, this), mediaAdaptor) + val concatAdaptor = + ConcatAdapter(CharacterDetailsAdapter(character, this), mediaAdaptor) val gridSize = (screenWidth / 124f).toInt() val gridLayoutManager = GridLayoutManager(this, gridSize) gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return when (position) { - 0 -> gridSize + 0 -> gridSize else -> 1 } } @@ -118,16 +134,19 @@ ThemeManager(this).applyTheme() binding.characterCover.scaleY = 1f * cap binding.characterCover.cardElevation = 32f * cap - binding.characterCover.visibility = if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE + binding.characterCover.visibility = + if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE if (percentage >= percent && !isCollapsed) { isCollapsed = true - if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg) + if (uiSettings.immersiveMode) this.window.statusBarColor = + ContextCompat.getColor(this, R.color.nav_bg) binding.characterAppBar.setBackgroundResource(R.color.nav_bg) } if (percentage <= percent && isCollapsed) { isCollapsed = false - if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status) + if (uiSettings.immersiveMode) this.window.statusBarColor = + ContextCompat.getColor(this, R.color.status) binding.characterAppBar.setBackgroundResource(R.color.bg) } } diff --git a/app/src/main/java/ani/dantotsu/media/CharacterDetailsAdapter.kt b/app/src/main/java/ani/dantotsu/media/CharacterDetailsAdapter.kt index 6734765d..15781d8b 100644 --- a/app/src/main/java/ani/dantotsu/media/CharacterDetailsAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/CharacterDetailsAdapter.kt @@ -15,7 +15,8 @@ import io.noties.markwon.SoftBreakAddsNewLinePlugin class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder { - val binding = ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false) return GenreViewHolder(binding) } @@ -23,20 +24,22 @@ class CharacterDetailsAdapter(private val character: Character, private val acti override fun onBindViewHolder(holder: GenreViewHolder, position: Int) { val binding = holder.binding val desc = - (if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") + + (if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") + (if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") + - (if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when(character.gender){ + (if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when (character.gender) { "Male" -> currActivity()!!.getString(R.string.male) "Female" -> currActivity()!!.getString(R.string.female) else -> character.gender } else "") + "\n" + character.description binding.characterDesc.isTextSelectable - val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()).usePlugin(SpoilerPlugin()).build() + val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()) + .usePlugin(SpoilerPlugin()).build() markWon.setMarkdown(binding.characterDesc, desc) } override fun getItemCount(): Int = 1 - inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) : RecyclerView.ViewHolder(binding.root) + inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) : + RecyclerView.ViewHolder(binding.root) } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/GenreActivity.kt b/app/src/main/java/ani/dantotsu/media/GenreActivity.kt index ef193136..e5960502 100644 --- a/app/src/main/java/ani/dantotsu/media/GenreActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/GenreActivity.kt @@ -14,9 +14,9 @@ import ani.dantotsu.databinding.ActivityGenreBinding import ani.dantotsu.initActivity import ani.dantotsu.loadData import ani.dantotsu.navBarHeight +import ani.dantotsu.others.LangSet import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch @@ -28,7 +28,7 @@ class GenreActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityGenreBinding.inflate(layoutInflater) setContentView(binding.root) initActivity(this) @@ -50,7 +50,8 @@ ThemeManager(this).applyTheme() model.doneListener?.invoke() } binding.mediaInfoGenresRecyclerView.adapter = adapter - binding.mediaInfoGenresRecyclerView.layoutManager = GridLayoutManager(this, (screenWidth / 156f).toInt()) + binding.mediaInfoGenresRecyclerView.layoutManager = + GridLayoutManager(this, (screenWidth / 156f).toInt()) lifecycleScope.launch(Dispatchers.IO) { model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) { diff --git a/app/src/main/java/ani/dantotsu/media/GenreAdapter.kt b/app/src/main/java/ani/dantotsu/media/GenreAdapter.kt index dfb8fcab..331b230c 100644 --- a/app/src/main/java/ani/dantotsu/media/GenreAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/GenreAdapter.kt @@ -37,7 +37,8 @@ class GenreAdapter( } override fun getItemCount(): Int = genres.size - inner class GenreViewHolder(val binding: ItemGenreBinding) : RecyclerView.ViewHolder(binding.root) { + inner class GenreViewHolder(val binding: ItemGenreBinding) : + RecyclerView.ViewHolder(binding.root) { init { itemView.setOnClickListener { ContextCompat.startActivity( @@ -48,15 +49,15 @@ class GenreAdapter( .putExtra("sortBy", Anilist.sortBy[2]) .putExtra("search", true) .also { - if (pos[bindingAdapterPosition].lowercase() == "hentai") { - if (!Anilist.adult) Toast.makeText( - itemView.context, - currActivity()?.getString(R.string.content_18), - Toast.LENGTH_SHORT - ).show() - it.putExtra("hentai", true) - } - }, + if (pos[bindingAdapterPosition].lowercase() == "hentai") { + if (!Anilist.adult) Toast.makeText( + itemView.context, + currActivity()?.getString(R.string.content_18), + Toast.LENGTH_SHORT + ).show() + it.putExtra("hentai", true) + } + }, null ) } diff --git a/app/src/main/java/ani/dantotsu/media/Media.kt b/app/src/main/java/ani/dantotsu/media/Media.kt index 872baf50..3360a94a 100644 --- a/app/src/main/java/ani/dantotsu/media/Media.kt +++ b/app/src/main/java/ani/dantotsu/media/Media.kt @@ -1,5 +1,6 @@ package ani.dantotsu.media +import android.graphics.Bitmap import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.MediaEdge import ani.dantotsu.connections.anilist.api.MediaList @@ -40,7 +41,7 @@ data class Media( var userUpdatedAt: Long? = null, var userStartedAt: FuzzyDate = FuzzyDate(), var userCompletedAt: FuzzyDate = FuzzyDate(), - var inCustomListsOf: MutableMap?= null, + var inCustomListsOf: MutableMap? = null, var userFavOrder: Int? = null, val status: String? = null, @@ -69,7 +70,7 @@ data class Media( var shareLink: String? = null, var selected: Selected? = null, - var idKitsu: String?=null, + var idKitsu: String? = null, var cameFromContinue: Boolean = false ) : Serializable { @@ -119,4 +120,5 @@ data class Media( object MediaSingleton { var media: Media? = null + var bitmap: Bitmap? = null } diff --git a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt index 6f9dabed..509b40a7 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaAdaptor.kt @@ -4,10 +4,14 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.AccelerateDecelerateInterpolator +import android.widget.ImageView import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.view.updateLayoutParams @@ -37,20 +41,43 @@ class MediaAdaptor( private val viewPager: ViewPager2? = null, ) : RecyclerView.Adapter() { - private val uiSettings = loadData("ui_settings") ?: UserInterfaceSettings() + private val uiSettings = + loadData("ui_settings") ?: UserInterfaceSettings() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (type) { - 0 -> MediaViewHolder(ItemMediaCompactBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - 1 -> MediaLargeViewHolder(ItemMediaLargeBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - 2 -> MediaPageViewHolder(ItemMediaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - 3 -> MediaPageSmallViewHolder( + 0 -> MediaViewHolder( + ItemMediaCompactBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + 1 -> MediaLargeViewHolder( + ItemMediaLargeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + 2 -> MediaPageViewHolder( + ItemMediaPageBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + 3 -> MediaPageSmallViewHolder( ItemMediaPageSmallBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) + else -> throw IllegalArgumentException() } @@ -65,10 +92,12 @@ class MediaAdaptor( val media = mediaList?.getOrNull(position) if (media != null) { b.itemCompactImage.loadImage(media.cover) - b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE + b.itemCompactOngoing.visibility = + if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE b.itemCompactTitle.text = media.userPreferredName b.itemCompactScore.text = - ((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString() + ((if (media.userScore == 0) (media.meanScore + ?: 0) else media.userScore) / 10.0).toString() b.itemCompactScoreBG.background = ContextCompat.getDrawable( b.root.context, (if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score) @@ -100,6 +129,7 @@ class MediaAdaptor( } } } + 1 -> { val b = (holder as MediaLargeViewHolder).binding setAnimation(activity, b.root, uiSettings) @@ -107,22 +137,29 @@ class MediaAdaptor( if (media != null) { b.itemCompactImage.loadImage(media.cover) b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400) - b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE + b.itemCompactOngoing.visibility = + if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE b.itemCompactTitle.text = media.userPreferredName b.itemCompactScore.text = - ((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString() + ((if (media.userScore == 0) (media.meanScore + ?: 0) else media.userScore) / 10.0).toString() b.itemCompactScoreBG.background = ContextCompat.getDrawable( b.root.context, (if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score) ) if (media.anime != null) { - b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural) - else currActivity()!!.getString(R.string.episode_singular) + b.itemTotal.text = " " + if ((media.anime.totalEpisodes + ?: 0) != 1 + ) currActivity()!!.getString(R.string.episode_plural) + else currActivity()!!.getString(R.string.episode_singular) b.itemCompactTotal.text = if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes - ?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString() + ?: "??").toString()) else (media.anime.totalEpisodes + ?: "??").toString() } else if (media.manga != null) { - b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural) + b.itemTotal.text = " " + if ((media.manga.totalChapters + ?: 0) != 1 + ) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(R.string.chapter_singular) b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}" } @@ -133,6 +170,7 @@ class MediaAdaptor( } } } + 2 -> { val b = (holder as MediaPageViewHolder).binding val media = mediaList?.get(position) @@ -145,7 +183,8 @@ class MediaAdaptor( AccelerateDecelerateInterpolator() ) ) - val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen + val banner = + if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen val context = b.itemCompactBanner.context if (!(context as Activity).isDestroyed) Glide.with(context as Context) @@ -153,22 +192,29 @@ class MediaAdaptor( .diskCacheStrategy(DiskCacheStrategy.ALL).override(400) .apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3))) .into(banner) - b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE + b.itemCompactOngoing.visibility = + if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE b.itemCompactTitle.text = media.userPreferredName b.itemCompactScore.text = - ((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString() + ((if (media.userScore == 0) (media.meanScore + ?: 0) else media.userScore) / 10.0).toString() b.itemCompactScoreBG.background = ContextCompat.getDrawable( b.root.context, (if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score) ) if (media.anime != null) { - b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural) + b.itemTotal.text = " " + if ((media.anime.totalEpisodes + ?: 0) != 1 + ) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(R.string.episode_singular) b.itemCompactTotal.text = if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes - ?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString() + ?: "??").toString()) else (media.anime.totalEpisodes + ?: "??").toString() } else if (media.manga != null) { - b.itemTotal.text =" " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural) + b.itemTotal.text = " " + if ((media.manga.totalChapters + ?: 0) != 1 + ) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(R.string.chapter_singular) b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}" } @@ -180,6 +226,7 @@ class MediaAdaptor( } } } + 3 -> { val b = (holder as MediaPageSmallViewHolder).binding val media = mediaList?.get(position) @@ -192,7 +239,8 @@ class MediaAdaptor( AccelerateDecelerateInterpolator() ) ) - val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen + val banner = + if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen val context = b.itemCompactBanner.context if (!(context as Activity).isDestroyed) Glide.with(context as Context) @@ -200,10 +248,12 @@ class MediaAdaptor( .diskCacheStrategy(DiskCacheStrategy.ALL).override(400) .apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3))) .into(banner) - b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE + b.itemCompactOngoing.visibility = + if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE b.itemCompactTitle.text = media.userPreferredName b.itemCompactScore.text = - ((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString() + ((if (media.userScore == 0) (media.meanScore + ?: 0) else media.userScore) / 10.0).toString() b.itemCompactScoreBG.background = ContextCompat.getDrawable( b.root.context, (if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score) @@ -218,13 +268,18 @@ class MediaAdaptor( } b.itemCompactStatus.text = media.status ?: "" if (media.anime != null) { - b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural) + b.itemTotal.text = " " + if ((media.anime.totalEpisodes + ?: 0) != 1 + ) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(R.string.episode_singular) b.itemCompactTotal.text = if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes - ?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString() + ?: "??").toString()) else (media.anime.totalEpisodes + ?: "??").toString() } else if (media.manga != null) { - b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural) + b.itemTotal.text = " " + if ((media.manga.totalChapters + ?: 0) != 1 + ) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(R.string.chapter_singular) b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}" } @@ -245,43 +300,73 @@ class MediaAdaptor( return type } - inner class MediaViewHolder(val binding: ItemMediaCompactBinding) : RecyclerView.ViewHolder(binding.root) { + inner class MediaViewHolder(val binding: ItemMediaCompactBinding) : + RecyclerView.ViewHolder(binding.root) { init { if (matchParent) itemView.updateLayoutParams { width = -1 } - itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) } + itemView.setSafeOnClickListener { + clicked( + bindingAdapterPosition, + resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) + ) + } itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) } } } - inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) : RecyclerView.ViewHolder(binding.root) { + inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) : + RecyclerView.ViewHolder(binding.root) { init { - itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) } + itemView.setSafeOnClickListener { + clicked( + bindingAdapterPosition, + resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) + ) + } itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) } } } @SuppressLint("ClickableViewAccessibility") - inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) : RecyclerView.ViewHolder(binding.root) { + inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) : + RecyclerView.ViewHolder(binding.root) { init { - binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) } + binding.itemCompactImage.setSafeOnClickListener { + clicked( + bindingAdapterPosition, + resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) + ) + } itemView.setOnTouchListener { _, _ -> true } binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) } } } @SuppressLint("ClickableViewAccessibility") - inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) : RecyclerView.ViewHolder(binding.root) { + inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) : + RecyclerView.ViewHolder(binding.root) { init { - binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) } - binding.itemCompactTitleContainer.setSafeOnClickListener { clicked(bindingAdapterPosition) } + binding.itemCompactImage.setSafeOnClickListener { + clicked( + bindingAdapterPosition, + resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) + ) + } + binding.itemCompactTitleContainer.setSafeOnClickListener { + clicked( + bindingAdapterPosition, + resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) + ) + } itemView.setOnTouchListener { _, _ -> true } binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) } } } - fun clicked(position: Int) { + fun clicked(position: Int, bitmap: Bitmap? = null) { if ((mediaList?.size ?: 0) > position && position != -1) { val media = mediaList?.get(position) + if (bitmap != null) MediaSingleton.bitmap = bitmap ContextCompat.startActivity( activity, Intent(activity, MediaDetailsActivity::class.java).putExtra( @@ -296,10 +381,53 @@ class MediaAdaptor( if ((mediaList?.size ?: 0) > position && position != -1) { val media = mediaList?.get(position) ?: return false if (activity.supportFragmentManager.findFragmentByTag("list") == null) { - MediaListDialogSmallFragment.newInstance(media).show(activity.supportFragmentManager, "list") + MediaListDialogSmallFragment.newInstance(media) + .show(activity.supportFragmentManager, "list") return true } } return false } + + fun getBitmapFromImageView(imageView: ImageView): Bitmap? { + val drawable = imageView.drawable ?: return null + + // If the drawable is a BitmapDrawable, then just get the bitmap + if (drawable is BitmapDrawable) { + return drawable.bitmap + } + + // Create a bitmap with the same dimensions as the drawable + val bitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + + // Draw the drawable onto the bitmap + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + + return bitmap + } + + fun resizeBitmap(source: Bitmap?, maxDimension: Int): Bitmap? { + if (source == null) return null + val width = source.width + val height = source.height + val newWidth: Int + val newHeight: Int + + if (width > height) { + newWidth = maxDimension + newHeight = (height * (maxDimension.toFloat() / width)).toInt() + } else { + newHeight = maxDimension + newWidth = (width * (maxDimension.toFloat() / height)).toInt() + } + + return Bitmap.createScaledBitmap(source, newWidth, newHeight, true) + } + } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt index 874e0af5..a902a1cf 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt @@ -31,24 +31,24 @@ import ani.dantotsu.R import ani.dantotsu.Refresh import ani.dantotsu.ZoomOutPageTransformer import ani.dantotsu.connections.anilist.Anilist -import ani.dantotsu.media.anime.AnimeWatchFragment import ani.dantotsu.copyToClipboard import ani.dantotsu.databinding.ActivityMediaBinding import ani.dantotsu.initActivity import ani.dantotsu.loadData import ani.dantotsu.loadImage +import ani.dantotsu.media.anime.AnimeWatchFragment import ani.dantotsu.media.manga.MangaReadFragment -import ani.dantotsu.navBarHeight import ani.dantotsu.media.novel.NovelReadFragment +import ani.dantotsu.navBarHeight import ani.dantotsu.openLinkInBrowser import ani.dantotsu.others.ImageViewDialog +import ani.dantotsu.others.LangSet import ani.dantotsu.others.getSerialized import ani.dantotsu.saveData import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.snackString import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet import com.flaviofaria.kenburnsview.RandomTransitionGenerator import com.google.android.material.appbar.AppBarLayout import com.google.android.material.navigation.NavigationBarView @@ -72,9 +72,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi @SuppressLint("SetTextI18n", "ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + var media: Media = intent.getSerialized("media") ?: return + ThemeManager(this).applyTheme(MediaSingleton.bitmap) + MediaSingleton.bitmap = null + super.onCreate(savedInstanceState) binding = ActivityMediaBinding.inflate(layoutInflater) setContentView(binding.root) screenWidth = resources.displayMetrics.widthPixels.toFloat() @@ -119,7 +121,7 @@ ThemeManager(this).applyTheme() viewPager.isUserInputEnabled = false viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings)) - var media: Media = intent.getSerialized("media") ?: return + val isDownload = intent.getBooleanExtra("download", false) media.selected = model.loadSelected(media, isDownload) diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt index 5235e6b5..35013e02 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt @@ -1,28 +1,29 @@ package ani.dantotsu.media import android.app.Activity -import android.content.Context import android.content.SharedPreferences -import android.os.Environment import android.os.Handler import android.os.Looper import androidx.fragment.app.FragmentManager import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import ani.dantotsu.FileUrl +import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist -import ani.dantotsu.media.anime.Episode -import ani.dantotsu.media.anime.SelectorDialogFragment +import ani.dantotsu.currContext import ani.dantotsu.loadData import ani.dantotsu.logger +import ani.dantotsu.media.anime.Episode +import ani.dantotsu.media.anime.SelectorDialogFragment import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.others.AniSkip import ani.dantotsu.others.Jikan import ani.dantotsu.others.Kitsu +import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.Book import ani.dantotsu.parsers.MangaImage import ani.dantotsu.parsers.MangaReadSources +import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.NovelSources import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.VideoExtractor @@ -30,25 +31,12 @@ import ani.dantotsu.parsers.WatchSources import ani.dantotsu.saveData import ani.dantotsu.snackString import ani.dantotsu.tryWithSuspend -import ani.dantotsu.currContext -import ani.dantotsu.R -import ani.dantotsu.download.Download -import ani.dantotsu.download.DownloadsManager -import ani.dantotsu.parsers.AnimeSources -import ani.dantotsu.parsers.AniyomiAdapter -import ani.dantotsu.parsers.DynamicMangaParser -import ani.dantotsu.parsers.HAnimeSources -import ani.dantotsu.parsers.HMangaSources -import ani.dantotsu.parsers.MangaSources import com.bumptech.glide.load.resource.bitmap.BitmapTransformation -import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File class MediaDetailsViewModel : ViewModel() { val scrolledToTop = MutableLiveData(true) @@ -62,17 +50,20 @@ class MediaDetailsViewModel : ViewModel() { val sharedPreferences = Injekt.get() val data = loadData("${media.id}-select") ?: Selected().let { it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) { - true ->sharedPreferences.getInt("settings_def_anime_source_s_r", 0) - else ->sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0) + true -> sharedPreferences.getInt("settings_def_anime_source_s_r", 0) + else -> sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0) } it.preferDub = loadData("settings_prefer_dub") ?: false saveSelected(media.id, it) it } if (isDownload) { - data.sourceIndex = when (media.anime != null) { - true -> AnimeSources.list.size - 1 - else -> MangaSources.list.size - 1 + data.sourceIndex = if (media.anime != null) { + AnimeSources.list.size - 1 + } else if (media.format == "MANGA" || media.format == "ONE_SHOT") { + MangaSources.list.size - 1 + } else { + NovelSources.list.size - 1 } } return data @@ -81,7 +72,9 @@ class MediaDetailsViewModel : ViewModel() { fun loadSelectedStringLocation(sourceName: String): Int { //find the location of the source in the list var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0 - if (location == -1) {location = 0} + if (location == -1) { + location = 0 + } return location } @@ -106,7 +99,9 @@ class MediaDetailsViewModel : ViewModel() { //Anime - private val kitsuEpisodes: MutableLiveData> = MutableLiveData>(null) + private val kitsuEpisodes: MutableLiveData> = + MutableLiveData>(null) + fun getKitsuEpisodes(): LiveData> = kitsuEpisodes suspend fun loadKitsuEpisodes(s: Media) { tryWithSuspend { @@ -114,7 +109,9 @@ class MediaDetailsViewModel : ViewModel() { } } - private val fillerEpisodes: MutableLiveData> = MutableLiveData>(null) + private val fillerEpisodes: MutableLiveData> = + MutableLiveData>(null) + fun getFillerEpisodes(): LiveData> = fillerEpisodes suspend fun loadFillerEpisodes(s: Media) { tryWithSuspend { @@ -145,7 +142,8 @@ class MediaDetailsViewModel : ViewModel() { suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) { watchSources?.saveResponse(i, id, source) - epsLoaded[i] = watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return + epsLoaded[i] = + watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return episodes.postValue(epsLoaded) } @@ -184,7 +182,12 @@ class MediaDetailsViewModel : ViewModel() { val timeStamps = MutableLiveData?>() private val timeStampsMap: MutableMap?> = mutableMapOf() - suspend fun loadTimeStamps(malId: Int?, episodeNum: Int?, duration: Long, useProxyForTimeStamps: Boolean) { + suspend fun loadTimeStamps( + malId: Int?, + episodeNum: Int?, + duration: Long, + useProxyForTimeStamps: Boolean + ) { malId ?: return episodeNum ?: return if (timeStampsMap.containsKey(episodeNum)) @@ -194,7 +197,11 @@ class MediaDetailsViewModel : ViewModel() { timeStamps.postValue(result) } - suspend fun loadEpisodeSingleVideo(ep: Episode, selected: Selected, post: Boolean = true): Boolean { + suspend fun loadEpisodeSingleVideo( + ep: Episode, + selected: Selected, + post: Boolean = true + ): Boolean { if (ep.extractors.isNullOrEmpty()) { val server = selected.server ?: return false @@ -204,8 +211,10 @@ class MediaDetailsViewModel : ViewModel() { selected.sourceIndex = selected.sourceIndex if (!post && !it.allowsPreloading) null else ep.sEpisode?.let { it1 -> - it.loadSingleVideoServer(server, link, ep.extra, - it1, post) + it.loadSingleVideoServer( + server, link, ep.extra, + it1, post + ) } } ?: return false) ep.allStreams = false @@ -228,7 +237,13 @@ class MediaDetailsViewModel : ViewModel() { } val epChanged = MutableLiveData(true) - fun onEpisodeClick(media: Media, i: String, manager: FragmentManager, launch: Boolean = true, prevEp: String? = null) { + fun onEpisodeClick( + media: Media, + i: String, + manager: FragmentManager, + launch: Boolean = true, + prevEp: String? = null + ) { Handler(Looper.getMainLooper()).post { if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) { if (media.anime?.episodes?.get(i) != null) { @@ -238,7 +253,8 @@ class MediaDetailsViewModel : ViewModel() { return@post } media.selected = this.loadSelected(media) - val selector = SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp) + val selector = + SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp) selector.show(manager, "dialog") } } @@ -248,13 +264,17 @@ class MediaDetailsViewModel : ViewModel() { //Manga var mangaReadSources: MangaReadSources? = null - private val mangaChapters = MutableLiveData>>(null) + private val mangaChapters = + MutableLiveData>>(null) private val mangaLoaded = mutableMapOf>() - fun getMangaChapters(): LiveData>> = mangaChapters + fun getMangaChapters(): LiveData>> = + mangaChapters + suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) { logger("Loading Manga Chapters : $mangaLoaded") if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend { - mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend + mangaLoaded[i] = + mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend } mangaChapters.postValue(mangaLoaded) } @@ -269,11 +289,17 @@ class MediaDetailsViewModel : ViewModel() { private val mangaChapter = MutableLiveData(null) fun getMangaChapter(): LiveData = mangaChapter - suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, series: String, post: Boolean = true): Boolean { - + suspend fun loadMangaChapterImages( + chapter: MangaChapter, + selected: Selected, + series: String, + post: Boolean = true + ): Boolean { + return tryWithSuspend(true) { chapter.addImages( - mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false + mangaReadSources?.get(selected.sourceIndex) + ?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false ) if (post) mangaChapter.postValue(chapter) true @@ -281,7 +307,8 @@ class MediaDetailsViewModel : ViewModel() { } fun loadTransformation(mangaImage: MangaImage, source: Int): BitmapTransformation? { - return if (mangaImage.useTransformation) mangaReadSources?.get(source)?.getTransformation() else null + return if (mangaImage.useTransformation) mangaReadSources?.get(source) + ?.getTransformation() else null } val novelSources = NovelSources @@ -296,7 +323,7 @@ class MediaDetailsViewModel : ViewModel() { } suspend fun autoSearchNovels(media: Media) { - val source = novelSources[media.selected?.sourceIndex?:0] + val source = novelSources[media.selected?.sourceIndex ?: 0] tryWithSuspend(post = true) { if (source != null) { novelResponses.postValue(source.sortedSearch(media)) @@ -307,7 +334,9 @@ class MediaDetailsViewModel : ViewModel() { val book: MutableLiveData = MutableLiveData(null) suspend fun loadBook(novel: ShowResponse, i: Int) { tryWithSuspend { - book.postValue(novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend) + book.postValue( + novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend + ) } } diff --git a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt index 75d452b3..a7b78f8e 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt @@ -43,7 +43,11 @@ class MediaInfoFragment : Fragment() { private var type = "ANIME" private val genreModel: GenresViewModel by activityViewModels() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = FragmentMediaInfoBinding.inflate(inflater, container, false) return binding.root } @@ -59,8 +63,8 @@ class MediaInfoFragment : Fragment() { binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE binding.mediaInfoContainer.updateLayoutParams { bottomMargin += 128f.px + navBarHeight } - model.scrolledToTop.observe(viewLifecycleOwner){ - if(it) binding.mediaInfoScroll.scrollTo(0,0) + model.scrolledToTop.observe(viewLifecycleOwner) { + if (it) binding.mediaInfoScroll.scrollTo(0, 0) } model.getMedia().observe(viewLifecycleOwner) { media -> @@ -68,30 +72,32 @@ class MediaInfoFragment : Fragment() { loaded = true binding.mediaInfoProgressBar.visibility = View.GONE binding.mediaInfoContainer.visibility = View.VISIBLE - binding.mediaInfoName.text = "\t\t\t" + (media.name?:media.nameRomaji) + binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji) binding.mediaInfoName.setOnLongClickListener { - copyToClipboard(media.name?:media.nameRomaji) + copyToClipboard(media.name ?: media.nameRomaji) true } - if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility = View.VISIBLE + if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility = + View.VISIBLE binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji binding.mediaInfoNameRomaji.setOnLongClickListener { copyToClipboard(media.nameRomaji) true } - binding.mediaInfoMeanScore.text = if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??" + binding.mediaInfoMeanScore.text = + if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??" binding.mediaInfoStatus.text = media.status binding.mediaInfoFormat.text = media.format binding.mediaInfoSource.text = media.source binding.mediaInfoStart.text = media.startDate?.toString() ?: "??" - binding.mediaInfoEnd.text =media.endDate?.toString() ?: "??" + binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??" if (media.anime != null) { binding.mediaInfoDuration.text = if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??" binding.mediaInfoDurationContainer.visibility = View.VISIBLE binding.mediaInfoSeasonContainer.visibility = View.VISIBLE binding.mediaInfoSeason.text = - (media.anime.season ?: "??")+ " " + (media.anime.seasonYear ?: "??") + (media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??") if (media.anime.mainStudio != null) { binding.mediaInfoStudioContainer.visibility = View.VISIBLE binding.mediaInfoStudio.text = media.anime.mainStudio!!.name @@ -246,7 +252,12 @@ class MediaInfoFragment : Fragment() { val end = a.indexOf('"', first).let { if (it != -1) it else return a } val name = a.subSequence(first, end).toString() return "${a.subSequence(0, first)}" + - "[$name](https://www.youtube.com/results?search_query=${URLEncoder.encode(name, "utf-8")})" + + "[$name](https://www.youtube.com/results?search_query=${ + URLEncoder.encode( + name, + "utf-8" + ) + })" + "${a.subSequence(end, a.length)}" } @@ -270,7 +281,11 @@ class MediaInfoFragment : Fragment() { } if (media.anime.op.isNotEmpty()) { - val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false) + val bind = ItemTitleTextBinding.inflate( + LayoutInflater.from(context), + parent, + false + ) bind.itemTitle.setText(R.string.opening) makeText(bind.itemText, media.anime.op) parent.addView(bind.root) @@ -278,7 +293,11 @@ class MediaInfoFragment : Fragment() { if (media.anime.ed.isNotEmpty()) { - val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false) + val bind = ItemTitleTextBinding.inflate( + LayoutInflater.from(context), + parent, + false + ) bind.itemTitle.setText(R.string.ending) makeText(bind.itemText, media.anime.ed) parent.addView(bind.root) @@ -458,7 +477,8 @@ class MediaInfoFragment : Fragment() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val cornerTop = ObjectAnimator.ofFloat(binding.root, "radius", 0f, 32f).setDuration(200) - val cornerNotTop = ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200) + val cornerNotTop = + ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200) var cornered = true cornerTop.start() binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ -> diff --git a/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt index da7d08fd..b1576e40 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaListDialogFragment.kt @@ -14,8 +14,8 @@ import androidx.lifecycle.lifecycleScope import ani.dantotsu.* import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.api.FuzzyDate -import ani.dantotsu.databinding.BottomSheetMediaListBinding import ani.dantotsu.connections.mal.MAL +import ani.dantotsu.databinding.BottomSheetMediaListBinding import com.google.android.material.switchmaterial.SwitchMaterial import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -27,7 +27,11 @@ class MediaListDialogFragment : BottomSheetDialogFragment() { private var _binding: BottomSheetMediaListBinding? = null private val binding get() = _binding!! - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetMediaListBinding.inflate(inflater, container, false) return binding.root } @@ -46,9 +50,13 @@ class MediaListDialogFragment : BottomSheetDialogFragment() { binding.mediaListLayout.visibility = View.VISIBLE val statuses: Array = resources.getStringArray(R.array.status) - val statusStrings = if (media?.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga) - val userStatus = if(media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0] - + val statusStrings = + if (media?.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray( + R.array.status_manga + ) + val userStatus = + if (media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0] + binding.mediaListStatus.setText(userStatus) binding.mediaListStatus.setAdapter( ArrayAdapter( @@ -160,7 +168,9 @@ class MediaListDialogFragment : BottomSheetDialogFragment() { val init = if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString() .toInt() else 0 - if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString()) + if (init < (total + ?: 5000) + ) binding.mediaListProgress.setText((init + 1).toString()) if (init + 1 == (total ?: 5000)) { binding.mediaListStatus.setText(statusStrings[2], false) onComplete() @@ -201,11 +211,15 @@ class MediaListDialogFragment : BottomSheetDialogFragment() { scope.launch { withContext(Dispatchers.IO) { if (media != null) { - val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull() + val progress = + _binding?.mediaListProgress?.text.toString().toIntOrNull() val score = - (_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt() - val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())] - val rewatch = _binding?.mediaListRewatch?.text?.toString()?.toIntOrNull() + (_binding?.mediaListScore?.text.toString().toDoubleOrNull() + ?.times(10))?.toInt() + val status = + statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())] + val rewatch = + _binding?.mediaListRewatch?.text?.toString()?.toIntOrNull() val notes = _binding?.mediaListNotes?.text?.toString() val startD = start.date val endD = end.date @@ -245,7 +259,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() { scope.launch { withContext(Dispatchers.IO) { Anilist.mutation.deleteList(id) - MAL.query.deleteList(media?.anime!=null,media?.idMAL) + MAL.query.deleteList(media?.anime != null, media?.idMAL) } Refresh.all() snackString(getString(R.string.deleted_from_list)) diff --git a/app/src/main/java/ani/dantotsu/media/MediaListDialogSmallFragment.kt b/app/src/main/java/ani/dantotsu/media/MediaListDialogSmallFragment.kt index 55692759..cb7390ce 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaListDialogSmallFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaListDialogSmallFragment.kt @@ -12,11 +12,9 @@ import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import ani.dantotsu.* import ani.dantotsu.connections.anilist.Anilist -import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding import ani.dantotsu.connections.mal.MAL +import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding import ani.dantotsu.others.getSerialized -import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -46,7 +44,11 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() { private var _binding: BottomSheetMediaListSmallBinding? = null private val binding get() = _binding!! - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetMediaListSmallBinding.inflate(inflater, container, false) return binding.root } @@ -60,8 +62,12 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() { binding.mediaListProgressBar.visibility = View.GONE binding.mediaListLayout.visibility = View.VISIBLE val statuses: Array = resources.getStringArray(R.array.status) - val statusStrings = if (media.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga) - val userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0] + val statusStrings = + if (media.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray( + R.array.status_manga + ) + val userStatus = + if (media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0] binding.mediaListStatus.setText(userStatus) binding.mediaListStatus.setAdapter( @@ -130,10 +136,26 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() { withContext(Dispatchers.IO) { withContext(Dispatchers.IO) { val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull() - val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt() - val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())] - Anilist.mutation.editList(media.id, progress, score, null, null, status, media.isListPrivate) - MAL.query.editList(media.idMAL, media.anime != null, progress, score, status) + val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull() + ?.times(10))?.toInt() + val status = + statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())] + Anilist.mutation.editList( + media.id, + progress, + score, + null, + null, + status, + media.isListPrivate + ) + MAL.query.editList( + media.idMAL, + media.anime != null, + progress, + score, + status + ) } } Refresh.all() diff --git a/app/src/main/java/ani/dantotsu/media/OtherDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/OtherDetailsViewModel.kt index 61c1c5c6..677d700f 100644 --- a/app/src/main/java/ani/dantotsu/media/OtherDetailsViewModel.kt +++ b/app/src/main/java/ani/dantotsu/media/OtherDetailsViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import ani.dantotsu.connections.anilist.Anilist import java.text.DateFormat -import java.util.* +import java.util.Date class OtherDetailsViewModel : ViewModel() { private val character: MutableLiveData = MutableLiveData(null) @@ -19,26 +19,28 @@ class OtherDetailsViewModel : ViewModel() { suspend fun loadStudio(m: Studio) { if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m)) } + private val author: MutableLiveData = MutableLiveData(null) fun getAuthor(): LiveData = author suspend fun loadAuthor(m: Author) { if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m)) } - private val calendar: MutableLiveData>> = MutableLiveData(null) - fun getCalendar(): LiveData>> = calendar + + private val calendar: MutableLiveData>> = MutableLiveData(null) + fun getCalendar(): LiveData>> = calendar suspend fun loadCalendar() { - val curr = System.currentTimeMillis()/1000 - val res = Anilist.query.recentlyUpdated(false,curr-86400,curr+(86400*6)) + val curr = System.currentTimeMillis() / 1000 + val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6)) val df = DateFormat.getDateInstance(DateFormat.FULL) - val map = mutableMapOf>() - val idMap = mutableMapOf>() + val map = mutableMapOf>() + val idMap = mutableMapOf>() res?.forEach { - val v = it.relation?.split(",")?.map { i-> i.toLong() }!! - val dateInfo = df.format(Date(v[1]*1000)) + val v = it.relation?.split(",")?.map { i -> i.toLong() }!! + val dateInfo = df.format(Date(v[1] * 1000)) val list = map.getOrPut(dateInfo) { mutableListOf() } val idList = idMap.getOrPut(dateInfo) { mutableListOf() } it.relation = "Episode ${v[0]}" - if(!idList.contains(it.id)) { + if (!idList.contains(it.id)) { idList.add(it.id) list.add(it) } diff --git a/app/src/main/java/ani/dantotsu/media/ProgressAdapter.kt b/app/src/main/java/ani/dantotsu/media/ProgressAdapter.kt index 1558da64..081d47fc 100644 --- a/app/src/main/java/ani/dantotsu/media/ProgressAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/ProgressAdapter.kt @@ -22,7 +22,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean) var bar: ProgressBar? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder { - val binding = ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ProgressViewHolder(binding) } @@ -33,7 +34,12 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean) val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() { override fun onDoubleClick(event: MotionEvent) { snackString(currContext()?.getString(R.string.cant_wait)) - ObjectAnimator.ofFloat(progressBar, "translationX", progressBar.translationX, progressBar.translationX + 100f) + ObjectAnimator.ofFloat( + progressBar, + "translationX", + progressBar.translationX, + progressBar.translationX + 100f + ) .setDuration(300).start() } @@ -51,7 +57,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean) } override fun getItemCount(): Int = 1 - inner class ProgressViewHolder(val binding: ItemProgressbarBinding) : RecyclerView.ViewHolder(binding.root) { + inner class ProgressViewHolder(val binding: ItemProgressbarBinding) : + RecyclerView.ViewHolder(binding.root) { init { itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 } } diff --git a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt index 1394f14a..b3cf36f5 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt @@ -16,8 +16,8 @@ import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.AnilistSearch import ani.dantotsu.connections.anilist.SearchResults import ani.dantotsu.databinding.ActivitySearchBinding -import ani.dantotsu.themes.ThemeManager import ani.dantotsu.others.LangSet +import ani.dantotsu.themes.ThemeManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.* @@ -35,12 +35,12 @@ class SearchActivity : AppCompatActivity() { private lateinit var concatAdapter: ConcatAdapter lateinit var result: SearchResults - lateinit var updateChips: (()->Unit) + lateinit var updateChips: (() -> Unit) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivitySearchBinding.inflate(layoutInflater) setContentView(binding.root) initActivity(this) @@ -83,10 +83,10 @@ ThemeManager(this).applyTheme() gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return when (position) { - 0 -> gridSize + 0 -> gridSize concatAdapter.itemCount - 1 -> gridSize - else -> when (style) { - 0 -> 1 + else -> when (style) { + 0 -> 1 else -> gridSize } } @@ -149,7 +149,7 @@ ThemeManager(this).applyTheme() } else headerAdaptor.requestFocus?.run() - if(intent.getBooleanExtra("search",false)) search() + if (intent.getBooleanExtra("search", false)) search() } } } diff --git a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt index cd47c907..67c523e6 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt @@ -20,14 +20,16 @@ import ani.dantotsu.saveData import com.google.android.material.checkbox.MaterialCheckBox.* -class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter() { +class SearchAdapter(private val activity: SearchActivity) : + RecyclerView.Adapter() { private val itemViewType = 6969 var search: Runnable? = null var requestFocus: Runnable? = null private var textWatcher: TextWatcher? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder { - val binding = ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) return SearchHeaderViewHolder(binding) } @@ -36,13 +38,15 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter val binding = holder.binding - val imm: InputMethodManager = activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager + val imm: InputMethodManager = + activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager when (activity.style) { 0 -> { binding.searchResultGrid.alpha = 1f binding.searchResultList.alpha = 0.33f } + 1 -> { binding.searchResultList.alpha = 1f binding.searchResultGrid.alpha = 0.33f @@ -62,7 +66,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also { activity.updateChips = { it.update() } } - binding.searchChipRecycler.layoutManager = LinearLayoutManager(binding.root.context, HORIZONTAL, false) + binding.searchChipRecycler.layoutManager = + LinearLayoutManager(binding.root.context, HORIZONTAL, false) binding.searchFilter.setOnClickListener { SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog") @@ -70,7 +75,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter fun searchTitle() { activity.result.apply { - search = if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null + search = + if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null onList = listOnly isAdult = adult } @@ -96,7 +102,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0) true } - else -> false + + else -> false } } binding.searchBar.setEndIconOnClickListener { searchTitle() } @@ -127,7 +134,7 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter binding.searchList.apply { if (Anilist.userid != null) { visibility = View.VISIBLE - checkedState = when(listOnly){ + checkedState = when (listOnly) { null -> STATE_UNCHECKED true -> STATE_CHECKED false -> STATE_INDETERMINATE @@ -135,10 +142,10 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter addOnCheckedStateChangedListener { _, state -> listOnly = when (state) { - STATE_CHECKED -> true + STATE_CHECKED -> true STATE_INDETERMINATE -> false - STATE_UNCHECKED -> null - else -> null + STATE_UNCHECKED -> null + else -> null } } @@ -158,20 +165,24 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter override fun getItemCount(): Int = 1 - inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : RecyclerView.ViewHolder(binding.root) + inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : + RecyclerView.ViewHolder(binding.root) override fun getItemViewType(position: Int): Int { return itemViewType } - class SearchChipAdapter(val activity: SearchActivity) : RecyclerView.Adapter() { + class SearchChipAdapter(val activity: SearchActivity) : + RecyclerView.Adapter() { private var chips = activity.result.toChipList() - inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root) + inner class SearchChipViewHolder(val binding: ItemChipBinding) : + RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder { - val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false) return SearchChipViewHolder(binding) } diff --git a/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt b/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt index 3fc2f05c..79fd8712 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt @@ -18,13 +18,17 @@ import ani.dantotsu.databinding.BottomSheetSearchFilterBinding import ani.dantotsu.databinding.ItemChipBinding import com.google.android.material.chip.Chip -class SearchFilterBottomDialog() : BottomSheetDialogFragment() { +class SearchFilterBottomDialog : BottomSheetDialogFragment() { private var _binding: BottomSheetSearchFilterBinding? = null private val binding get() = _binding!! private lateinit var activity: SearchActivity - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetSearchFilterBinding.inflate(inflater, container, false) return binding.root } @@ -99,7 +103,7 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() { ArrayAdapter( binding.root.context, R.layout.item_dropdown, - (1970 until 2024).map { it.toString() }.reversed().toTypedArray() + (1970 until 2025).map { it.toString() }.reversed().toTypedArray() ) ) } @@ -129,24 +133,25 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() { } binding.searchGenresGrid.isChecked = false - binding.searchFilterTags.adapter = FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip -> - val tag = chip.text.toString() - chip.isChecked = selectedTags.contains(tag) - chip.isCloseIconVisible = exTags.contains(tag) - chip.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - chip.isCloseIconVisible = false - exTags.remove(tag) - selectedTags.add(tag) - } else - selectedTags.remove(tag) + binding.searchFilterTags.adapter = + FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip -> + val tag = chip.text.toString() + chip.isChecked = selectedTags.contains(tag) + chip.isCloseIconVisible = exTags.contains(tag) + chip.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + chip.isCloseIconVisible = false + exTags.remove(tag) + selectedTags.add(tag) + } else + selectedTags.remove(tag) + } + chip.setOnLongClickListener { + chip.isChecked = false + chip.isCloseIconVisible = true + exTags.add(tag) + } } - chip.setOnLongClickListener { - chip.isChecked = false - chip.isCloseIconVisible = true - exTags.add(tag) - } - } binding.searchTagsGrid.setOnCheckedChangeListener { _, isChecked -> binding.searchFilterTags.layoutManager = if (!isChecked) LinearLayoutManager(binding.root.context, HORIZONTAL, false) @@ -158,10 +163,12 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() { class FilterChipAdapter(val list: List, private val perform: ((Chip) -> Unit)) : RecyclerView.Adapter() { - inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root) + inner class SearchChipViewHolder(val binding: ItemChipBinding) : + RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder { - val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false) return SearchChipViewHolder(binding) } diff --git a/app/src/main/java/ani/dantotsu/media/SourceAdapter.kt b/app/src/main/java/ani/dantotsu/media/SourceAdapter.kt index 0b487ff0..c1dd18b2 100644 --- a/app/src/main/java/ani/dantotsu/media/SourceAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/SourceAdapter.kt @@ -17,7 +17,8 @@ abstract class SourceAdapter( private val scope: CoroutineScope ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SourceViewHolder { - val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false) return SourceViewHolder(binding) } @@ -34,7 +35,8 @@ abstract class SourceAdapter( abstract suspend fun onItemClick(source: ShowResponse) - inner class SourceViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) { + inner class SourceViewHolder(val binding: ItemCharacterBinding) : + RecyclerView.ViewHolder(binding.root) { init { itemView.setOnClickListener { dialogFragment.dismiss() diff --git a/app/src/main/java/ani/dantotsu/media/SourceSearchDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/SourceSearchDialogFragment.kt index 46d8454a..3925bcce 100644 --- a/app/src/main/java/ani/dantotsu/media/SourceSearchDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/SourceSearchDialogFragment.kt @@ -13,8 +13,8 @@ import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import ani.dantotsu.BottomSheetDialogFragment -import ani.dantotsu.media.anime.AnimeSourceAdapter import ani.dantotsu.databinding.BottomSheetSourceSearchBinding +import ani.dantotsu.media.anime.AnimeSourceAdapter import ani.dantotsu.media.manga.MangaSourceAdapter import ani.dantotsu.navBarHeight import ani.dantotsu.parsers.AnimeSources @@ -38,7 +38,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() { var id: Int? = null var media: Media? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetSourceSearchBinding.inflate(inflater, container, false) return binding.root } @@ -47,7 +51,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() { binding.mediaListContainer.updateLayoutParams { bottomMargin += navBarHeight } val scope = requireActivity().lifecycleScope - val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + val imm = + requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager model.getMedia().observe(viewLifecycleOwner) { media = it if (media != null) { @@ -65,6 +70,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() { anime = false (if (media!!.isAdult) HMangaSources else MangaSources)[i!!] } + fun search() { binding.searchBarText.clearFocus() imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0) @@ -86,7 +92,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() { search() true } - else -> false + + else -> false } } binding.searchBar.setEndIconOnClickListener { search() } @@ -101,7 +108,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() { else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope) binding.searchRecyclerView.layoutManager = GridLayoutManager( requireActivity(), - clamp(requireActivity().resources.displayMetrics.widthPixels / 124f.px, 1, 4) + clamp( + requireActivity().resources.displayMetrics.widthPixels / 124f.px, + 1, + 4 + ) ) } } diff --git a/app/src/main/java/ani/dantotsu/media/StudioActivity.kt b/app/src/main/java/ani/dantotsu/media/StudioActivity.kt index 6839e117..53e04a37 100644 --- a/app/src/main/java/ani/dantotsu/media/StudioActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/StudioActivity.kt @@ -12,11 +12,17 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager -import ani.dantotsu.* +import ani.dantotsu.EmptyAdapter +import ani.dantotsu.R +import ani.dantotsu.Refresh import ani.dantotsu.databinding.ActivityStudioBinding -import ani.dantotsu.others.getSerialized -import ani.dantotsu.themes.ThemeManager +import ani.dantotsu.initActivity +import ani.dantotsu.navBarHeight import ani.dantotsu.others.LangSet +import ani.dantotsu.others.getSerialized +import ani.dantotsu.px +import ani.dantotsu.statusBarHeight +import ani.dantotsu.themes.ThemeManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -31,7 +37,7 @@ class StudioActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityStudioBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt index 25059937..17759141 100644 --- a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt +++ b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt @@ -1,18 +1,13 @@ package ani.dantotsu.media import android.content.Context -import android.os.Environment import ani.dantotsu.parsers.SubtitleType import eu.kanade.tachiyomi.network.NetworkHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.Request import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.FileInputStream class SubtitleDownloader { @@ -30,7 +25,7 @@ class SubtitleDownloader { // Check if response is successful if (response.isSuccessful) { - val responseBody = response.body?.string() + val responseBody = response.body.string() val subtitleType = when { diff --git a/app/src/main/java/ani/dantotsu/media/TitleAdapter.kt b/app/src/main/java/ani/dantotsu/media/TitleAdapter.kt index 45e8b1fa..03416ec5 100644 --- a/app/src/main/java/ani/dantotsu/media/TitleAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/TitleAdapter.kt @@ -5,8 +5,10 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.databinding.ItemTitleBinding -class TitleAdapter(private val text: String) : RecyclerView.Adapter() { - inner class TitleViewHolder(val binding: ItemTitleBinding) : RecyclerView.ViewHolder(binding.root) +class TitleAdapter(private val text: String) : + RecyclerView.Adapter() { + inner class TitleViewHolder(val binding: ItemTitleBinding) : + RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder { val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false) diff --git a/app/src/main/java/ani/dantotsu/media/anime/Anime.kt b/app/src/main/java/ani/dantotsu/media/anime/Anime.kt index 9a014c90..bee71d03 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/Anime.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/Anime.kt @@ -15,7 +15,7 @@ data class Anime( var ed: ArrayList = arrayListOf(), var mainStudio: Studio? = null, - var author: Author?=null, + var author: Author? = null, var youtube: String? = null, var nextAiringEpisode: Int? = null, diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt new file mode 100644 index 00000000..17d20bc6 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeNameAdapter.kt @@ -0,0 +1,20 @@ +package ani.dantotsu.media.anime + +import java.util.regex.Matcher +import java.util.regex.Pattern + +class AnimeNameAdapter { + companion object { + fun findSeasonNumber(text: String): Int? { + val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)" + val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE) + val seasonMatcher: Matcher = seasonPattern.matcher(text) + + return if (seasonMatcher.find()) { + seasonMatcher.group(2)?.toInt() + } else { + null + } + } + } +} \ No newline at end of file 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 a4573ff0..e01548da 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchAdapter.kt @@ -1,23 +1,17 @@ package ani.dantotsu.media.anime import android.annotation.SuppressLint -import android.app.AlertDialog -import android.content.Context import android.content.Intent import android.net.Uri -import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter -import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout -import android.widget.Toast import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.* import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemChipBinding @@ -27,21 +21,11 @@ import ani.dantotsu.media.SourceSearchDialogFragment import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.DynamicAnimeParser import ani.dantotsu.parsers.WatchSources -import ani.dantotsu.settings.ExtensionsActivity -import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import com.google.android.material.chip.Chip -import com.google.android.material.tabs.TabLayout -import com.google.android.material.textfield.TextInputLayout -import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource -import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager -import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.lang.IndexOutOfBoundsException class AnimeWatchAdapter( private val media: Media, @@ -72,23 +56,33 @@ class AnimeWatchAdapter( } binding.animeSourceDubbed.isChecked = media.selected!!.preferDub - binding.animeSourceDubbedText.text = if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed) + binding.animeSourceDubbedText.text = + if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString( + R.string.subbed + ) //PreferDub var changing = false binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked -> - binding.animeSourceDubbedText.text = if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed) + binding.animeSourceDubbedText.text = + if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString( + R.string.subbed + ) if (!changing) fragment.onDubClicked(isChecked) } //Wrong Title binding.animeSourceSearch.setOnClickListener { - SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null) + SourceSearchDialogFragment().show( + fragment.requireActivity().supportFragmentManager, + null + ) } //Source Selection - var source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it } - setLanguageList(media.selected!!.langIndex,source) + var source = + media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it } + setLanguageList(media.selected!!.langIndex, source) if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) { binding.animeSource.setText(watchSources.names[source]) watchSources[source].apply { @@ -100,7 +94,13 @@ class AnimeWatchAdapter( } } - binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, watchSources.names)) + binding.animeSource.setAdapter( + ArrayAdapter( + fragment.requireContext(), + R.layout.item_dropdown, + watchSources.names + ) + ) binding.animeSourceTitle.isSelected = true binding.animeSource.setOnItemClickListener { _, _, i, _ -> fragment.onSourceChange(i).apply { @@ -109,9 +109,10 @@ class AnimeWatchAdapter( changing = true binding.animeSourceDubbed.isChecked = selectDub changing = false - binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE + binding.animeSourceDubbedCont.visibility = + if (isDubAvailableSeparately) View.VISIBLE else View.GONE source = i - setLanguageList(0,i) + setLanguageList(0, i) } subscribeButton(false) fragment.loadEpisodes(i, false) @@ -124,11 +125,13 @@ class AnimeWatchAdapter( fragment.onLangChange(i) fragment.onSourceChange(media.selected!!.sourceIndex).apply { binding.animeSourceTitle.text = showUserText - showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } + showUserTextListener = + { MainScope().launch { binding.animeSourceTitle.text = it } } changing = true binding.animeSourceDubbed.isChecked = selectDub changing = false - binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE + binding.animeSourceDubbedCont.visibility = + if (isDubAvailableSeparately) View.VISIBLE else View.GONE setLanguageList(i, source) } subscribeButton(false) @@ -146,7 +149,7 @@ class AnimeWatchAdapter( //Subscription - subscribe = MediaDetailsActivity.PopImageButton( + subscribe = MediaDetailsActivity.PopImageButton( fragment.lifecycleScope, binding.animeSourceSubscribe, R.drawable.ic_round_notifications_active_24, @@ -161,7 +164,7 @@ class AnimeWatchAdapter( subscribeButton(false) binding.animeSourceSubscribe.setOnLongClickListener { - openSettings(fragment.requireContext(),getChannelId(true,media.id)) + openSettings(fragment.requireContext(), getChannelId(true, media.id)) } //Icons @@ -200,12 +203,12 @@ class AnimeWatchAdapter( style = 2 fragment.onIconPressed(style, reversed) } - binding.animeScanlatorTop.visibility= View.GONE + binding.animeScanlatorTop.visibility = View.GONE //Episode Handling handleEpisodes() } - fun subscribeButton(enabled : Boolean) { + fun subscribeButton(enabled: Boolean) { subscribe?.enabled(enabled) } @@ -219,14 +222,26 @@ class AnimeWatchAdapter( for (position in arr.indices) { val last = if (position + 1 == arr.size) names.size else (limit * (position + 1)) val chip = - ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root + ItemChipBinding.inflate( + LayoutInflater.from(fragment.context), + binding.animeSourceChipGroup, + false + ).root chip.isCheckable = true fun selected() { chip.isChecked = true - binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0) + binding.animeWatchChipScroll.smoothScrollTo( + (chip.left - screenWidth / 2) + (chip.width / 2), + 0 + ) } chip.text = "${names[limit * (position)]} - ${names[last - 1]}" - chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color)) + chip.setTextColor( + ContextCompat.getColorStateList( + fragment.requireContext(), + R.color.chip_text_color + ) + ) chip.setOnClickListener { selected() @@ -239,7 +254,14 @@ class AnimeWatchAdapter( } } if (select != null) - binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } } + binding.animeWatchChipScroll.apply { + post { + scrollTo( + (select.left - screenWidth / 2) + (select.width / 2), + 0 + ) + } + } } } @@ -281,7 +303,9 @@ class AnimeWatchAdapter( } } val ep = media.anime.episodes!![continueEp]!! - binding.itemEpisodeImage.loadImage(ep.thumb ?: FileUrl[media.banner ?: media.cover], 0) + binding.itemEpisodeImage.loadImage( + ep.thumb ?: FileUrl[media.banner ?: media.cover], 0 + ) if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE binding.animeSourceContinueText.text = currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}" @@ -321,10 +345,17 @@ class AnimeWatchAdapter( } try { binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang) - }catch (e: IndexOutOfBoundsException) { - binding?.animeSourceLanguage?.setText(parser.extension.sources.firstOrNull()?.lang ?: "Unknown") + } catch (e: IndexOutOfBoundsException) { + binding?.animeSourceLanguage?.setText( + parser.extension.sources.firstOrNull()?.lang ?: "Unknown" + ) } - binding?.animeSourceLanguage?.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, parser.extension.sources.map { it.lang })) + binding?.animeSourceLanguage?.setAdapter( + ArrayAdapter( + fragment.requireContext(), + R.layout.item_dropdown, + parser.extension.sources.map { it.lang }) + ) } } @@ -332,7 +363,8 @@ class AnimeWatchAdapter( override fun getItemCount(): Int = 1 - inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) { + inner class ViewHolder(val binding: ItemAnimeWatchBinding) : + RecyclerView.ViewHolder(binding.root) { init { //Timer countDown(media, binding.animeSourceContainer) 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 0dc1fda7..199f4ffd 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt @@ -8,8 +8,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.ImageView -import android.widget.LinearLayout import android.widget.Toast import androidx.cardview.widget.CardView import androidx.core.math.MathUtils @@ -28,8 +26,6 @@ import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.parsers.AnimeParser import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.HAnimeSources -import ani.dantotsu.settings.ExtensionsActivity -import ani.dantotsu.settings.InstalledAnimeExtensionsFragment import ani.dantotsu.settings.PlayerSettings import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment @@ -40,17 +36,12 @@ import ani.dantotsu.subcriptions.SubscriptionHelper import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription import com.google.android.material.appbar.AppBarLayout import com.google.android.material.navigationrail.NavigationRailView -import com.google.android.material.tabs.TabLayout -import com.google.android.material.textfield.TextInputLayout import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource -import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import kotlin.math.ceil import kotlin.math.max import kotlin.math.roundToInt @@ -96,8 +87,10 @@ class AnimeWatchFragment : Fragment() { maxGridSize = max(4, maxGridSize - (maxGridSize % 2)) playerSettings = - loadData("player_settings", toast = false) ?: PlayerSettings().apply { saveData("player_settings", this) } - uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } + loadData("player_settings", toast = false) + ?: PlayerSettings().apply { saveData("player_settings", this) } + uiSettings = loadData("ui_settings", toast = false) + ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize) @@ -106,11 +99,11 @@ class AnimeWatchFragment : Fragment() { val style = episodeAdapter.getItemViewType(position) return when (position) { - 0 -> maxGridSize + 0 -> maxGridSize else -> when (style) { - 0 -> maxGridSize - 1 -> 2 - 2 -> 1 + 0 -> maxGridSize + 1 -> 2 + 2 -> 1 else -> maxGridSize } } @@ -129,7 +122,8 @@ class AnimeWatchFragment : Fragment() { media = it media.selected = model.loadSelected(media) - subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id) + subscribed = + SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id) style = media.selected!!.recyclerStyle reverse = media.selected!!.recyclerReversed @@ -141,9 +135,11 @@ class AnimeWatchFragment : Fragment() { model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!) - episodeAdapter = EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this) + episodeAdapter = + EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this) - binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, episodeAdapter) + binding.animeSourceRecycler.adapter = + ConcatAdapter(headerAdapter, episodeAdapter) lifecycleScope.launch(Dispatchers.IO) { awaitAll( @@ -165,15 +161,20 @@ class AnimeWatchFragment : Fragment() { episodes.forEach { (i, episode) -> if (media.anime?.fillerEpisodes != null) { if (media.anime!!.fillerEpisodes!!.containsKey(i)) { - episode.title = episode.title ?: media.anime!!.fillerEpisodes!![i]?.title + episode.title = + episode.title ?: media.anime!!.fillerEpisodes!![i]?.title episode.filler = media.anime!!.fillerEpisodes!![i]?.filler ?: false } } if (media.anime?.kitsuEpisodes != null) { if (media.anime!!.kitsuEpisodes!!.containsKey(i)) { - episode.desc = episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc - episode.title = episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title - episode.thumb = episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb ?: FileUrl[media.cover] + episode.desc = + episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc + episode.title = + episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title + episode.thumb = + episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb + ?: FileUrl[media.cover] } } } @@ -187,7 +188,7 @@ class AnimeWatchFragment : Fragment() { val limit = when { (divisions < 25) -> 25 (divisions < 50) -> 50 - else -> 100 + else -> 100 } headerAdapter.clearChips() if (total > limit) { @@ -247,7 +248,12 @@ class AnimeWatchFragment : Fragment() { selected.preferDub = checked model.saveSelected(media.id, selected, requireActivity()) media.selected = selected - lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) } + lifecycleScope.launch(Dispatchers.IO) { + model.forceLoadEpisode( + media, + selected.sourceIndex + ) + } } fun loadEpisodes(i: Int, invalidate: Boolean) { @@ -289,7 +295,8 @@ class AnimeWatchFragment : Fragment() { else getString(R.string.unsubscribed_notification) ) } - fun openSettings(pkg: AnimeExtension.Installed){ + + fun openSettings(pkg: AnimeExtension.Installed) { val changeUIVisibility: (Boolean) -> Unit = { show -> val activity = requireActivity() as MediaDetailsActivity val visibility = if (show) View.VISIBLE else View.GONE @@ -297,9 +304,9 @@ class AnimeWatchFragment : Fragment() { activity.findViewById(R.id.mediaViewPager).visibility = visibility activity.findViewById(R.id.mediaCover).visibility = visibility activity.findViewById(R.id.mediaClose).visibility = visibility - try{ + try { activity.findViewById(R.id.mediaTab).visibility = visibility - }catch (e: ClassCastException){ + } catch (e: ClassCastException) { activity.findViewById(R.id.mediaTab).visibility = visibility } activity.findViewById(R.id.fragmentExtensionsContainer).visibility = @@ -363,46 +370,46 @@ class AnimeWatchFragment : Fragment() { } } - fun onEpisodeClick(i: String) { - model.continueMedia = false - model.saveSelected(media.id, media.selected!!, requireActivity()) - model.onEpisodeClick(media, i, requireActivity().supportFragmentManager) + fun onEpisodeClick(i: String) { + model.continueMedia = false + model.saveSelected(media.id, media.selected!!, requireActivity()) + model.onEpisodeClick(media, i, requireActivity().supportFragmentManager) + } + + @SuppressLint("NotifyDataSetChanged") + private fun reload() { + val selected = model.loadSelected(media) + + //Find latest episode for subscription + selected.latest = + media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f + selected.latest = + media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest + + model.saveSelected(media.id, selected, requireActivity()) + headerAdapter.handleEpisodes() + episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size) + var arr: ArrayList = arrayListOf() + if (media.anime!!.episodes != null) { + val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null + arr.addAll( + media.anime!!.episodes!!.values.toList() + .slice(start..(end ?: (media.anime!!.episodes!!.size - 1))) + ) + if (reverse) + arr = (arr.reversed() as? ArrayList) ?: arr } + episodeAdapter.arr = arr + episodeAdapter.updateType(style ?: uiSettings.animeDefaultView) + episodeAdapter.notifyItemRangeInserted(0, arr.size) + } - @SuppressLint("NotifyDataSetChanged") - private fun reload() { - val selected = model.loadSelected(media) + override fun onDestroy() { + model.watchSources?.flushText() + super.onDestroy() + } - //Find latest episode for subscription - selected.latest = - media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f - selected.latest = - media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest - - model.saveSelected(media.id, selected, requireActivity()) - headerAdapter.handleEpisodes() - episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size) - var arr: ArrayList = arrayListOf() - if (media.anime!!.episodes != null) { - val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null - arr.addAll( - media.anime!!.episodes!!.values.toList() - .slice(start..(end ?: (media.anime!!.episodes!!.size - 1))) - ) - if (reverse) - arr = (arr.reversed() as? ArrayList) ?: arr - } - episodeAdapter.arr = arr - episodeAdapter.updateType(style ?: uiSettings.animeDefaultView) - episodeAdapter.notifyItemRangeInserted(0, arr.size) - } - - override fun onDestroy() { - model.watchSources?.flushText() - super.onDestroy() - } - - var state: Parcelable? = null + var state: Parcelable? = null override fun onResume() { super.onResume() binding.mediaInfoProgressBar.visibility = progress diff --git a/app/src/main/java/ani/dantotsu/media/anime/Episode.kt b/app/src/main/java/ani/dantotsu/media/anime/Episode.kt index a1112338..14615246 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/Episode.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/Episode.kt @@ -14,12 +14,12 @@ data class Episode( var selectedExtractor: String? = null, var selectedVideo: Int = 0, var selectedSubtitle: Int? = -1, - var extractors: MutableList?=null, - @Transient var extractorCallback: ((VideoExtractor) -> Unit)?=null, + var extractors: MutableList? = null, + @Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null, var allStreams: Boolean = false, var watched: Long? = null, var maxLength: Long? = null, - val extra: Map?=null, + val extra: Map? = null, val sEpisode: eu.kanade.tachiyomi.animesource.model.SEpisode? = null ) : Serializable 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 17924c80..1a85b9bf 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/EpisodeAdapters.kt @@ -41,15 +41,30 @@ class EpisodeAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return (when (viewType) { - 0 -> EpisodeListViewHolder(ItemEpisodeListBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - 1 -> EpisodeGridViewHolder(ItemEpisodeGridBinding.inflate(LayoutInflater.from(parent.context), parent, false)) - 2 -> EpisodeCompactViewHolder( + 0 -> EpisodeListViewHolder( + ItemEpisodeListBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + 1 -> EpisodeGridViewHolder( + ItemEpisodeGridBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + + 2 -> EpisodeCompactViewHolder( ItemEpisodeCompactBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) + else -> throw IllegalArgumentException() }) } @@ -62,15 +77,21 @@ class EpisodeAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val ep = arr[position] val title = - "${if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString(R.string.episode_singular)} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}" + "${ + if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString( + R.string.episode_singular + ) + } ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}" when (holder) { - is EpisodeListViewHolder -> { + is EpisodeListViewHolder -> { val binding = holder.binding setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) - val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } - Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage) + val thumb = + ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } + Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0) + .into(binding.itemEpisodeImage) binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeTitle.text = title @@ -81,7 +102,8 @@ class EpisodeAdapter( binding.itemEpisodeFiller.visibility = View.GONE binding.itemEpisodeFillerView.visibility = View.GONE } - binding.itemEpisodeDesc.visibility = if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE + binding.itemEpisodeDesc.visibility = + if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE binding.itemEpisodeDesc.text = ep.desc ?: "" if (media.userProgress != null) { @@ -110,12 +132,14 @@ class EpisodeAdapter( ) } - is EpisodeGridViewHolder -> { + is EpisodeGridViewHolder -> { val binding = holder.binding setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) - val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } - Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage) + val thumb = + ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } + Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0) + .into(binding.itemEpisodeImage) binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeTitle.text = title @@ -155,7 +179,8 @@ class EpisodeAdapter( val binding = holder.binding setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) binding.itemEpisodeNumber.text = ep.number - binding.itemEpisodeFillerView.visibility = if (ep.filler) View.VISIBLE else View.GONE + binding.itemEpisodeFillerView.visibility = + if (ep.filler) View.VISIBLE else View.GONE if (media.userProgress != null) { if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) binding.itemEpisodeViewedCover.visibility = View.VISIBLE @@ -180,7 +205,8 @@ class EpisodeAdapter( override fun getItemCount(): Int = arr.size - inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) { + inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) : + RecyclerView.ViewHolder(binding.root) { init { itemView.setOnClickListener { if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0) @@ -189,7 +215,8 @@ class EpisodeAdapter( } } - inner class EpisodeGridViewHolder(val binding: ItemEpisodeGridBinding) : RecyclerView.ViewHolder(binding.root) { + inner class EpisodeGridViewHolder(val binding: ItemEpisodeGridBinding) : + RecyclerView.ViewHolder(binding.root) { init { itemView.setOnClickListener { if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0) @@ -198,7 +225,8 @@ class EpisodeAdapter( } } - inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) : RecyclerView.ViewHolder(binding.root) { + inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) : + RecyclerView.ViewHolder(binding.root) { init { itemView.setOnClickListener { if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0) 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 abf500ce..f45d5740 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -17,7 +17,6 @@ import android.graphics.drawable.Animatable import android.hardware.SensorManager import android.media.AudioManager import android.media.AudioManager.* -import android.media.PlaybackParams import android.net.Uri import android.os.Build import android.os.Bundle @@ -62,6 +61,8 @@ import ani.dantotsu.* import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.discord.Discord +import ani.dantotsu.connections.discord.DiscordService +import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton import ani.dantotsu.connections.discord.RPC import ani.dantotsu.connections.updateProgress import ani.dantotsu.databinding.ActivityExoplayerBinding @@ -71,6 +72,7 @@ import ani.dantotsu.media.SubtitleDownloader import ani.dantotsu.others.AniSkip import ani.dantotsu.others.AniSkip.getType import ani.dantotsu.others.Download.download +import ani.dantotsu.others.LangSet import ani.dantotsu.others.ResettableTimer import ani.dantotsu.others.getSerialized import ani.dantotsu.parsers.* @@ -78,7 +80,6 @@ import ani.dantotsu.settings.PlayerSettings import ani.dantotsu.settings.PlayerSettingsActivity import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet import com.bumptech.glide.Glide import com.google.android.material.slider.Slider import com.google.firebase.crashlytics.FirebaseCrashlytics @@ -813,15 +814,15 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { fun fastForward() { isFastForwarding = true - exoPlayer.setPlaybackSpeed(2f) - snackString("Playing at 2x speed") + exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2) + snackString("Playing at ${exoPlayer.playbackParameters.speed}x speed") } fun stopFastForward() { if (isFastForwarding) { isFastForwarding = false - exoPlayer.setPlaybackSpeed(1f) - snackString("Playing at normal speed") + exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed / 2) + snackString("Playing at default speed: ${exoPlayer.playbackParameters.speed}x") } } @@ -862,6 +863,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { override fun onLongClick(event: MotionEvent) { if (settings.fastforward) fastForward() } + override fun onDoubleClick(event: MotionEvent) { doubleTap(true, event) } @@ -994,22 +996,40 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { playbackPosition = loadData("${media.id}_${ep.number}", this) ?: 0 initPlayer() preloading = false - rpc = Discord.defaultRPC() - rpc?.send { - type = RPC.Type.WATCHING - activityName = media.userPreferredName - details = ep.title?.takeIf { it.isNotEmpty() } ?: getString( - R.string.episode_num, - ep.number + val context = this + + lifecycleScope.launch { + val presence = RPC.createPresence(RPC.Companion.RPCData( + applicationId = Discord.application_Id, + type = RPC.Type.WATCHING, + activityName = media.userPreferredName, + details = ep.title?.takeIf { it.isNotEmpty() } ?: getString( + R.string.episode_num, + ep.number + ), + state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}", + largeImage = media.cover?.let { RPC.Link(media.userPreferredName, it) }, + smallImage = RPC.Link( + "Dantotsu", + Discord.small_Image + ), + buttons = mutableListOf( + RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""), + RPC.Link( + "Stream on Dantotsu", + "https://github.com/rebelonion/Dantotsu/" + ) + ) ) - state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}" - media.cover?.let { cover -> - largeImage = RPC.Link(media.userPreferredName, cover) - } - media.shareLink?.let { link -> - buttons.add(0, RPC.Link(getString(R.string.view_anime), link)) + ) + + val intent = Intent(context, DiscordService::class.java).apply { + putExtra("presence", presence) } + DiscordServiceRunningSingleton.running = true + startService(intent) } + updateProgress() } } @@ -1129,7 +1149,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { if (settings.askIndividual) loadData("${media.id}_progressDialog") ?: true else false if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true) - AlertDialog.Builder(this, R.style.DialogTheme) + AlertDialog.Builder(this, R.style.MyPopup) .setTitle(getString(R.string.auto_update, media.userPreferredName)) .apply { setOnCancelListener { hideSystemBars() } @@ -1278,6 +1298,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { } val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType) + logger("url: ${video!!.file.url}") + logger("mimeType: $mimeType") if (sub != null) { val listofnotnullsubs = immutableListOf(sub).filterNotNull() @@ -1301,7 +1323,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { .setAllowMultipleAdaptiveSelections(true) .setPreferredTextLanguage(subtitle?.language ?: "en") .setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE) - .setRendererDisabled(C.TRACK_TYPE_VIDEO, false) + .setRendererDisabled(TRACK_TYPE_VIDEO, false) .setRendererDisabled(C.TRACK_TYPE_AUDIO, false) .setRendererDisabled(C.TRACK_TYPE_TEXT, false) .setMinVideoSize( @@ -1310,7 +1332,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { ) .setMaxVideoSize(1, 1) //.setOverrideForType( - // TrackSelectionOverride(trackSelector, 2)) + // TrackSelectionOverride(trackSelector, 2)) ) if (playbackPosition != 0L && !changingServer && !settings.alwaysContinue) { @@ -1329,17 +1351,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { ) AlertDialog.Builder(this, R.style.DialogTheme) .setTitle(getString(R.string.continue_from, time)).apply { - setCancelable(false) - setPositiveButton(getString(R.string.yes)) { d, _ -> - buildExoplayer() - d.dismiss() - } - setNegativeButton(getString(R.string.no)) { d, _ -> - playbackPosition = 0L - buildExoplayer() - d.dismiss() - } - }.show() + setCancelable(false) + setPositiveButton(getString(R.string.yes)) { d, _ -> + buildExoplayer() + d.dismiss() + } + setNegativeButton(getString(R.string.no)) { d, _ -> + playbackPosition = 0L + buildExoplayer() + d.dismiss() + } + }.show() } else buildExoplayer() } @@ -1404,7 +1426,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { exoPlayer.release() VideoCache.release() mediaSession?.release() - rpc?.close() + val stopIntent = Intent(this, DiscordService::class.java) + DiscordServiceRunningSingleton.running = false + stopService(stopIntent) + } override fun onSaveInstanceState(outState: Bundle) { @@ -1589,17 +1614,19 @@ class ExoplayerView : AppCompatActivity(), Player.Listener { println("Track__: ${it.isSelected}") println("Track__: ${it.type}") println("Track__: ${it.mediaTrackGroup.id}") - if (it.type == 3 && it.mediaTrackGroup.id == "1:"){ + if (it.type == 3 && it.mediaTrackGroup.id == "1:") { playerView.player?.trackSelectionParameters = playerView.player?.trackSelectionParameters?.buildUpon() ?.setOverrideForType( - TrackSelectionOverride(it.mediaTrackGroup, it.length - 1)) + TrackSelectionOverride(it.mediaTrackGroup, it.length - 1) + ) ?.build()!! - }else if(it.type == 3){ + } else if (it.type == 3) { playerView.player?.trackSelectionParameters = playerView.player?.trackSelectionParameters?.buildUpon() ?.addOverride( - TrackSelectionOverride(it.mediaTrackGroup, listOf())) + TrackSelectionOverride(it.mediaTrackGroup, listOf()) + ) ?.build()!! } } 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 23ac9f04..d42bcd94 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt @@ -25,8 +25,6 @@ import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.others.Download.download import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoType -import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -54,7 +52,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetSelectorBinding.inflate(inflater, container, false) val window = dialog?.window window?.statusBarColor = Color.TRANSPARENT @@ -71,7 +73,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { media = m if (media != null && !loaded) { loaded = true - val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode) + val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode) episode = ep if (ep != null) { @@ -92,14 +94,17 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } fun load() { - val size = ep.extractors?.find { it.server.name == selected }?.videos?.size - if (size!=null && size >= media!!.selected!!.video) { - media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor = selected - media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo = media!!.selected!!.video + val size = + ep.extractors?.find { it.server.name == selected }?.videos?.size + if (size != null && size >= media!!.selected!!.video) { + media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor = + selected + media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo = + media!!.selected!!.video startExoplayer(media!!) } else fail() } - + if (ep.extractors.isNullOrEmpty()) { model.getEpisode().observe(this) { if (it != null) { @@ -116,8 +121,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { }) fail() } } else load() - } - else { + } else { binding.selectorRecyclerView.updateLayoutParams { bottomMargin = navBarHeight } @@ -130,10 +134,14 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { saveData("make_default", makeDefault) } binding.selectorRecyclerView.layoutManager = - LinearLayoutManager(requireActivity(), LinearLayoutManager.VERTICAL, false) + LinearLayoutManager( + requireActivity(), + LinearLayoutManager.VERTICAL, + false + ) val adapter = ExtractorAdapter() binding.selectorRecyclerView.adapter = adapter - if (!ep.allStreams ) { + if (!ep.allStreams) { ep.extractorCallback = { scope.launch { adapter.add(it) @@ -141,12 +149,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } model.getEpisode().observe(this) { if (it != null) { - media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep) + media!!.anime?.episodes?.set( + media!!.anime?.selectedEpisode!!, + ep + ) } } scope.launch(Dispatchers.IO) { model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex) - withContext(Dispatchers.Main){ + withContext(Dispatchers.Main) { binding.selectorProgressBar.visibility = View.GONE } } @@ -175,7 +186,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { ExoplayerView.initialized = true startActivity(intent) } else { - model.setEpisode(media.anime!!.episodes!![media.anime.selectedEpisode!!]!!, "startExo no launch") + model.setEpisode( + media.anime!!.episodes!![media.anime.selectedEpisode!!]!!, + "startExo no launch" + ) } } @@ -186,54 +200,72 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } } - private inner class ExtractorAdapter : RecyclerView.Adapter() { + private inner class ExtractorAdapter : + RecyclerView.Adapter() { val links = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder = - StreamViewHolder(ItemStreamBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + StreamViewHolder( + ItemStreamBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { val extractor = links[position] holder.binding.streamName.text = extractor.server.name - holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext()) - holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor) + holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext()) + holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor) } override fun getItemCount(): Int = links.size - fun add(videoExtractor: VideoExtractor){ - if(videoExtractor.videos.isNotEmpty()) { + fun add(videoExtractor: VideoExtractor) { + if (videoExtractor.videos.isNotEmpty()) { links.add(videoExtractor) notifyItemInserted(links.size - 1) } } fun addAll(extractors: List?) { - links.addAll(extractors?:return) - notifyItemRangeInserted(0,extractors.size) + links.addAll(extractors ?: return) + notifyItemRangeInserted(0, extractors.size) } - private inner class StreamViewHolder(val binding: ItemStreamBinding) : RecyclerView.ViewHolder(binding.root) + private inner class StreamViewHolder(val binding: ItemStreamBinding) : + RecyclerView.ViewHolder(binding.root) } - private inner class VideoAdapter(private val extractor : VideoExtractor) : RecyclerView.Adapter() { + private inner class VideoAdapter(private val extractor: VideoExtractor) : + RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder { - return UrlViewHolder(ItemUrlBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + return UrlViewHolder( + ItemUrlBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) } @SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: UrlViewHolder, position: Int) { val binding = holder.binding val video = extractor.videos[position] - binding.urlQuality.text = if(video.quality!=null) "${video.quality}p" else "Default Quality" + binding.urlQuality.text = + if (video.quality != null) "${video.quality}p" else "Default Quality" binding.urlNote.text = video.extraNote ?: "" binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE binding.urlDownload.visibility = View.VISIBLE binding.urlDownload.setSafeOnClickListener { - media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor = extractor.server.name - media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo = position + media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor = + extractor.server.name + media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo = + position binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) download( requireActivity(), @@ -245,10 +277,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { if (video.format == VideoType.CONTAINER) { binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE binding.urlSize.text = - // if video size is null or 0, show "Unknown Size" else show the size in MB - (if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat("#.##").format(video.size ?: 0).toString()+ " MB")) - } - else { + // if video size is null or 0, show "Unknown Size" else show the size in MB + (if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat( + "#.##" + ).format(video.size ?: 0).toString() + " MB")) + } else { binding.urlQuality.text = "Multi Quality" if ((loadData("settings_download_manager") ?: 0) == 0) { binding.urlDownload.visibility = View.GONE @@ -258,12 +291,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { override fun getItemCount(): Int = extractor.videos.size - private inner class UrlViewHolder(val binding: ItemUrlBinding) : RecyclerView.ViewHolder(binding.root) { + private inner class UrlViewHolder(val binding: ItemUrlBinding) : + RecyclerView.ViewHolder(binding.root) { init { itemView.setSafeOnClickListener { tryWith(true) { - media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor = extractor.server.name - media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = bindingAdapterPosition + media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor = + extractor.server.name + media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = + bindingAdapterPosition if (makeDefault) { media!!.selected!!.server = extractor.server.name media!!.selected!!.video = bindingAdapterPosition @@ -274,12 +310,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } itemView.setOnLongClickListener { val video = extractor.videos[bindingAdapterPosition] - val intent= Intent(Intent.ACTION_VIEW).apply { - setDataAndType(Uri.parse(video.file.url),"video/*") + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(Uri.parse(video.file.url), "video/*") } - copyToClipboard(video.file.url,true) + copyToClipboard(video.file.url, true) dismiss() - startActivity(Intent.createChooser(intent,"Open Video in :")) + startActivity(Intent.createChooser(intent, "Open Video in :")) true } } @@ -287,7 +323,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } companion object { - fun newInstance(server: String? = null, la: Boolean = true, prev: String? = null): SelectorDialogFragment = + fun newInstance( + server: String? = null, + la: Boolean = true, + prev: String? = null + ): SelectorDialogFragment = SelectorDialogFragment().apply { arguments = Bundle().apply { putString("server", server) 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 9314eb6e..406efb3c 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt @@ -24,7 +24,11 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() { val model: MediaDetailsViewModel by activityViewModels() private lateinit var episode: Episode - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false) return binding.root } @@ -34,17 +38,27 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() { model.getMedia().observe(viewLifecycleOwner) { media -> episode = media?.anime?.episodes?.get(media.anime.selectedEpisode) ?: return@observe - val currentExtractor = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return@observe + val currentExtractor = + episode.extractors?.find { it.server.name == episode.selectedExtractor } + ?: return@observe binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext()) binding.subtitlesRecycler.adapter = SubtitleAdapter(currentExtractor.subtitles) } } - inner class SubtitleAdapter(val subtitles: List) : RecyclerView.Adapter() { - inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) : RecyclerView.ViewHolder(binding.root) + inner class SubtitleAdapter(val subtitles: List) : + RecyclerView.Adapter() { + inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) : + RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder = - StreamViewHolder(ItemSubtitleTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + StreamViewHolder( + ItemSubtitleTextBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { val binding = holder.binding @@ -60,7 +74,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() { binding.root.setOnClickListener { episode.selectedSubtitle = null model.setEpisode(episode, "Subtitle") - model.getMedia().observe(viewLifecycleOwner){media -> + model.getMedia().observe(viewLifecycleOwner) { media -> val mediaID: Int = media.id saveData("subLang_${mediaID}", "None", activity) } @@ -87,7 +101,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() { "pl-PL" -> "[pl-PL] Polish" "ro-RO" -> "[ro-RO] Romanian" "sv-SE" -> "[sv-SE] Swedish" - else -> if(subtitles[position - 1].language matches Regex("([a-z]{2})-([A-Z]{2}|\\d{3})")) "[${subtitles[position - 1].language}]" else subtitles[position - 1].language + else -> if (subtitles[position - 1].language matches Regex("([a-z]{2})-([A-Z]{2}|\\d{3})")) "[${subtitles[position - 1].language}]" else subtitles[position - 1].language } model.getMedia().observe(viewLifecycleOwner) { media -> val mediaID: Int = media.id @@ -100,7 +114,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() { binding.root.setOnClickListener { episode.selectedSubtitle = position - 1 model.setEpisode(episode, "Subtitle") - model.getMedia().observe(viewLifecycleOwner){media -> + model.getMedia().observe(viewLifecycleOwner) { media -> val mediaID: Int = media.id saveData("subLang_${mediaID}", subtitles[position - 1].language, activity) } diff --git a/app/src/main/java/ani/dantotsu/media/anime/VideoCache.kt b/app/src/main/java/ani/dantotsu/media/anime/VideoCache.kt index 6c463a5f..340966fa 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/VideoCache.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/VideoCache.kt @@ -14,7 +14,10 @@ object VideoCache { val databaseProvider = StandaloneDatabaseProvider(context) if (simpleCache == null) simpleCache = SimpleCache( - File(context.cacheDir, "exoplayer").also { it.deleteOnExit() }, // Ensures always fresh file + File( + context.cacheDir, + "exoplayer" + ).also { it.deleteOnExit() }, // Ensures always fresh file LeastRecentlyUsedCacheEvictor(300L * 1024L * 1024L), databaseProvider ) diff --git a/app/src/main/java/ani/dantotsu/media/manga/Manga.kt b/app/src/main/java/ani/dantotsu/media/manga/Manga.kt index 3179a1ec..24275add 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/Manga.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/Manga.kt @@ -8,5 +8,5 @@ data class Manga( var selectedChapter: String? = null, var chapters: MutableMap? = null, var slug: String? = null, - var author: Author?=null, + var author: Author? = null, ) : Serializable \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt index 16fb83b9..50eec776 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaCache.kt @@ -10,7 +10,6 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import android.util.LruCache -import android.widget.Toast import ani.dantotsu.logger import ani.dantotsu.snackString import eu.kanade.tachiyomi.source.model.Page @@ -23,8 +22,12 @@ import java.io.FileOutputStream data class ImageData( val page: Page, val source: HttpSource -){ - suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? { +) { + suspend fun fetchAndProcessImage( + page: Page, + httpSource: HttpSource, + context: Context + ): Bitmap? { return withContext(Dispatchers.IO) { try { // Fetch the image @@ -52,16 +55,26 @@ data class ImageData( } } -fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) { +fun saveImage( + bitmap: Bitmap, + contentResolver: ContentResolver, + filename: String, + format: Bitmap.CompressFormat, + quality: Int +) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, filename) put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}") - put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga") + put( + MediaStore.MediaColumns.RELATIVE_PATH, + "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga" + ) } - val uri: Uri? = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + val uri: Uri? = + contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) uri?.let { contentResolver.openOutputStream(it)?.use { os -> @@ -69,7 +82,8 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String } } } else { - val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Manga") + val directory = + File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Manga") if (!directory.exists()) { directory.mkdirs() } @@ -85,7 +99,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String } } -class MangaCache() { +class MangaCache { private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024 / 2).toInt() private val cache = LruCache(maxMemory) diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt index 0bfc3237..6af71788 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaChapter.kt @@ -15,7 +15,14 @@ data class MangaChapter( val scanlator: String? = null, var progress: String? = "" ) : Serializable { - constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter, chapter.scanlator) + constructor(chapter: MangaChapter) : this( + chapter.number, + chapter.link, + chapter.title, + chapter.description, + chapter.sChapter, + chapter.scanlator + ) private val images = mutableListOf() fun images(): List = images 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 b14a81f1..c4752a04 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaChapterAdapter.kt @@ -5,16 +5,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.LinearInterpolator -import androidx.core.content.ContentProviderCompat.requireContext import androidx.lifecycle.coroutineScope import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.R +import ani.dantotsu.connections.updateProgress +import ani.dantotsu.currContext import ani.dantotsu.databinding.ItemChapterListBinding import ani.dantotsu.databinding.ItemEpisodeCompactBinding import ani.dantotsu.media.Media import ani.dantotsu.setAnimation -import ani.dantotsu.connections.updateProgress -import ani.dantotsu.currContext import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -124,7 +123,7 @@ class MangaChapterAdapter( if (progress != null) { binding.itemChapterTitle.visibility = View.VISIBLE binding.itemChapterTitle.text = "$progress" - }else{ + } else { binding.itemChapterTitle.visibility = View.GONE binding.itemChapterTitle.text = "" } @@ -154,9 +153,10 @@ class MangaChapterAdapter( // Add chapter number to active coroutines set activeCoroutines.add(chapterNumber) while (activeDownloads.contains(chapterNumber)) { - binding.itemDownload.animate().rotationBy(360f).setDuration(1000).setInterpolator( - LinearInterpolator() - ).start() + binding.itemDownload.animate().rotationBy(360f).setDuration(1000) + .setInterpolator( + LinearInterpolator() + ).start() delay(1000) } // Remove chapter number from active coroutines set @@ -171,8 +171,16 @@ class MangaChapterAdapter( init { val theme = currContext()?.theme - theme?.resolveAttribute(com.google.android.material.R.attr.colorError, typedValue1, true) - theme?.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue2, true) + theme?.resolveAttribute( + com.google.android.material.R.attr.colorError, + typedValue1, + true + ) + theme?.resolveAttribute( + com.google.android.material.R.attr.colorPrimary, + typedValue2, + true + ) itemView.setOnClickListener { if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) fragment.onMangaChapterClick(arr[bindingAdapterPosition].number) 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 8ef56d92..265122d6 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadAdapter.kt @@ -13,15 +13,12 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.* -import ani.dantotsu.App.Companion.context -import ani.dantotsu.media.anime.handleProgress import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.SourceSearchDialogFragment -import ani.dantotsu.parsers.AnimeSources -import ani.dantotsu.parsers.DynamicAnimeParser +import ani.dantotsu.media.anime.handleProgress import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.MangaReadSources import ani.dantotsu.parsers.MangaSources @@ -30,7 +27,6 @@ import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import com.google.android.material.chip.Chip import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch -import java.lang.IndexOutOfBoundsException class MangaReadAdapter( private val media: Media, @@ -57,12 +53,16 @@ class MangaReadAdapter( //Wrong Title binding.animeSourceSearch.setOnClickListener { - SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null) + SourceSearchDialogFragment().show( + fragment.requireActivity().supportFragmentManager, + null + ) } //Source Selection - var source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it } - setLanguageList(media.selected!!.langIndex,source) + var source = + media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it } + setLanguageList(media.selected!!.langIndex, source) if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) { binding.animeSource.setText(mangaReadSources.names[source]) mangaReadSources[source].apply { @@ -70,14 +70,20 @@ class MangaReadAdapter( showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } } } - binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, mangaReadSources.names)) + binding.animeSource.setAdapter( + ArrayAdapter( + fragment.requireContext(), + R.layout.item_dropdown, + mangaReadSources.names + ) + ) binding.animeSourceTitle.isSelected = true binding.animeSource.setOnItemClickListener { _, _, i, _ -> fragment.onSourceChange(i).apply { binding.animeSourceTitle.text = showUserText showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } source = i - setLanguageList(0,i) + setLanguageList(0, i) } subscribeButton(false) //invalidate if it's the last source @@ -92,7 +98,8 @@ class MangaReadAdapter( fragment.onLangChange(i) fragment.onSourceChange(media.selected!!.sourceIndex).apply { binding.animeSourceTitle.text = showUserText - showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } + showUserTextListener = + { MainScope().launch { binding.animeSourceTitle.text = it } } setLanguageList(i, source) } subscribeButton(false) @@ -139,7 +146,8 @@ class MangaReadAdapter( } binding.animeScanlatorTop.setOnClickListener { - val dialogView = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null) + val dialogView = + LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null) val checkboxContainer = dialogView.findViewById(R.id.checkboxContainer) // Dynamically add checkboxes @@ -149,10 +157,10 @@ class MangaReadAdapter( text = option } //set checked if it's already selected - if(media.selected!!.scanlators != null){ + if (media.selected!!.scanlators != null) { checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true scanlatorSelectionListener?.onScanlatorsSelected() - }else{ + } else { checkBox.isChecked = true } checkboxContainer.addView(checkBox) @@ -178,8 +186,8 @@ class MangaReadAdapter( } var selected = when (style) { - 0 -> binding.animeSourceList - 1 -> binding.animeSourceCompact + 0 -> binding.animeSourceList + 1 -> binding.animeSourceCompact else -> binding.animeSourceList } selected.alpha = 1f @@ -217,14 +225,26 @@ class MangaReadAdapter( for (position in arr.indices) { val last = if (position + 1 == arr.size) names.size else (limit * (position + 1)) val chip = - ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root + ItemChipBinding.inflate( + LayoutInflater.from(fragment.context), + binding.animeSourceChipGroup, + false + ).root chip.isCheckable = true fun selected() { chip.isChecked = true - binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0) + binding.animeWatchChipScroll.smoothScrollTo( + (chip.left - screenWidth / 2) + (chip.width / 2), + 0 + ) } chip.text = "${names[limit * (position)]} - ${names[last - 1]}" - chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color)) + chip.setTextColor( + ContextCompat.getColorStateList( + fragment.requireContext(), + R.color.chip_text_color + ) + ) chip.setOnClickListener { selected() @@ -237,7 +257,14 @@ class MangaReadAdapter( } } if (select != null) - binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } } + binding.animeWatchChipScroll.apply { + post { + scrollTo( + (select.left - screenWidth / 2) + (select.width / 2), + 0 + ) + } + } } } @@ -259,7 +286,9 @@ class MangaReadAdapter( val chapter = media.manga.chapters!![chapterKey]!! chapter.scanlator !in hiddenScanlators } - val formattedChapters = filteredChapters.map { MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString() } + val formattedChapters = filteredChapters.map { + MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString() + } if (formattedChapters.contains(continueEp)) { continueEp = chapters[formattedChapters.indexOf(continueEp)] binding.animeSourceContinue.visibility = View.VISIBLE @@ -317,10 +346,17 @@ class MangaReadAdapter( } try { binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang) - }catch (e: IndexOutOfBoundsException) { - binding?.animeSourceLanguage?.setText(parser.extension.sources.firstOrNull()?.lang ?: "Unknown") + } catch (e: IndexOutOfBoundsException) { + binding?.animeSourceLanguage?.setText( + parser.extension.sources.firstOrNull()?.lang ?: "Unknown" + ) } - binding?.animeSourceLanguage?.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, parser.extension.sources.map { it.lang })) + binding?.animeSourceLanguage?.setAdapter( + ArrayAdapter( + fragment.requireContext(), + R.layout.item_dropdown, + parser.extension.sources.map { it.lang }) + ) } } @@ -328,7 +364,8 @@ class MangaReadAdapter( override fun getItemCount(): Int = 1 - inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) + inner class ViewHolder(val binding: ItemAnimeWatchBinding) : + RecyclerView.ViewHolder(binding.root) } interface ScanlatorSelectionListener { 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 4adc7f0a..d59b84ba 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -1,5 +1,6 @@ package ani.dantotsu.media.manga +import android.Manifest import android.annotation.SuppressLint import android.app.AlertDialog import android.content.BroadcastReceiver @@ -16,6 +17,7 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.Toast import androidx.cardview.widget.CardView +import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.math.MathUtils.clamp import androidx.core.view.updatePadding @@ -30,11 +32,11 @@ import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.manga.MangaDownloaderService -import ani.dantotsu.download.manga.ServiceDataSingleton -import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog +import ani.dantotsu.download.manga.MangaServiceDataSingleton import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsViewModel +import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.MangaParser @@ -59,10 +61,8 @@ import uy.kohesive.injekt.api.get import kotlin.math.ceil import kotlin.math.max import kotlin.math.roundToInt -import android.Manifest -import androidx.core.app.ActivityCompat -open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { +open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { private var _binding: FragmentAnimeWatchBinding? = null private val binding get() = _binding!! private val model: MediaDetailsViewModel by activityViewModels() @@ -85,7 +85,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { var continueEp: Boolean = false var loaded = false - val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } + val uiSettings = loadData("ui_settings", toast = false) + ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } override fun onCreateView( inflater: LayoutInflater, @@ -105,7 +106,12 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { addAction(ACTION_DOWNLOAD_PROGRESS) } - ContextCompat.registerReceiver(requireContext(), downloadStatusReceiver, intentFilter, ContextCompat.RECEIVER_EXPORTED) + ContextCompat.registerReceiver( + requireContext(), + downloadStatusReceiver, + intentFilter, + ContextCompat.RECEIVER_EXPORTED + ) binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) screenWidth = resources.displayMetrics.widthPixels.dp @@ -120,10 +126,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { val style = chapterAdapter.getItemViewType(position) return when (position) { - 0 -> maxGridSize + 0 -> maxGridSize else -> when (style) { - 0 -> maxGridSize - 1 -> 1 + 0 -> maxGridSize + 1 -> 1 else -> maxGridSize } } @@ -146,7 +152,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { if (media.format == "MANGA" || media.format == "ONE SHOT") { media.selected = model.loadSelected(media) - subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id) + subscribed = + SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id) style = media.selected!!.recyclerStyle reverse = media.selected!!.recyclerReversed @@ -156,13 +163,15 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!) headerAdapter.scanlatorSelectionListener = this - chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this) + chapterAdapter = + MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this) - for (download in downloadManager.mangaDownloads){ + for (download in downloadManager.mangaDownloads) { chapterAdapter.stopDownload(download.chapter) } - binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter) + binding.animeSourceRecycler.adapter = + ConcatAdapter(headerAdapter, chapterAdapter) lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, media.selected!!.sourceIndex) @@ -173,7 +182,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { } } else { binding.animeNotSupported.visibility = View.VISIBLE - binding.animeNotSupported.text = getString(R.string.not_supported, media.format ?: "") + binding.animeNotSupported.text = + getString(R.string.not_supported, media.format ?: "") } } } @@ -207,7 +217,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { val limit = when { (divisions < 25) -> 25 (divisions < 50) -> 50 - else -> 100 + else -> 100 } headerAdapter.clearChips() if (total > limit) { @@ -302,7 +312,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { ) } - fun openSettings(pkg: MangaExtension.Installed){ + fun openSettings(pkg: MangaExtension.Installed) { val changeUIVisibility: (Boolean) -> Unit = { show -> val activity = requireActivity() as MediaDetailsActivity val visibility = if (show) View.VISIBLE else View.GONE @@ -310,9 +320,9 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { activity.findViewById(R.id.mediaViewPager).visibility = visibility activity.findViewById(R.id.mediaCover).visibility = visibility activity.findViewById(R.id.mediaClose).visibility = visibility - try{ + try { activity.findViewById(R.id.mediaTab).visibility = visibility - }catch (e: ClassCastException){ + } catch (e: ClassCastException) { activity.findViewById(R.id.mediaTab).visibility = visibility } activity.findViewById(R.id.fragmentExtensionsContainer).visibility = @@ -335,7 +345,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { // Move the fragment transaction here val fragment = - MangaSourcePreferencesFragment().getInstance(selectedSetting.id){ + MangaSourcePreferencesFragment().getInstance(selectedSetting.id) { changeUIVisibility(true) loadChapters(media.selected!!.sourceIndex, true) } @@ -353,7 +363,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { .show() } else { // If there's only one setting, proceed with the fragment transaction - val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id){ + val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) { changeUIVisibility(true) loadChapters(media.selected!!.sourceIndex, true) } @@ -376,7 +386,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { media.manga?.chapters?.get(i)?.let { media.manga?.selectedChapter = i model.saveSelected(media.id, media.selected!!, requireActivity()) - ChapterLoaderDialog.newInstance(it, true).show(requireActivity().supportFragmentManager, "dialog") + ChapterLoaderDialog.newInstance(it, true) + .show(requireActivity().supportFragmentManager, "dialog") } } @@ -393,7 +404,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { model.continueMedia = false media.manga?.chapters?.get(i)?.let { chapter -> - val parser = model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser + val parser = + model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser parser?.let { CoroutineScope(Dispatchers.IO).launch { val images = parser.imageList("", chapter.sChapter) @@ -408,15 +420,15 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { simultaneousDownloads = 2 ) - ServiceDataSingleton.downloadQueue.offer(downloadTask) + MangaServiceDataSingleton.downloadQueue.offer(downloadTask) // If the service is not already running, start it - if (!ServiceDataSingleton.isServiceRunning) { + if (!MangaServiceDataSingleton.isServiceRunning) { val intent = Intent(context, MangaDownloaderService::class.java) withContext(Dispatchers.Main) { ContextCompat.startForegroundService(requireContext(), intent) } - ServiceDataSingleton.isServiceRunning = true + MangaServiceDataSingleton.isServiceRunning = true } // Inform the adapter that the download has started @@ -439,10 +451,17 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { } - fun onMangaChapterRemoveDownloadClick(i: String){ - downloadManager.removeDownload(Download(media.nameMAL?:media.nameRomaji, i, Download.Type.MANGA)) + fun onMangaChapterRemoveDownloadClick(i: String) { + downloadManager.removeDownload( + Download( + media.nameMAL ?: media.nameRomaji, + i, + Download.Type.MANGA + ) + ) chapterAdapter.deleteDownload(i) } + fun onMangaChapterStopDownloadClick(i: String) { val cancelIntent = Intent().apply { action = MangaDownloaderService.ACTION_CANCEL_DOWNLOAD @@ -451,11 +470,19 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { requireContext().sendBroadcast(cancelIntent) // Remove the download from the manager and update the UI - downloadManager.removeDownload(Download(media.nameMAL?:media.nameRomaji, i, Download.Type.MANGA)) + downloadManager.removeDownload( + Download( + media.nameMAL ?: media.nameRomaji, + i, + Download.Type.MANGA + ) + ) chapterAdapter.purgeDownload(i) } + private val downloadStatusReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { + if (!this@MangaReadFragment::chapterAdapter.isInitialized) return when (intent.action) { ACTION_DOWNLOAD_STARTED -> { val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER) @@ -491,8 +518,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { val selected = model.loadSelected(media) //Find latest chapter for subscription - selected.latest = media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f - selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest + selected.latest = + media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f + selected.latest = + media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest model.saveSelected(media.id, selected, requireActivity()) headerAdapter.handleChapters() @@ -501,7 +530,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { if (media.manga!!.chapters != null) { val end = if (end != null && end!! < media.manga!!.chapters!!.size) end else null arr.addAll( - media.manga!!.chapters!!.values.toList().slice(start..(end ?: (media.manga!!.chapters!!.size - 1))) + media.manga!!.chapters!!.values.toList() + .slice(start..(end ?: (media.manga!!.chapters!!.size - 1))) ) if (reverse) arr = (arr.reversed() as? ArrayList) ?: arr 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 bae545bb..6a18b9e8 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 @@ -14,8 +14,8 @@ import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.* +import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaChapter -import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.settings.CurrentReaderSettings import com.alexvasilkov.gestures.views.GestureFrameLayout import com.bumptech.glide.Glide @@ -23,12 +23,9 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import ani.dantotsu.media.manga.MangaCache -import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File @@ -118,7 +115,10 @@ abstract class BaseImageAdapter( abstract suspend fun loadImage(position: Int, parent: View): Boolean companion object { - suspend fun Context.loadBitmap_old(link: FileUrl, transforms: List): Bitmap? { //still used in some places + suspend fun Context.loadBitmap_old( + link: FileUrl, + transforms: List + ): Bitmap? { //still used in some places return tryWithSuspend { withContext(Dispatchers.IO) { Glide.with(this@loadBitmap_old) @@ -135,8 +135,7 @@ abstract class BaseImageAdapter( .let { if (transforms.isNotEmpty()) { it.transform(*transforms.toTypedArray()) - } - else { + } else { it } } @@ -146,7 +145,10 @@ abstract class BaseImageAdapter( } } - suspend fun Context.loadBitmap(link: FileUrl, transforms: List): Bitmap? { + suspend fun Context.loadBitmap( + link: FileUrl, + transforms: List + ): Bitmap? { return tryWithSuspend { val mangaCache = uy.kohesive.injekt.Injekt.get() withContext(Dispatchers.IO) { @@ -161,7 +163,11 @@ abstract class BaseImageAdapter( .diskCacheStrategy(DiskCacheStrategy.NONE) } else { mangaCache.get(link.url)?.let { imageData -> - val bitmap = imageData.fetchAndProcessImage(imageData.page, imageData.source, context = this@loadBitmap) + val bitmap = imageData.fetchAndProcessImage( + imageData.page, + imageData.source, + context = this@loadBitmap + ) it.load(bitmap) .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.NONE) diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt index 3cf0c0e1..8d31e9d1 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ChapterLoaderDialog.kt @@ -14,9 +14,9 @@ import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.R import ani.dantotsu.currActivity import ani.dantotsu.databinding.BottomSheetSelectorBinding -import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaSingleton +import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.others.getSerialized import ani.dantotsu.tryWith import kotlinx.coroutines.Dispatchers @@ -29,8 +29,8 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() { val model: MediaDetailsViewModel by activityViewModels() - private val launch : Boolean by lazy { arguments?.getBoolean("launch", false) ?: false } - private val chp : MangaChapter by lazy { arguments?.getSerialized("next")!! } + private val launch: Boolean by lazy { arguments?.getBoolean("launch", false) ?: false } + private val chp: MangaChapter by lazy { arguments?.getSerialized("next")!! } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { var loaded = false @@ -47,13 +47,21 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() { loaded = true binding.selectorAutoText.text = chp.title lifecycleScope.launch(Dispatchers.IO) { - if(model.loadMangaChapterImages(chp, m.selected!!, m.nameMAL?:m.nameRomaji)) { + if (model.loadMangaChapterImages( + chp, + m.selected!!, + m.nameMAL ?: m.nameRomaji + ) + ) { val activity = currActivity() activity?.runOnUiThread { tryWith { dismiss() } - if(launch) { + if (launch) { MediaSingleton.media = m - val intent = Intent(activity, MangaReaderActivity::class.java)//.apply { putExtra("media", m) } + val intent = Intent( + activity, + MangaReaderActivity::class.java + )//.apply { putExtra("media", m) } activity.startActivity(intent) } } @@ -63,7 +71,11 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetSelectorBinding.inflate(inflater, container, false) val window = dialog?.window window?.statusBarColor = Color.TRANSPARENT diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt index 81ec8548..97651499 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ImageAdapter.kt @@ -30,15 +30,15 @@ open class ImageAdapter( inner class ImageViewHolder(binding: ItemImageBinding) : RecyclerView.ViewHolder(binding.root) - open suspend fun loadBitmap(position: Int, parent: View) : Bitmap? { + open suspend fun loadBitmap(position: Int, parent: View): Bitmap? { val link = images.getOrNull(position)?.url ?: return null if (link.url.isEmpty()) return null val transforms = mutableListOf() val parserTransformation = activity.getTransformation(images[position]) - if(parserTransformation!=null) transforms.add(parserTransformation) - if(settings.cropBorders) { + if (parserTransformation != null) transforms.add(parserTransformation) + if (settings.cropBorders) { transforms.add(RemoveBordersTransformation(true, settings.cropBorderThreshold)) transforms.add(RemoveBordersTransformation(false, settings.cropBorderThreshold)) } @@ -47,7 +47,8 @@ open class ImageAdapter( } override suspend fun loadImage(position: Int, parent: View): Boolean { - val imageView = parent.findViewById(R.id.imgProgImageNoGestures) ?: return false + val imageView = parent.findViewById(R.id.imgProgImageNoGestures) + ?: return false val progress = parent.findViewById(R.id.imgProgProgress) ?: return false imageView.recycle() imageView.visibility = View.GONE @@ -60,10 +61,12 @@ open class ImageAdapter( if (settings.layout != PAGED) parent.updateLayoutParams { if (settings.direction != LEFT_TO_RIGHT && settings.direction != RIGHT_TO_LEFT) { - sHeight = if (settings.wrapImages) bitmap.height else (sWidth * bitmap.height * 1f / bitmap.width).toInt() + sHeight = + if (settings.wrapImages) bitmap.height else (sWidth * bitmap.height * 1f / bitmap.width).toInt() height = sHeight } else { - sWidth = if (settings.wrapImages) bitmap.width else (sHeight * bitmap.width * 1f / bitmap.height).toInt() + sWidth = + if (settings.wrapImages) bitmap.width else (sHeight * bitmap.width * 1f / bitmap.height).toInt() width = sWidth } } @@ -73,7 +76,8 @@ open class ImageAdapter( val parentArea = sWidth * sHeight * 1f val bitmapArea = bitmap.width * bitmap.height * 1f - val scale = if (parentArea < bitmapArea) (bitmapArea / parentArea) else (parentArea / bitmapArea) + val scale = + if (parentArea < bitmapArea) (bitmapArea / parentArea) else (parentArea / bitmapArea) imageView.maxScale = scale * 1.1f imageView.minScale = scale 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 a5682716..f39497a1 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 @@ -3,6 +3,7 @@ package ani.dantotsu.media.manga.mangareader import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.app.AlertDialog +import android.content.Intent import android.content.res.Configuration import android.graphics.Bitmap import android.os.Build @@ -25,6 +26,8 @@ import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.* import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.discord.Discord +import ani.dantotsu.connections.discord.DiscordService +import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton import ani.dantotsu.connections.discord.RPC import ani.dantotsu.connections.updateProgress import ani.dantotsu.databinding.ActivityMangaReaderBinding @@ -35,7 +38,7 @@ import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaNameAdapter import ani.dantotsu.others.ImageViewDialog -import ani.dantotsu.others.getSerialized +import ani.dantotsu.others.LangSet import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.MangaImage import ani.dantotsu.parsers.MangaSources @@ -46,7 +49,6 @@ import ani.dantotsu.settings.CurrentReaderSettings.Layouts.* import ani.dantotsu.settings.ReaderSettings import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet import com.alexvasilkov.gestures.views.GestureFrameLayout import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView @@ -54,8 +56,6 @@ import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -94,14 +94,17 @@ class MangaReaderActivity : AppCompatActivity() { var sliding = false var isAnimating = false - private var rpc : RPC? = null + private var rpc: RPC? = null override fun onAttachedToWindow() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !settings.showSystemBars) { val displayCutout = window.decorView.rootWindowInsets.displayCutout if (displayCutout != null) { if (displayCutout.boundingRects.size > 0) { - notchHeight = min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height()) + notchHeight = min( + displayCutout.boundingRects[0].width(), + displayCutout.boundingRects[0].height() + ) checkNotch() } } @@ -121,14 +124,18 @@ class MangaReaderActivity : AppCompatActivity() { override fun onDestroy() { mangaCache.clear() - rpc?.close() + if (isOnline(baseContext)) { //TODO: + DiscordServiceRunningSingleton.running = false + val stopIntent = Intent(this, DiscordService::class.java) + stopService(stopIntent) + } super.onDestroy() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityMangaReaderBinding.inflate(layoutInflater) setContentView(binding.root) @@ -140,8 +147,14 @@ ThemeManager(this).applyTheme() progress { finish() } } - settings = loadData("reader_settings", this) ?: ReaderSettings().apply { saveData("reader_settings", this) } - uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } + settings = loadData("reader_settings", this) + ?: ReaderSettings().apply { saveData("reader_settings", this) } + uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply { + saveData( + "ui_settings", + this + ) + } controllerDuration = (uiSettings.animationSpeed * 200).toLong() hideBars() @@ -166,9 +179,11 @@ ThemeManager(this).applyTheme() if (fromUser) { sliding = true if (settings.default.layout != PAGED) - binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 } ?: 1)) + binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 } + ?: 1)) else - binding.mangaReaderPager.currentItem = (value.toInt() - 1) / (dualPage { 2 } ?: 1) + binding.mangaReaderPager.currentItem = + (value.toInt() - 1) / (dualPage { 2 } ?: 1) pageSliderHide() } } @@ -194,25 +209,25 @@ ThemeManager(this).applyTheme() model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources binding.mangaReaderSource.visibility = if (settings.showSource) View.VISIBLE else View.GONE - if(model.mangaReadSources!!.names.isEmpty()){ + if (model.mangaReadSources!!.names.isEmpty()) { //try to reload sources try { - if (media.isAdult) { - val mangaSources = MangaSources - val scope = lifecycleScope - scope.launch(Dispatchers.IO) { - mangaSources.init(Injekt.get().installedExtensionsFlow) + if (media.isAdult) { + val mangaSources = MangaSources + val scope = lifecycleScope + scope.launch(Dispatchers.IO) { + mangaSources.init(Injekt.get().installedExtensionsFlow) + } + model.mangaReadSources = mangaSources + } else { + val mangaSources = HMangaSources + val scope = lifecycleScope + scope.launch(Dispatchers.IO) { + mangaSources.init(Injekt.get().installedExtensionsFlow) + } + model.mangaReadSources = mangaSources } - model.mangaReadSources = mangaSources - }else{ - val mangaSources = HMangaSources - val scope = lifecycleScope - scope.launch(Dispatchers.IO) { - mangaSources.init(Injekt.get().installedExtensionsFlow) - } - model.mangaReadSources = mangaSources - } - }catch (e: Exception){ + } catch (e: Exception) { Firebase.crashlytics.recordException(e) logError(e) } @@ -221,7 +236,8 @@ ThemeManager(this).applyTheme() if (media.selected!!.sourceIndex >= model.mangaReadSources!!.names.size) { media.selected!!.sourceIndex = 0 } - binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.sourceIndex] + binding.mangaReaderSource.text = + model.mangaReadSources!!.names[media.selected!!.sourceIndex] binding.mangaReaderTitle.text = media.userPreferredName @@ -234,39 +250,49 @@ ThemeManager(this).applyTheme() chaptersTitleArr.add("${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") "" else "Chapter "}${chapter.number}${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") " : " + chapter.title else ""}") } - showProgressDialog = if (settings.askIndividual) loadData("${media.id}_progressDialog") != true else false + showProgressDialog = + if (settings.askIndividual) loadData("${media.id}_progressDialog") != true else false progressDialog = if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true) - AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.title_update_progress)).apply { - setMultiChoiceItems( - arrayOf(getString(R.string.dont_ask_again, media.userPreferredName)), - booleanArrayOf(false) - ) { _, _, isChecked -> - if (isChecked) progressDialog = null - saveData("${media.id}_progressDialog", isChecked) - showProgressDialog = isChecked + AlertDialog.Builder(this, R.style.MyPopup) + .setTitle(getString(R.string.title_update_progress)).apply { + setMultiChoiceItems( + arrayOf(getString(R.string.dont_ask_again, media.userPreferredName)), + booleanArrayOf(false) + ) { _, _, isChecked -> + if (isChecked) progressDialog = null + saveData("${media.id}_progressDialog", isChecked) + showProgressDialog = isChecked + } + setOnCancelListener { hideBars() } } - setOnCancelListener { hideBars() } - } else null //Chapter Change fun change(index: Int) { mangaCache.clear() saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this) - ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!).show(supportFragmentManager, "dialog") + ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!) + .show(supportFragmentManager, "dialog") } //ChapterSelector - binding.mangaReaderChapterSelect.adapter = NoPaddingArrayAdapter(this, R.layout.item_dropdown, chaptersTitleArr) + binding.mangaReaderChapterSelect.adapter = + NoPaddingArrayAdapter(this, R.layout.item_dropdown, chaptersTitleArr) binding.mangaReaderChapterSelect.setSelection(currentChapterIndex) - binding.mangaReaderChapterSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { - if (position != currentChapterIndex) change(position) - } + binding.mangaReaderChapterSelect.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + p0: AdapterView<*>?, + p1: View?, + position: Int, + p3: Long + ) { + if (position != currentChapterIndex) change(position) + } - override fun onNothingSelected(parent: AdapterView<*>) {} - } + override fun onNothingSelected(parent: AdapterView<*>) {} + } binding.mangaReaderSettings.setSafeOnClickListener { ReaderSettingsDialogFragment.newInstance().show(supportFragmentManager, "settings") @@ -297,40 +323,69 @@ ThemeManager(this).applyTheme() saveData("${media.id}_current_chp", chap.number, this) currentChapterIndex = chaptersArr.indexOf(chap.number) binding.mangaReaderChapterSelect.setSelection(currentChapterIndex) - 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() - rpc?.close() - rpc = Discord.defaultRPC() - rpc?.send { - type = RPC.Type.WATCHING - activityName = media.userPreferredName - details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number) - state = "${chap.number}/${media.manga?.totalChapters ?: "??"}" - media.cover?.let { cover -> - largeImage = RPC.Link(media.userPreferredName, cover) - } - media.shareLink?.let { link -> - buttons.add(0, RPC.Link(getString(R.string.view_manga), link)) + val context = this + if (isOnline(context)) { + lifecycleScope.launch { + val presence = RPC.createPresence( + RPC.Companion.RPCData( + applicationId = Discord.application_Id, + type = RPC.Type.WATCHING, + activityName = media.userPreferredName, + 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) + }, + smallImage = RPC.Link( + "Dantotsu", + Discord.small_Image + ), + buttons = mutableListOf( + RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""), + RPC.Link( + "Stream on Dantotsu", + "https://github.com/rebelonion/Dantotsu/" + ) + ) + ) + ) + val intent = Intent(context, DiscordService::class.java).apply { + putExtra("presence", presence) + } + DiscordServiceRunningSingleton.running = true + startService(intent) } } } } - scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!, media.nameMAL?:media.nameRomaji) } + scope.launch(Dispatchers.IO) { + model.loadMangaChapterImages( + chapter, + media.selected!!, + media.nameMAL ?: media.nameRomaji + ) + } } private val snapHelper = PagerSnapHelper() fun dualPage(callback: () -> T): T? { return when (settings.default.dualPageMode) { - No -> null + No -> null Automatic -> { val orientation = resources.configuration.orientation if (orientation == Configuration.ORIENTATION_LANDSCAPE) callback.invoke() else null } - Force -> callback.invoke() + + Force -> callback.invoke() } } @@ -361,7 +416,8 @@ ThemeManager(this).applyTheme() maxChapterPage = chapImages.size.toLong() saveData("${media.id}_${chapter.number}_max", maxChapterPage) - imageAdapter = dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter) + imageAdapter = + dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter) if (chapImages.size > 1) { binding.mangaReaderSlider.apply { @@ -382,8 +438,10 @@ ThemeManager(this).applyTheme() if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)) { binding.mangaReaderSwipy.vertical = true if (settings.default.direction == TOP_TO_BOTTOM) { - binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) - binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) + binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) + ?: getString(R.string.no_chapter) + binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) + ?: getString(R.string.no_chapter) binding.mangaReaderSwipy.onTopSwiped = { binding.mangaReaderPreviousChapter.performClick() } @@ -391,8 +449,10 @@ ThemeManager(this).applyTheme() binding.mangaReaderNextChapter.performClick() } } else { - binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) - binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) + binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) + ?: getString(R.string.no_chapter) + binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) + ?: getString(R.string.no_chapter) binding.mangaReaderSwipy.onTopSwiped = { binding.mangaReaderNextChapter.performClick() } @@ -415,8 +475,10 @@ ThemeManager(this).applyTheme() } else { binding.mangaReaderSwipy.vertical = false if (settings.default.direction == RIGHT_TO_LEFT) { - binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) - binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) + binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) + ?: getString(R.string.no_chapter) + binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) + ?: getString(R.string.no_chapter) binding.mangaReaderSwipy.onLeftSwiped = { binding.mangaReaderNextChapter.performClick() } @@ -424,8 +486,10 @@ ThemeManager(this).applyTheme() binding.mangaReaderPreviousChapter.performClick() } } else { - binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) - binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) + binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) + ?: getString(R.string.no_chapter) + binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) + ?: getString(R.string.no_chapter) binding.mangaReaderSwipy.onLeftSwiped = { binding.mangaReaderPreviousChapter.performClick() } @@ -450,7 +514,8 @@ ThemeManager(this).applyTheme() if (settings.default.layout != PAGED) { binding.mangaReaderRecyclerContainer.visibility = View.VISIBLE - binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled = settings.default.rotation + binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled = + settings.default.rotation val detector = GestureDetectorCompat(this, object : GesturesListener() { override fun onLongPress(e: MotionEvent) { @@ -458,18 +523,31 @@ ThemeManager(this).applyTheme() child ?: return@let false val pos = binding.mangaReaderRecycler.getChildAdapterPosition(child) val callback: (ImageViewDialog) -> Unit = { dialog -> - lifecycleScope.launch { imageAdapter?.loadImage(pos, child as GestureFrameLayout) } - binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + lifecycleScope.launch { + imageAdapter?.loadImage( + pos, + child as GestureFrameLayout + ) + } + binding.mangaReaderRecycler.performHapticFeedback( + HapticFeedbackConstants.LONG_PRESS + ) dialog.dismiss() } dualPage { - val page = chapter.dualPages().getOrNull(pos) ?: return@dualPage false + val page = + chapter.dualPages().getOrNull(pos) ?: return@dualPage false val nextPage = page.second if (settings.default.direction != LEFT_TO_RIGHT && nextPage != null) onImageLongClicked(pos * 2, nextPage, page.first, callback) else onImageLongClicked(pos * 2, page.first, nextPage, callback) - } ?: onImageLongClicked(pos, chapImages.getOrNull(pos) ?: return@let false, null, callback) + } ?: onImageLongClicked( + pos, + chapImages.getOrNull(pos) ?: return@let false, + null, + callback + ) } ) binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) super.onLongPress(e) @@ -511,12 +589,16 @@ ThemeManager(this).applyTheme() && (!v.canScrollVertically(-1) || !v.canScrollVertically(1))) || ((direction == LEFT_TO_RIGHT || direction == RIGHT_TO_LEFT) - && (!v.canScrollHorizontally(-1) || !v.canScrollHorizontally(1))) + && (!v.canScrollHorizontally(-1) || !v.canScrollHorizontally( + 1 + ))) ) { handleController(true) } else handleController(false) } - updatePageNumber(manager.findLastVisibleItemPosition().toLong() * (dualPage { 2 } ?: 1) + 1) + updatePageNumber( + manager.findLastVisibleItemPosition().toLong() * (dualPage { 2 } + ?: 1) + 1) super.onScrolled(v, dx, dy) } }) @@ -578,7 +660,7 @@ ThemeManager(this).applyTheme() private var onVolumeDown: (() -> Unit)? = null override fun dispatchKeyEvent(event: KeyEvent): Boolean { return when (event.keyCode) { - KEYCODE_VOLUME_UP, KEYCODE_DPAD_UP, KEYCODE_PAGE_UP -> { + KEYCODE_VOLUME_UP, KEYCODE_DPAD_UP, KEYCODE_PAGE_UP -> { if (event.keyCode == KEYCODE_VOLUME_UP) if (!settings.default.volumeButtons) return false @@ -587,6 +669,7 @@ ThemeManager(this).applyTheme() true } else false } + KEYCODE_VOLUME_DOWN, KEYCODE_DPAD_DOWN, KEYCODE_PAGE_DOWN -> { if (event.keyCode == KEYCODE_VOLUME_DOWN) if (!settings.default.volumeButtons) @@ -596,7 +679,8 @@ ThemeManager(this).applyTheme() true } else false } - else -> { + + else -> { super.dispatchKeyEvent(event) } } @@ -670,8 +754,14 @@ ThemeManager(this).applyTheme() isContVisible = false if (!isAnimating) { isAnimating = true - ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f).setDuration(controllerDuration).start() - ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 0f, 128f) + ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f) + .setDuration(controllerDuration).start() + 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() } @@ -680,7 +770,8 @@ ThemeManager(this).applyTheme() } else { isContVisible = true binding.mangaReaderCont.visibility = View.VISIBLE - ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 0f, 1f).setDuration(controllerDuration).start() + ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 0f, 1f) + .setDuration(controllerDuration).start() ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", -128f, 0f) .apply { interpolator = overshoot;duration = controllerDuration;start() } ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 128f, 0f) @@ -706,7 +797,7 @@ ThemeManager(this).applyTheme() model.loadMangaChapterImages( chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!, media.selected!!, - media.nameMAL?:media.nameRomaji, + media.nameMAL ?: media.nameRomaji, false ) loading = false @@ -719,7 +810,11 @@ ThemeManager(this).applyTheme() progressDialog?.setCancelable(false) ?.setPositiveButton(getString(R.string.yes)) { dialog, _ -> saveData("${media.id}_save_progress", true) - updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString()) + updateProgress( + media, + MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!) + .toString() + ) dialog.dismiss() runnable.run() } @@ -731,7 +826,11 @@ ThemeManager(this).applyTheme() progressDialog?.show() } else { if (loadData("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true) - updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString()) + updateProgress( + media, + MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!) + .toString() + ) runnable.run() } } else { diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/PreloadLinearLayoutManager.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/PreloadLinearLayoutManager.kt index 3a9792a3..7609149f 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/PreloadLinearLayoutManager.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/PreloadLinearLayoutManager.kt @@ -10,14 +10,15 @@ import kotlin.math.max class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean) : LinearLayoutManager(context, orientation, reverseLayout) { - private val mOrientationHelper: OrientationHelper = OrientationHelper.createOrientationHelper(this, orientation) + private val mOrientationHelper: OrientationHelper = + OrientationHelper.createOrientationHelper(this, orientation) /** * As [LinearLayoutManager.collectAdjacentPrefetchPositions] will prefetch one view for us, * we only need to prefetch additional ones. */ var preloadItemCount = 1 - set(count){ + set(count) { require(count >= 1) { "preloadItemCount must not be smaller than 1!" } field = count - 1 } @@ -37,7 +38,8 @@ class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayo val currentPosition: Int = getPosition(child ?: return) + layoutDirection if (layoutDirection == 1) { - val scrollingOffset = (mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.endAfterPadding) + val scrollingOffset = + (mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.endAfterPadding) ((currentPosition + 1) until (currentPosition + preloadItemCount + 1)).forEach { if (it >= 0 && it < state.itemCount) { layoutPrefetchRegistry.addPosition(it, max(0, scrollingOffset)) diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ReaderSettingsDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ReaderSettingsDialogFragment.kt index ec27ecae..fc399e19 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/ReaderSettingsDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/ReaderSettingsDialogFragment.kt @@ -14,7 +14,11 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() { private var _binding: BottomSheetCurrentReaderSettingsBinding? = null private val binding get() = _binding!! - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetCurrentReaderSettingsBinding.inflate(inflater, container, false) return binding.root } @@ -24,11 +28,14 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() { val activity = requireActivity() as MangaReaderActivity val settings = activity.settings.default - binding.readerDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal] + binding.readerDirectionText.text = + resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal] binding.readerDirection.rotation = 90f * (settings.direction.ordinal) binding.readerDirection.setOnClickListener { - settings.direction = Directions[settings.direction.ordinal + 1] ?: Directions.TOP_TO_BOTTOM - binding.readerDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal] + settings.direction = + Directions[settings.direction.ordinal + 1] ?: Directions.TOP_TO_BOTTOM + binding.readerDirectionText.text = + resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal] binding.readerDirection.rotation = 90f * (settings.direction.ordinal) activity.applySettings() } @@ -39,36 +46,39 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() { binding.readerContinuous ) - binding.readerPadding.isEnabled = settings.layout.ordinal!=0 - fun paddingAvailable(enable:Boolean){ + binding.readerPadding.isEnabled = settings.layout.ordinal != 0 + fun paddingAvailable(enable: Boolean) { binding.readerPadding.isEnabled = enable } binding.readerPadding.isChecked = settings.padding - binding.readerPadding.setOnCheckedChangeListener { _,isChecked -> + binding.readerPadding.setOnCheckedChangeListener { _, isChecked -> settings.padding = isChecked activity.applySettings() } binding.readerCropBorders.isChecked = settings.cropBorders - binding.readerCropBorders.setOnCheckedChangeListener { _,isChecked -> + binding.readerCropBorders.setOnCheckedChangeListener { _, isChecked -> settings.cropBorders = isChecked activity.applySettings() } - binding.readerLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal] + binding.readerLayoutText.text = + resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal] var selected = list[settings.layout.ordinal] selected.alpha = 1f - list.forEachIndexed { index , imageButton -> + list.forEachIndexed { index, imageButton -> imageButton.setOnClickListener { selected.alpha = 0.33f selected = imageButton selected.alpha = 1f - settings.layout = CurrentReaderSettings.Layouts[index]?:CurrentReaderSettings.Layouts.CONTINUOUS - binding.readerLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal] + settings.layout = + CurrentReaderSettings.Layouts[index] ?: CurrentReaderSettings.Layouts.CONTINUOUS + binding.readerLayoutText.text = + resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal] activity.applySettings() - paddingAvailable(settings.layout.ordinal!=0) + paddingAvailable(settings.layout.ordinal != 0) } } @@ -87,7 +97,8 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() { selectedDual.alpha = 0.33f selectedDual = imageButton selectedDual.alpha = 1f - settings.dualPageMode = CurrentReaderSettings.DualPageModes[index] ?: CurrentReaderSettings.DualPageModes.Automatic + settings.dualPageMode = CurrentReaderSettings.DualPageModes[index] + ?: CurrentReaderSettings.DualPageModes.Automatic binding.readerDualPageText.text = settings.dualPageMode.toString() activity.applySettings() } @@ -111,37 +122,37 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() { } binding.readerKeepScreenOn.isChecked = settings.keepScreenOn - binding.readerKeepScreenOn.setOnCheckedChangeListener { _,isChecked -> + binding.readerKeepScreenOn.setOnCheckedChangeListener { _, isChecked -> settings.keepScreenOn = isChecked activity.applySettings() } binding.readerHidePageNumbers.isChecked = settings.hidePageNumbers - binding.readerHidePageNumbers.setOnCheckedChangeListener { _,isChecked -> + binding.readerHidePageNumbers.setOnCheckedChangeListener { _, isChecked -> settings.hidePageNumbers = isChecked activity.applySettings() } binding.readerOverscroll.isChecked = settings.overScrollMode - binding.readerOverscroll.setOnCheckedChangeListener { _,isChecked -> + binding.readerOverscroll.setOnCheckedChangeListener { _, isChecked -> settings.overScrollMode = isChecked activity.applySettings() } binding.readerVolumeButton.isChecked = settings.volumeButtons - binding.readerVolumeButton.setOnCheckedChangeListener { _,isChecked -> + binding.readerVolumeButton.setOnCheckedChangeListener { _, isChecked -> settings.volumeButtons = isChecked activity.applySettings() } binding.readerWrapImage.isChecked = settings.wrapImages - binding.readerWrapImage.setOnCheckedChangeListener { _,isChecked -> + binding.readerWrapImage.setOnCheckedChangeListener { _, isChecked -> settings.wrapImages = isChecked activity.applySettings() } binding.readerLongClickImage.isChecked = settings.longClickImage - binding.readerLongClickImage.setOnCheckedChangeListener { _,isChecked -> + binding.readerLongClickImage.setOnCheckedChangeListener { _, isChecked -> settings.longClickImage = isChecked activity.applySettings() } @@ -152,7 +163,7 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() { super.onDestroy() } - companion object{ + companion object { fun newInstance() = ReaderSettingsDialogFragment() } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/RemoveBordersTransformation.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/RemoveBordersTransformation.kt index e90c80bf..223f2760 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/RemoveBordersTransformation.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/RemoveBordersTransformation.kt @@ -6,7 +6,8 @@ import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import java.security.MessageDigest -class RemoveBordersTransformation(private val white:Boolean, private val threshHold:Int) : BitmapTransformation() { +class RemoveBordersTransformation(private val white: Boolean, private val threshHold: Int) : + BitmapTransformation() { override fun transform( pool: BitmapPool, @@ -95,6 +96,6 @@ class RemoveBordersTransformation(private val white:Boolean, private val threshH private fun isPixelNotWhite(pixel: Int): Boolean { val brightness = Color.red(pixel) + Color.green(pixel) + Color.blue(pixel) - return if(white) brightness < (255-threshHold) else brightness > threshHold + return if (white) brightness < (255 - threshHold) else brightness > threshHold } } diff --git a/app/src/main/java/ani/dantotsu/media/manga/mangareader/Swipy.kt b/app/src/main/java/ani/dantotsu/media/manga/mangareader/Swipy.kt index bab71149..be481c6c 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/mangareader/Swipy.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/mangareader/Swipy.kt @@ -13,7 +13,7 @@ class Swipy @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : FrameLayout(context, attrs) { - var dragDivider : Int = 5 + var dragDivider: Int = 5 var vertical = true //public, in case a different sub child needs to be considered @@ -100,7 +100,7 @@ class Swipy @JvmOverloads constructor( } when (action) { - MotionEvent.ACTION_DOWN -> { + MotionEvent.ACTION_DOWN -> { activePointerId = ev.getPointerId(0) isBeingDragged = false pointerIndex = ev.findPointerIndex(activePointerId) @@ -109,7 +109,8 @@ class Swipy @JvmOverloads constructor( } initialDown = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex) } - MotionEvent.ACTION_MOVE -> { + + MotionEvent.ACTION_MOVE -> { if (activePointerId == INVALID_POINTER) { //("Got ACTION_MOVE event but don't have an active pointer id.") return false @@ -121,7 +122,8 @@ class Swipy @JvmOverloads constructor( val pos = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex) startDragging(pos) } - MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev) + + MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev) MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { isBeingDragged = false activePointerId = INVALID_POINTER @@ -138,11 +140,12 @@ class Swipy @JvmOverloads constructor( return false } when (action) { - MotionEvent.ACTION_DOWN -> { + MotionEvent.ACTION_DOWN -> { activePointerId = ev.getPointerId(0) isBeingDragged = false } - MotionEvent.ACTION_MOVE -> { + + MotionEvent.ACTION_MOVE -> { pointerIndex = ev.findPointerIndex(activePointerId) if (pointerIndex < 0) { //("Got ACTION_MOVE event but have an invalid active pointer id.") @@ -160,16 +163,16 @@ class Swipy @JvmOverloads constructor( if (overscroll > 0) { parent.requestDisallowInterceptTouchEvent(true) - if (vertical){ - val totalDragDistance = Resources.getSystem().displayMetrics.heightPixels / dragDivider + if (vertical) { + val totalDragDistance = + Resources.getSystem().displayMetrics.heightPixels / dragDivider if (verticalPos == VerticalPosition.Top) topBeingSwiped.invoke(overscroll / totalDragDistance) else bottomBeingSwiped.invoke(overscroll / totalDragDistance) - } - - else { - val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider + } else { + val totalDragDistance = + Resources.getSystem().displayMetrics.widthPixels / dragDivider if (horizontalPos == HorizontalPosition.Left) leftBeingSwiped.invoke(overscroll / totalDragDistance) else @@ -180,6 +183,7 @@ class Swipy @JvmOverloads constructor( } } } + MotionEvent.ACTION_POINTER_DOWN -> { pointerIndex = ev.actionIndex if (pointerIndex < 0) { @@ -188,8 +192,9 @@ class Swipy @JvmOverloads constructor( } activePointerId = ev.getPointerId(pointerIndex) } - MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev) - MotionEvent.ACTION_UP -> { + + MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev) + MotionEvent.ACTION_UP -> { if (vertical) { topBeingSwiped.invoke(0f) bottomBeingSwiped.invoke(0f) @@ -216,7 +221,8 @@ class Swipy @JvmOverloads constructor( activePointerId = INVALID_POINTER return false } - MotionEvent.ACTION_CANCEL -> return false + + MotionEvent.ACTION_CANCEL -> return false } return true } @@ -235,21 +241,20 @@ class Swipy @JvmOverloads constructor( private fun finishSpinner(overscrollDistance: Float) { - if (vertical) { - val totalDragDistance = Resources.getSystem().displayMetrics.heightPixels / dragDivider - if (overscrollDistance > totalDragDistance) - if (verticalPos == VerticalPosition.Top) - onTopSwiped.invoke() - else - onBottomSwiped.invoke() - } - else { - val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider - if (overscrollDistance > totalDragDistance) + if (vertical) { + val totalDragDistance = Resources.getSystem().displayMetrics.heightPixels / dragDivider + if (overscrollDistance > totalDragDistance) + if (verticalPos == VerticalPosition.Top) + onTopSwiped.invoke() + else + onBottomSwiped.invoke() + } else { + val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider + if (overscrollDistance > totalDragDistance) if (horizontalPos == HorizontalPosition.Left) onLeftSwiped.invoke() else onRightSwiped.invoke() - } + } } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/novel/BookDialog.kt b/app/src/main/java/ani/dantotsu/media/novel/BookDialog.kt index 68c49cc2..163275a6 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/BookDialog.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/BookDialog.kt @@ -13,8 +13,6 @@ import ani.dantotsu.loadImage import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.others.getSerialized import ani.dantotsu.parsers.ShowResponse -import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -25,9 +23,18 @@ class BookDialog : BottomSheetDialogFragment() { private val viewModel by activityViewModels() - private lateinit var novelName:String + private lateinit var novelName: String private lateinit var novel: ShowResponse - private var source:Int = 0 + private var source: Int = 0 + + interface Callback { + fun onDownloadTriggered(link: String) + } + + private var callback: Callback? = null + fun setCallback(callback: Callback) { + this.callback = callback + } override fun onCreate(savedInstanceState: Bundle?) { arguments?.let { @@ -38,7 +45,11 @@ class BookDialog : BottomSheetDialogFragment() { super.onCreate(savedInstanceState) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetBookBinding.inflate(inflater, container, false) return binding.root } @@ -47,11 +58,11 @@ class BookDialog : BottomSheetDialogFragment() { binding.bookRecyclerView.layoutManager = LinearLayoutManager(requireContext()) viewModel.book.observe(viewLifecycleOwner) { - if(it!=null){ + if (it != null) { binding.itemBookTitle.text = it.name binding.itemBookDesc.text = it.description binding.itemBookImage.loadImage(it.img) - binding.bookRecyclerView.adapter = UrlAdapter(it.links, it, novelName) + binding.bookRecyclerView.adapter = UrlAdapter(it.links, it, novelName, callback) } } lifecycleScope.launch(Dispatchers.IO) { @@ -65,7 +76,7 @@ class BookDialog : BottomSheetDialogFragment() { } companion object { - fun newInstance(novelName:String, novel:ShowResponse, source: Int) : BookDialog{ + fun newInstance(novelName: String, novel: ShowResponse, source: Int): BookDialog { val bundle = Bundle().apply { putString("novelName", novelName) putInt("source", source) diff --git a/app/src/main/java/ani/dantotsu/media/novel/NovelReadAdapter.kt b/app/src/main/java/ani/dantotsu/media/novel/NovelReadAdapter.kt index dd7a79a1..7f9d34f4 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelReadAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelReadAdapter.kt @@ -22,12 +22,14 @@ class NovelReadAdapter( var progress: View? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NovelReadAdapter.ViewHolder { - val binding = ItemNovelHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemNovelHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) progress = binding.progress.root return ViewHolder(binding) } - private val imm = fragment.requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + private val imm = fragment.requireContext() + .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager override fun onBindViewHolder(holder: ViewHolder, position: Int) { val binding = holder.binding @@ -35,7 +37,8 @@ class NovelReadAdapter( fun search(): Boolean { val query = binding.searchBarText.text.toString() - val source = media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it } + val source = + media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it } fragment.source = source binding.searchBarText.clearFocus() @@ -44,11 +47,18 @@ class NovelReadAdapter( return true } - val source = media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it } + val source = + media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it } if (novelReadSources.names.isNotEmpty() && source in 0 until novelReadSources.names.size) { binding.animeSource.setText(novelReadSources.names[source], false) } - binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, novelReadSources.names)) + binding.animeSource.setAdapter( + ArrayAdapter( + fragment.requireContext(), + R.layout.item_dropdown, + novelReadSources.names + ) + ) binding.animeSource.setOnItemClickListener { _, _, i, _ -> fragment.onSourceChange(i) search() @@ -58,13 +68,14 @@ class NovelReadAdapter( binding.searchBarText.setOnEditorActionListener { _, actionId, _ -> return@setOnEditorActionListener when (actionId) { IME_ACTION_SEARCH -> search() - else -> false + else -> false } } binding.searchBar.setEndIconOnClickListener { search() } } - override fun getItemCount(): Int = 0 + override fun getItemCount(): Int = 1 - inner class ViewHolder(val binding: ItemNovelHeaderBinding) : RecyclerView.ViewHolder(binding.root) + inner class ViewHolder(val binding: ItemNovelHeaderBinding) : + RecyclerView.ViewHolder(binding.root) } \ No newline at end of file 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 e6c62b24..7154412a 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelReadFragment.kt @@ -1,12 +1,20 @@ package ani.dantotsu.media.novel +import android.content.BroadcastReceiver +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 +import android.util.Log 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 @@ -14,16 +22,29 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.databinding.FragmentAnimeWatchBinding +import ani.dantotsu.download.Download +import ani.dantotsu.download.DownloadsManager +import ani.dantotsu.download.novel.NovelDownloaderService +import ani.dantotsu.download.novel.NovelServiceDataSingleton import ani.dantotsu.loadData import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsViewModel +import ani.dantotsu.media.novel.novelreader.NovelReaderActivity import ani.dantotsu.navBarHeight +import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.saveData import ani.dantotsu.settings.UserInterfaceSettings +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers 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() { +class NovelReadFragment : Fragment(), + DownloadTriggerCallback, + DownloadedCheckCallback { private var _binding: FragmentAnimeWatchBinding? = null private val binding get() = _binding!! @@ -40,11 +61,141 @@ class NovelReadFragment : Fragment() { private var continueEp: Boolean = false var loaded = false - val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } + val uiSettings = loadData("ui_settings", toast = false) + ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } + override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) { + Log.e("downloadTrigger", novelDownloadPackage.link) + val downloadTask = NovelDownloaderService.DownloadTask( + title = media.nameMAL ?: media.nameRomaji, + chapter = novelDownloadPackage.novelName, + downloadLink = novelDownloadPackage.link, + originalLink = novelDownloadPackage.originalLink, + sourceMedia = media, + coverUrl = novelDownloadPackage.coverUrl, + retries = 2, + ) + NovelServiceDataSingleton.downloadQueue.offer(downloadTask) + CoroutineScope(Dispatchers.IO).launch { + if (!NovelServiceDataSingleton.isServiceRunning) { + val intent = Intent(context, NovelDownloaderService::class.java) + withContext(Dispatchers.Main) { + ContextCompat.startForegroundService(requireContext(), intent) + } + NovelServiceDataSingleton.isServiceRunning = true + } + + } + } + + override fun downloadedCheckWithStart(novel: ShowResponse): Boolean { + val downloadsManager = Injekt.get() + if (downloadsManager.queryDownload( + Download( + media.nameMAL ?: media.nameRomaji, + novel.name, + Download.Type.NOVEL + ) + ) + ) { + val file = File( + context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "${DownloadsManager.novelLocation}/${media.nameMAL ?: media.nameRomaji}/${novel.name}/0.epub" + ) + if (!file.exists()) return false + val fileUri = FileProvider.getUriForFile( + requireContext(), + "${requireContext().packageName}.provider", + file + ) + val intent = Intent(context, NovelReaderActivity::class.java).apply { + action = Intent.ACTION_VIEW + setDataAndType(fileUri, "application/epub+zip") + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + startActivity(intent) + return true + } else { + return false + } + } + + override fun downloadedCheck(novel: ShowResponse): Boolean { + val downloadsManager = Injekt.get() + return downloadsManager.queryDownload( + Download( + media.nameMAL ?: media.nameRomaji, + novel.name, + Download.Type.NOVEL + ) + ) + } + + override fun deleteDownload(novel: ShowResponse) { + val downloadsManager = Injekt.get() + downloadsManager.removeDownload( + Download( + media.nameMAL ?: media.nameRomaji, + novel.name, + Download.Type.NOVEL + ) + ) + } + + private val downloadStatusReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (!this@NovelReadFragment::novelResponseAdapter.isInitialized) return + when (intent.action) { + ACTION_DOWNLOAD_STARTED -> { + val link = intent.getStringExtra(EXTRA_NOVEL_LINK) + link?.let { + novelResponseAdapter.startDownload(it) + } + } + + ACTION_DOWNLOAD_FINISHED -> { + val link = intent.getStringExtra(EXTRA_NOVEL_LINK) + link?.let { + novelResponseAdapter.stopDownload(it) + } + } + + ACTION_DOWNLOAD_FAILED -> { + val link = intent.getStringExtra(EXTRA_NOVEL_LINK) + link?.let { + novelResponseAdapter.purgeDownload(it) + } + } + + ACTION_DOWNLOAD_PROGRESS -> { + val link = intent.getStringExtra(EXTRA_NOVEL_LINK) + val progress = intent.getIntExtra("progress", 0) + link?.let { + novelResponseAdapter.updateDownloadProgress(it, progress) + } + } + } + } + } + + var response: List? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val intentFilter = IntentFilter().apply { + addAction(ACTION_DOWNLOAD_STARTED) + addAction(ACTION_DOWNLOAD_FINISHED) + addAction(ACTION_DOWNLOAD_FAILED) + addAction(ACTION_DOWNLOAD_PROGRESS) + } + + ContextCompat.registerReceiver( + requireContext(), + downloadStatusReceiver, + intentFilter, + ContextCompat.RECEIVER_EXPORTED + ) + binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.animeSourceRecycler.layoutManager = LinearLayoutManager(requireContext()) @@ -63,8 +214,13 @@ class NovelReadFragment : Fragment() { val sel = media.selected searchQuery = sel?.server ?: media.name ?: media.nameRomaji headerAdapter = NovelReadAdapter(media, this, model.novelSources) - novelResponseAdapter = NovelResponseAdapter(this) - binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, novelResponseAdapter) + novelResponseAdapter = NovelResponseAdapter( + this, + this, + this + ) // probably a better way to do this but it works + binding.animeSourceRecycler.adapter = + ConcatAdapter(headerAdapter, novelResponseAdapter) loaded = true Handler(Looper.getMainLooper()).postDelayed({ search(searchQuery, sel?.sourceIndex ?: 0, auto = sel?.server == null) @@ -74,6 +230,7 @@ class NovelReadFragment : Fragment() { } model.novelResponses.observe(viewLifecycleOwner) { if (it != null) { + response = it searching = false novelResponseAdapter.submitList(it) headerAdapter.progress?.visibility = View.GONE @@ -89,8 +246,9 @@ class NovelReadFragment : Fragment() { searchQuery = query headerAdapter.progress?.visibility = View.VISIBLE lifecycleScope.launch(Dispatchers.IO) { - if (auto || query=="") model.autoSearchNovels(media) - else model.searchNovels(query, source) + if (auto || query == "") model.autoSearchNovels(media) + //else model.searchNovels(query, source) + else model.autoSearchNovels(media) //testing } searching = true if (save) { @@ -121,6 +279,7 @@ class NovelReadFragment : Fragment() { override fun onDestroy() { model.mangaReadSources?.flushText() + requireContext().unregisterReceiver(downloadStatusReceiver) super.onDestroy() } @@ -135,4 +294,22 @@ class NovelReadFragment : Fragment() { super.onPause() state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() } + + companion object { + const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED" + const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED" + const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED" + const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS" + const val EXTRA_NOVEL_LINK = "extra_novel_link" + } +} + +interface DownloadTriggerCallback { + fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) +} + +interface DownloadedCheckCallback { + fun downloadedCheck(novel: ShowResponse): Boolean + fun downloadedCheckWithStart(novel: ShowResponse): Boolean + fun deleteDownload(novel: ShowResponse) } \ No newline at end of file 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 51ec9de6..a7817768 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/NovelResponseAdapter.kt @@ -1,22 +1,32 @@ package ani.dantotsu.media.novel +import android.util.Log +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.R import ani.dantotsu.databinding.ItemNovelResponseBinding import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.setAnimation +import ani.dantotsu.snackString import com.bumptech.glide.Glide import com.bumptech.glide.load.model.GlideUrl -class NovelResponseAdapter(val fragment: NovelReadFragment) : RecyclerView.Adapter() { +class NovelResponseAdapter( + val fragment: NovelReadFragment, + val downloadTriggerCallback: DownloadTriggerCallback, + val downloadedCheckCallback: DownloadedCheckCallback +) : RecyclerView.Adapter() { val list: MutableList = mutableListOf() - inner class ViewHolder(val binding: ItemNovelResponseBinding) : RecyclerView.ViewHolder(binding.root) + inner class ViewHolder(val binding: ItemNovelResponseBinding) : + RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val bind = ItemNovelResponseBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val bind = + ItemNovelResponseBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder(bind) } @@ -27,19 +37,142 @@ class NovelResponseAdapter(val fragment: NovelReadFragment) : RecyclerView.Adapt val novel = list[position] setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) - val cover = GlideUrl(novel.coverUrl.url){ novel.coverUrl.headers } - Glide.with(binding.itemEpisodeImage).load(cover).override(400,0).into(binding.itemEpisodeImage) + val cover = GlideUrl(novel.coverUrl.url) { novel.coverUrl.headers } + Glide.with(binding.itemEpisodeImage).load(cover).override(400, 0) + .into(binding.itemEpisodeImage) + + val typedValue = TypedValue() + fragment.requireContext().theme?.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue, true) + val color = typedValue.data binding.itemEpisodeTitle.text = novel.name - binding.itemEpisodeFiller.text = novel.extra?.get("0") ?: "" + binding.itemEpisodeFiller.text = + if (downloadedCheckCallback.downloadedCheck(novel)) { + "Downloaded" + } else { + novel.extra?.get("0") ?: "" + } + if (binding.itemEpisodeFiller.text.contains("Downloading")) { + binding.itemEpisodeFiller.setTextColor( + fragment.requireContext().getColor(android.R.color.holo_blue_light) + ) + } else if (binding.itemEpisodeFiller.text.contains("Downloaded")) { + binding.itemEpisodeFiller.setTextColor( + fragment.requireContext().getColor(android.R.color.holo_green_light) + ) + } else { + binding.itemEpisodeFiller.setTextColor(color) + } binding.itemEpisodeDesc2.text = novel.extra?.get("1") ?: "" val desc = novel.extra?.get("2") - binding.itemEpisodeDesc.visibility = if (desc != null && desc.trim(' ') != "") View.VISIBLE else View.GONE + binding.itemEpisodeDesc.visibility = + if (desc != null && desc.trim(' ') != "") View.VISIBLE else View.GONE binding.itemEpisodeDesc.text = desc ?: "" binding.root.setOnClickListener { - BookDialog.newInstance(fragment.novelName, novel, fragment.source) - .show(fragment.parentFragmentManager, "dialog") + //make sure the file is not downloading + if (activeDownloads.contains(novel.link)) { + return@setOnClickListener + } + if (downloadedCheckCallback.downloadedCheckWithStart(novel)) { + return@setOnClickListener + } + + val bookDialog = BookDialog.newInstance(fragment.novelName, novel, fragment.source) + + bookDialog.setCallback(object : BookDialog.Callback { + override fun onDownloadTriggered(link: String) { + downloadTriggerCallback.downloadTrigger( + NovelDownloadPackage( + link, + novel.coverUrl.url, + novel.name, + novel.link + ) + ) + bookDialog.dismiss() + } + }) + bookDialog.show(fragment.parentFragmentManager, "dialog") + + } + + binding.root.setOnLongClickListener { + val builder = androidx.appcompat.app.AlertDialog.Builder(fragment.requireContext(), R.style.DialogTheme) + builder.setTitle("Delete ${novel.name}?") + builder.setMessage("Are you sure you want to delete ${novel.name}?") + builder.setPositiveButton("Yes") { _, _ -> + downloadedCheckCallback.deleteDownload(novel) + deleteDownload(novel.link) + snackString("Deleted ${novel.name}") + if (binding.itemEpisodeFiller.text.toString().contains("Download", ignoreCase = true)) { + binding.itemEpisodeFiller.text = "" + } + } + builder.setNegativeButton("No") { _, _ -> + // Do nothing + } + builder.show() + true + } + } + + private val activeDownloads = mutableSetOf() + private val downloadedChapters = mutableSetOf() + + fun startDownload(link: String) { + activeDownloads.add(link) + val position = list.indexOfFirst { it.link == link } + if (position != -1) { + list[position].extra?.remove("0") + list[position].extra?.set("0", "Downloading: 0%") + notifyItemChanged(position) + } + + } + + fun stopDownload(link: String) { + activeDownloads.remove(link) + downloadedChapters.add(link) + val position = list.indexOfFirst { it.link == link } + if (position != -1) { + list[position].extra?.remove("0") + list[position].extra?.set("0", "Downloaded") + notifyItemChanged(position) + } + } + + fun deleteDownload(link: String) { //TODO: + downloadedChapters.remove(link) + val position = list.indexOfFirst { it.link == link } + if (position != -1) { + list[position].extra?.remove("0") + list[position].extra?.set("0", "") + notifyItemChanged(position) + } + } + + fun purgeDownload(link: String) { + activeDownloads.remove(link) + downloadedChapters.remove(link) + val position = list.indexOfFirst { it.link == link } + if (position != -1) { + list[position].extra?.remove("0") + list[position].extra?.set("0", "Failed") + notifyItemChanged(position) + } + } + + fun updateDownloadProgress(link: String, progress: Int) { + if (!activeDownloads.contains(link)) { + activeDownloads.add(link) + } + val position = list.indexOfFirst { it.link == link } + if (position != -1) { + list[position].extra?.remove("0") + list[position].extra?.set("0", "Downloading: $progress%") + Log.d("NovelResponseAdapter", "updateDownloadProgress: $progress, position: $position") + notifyItemChanged(position) } } @@ -54,4 +187,11 @@ class NovelResponseAdapter(val fragment: NovelReadFragment) : RecyclerView.Adapt list.clear() notifyItemRangeRemoved(0, size) } -} \ No newline at end of file +} + +data class NovelDownloadPackage( + val link: String, + val coverUrl: String, + val novelName: String, + val originalLink: String +) \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/novel/UrlAdapter.kt b/app/src/main/java/ani/dantotsu/media/novel/UrlAdapter.kt index 4ef86301..96e9bcf8 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/UrlAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/UrlAdapter.kt @@ -9,16 +9,26 @@ import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.FileUrl import ani.dantotsu.copyToClipboard import ani.dantotsu.databinding.ItemUrlBinding -import ani.dantotsu.others.Download.download import ani.dantotsu.parsers.Book import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.tryWith -class UrlAdapter(private val urls: List, val book: Book, val novel: String) : +class UrlAdapter( + private val urls: List, + val book: Book, + val novel: String, + val callback: BookDialog.Callback? +) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder { - return UrlViewHolder(ItemUrlBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + return UrlViewHolder( + ItemUrlBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) } @SuppressLint("SetTextI18n") @@ -26,6 +36,7 @@ class UrlAdapter(private val urls: List, val book: Book, val novel: Str val binding = holder.binding val url = urls[position] binding.urlQuality.text = url.url + binding.urlQuality.maxLines = 4 binding.urlDownload.visibility = View.VISIBLE } @@ -36,12 +47,14 @@ class UrlAdapter(private val urls: List, val book: Book, val novel: Str itemView.setSafeOnClickListener { tryWith(true) { binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - download( + callback?.onDownloadTriggered(book.links[bindingAdapterPosition].url) + /*download( itemView.context, book, bindingAdapterPosition, novel - ) + )*/ + } } itemView.setOnLongClickListener { 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 e57edcaa..069fef28 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 @@ -2,8 +2,10 @@ package ani.dantotsu.media.novel.novelreader import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.content.Intent import android.content.pm.ActivityInfo import android.graphics.Color +import android.net.Uri import android.os.Build import android.os.Bundle import android.util.Base64 @@ -14,12 +16,14 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.animation.OvershootInterpolator import android.widget.AdapterView +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.net.toUri import androidx.core.view.GestureDetectorCompat import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope +import androidx.webkit.WebViewCompat import ani.dantotsu.GesturesListener import ani.dantotsu.NoPaddingArrayAdapter import ani.dantotsu.R @@ -27,6 +31,7 @@ import ani.dantotsu.databinding.ActivityNovelReaderBinding import ani.dantotsu.hideSystemBars import ani.dantotsu.loadData import ani.dantotsu.others.ImageViewDialog +import ani.dantotsu.others.LangSet import ani.dantotsu.saveData import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.settings.CurrentNovelReaderSettings @@ -35,7 +40,6 @@ import ani.dantotsu.settings.NovelReaderSettings import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.snackString import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet import ani.dantotsu.tryWith import com.google.android.material.slider.Slider import com.vipulog.ebookreader.Book @@ -137,8 +141,22 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + //check for supported webview + val webViewVersion = WebViewCompat.getCurrentWebViewPackage(this)?.versionName + val firstVersion = webViewVersion?.split(".")?.firstOrNull()?.toIntOrNull() + if (webViewVersion == null || firstVersion == null || firstVersion < 87) { + Toast.makeText(this, "Please update WebView from PlayStore", Toast.LENGTH_LONG).show() + //open playstore + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.webview") + startActivity(intent) + //stop reader + finish() + return + } + LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityNovelReaderBinding.inflate(layoutInflater) setContentView(binding.root) @@ -161,7 +179,8 @@ ThemeManager(this).applyTheme() binding.novelReaderBack.setOnClickListener { finish() } binding.novelReaderSettings.setSafeOnClickListener { - NovelReaderSettingsDialogFragment.newInstance().show(supportFragmentManager, NovelReaderSettingsDialogFragment.TAG) + NovelReaderSettingsDialogFragment.newInstance() + .show(supportFragmentManager, NovelReaderSettingsDialogFragment.TAG) } val gestureDetector = GestureDetectorCompat(this, object : GesturesListener() { @@ -233,14 +252,21 @@ ThemeManager(this).applyTheme() binding.novelReaderSource.text = book.author?.joinToString(", ") val tocLabels = book.toc.map { it.label ?: "" } - binding.novelReaderChapterSelect.adapter = NoPaddingArrayAdapter(this, R.layout.item_dropdown, tocLabels) - binding.novelReaderChapterSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - binding.bookReader.goto(book.toc[position].href) - } + binding.novelReaderChapterSelect.adapter = + NoPaddingArrayAdapter(this, R.layout.item_dropdown, tocLabels) + binding.novelReaderChapterSelect.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + binding.bookReader.goto(book.toc[position].href) + } - override fun onNothingSelected(parent: AdapterView<*>?) {} - } + override fun onNothingSelected(parent: AdapterView<*>?) {} + } binding.bookReader.getAppearance { currentTheme = it @@ -295,7 +321,7 @@ ThemeManager(this).applyTheme() private var onVolumeDown: (() -> Unit)? = null override fun dispatchKeyEvent(event: KeyEvent): Boolean { return when (event.keyCode) { - KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_PAGE_UP -> { + KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_PAGE_UP -> { if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP) if (!settings.default.volumeButtons) return false @@ -315,7 +341,7 @@ ThemeManager(this).applyTheme() } else false } - else -> { + else -> { super.dispatchKeyEvent(event) } } @@ -326,10 +352,11 @@ ThemeManager(this).applyTheme() saveData("${sanitizedBookId}_current_settings", settings.default) hideBars() - currentTheme = themes.first { it.name.equals(settings.default.currentThemeName, ignoreCase = true) } + currentTheme = + themes.first { it.name.equals(settings.default.currentThemeName, ignoreCase = true) } when (settings.default.layout) { - CurrentNovelReaderSettings.Layouts.PAGED -> { + CurrentNovelReaderSettings.Layouts.PAGED -> { currentTheme?.flow = ReaderFlow.PAGINATED } @@ -340,9 +367,10 @@ ThemeManager(this).applyTheme() requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER when (settings.default.dualPageMode) { - CurrentReaderSettings.DualPageModes.No -> currentTheme?.maxColumnCount = 1 + CurrentReaderSettings.DualPageModes.No -> currentTheme?.maxColumnCount = 1 CurrentReaderSettings.DualPageModes.Automatic -> currentTheme?.maxColumnCount = 2 - CurrentReaderSettings.DualPageModes.Force -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + CurrentReaderSettings.DualPageModes.Force -> requestedOrientation = + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE } currentTheme?.lineHeight = settings.default.lineHeight @@ -393,7 +421,8 @@ ThemeManager(this).applyTheme() isContVisible = false if (!isAnimating) { isAnimating = true - ObjectAnimator.ofFloat(binding.novelReaderCont, "alpha", 1f, 0f).setDuration(controllerDuration).start() + ObjectAnimator.ofFloat(binding.novelReaderCont, "alpha", 1f, 0f) + .setDuration(controllerDuration).start() ObjectAnimator.ofFloat(binding.novelReaderBottomCont, "translationY", 0f, 128f) .apply { interpolator = overshoot;duration = controllerDuration;start() } ObjectAnimator.ofFloat(binding.novelReaderTopLayout, "translationY", 0f, -128f) @@ -403,7 +432,8 @@ ThemeManager(this).applyTheme() } else { isContVisible = true binding.novelReaderCont.visibility = View.VISIBLE - ObjectAnimator.ofFloat(binding.novelReaderCont, "alpha", 0f, 1f).setDuration(controllerDuration).start() + ObjectAnimator.ofFloat(binding.novelReaderCont, "alpha", 0f, 1f) + .setDuration(controllerDuration).start() ObjectAnimator.ofFloat(binding.novelReaderTopLayout, "translationY", -128f, 0f) .apply { interpolator = overshoot;duration = controllerDuration;start() } ObjectAnimator.ofFloat(binding.novelReaderBottomCont, "translationY", 128f, 0f) @@ -418,7 +448,10 @@ ThemeManager(this).applyTheme() val displayCutout = window.decorView.rootWindowInsets.displayCutout if (displayCutout != null) { if (displayCutout.boundingRects.size > 0) { - notchHeight = min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height()) + notchHeight = min( + displayCutout.boundingRects[0].width(), + displayCutout.boundingRects[0].height() + ) applyNotchMargin() } } diff --git a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt index be609ff5..27a9caaa 100644 --- a/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/novel/novelreader/NovelReaderSettingsDialogFragment.kt @@ -17,7 +17,11 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() { private val binding get() = _binding!! - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetCurrentNovelReaderSettingsBinding.inflate(inflater, container, false) return binding.root } @@ -29,10 +33,16 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() { val settings = activity.settings.default val themeLabels = activity.themes.map { it.name } - binding.themeSelect.adapter = NoPaddingArrayAdapter(activity, R.layout.item_dropdown, themeLabels) + binding.themeSelect.adapter = + NoPaddingArrayAdapter(activity, R.layout.item_dropdown, themeLabels) binding.themeSelect.setSelection(themeLabels.indexOfFirst { it == settings.currentThemeName }) binding.themeSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { settings.currentThemeName = themeLabels[position] activity.applySettings() } @@ -54,7 +64,8 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() { selected.alpha = 0.33f selected = imageButton selected.alpha = 1f - settings.layout = CurrentNovelReaderSettings.Layouts[index]?:CurrentNovelReaderSettings.Layouts.PAGED + settings.layout = CurrentNovelReaderSettings.Layouts[index] + ?: CurrentNovelReaderSettings.Layouts.PAGED binding.layoutText.text = settings.layout.string activity.applySettings() } @@ -75,7 +86,8 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() { selectedDual.alpha = 0.33f selectedDual = imageButton selectedDual.alpha = 1f - settings.dualPageMode = CurrentReaderSettings.DualPageModes[index] ?: CurrentReaderSettings.DualPageModes.Automatic + settings.dualPageMode = CurrentReaderSettings.DualPageModes[index] + ?: CurrentReaderSettings.DualPageModes.Automatic binding.dualPageText.text = settings.dualPageMode.toString() activity.applySettings() } @@ -164,19 +176,19 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() { } binding.useDarkTheme.isChecked = settings.useDarkTheme - binding.useDarkTheme.setOnCheckedChangeListener { _,isChecked -> + binding.useDarkTheme.setOnCheckedChangeListener { _, isChecked -> settings.useDarkTheme = isChecked activity.applySettings() } binding.keepScreenOn.isChecked = settings.keepScreenOn - binding.keepScreenOn.setOnCheckedChangeListener { _,isChecked -> + binding.keepScreenOn.setOnCheckedChangeListener { _, isChecked -> settings.keepScreenOn = isChecked activity.applySettings() } binding.volumeButton.isChecked = settings.volumeButtons - binding.volumeButton.setOnCheckedChangeListener { _,isChecked -> + binding.volumeButton.setOnCheckedChangeListener { _, isChecked -> settings.volumeButtons = isChecked activity.applySettings() } @@ -189,7 +201,7 @@ class NovelReaderSettingsDialogFragment : BottomSheetDialogFragment() { } - companion object{ + companion object { fun newInstance() = NovelReaderSettingsDialogFragment() const val TAG = "NovelReaderSettingsDialogFragment" } 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 9bdc61cc..047efc05 100644 --- a/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/user/ListActivity.kt @@ -16,9 +16,9 @@ import ani.dantotsu.R import ani.dantotsu.Refresh import ani.dantotsu.databinding.ActivityListBinding import ani.dantotsu.loadData +import ani.dantotsu.others.LangSet import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import kotlinx.coroutines.Dispatchers @@ -34,14 +34,18 @@ class ListActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityListBinding.inflate(layoutInflater) val typedValue = TypedValue() theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true) val primaryColor = typedValue.data val typedValue2 = TypedValue() - theme.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue2, true) + theme.resolveAttribute( + com.google.android.material.R.attr.colorOnBackground, + typedValue2, + true + ) val titleTextColor = typedValue2.data val typedValue3 = TypedValue() theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true) @@ -54,7 +58,7 @@ ThemeManager(this).applyTheme() window.navigationBarColor = primaryColor binding.listTabLayout.setBackgroundColor(primaryColor) binding.listAppBar.setBackgroundColor(primaryColor) - binding.listTitle.setTextColor(titleTextColor) + binding.listTitle.setTextColor(primaryTextColor) binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor) binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor) val uiSettings = loadData("ui_settings") ?: UserInterfaceSettings() @@ -63,33 +67,50 @@ ThemeManager(this).applyTheme() ContextCompat.getColor(this, R.color.nav_bg_inv) binding.root.fitsSystemWindows = true - }else{ + } else { binding.root.fitsSystemWindows = false requestWindowFeature(Window.FEATURE_NO_TITLE) - window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) + window.setFlags( + WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN + ) } setContentView(binding.root) val anime = intent.getBooleanExtra("anime", true) - binding.listTitle.text = intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List" + binding.listTitle.text = + intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List" binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { this@ListActivity.selectedTabIdx = tab?.position ?: 0 } - override fun onTabUnselected(tab: TabLayout.Tab?) { } - override fun onTabReselected(tab: TabLayout.Tab?) { } + + override fun onTabUnselected(tab: TabLayout.Tab?) {} + override fun onTabReselected(tab: TabLayout.Tab?) {} }) val model: ListViewModel by viewModels() model.getLists().observe(this) { - val defaultKeys = listOf("Reading", "Watching", "Completed", "Paused", "Dropped", "Planning", "Favourites", "Rewatching", "Rereading", "All") - val userKeys : Array = resources.getStringArray(R.array.keys) + val defaultKeys = listOf( + "Reading", + "Watching", + "Completed", + "Paused", + "Dropped", + "Planning", + "Favourites", + "Rewatching", + "Rereading", + "All" + ) + val userKeys: Array = resources.getStringArray(R.array.keys) if (it != null) { binding.listProgressBar.visibility = View.GONE - binding.listViewPager.adapter = ListViewPagerAdapter(it.size, false,this) - val keys = it.keys.toList().map { key -> userKeys.getOrNull(defaultKeys.indexOf(key))?: key } + binding.listViewPager.adapter = ListViewPagerAdapter(it.size, false, this) + val keys = it.keys.toList() + .map { key -> userKeys.getOrNull(defaultKeys.indexOf(key)) ?: key } val values = it.values.toList() val savedTab = this.selectedTabIdx TabLayoutMediator(binding.listTabLayout, binding.listViewPager) { tab, position -> @@ -103,32 +124,43 @@ ThemeManager(this).applyTheme() live.observe(this) { if (it) { scope.launch { - withContext(Dispatchers.IO) { model.loadLists(anime, intent.getIntExtra("userId", 0)) } + withContext(Dispatchers.IO) { + model.loadLists( + anime, + intent.getIntExtra("userId", 0) + ) + } live.postValue(false) } } } - binding.listSort.setOnClickListener { - val popup = PopupMenu(this, it) - popup.setOnMenuItemClickListener { item -> - val sort = when (item.itemId) { - R.id.score -> "score" - R.id.title -> "title" - R.id.updated -> "updatedAt" - R.id.release -> "release" - else -> null - } + binding.listSort.setOnClickListener { + val popup = PopupMenu(this, it) + popup.setOnMenuItemClickListener { item -> + val sort = when (item.itemId) { + R.id.score -> "score" + R.id.title -> "title" + R.id.updated -> "updatedAt" + R.id.release -> "release" + else -> null + } - binding.listProgressBar.visibility = View.VISIBLE - binding.listViewPager.adapter = null - scope.launch { - withContext(Dispatchers.IO) { model.loadLists(anime, intent.getIntExtra("userId", 0), sort) } - } - true - } - popup.inflate(R.menu.list_sort_menu) - popup.show() - } + binding.listProgressBar.visibility = View.VISIBLE + binding.listViewPager.adapter = null + scope.launch { + withContext(Dispatchers.IO) { + model.loadLists( + anime, + intent.getIntExtra("userId", 0), + sort + ) + } + } + true + } + popup.inflate(R.menu.list_sort_menu) + popup.show() + } } } diff --git a/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt b/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt index 6b0f87de..8053996a 100644 --- a/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/user/ListFragment.kt @@ -11,8 +11,6 @@ import ani.dantotsu.databinding.FragmentListBinding import ani.dantotsu.media.Media import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.OtherDetailsViewModel -import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet class ListFragment : Fragment() { private var _binding: FragmentListBinding? = null @@ -29,7 +27,11 @@ class ListFragment : Fragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = FragmentListBinding.inflate(inflater, container, false) return binding.root } @@ -42,7 +44,10 @@ class ListFragment : Fragment() { if (grid != null && list != null) { val adapter = MediaAdaptor(if (grid!!) 0 else 1, list!!, requireActivity(), true) binding.listRecyclerView.layoutManager = - GridLayoutManager(requireContext(), if (grid!!) (screenWidth / 124f).toInt() else 1) + GridLayoutManager( + requireContext(), + if (grid!!) (screenWidth / 124f).toInt() else 1 + ) binding.listRecyclerView.adapter = adapter } } diff --git a/app/src/main/java/ani/dantotsu/media/user/ListViewPagerAdapter.kt b/app/src/main/java/ani/dantotsu/media/user/ListViewPagerAdapter.kt index 45bf00b0..979059dc 100644 --- a/app/src/main/java/ani/dantotsu/media/user/ListViewPagerAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/user/ListViewPagerAdapter.kt @@ -4,8 +4,13 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter -class ListViewPagerAdapter(private val size: Int, private val calendar: Boolean, fragment: FragmentActivity) : +class ListViewPagerAdapter( + private val size: Int, + private val calendar: Boolean, + fragment: FragmentActivity +) : FragmentStateAdapter(fragment) { override fun getItemCount(): Int = size - override fun createFragment(position: Int): Fragment = ListFragment.newInstance(position, calendar) + override fun createFragment(position: Int): Fragment = + ListFragment.newInstance(position, calendar) } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt b/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt index 5a5579b1..fb780651 100644 --- a/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt +++ b/app/src/main/java/ani/dantotsu/offline/OfflineFragment.kt @@ -13,7 +13,11 @@ import ani.dantotsu.startMainActivity import ani.dantotsu.statusBarHeight class OfflineFragment : Fragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { val binding = FragmentOfflineBinding.inflate(inflater, container, false) binding.refreshContainer.updateLayoutParams { topMargin = statusBarHeight diff --git a/app/src/main/java/ani/dantotsu/others/AniSkip.kt b/app/src/main/java/ani/dantotsu/others/AniSkip.kt index b1cb3f3f..84448f09 100644 --- a/app/src/main/java/ani/dantotsu/others/AniSkip.kt +++ b/app/src/main/java/ani/dantotsu/others/AniSkip.kt @@ -8,12 +8,21 @@ import java.net.URLEncoder object AniSkip { @Suppress("BlockingMethodInNonBlockingContext") - suspend fun getResult(malId: Int, episodeNumber: Int, episodeLength: Long, useProxyForTimeStamps: Boolean): List? { + suspend fun getResult( + malId: Int, + episodeNumber: Int, + episodeLength: Long, + useProxyForTimeStamps: Boolean + ): List? { val url = "https://api.aniskip.com/v2/skip-times/$malId/$episodeNumber?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=$episodeLength" return tryWithSuspend { - val a = if(useProxyForTimeStamps) - client.get("https://corsproxy.io/?${URLEncoder.encode(url, "utf-8").replace("+", "%20")}") + val a = if (useProxyForTimeStamps) + client.get( + "https://corsproxy.io/?${ + URLEncoder.encode(url, "utf-8").replace("+", "%20") + }" + ) else client.get(url) val res = a.parsed() @@ -40,8 +49,8 @@ object AniSkip { fun String.getType(): String { return when (this) { - "op" -> "Opening" - "ed" -> "Ending" + "op" -> "Opening" + "ed" -> "Ending" "recap" -> "Recap" "mixed-ed" -> "Mixed Ending" "mixed-op" -> "Mixed Opening" diff --git a/app/src/main/java/ani/dantotsu/others/AppUpdater.kt b/app/src/main/java/ani/dantotsu/others/AppUpdater.kt index e6a39fb0..b753e908 100644 --- a/app/src/main/java/ani/dantotsu/others/AppUpdater.kt +++ b/app/src/main/java/ani/dantotsu/others/AppUpdater.kt @@ -24,16 +24,17 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.decodeFromJsonElement +import rx.android.BuildConfig import java.io.File import java.text.SimpleDateFormat import java.util.* object AppUpdater { - suspend fun check(activity: FragmentActivity, post:Boolean=false) { - if(post) snackString(currContext()?.getString(R.string.checking_for_update)) + suspend fun check(activity: FragmentActivity, post: Boolean = false) { + if (post) snackString(currContext()?.getString(R.string.checking_for_update)) val repo = activity.getString(R.string.repo) tryWithSuspend { - val (md, version) = if(BuildConfig.DEBUG){ + val (md, version) = if (BuildConfig.DEBUG) { val res = client.get("https://api.github.com/repos/$repo/releases") .parsed().map { Mapper.json.decodeFromJsonElement(it) @@ -41,9 +42,9 @@ object AppUpdater { val r = res.filter { it.prerelease }.maxByOrNull { it.timeStamp() } ?: throw Exception("No Pre Release Found") - val v = r.tagName.substringAfter("v","") + val v = r.tagName.substringAfter("v", "") (r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") } - }else{ + } else { val res = client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text res to res.substringAfter("# ").substringBefore("\n") @@ -53,15 +54,23 @@ object AppUpdater { val dontShow = loadData("dont_ask_for_update_$version") ?: false if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread { CustomBottomDialog.newInstance().apply { - setTitleText("${if (BuildConfig.DEBUG) "Beta " else ""}Update " + currContext()!!.getString(R.string.available)) + setTitleText( + "${if (BuildConfig.DEBUG) "Beta " else ""}Update " + currContext()!!.getString( + R.string.available + ) + ) addView( TextView(activity).apply { - val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()).build() + val markWon = Markwon.builder(activity) + .usePlugin(SoftBreakAddsNewLinePlugin.create()).build() markWon.setMarkdown(this, md) } ) - setCheck(currContext()!!.getString(R.string.dont_show_again, version), false) { isChecked -> + setCheck( + currContext()!!.getString(R.string.dont_show_again, version), + false + ) { isChecked -> if (isChecked) { saveData("dont_ask_for_update_$version", true) } @@ -71,11 +80,11 @@ object AppUpdater { try { client.get("https://api.github.com/repos/$repo/releases/tags/v$version") .parsed().assets?.find { - it.browserDownloadURL.endsWith("apk") - }?.browserDownloadURL.apply { - if (this != null) activity.downloadUpdate(version, this) - else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version") - } + it.browserDownloadURL.endsWith("apk") + }?.browserDownloadURL.apply { + if (this != null) activity.downloadUpdate(version, this) + else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version") + } } catch (e: Exception) { logError(e) } @@ -88,25 +97,24 @@ object AppUpdater { show(activity.supportFragmentManager, "dialog") } } - else{ - if(post) snackString(currContext()?.getString(R.string.no_update_found)) + else { + if (post) snackString(currContext()?.getString(R.string.no_update_found)) } } } private fun compareVersion(version: String): Boolean { - if(BuildConfig.DEBUG) { + if (BuildConfig.DEBUG) { return BuildConfig.VERSION_NAME != version - } - else { + } else { fun toDouble(list: List): Double { return list.mapIndexed { i: Int, s: String -> when (i) { - 0 -> s.toDouble() * 100 - 1 -> s.toDouble() * 10 - 2 -> s.toDouble() - else -> s.toDoubleOrNull()?: 0.0 + 0 -> s.toDouble() * 100 + 1 -> s.toDouble() * 10 + 2 -> s.toDouble() + else -> s.toDoubleOrNull() ?: 0.0 } }.sum() } @@ -210,7 +218,7 @@ object AppUpdater { val tagName: String, val prerelease: Boolean, @SerialName("created_at") - val createdAt : String, + val createdAt: String, val body: String? = null, val assets: List? = null ) { diff --git a/app/src/main/java/ani/dantotsu/others/CustomBottomDialog.kt b/app/src/main/java/ani/dantotsu/others/CustomBottomDialog.kt index 3c758f60..e5446bd0 100644 --- a/app/src/main/java/ani/dantotsu/others/CustomBottomDialog.kt +++ b/app/src/main/java/ani/dantotsu/others/CustomBottomDialog.kt @@ -17,8 +17,9 @@ open class CustomBottomDialog : BottomSheetDialogFragment() { fun addView(view: View) { viewList.add(view) } - var title: String?=null - fun setTitleText(string: String){ + + var title: String? = null + fun setTitleText(string: String) { title = string } @@ -46,7 +47,11 @@ open class CustomBottomDialog : BottomSheetDialogFragment() { positiveCallback = callback } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetCustomBinding.inflate(inflater, container, false) val window = dialog?.window window?.statusBarColor = Color.TRANSPARENT @@ -70,16 +75,16 @@ open class CustomBottomDialog : BottomSheetDialogFragment() { checkCallback?.invoke(checked) } } - - if(negativeText!=null) binding.bottomDialogCustomNegative.apply { + + if (negativeText != null) binding.bottomDialogCustomNegative.apply { visibility = View.VISIBLE text = negativeText - setOnClickListener { + setOnClickListener { negativeCallback?.invoke() } } - if(positiveText!=null) binding.bottomDialogCustomPositive.apply { + if (positiveText != null) binding.bottomDialogCustomPositive.apply { visibility = View.VISIBLE text = positiveText setOnClickListener { diff --git a/app/src/main/java/ani/dantotsu/others/Download.kt b/app/src/main/java/ani/dantotsu/others/Download.kt index b9d9a27a..8a8b496a 100644 --- a/app/src/main/java/ani/dantotsu/others/Download.kt +++ b/app/src/main/java/ani/dantotsu/others/Download.kt @@ -12,10 +12,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import ani.dantotsu.FileUrl import ani.dantotsu.R -import ani.dantotsu.media.anime.Episode import ani.dantotsu.currContext import ani.dantotsu.defaultHeaders import ani.dantotsu.loadData +import ani.dantotsu.media.anime.Episode import ani.dantotsu.parsers.Book import ani.dantotsu.toast import kotlinx.coroutines.CoroutineScope @@ -50,12 +50,17 @@ object Download { fun download(context: Context, episode: Episode, animeTitle: String) { toast(context.getString(R.string.downloading)) - val extractor = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return + val extractor = + episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return val video = if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else return val regex = "[\\\\/:*?\"<>|]".toRegex() val aTitle = animeTitle.replace(regex, "") - val title = "Episode ${episode.number}${if (episode.title != null) " - ${episode.title}" else ""}".replace(regex, "") + val title = + "Episode ${episode.number}${if (episode.title != null) " - ${episode.title}" else ""}".replace( + regex, + "" + ) val notif = "$title : $aTitle" val folder = "/Anime/${aTitle}/" @@ -64,7 +69,7 @@ object Download { download(context, file, fileName, folder, notif) } - fun download(context: Context, book:Book, pos:Int, novelTitle:String){ + fun download(context: Context, book: Book, pos: Int, novelTitle: String) { toast(currContext()?.getString(R.string.downloading)) val regex = "[\\\\/:*?\"<>|]".toRegex() val nTitle = novelTitle.replace(regex, "") @@ -77,19 +82,32 @@ object Download { download(context, file, fileName, folder, notif) } - fun download(context: Context, file: FileUrl, fileName: String, folder: String, notif: String? = null) { - if(!file.url.startsWith("http")) + fun download( + context: Context, + file: FileUrl, + fileName: String, + folder: String, + notif: String? = null + ) { + if (!file.url.startsWith("http")) toast(context.getString(R.string.invalid_url)) else when (loadData("settings_download_manager", context, false) ?: 0) { - 1 -> oneDM(context, file, notif ?: fileName) - 2 -> adm(context, file, fileName, folder) + 1 -> oneDM(context, file, notif ?: fileName) + 2 -> adm(context, file, fileName, folder) else -> defaultDownload(context, file, fileName, folder, notif ?: fileName) } } - private fun defaultDownload(context: Context, file: FileUrl, fileName: String, folder: String, notif: String) { - val manager = context.getSystemService(AppCompatActivity.DOWNLOAD_SERVICE) as DownloadManager + private fun defaultDownload( + context: Context, + file: FileUrl, + fileName: String, + folder: String, + notif: String + ) { + val manager = + context.getSystemService(AppCompatActivity.DOWNLOAD_SERVICE) as DownloadManager val request: DownloadManager.Request = DownloadManager.Request(Uri.parse(file.url)) file.headers.forEach { request.addRequestHeader(it.key, it.value) @@ -124,15 +142,24 @@ object Download { } private fun oneDM(context: Context, file: FileUrl, notif: String) { - val appName = if (isPackageInstalled("idm.internet.download.manager.plus", context.packageManager)) { - "idm.internet.download.manager.plus" - } else if (isPackageInstalled("idm.internet.download.manager", context.packageManager)) { - "idm.internet.download.manager" - } else if (isPackageInstalled("idm.internet.download.manager.adm.lite", context.packageManager)) { - "idm.internet.download.manager.adm.lite" - } else { - "" - } + val appName = + if (isPackageInstalled("idm.internet.download.manager.plus", context.packageManager)) { + "idm.internet.download.manager.plus" + } else if (isPackageInstalled( + "idm.internet.download.manager", + context.packageManager + ) + ) { + "idm.internet.download.manager" + } else if (isPackageInstalled( + "idm.internet.download.manager.adm.lite", + context.packageManager + ) + ) { + "idm.internet.download.manager.adm.lite" + } else { + "" + } if (appName.isNotEmpty()) { val bundle = Bundle() defaultHeaders.forEach { a -> bundle.putString(a.key, a.value) } @@ -177,7 +204,9 @@ object Download { } else { ContextCompat.startActivity( context, - Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.dv.adm")).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.dv.adm")).addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + ), null ) toast(currContext()?.getString(R.string.install_adm)) diff --git a/app/src/main/java/ani/dantotsu/others/GlideApp.kt b/app/src/main/java/ani/dantotsu/others/GlideApp.kt index edc2747d..97e26ad4 100644 --- a/app/src/main/java/ani/dantotsu/others/GlideApp.kt +++ b/app/src/main/java/ani/dantotsu/others/GlideApp.kt @@ -15,7 +15,7 @@ import java.io.InputStream @GlideModule -class DantotsuGlideApp : AppGlideModule(){ +class DantotsuGlideApp : AppGlideModule() { @SuppressLint("CheckResult") override fun applyOptions(context: Context, builder: GlideBuilder) { super.applyOptions(context, builder) diff --git a/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt b/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt index 03013d08..aa792d9f 100644 --- a/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt +++ b/app/src/main/java/ani/dantotsu/others/ImageViewDialog.kt @@ -19,8 +19,6 @@ import ani.dantotsu.saveImageToDownloads import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.shareImage import ani.dantotsu.snackString -import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet import ani.dantotsu.toast import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.davemorrissey.labs.subscaleview.ImageSource @@ -50,7 +48,11 @@ class ImageViewDialog : BottomSheetDialogFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetImageBinding.inflate(inflater, container, false) return binding.root } @@ -81,13 +83,16 @@ class ImageViewDialog : BottomSheetDialogFragment() { val binding = _binding ?: return@launch var bitmap = context.loadBitmap_old(image, trans1 ?: listOf()) - var bitmap2 = if (image2 != null) context.loadBitmap_old(image2, trans2 ?: listOf()) else null + var bitmap2 = + if (image2 != null) context.loadBitmap_old(image2, trans2 ?: listOf()) else null if (bitmap == null) { bitmap = context.loadBitmap(image, trans1 ?: listOf()) - bitmap2 = if (image2 != null) context.loadBitmap(image2, trans2 ?: listOf()) else null + bitmap2 = + if (image2 != null) context.loadBitmap(image2, trans2 ?: listOf()) else null } - bitmap = if (bitmap2 != null && bitmap != null) mergeBitmap(bitmap, bitmap2,) else bitmap + bitmap = + if (bitmap2 != null && bitmap != null) mergeBitmap(bitmap, bitmap2) else bitmap if (bitmap != null) { binding.bottomImageShare.isEnabled = true @@ -100,10 +105,11 @@ class ImageViewDialog : BottomSheetDialogFragment() { } binding.bottomImageView.setImage(ImageSource.cachedBitmap(bitmap)) - ObjectAnimator.ofFloat(binding.bottomImageView, "alpha", 0f, 1f).setDuration(400L).start() + ObjectAnimator.ofFloat(binding.bottomImageView, "alpha", 0f, 1f).setDuration(400L) + .start() binding.bottomImageProgress.visibility = View.GONE } else { - toast(context?.getString(R.string.loading_image_failed)) + toast(context.getString(R.string.loading_image_failed)) binding.bottomImageNo.visibility = View.VISIBLE binding.bottomImageProgress.visibility = View.GONE } @@ -116,7 +122,12 @@ class ImageViewDialog : BottomSheetDialogFragment() { } companion object { - fun newInstance(title: String, image: FileUrl, showReload: Boolean = false, image2: FileUrl?) = ImageViewDialog().apply { + fun newInstance( + title: String, + image: FileUrl, + showReload: Boolean = false, + image2: FileUrl? + ) = ImageViewDialog().apply { arguments = Bundle().apply { putString("title", title) putBoolean("reload", showReload) diff --git a/app/src/main/java/ani/dantotsu/others/Jikan.kt b/app/src/main/java/ani/dantotsu/others/Jikan.kt index e08231a0..518dca3d 100644 --- a/app/src/main/java/ani/dantotsu/others/Jikan.kt +++ b/app/src/main/java/ani/dantotsu/others/Jikan.kt @@ -1,7 +1,7 @@ package ani.dantotsu.others -import ani.dantotsu.media.anime.Episode import ani.dantotsu.client +import ani.dantotsu.media.anime.Episode import ani.dantotsu.tryWithSuspend import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -23,9 +23,10 @@ object Jikan { val res = query("anime/$malId/episodes?page=$page") res?.data?.forEach { val ep = it.malID.toString() - eps[ep] = Episode(ep, title = it.title, + eps[ep] = Episode( + ep, title = it.title, //Personal revenge with 34566 :prayge: - filler = if(malId!=34566) it.filler else true, + filler = if (malId != 34566) it.filler else true, ) } hasNextPage = res?.pagination?.hasNextPage == true diff --git a/app/src/main/java/ani/dantotsu/others/JsUnpacker.kt b/app/src/main/java/ani/dantotsu/others/JsUnpacker.kt index 790958a3..35ebe7b5 100644 --- a/app/src/main/java/ani/dantotsu/others/JsUnpacker.kt +++ b/app/src/main/java/ani/dantotsu/others/JsUnpacker.kt @@ -1,7 +1,7 @@ package ani.dantotsu.others import ani.dantotsu.logger -import java.util.regex.* +import java.util.regex.Pattern import kotlin.math.pow // https://github.com/cylonu87/JsUnpacker @@ -31,7 +31,10 @@ class JsUnpacker(packedJS: String?) { val js = packedJS ?: return null try { var p = - Pattern.compile("""\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)""", Pattern.DOTALL) + Pattern.compile( + """\}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)""", + Pattern.DOTALL + ) var m = p.matcher(js) if (m.find() && m.groupCount() == 4) { val payload = m.group(1)?.replace("\\'", "'") ?: return null @@ -79,7 +82,10 @@ class JsUnpacker(packedJS: String?) { } else { val tmp = StringBuilder(str).reverse().toString() for (i in tmp.indices) { - ret += (radix.toDouble().pow(i.toDouble()) * dictionary!![tmp.substring(i, i + 1)]!!).toInt() + ret += (radix.toDouble().pow(i.toDouble()) * dictionary!![tmp.substring( + i, + i + 1 + )]!!).toInt() } } return ret @@ -91,12 +97,15 @@ class JsUnpacker(packedJS: String?) { radix < 62 -> { alphabet = a62.substring(0, radix) } + radix in 63..94 -> { alphabet = a95.substring(0, radix) } + radix == 62 -> { alphabet = a62 } + radix == 95 -> { alphabet = a95 } diff --git a/app/src/main/java/ani/dantotsu/others/Kitsu.kt b/app/src/main/java/ani/dantotsu/others/Kitsu.kt index 6f0ae8cf..e8155729 100644 --- a/app/src/main/java/ani/dantotsu/others/Kitsu.kt +++ b/app/src/main/java/ani/dantotsu/others/Kitsu.kt @@ -18,7 +18,13 @@ object Kitsu { "DNT" to "1", "Origin" to "https://kitsu.io" ) - val json = tryWithSuspend { client.post("https://kitsu.io/api/graphql", headers, data = mapOf("query" to query)) } + val json = tryWithSuspend { + client.post( + "https://kitsu.io/api/graphql", + headers, + data = mapOf("query" to query) + ) + } return json?.parsed() } @@ -54,8 +60,8 @@ query { val result = getKitsuData(query) ?: return null logger("Kitsu : result=$result", print) media.idKitsu = result.data?.lookupMapping?.id - return (result.data?.lookupMapping?.episodes?.nodes?:return null).mapNotNull { ep -> - val num = ep?.num?.toString()?:return@mapNotNull null + return (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep -> + val num = ep?.num?.toString() ?: return@mapNotNull null num to Episode( number = num, title = ep.titles?.canonical, @@ -70,39 +76,46 @@ query { @SerialName("data") val data: Data? = null ) { @Serializable - data class Data ( + data class Data( @SerialName("lookupMapping") val lookupMapping: LookupMapping? = null ) + @Serializable - data class LookupMapping ( + data class LookupMapping( @SerialName("id") val id: String? = null, @SerialName("episodes") val episodes: Episodes? = null ) + @Serializable - data class Episodes ( + data class Episodes( @SerialName("nodes") val nodes: List? = null ) + @Serializable - data class Node ( + data class Node( @SerialName("number") val num: Long? = null, @SerialName("titles") val titles: Titles? = null, @SerialName("description") val description: Description? = null, @SerialName("thumbnail") val thumbnail: Thumbnail? = null ) + @Serializable - data class Description ( + data class Description( @SerialName("en") val en: String? = null ) + @Serializable - data class Thumbnail ( + data class Thumbnail( @SerialName("original") val original: Original? = null ) + @Serializable - data class Original ( + data class Original( @SerialName("url") val url: String? = null ) + @Serializable - data class Titles ( + data class Titles( @SerialName("canonical") val canonical: String? = null ) diff --git a/app/src/main/java/ani/dantotsu/others/LangSet.kt b/app/src/main/java/ani/dantotsu/others/LangSet.kt index fd5ba8a3..3e7857da 100644 --- a/app/src/main/java/ani/dantotsu/others/LangSet.kt +++ b/app/src/main/java/ani/dantotsu/others/LangSet.kt @@ -6,13 +6,12 @@ import android.content.res.Resources import java.util.Locale - - class LangSet { - companion object{ + companion object { fun setLocale(activity: Activity) { - val useCursedLang = activity.getSharedPreferences("Dantotsu", Activity.MODE_PRIVATE).getBoolean("use_cursed_lang", false) - val locale = if(useCursedLang) Locale("en", "DW") else Locale("en", "US") + val useCursedLang = activity.getSharedPreferences("Dantotsu", Activity.MODE_PRIVATE) + .getBoolean("use_cursed_lang", false) + val locale = if (useCursedLang) Locale("en", "DW") else Locale("en", "US") Locale.setDefault(locale) val resources: Resources = activity.resources val config: Configuration = resources.configuration diff --git a/app/src/main/java/ani/dantotsu/others/MalScraper.kt b/app/src/main/java/ani/dantotsu/others/MalScraper.kt index 048dc0ed..8b39ac64 100644 --- a/app/src/main/java/ani/dantotsu/others/MalScraper.kt +++ b/app/src/main/java/ani/dantotsu/others/MalScraper.kt @@ -17,11 +17,14 @@ object MalScraper { try { withTimeout(6000) { if (media.anime != null) { - val res = client.get("https://myanimelist.net/anime/${media.idMAL}", headers).document + val res = + client.get("https://myanimelist.net/anime/${media.idMAL}", headers).document val a = res.select(".title-english").text() media.nameMAL = if (a != "") a else res.select(".title-name").text() media.typeMAL = - if (res.select("div.spaceit_pad > a").isNotEmpty()) res.select("div.spaceit_pad > a")[0].text() else null + if (res.select("div.spaceit_pad > a") + .isNotEmpty() + ) res.select("div.spaceit_pad > a")[0].text() else null media.anime.op = arrayListOf() res.select(".opnening > table > tbody > tr").forEach { val text = it.text() @@ -35,12 +38,15 @@ object MalScraper { media.anime.ed.add(it.text()) } } else { - val res = client.get("https://myanimelist.net/manga/${media.idMAL}", headers).document + val res = + client.get("https://myanimelist.net/manga/${media.idMAL}", headers).document val b = res.select(".title-english").text() val a = res.select(".h1-title").text().removeSuffix(b) media.nameMAL = a media.typeMAL = - if (res.select("div.spaceit_pad > a").isNotEmpty()) res.select("div.spaceit_pad > a")[0].text() else null + if (res.select("div.spaceit_pad > a") + .isNotEmpty() + ) res.select("div.spaceit_pad > a")[0].text() else null } } } catch (e: Exception) { diff --git a/app/src/main/java/ani/dantotsu/others/MalSyncBackup.kt b/app/src/main/java/ani/dantotsu/others/MalSyncBackup.kt index 5ff7d9c1..66e48603 100644 --- a/app/src/main/java/ani/dantotsu/others/MalSyncBackup.kt +++ b/app/src/main/java/ani/dantotsu/others/MalSyncBackup.kt @@ -31,8 +31,8 @@ object MalSyncBackup { val isDub = page.title.lowercase().replace(" ", "").endsWith("(dub)") val slug = if (dub == isDub) page.identifier else null if (slug != null && page.active == true && page.url != null) { - val url = when(name){ - "Gogoanime" -> slug + val url = when (name) { + "Gogoanime" -> slug "Tenshi" -> slug else -> page.url } diff --git a/app/src/main/java/ani/dantotsu/others/OutlineTextView.kt b/app/src/main/java/ani/dantotsu/others/OutlineTextView.kt index c5ee8efd..e2c17cc0 100644 --- a/app/src/main/java/ani/dantotsu/others/OutlineTextView.kt +++ b/app/src/main/java/ani/dantotsu/others/OutlineTextView.kt @@ -25,7 +25,11 @@ class OutlineTextView : AppCompatTextView { } - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { initResources(context, attrs) } @@ -55,7 +59,8 @@ class OutlineTextView : AppCompatTextView { strokeWidth = width.toPx(context) } - private fun Float.toPx(context: Context) = (this * context.resources.displayMetrics.scaledDensity + 0.5F) + private fun Float.toPx(context: Context) = + (this * context.resources.displayMetrics.scaledDensity + 0.5F) override fun invalidate() { if (isDrawing) return diff --git a/app/src/main/java/ani/dantotsu/others/ResettableTimer.kt b/app/src/main/java/ani/dantotsu/others/ResettableTimer.kt index af3a1162..cb0066c0 100644 --- a/app/src/main/java/ani/dantotsu/others/ResettableTimer.kt +++ b/app/src/main/java/ani/dantotsu/others/ResettableTimer.kt @@ -1,7 +1,8 @@ package ani.dantotsu.others -import java.util.* -import java.util.concurrent.atomic.* +import java.util.Timer +import java.util.TimerTask +import java.util.concurrent.atomic.AtomicBoolean class ResettableTimer { var resetLock = AtomicBoolean(false) diff --git a/app/src/main/java/ani/dantotsu/others/SpoilerPlugin.kt b/app/src/main/java/ani/dantotsu/others/SpoilerPlugin.kt index 635150fe..fd746664 100644 --- a/app/src/main/java/ani/dantotsu/others/SpoilerPlugin.kt +++ b/app/src/main/java/ani/dantotsu/others/SpoilerPlugin.kt @@ -10,7 +10,7 @@ import android.view.View import android.widget.TextView import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.utils.ColorUtils -import java.util.regex.* +import java.util.regex.Pattern class SpoilerPlugin : AbstractMarkwonPlugin() { override fun beforeSetText(textView: TextView, markdown: Spanned) { diff --git a/app/src/main/java/ani/dantotsu/others/Xpandable.kt b/app/src/main/java/ani/dantotsu/others/Xpandable.kt index ea6bfa7b..527380ef 100644 --- a/app/src/main/java/ani/dantotsu/others/Xpandable.kt +++ b/app/src/main/java/ani/dantotsu/others/Xpandable.kt @@ -29,8 +29,8 @@ class Xpandable @JvmOverloads constructor( }, 300) } - if(!expanded) children.forEach { - if (it != getChildAt(0)){ + if (!expanded) children.forEach { + if (it != getChildAt(0)) { it.visibility = GONE } } @@ -40,7 +40,7 @@ class Xpandable @JvmOverloads constructor( private fun hideAll() { children.forEach { - if (it != getChildAt(0)){ + if (it != getChildAt(0)) { ObjectAnimator.ofFloat(it, "scaleY", 1f, 0.5f).setDuration(200).start() ObjectAnimator.ofFloat(it, "translationY", 0f, -32f).setDuration(200).start() ObjectAnimator.ofFloat(it, "alpha", 1f, 0f).setDuration(200).start() @@ -54,7 +54,7 @@ class Xpandable @JvmOverloads constructor( private fun showAll() { children.forEach { - if (it != getChildAt(0)){ + if (it != getChildAt(0)) { it.visibility = VISIBLE ObjectAnimator.ofFloat(it, "scaleY", 0.5f, 1f).setDuration(200).start() ObjectAnimator.ofFloat(it, "translationY", -32f, 0f).setDuration(200).start() diff --git a/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt b/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt index f1d846eb..1e38fc0e 100644 --- a/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt +++ b/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchActivity.kt @@ -15,8 +15,8 @@ import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.databinding.ActivityImageSearchBinding import ani.dantotsu.media.MediaDetailsActivity -import ani.dantotsu.themes.ThemeManager import ani.dantotsu.others.LangSet +import ani.dantotsu.themes.ThemeManager import ani.dantotsu.toast import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -36,7 +36,7 @@ class ImageSearchActivity : AppCompatActivity() { } val inputStream = contentResolver.openInputStream(imageUri) - if(inputStream != null) viewModel.analyzeImage(inputStream) + if (inputStream != null) viewModel.analyzeImage(inputStream) else toast(getString(R.string.error_loading_image)) withContext(Dispatchers.Main) { @@ -49,7 +49,7 @@ class ImageSearchActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityImageSearchBinding.inflate(layoutInflater) setContentView(binding.root) @@ -76,7 +76,7 @@ ThemeManager(this).applyTheme() override fun onItemClick(searchResult: ImageSearchViewModel.ImageResult) { lifecycleScope.launch(Dispatchers.IO) { val id = searchResult.anilist?.id?.toInt() - if (id==null){ + if (id == null) { toast(getString(R.string.no_anilist_id_found)) return@launch } diff --git a/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchResultAdapter.kt b/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchResultAdapter.kt index 917e9cfe..df996a08 100644 --- a/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchResultAdapter.kt +++ b/app/src/main/java/ani/dantotsu/others/imagesearch/ImageSearchResultAdapter.kt @@ -20,10 +20,12 @@ class ImageSearchResultAdapter(private val searchResults: List?, sAnime: SAnime): List + abstract suspend fun loadEpisodes( + animeLink: String, + extra: Map?, + sAnime: SAnime + ): List /** * Takes ShowResponse.link, ShowResponse.extra & the Last Largest Episode Number known by app as arguments @@ -31,10 +35,15 @@ abstract class AnimeParser : BaseParser() { * Returns the latest episode (If overriding, Make sure the episode is actually the latest episode) * Returns null, if no latest episode is found. * **/ - open suspend fun getLatestEpisode(animeLink: String, extra: Map?, sAnime: SAnime, latest: Float): Episode?{ + open suspend fun getLatestEpisode( + animeLink: String, + extra: Map?, + sAnime: SAnime, + latest: Float + ): Episode? { val episodes = loadEpisodes(animeLink, extra, sAnime) val max = episodes - .maxByOrNull { it.number.toFloatOrNull()?:0f } + .maxByOrNull { it.number.toFloatOrNull() ?: 0f } return max ?.takeIf { latest < (it.number.toFloatOrNull() ?: 0.001f) } } @@ -44,7 +53,11 @@ abstract class AnimeParser : BaseParser() { * * This returns a Map of "Video Server's Name" & "Link/Data" of all the Video Servers present on the site, which can be further used by loadVideoServers() & loadSingleVideoServer() * **/ - abstract suspend fun loadVideoServers(episodeLink: String, extra: Map?, sEpisode: SEpisode): List + abstract suspend fun loadVideoServers( + episodeLink: String, + extra: Map?, + sEpisode: SEpisode + ): List /** @@ -75,10 +88,12 @@ abstract class AnimeParser : BaseParser() { * **/ open suspend fun getVideoExtractor(server: VideoServer): VideoExtractor? { var domain = Uri.parse(server.embed.url).host ?: return null - if (domain.startsWith("www.")) {domain = domain.substring(4)} + if (domain.startsWith("www.")) { + domain = domain.substring(4) + } val extractor: VideoExtractor? = when (domain) { - else -> { + else -> { println("$name : No extractor found for: $domain | ${server.embed.url}") null } @@ -98,7 +113,12 @@ abstract class AnimeParser : BaseParser() { * * Doesn't need to be overridden, if the parser is following the norm. * **/ - open suspend fun loadByVideoServers(episodeUrl: String, extra: Map?, sEpisode: SEpisode, callback: (VideoExtractor) -> Unit) { + open suspend fun loadByVideoServers( + episodeUrl: String, + extra: Map?, + sEpisode: SEpisode, + callback: (VideoExtractor) -> Unit + ) { tryWithSuspend(true) { loadVideoServers(episodeUrl, extra, sEpisode).asyncMap { getVideoExtractor(it)?.apply { @@ -116,7 +136,13 @@ abstract class AnimeParser : BaseParser() { * * Doesn't need to be overridden, if the parser is following the norm. * **/ - open suspend fun loadSingleVideoServer(serverName: String, episodeUrl: String, extra: Map?, sEpisode: SEpisode, post: Boolean): VideoExtractor? { + open suspend fun loadSingleVideoServer( + serverName: String, + episodeUrl: String, + extra: Map?, + sEpisode: SEpisode, + post: Boolean + ): VideoExtractor? { return tryWithSuspend(post) { loadVideoServers(episodeUrl, extra, sEpisode).apply { find { it.name == serverName }?.also { @@ -159,27 +185,43 @@ abstract class AnimeParser : BaseParser() { val dub = if (isDubAvailableSeparately) "_${if (selectDub) "dub" else "sub"}" else "" var loaded = loadData("${saveName}${dub}_$mediaId") if (loaded == null && malSyncBackupName.isNotEmpty()) - loaded = MalSyncBackup.get(mediaId, malSyncBackupName, selectDub)?.also { saveShowResponse(mediaId, it, true) } + loaded = MalSyncBackup.get(mediaId, malSyncBackupName, selectDub) + ?.also { saveShowResponse(mediaId, it, true) } return loaded } override fun saveShowResponse(mediaId: Int, response: ShowResponse?, selected: Boolean) { if (response != null) { checkIfVariablesAreEmpty() - setUserText("${if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString(R.string.found)} : ${response.name}") + setUserText( + "${ + if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString( + R.string.found + ) + } : ${response.name}" + ) val dub = if (isDubAvailableSeparately) "_${if (selectDub) "dub" else "sub"}" else "" saveData("${saveName}${dub}_$mediaId", response) } } } -class EmptyAnimeParser: AnimeParser() { +class EmptyAnimeParser : AnimeParser() { override val name: String = "None" override val saveName: String = "None" override val isDubAvailableSeparately: Boolean = false - override suspend fun loadEpisodes(animeLink: String, extra: Map?, sAnime: SAnime): List = emptyList() - override suspend fun loadVideoServers(episodeLink: String, extra: Map?, sEpisode: SEpisode): List = emptyList() + override suspend fun loadEpisodes( + animeLink: String, + extra: Map?, + sAnime: SAnime + ): List = emptyList() + + override suspend fun loadVideoServers( + episodeLink: String, + extra: Map?, + sEpisode: SEpisode + ): List = emptyList() override suspend fun search(query: String): List = emptyList() } @@ -209,7 +251,7 @@ data class Episode( /** * In case, you want to pass extra data * **/ - val extra: Map? = null, + val extra: Map? = null, //SEpisode from Aniyomi val sEpisode: SEpisode? = null @@ -221,7 +263,7 @@ data class Episode( thumbnail: String, description: String? = null, isFiller: Boolean = false, - extra: Map? = null + extra: Map? = null ) : this(number, link, title, FileUrl(thumbnail), description, isFiller, extra) constructor( @@ -231,7 +273,7 @@ data class Episode( thumbnail: String, description: String? = null, isFiller: Boolean = false, - extra: Map? = null, + extra: Map? = null, sEpisode: SEpisode? = null ) : this(number, link, title, FileUrl(thumbnail), description, isFiller, extra, sEpisode) } diff --git a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt index 1b66928c..c48a4b3e 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AnimeSources.kt @@ -1,8 +1,8 @@ package ani.dantotsu.parsers import ani.dantotsu.Lazier -import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import ani.dantotsu.lazyList +import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first @@ -30,8 +30,8 @@ object AnimeSources : WatchSources() { object HAnimeSources : WatchSources() { - private val aList: List> = lazyList( + private val aList: List> = lazyList( ) - override val list = listOf(aList,AnimeSources.list).flatten() + override val list = listOf(aList, AnimeSources.list).flatten() } diff --git a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt index 18d5fce1..ea2fa1a0 100644 --- a/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt +++ b/app/src/main/java/ani/dantotsu/parsers/AniyomiAdapter.kt @@ -3,7 +3,6 @@ package ani.dantotsu.parsers import android.content.ContentResolver import android.content.ContentValues import android.content.Context -import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri @@ -11,34 +10,28 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import android.widget.Toast -import androidx.core.content.ContextCompat import ani.dantotsu.FileUrl -import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension -import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource -import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException import ani.dantotsu.currContext -import ani.dantotsu.download.manga.MangaDownloaderService -import ani.dantotsu.download.manga.ServiceDataSingleton import ani.dantotsu.logger +import ani.dantotsu.media.anime.AnimeNameAdapter import ani.dantotsu.media.manga.ImageData import ani.dantotsu.media.manga.MangaCache import com.google.firebase.crashlytics.FirebaseCrashlytics -import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource -import eu.kanade.tachiyomi.animesource.model.AnimeFilter -import eu.kanade.tachiyomi.animesource.model.SEpisode -import eu.kanade.tachiyomi.animesource.model.AnimeFilterList +import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource import eu.kanade.tachiyomi.animesource.model.AnimesPage import eu.kanade.tachiyomi.animesource.model.SAnime +import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.Track import eu.kanade.tachiyomi.animesource.model.Video +import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.manga.model.MangaExtension -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.network.interceptor.CloudflareBypassException import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.lang.awaitSingle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -47,16 +40,13 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.io.File import java.io.FileOutputStream -import java.io.OutputStream +import java.io.UnsupportedEncodingException import java.net.URL import java.net.URLDecoder -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.* -import java.io.UnsupportedEncodingException import java.util.regex.Pattern class AniyomiAdapter { @@ -114,12 +104,19 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { it } } else { - // Sort by the episode_number field - res.sortedBy { it.episode_number } + var episodeCounter = 1f + // Group by season, sort within each season, and then renumber while keeping episode number 0 as is + val seasonGroups = + res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 } + seasonGroups.keys.sorted().flatMap { season -> + seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode -> + if (episode.episode_number != 0f) { // Skip renumbering for episode number 0 + episode.episode_number = episodeCounter++ + } + episode + } ?: emptyList() + } } - - // Transform SEpisode objects to Episode objects - return sortedEpisodes.map { SEpisodeToEpisode(it) } } catch (e: Exception) { println("Exception: $e") @@ -163,7 +160,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() { extension.sources[sourceLanguage] } as? AnimeCatalogueSource ?: return emptyList() return try { - val res = source.fetchSearchAnime(1, query, AnimeFilterList()).toBlocking().first() + val res = source.fetchSearchAnime(1, query, source.getFilterList()).awaitSingle() convertAnimesPageToShowResponse(res) } catch (e: CloudflareBypassException) { logger("Exception in search: $e") @@ -305,7 +302,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { return ret } - suspend fun imageList(chapterLink: String, sChapter: SChapter): List{ + suspend fun imageList(chapterLink: String, sChapter: SChapter): List { val source = try { extension.sources[sourceLanguage] } catch (e: Exception) { @@ -351,7 +348,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { println("Response: ${response.message}") // Convert the Response to an InputStream - val inputStream = response.body?.byteStream() + val inputStream = response.body.byteStream() // Convert InputStream to Bitmap val bitmap = BitmapFactory.decodeStream(inputStream) @@ -382,7 +379,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { val response = httpSource.getImage(page) // Convert the Response to an InputStream - val inputStream = response.body?.byteStream() + val inputStream = response.body.byteStream() // Convert InputStream to Bitmap val bitmap = BitmapFactory.decodeStream(inputStream) @@ -462,7 +459,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() { } as? HttpSource ?: return emptyList() return try { - val res = source.fetchSearchManga(1, query, FilterList()).toBlocking().first() + val res = source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle() logger("res observable: $res") convertMangasPageToShowResponse(res) } catch (e: CloudflareBypassException) { @@ -581,7 +578,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() { } } - private fun AniVideoToSaiVideo(aniVideo: eu.kanade.tachiyomi.animesource.model.Video): ani.dantotsu.parsers.Video { + private fun AniVideoToSaiVideo(aniVideo: Video): ani.dantotsu.parsers.Video { // Find the number value from the .quality string val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0 @@ -611,14 +608,15 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() { // If the format is still undetermined, log an error or handle it appropriately if (format == null) { logger("Unknown video format: $videoUrl") - FirebaseCrashlytics.getInstance().recordException(Exception("Unknown video format: $videoUrl")) + FirebaseCrashlytics.getInstance() + .recordException(Exception("Unknown video format: $videoUrl")) format = VideoType.CONTAINER } val headersMap: Map = aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf() - return ani.dantotsu.parsers.Video( + return Video( number, format, FileUrl(videoUrl, headersMap), @@ -650,7 +648,7 @@ class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() { private fun findSubtitleType(url: String): SubtitleType? { // First, try to determine the type based on the URL file extension - val type: SubtitleType? = when { + val type: SubtitleType = when { url.endsWith(".vtt", true) -> SubtitleType.VTT url.endsWith(".ass", true) -> SubtitleType.ASS url.endsWith(".srt", true) -> SubtitleType.SRT diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt index f8df772b..03562f68 100644 --- a/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/BaseParser.kt @@ -1,13 +1,18 @@ package ani.dantotsu.parsers -import ani.dantotsu.* +import ani.dantotsu.FileUrl +import ani.dantotsu.R +import ani.dantotsu.currContext +import ani.dantotsu.loadData +import ani.dantotsu.logger import ani.dantotsu.media.Media +import ani.dantotsu.saveData import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.source.model.SManga +import me.xdrop.fuzzywuzzy.FuzzySearch import java.io.Serializable import java.net.URLDecoder import java.net.URLEncoder -import me.xdrop.fuzzywuzzy.FuzzySearch abstract class BaseParser { @@ -50,24 +55,44 @@ abstract class BaseParser { * Isn't necessary to override, but recommended, if you want to improve auto search results * **/ open suspend fun autoSearch(mediaObj: Media): ShowResponse? { - var response: ShowResponse? = null//loadSavedShowResponse(mediaObj.id) + var response: ShowResponse? = loadSavedShowResponse(mediaObj.id) if (response != null && this !is OfflineMangaParser) { saveShowResponse(mediaObj.id, response, true) } else { setUserText("Searching : ${mediaObj.mainName()}") + logger("Searching : ${mediaObj.mainName()}") val results = search(mediaObj.mainName()) + //log all results + results.forEach { + logger("Result: ${it.name}") + } val sortedResults = if (results.isNotEmpty()) { - results.sortedByDescending { FuzzySearch.ratio(it.name.lowercase(), mediaObj.mainName().lowercase()) } + results.sortedByDescending { + FuzzySearch.ratio( + it.name.lowercase(), + mediaObj.mainName().lowercase() + ) + } } else { emptyList() } response = sortedResults.firstOrNull() - if (response == null || FuzzySearch.ratio(response.name.lowercase(), mediaObj.mainName().lowercase()) < 100) { + if (response == null || FuzzySearch.ratio( + response.name.lowercase(), + mediaObj.mainName().lowercase() + ) < 100 + ) { setUserText("Searching : ${mediaObj.nameRomaji}") + logger("Searching : ${mediaObj.nameRomaji}") val romajiResults = search(mediaObj.nameRomaji) val sortedRomajiResults = if (romajiResults.isNotEmpty()) { - romajiResults.sortedByDescending { FuzzySearch.ratio(it.name.lowercase(), mediaObj.nameRomaji.lowercase()) } + romajiResults.sortedByDescending { + FuzzySearch.ratio( + it.name.lowercase(), + mediaObj.nameRomaji.lowercase() + ) + } } else { emptyList() } @@ -78,8 +103,14 @@ abstract class BaseParser { logger("No exact match found in results. Using closest match from RomajiResults.") closestRomaji } else { - val romajiRatio = FuzzySearch.ratio(closestRomaji?.name?.lowercase() ?: "", mediaObj.nameRomaji.lowercase()) - val mainNameRatio = FuzzySearch.ratio(response.name.lowercase(), mediaObj.mainName().lowercase()) + val romajiRatio = FuzzySearch.ratio( + closestRomaji?.name?.lowercase() ?: "", + mediaObj.nameRomaji.lowercase() + ) + val mainNameRatio = FuzzySearch.ratio( + response.name.lowercase(), + mediaObj.mainName().lowercase() + ) logger("Fuzzy ratio for closest match in results: $mainNameRatio for ${response.name.lowercase()}") logger("Fuzzy ratio for closest match in RomajiResults: $romajiRatio for ${closestRomaji?.name?.lowercase() ?: "None"}") @@ -113,7 +144,13 @@ abstract class BaseParser { open fun saveShowResponse(mediaId: Int, response: ShowResponse?, selected: Boolean = false) { if (response != null) { checkIfVariablesAreEmpty() - setUserText("${if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString(R.string.found)} : ${response.name}") + setUserText( + "${ + if (selected) currContext()!!.getString(R.string.selected) else currContext()!!.getString( + R.string.found + ) + } : ${response.name}" + ) saveData("${saveName}_$mediaId", response) } } @@ -161,7 +198,7 @@ data class ShowResponse( val total: Int? = null, //In case you want to sent some extra data - val extra : Map?=null, + val extra: MutableMap? = null, //SAnime object from Aniyomi val sAnime: SAnime? = null, @@ -169,10 +206,23 @@ data class ShowResponse( //SManga object from Aniyomi val sManga: SManga? = null ) : Serializable { - constructor(name: String, link: String, coverUrl: String, otherNames: List = listOf(), total: Int? = null, extra: Map?=null) + constructor( + name: String, + link: String, + coverUrl: String, + otherNames: List = listOf(), + total: Int? = null, + extra: MutableMap? = null + ) : this(name, link, FileUrl(coverUrl), otherNames, total, extra) - constructor(name: String, link: String, coverUrl: String, otherNames: List = listOf(), total: Int? = null) + constructor( + name: String, + link: String, + coverUrl: String, + otherNames: List = listOf(), + total: Int? = null + ) : this(name, link, FileUrl(coverUrl), otherNames, total) constructor(name: String, link: String, coverUrl: String, otherNames: List = listOf()) diff --git a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt index 09292577..f8535772 100644 --- a/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/BaseSources.kt @@ -2,9 +2,9 @@ package ani.dantotsu.parsers import ani.dantotsu.Lazier import ani.dantotsu.logger +import ani.dantotsu.media.Media import ani.dantotsu.media.anime.Episode import ani.dantotsu.media.manga.MangaChapter -import ani.dantotsu.media.Media import ani.dantotsu.tryWithSuspend import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.source.model.SManga @@ -30,7 +30,6 @@ abstract class WatchSources : BaseSources() { extra: Map?, sAnime: SAnime? ): MutableMap { - println("finder333 $showLink") val map = mutableMapOf() val parser = get(i) tryWithSuspend(true) { diff --git a/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt index ed481c9d..8948d495 100644 --- a/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/MangaParser.kt @@ -1,8 +1,6 @@ package ani.dantotsu.parsers -import android.graphics.Bitmap import ani.dantotsu.FileUrl -import ani.dantotsu.media.Media import ani.dantotsu.media.manga.MangaNameAdapter import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import eu.kanade.tachiyomi.source.model.Page @@ -15,7 +13,11 @@ abstract class MangaParser : BaseParser() { /** * Takes ShowResponse.link and ShowResponse.extra (if any) as arguments & gives a list of total chapters present on the site. * **/ - abstract suspend fun loadChapters(mangaLink: String, extra: Map?, sManga: SManga): List + abstract suspend fun loadChapters( + mangaLink: String, + extra: Map?, + sManga: SManga + ): List /** * Takes ShowResponse.link, ShowResponse.extra & the Last Largest Chapter Number known by app as arguments @@ -23,9 +25,14 @@ abstract class MangaParser : BaseParser() { * Returns the latest chapter (If overriding, Make sure the chapter is actually the latest chapter) * Returns null, if no latest chapter is found. * **/ - open suspend fun getLatestChapter(mangaLink: String, extra: Map?, sManga: SManga, latest: Float): MangaChapter? { - val chapter = loadChapters(mangaLink, extra, sManga) - val max = chapter + open suspend fun getLatestChapter( + mangaLink: String, + extra: Map?, + sManga: SManga, + latest: Float + ): MangaChapter? { + val chapter = loadChapters(mangaLink, extra, sManga) + val max = chapter .maxByOrNull { MangaNameAdapter.findChapterNumber(it.number) ?: 0f } return max ?.takeIf { latest < (MangaNameAdapter.findChapterNumber(it.number) ?: 0.001f) } @@ -40,13 +47,18 @@ abstract class MangaParser : BaseParser() { open fun getTransformation(): BitmapTransformation? = null } -class EmptyMangaParser: MangaParser() { +class EmptyMangaParser : MangaParser() { override val name: String = "None" override val saveName: String = "None" - override suspend fun loadChapters(mangaLink: String, extra: Map?, sManga: SManga): List = emptyList() + override suspend fun loadChapters( + mangaLink: String, + extra: Map?, + sManga: SManga + ): List = emptyList() - override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List = emptyList() + override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List = + emptyList() override suspend fun search(query: String): List = emptyList() } @@ -82,7 +94,7 @@ data class MangaImage( val useTransformation: Boolean = false, val page: Page? = null, -) : Serializable{ - constructor(url: String,useTransformation: Boolean=false, page: Page? = null) - : this(FileUrl(url),useTransformation, page) +) : Serializable { + constructor(url: String, useTransformation: Boolean = false, page: Page? = null) + : this(FileUrl(url), useTransformation, page) } diff --git a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt index 511049ce..0f8a5642 100644 --- a/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/MangaSources.kt @@ -15,11 +15,17 @@ object MangaSources : MangaReadSources() { suspend fun init(fromExtensions: StateFlow>) { // Initialize with the first value from StateFlow val initialExtensions = fromExtensions.first() - list = createParsersFromExtensions(initialExtensions) + Lazier({ OfflineMangaParser() }, "Downloaded") + list = createParsersFromExtensions(initialExtensions) + Lazier( + { OfflineMangaParser() }, + "Downloaded" + ) // Update as StateFlow emits new values fromExtensions.collect { extensions -> - list = createParsersFromExtensions(extensions) + Lazier({ OfflineMangaParser() }, "Downloaded") + list = createParsersFromExtensions(extensions) + Lazier( + { OfflineMangaParser() }, + "Downloaded" + ) } } @@ -34,7 +40,8 @@ object MangaSources : MangaReadSources() { object HMangaSources : MangaReadSources() { val aList: List> = lazyList() suspend fun init(fromExtensions: StateFlow>) { - //todo + //todo } - override val list = listOf(aList,MangaSources.list).flatten() + + override val list = listOf(aList, MangaSources.list).flatten() } diff --git a/app/src/main/java/ani/dantotsu/parsers/NovelInterface.kt b/app/src/main/java/ani/dantotsu/parsers/NovelInterface.kt new file mode 100644 index 00000000..af3be5cd --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/NovelInterface.kt @@ -0,0 +1,9 @@ +package ani.dantotsu.parsers + +import com.lagradost.nicehttp.Requests + + +interface NovelInterface { + suspend fun search(query: String, client: Requests): List + suspend fun loadBook(link: String, extra: Map?, client: Requests): Book +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt index 33bc00d6..9a625600 100644 --- a/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/NovelParser.kt @@ -9,7 +9,7 @@ abstract class NovelParser : BaseParser() { abstract suspend fun loadBook(link: String, extra: Map?): Book - fun List.sortByVolume(query:String) : List { + fun List.sortByVolume(query: String): List { val sorted = groupBy { res -> val match = volumeRegex.find(res.name)?.groupValues ?.firstOrNull { it.isNotEmpty() } @@ -31,8 +31,20 @@ abstract class NovelParser : BaseParser() { } suspend fun sortedSearch(mediaObj: Media): List { - val query = mediaObj.name ?: mediaObj.nameRomaji - return search(query).sortByVolume(query) + //val query = mediaObj.name ?: mediaObj.nameRomaji + //return search(query).sortByVolume(query) + val results: List + return if(mediaObj.name != null) { + val query = mediaObj.name + results = search(query).sortByVolume(query) + results.ifEmpty { + val q = mediaObj.nameRomaji + search(q).sortByVolume(q) + } + } else { + val query = mediaObj.nameRomaji + search(query).sortByVolume(query) + } } } @@ -42,7 +54,12 @@ data class Book( val description: String? = null, val links: List ) { - constructor (name: String, img: String, description: String? = null, links: List) : this( + constructor ( + name: String, + img: String, + description: String? = null, + links: List + ) : this( name, FileUrl(img), description, diff --git a/app/src/main/java/ani/dantotsu/parsers/NovelSources.kt b/app/src/main/java/ani/dantotsu/parsers/NovelSources.kt index fb5445dd..e5298bce 100644 --- a/app/src/main/java/ani/dantotsu/parsers/NovelSources.kt +++ b/app/src/main/java/ani/dantotsu/parsers/NovelSources.kt @@ -1,9 +1,38 @@ package ani.dantotsu.parsers +import android.util.Log import ani.dantotsu.Lazier -import ani.dantotsu.lazyList +import ani.dantotsu.parsers.novel.DynamicNovelParser +import ani.dantotsu.parsers.novel.NovelExtension +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first object NovelSources : NovelReadSources() { - override val list: List> = lazyList( - ) + override var list: List> = emptyList() + + suspend fun init(fromExtensions: StateFlow>) { + // Initialize with the first value from StateFlow + val initialExtensions = fromExtensions.first() + list = createParsersFromExtensions(initialExtensions) + Lazier( + { OfflineNovelParser() }, + "Downloaded" + ) + + // Update as StateFlow emits new values + fromExtensions.collect { extensions -> + list = createParsersFromExtensions(extensions) + Lazier( + { OfflineNovelParser() }, + "Downloaded" + ) + } + } + + private fun createParsersFromExtensions(extensions: List): List> { + Log.d("NovelSources", "createParsersFromExtensions") + Log.d("NovelSources", extensions.toString()) + return extensions.map { extension -> + val name = extension.name + Lazier({ DynamicNovelParser(extension) }, name) + } + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt index 3684ee8b..218a0f9a 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt @@ -12,7 +12,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File -class OfflineMangaParser: MangaParser() { +class OfflineMangaParser : MangaParser() { private val downloadManager = Injekt.get() override val hostUrl: String = "Offline" @@ -32,7 +32,14 @@ class OfflineMangaParser: MangaParser() { if (directory.exists()) { directory.listFiles()?.forEach { if (it.isDirectory) { - val chapter = MangaChapter(it.name, "$mangaLink/${it.name}", it.name, null, null, SChapter.create()) + val chapter = MangaChapter( + it.name, + "$mangaLink/${it.name}", + it.name, + null, + null, + SChapter.create() + ) chapters.add(chapter) } } diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt new file mode 100644 index 00000000..ca47b50a --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt @@ -0,0 +1,86 @@ +package ani.dantotsu.parsers + +import android.os.Environment +import ani.dantotsu.currContext +import ani.dantotsu.download.DownloadsManager +import ani.dantotsu.logger +import ani.dantotsu.media.manga.MangaNameAdapter +import eu.kanade.tachiyomi.source.model.SChapter +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 OfflineNovelParser: NovelParser() { + private val downloadManager = Injekt.get() + + override val hostUrl: String = "Offline" + override val name: String = "Offline" + override val saveName: String = "Offline" + + override val volumeRegex = + Regex("vol\\.? (\\d+(\\.\\d+)?)|volume (\\d+(\\.\\d+)?)", RegexOption.IGNORE_CASE) + + override suspend fun loadBook(link: String, extra: Map?): Book { + //link should be a directory + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Novel/$link" + ) + val chapters = mutableListOf() + if (directory.exists()) { + directory.listFiles()?.forEach { + if (it.isDirectory) { + val chapter = Book( + it.name, + it.absolutePath + "/cover.jpg", + null, + listOf(it.absolutePath + "/0.epub") + ) + chapters.add(chapter) + } + } + chapters.sortBy { MangaNameAdapter.findChapterNumber(it.name) } + return chapters.first() + } + return Book( + "error", + "", + null, + listOf("error") + ) + } + + override suspend fun search(query: String): List { + val titles = downloadManager.novelDownloads.map { it.title }.distinct() + val returnTitles: MutableList = mutableListOf() + for (title in titles) { + if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { + returnTitles.add(title) + } + } + val returnList: MutableList = mutableListOf() + for (title in returnTitles) { + //need to search the subdirectories for the ShowResponses + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Novel/$title" + ) + val names = mutableListOf() + if (directory.exists()) { + directory.listFiles()?.forEach { + if (it.isDirectory) { + names.add(it.name) + } + } + } + val cover = currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + "/Dantotsu/Novel/$title/cover.jpg" + names.forEach { + returnList.add(ShowResponse(it, it, cover)) + } + } + return returnList + } + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/StringMatcher.kt b/app/src/main/java/ani/dantotsu/parsers/StringMatcher.kt index a7bf0cd3..191fecff 100644 --- a/app/src/main/java/ani/dantotsu/parsers/StringMatcher.kt +++ b/app/src/main/java/ani/dantotsu/parsers/StringMatcher.kt @@ -58,10 +58,16 @@ class StringMatcher { return shows // Return original list if no closest show found } logger("Closest show found for $target is ${closestShowAndIndex.first.name}") - return listOf(shows[closestIndex]) + shows.subList(0, closestIndex) + shows.subList(closestIndex + 1, shows.size) + return listOf(shows[closestIndex]) + shows.subList(0, closestIndex) + shows.subList( + closestIndex + 1, + shows.size + ) } - private fun closestShow(target: String, shows: List): Pair { + private fun closestShow( + target: String, + shows: List + ): Pair { var minDistance = Int.MAX_VALUE var closestShow = ShowResponse("", "", "") var closestIndex = -1 diff --git a/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt b/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt index fc6ae7c8..ce56182c 100644 --- a/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt +++ b/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt @@ -56,14 +56,19 @@ abstract class VideoExtractor : Serializable { data class VideoServer( val name: String, val embed: FileUrl, - val extraData : Map?=null, + val extraData: Map? = null, val video: eu.kanade.tachiyomi.animesource.model.Video? = null ) : Serializable { - constructor(name: String, embedUrl: String,extraData: Map?=null) - : this(name, FileUrl(embedUrl),extraData) + constructor(name: String, embedUrl: String, extraData: Map? = null) + : this(name, FileUrl(embedUrl), extraData) - constructor(name: String, embedUrl: String,extraData: Map?=null, video: eu.kanade.tachiyomi.animesource.model.Video?) - : this(name, FileUrl(embedUrl),extraData, video) + constructor( + name: String, + embedUrl: String, + extraData: Map? = null, + video: eu.kanade.tachiyomi.animesource.model.Video? + ) + : this(name, FileUrl(embedUrl), extraData, video) } /** @@ -117,7 +122,13 @@ data class Video( val extraNote: String? = null, ) : Serializable { - constructor(quality: Int? = null, videoType: VideoType, url: String, size: Double?, extraNote: String? = null) + constructor( + quality: Int? = null, + videoType: VideoType, + url: String, + size: Double?, + extraNote: String? = null + ) : this(quality, videoType, FileUrl(url), size, extraNote) constructor(quality: Int? = null, videoType: VideoType, url: String, size: Double?) @@ -149,15 +160,19 @@ data class Subtitle( * * Supports VTT, SRT & ASS * **/ - val type:SubtitleType = SubtitleType.VTT, + val type: SubtitleType = SubtitleType.VTT, ) : Serializable { - constructor(language: String, url: String, type: SubtitleType = SubtitleType.VTT) : this(language, FileUrl(url), type) + constructor(language: String, url: String, type: SubtitleType = SubtitleType.VTT) : this( + language, + FileUrl(url), + type + ) } -enum class VideoType{ +enum class VideoType { CONTAINER, M3U8, DASH } -enum class SubtitleType{ +enum class SubtitleType { VTT, ASS, SRT, UNKNOWN } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelAdapter.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelAdapter.kt new file mode 100644 index 00000000..13da2ce2 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelAdapter.kt @@ -0,0 +1,41 @@ +package ani.dantotsu.parsers.novel + +import ani.dantotsu.parsers.Book +import ani.dantotsu.parsers.NovelInterface +import ani.dantotsu.parsers.NovelParser +import ani.dantotsu.parsers.ShowResponse +import eu.kanade.tachiyomi.network.NetworkHelper +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class NovelAdapter + +class DynamicNovelParser(extension: NovelExtension.Installed) : NovelParser() { + override val volumeRegex = + Regex("vol\\.? (\\d+(\\.\\d+)?)|volume (\\d+(\\.\\d+)?)", RegexOption.IGNORE_CASE) + var extension: NovelExtension.Installed + val client = Injekt.get().requestClient + + init { + this.extension = extension + } + + override suspend fun search(query: String): List { + val source = extension.sources.firstOrNull() + if (source is NovelInterface) { + return source.search(query, client) + } else { + return emptyList() + } + } + + override suspend fun loadBook(link: String, extra: Map?): Book { + val source = extension.sources.firstOrNull() + if (source is NovelInterface) { + return source.loadBook(link, extra, client) + } else { + return Book("", "", "", emptyList()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtension.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtension.kt new file mode 100644 index 00000000..42595aaf --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtension.kt @@ -0,0 +1,56 @@ +package ani.dantotsu.parsers.novel + +import android.graphics.drawable.Drawable +import ani.dantotsu.parsers.NovelInterface + +sealed class NovelExtension { + abstract val name: String + abstract val pkgName: String + abstract val versionName: String + abstract val versionCode: Long + + data class Installed( + override val name: String, + override val pkgName: String, + override val versionName: String, + override val versionCode: Long, + val sources: List, + val icon: Drawable?, + val hasUpdate: Boolean = false, + val isObsolete: Boolean = false, + val isUnofficial: Boolean = false, + ) : NovelExtension() + + data class Available( + override val name: String, + override val pkgName: String, + override val versionName: String, + override val versionCode: Long, + val sources: List, + val iconUrl: String, + ) : NovelExtension() +} + +data class AvailableNovelSources( + val id: Long, + val lang: String, + val name: String, + val baseUrl: String, +) { + fun toNovelSourceData(): NovelSourceData { + return NovelSourceData( + id = this.id, + lang = this.lang, + name = this.name, + ) + } +} + +data class NovelSourceData( + val id: Long, + val lang: String, + val name: String, +) { + + val isMissingInfo: Boolean = name.isBlank() || lang.isBlank() +} diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt new file mode 100644 index 00000000..ea23fbeb --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionGithubApi.kt @@ -0,0 +1,189 @@ +package ani.dantotsu.parsers.novel + + +import android.content.Context +import ani.dantotsu.currContext +import ani.dantotsu.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.network.GET +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.network.awaitSuccess +import eu.kanade.tachiyomi.network.parseAs +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import logcat.LogPriority +import tachiyomi.core.util.lang.withIOContext +import tachiyomi.core.util.system.logcat +import uy.kohesive.injekt.injectLazy +import java.util.Date +import kotlin.time.Duration.Companion.days + +class NovelExtensionGithubApi { + + private val networkService: NetworkHelper by injectLazy() + private val novelExtensionManager: NovelExtensionManager by injectLazy() + private val json: Json by injectLazy() + + private val lastExtCheck: Long = + currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + ?.getLong("last_ext_check", 0) ?: 0 + + private var requiresFallbackSource = false + + suspend fun findExtensions(): List { + return withIOContext { + val githubResponse = if (requiresFallbackSource) { + null + } else { + try { + networkService.client + .newCall(GET("${REPO_URL_PREFIX}index.min.json")) + .awaitSuccess() + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) { "Failed to get extensions from GitHub" } + requiresFallbackSource = true + null + } + } + + val response = githubResponse ?: run { + logger("using fallback source") + networkService.client + .newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json")) + .awaitSuccess() + } + + logger("response: $response") + + val extensions = with(json) { + response + .parseAs>() + .toExtensions() + } + + // Sanity check - a small number of extensions probably means something broke + // with the repo generator + /*if (extensions.size < 10) { //TODO: uncomment when more extensions are added + throw Exception() + }*/ + logger("extensions: $extensions") + extensions + } + } + + suspend fun checkForUpdates( + context: Context, + fromAvailableExtensionList: Boolean = false + ): List? { + // Limit checks to once a day at most + if (fromAvailableExtensionList && Date().time < lastExtCheck + 1.days.inWholeMilliseconds) { + return null + } + + val extensions = if (fromAvailableExtensionList) { + novelExtensionManager.availableExtensionsFlow.value + } else { + findExtensions().also { + context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit() + ?.putLong("last_ext_check", Date().time)?.apply() + } + } + + val installedExtensions = NovelExtensionLoader.loadExtensions(context) + .filterIsInstance() + .map { it.extension } + + val extensionsWithUpdate = mutableListOf() + for (installedExt in installedExtensions) { + val pkgName = installedExt.pkgName + val availableExt = extensions.find { it.pkgName == pkgName } ?: continue + + val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode + val hasUpdate = installedExt.isUnofficial.not() && (hasUpdatedVer) + if (hasUpdate) { + extensionsWithUpdate.add(installedExt) + } + } + + if (extensionsWithUpdate.isNotEmpty()) { + ExtensionUpdateNotifier(context).promptUpdates(extensionsWithUpdate.map { it.name }) + } + + return extensionsWithUpdate + } + + private fun List.toExtensions(): List { + return mapNotNull { extension -> + val sources = extension.sources?.map { source -> + NovelExtensionSourceJsonObject( + source.id, + source.lang, + source.name, + source.baseUrl, + ) + } + val iconUrl = "${REPO_URL_PREFIX}icon/${extension.pkg}.png" + NovelExtension.Available( + extension.name, + extension.pkg, + extension.apk, + extension.code, + sources?.toSources() ?: emptyList(), + iconUrl, + ) + } + } + + private fun List.toSources(): List { + return map { source -> + AvailableNovelSources( + source.id, + source.lang, + source.name, + source.baseUrl, + ) + } + } + + fun getApkUrl(extension: NovelExtension.Available): String { + return "${getUrlPrefix()}apk/${extension.pkgName}.apk" + } + + private fun getUrlPrefix(): String { + return if (requiresFallbackSource) { + FALLBACK_REPO_URL_PREFIX + } else { + REPO_URL_PREFIX + } + } +} + +private const val REPO_URL_PREFIX = + "https://raw.githubusercontent.com/dannovels/novel-extensions/main/" +private const val FALLBACK_REPO_URL_PREFIX = + "https://gcore.jsdelivr.net/gh/dannovels/novel-extensions@latest/" + +@Serializable +private data class NovelExtensionJsonObject( + val name: String, + val pkg: String, + val apk: String, + val lang: String, + val code: Long, + val version: String, + val nsfw: Int, + val hasReadme: Int = 0, + val hasChangelog: Int = 0, + val sources: List?, +) + +@Serializable +private data class NovelExtensionSourceJsonObject( + val id: Long, + val lang: String, + val name: String, + val baseUrl: String, +) + diff --git a/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionInstallReceiver.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionInstallReceiver.kt new file mode 100644 index 00000000..7835cb1d --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionInstallReceiver.kt @@ -0,0 +1,66 @@ +package ani.dantotsu.parsers.novel + +import android.os.FileObserver +import android.util.Log +import ani.dantotsu.parsers.novel.FileObserver.fileObserver +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?) { + Log.e("NovelExtensionFileObserver", "Event: $event") + if (file == null) return + + val fullPath = File(path, file) + + when (event) { + CREATE -> { + Log.e("NovelExtensionFileObserver", "File created: $fullPath") + listener.onExtensionFileCreated(fullPath) + } + + DELETE -> { + Log.e("NovelExtensionFileObserver", "File deleted: $fullPath") + listener.onExtensionFileDeleted(fullPath) + } + + MODIFY -> { + Log.e("NovelExtensionFileObserver", "File modified: $fullPath") + listener.onExtensionFileModified(fullPath) + } + } + } + + /** + * Loads the extension from the file. + * + * @param file The file name of the extension. + */ + //private suspend fun loadExtensionFromFile(file: String): String { + // return file + //} + + 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/NovelExtensionInstaller.kt b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionInstaller.kt new file mode 100644 index 00000000..8bbf0561 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionInstaller.kt @@ -0,0 +1,384 @@ +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 android.util.Log +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import androidx.core.net.toUri +import ani.dantotsu.snackString +import com.jakewharton.rxrelay.PublishRelay +import eu.kanade.tachiyomi.extension.InstallStep +import eu.kanade.tachiyomi.util.storage.getUriCompat +import logcat.LogPriority +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import tachiyomi.core.util.system.logcat +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.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()) { + Log.i("Install APK", "APK file deleted successfully.") + } else { + Log.e("Install APK", "Failed to delete APK file.") + } + } else { + Log.e("Install APK", "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" + + val destinationPathDirectory = + context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/" + val destinationPathDirectoryFile = File(destinationPathDirectory) + + + // Check if source path is obtained correctly + if (sourcePath == null) { + Log.e("Install APK", "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) { + Log.e("Install APK", "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) + Log.i("Install APK", "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()) { + Log.i("Uninstall APK", "File is not writable. Giving write permission.") + val a = fileToDelete.setWritable(true) + Log.i("Uninstall APK", "Success: $a") + } + //set the directory to writable + val destinationDir = File(apkPath).parentFile + if (destinationDir?.exists() == false) { + destinationDir.mkdirs() + } + val s = destinationDir?.setWritable(true) + Log.i("Uninstall APK", "Success destinationDir: $s") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + Files.delete(fileToDelete.toPath()) + } catch (e: Exception) { + Log.e("Uninstall APK", "Failed to delete APK file.") + Log.e("Uninstall APK", e.toString()) + snackString("Failed to delete APK file.") + } + } else { + if (fileToDelete.exists()) { + if (fileToDelete.delete()) { + Log.i("Uninstall APK", "APK file deleted successfully.") + snackString("APK file deleted successfully.") + } else { + Log.e("Uninstall APK", "Failed to delete APK file.") + snackString("Failed to delete APK file.") + } + } else { + Log.e("Uninstall APK", "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()) { + Log.i("File Copy", "File deleted successfully.") + } else { + Log.e("File Copy", "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() + } + + Log.i("File Copy", "File copied to internal storage.") + } + + 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) { + logcat(LogPriority.ERROR) { "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") + Log.i("Install APK", "Package name: $pkgName") + return pkgName ?: "" + } + } + + companion object { + const val APK_MIME = "application/vnd.android.package-archive" + const val EXTRA_DOWNLOAD_ID = "NovelExtensionInstaller.extra.DOWNLOAD_ID" + 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 new file mode 100644 index 00000000..b0f6693b --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionLoader.kt @@ -0,0 +1,152 @@ +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 android.util.Log +import ani.dantotsu.logger +import ani.dantotsu.parsers.NovelInterface +import ani.dantotsu.snackString +import com.google.firebase.crashlytics.FirebaseCrashlytics +import dalvik.system.PathClassLoader +import eu.kanade.tachiyomi.util.lang.Hash +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 + Log.e("NovelExtensionLoader", "Loading extensions from $installDir") + Log.e( + "NovelExtensionLoader", + "Loading extensions from ${File(installDir).listFiles()?.size}" + ) + File(installDir).setWritable(false) + File(installDir).listFiles()?.forEach { + //set the file to read only + it.setWritable(false) + Log.e("NovelExtensionLoader", "Loading extension ${it.name}") + val extension = loadExtension(context, it) + if (extension is NovelLoadResult.Success) { + results.add(extension) + } else { + logger("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. + */ + 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) + } + val pkgInfo = try { + context.packageManager.getPackageArchiveInfo(path, 0) + } catch (error: Exception) { + // Unlikely, but the package may have been uninstalled at this point + logger("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("Package ${packageInfo.packageName} isn't signed") + logger("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 { + Log.e("NovelExtensionLoader", "isFileWritable: ${file.canWrite()}") + if (file.canWrite()) { + val a = file.setWritable(false) + Log.e("NovelExtensionLoader", "success: $a") + } + Log.e("NovelExtensionLoader", "isFileWritable: ${file.canWrite()}") + val classLoader = PathClassLoader(file.absolutePath, null, context.classLoader) + val className = + "some.random.novelextensions.${className.lowercase(Locale.getDefault())}.$className" + val loadedClass = classLoader.loadClass(className) + val instance = loadedClass.newInstance() + val novelInterfaceInstance = instance as? NovelInterface + listOfNotNull(novelInterfaceInstance) + } catch (e: Exception) { + e.printStackTrace() + FirebaseCrashlytics.getInstance().recordException(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 new file mode 100644 index 00000000..c7a9723c --- /dev/null +++ b/app/src/main/java/ani/dantotsu/parsers/novel/NovelExtensionManager.kt @@ -0,0 +1,247 @@ +package ani.dantotsu.parsers.novel + +import android.content.Context +import android.graphics.drawable.Drawable +import ani.dantotsu.logger +import ani.dantotsu.snackString +import eu.kanade.tachiyomi.extension.InstallStep +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 + private set + + + /** + * API where all the available Novel extensions can be found. + */ + private val api = NovelExtensionGithubApi() + + /** + * The installer which installs, updates and uninstalls the Novel extensions. + */ + private val installer by lazy { NovelExtensionInstaller(context) } + + private val iconMap = mutableMapOf() + + private val _installedNovelExtensionsFlow = + MutableStateFlow(emptyList()) + val installedExtensionsFlow = _installedNovelExtensionsFlow.asStateFlow() + + private val _availableNovelExtensionsFlow = + MutableStateFlow(emptyList()) + val availableExtensionsFlow = _availableNovelExtensionsFlow.asStateFlow() + + private var availableNovelExtensionsSourcesData: Map = emptyMap() + + private fun setupAvailableNovelExtensionsSourcesDataMap(novelExtensions: List) { + if (novelExtensions.isEmpty()) return + availableNovelExtensionsSourcesData = novelExtensions + .flatMap { ext -> ext.sources.map { it.toNovelSourceData() } } + .associateBy { it.id } + } + + fun getSourceData(id: Long) = availableNovelExtensionsSourcesData[id] + + init { + initNovelExtensions() + val path = context.getExternalFilesDir(null)?.absolutePath + "/extensions/novel/" + NovelExtensionFileObserver(NovelInstallationListener(), path).register() + } + + private fun initNovelExtensions() { + val novelExtensions = NovelExtensionLoader.loadExtensions(context) + + _installedNovelExtensionsFlow.value = novelExtensions + .filterIsInstance() + .map { it.extension } + + isInitialized = true + } + + /** + * Finds the available manga extensions in the [api] and updates [availableExtensions]. + */ + suspend fun findAvailableExtensions() { + val extensions: List = try { + api.findExtensions() + } catch (e: Exception) { + logger("Error finding extensions: ${e.message}") + withUIContext { snackString("Failed to get Novel extensions list") } + emptyList() + } + + _availableNovelExtensionsFlow.value = extensions + updatedInstalledNovelExtensionsStatuses(extensions) + setupAvailableNovelExtensionsSourcesDataMap(extensions) + } + + private fun updatedInstalledNovelExtensionsStatuses(availableNovelExtensions: List) { + if (availableNovelExtensions.isEmpty()) { + return + } + + val mutInstalledNovelExtensions = _installedNovelExtensionsFlow.value.toMutableList() + var hasChanges = false + + for ((index, installedExt) in mutInstalledNovelExtensions.withIndex()) { + val pkgName = installedExt.pkgName + val availableExt = availableNovelExtensions.find { it.pkgName == pkgName } + + if (availableExt == null && !installedExt.isObsolete) { + mutInstalledNovelExtensions[index] = installedExt.copy(isObsolete = true) + hasChanges = true + } else if (availableExt != null) { + val hasUpdate = installedExt.updateExists(availableExt) + + if (installedExt.hasUpdate != hasUpdate) { + mutInstalledNovelExtensions[index] = installedExt.copy(hasUpdate = hasUpdate) + hasChanges = true + } + } + } + if (hasChanges) { + _installedNovelExtensionsFlow.value = mutInstalledNovelExtensions + } + } + + /** + * Returns an observable of the installation process for the given novel extension. It will complete + * once the novel extension is installed or throws an error. The process will be canceled if + * unsubscribed before its completion. + * + * @param extension The anime extension to be installed. + */ + fun installExtension(extension: NovelExtension.Available): Observable { + return installer.downloadAndInstall(api.getApkUrl(extension), extension) + } + + /** + * Returns an observable of the installation process for the given anime extension. It will complete + * once the anime extension is updated or throws an error. The process will be canceled if + * unsubscribed before its completion. + * + * @param extension The anime extension to be updated. + */ + fun updateExtension(extension: NovelExtension.Installed): Observable { + val availableExt = + _availableNovelExtensionsFlow.value.find { it.pkgName == extension.pkgName } + ?: return Observable.empty() + return installExtension(availableExt) + } + + fun cancelInstallUpdateExtension(extension: NovelExtension) { + installer.cancelInstall(extension.pkgName) + } + + /** + * Sets to "installing" status of an novel extension installation. + * + * @param downloadId The id of the download. + */ + fun setInstalling(downloadId: Long) { + installer.updateInstallStep(downloadId, InstallStep.Installing) + } + + fun updateInstallStep(downloadId: Long, step: InstallStep) { + installer.updateInstallStep(downloadId, step) + } + + /** + * Uninstalls the novel extension that matches the given package name. + * + * @param pkgName The package name of the application to uninstall. + */ + fun uninstallExtension(pkgName: String, context: Context) { + installer.uninstallApk(pkgName, context) + } + + /** + * Registers the given novel extension in this and the source managers. + * + * @param extension The anime extension to be registered. + */ + private fun registerNewExtension(extension: NovelExtension.Installed) { + _installedNovelExtensionsFlow.value += extension + } + + /** + * Registers the given updated novel extension in this and the source managers previously removing + * the outdated ones. + * + * @param extension The anime extension to be registered. + */ + private fun registerUpdatedExtension(extension: NovelExtension.Installed) { + val mutInstalledNovelExtensions = _installedNovelExtensionsFlow.value.toMutableList() + val oldNovelExtension = mutInstalledNovelExtensions.find { it.pkgName == extension.pkgName } + if (oldNovelExtension != null) { + mutInstalledNovelExtensions -= oldNovelExtension + } + mutInstalledNovelExtensions += extension + _installedNovelExtensionsFlow.value = mutInstalledNovelExtensions + } + + /** + * Unregisters the novel extension in this and the source managers given its package name. Note this + * method is called for every uninstalled application in the system. + * + * @param pkgName The package name of the uninstalled application. + */ + private fun unregisterNovelExtension(pkgName: String) { + val installedNovelExtension = + _installedNovelExtensionsFlow.value.find { it.pkgName == pkgName } + if (installedNovelExtension != null) { + _installedNovelExtensionsFlow.value -= installedNovelExtension + } + } + + /** + * 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()) + } + } + } + + override fun onExtensionFileDeleted(file: File) { + val pkgName = file.nameWithoutExtension + unregisterNovelExtension(pkgName) + } + + override fun onExtensionFileModified(file: File) { + NovelExtensionLoader.loadExtension(context, file).let { + if (it is NovelLoadResult.Success) { + registerUpdatedExtension(it.extension.withUpdateCheck()) + } + } + } + } + + /** + * AnimeExtension method to set the update field of an installed anime extension. + */ + private fun NovelExtension.Installed.withUpdateCheck(): NovelExtension.Installed { + return if (updateExists()) { + copy(hasUpdate = true) + } else { + this + } + } + + private fun NovelExtension.Installed.updateExists(availableNovelExtension: NovelExtension.Available? = null): Boolean { + val availableExt = availableNovelExtension + ?: _availableNovelExtensionsFlow.value.find { it.pkgName == pkgName } + if (isUnofficial || availableExt == null) return false + + return (availableExt.versionCode > versionCode) + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt index 2ff5ef23..e46b1d71 100644 --- a/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/AnimeExtensionsFragment.kt @@ -17,6 +17,7 @@ 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 com.google.firebase.crashlytics.FirebaseCrashlytics import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager @@ -52,7 +53,8 @@ class AnimeExtensionsFragment : Fragment(), binding.allAnimeExtensionsRecyclerView.isNestedScrollingEnabled = false binding.allAnimeExtensionsRecyclerView.adapter = adapter binding.allAnimeExtensionsRecyclerView.layoutManager = LinearLayoutManager(context) - (binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = true + (binding.allAnimeExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = + true lifecycleScope.launch { viewModel.pagerFlow.collectLatest { @@ -101,6 +103,7 @@ class AnimeExtensionsFragment : Fragment(), .setContentText("Error: ${error.message}") .setPriority(NotificationCompat.PRIORITY_HIGH) notificationManager.notify(1, builder.build()) + snackString("Installation failed: ${error.message}") }, { val builder = NotificationCompat.Builder( @@ -113,6 +116,7 @@ class AnimeExtensionsFragment : Fragment(), .setPriority(NotificationCompat.PRIORITY_LOW) notificationManager.notify(1, builder.build()) viewModel.invalidatePager() + snackString("Extension installed") } ) } diff --git a/app/src/main/java/ani/dantotsu/settings/DevelopersAdapter.kt b/app/src/main/java/ani/dantotsu/settings/DevelopersAdapter.kt index 545b5208..7375d101 100644 --- a/app/src/main/java/ani/dantotsu/settings/DevelopersAdapter.kt +++ b/app/src/main/java/ani/dantotsu/settings/DevelopersAdapter.kt @@ -11,9 +11,11 @@ import ani.dantotsu.setAnimation class DevelopersAdapter(private val developers: Array) : RecyclerView.Adapter() { - private val uiSettings = loadData("ui_settings") ?: UserInterfaceSettings() + private val uiSettings = + loadData("ui_settings") ?: UserInterfaceSettings() - inner class DeveloperViewHolder(val binding: ItemDeveloperBinding) : RecyclerView.ViewHolder(binding.root) { + inner class DeveloperViewHolder(val binding: ItemDeveloperBinding) : + RecyclerView.ViewHolder(binding.root) { init { itemView.setOnClickListener { openLinkInBrowser(developers[bindingAdapterPosition].url) @@ -22,7 +24,13 @@ class DevelopersAdapter(private val developers: Array) : } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeveloperViewHolder { - return DeveloperViewHolder(ItemDeveloperBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + return DeveloperViewHolder( + ItemDeveloperBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) } override fun onBindViewHolder(holder: DeveloperViewHolder, position: Int) { diff --git a/app/src/main/java/ani/dantotsu/settings/DevelopersDialogFragment.kt b/app/src/main/java/ani/dantotsu/settings/DevelopersDialogFragment.kt index 7a999083..68542836 100644 --- a/app/src/main/java/ani/dantotsu/settings/DevelopersDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/DevelopersDialogFragment.kt @@ -13,12 +13,37 @@ class DevelopersDialogFragment : BottomSheetDialogFragment() { private val binding get() = _binding!! private val developers = arrayOf( - Developer("rebelonion","https://avatars.githubusercontent.com/u/87634197?v=4","Owner and Maintainer","https://github.com/rebelonion"), - Developer("Wai What", "https://avatars.githubusercontent.com/u/149729762?v=4", "Icon Designer", "https://github.com/WaiWhat"), - Developer("Aayush262", "https://avatars.githubusercontent.com/u/99584765?v=4", "Contributor", "https://github.com/aayush2622"), + Developer( + "rebelonion", + "https://avatars.githubusercontent.com/u/87634197?v=4", + "Owner and Maintainer", + "https://github.com/rebelonion" + ), + Developer( + "Wai What", + "https://avatars.githubusercontent.com/u/149729762?v=4", + "Icon Designer", + "https://github.com/WaiWhat" + ), + Developer( + "Aayush262", + "https://avatars.githubusercontent.com/u/99584765?v=4", + "Contributor", + "https://github.com/aayush2622" + ), + Developer( + "MarshMeadow", + "https://avatars.githubusercontent.com/u/88599122?v=4", + "Beta Icon Designer", + "https://github.com/MarshMeadow?tab=repositories" + ), ) - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetDevelopersBinding.inflate(inflater, container, false) return binding.root } diff --git a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt index 3d8a155b..c733319f 100644 --- a/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ExtensionsActivity.kt @@ -6,11 +6,8 @@ import android.os.Build.VERSION.* import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import android.view.View import android.view.ViewGroup import android.widget.AutoCompleteTextView -import android.widget.LinearLayout -import android.widget.SearchView import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu @@ -20,16 +17,13 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.* import ani.dantotsu.databinding.ActivityExtensionsBinding -import ani.dantotsu.themes.ThemeManager import ani.dantotsu.others.LangSet +import ani.dantotsu.themes.ThemeManager import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -class ExtensionsActivity : AppCompatActivity() { +class ExtensionsActivity : AppCompatActivity() { private val restartMainActivity = object : OnBackPressedCallback(false) { override fun handleOnBackPressed() = startMainActivity(this@ExtensionsActivity) } @@ -40,16 +34,17 @@ class ExtensionsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityExtensionsBinding.inflate(layoutInflater) setContentView(binding.root) val tabLayout = findViewById(R.id.tabLayout) val viewPager = findViewById(R.id.viewPager) + viewPager.offscreenPageLimit = 1 viewPager.adapter = object : FragmentStateAdapter(this) { - override fun getItemCount(): Int = 4 + override fun getItemCount(): Int = 6 override fun createFragment(position: Int): Fragment { return when (position) { @@ -57,24 +52,56 @@ ThemeManager(this).applyTheme() 1 -> AnimeExtensionsFragment() 2 -> InstalledMangaExtensionsFragment() 3 -> MangaExtensionsFragment() + 4 -> InstalledNovelExtensionsFragment() + 5 -> NovelExtensionsFragment() else -> AnimeExtensionsFragment() } } + } + val searchView: AutoCompleteTextView = findViewById(R.id.searchViewText) + + tabLayout.addOnTabSelectedListener( + object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + searchView.setText("") + searchView.clearFocus() + tabLayout.clearFocus() + viewPager.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + } + } + + override fun onTabUnselected(tab: TabLayout.Tab) { + viewPager.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + } + tabLayout.clearFocus() + } + + override fun onTabReselected(tab: TabLayout.Tab) { + viewPager.updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + } + // Do nothing + } + } + ) + TabLayoutMediator(tabLayout, viewPager) { tab, position -> tab.text = when (position) { 0 -> "Installed Anime" 1 -> "Available Anime" 2 -> "Installed Manga" 3 -> "Available Manga" + 4 -> "Installed Novels" + 5 -> "Available Novels" else -> null } }.attach() - val searchView: AutoCompleteTextView = findViewById(R.id.searchViewText) - searchView.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable?) { } @@ -83,7 +110,8 @@ ThemeManager(this).applyTheme() } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - val currentFragment = supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}") + val currentFragment = + supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}") if (currentFragment is SearchQueryHandler) { currentFragment.updateContentBasedOnQuery(s?.toString()?.trim()) } diff --git a/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt b/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt index bd10b561..c03ba9e1 100644 --- a/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/FAQActivity.kt @@ -7,8 +7,8 @@ import ani.dantotsu.R import ani.dantotsu.currContext import ani.dantotsu.databinding.ActivityFaqBinding import ani.dantotsu.initActivity -import ani.dantotsu.themes.ThemeManager import ani.dantotsu.others.LangSet +import ani.dantotsu.themes.ThemeManager class FAQActivity : AppCompatActivity() { private lateinit var binding: ActivityFaqBinding @@ -107,7 +107,7 @@ class FAQActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityFaqBinding.inflate(layoutInflater) setContentView(binding.root) diff --git a/app/src/main/java/ani/dantotsu/settings/FAQAdapter.kt b/app/src/main/java/ani/dantotsu/settings/FAQAdapter.kt index 39f91e90..f854bd89 100644 --- a/app/src/main/java/ani/dantotsu/settings/FAQAdapter.kt +++ b/app/src/main/java/ani/dantotsu/settings/FAQAdapter.kt @@ -12,14 +12,25 @@ import ani.dantotsu.setAnimation import io.noties.markwon.Markwon import io.noties.markwon.SoftBreakAddsNewLinePlugin -class FAQAdapter(private val questions: List>, private val manager: FragmentManager) : +class FAQAdapter( + private val questions: List>, + private val manager: FragmentManager +) : RecyclerView.Adapter() { - private val uiSettings = loadData("ui_settings") ?: UserInterfaceSettings() + private val uiSettings = + loadData("ui_settings") ?: UserInterfaceSettings() - inner class FAQViewHolder(val binding: ItemQuestionBinding) : RecyclerView.ViewHolder(binding.root) + inner class FAQViewHolder(val binding: ItemQuestionBinding) : + RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FAQViewHolder { - return FAQViewHolder(ItemQuestionBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + return FAQViewHolder( + ItemQuestionBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) } override fun onBindViewHolder(holder: FAQViewHolder, position: Int) { @@ -33,7 +44,8 @@ class FAQAdapter(private val questions: List>, priva setTitleText(faq.second) addView( TextView(b.context).apply { - val markWon = Markwon.builder(b.context).usePlugin(SoftBreakAddsNewLinePlugin.create()).build() + val markWon = Markwon.builder(b.context) + .usePlugin(SoftBreakAddsNewLinePlugin.create()).build() markWon.setMarkdown(this, faq.third) } ) diff --git a/app/src/main/java/ani/dantotsu/settings/ForksDialogFragment.kt b/app/src/main/java/ani/dantotsu/settings/ForksDialogFragment.kt index d4994edb..b1a4db01 100644 --- a/app/src/main/java/ani/dantotsu/settings/ForksDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/ForksDialogFragment.kt @@ -14,10 +14,19 @@ class ForksDialogFragment : BottomSheetDialogFragment() { private val binding get() = _binding!! private val developers = arrayOf( - Developer("Dantotsu","https://avatars.githubusercontent.com/u/87634197?v=4","rebelonion","https://github.com/rebelonion/Dantotsu"), + Developer( + "Dantotsu", + "https://avatars.githubusercontent.com/u/87634197?v=4", + "rebelonion", + "https://github.com/rebelonion/Dantotsu" + ), ) - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetDevelopersBinding.inflate(inflater, container, false) return binding.root } diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt index b668cf2a..7e257417 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledAnimeExtensionsFragment.kt @@ -24,8 +24,8 @@ import ani.dantotsu.R import ani.dantotsu.databinding.FragmentAnimeExtensionsBinding import ani.dantotsu.loadData import ani.dantotsu.others.LanguageMapper -import ani.dantotsu.saveData import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment +import ani.dantotsu.snackString import com.google.android.material.tabs.TabLayout import com.google.android.material.textfield.TextInputLayout import com.google.firebase.crashlytics.FirebaseCrashlytics @@ -37,8 +37,9 @@ import kotlinx.coroutines.launch import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.Locale -class InstalledAnimeExtensionsFragment : Fragment() { +class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { private var _binding: FragmentAnimeExtensionsBinding? = null @@ -46,84 +47,80 @@ class InstalledAnimeExtensionsFragment : Fragment() { private lateinit var extensionsRecyclerView: RecyclerView val skipIcons = loadData("skip_extension_icons") ?: false private val animeExtensionManager: AnimeExtensionManager = Injekt.get() - private val extensionsAdapter = AnimeExtensionsAdapter({ pkg -> - val allSettings = pkg.sources.filterIsInstance() - if (allSettings.isNotEmpty()) { - var selectedSetting = allSettings[0] - if (allSettings.size > 1) { - val names = allSettings.map { it.lang }.toTypedArray() - var selectedIndex = 0 - AlertDialog.Builder(requireContext()) - .setTitle("Select a Source") - .setSingleChoiceItems(names, selectedIndex) { _, which -> - selectedIndex = which - } - .setPositiveButton("OK") { dialog, _ -> - selectedSetting = allSettings[selectedIndex] - dialog.dismiss() + private val extensionsAdapter = AnimeExtensionsAdapter( + { pkg -> + val allSettings = pkg.sources.filterIsInstance() + if (allSettings.isNotEmpty()) { + var selectedSetting = allSettings[0] + if (allSettings.size > 1) { + val names = allSettings.map { it.lang }.toTypedArray() + var selectedIndex = 0 + AlertDialog.Builder(requireContext(), R.style.MyPopup) + .setTitle("Select a Source") + .setSingleChoiceItems(names, selectedIndex) { dialog, which -> + selectedIndex = which + selectedSetting = allSettings[selectedIndex] + dialog.dismiss() - // Move the fragment transaction here - val eActivity = requireActivity() as ExtensionsActivity - eActivity.runOnUiThread { - val fragment = - AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { + // Move the fragment transaction here + val eActivity = requireActivity() as ExtensionsActivity + eActivity.runOnUiThread { + val fragment = + AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { - eActivity.findViewById(R.id.viewPager).visibility = - View.VISIBLE - eActivity.findViewById(R.id.tabLayout).visibility = - View.VISIBLE - eActivity.findViewById(R.id.searchView).visibility = - View.VISIBLE - eActivity.findViewById(R.id.fragmentExtensionsContainer).visibility = - View.GONE - } - parentFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) - .replace(R.id.fragmentExtensionsContainer, fragment) - .addToBackStack(null) - .commit() + eActivity.findViewById(R.id.viewPager).visibility = + View.VISIBLE + eActivity.findViewById(R.id.tabLayout).visibility = + View.VISIBLE + eActivity.findViewById(R.id.searchView).visibility = + View.VISIBLE + eActivity.findViewById(R.id.fragmentExtensionsContainer).visibility = + View.GONE + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() + } } - } - .setNegativeButton("Cancel") { dialog, _ -> - dialog.cancel() - return@setNegativeButton - } - .show() - } else { - // If there's only one setting, proceed with the fragment transaction - val eActivity = requireActivity() as ExtensionsActivity - eActivity.runOnUiThread { - val fragment = - AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { + .show() + } else { + // If there's only one setting, proceed with the fragment transaction + val eActivity = requireActivity() as ExtensionsActivity + eActivity.runOnUiThread { + val fragment = + AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { - eActivity.findViewById(R.id.viewPager).visibility = - View.VISIBLE - eActivity.findViewById(R.id.tabLayout).visibility = - View.VISIBLE - eActivity.findViewById(R.id.searchView).visibility = - View.VISIBLE - eActivity.findViewById(R.id.fragmentExtensionsContainer).visibility = - View.GONE - } - parentFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) - .replace(R.id.fragmentExtensionsContainer, fragment) - .addToBackStack(null) - .commit() + eActivity.findViewById(R.id.viewPager).visibility = + View.VISIBLE + eActivity.findViewById(R.id.tabLayout).visibility = + View.VISIBLE + eActivity.findViewById(R.id.searchView).visibility = + View.VISIBLE + eActivity.findViewById(R.id.fragmentExtensionsContainer).visibility = + View.GONE + } + parentFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) + .replace(R.id.fragmentExtensionsContainer, fragment) + .addToBackStack(null) + .commit() + } } - } - // Hide ViewPager2 and TabLayout - val activity = requireActivity() as ExtensionsActivity - activity.findViewById(R.id.viewPager).visibility = View.GONE - activity.findViewById(R.id.tabLayout).visibility = View.GONE - activity.findViewById(R.id.searchView).visibility = View.GONE - activity.findViewById(R.id.fragmentExtensionsContainer).visibility = View.VISIBLE - } else { - Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) - .show() - } - }, + // Hide ViewPager2 and TabLayout + val activity = requireActivity() as ExtensionsActivity + activity.findViewById(R.id.viewPager).visibility = View.GONE + activity.findViewById(R.id.tabLayout).visibility = View.GONE + activity.findViewById(R.id.searchView).visibility = View.GONE + activity.findViewById(R.id.fragmentExtensionsContainer).visibility = + View.VISIBLE + } else { + Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) + .show() + } + }, { pkg -> if (isAdded) { // Check if the fragment is currently added to its activity val context = requireContext() // Store context in a variable @@ -157,6 +154,7 @@ class InstalledAnimeExtensionsFragment : Fragment() { .setContentText("Error: ${error.message}") .setPriority(NotificationCompat.PRIORITY_HIGH) notificationManager.notify(1, builder.build()) + snackString("Update failed: ${error.message}") }, { val builder = NotificationCompat.Builder( @@ -168,10 +166,12 @@ class InstalledAnimeExtensionsFragment : Fragment() { .setContentText("The extension has been successfully updated.") .setPriority(NotificationCompat.PRIORITY_LOW) notificationManager.notify(1, builder.build()) + snackString("Extension updated") } ) } else { animeExtensionManager.uninstallExtension(pkg.pkgName) + snackString("Extension uninstalled") } } }, skipIcons @@ -202,6 +202,9 @@ class InstalledAnimeExtensionsFragment : Fragment() { super.onDestroyView();_binding = null } + override fun updateContentBasedOnQuery(query: String?) { + extensionsAdapter.filter(query ?: "", animeExtensionManager.installedExtensionsFlow.value) + } private class AnimeExtensionsAdapter( private val onSettingsClicked: (AnimeExtension.Installed) -> Unit, @@ -224,7 +227,7 @@ class InstalledAnimeExtensionsFragment : Fragment() { override fun onBindViewHolder(holder: ViewHolder, position: Int) { val extension = getItem(position) // Use getItem() from ListAdapter val nsfw = if (extension.isNsfw) "(18+)" else "" - val lang= LanguageMapper.mapLanguageCodeToName(extension.lang) + val lang = LanguageMapper.mapLanguageCodeToName(extension.lang) holder.extensionNameTextView.text = extension.name holder.extensionVersionTextView.text = "$lang ${extension.versionName} $nsfw" if (!skipIcons) { @@ -243,12 +246,23 @@ class InstalledAnimeExtensionsFragment : Fragment() { } } + fun filter(query: String, currentList: List) { + val filteredList = ArrayList() + for (extension in currentList) { + if (extension.name.lowercase(Locale.ROOT).contains(query.lowercase(Locale.ROOT))) { + filteredList.add(extension) + } + } + submitList(filteredList) + } + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) - val extensionVersionTextView: TextView = view.findViewById(R.id.extensionVersionTextView) + val extensionVersionTextView: TextView = + view.findViewById(R.id.extensionVersionTextView) val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView) val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) - val closeTextView: ImageView = view.findViewById(R.id.closeTextView) + val closeTextView: ImageView = view.findViewById(R.id.closeTextView) } companion object { diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt index 23446c48..9b2741f2 100644 --- a/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/InstalledMangaExtensionsFragment.kt @@ -24,8 +24,9 @@ import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.R import ani.dantotsu.databinding.FragmentMangaExtensionsBinding import ani.dantotsu.loadData -import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment import ani.dantotsu.others.LanguageMapper +import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment +import ani.dantotsu.snackString import com.google.android.material.tabs.TabLayout import com.google.android.material.textfield.TextInputLayout import com.google.firebase.crashlytics.FirebaseCrashlytics @@ -37,8 +38,9 @@ import kotlinx.coroutines.launch import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.util.Locale -class InstalledMangaExtensionsFragment : Fragment() { +class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler { private var _binding: FragmentMangaExtensionsBinding? = null private val binding get() = _binding!! private lateinit var extensionsRecyclerView: RecyclerView @@ -60,34 +62,28 @@ class InstalledMangaExtensionsFragment : Fragment() { if (allSettings.size > 1) { val names = allSettings.map { it.lang }.toTypedArray() var selectedIndex = 0 - AlertDialog.Builder(requireContext()) + AlertDialog.Builder(requireContext(), R.style.MyPopup) .setTitle("Select a Source") - .setSingleChoiceItems(names, selectedIndex) { _, which -> + .setSingleChoiceItems(names, selectedIndex) { dialog, which -> selectedIndex = which - } - .setPositiveButton("OK") { dialog, _ -> selectedSetting = allSettings[selectedIndex] dialog.dismiss() // Move the fragment transaction here - val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id){ - changeUIVisibility(true) - } + val fragment = + MangaSourcePreferencesFragment().getInstance(selectedSetting.id) { + changeUIVisibility(true) + } parentFragmentManager.beginTransaction() .setCustomAnimations(R.anim.slide_up, R.anim.slide_down) .replace(R.id.fragmentExtensionsContainer, fragment) .addToBackStack(null) .commit() } - .setNegativeButton("Cancel") { dialog, _ -> - dialog.cancel() - changeUIVisibility(true) - return@setNegativeButton - } .show() } else { // If there's only one setting, proceed with the fragment transaction - val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id){ + val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) { changeUIVisibility(true) } parentFragmentManager.beginTransaction() @@ -105,56 +101,60 @@ class InstalledMangaExtensionsFragment : Fragment() { } }, { pkg -> - if (isAdded) { // Check if the fragment is currently added to its activity - val context = requireContext() // Store context in a variable - val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once + if (isAdded) { // Check if the fragment is currently added to its activity + val context = requireContext() // Store context in a variable + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once - if (pkg.hasUpdate) { - mangaExtensionManager.updateExtension(pkg) - .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread - .subscribe( - { installStep -> - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(R.drawable.ic_round_sync_24) - .setContentTitle("Updating extension") - .setContentText("Step: $installStep") - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - }, - { error -> - FirebaseCrashlytics.getInstance().recordException(error) - Log.e("MangaExtensionsAdapter", "Error: ", error) // Log the error - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_ERROR - ) - .setSmallIcon(R.drawable.ic_round_info_24) - .setContentTitle("Update failed: ${error.message}") - .setContentText("Error: ${error.message}") - .setPriority(NotificationCompat.PRIORITY_HIGH) - notificationManager.notify(1, builder.build()) - }, - { - val builder = NotificationCompat.Builder( - context, - Notifications.CHANNEL_DOWNLOADER_PROGRESS - ) - .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) - .setContentTitle("Update complete") - .setContentText("The extension has been successfully updated.") - .setPriority(NotificationCompat.PRIORITY_LOW) - notificationManager.notify(1, builder.build()) - } - ) - } else { - mangaExtensionManager.uninstallExtension(pkg.pkgName) + if (pkg.hasUpdate) { + mangaExtensionManager.updateExtension(pkg) + .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread + .subscribe( + { installStep -> + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(R.drawable.ic_round_sync_24) + .setContentTitle("Updating extension") + .setContentText("Step: $installStep") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + }, + { error -> + FirebaseCrashlytics.getInstance().recordException(error) + Log.e("MangaExtensionsAdapter", "Error: ", error) // Log the error + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_ERROR + ) + .setSmallIcon(R.drawable.ic_round_info_24) + .setContentTitle("Update failed: ${error.message}") + .setContentText("Error: ${error.message}") + .setPriority(NotificationCompat.PRIORITY_HIGH) + notificationManager.notify(1, builder.build()) + snackString("Update failed: ${error.message}") + }, + { + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) + .setContentTitle("Update complete") + .setContentText("The extension has been successfully updated.") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + snackString("Extension updated") + } + ) + } else { + mangaExtensionManager.uninstallExtension(pkg.pkgName) + snackString("Extension uninstalled") + } } - } - }, skipIcons) + }, skipIcons + ) override fun onCreateView( inflater: LayoutInflater, @@ -181,6 +181,9 @@ class InstalledMangaExtensionsFragment : Fragment() { super.onDestroyView();_binding = null } + override fun updateContentBasedOnQuery(query: String?) { + extensionsAdapter.filter(query ?: "", mangaExtensionManager.installedExtensionsFlow.value) + } private class MangaExtensionsAdapter( private val onSettingsClicked: (MangaExtension.Installed) -> Unit, @@ -224,9 +227,20 @@ class InstalledMangaExtensionsFragment : Fragment() { } } + fun filter(query: String, currentList: List) { + val filteredList = ArrayList() + for (extension in currentList) { + if (extension.name.lowercase(Locale.ROOT).contains(query.lowercase(Locale.ROOT))) { + filteredList.add(extension) + } + } + submitList(filteredList) + } + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) - val extensionVersionTextView: TextView = view.findViewById(R.id.extensionVersionTextView) + val extensionVersionTextView: TextView = + view.findViewById(R.id.extensionVersionTextView) val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView) val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) val closeTextView: ImageView = view.findViewById(R.id.closeTextView) diff --git a/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt new file mode 100644 index 00000000..c0a61dd5 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/InstalledNovelExtensionsFragment.kt @@ -0,0 +1,215 @@ +package ani.dantotsu.settings + +import android.app.NotificationManager +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.core.app.NotificationCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.R +import ani.dantotsu.currContext +import ani.dantotsu.databinding.FragmentNovelExtensionsBinding +import ani.dantotsu.loadData +import ani.dantotsu.others.LanguageMapper +import ani.dantotsu.parsers.novel.NovelExtension +import ani.dantotsu.parsers.novel.NovelExtensionManager +import ani.dantotsu.snackString +import com.google.firebase.crashlytics.FirebaseCrashlytics +import eu.kanade.tachiyomi.data.notification.Notifications +import kotlinx.coroutines.launch +import rx.android.schedulers.AndroidSchedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.Locale + +class InstalledNovelExtensionsFragment : Fragment(), SearchQueryHandler { + private var _binding: FragmentNovelExtensionsBinding? = null + private val binding get() = _binding!! + private lateinit var extensionsRecyclerView: RecyclerView + val skipIcons = loadData("skip_extension_icons") ?: false + private val novelExtensionManager: NovelExtensionManager = Injekt.get() + private val extensionsAdapter = NovelExtensionsAdapter({ pkg -> + Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT) + .show() + }, + { pkg -> + if (isAdded) { // Check if the fragment is currently added to its activity + val context = requireContext() // Store context in a variable + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once + + if (pkg.hasUpdate) { + novelExtensionManager.updateExtension(pkg) + .observeOn(AndroidSchedulers.mainThread()) // Observe on main thread + .subscribe( + { installStep -> + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(R.drawable.ic_round_sync_24) + .setContentTitle("Updating extension") + .setContentText("Step: $installStep") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + }, + { error -> + FirebaseCrashlytics.getInstance().recordException(error) + Log.e("NovelExtensionsAdapter", "Error: ", error) // Log the error + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_ERROR + ) + .setSmallIcon(R.drawable.ic_round_info_24) + .setContentTitle("Update failed: ${error.message}") + .setContentText("Error: ${error.message}") + .setPriority(NotificationCompat.PRIORITY_HIGH) + notificationManager.notify(1, builder.build()) + snackString("Update failed: ${error.message}") + }, + { + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(androidx.media3.ui.R.drawable.exo_ic_check) + .setContentTitle("Update complete") + .setContentText("The extension has been successfully updated.") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + snackString("Extension updated") + } + ) + } else { + novelExtensionManager.uninstallExtension(pkg.pkgName, currContext() ?: context) + snackString("Extension uninstalled") + } + } + }, skipIcons + ) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentNovelExtensionsBinding.inflate(inflater, container, false) + + extensionsRecyclerView = binding.allNovelExtensionsRecyclerView + extensionsRecyclerView.layoutManager = LinearLayoutManager(requireContext()) + extensionsRecyclerView.adapter = extensionsAdapter + + + lifecycleScope.launch { + novelExtensionManager.installedExtensionsFlow.collect { extensions -> + extensionsAdapter.updateData(extensions) + } + } + val extensionsRecyclerView: RecyclerView = binding.allNovelExtensionsRecyclerView + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView();_binding = null + } + + override fun updateContentBasedOnQuery(query: String?) { + extensionsAdapter.filter(query ?: "", novelExtensionManager.installedExtensionsFlow.value) + } + + private class NovelExtensionsAdapter( + private val onSettingsClicked: (NovelExtension.Installed) -> Unit, + private val onUninstallClicked: (NovelExtension.Installed) -> Unit, + skipIcons: Boolean + ) : ListAdapter( + DIFF_CALLBACK_INSTALLED + ) { + + val skipIcons = skipIcons + + fun updateData(newExtensions: List) { + Log.d("NovelExtensionsAdapter", "updateData: $newExtensions") + submitList(newExtensions) // Use submitList instead of manual list handling + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_extension, parent, false) + Log.d("NovelExtensionsAdapter", "onCreateViewHolder: $view") + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val extension = getItem(position) // Use getItem() from ListAdapter + val nsfw = "" + val lang = LanguageMapper.mapLanguageCodeToName("all") + holder.extensionNameTextView.text = extension.name + holder.extensionVersionTextView.text = "$lang ${extension.versionName} $nsfw" + if (!skipIcons) { + holder.extensionIconImageView.setImageDrawable(extension.icon) + } + if (extension.hasUpdate) { + holder.closeTextView.setImageResource(R.drawable.ic_round_sync_24) + } else { + holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24) + } + holder.closeTextView.setOnClickListener { + onUninstallClicked(extension) + } + holder.settingsImageView.setOnClickListener { + onSettingsClicked(extension) + } + } + + fun filter(query: String, currentList: List) { + val filteredList = ArrayList() + for (extension in currentList) { + if (extension.name.lowercase(Locale.ROOT).contains(query.lowercase(Locale.ROOT))) { + filteredList.add(extension) + } + } + submitList(filteredList) + } + + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val extensionNameTextView: TextView = view.findViewById(R.id.extensionNameTextView) + val extensionVersionTextView: TextView = + view.findViewById(R.id.extensionVersionTextView) + val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView) + val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) + val closeTextView: ImageView = view.findViewById(R.id.closeTextView) + } + + companion object { + val DIFF_CALLBACK_INSTALLED = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: NovelExtension.Installed, + newItem: NovelExtension.Installed + ): Boolean { + return oldItem.pkgName == newItem.pkgName + } + + override fun areContentsTheSame( + oldItem: NovelExtension.Installed, + newItem: NovelExtension.Installed + ): Boolean { + return oldItem == newItem + } + } + } + } + + +} \ 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 a949bd86..e3d14ee6 100644 --- a/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/MangaExtensionsFragment.kt @@ -10,24 +10,23 @@ import androidx.core.app.NotificationCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import androidx.paging.PagingData import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.R import ani.dantotsu.databinding.FragmentMangaExtensionsBinding -import com.google.firebase.crashlytics.FirebaseCrashlytics -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.launch -import rx.android.schedulers.AndroidSchedulers -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get 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 kotlinx.coroutines.flow.Flow +import ani.dantotsu.snackString +import com.google.firebase.crashlytics.FirebaseCrashlytics +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 +import kotlinx.coroutines.launch +import rx.android.schedulers.AndroidSchedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get class MangaExtensionsFragment : Fragment(), SearchQueryHandler, OnMangaInstallClickListener { @@ -55,7 +54,8 @@ class MangaExtensionsFragment : Fragment(), binding.allMangaExtensionsRecyclerView.isNestedScrollingEnabled = false binding.allMangaExtensionsRecyclerView.adapter = adapter binding.allMangaExtensionsRecyclerView.layoutManager = LinearLayoutManager(context) - (binding.allMangaExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = true + (binding.allMangaExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = + true lifecycleScope.launch { viewModel.pagerFlow.collectLatest { pagingData -> @@ -104,6 +104,7 @@ class MangaExtensionsFragment : Fragment(), .setContentText("Error: ${error.message}") .setPriority(NotificationCompat.PRIORITY_HIGH) notificationManager.notify(1, builder.build()) + snackString("Installation failed: ${error.message}") }, { val builder = NotificationCompat.Builder( @@ -116,6 +117,7 @@ class MangaExtensionsFragment : Fragment(), .setPriority(NotificationCompat.PRIORITY_LOW) notificationManager.notify(1, builder.build()) viewModel.invalidatePager() + snackString("Extension installed") } ) } @@ -126,5 +128,4 @@ class MangaExtensionsFragment : Fragment(), } - } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/NovelExtensionsFragment.kt b/app/src/main/java/ani/dantotsu/settings/NovelExtensionsFragment.kt new file mode 100644 index 00000000..a700dded --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/NovelExtensionsFragment.kt @@ -0,0 +1,132 @@ +package ani.dantotsu.settings + +import android.app.NotificationManager +import android.content.Context +import android.os.Bundle +import android.util.Log +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.databinding.FragmentNovelExtensionsBinding +import ani.dantotsu.parsers.novel.NovelExtension +import ani.dantotsu.parsers.novel.NovelExtensionManager +import ani.dantotsu.settings.paging.NovelExtensionAdapter +import ani.dantotsu.settings.paging.NovelExtensionsViewModel +import ani.dantotsu.settings.paging.NovelExtensionsViewModelFactory +import ani.dantotsu.settings.paging.OnNovelInstallClickListener +import ani.dantotsu.snackString +import com.google.firebase.crashlytics.FirebaseCrashlytics +import eu.kanade.tachiyomi.data.notification.Notifications +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import rx.android.schedulers.AndroidSchedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class NovelExtensionsFragment : Fragment(), + SearchQueryHandler, OnNovelInstallClickListener { + private var _binding: FragmentNovelExtensionsBinding? = null + private val binding get() = _binding!! + + private val viewModel: NovelExtensionsViewModel by viewModels { + NovelExtensionsViewModelFactory(novelExtensionManager) + } + + private val adapter by lazy { + NovelExtensionAdapter(this) + } + + private val novelExtensionManager: NovelExtensionManager = Injekt.get() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentNovelExtensionsBinding.inflate(inflater, container, false) + + binding.allNovelExtensionsRecyclerView.isNestedScrollingEnabled = false + binding.allNovelExtensionsRecyclerView.adapter = adapter + binding.allNovelExtensionsRecyclerView.layoutManager = LinearLayoutManager(context) + (binding.allNovelExtensionsRecyclerView.layoutManager as LinearLayoutManager).isItemPrefetchEnabled = + true + + lifecycleScope.launch { + viewModel.pagerFlow.collectLatest { pagingData -> + Log.d("NovelExtensionsFragment", "collectLatest") + adapter.submitData(pagingData) + } + } + + + viewModel.invalidatePager() // Force a refresh of the pager + return binding.root + } + + override fun updateContentBasedOnQuery(query: String?) { + viewModel.setSearchQuery(query ?: "") + } + + override fun onInstallClick(pkg: NovelExtension.Available) { + if (isAdded) { // Check if the fragment is currently added to its activity + val context = requireContext() + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Start the installation process + novelExtensionManager.installExtension(pkg) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { installStep -> + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(R.drawable.ic_round_sync_24) + .setContentTitle("Installing extension") + .setContentText("Step: $installStep") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + }, + { error -> + FirebaseCrashlytics.getInstance().recordException(error) + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_ERROR + ) + .setSmallIcon(R.drawable.ic_round_info_24) + .setContentTitle("Installation failed: ${error.message}") + .setContentText("Error: ${error.message}") + .setPriority(NotificationCompat.PRIORITY_HIGH) + notificationManager.notify(1, builder.build()) + snackString("Installation failed: ${error.message}") + }, + { + val builder = NotificationCompat.Builder( + context, + Notifications.CHANNEL_DOWNLOADER_PROGRESS + ) + .setSmallIcon(R.drawable.ic_round_download_24) + .setContentTitle("Installation complete") + .setContentText("The extension has been successfully installed.") + .setPriority(NotificationCompat.PRIORITY_LOW) + notificationManager.notify(1, builder.build()) + viewModel.invalidatePager() + snackString("Extension installed") + } + ) + } + } + + override fun onDestroyView() { + super.onDestroyView();_binding = null + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt index 17fbdb15..9b335078 100644 --- a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt @@ -11,13 +11,20 @@ import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.view.updateLayoutParams import androidx.core.widget.addTextChangedListener -import ani.dantotsu.* +import ani.dantotsu.R import ani.dantotsu.databinding.ActivityPlayerSettingsBinding +import ani.dantotsu.initActivity +import ani.dantotsu.loadData import ani.dantotsu.media.Media +import ani.dantotsu.navBarHeight +import ani.dantotsu.others.LangSet import ani.dantotsu.others.getSerialized import ani.dantotsu.parsers.Subtitle +import ani.dantotsu.saveData +import ani.dantotsu.snackString +import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet +import ani.dantotsu.toast import com.google.android.material.snackbar.Snackbar import kotlin.math.roundToInt @@ -26,14 +33,14 @@ class PlayerSettingsActivity : AppCompatActivity() { lateinit var binding: ActivityPlayerSettingsBinding private val player = "player_settings" - var media:Media?=null - var subtitle:Subtitle?=null + var media: Media? = null + var subtitle: Subtitle? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityPlayerSettingsBinding.inflate(layoutInflater) setContentView(binding.root) @@ -55,7 +62,12 @@ ThemeManager(this).applyTheme() bottomMargin = navBarHeight } - val settings = loadData(player, toast = false) ?: PlayerSettings().apply { saveData(player, this) } + val settings = loadData(player, toast = false) ?: PlayerSettings().apply { + saveData( + player, + this + ) + } binding.playerSettingsBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() @@ -68,28 +80,36 @@ ThemeManager(this).applyTheme() saveData(player, settings) } - binding.playerSettingsQualityHeight.setText((loadData("maxHeight", toast = false) ?: 480).toString()) + binding.playerSettingsQualityHeight.setText( + (loadData("maxHeight", toast = false) ?: 480).toString() + ) binding.playerSettingsQualityHeight.addTextChangedListener { val height = binding.playerSettingsQualityHeight.text.toString().toIntOrNull() saveData("maxHeight", height) } - binding.playerSettingsQualityWidth.setText((loadData("maxWidth", toast = false) ?: 720).toString()) + binding.playerSettingsQualityWidth.setText( + (loadData("maxWidth", toast = false) ?: 720).toString() + ) binding.playerSettingsQualityWidth.addTextChangedListener { val height = binding.playerSettingsQualityWidth.text.toString().toIntOrNull() saveData("maxWidth", height) } - val speeds = arrayOf(0.25f, 0.33f, 0.5f, 0.66f, 0.75f, 1f, 1.25f, 1.33f, 1.5f, 1.66f, 1.75f, 2f) + val speeds = + arrayOf(0.25f, 0.33f, 0.5f, 0.66f, 0.75f, 1f, 1.25f, 1.33f, 1.5f, 1.66f, 1.75f, 2f) val cursedSpeeds = arrayOf(1f, 1.25f, 1.5f, 1.75f, 2f, 2.5f, 3f, 4f, 5f, 10f, 25f, 50f) var curSpeedArr = if (settings.cursedSpeeds) cursedSpeeds else speeds var speedsName = curSpeedArr.map { "${it}x" }.toTypedArray() - binding.playerSettingsSpeed.text = getString(R.string.default_playback_speed, speedsName[settings.defaultSpeed]) - val speedDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.default_speed)) + binding.playerSettingsSpeed.text = + getString(R.string.default_playback_speed, speedsName[settings.defaultSpeed]) + val speedDialog = AlertDialog.Builder(this, R.style.DialogTheme) + .setTitle(getString(R.string.default_speed)) binding.playerSettingsSpeed.setOnClickListener { speedDialog.setSingleChoiceItems(speedsName, settings.defaultSpeed) { dialog, i -> settings.defaultSpeed = i - binding.playerSettingsSpeed.text = getString(R.string.default_playback_speed, speedsName[i]) + binding.playerSettingsSpeed.text = + getString(R.string.default_playback_speed, speedsName[i]) saveData(player, settings) dialog.dismiss() }.show() @@ -101,7 +121,8 @@ ThemeManager(this).applyTheme() curSpeedArr = if (settings.cursedSpeeds) cursedSpeeds else speeds settings.defaultSpeed = if (settings.cursedSpeeds) 0 else 5 speedsName = curSpeedArr.map { "${it}x" }.toTypedArray() - binding.playerSettingsSpeed.text = getString(R.string.default_playback_speed, speedsName[settings.defaultSpeed]) + binding.playerSettingsSpeed.text = + getString(R.string.default_playback_speed, speedsName[settings.defaultSpeed]) saveData(player, settings) } @@ -155,7 +176,8 @@ ThemeManager(this).applyTheme() if (isChecked) snackString(getString(R.string.very_bold)) saveData(player, settings) } - binding.playerSettingsCompletePercentage.value = (settings.watchPercentage * 100).roundToInt().toFloat() + binding.playerSettingsCompletePercentage.value = + (settings.watchPercentage * 100).roundToInt().toFloat() binding.playerSettingsCompletePercentage.addOnChangeListener { _, value, _ -> settings.watchPercentage = value / 100 saveData(player, settings) @@ -230,7 +252,8 @@ ThemeManager(this).applyTheme() } val resizeModes = arrayOf("Original", "Zoom", "Stretch") - val resizeDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.default_resize_mode)) + val resizeDialog = AlertDialog.Builder(this, R.style.DialogTheme) + .setTitle(getString(R.string.default_resize_mode)) binding.playerResizeMode.setOnClickListener { resizeDialog.setSingleChoiceItems(resizeModes, settings.resize) { dialog, count -> settings.resize = count @@ -244,7 +267,11 @@ ThemeManager(this).applyTheme() R.string.restart_app, Snackbar.LENGTH_SHORT ).apply { val mainIntent = - Intent.makeRestartActivityTask(context.packageManager.getLaunchIntentForPackage(context.packageName)!!.component) + Intent.makeRestartActivityTask( + context.packageManager.getLaunchIntentForPackage( + context.packageName + )!!.component + ) setAction("Do it!") { context.startActivity(mainIntent) Runtime.getRuntime().exit(0) @@ -256,7 +283,7 @@ ThemeManager(this).applyTheme() fun toggleButton(button: android.widget.Button, toggle: Boolean) { button.isClickable = toggle button.alpha = when (toggle) { - true -> 1f + true -> 1f false -> 0.5f } } @@ -269,21 +296,24 @@ ThemeManager(this).applyTheme() binding.subtitleFontSizeCard.isEnabled = isChecked binding.subtitleFontSizeCard.isClickable = isChecked binding.subtitleFontSizeCard.alpha = when (isChecked) { - true -> 1f + true -> 1f false -> 0.5f } binding.subtitleFontSize.isEnabled = isChecked binding.subtitleFontSize.isClickable = isChecked binding.subtitleFontSize.alpha = when (isChecked) { - true -> 1f - false -> 0.5f - } - ActivityPlayerSettingsBinding.bind(binding.root).subtitleFontSizeText.isEnabled = isChecked - ActivityPlayerSettingsBinding.bind(binding.root).subtitleFontSizeText.isClickable = isChecked - ActivityPlayerSettingsBinding.bind(binding.root).subtitleFontSizeText.alpha = when (isChecked) { - true -> 1f + true -> 1f false -> 0.5f } + ActivityPlayerSettingsBinding.bind(binding.root).subtitleFontSizeText.isEnabled = + isChecked + ActivityPlayerSettingsBinding.bind(binding.root).subtitleFontSizeText.isClickable = + isChecked + ActivityPlayerSettingsBinding.bind(binding.root).subtitleFontSizeText.alpha = + when (isChecked) { + true -> 1f + false -> 0.5f + } } binding.subSwitch.isChecked = settings.subtitles binding.subSwitch.setOnCheckedChangeListener { _, isChecked -> @@ -293,10 +323,26 @@ ThemeManager(this).applyTheme() restartApp() } val colorsPrimary = - arrayOf("Black", "Dark Gray", "Gray", "Light Gray", "White", "Red", "Yellow", "Green", "Cyan", "Blue", "Magenta") - val primaryColorDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.primary_sub_color)) + arrayOf( + "Black", + "Dark Gray", + "Gray", + "Light Gray", + "White", + "Red", + "Yellow", + "Green", + "Cyan", + "Blue", + "Magenta" + ) + val primaryColorDialog = AlertDialog.Builder(this, R.style.DialogTheme) + .setTitle(getString(R.string.primary_sub_color)) binding.videoSubColorPrimary.setOnClickListener { - primaryColorDialog.setSingleChoiceItems(colorsPrimary, settings.primaryColor) { dialog, count -> + primaryColorDialog.setSingleChoiceItems( + colorsPrimary, + settings.primaryColor + ) { dialog, count -> settings.primaryColor = count saveData(player, settings) dialog.dismiss() @@ -316,16 +362,21 @@ ThemeManager(this).applyTheme() "Magenta", "Transparent" ) - val secondaryColorDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.outline_sub_color)) + val secondaryColorDialog = AlertDialog.Builder(this, R.style.DialogTheme) + .setTitle(getString(R.string.outline_sub_color)) binding.videoSubColorSecondary.setOnClickListener { - secondaryColorDialog.setSingleChoiceItems(colorsSecondary, settings.secondaryColor) { dialog, count -> + secondaryColorDialog.setSingleChoiceItems( + colorsSecondary, + settings.secondaryColor + ) { dialog, count -> settings.secondaryColor = count saveData(player, settings) dialog.dismiss() }.show() } val typesOutline = arrayOf("Outline", "Shine", "Drop Shadow", "None") - val outlineDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.outline_type)) + val outlineDialog = AlertDialog.Builder(this, R.style.DialogTheme) + .setTitle(getString(R.string.outline_type)) binding.videoSubOutline.setOnClickListener { outlineDialog.setSingleChoiceItems(typesOutline, settings.outline) { dialog, count -> settings.outline = count @@ -347,9 +398,13 @@ ThemeManager(this).applyTheme() "Blue", "Magenta" ) - val subBackgroundDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.outline_sub_color)) + val subBackgroundDialog = AlertDialog.Builder(this, R.style.DialogTheme) + .setTitle(getString(R.string.outline_sub_color)) binding.videoSubColorBackground.setOnClickListener { - subBackgroundDialog.setSingleChoiceItems(colorsSubBackground, settings.subBackground) { dialog, count -> + subBackgroundDialog.setSingleChoiceItems( + colorsSubBackground, + settings.subBackground + ) { dialog, count -> settings.subBackground = count saveData(player, settings) dialog.dismiss() @@ -370,16 +425,28 @@ ThemeManager(this).applyTheme() "Blue", "Magenta" ) - val subWindowDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.outline_sub_color)) + val subWindowDialog = AlertDialog.Builder(this, R.style.DialogTheme) + .setTitle(getString(R.string.outline_sub_color)) binding.videoSubColorWindow.setOnClickListener { - subWindowDialog.setSingleChoiceItems(colorsSubWindow, settings.subWindow) { dialog, count -> + subWindowDialog.setSingleChoiceItems( + colorsSubWindow, + settings.subWindow + ) { dialog, count -> settings.subWindow = count saveData(player, settings) dialog.dismiss() }.show() } - val fonts = arrayOf("Poppins Semi Bold", "Poppins Bold", "Poppins", "Poppins Thin","Century Gothic","Century Gothic Bold") - val fontDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.subtitle_font)) + val fonts = arrayOf( + "Poppins Semi Bold", + "Poppins Bold", + "Poppins", + "Poppins Thin", + "Century Gothic", + "Century Gothic Bold" + ) + val fontDialog = AlertDialog.Builder(this, R.style.DialogTheme) + .setTitle(getString(R.string.subtitle_font)) binding.videoSubFont.setOnClickListener { fontDialog.setSingleChoiceItems(fonts, settings.font) { dialog, count -> settings.font = count diff --git a/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt index ebde9bbc..61c87afe 100644 --- a/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/ReaderSettingsActivity.kt @@ -9,11 +9,11 @@ import ani.dantotsu.databinding.ActivityReaderSettingsBinding import ani.dantotsu.initActivity import ani.dantotsu.loadData import ani.dantotsu.navBarHeight +import ani.dantotsu.others.LangSet import ani.dantotsu.saveData import ani.dantotsu.snackString import ani.dantotsu.statusBarHeight import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet class ReaderSettingsActivity : AppCompatActivity() { lateinit var binding: ActivityReaderSettingsBinding @@ -21,7 +21,7 @@ class ReaderSettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityReaderSettingsBinding.inflate(layoutInflater) setContentView(binding.root) @@ -31,7 +31,12 @@ ThemeManager(this).applyTheme() bottomMargin = navBarHeight } - val settings = loadData(reader, toast = false) ?: ReaderSettings().apply { saveData(reader, this) } + val settings = loadData(reader, toast = false) ?: ReaderSettings().apply { + saveData( + reader, + this + ) + } binding.readerSettingsBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() @@ -43,13 +48,13 @@ ThemeManager(this).applyTheme() settings.showSource = isChecked saveData(reader, settings) } - + binding.readerSettingsSystemBars.isChecked = settings.showSystemBars binding.readerSettingsSystemBars.setOnCheckedChangeListener { _, isChecked -> settings.showSystemBars = isChecked saveData(reader, settings) } - + binding.readerSettingsAutoWebToon.isChecked = settings.autoDetectWebtoon binding.readerSettingsAutoWebToon.setOnCheckedChangeListener { _, isChecked -> settings.autoDetectWebtoon = isChecked @@ -63,7 +68,8 @@ ThemeManager(this).applyTheme() binding.readerSettingsContinuous ) - binding.readerSettingsLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.default.layout.ordinal] + binding.readerSettingsLayoutText.text = + resources.getStringArray(R.array.manga_layouts)[settings.default.layout.ordinal] var selectedLayout = layoutList[settings.default.layout.ordinal] selectedLayout.alpha = 1f @@ -72,17 +78,23 @@ ThemeManager(this).applyTheme() selectedLayout.alpha = 0.33f selectedLayout = imageButton selectedLayout.alpha = 1f - settings.default.layout = CurrentReaderSettings.Layouts[index]?:CurrentReaderSettings.Layouts.CONTINUOUS - binding.readerSettingsLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.default.layout.ordinal] + settings.default.layout = + CurrentReaderSettings.Layouts[index] ?: CurrentReaderSettings.Layouts.CONTINUOUS + binding.readerSettingsLayoutText.text = + resources.getStringArray(R.array.manga_layouts)[settings.default.layout.ordinal] saveData(reader, settings) } } - binding.readerSettingsDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.default.direction.ordinal] + binding.readerSettingsDirectionText.text = + resources.getStringArray(R.array.manga_directions)[settings.default.direction.ordinal] binding.readerSettingsDirection.rotation = 90f * (settings.default.direction.ordinal) binding.readerSettingsDirection.setOnClickListener { - settings.default.direction = CurrentReaderSettings.Directions[settings.default.direction.ordinal + 1] ?: CurrentReaderSettings.Directions.TOP_TO_BOTTOM - binding.readerSettingsDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.default.direction.ordinal] + settings.default.direction = + CurrentReaderSettings.Directions[settings.default.direction.ordinal + 1] + ?: CurrentReaderSettings.Directions.TOP_TO_BOTTOM + binding.readerSettingsDirectionText.text = + resources.getStringArray(R.array.manga_directions)[settings.default.direction.ordinal] binding.readerSettingsDirection.rotation = 90f * (settings.default.direction.ordinal) saveData(reader, settings) } @@ -102,7 +114,8 @@ ThemeManager(this).applyTheme() selectedDual.alpha = 0.33f selectedDual = imageButton selectedDual.alpha = 1f - settings.default.dualPageMode = CurrentReaderSettings.DualPageModes[index] ?: CurrentReaderSettings.DualPageModes.Automatic + settings.default.dualPageMode = CurrentReaderSettings.DualPageModes[index] + ?: CurrentReaderSettings.DualPageModes.Automatic binding.readerSettingsDualPageText.text = settings.default.dualPageMode.toString() saveData(reader, settings) } @@ -149,29 +162,29 @@ ThemeManager(this).applyTheme() } binding.readerSettingsOverscroll.isChecked = settings.default.overScrollMode - binding.readerSettingsOverscroll.setOnCheckedChangeListener { _,isChecked -> + binding.readerSettingsOverscroll.setOnCheckedChangeListener { _, isChecked -> settings.default.overScrollMode = isChecked saveData(reader, settings) } binding.readerSettingsVolumeButton.isChecked = settings.default.volumeButtons - binding.readerSettingsVolumeButton.setOnCheckedChangeListener { _,isChecked -> + binding.readerSettingsVolumeButton.setOnCheckedChangeListener { _, isChecked -> settings.default.volumeButtons = isChecked saveData(reader, settings) } binding.readerSettingsWrapImages.isChecked = settings.default.wrapImages - binding.readerSettingsWrapImages.setOnCheckedChangeListener { _,isChecked -> + binding.readerSettingsWrapImages.setOnCheckedChangeListener { _, isChecked -> settings.default.wrapImages = isChecked saveData(reader, settings) } binding.readerSettingsLongClickImage.isChecked = settings.default.longClickImage - binding.readerSettingsLongClickImage.setOnCheckedChangeListener { _,isChecked -> + binding.readerSettingsLongClickImage.setOnCheckedChangeListener { _, isChecked -> settings.default.longClickImage = isChecked saveData(reader, settings) } - + //Update Progress binding.readerSettingsAskUpdateProgress.isChecked = settings.askIndividual binding.readerSettingsAskUpdateProgress.setOnCheckedChangeListener { _, isChecked -> diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt index 0ef44bc8..e9293b64 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt @@ -11,6 +11,7 @@ import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter +import android.widget.LinearLayout import android.widget.TextView import android.widget.Toast import androidx.activity.OnBackPressedCallback @@ -25,6 +26,7 @@ import ani.dantotsu.connections.mal.MAL import ani.dantotsu.databinding.ActivitySettingsBinding import ani.dantotsu.others.AppUpdater import ani.dantotsu.others.CustomBottomDialog +import ani.dantotsu.others.LangSet import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.MangaSources import ani.dantotsu.subcriptions.Notifications @@ -33,9 +35,9 @@ import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes import ani.dantotsu.themes.ThemeManager -import ani.dantotsu.others.LangSet import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText +import com.skydoves.colorpickerview.listeners.ColorListener import eu.kanade.domain.base.BasePreferences import eu.kanade.tachiyomi.network.NetworkPreferences import io.noties.markwon.Markwon @@ -61,7 +63,7 @@ class SettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivitySettingsBinding.inflate(layoutInflater) setContentView(binding.root) @@ -72,20 +74,21 @@ ThemeManager(this).applyTheme() fun getArch(): String { SUPPORTED_ABIS.forEach { when (it) { - "arm64-v8a" -> return "aarch64" + "arm64-v8a" -> return "aarch64" "armeabi-v7a" -> return "arm" - "x86_64" -> return "x86_64" - "x86" -> return "i686" + "x86_64" -> return "x86_64" + "x86" -> return "i686" } } - return System.getProperty("os.arch") ?: System.getProperty("os.product.cpu.abi") ?: "Unknown Architecture" + return System.getProperty("os.arch") ?: System.getProperty("os.product.cpu.abi") + ?: "Unknown Architecture" } val info = """ -dantotsu Version: ${BuildConfig.VERSION_NAME} -Device: $BRAND $DEVICE -Architecture: ${getArch()} -OS Version: $CODENAME $RELEASE ($SDK_INT) + dantotsu Version: ${BuildConfig.VERSION_NAME} + Device: $BRAND $DEVICE + Architecture: ${getArch()} + OS Version: $CODENAME $RELEASE ($SDK_INT) """.trimIndent() copyToClipboard(info, false) toast(getString(R.string.copied_device_info)) @@ -103,43 +106,125 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) onBackPressedDispatcher.onBackPressed() } - binding.settingsUseMaterialYou.isChecked = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_material_you", false) + binding.settingsUseMaterialYou.isChecked = + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean( + "use_material_you", + false + ) binding.settingsUseMaterialYou.setOnCheckedChangeListener { _, isChecked -> - getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean("use_material_you", isChecked).apply() + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit() + .putBoolean("use_material_you", isChecked).apply() + if (isChecked) binding.settingsUseCustomTheme.isChecked = false restartApp() } - binding.settingsUseOLED.isChecked = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_oled", false) + binding.settingsUseCustomTheme.isChecked = + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean( + "use_custom_theme", + false + ) + binding.settingsUseCustomTheme.setOnCheckedChangeListener { _, isChecked -> + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit() + .putBoolean("use_custom_theme", isChecked).apply() + if (isChecked) { + binding.settingsUseMaterialYou.isChecked = false + } + + restartApp() + } + + binding.settingsUseSourceTheme.isChecked = + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean( + "use_source_theme", + false + ) + binding.settingsUseSourceTheme.setOnCheckedChangeListener { _, isChecked -> + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit() + .putBoolean("use_source_theme", isChecked).apply() + } + + binding.settingsUseOLED.isChecked = + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_oled", false) binding.settingsUseOLED.setOnCheckedChangeListener { _, isChecked -> - getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean("use_oled", isChecked).apply() + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit() + .putBoolean("use_oled", isChecked).apply() restartApp() } - val themeString = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getString("theme", "PURPLE")!! - binding.themeSwitcher.setText(themeString.substring(0, 1) + themeString.substring(1).lowercase()) + val themeString = + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getString("theme", "PURPLE")!! + binding.themeSwitcher.setText( + themeString.substring(0, 1) + themeString.substring(1).lowercase() + ) - binding.themeSwitcher.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, ThemeManager.Companion.Theme.values().map { it.theme.substring(0, 1) + it.theme.substring(1).lowercase() })) + binding.themeSwitcher.setAdapter( + ArrayAdapter( + this, + R.layout.item_dropdown, + ThemeManager.Companion.Theme.values() + .map { it.theme.substring(0, 1) + it.theme.substring(1).lowercase() }) + ) binding.themeSwitcher.setOnItemClickListener { _, _, i, _ -> - getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putString("theme", ThemeManager.Companion.Theme.values()[i].theme).apply() + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit() + .putString("theme", ThemeManager.Companion.Theme.values()[i].theme).apply() //ActivityHelper.shouldRefreshMainActivity = true binding.themeSwitcher.clearFocus() restartApp() } + + binding.customTheme.setOnClickListener { + var passedColor: Int = 0 + val dialogView = layoutInflater.inflate(R.layout.dialog_color_picker, null) + val alertDialog = AlertDialog.Builder(this, R.style.MyPopup) + .setTitle("Custom Theme") + .setView(dialogView) + .setPositiveButton("OK") { dialog, _ -> + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit() + .putInt("custom_theme_int", passedColor).apply() + logger("Custom Theme: $passedColor") + dialog.dismiss() + restartApp() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + .create() + val colorPickerView = + dialogView.findViewById(R.id.colorPickerView) + colorPickerView.setColorListener(ColorListener { color, fromUser -> + val linearLayout = dialogView.findViewById(R.id.linear) + passedColor = color + linearLayout.setBackgroundColor(color) + }) + + alertDialog.show() + } + //val animeSource = loadData("settings_def_anime_source_s")?.let { if (it >= AnimeSources.names.size) 0 else it } ?: 0 - val animeSource = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("settings_def_anime_source_s_r", 0) + val animeSource = getSharedPreferences( + "Dantotsu", + Context.MODE_PRIVATE + ).getInt("settings_def_anime_source_s_r", 0) if (AnimeSources.names.isNotEmpty() && animeSource in 0 until AnimeSources.names.size) { binding.animeSource.setText(AnimeSources.names[animeSource], false) } - binding.animeSource.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, AnimeSources.names)) + binding.animeSource.setAdapter( + ArrayAdapter( + this, + R.layout.item_dropdown, + AnimeSources.names + ) + ) binding.animeSource.setOnItemClickListener { _, _, i, _ -> //saveData("settings_def_anime_source_s", i) - getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putInt("settings_def_anime_source_s_r", i).apply() + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit() + .putInt("settings_def_anime_source_s_r", i).apply() binding.animeSource.clearFocus() } @@ -148,7 +233,8 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) } val managers = arrayOf("Default", "1DM", "ADM") - val downloadManagerDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle("Download Manager") + val downloadManagerDialog = + AlertDialog.Builder(this, R.style.DialogTheme).setTitle("Download Manager") var downloadManager = loadData("settings_download_manager") ?: 0 binding.settingsDownloadManager.setOnClickListener { downloadManagerDialog.setSingleChoiceItems(managers, downloadManager) { dialog, count -> @@ -158,11 +244,12 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) }.show() } - binding.settingsForceLegacyInstall.isChecked = extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY + binding.settingsForceLegacyInstall.isChecked = + extensionInstaller.get() == BasePreferences.ExtensionInstaller.LEGACY binding.settingsForceLegacyInstall.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { extensionInstaller.set(BasePreferences.ExtensionInstaller.LEGACY) - }else{ + } else { extensionInstaller.set(BasePreferences.ExtensionInstaller.PACKAGEINSTALLER) } } @@ -177,29 +264,48 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) } - // binding.userAgent.setText(networkPreferences.defaultUserAgent().get()) - binding.userAgent.setOnClickListener{ + binding.userAgent.setOnClickListener { val dialogView = layoutInflater.inflate(R.layout.dialog_user_agent, null) val editText = dialogView.findViewById(R.id.userAgentTextBox) editText.setText(networkPreferences.defaultUserAgent().get()) - val alertDialog = AlertDialog.Builder(this) + val alertDialog = AlertDialog.Builder(this, R.style.MyPopup) + .setTitle("User Agent") .setView(dialogView) .setPositiveButton("OK") { dialog, _ -> networkPreferences.defaultUserAgent().set(editText.text.toString()) dialog.dismiss() } - .setNegativeButton("Reset") { dialog, _ -> - networkPreferences.defaultUserAgent().set("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0") // Reset to default or empty + .setNeutralButton("Reset") { dialog, _ -> + networkPreferences.defaultUserAgent() + .set("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:110.0) Gecko/20100101 Firefox/110.0") // Reset to default or empty editText.setText("") dialog.dismiss() } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } .create() alertDialog.show() } - val exDns = listOf("None", "Cloudflare", "Google", "AdGuard", "Quad9", "AliDNS", "DNSPod", "360", "Quad101", "Mullvad", "Controld", "Njalla", "Shecan", "Libre") + val exDns = listOf( + "None", + "Cloudflare", + "Google", + "AdGuard", + "Quad9", + "AliDNS", + "DNSPod", + "360", + "Quad101", + "Mullvad", + "Controld", + "Njalla", + "Shecan", + "Libre" + ) binding.settingsExtensionDns.setText(exDns[networkPreferences.dohProvider().get()], false) binding.settingsExtensionDns.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, exDns)) binding.settingsExtensionDns.setOnItemClickListener { _, _, i, _ -> @@ -246,19 +352,29 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) saveData("settings_prefer_dub", isChecked) } - //val mangaSource = loadData("settings_def_manga_source_s")?.let { if (it >= MangaSources.names.size) 0 else it } ?: 0 - val mangaSource = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("settings_def_manga_source_s_r", 0) - if (MangaSources.names.isNotEmpty() && mangaSource in 0 until MangaSources.names.size) { - binding.mangaSource.setText(MangaSources.names[mangaSource], false) + //val mangaSource = loadData("settings_def_manga_source_s")?.let { if (it >= MangaSources.names.size) 0 else it } ?: 0 + val mangaSource = getSharedPreferences( + "Dantotsu", + Context.MODE_PRIVATE + ).getInt("settings_def_manga_source_s_r", 0) + if (MangaSources.names.isNotEmpty() && mangaSource in 0 until MangaSources.names.size) { + binding.mangaSource.setText(MangaSources.names[mangaSource], false) } // Set up the dropdown adapter. - binding.mangaSource.setAdapter(ArrayAdapter(this, R.layout.item_dropdown, MangaSources.names)) + binding.mangaSource.setAdapter( + ArrayAdapter( + this, + R.layout.item_dropdown, + MangaSources.names + ) + ) // Set up the item click listener for the dropdown. binding.mangaSource.setOnItemClickListener { _, _, i, _ -> //saveData("settings_def_manga_source_s", i) - getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putInt("settings_def_manga_source_s_r", i).apply() + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit() + .putInt("settings_def_manga_source_s_r", i).apply() binding.mangaSource.clearFocus() } @@ -267,10 +383,11 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) } val uiSettings: UserInterfaceSettings = - loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } + loadData("ui_settings", toast = false) + ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } var previous: View = when (uiSettings.darkMode) { - null -> binding.settingsUiAuto - true -> binding.settingsUiDark + null -> binding.settingsUiAuto + true -> binding.settingsUiDark false -> binding.settingsUiLight } previous.alpha = 1f @@ -291,6 +408,7 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) } binding.settingsUiLight.setOnClickListener { + binding.settingsUseOLED.isChecked = false uiTheme(false, it) } @@ -299,9 +417,9 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) } var previousStart: View = when (uiSettings.defaultStartUpTab) { - 0 -> binding.uiSettingsAnime - 1 -> binding.uiSettingsHome - 2 -> binding.uiSettingsManga + 0 -> binding.uiSettingsAnime + 1 -> binding.uiSettingsHome + 2 -> binding.uiSettingsManga else -> binding.uiSettingsHome } previousStart.alpha = 1f @@ -333,9 +451,9 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) } var previousEp: View = when (uiSettings.animeDefaultView) { - 0 -> binding.settingsEpList - 1 -> binding.settingsEpGrid - 2 -> binding.settingsEpCompact + 0 -> binding.settingsEpList + 1 -> binding.settingsEpGrid + 2 -> binding.settingsEpCompact else -> binding.settingsEpList } previousEp.alpha = 1f @@ -360,8 +478,8 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) } var previousChp: View = when (uiSettings.mangaDefaultView) { - 0 -> binding.settingsChpList - 1 -> binding.settingsChpCompact + 0 -> binding.settingsChpList + 1 -> binding.settingsChpCompact else -> binding.settingsChpList } previousChp.alpha = 1f @@ -416,11 +534,16 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) binding.settingsLogo.setSafeOnClickListener { cursedCounter++ (binding.settingsLogo.drawable as Animatable).start() - if (cursedCounter % 7 == 0){ + if (cursedCounter % 7 == 0) { snackString("youwu have been cuwsed :pwayge:") - getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean("use_cursed_lang", - getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_cursed_lang", false).not()).apply() - } else{ + getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit().putBoolean( + "use_cursed_lang", + getSharedPreferences( + "Dantotsu", + Context.MODE_PRIVATE + ).getBoolean("use_cursed_lang", false).not() + ).apply() + } else { snackString(array[(Math.random() * array.size).toInt()], this) } @@ -454,12 +577,15 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) if (it > 0) "${if (hours > 0) "$hours hrs " else ""}${if (mins > 0) "$mins mins" else ""}" else getString(R.string.do_not_update) }.toTypedArray() - binding.settingsSubscriptionsTime.text = getString(R.string.subscriptions_checking_time_s, timeNames[curTime]) - val speedDialog = AlertDialog.Builder(this, R.style.DialogTheme).setTitle(R.string.subscriptions_checking_time) + binding.settingsSubscriptionsTime.text = + getString(R.string.subscriptions_checking_time_s, timeNames[curTime]) + val speedDialog = AlertDialog.Builder(this, R.style.DialogTheme) + .setTitle(R.string.subscriptions_checking_time) binding.settingsSubscriptionsTime.setOnClickListener { speedDialog.setSingleChoiceItems(timeNames, curTime) { dialog, i -> curTime = i - binding.settingsSubscriptionsTime.text = getString(R.string.subscriptions_checking_time_s, timeNames[i]) + binding.settingsSubscriptionsTime.text = + getString(R.string.subscriptions_checking_time_s, timeNames[i]) saveData("subscriptions_time_s", curTime) dialog.dismiss() startSubscription(true) @@ -471,7 +597,8 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) true } - binding.settingsNotificationsCheckingSubscriptions.isChecked = loadData("subscription_checking_notifications") ?: true + binding.settingsNotificationsCheckingSubscriptions.isChecked = + loadData("subscription_checking_notifications") ?: true binding.settingsNotificationsCheckingSubscriptions.setOnCheckedChangeListener { _, isChecked -> saveData("subscription_checking_notifications", isChecked) if (isChecked) @@ -520,7 +647,8 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) setTitleText(title) addView( TextView(it.context).apply { - val markWon = Markwon.builder(it.context).usePlugin(SoftBreakAddsNewLinePlugin.create()).build() + val markWon = Markwon.builder(it.context) + .usePlugin(SoftBreakAddsNewLinePlugin.create()).build() markWon.setMarkdown(this, full) } ) @@ -574,11 +702,24 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) } if (Discord.token != null) { - if (Discord.avatar != null) { - binding.settingsDiscordAvatar.loadImage(Discord.avatar) + val id = getSharedPreferences( + getString(R.string.preference_file_key), + Context.MODE_PRIVATE + ).getString("discord_id", null) + val avatar = getSharedPreferences( + getString(R.string.preference_file_key), + Context.MODE_PRIVATE + ).getString("discord_avatar", null) + val username = getSharedPreferences( + getString(R.string.preference_file_key), + Context.MODE_PRIVATE + ).getString("discord_username", null) + if (id != null && avatar != null) { + binding.settingsDiscordAvatar.loadImage("https://cdn.discordapp.com/avatars/$id/$avatar.png") } binding.settingsDiscordUsername.visibility = View.VISIBLE - binding.settingsDiscordUsername.text = Discord.userid ?: Discord.token?.replace(Regex("."),"*") + binding.settingsDiscordUsername.text = + username ?: Discord.token?.replace(Regex("."), "*") binding.settingsDiscordLogin.setText(R.string.logout) binding.settingsDiscordLogin.setOnClickListener { Discord.removeSavedToken(this) @@ -623,13 +764,18 @@ OS Version: $CODENAME $RELEASE ($SDK_INT) } } } + private fun restartApp() { Snackbar.make( binding.root, R.string.restart_app, Snackbar.LENGTH_SHORT ).apply { val mainIntent = - Intent.makeRestartActivityTask(context.packageManager.getLaunchIntentForPackage(context.packageName)!!.component) + Intent.makeRestartActivityTask( + context.packageManager.getLaunchIntentForPackage( + context.packageName + )!!.component + ) setAction("Do it!") { context.startActivity(mainIntent) Runtime.getRuntime().exit(0) diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt b/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt index 0b10ca1d..f59f7e1d 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsDialogFragment.kt @@ -10,21 +10,31 @@ import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Switch import androidx.core.content.ContextCompat -import ani.dantotsu.* +import ani.dantotsu.BottomSheetDialogFragment +import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist -import ani.dantotsu.others.imagesearch.ImageSearchActivity import ani.dantotsu.databinding.BottomSheetSettingsBinding import ani.dantotsu.download.DownloadContainerActivity import ani.dantotsu.download.manga.OfflineMangaFragment +import ani.dantotsu.loadData +import ani.dantotsu.loadImage +import ani.dantotsu.openLinkInBrowser +import ani.dantotsu.others.imagesearch.ImageSearchActivity +import ani.dantotsu.setSafeOnClickListener +import ani.dantotsu.startMainActivity +import ani.dantotsu.toast class SettingsDialogFragment(val pageType: PageType) : BottomSheetDialogFragment() { private var _binding: BottomSheetSettingsBinding? = null private val binding get() = _binding!! - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { _binding = BottomSheetSettingsBinding.inflate(inflater, container, false) return binding.root } @@ -43,7 +53,7 @@ class SettingsDialogFragment(val pageType: PageType) : BottomSheetDialogFragment binding.settingsLogin.setOnClickListener { Anilist.removeSavedToken(it.context) dismiss() - startMainActivity(requireActivity(),) + startMainActivity(requireActivity()) } binding.settingsUsername.text = Anilist.username binding.settingsUserAvatar.loadImage(Anilist.avatar) @@ -73,15 +83,17 @@ class SettingsDialogFragment(val pageType: PageType) : BottomSheetDialogFragment dismiss() } binding.settingsDownloads.setSafeOnClickListener { - when(pageType) { + when (pageType) { PageType.MANGA -> { val intent = Intent(activity, DownloadContainerActivity::class.java) intent.putExtra("FRAGMENT_CLASS_NAME", OfflineMangaFragment::class.java.name) startActivity(intent) } + PageType.ANIME -> { try { - val arrayOfFiles = ContextCompat.getExternalFilesDirs(requireContext(), null) + val arrayOfFiles = + ContextCompat.getExternalFilesDirs(requireContext(), null) startActivity( if (loadData("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) { val parentDirectory = arrayOfFiles[1].toString() @@ -93,9 +105,11 @@ class SettingsDialogFragment(val pageType: PageType) : BottomSheetDialogFragment toast(getString(R.string.file_manager_not_found)) } } + PageType.HOME -> { try { - val arrayOfFiles = ContextCompat.getExternalFilesDirs(requireContext(), null) + val arrayOfFiles = + ContextCompat.getExternalFilesDirs(requireContext(), null) startActivity( if (loadData("sd_dl") == true && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) { val parentDirectory = arrayOfFiles[1].toString() @@ -118,8 +132,8 @@ class SettingsDialogFragment(val pageType: PageType) : BottomSheetDialogFragment _binding = null } - companion object{ - enum class PageType{ + companion object { + enum class PageType { MANGA, ANIME, HOME } } diff --git a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettings.kt b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettings.kt index ba71457c..39dd18b1 100644 --- a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettings.kt +++ b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettings.kt @@ -12,7 +12,15 @@ data class UserInterfaceSettings( var immersiveMode: Boolean = false, var smallView: Boolean = true, var defaultStartUpTab: Int = 1, - var homeLayoutShow: MutableList = mutableListOf(true, false, false, true, false, false, true), + var homeLayoutShow: MutableList = mutableListOf( + true, + false, + false, + true, + false, + false, + true + ), //Animations var bannerAnimations: Boolean = true, diff --git a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt index 8c238b48..d1e46215 100644 --- a/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/UserInterfaceSettingsActivity.kt @@ -6,10 +6,15 @@ import android.os.Bundle import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.view.updateLayoutParams -import ani.dantotsu.* +import ani.dantotsu.R import ani.dantotsu.databinding.ActivityUserInterfaceSettingsBinding -import ani.dantotsu.themes.ThemeManager +import ani.dantotsu.initActivity +import ani.dantotsu.loadData +import ani.dantotsu.navBarHeight import ani.dantotsu.others.LangSet +import ani.dantotsu.saveData +import ani.dantotsu.statusBarHeight +import ani.dantotsu.themes.ThemeManager import com.google.android.material.snackbar.Snackbar class UserInterfaceSettingsActivity : AppCompatActivity() { @@ -18,7 +23,7 @@ class UserInterfaceSettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) LangSet.setLocale(this) -ThemeManager(this).applyTheme() + ThemeManager(this).applyTheme() binding = ActivityUserInterfaceSettingsBinding.inflate(layoutInflater) setContentView(binding.root) @@ -28,7 +33,8 @@ ThemeManager(this).applyTheme() bottomMargin = navBarHeight } - val settings = loadData(ui, toast = false) ?: UserInterfaceSettings().apply { saveData(ui, this) } + val settings = loadData(ui, toast = false) + ?: UserInterfaceSettings().apply { saveData(ui, this) } binding.uiSettingsBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() @@ -36,8 +42,12 @@ ThemeManager(this).applyTheme() val views = resources.getStringArray(R.array.home_layouts) binding.uiSettingsHomeLayout.setOnClickListener { - AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.home_layout_show)).apply { - setMultiChoiceItems(views, settings.homeLayoutShow.toBooleanArray()) { _, i, value -> + AlertDialog.Builder(this, R.style.DialogTheme) + .setTitle(getString(R.string.home_layout_show)).apply { + setMultiChoiceItems( + views, + settings.homeLayoutShow.toBooleanArray() + ) { _, i, value -> settings.homeLayoutShow[i] = value saveData(ui, settings) } @@ -100,7 +110,11 @@ ThemeManager(this).applyTheme() R.string.restart_app, Snackbar.LENGTH_SHORT ).apply { val mainIntent = - Intent.makeRestartActivityTask(context.packageManager.getLaunchIntentForPackage(context.packageName)!!.component) + Intent.makeRestartActivityTask( + context.packageManager.getLaunchIntentForPackage( + context.packageName + )!!.component + ) setAction("Do it!") { context.startActivity(mainIntent) Runtime.getRuntime().exit(0) diff --git a/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt b/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt index b82629f5..c3e3b4f4 100644 --- a/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt +++ b/app/src/main/java/ani/dantotsu/settings/extensionprefs/AnimePreferenceFragmentCompat.kt @@ -1,12 +1,8 @@ package ani.dantotsu.settings.extensionprefs import android.content.Context -import android.content.SharedPreferences import android.os.Bundle -import android.util.Log import android.util.TypedValue -import android.view.View -import android.widget.FrameLayout import androidx.core.os.bundleOf import androidx.lifecycle.lifecycleScope import androidx.preference.DialogPreference @@ -14,11 +10,6 @@ import androidx.preference.EditTextPreference import androidx.preference.PreferenceFragmentCompat import androidx.preference.forEach import androidx.preference.getOnBindEditTextListener -import androidx.viewpager2.widget.ViewPager2 -import ani.dantotsu.R -import ani.dantotsu.settings.ExtensionsActivity -import com.google.android.material.tabs.TabLayout -import com.google.android.material.textfield.TextInputLayout import eu.kanade.tachiyomi.PreferenceScreen import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore @@ -33,9 +24,14 @@ class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() { preferenceScreen = populateAnimePreferenceScreen() //set background color val color = TypedValue() - requireContext().theme.resolveAttribute(com.google.android.material.R.attr.backgroundColor, color, true) + requireContext().theme.resolveAttribute( + com.google.android.material.R.attr.backgroundColor, + color, + true + ) view?.setBackgroundColor(color.data) } + private var onCloseAction: (() -> Unit)? = null @@ -48,7 +44,8 @@ class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() { val sourceId = requireArguments().getLong(SOURCE_ID) val source = Injekt.get().get(sourceId)!! check(source is ConfigurableAnimeSource) - val sharedPreferences = requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE) + val sharedPreferences = + requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE) val dataStore = SharedPreferencesDataStore(sharedPreferences) preferenceManager.preferenceDataStore = dataStore val sourceScreen = preferenceManager.createPreferenceScreen(requireContext()) @@ -57,7 +54,7 @@ class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() { pref.isIconSpaceReserved = false if (pref is DialogPreference) { pref.dialogTitle = pref.title - //println("pref.dialogTitle: ${pref.dialogTitle}") + //println("pref.dialogTitle: ${pref.dialogTitle}") //TODO: could be useful for dub/sub selection } /*for (entry in sharedPreferences.all.entries) { Log.d("Preferences", "Key: ${entry.key}, Value: ${entry.value}") @@ -75,7 +72,11 @@ class AnimeSourcePreferencesFragment : PreferenceFragmentCompat() { return sourceScreen } - fun getInstance(sourceId: Long, onCloseAction: (() -> Unit)? = null): AnimeSourcePreferencesFragment { + + fun getInstance( + sourceId: Long, + onCloseAction: (() -> Unit)? = null + ): AnimeSourcePreferencesFragment { val fragment = AnimeSourcePreferencesFragment() fragment.arguments = bundleOf(SOURCE_ID to sourceId) fragment.onCloseAction = onCloseAction diff --git a/app/src/main/java/ani/dantotsu/settings/extensionprefs/MangaPreferenceFragmentCompat.kt b/app/src/main/java/ani/dantotsu/settings/extensionprefs/MangaPreferenceFragmentCompat.kt index d571ef77..4ccee760 100644 --- a/app/src/main/java/ani/dantotsu/settings/extensionprefs/MangaPreferenceFragmentCompat.kt +++ b/app/src/main/java/ani/dantotsu/settings/extensionprefs/MangaPreferenceFragmentCompat.kt @@ -2,8 +2,6 @@ package ani.dantotsu.settings.extensionprefs import android.content.Context import android.os.Bundle -import android.view.View -import android.widget.FrameLayout import androidx.core.os.bundleOf import androidx.lifecycle.lifecycleScope import androidx.preference.DialogPreference @@ -11,11 +9,6 @@ import androidx.preference.EditTextPreference import androidx.preference.PreferenceFragmentCompat import androidx.preference.forEach import androidx.preference.getOnBindEditTextListener -import androidx.viewpager2.widget.ViewPager2 -import ani.dantotsu.R -import ani.dantotsu.settings.ExtensionsActivity -import com.google.android.material.tabs.TabLayout -import com.google.android.material.textfield.TextInputLayout import eu.kanade.tachiyomi.PreferenceScreen import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore import eu.kanade.tachiyomi.source.ConfigurableSource @@ -29,6 +22,7 @@ class MangaSourcePreferencesFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceScreen = populateMangaPreferenceScreen() } + private var onCloseAction: (() -> Unit)? = null override fun onDestroyView() { @@ -41,7 +35,8 @@ class MangaSourcePreferencesFragment : PreferenceFragmentCompat() { val sourceId = requireArguments().getLong(SOURCE_ID) val source = Injekt.get().get(sourceId)!! check(source is ConfigurableSource) - val sharedPreferences = requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE) + val sharedPreferences = + requireContext().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE) val dataStore = SharedPreferencesDataStore(sharedPreferences) preferenceManager.preferenceDataStore = dataStore val sourceScreen = preferenceManager.createPreferenceScreen(requireContext()) @@ -65,7 +60,11 @@ class MangaSourcePreferencesFragment : PreferenceFragmentCompat() { return sourceScreen } - fun getInstance(sourceId: Long, onCloseAction: (() -> Unit)? = null): MangaSourcePreferencesFragment { + + fun getInstance( + sourceId: Long, + onCloseAction: (() -> Unit)? = null + ): MangaSourcePreferencesFragment { val fragment = MangaSourcePreferencesFragment() fragment.arguments = bundleOf(SOURCE_ID to sourceId) fragment.onCloseAction = onCloseAction diff --git a/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt b/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt index 235d4f15..045ca140 100644 --- a/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt +++ b/app/src/main/java/ani/dantotsu/settings/paging/AnimePagingSource.kt @@ -1,8 +1,8 @@ package ani.dantotsu.settings.paging -import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup +import android.view.animation.LinearInterpolator import android.widget.ImageView import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -16,18 +16,25 @@ import androidx.paging.PagingState import androidx.paging.cachedIn import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.R import ani.dantotsu.databinding.ItemExtensionAllBinding import ani.dantotsu.loadData import ani.dantotsu.others.LanguageMapper import com.bumptech.glide.Glide import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class AnimeExtensionsViewModelFactory( @@ -47,11 +54,19 @@ class AnimeExtensionsViewModel( fun setSearchQuery(query: String) { searchQuery.value = query } + fun invalidatePager() { currentPagingSource?.invalidate() } + @OptIn(ExperimentalCoroutinesApi::class) - val pagerFlow: Flow> = searchQuery.flatMapLatest { query -> + val pagerFlow: Flow> = combine( + animeExtensionManager.availableExtensionsFlow, + animeExtensionManager.installedExtensionsFlow, + searchQuery + ) { available, installed, query -> + Triple(available, installed, query) + }.flatMapLatest { (available, installed, query) -> Pager( PagingConfig( pageSize = 15, @@ -59,34 +74,31 @@ class AnimeExtensionsViewModel( prefetchDistance = 15 ) ) { - AnimeExtensionPagingSource( - animeExtensionManager.availableExtensionsFlow, - animeExtensionManager.installedExtensionsFlow, - searchQuery - ).also { currentPagingSource = it } + AnimeExtensionPagingSource(available, installed, query) }.flow }.cachedIn(viewModelScope) } class AnimeExtensionPagingSource( - private val availableExtensionsFlow: StateFlow>, - private val installedExtensionsFlow: StateFlow>, - private val searchQuery: StateFlow + private val availableExtensionsFlow: List, + private val installedExtensionsFlow: List, + private val searchQuery: String ) : PagingSource() { override suspend fun load(params: LoadParams): LoadResult { val position = params.key ?: 0 - val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet() - val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions } - val query = searchQuery.first() - val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: false + val installedExtensions = installedExtensionsFlow.map { it.pkgName }.toSet() + val availableExtensions = + availableExtensionsFlow.filterNot { it.pkgName in installedExtensions } + val query = searchQuery + val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true val filteredExtensions = if (query.isEmpty()) { availableExtensions } else { availableExtensions.filter { it.name.contains(query, ignoreCase = true) } } - val filternfsw = if(isNsfwEnabled) { + val filternfsw = if (isNsfwEnabled) { filteredExtensions } else { filteredExtensions.filterNot { it.isNsfw } @@ -120,12 +132,18 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: AnimeExtension.Available, newItem: AnimeExtension.Available): Boolean { + override fun areItemsTheSame( + oldItem: AnimeExtension.Available, + newItem: AnimeExtension.Available + ): Boolean { // Your logic here return oldItem.pkgName == newItem.pkgName } - override fun areContentsTheSame(oldItem: AnimeExtension.Available, newItem: AnimeExtension.Available): Boolean { + override fun areContentsTheSame( + oldItem: AnimeExtension.Available, + newItem: AnimeExtension.Available + ): Boolean { // Your logic here return oldItem == newItem } @@ -133,7 +151,8 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimeExtensionViewHolder { - val binding = ItemExtensionAllBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemExtensionAllBinding.inflate(LayoutInflater.from(parent.context), parent, false) return AnimeExtensionViewHolder(binding) } @@ -149,23 +168,51 @@ class AnimeExtensionAdapter(private val clickListener: OnAnimeInstallClickListen } } - inner class AnimeExtensionViewHolder(private val binding: ItemExtensionAllBinding) : RecyclerView.ViewHolder(binding.root) { + inner class AnimeExtensionViewHolder(private val binding: ItemExtensionAllBinding) : + RecyclerView.ViewHolder(binding.root) { + + private val job = Job() + private val scope = CoroutineScope(Dispatchers.Main + job) + init { binding.closeTextView.setOnClickListener { val extension = getItem(bindingAdapterPosition) if (extension != null) { clickListener.onInstallClick(extension) + binding.closeTextView.setImageResource(R.drawable.ic_sync) + scope.launch { + while (isActive) { + withContext(Dispatchers.Main) { + binding.closeTextView.animate() + .rotationBy(360f) + .setDuration(1000) + .setInterpolator(LinearInterpolator()) + .start() + } + delay(1000) + } + } } } } + val extensionIconImageView: ImageView = binding.extensionIconImageView - fun bind(extension: AnimeExtension.Available) { + fun bind(extension: AnimeExtension.Available) { val nsfw = if (extension.isNsfw) "(18+)" else "" - val lang= LanguageMapper.mapLanguageCodeToName(extension.lang) + val lang = LanguageMapper.mapLanguageCodeToName(extension.lang) binding.extensionNameTextView.text = extension.name binding.extensionVersionTextView.text = "$lang ${extension.versionName} $nsfw" } + + fun clear() { + job.cancel() // Cancel the coroutine when the view is recycled + } + } + + override fun onViewRecycled(holder: AnimeExtensionViewHolder) { + super.onViewRecycled(holder) + holder.clear() } } diff --git a/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt b/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt index 717cb1d2..0ae7e01d 100644 --- a/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt +++ b/app/src/main/java/ani/dantotsu/settings/paging/MangaPagingSource.kt @@ -1,8 +1,8 @@ package ani.dantotsu.settings.paging -import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup +import android.view.animation.LinearInterpolator import android.widget.ImageView import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -16,18 +16,25 @@ import androidx.paging.PagingState import androidx.paging.cachedIn import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.R import ani.dantotsu.databinding.ItemExtensionAllBinding import ani.dantotsu.loadData import ani.dantotsu.others.LanguageMapper import com.bumptech.glide.Glide import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.model.MangaExtension +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class MangaExtensionsViewModelFactory( private val mangaExtensionManager: MangaExtensionManager @@ -52,7 +59,13 @@ class MangaExtensionsViewModel( } @OptIn(ExperimentalCoroutinesApi::class) - val pagerFlow: Flow> = searchQuery.flatMapLatest { query -> + val pagerFlow: Flow> = combine( + mangaExtensionManager.availableExtensionsFlow, + mangaExtensionManager.installedExtensionsFlow, + searchQuery + ) { available, installed, query -> + Triple(available, installed, query) + }.flatMapLatest { (available, installed, query) -> Pager( PagingConfig( pageSize = 15, @@ -60,34 +73,31 @@ class MangaExtensionsViewModel( prefetchDistance = 15 ) ) { - MangaExtensionPagingSource( - mangaExtensionManager.availableExtensionsFlow, - mangaExtensionManager.installedExtensionsFlow, - searchQuery - ).also { currentPagingSource = it } + MangaExtensionPagingSource(available, installed, query) }.flow }.cachedIn(viewModelScope) } class MangaExtensionPagingSource( - private val availableExtensionsFlow: StateFlow>, - private val installedExtensionsFlow: StateFlow>, - private val searchQuery: StateFlow + private val availableExtensionsFlow: List, + private val installedExtensionsFlow: List, + private val searchQuery: String ) : PagingSource() { override suspend fun load(params: LoadParams): LoadResult { val position = params.key ?: 0 - val installedExtensions = installedExtensionsFlow.first().map { it.pkgName }.toSet() - val availableExtensions = availableExtensionsFlow.first().filterNot { it.pkgName in installedExtensions } - val query = searchQuery.first() - val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: false + val installedExtensions = installedExtensionsFlow.map { it.pkgName }.toSet() + val availableExtensions = + availableExtensionsFlow.filterNot { it.pkgName in installedExtensions } + val query = searchQuery + val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true val filteredExtensions = if (query.isEmpty()) { availableExtensions } else { availableExtensions.filter { it.name.contains(query, ignoreCase = true) } } - val filternfsw = if(isNsfwEnabled) { + val filternfsw = if (isNsfwEnabled) { filteredExtensions } else { filteredExtensions.filterNot { it.isNsfw } @@ -121,18 +131,25 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean { + override fun areItemsTheSame( + oldItem: MangaExtension.Available, + newItem: MangaExtension.Available + ): Boolean { return oldItem.pkgName == newItem.pkgName } - override fun areContentsTheSame(oldItem: MangaExtension.Available, newItem: MangaExtension.Available): Boolean { + override fun areContentsTheSame( + oldItem: MangaExtension.Available, + newItem: MangaExtension.Available + ): Boolean { return oldItem == newItem } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaExtensionViewHolder { - val binding = ItemExtensionAllBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = + ItemExtensionAllBinding.inflate(LayoutInflater.from(parent.context), parent, false) return MangaExtensionViewHolder(binding) } @@ -148,22 +165,50 @@ class MangaExtensionAdapter(private val clickListener: OnMangaInstallClickListen } } - inner class MangaExtensionViewHolder(private val binding: ItemExtensionAllBinding) : RecyclerView.ViewHolder(binding.root) { + inner class MangaExtensionViewHolder(private val binding: ItemExtensionAllBinding) : + RecyclerView.ViewHolder(binding.root) { + + private val job = Job() + private val scope = CoroutineScope(Dispatchers.Main + job) + init { binding.closeTextView.setOnClickListener { val extension = getItem(bindingAdapterPosition) if (extension != null) { clickListener.onInstallClick(extension) + binding.closeTextView.setImageResource(R.drawable.ic_sync) + scope.launch { + while (isActive) { + withContext(Dispatchers.Main) { + binding.closeTextView.animate() + .rotationBy(360f) + .setDuration(1000) + .setInterpolator(LinearInterpolator()) + .start() + } + delay(1000) + } + } } } } + val extensionIconImageView: ImageView = binding.extensionIconImageView fun bind(extension: MangaExtension.Available) { val nsfw = if (extension.isNsfw) "(18+)" else "" - val lang= LanguageMapper.mapLanguageCodeToName(extension.lang) + val lang = LanguageMapper.mapLanguageCodeToName(extension.lang) binding.extensionNameTextView.text = extension.name binding.extensionVersionTextView.text = "$lang ${extension.versionName} $nsfw" } + + fun clear() { + job.cancel() // Cancel the coroutine when the view is recycled + } + } + + override fun onViewRecycled(holder: MangaExtensionViewHolder) { + super.onViewRecycled(holder) + holder.clear() } } diff --git a/app/src/main/java/ani/dantotsu/settings/paging/NovelPagingSource.kt b/app/src/main/java/ani/dantotsu/settings/paging/NovelPagingSource.kt new file mode 100644 index 00000000..82585ce0 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/paging/NovelPagingSource.kt @@ -0,0 +1,220 @@ +package ani.dantotsu.settings.paging + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.view.animation.LinearInterpolator +import android.widget.ImageView +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingDataAdapter +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.cachedIn +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.R +import ani.dantotsu.databinding.ItemExtensionAllBinding +import ani.dantotsu.loadData +import ani.dantotsu.others.LanguageMapper +import ani.dantotsu.parsers.novel.NovelExtension +import ani.dantotsu.parsers.novel.NovelExtensionManager +import com.bumptech.glide.Glide +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +class NovelExtensionsViewModelFactory( + private val novelExtensionManager: NovelExtensionManager +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return NovelExtensionsViewModel(novelExtensionManager) as T + } +} + +class NovelExtensionsViewModel( + private val novelExtensionManager: NovelExtensionManager +) : ViewModel() { + private val searchQuery = MutableStateFlow("") + private var currentPagingSource: NovelExtensionPagingSource? = null + + fun setSearchQuery(query: String) { + searchQuery.value = query + } + + fun invalidatePager() { + currentPagingSource?.invalidate() + + } + + @OptIn(ExperimentalCoroutinesApi::class) + val pagerFlow: Flow> = combine( + novelExtensionManager.availableExtensionsFlow, + novelExtensionManager.installedExtensionsFlow, + searchQuery + ) { available, installed, query -> + Triple(available, installed, query) + }.flatMapLatest { (available, installed, query) -> + Pager( + PagingConfig( + pageSize = 15, + initialLoadSize = 15, + prefetchDistance = 15 + ) + ) { + NovelExtensionPagingSource(available, installed, query) + }.flow + }.cachedIn(viewModelScope) +} + + +class NovelExtensionPagingSource( + private val availableExtensionsFlow: List, + private val installedExtensionsFlow: List, + private val searchQuery: String +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + val position = params.key ?: 0 + val installedExtensions = installedExtensionsFlow.map { it.pkgName }.toSet() + val availableExtensions = + availableExtensionsFlow.filterNot { it.pkgName in installedExtensions } + val query = searchQuery + val isNsfwEnabled: Boolean = loadData("NFSWExtension") ?: true + val filteredExtensions = if (query.isEmpty()) { + availableExtensions + } else { + availableExtensions.filter { it.name.contains(query, ignoreCase = true) } + } + val filternfsw = filteredExtensions + /*val filternfsw = if(isNsfwEnabled) { currently not implemented + filteredExtensions + } else { + filteredExtensions.filterNot { it.isNsfw } + }*/ + return try { + val sublist = filternfsw.subList( + fromIndex = position, + toIndex = (position + params.loadSize).coerceAtMost(filternfsw.size) + ) + LoadResult.Page( + data = sublist, + prevKey = if (position == 0) null else position - params.loadSize, + nextKey = if (position + params.loadSize >= filternfsw.size) null else position + params.loadSize + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return null + } +} + +class NovelExtensionAdapter(private val clickListener: OnNovelInstallClickListener) : + PagingDataAdapter( + DIFF_CALLBACK + ) { + + private val skipIcons = loadData("skip_extension_icons") ?: false + + companion object { + private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: NovelExtension.Available, + newItem: NovelExtension.Available + ): Boolean { + return oldItem.pkgName == newItem.pkgName + } + + override fun areContentsTheSame( + oldItem: NovelExtension.Available, + newItem: NovelExtension.Available + ): Boolean { + return oldItem == newItem + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NovelExtensionViewHolder { + val binding = + ItemExtensionAllBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return NovelExtensionViewHolder(binding) + } + + override fun onBindViewHolder(holder: NovelExtensionViewHolder, position: Int) { + val extension = getItem(position) + if (extension != null) { + if (!skipIcons) { + Glide.with(holder.itemView.context) + .load(extension.iconUrl) + .into(holder.extensionIconImageView) + } + holder.bind(extension) + } + } + + inner class NovelExtensionViewHolder(private val binding: ItemExtensionAllBinding) : + RecyclerView.ViewHolder(binding.root) { + + private val job = Job() + private val scope = CoroutineScope(Dispatchers.Main + job) + + init { + binding.closeTextView.setOnClickListener { + val extension = getItem(bindingAdapterPosition) + if (extension != null) { + clickListener.onInstallClick(extension) + binding.closeTextView.setImageResource(R.drawable.ic_sync) + scope.launch { + while (isActive) { + withContext(Dispatchers.Main) { + binding.closeTextView.animate() + .rotationBy(360f) + .setDuration(1000) + .setInterpolator(LinearInterpolator()) + .start() + } + delay(1000) + } + } + } + } + } + + val extensionIconImageView: ImageView = binding.extensionIconImageView + fun bind(extension: NovelExtension.Available) { + val nsfw = "" + val lang = LanguageMapper.mapLanguageCodeToName("all") + binding.extensionNameTextView.text = extension.name + binding.extensionVersionTextView.text = "$lang ${extension.versionName} $nsfw" + } + + fun clear() { + job.cancel() // Cancel the coroutine when the view is recycled + } + } + + override fun onViewRecycled(holder: NovelExtensionViewHolder) { + super.onViewRecycled(holder) + holder.clear() + } +} + +interface OnNovelInstallClickListener { + fun onInstallClick(pkg: NovelExtension.Available) +} diff --git a/app/src/main/java/ani/dantotsu/subcriptions/AlarmReceiver.kt b/app/src/main/java/ani/dantotsu/subcriptions/AlarmReceiver.kt index 0849ca07..a326c531 100644 --- a/app/src/main/java/ani/dantotsu/subcriptions/AlarmReceiver.kt +++ b/app/src/main/java/ani/dantotsu/subcriptions/AlarmReceiver.kt @@ -5,11 +5,14 @@ import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.os.Build -import ani.dantotsu.* +import ani.dantotsu.currContext +import ani.dantotsu.isOnline +import ani.dantotsu.loadData +import ani.dantotsu.logger import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes +import ani.dantotsu.tryWith import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/app/src/main/java/ani/dantotsu/subcriptions/Notifications.kt b/app/src/main/java/ani/dantotsu/subcriptions/Notifications.kt index 981ad580..0599a5be 100644 --- a/app/src/main/java/ani/dantotsu/subcriptions/Notifications.kt +++ b/app/src/main/java/ani/dantotsu/subcriptions/Notifications.kt @@ -58,15 +58,28 @@ class Notifications { ) } - fun createChannel(context: Context, group: Group?, id: String, name: String, silent: Boolean = false) { + fun createChannel( + context: Context, + group: Group?, + id: String, + name: String, + silent: Boolean = false + ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val importance = if (!silent) NotificationManager.IMPORTANCE_HIGH else NotificationManager.IMPORTANCE_LOW + val importance = + if (!silent) NotificationManager.IMPORTANCE_HIGH else NotificationManager.IMPORTANCE_LOW val mChannel = NotificationChannel(id, name, importance) - val notificationManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val notificationManager = + context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager if (group != null) { - notificationManager.createNotificationChannelGroup(NotificationChannelGroup(group.name, group.title)) + notificationManager.createNotificationChannelGroup( + NotificationChannelGroup( + group.name, + group.title + ) + ) mChannel.group = group.name } @@ -76,7 +89,8 @@ class Notifications { fun deleteChannel(context: Context, id: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val notificationManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val notificationManager = + context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager notificationManager.deleteNotificationChannel(id) } } @@ -117,20 +131,21 @@ class Notifications { .submit() .get() } + @Suppress("BlockingMethodInNonBlockingContext") val largeBitmap = if (largeImg != null) Glide.with(context) - .asBitmap() - .load(GlideUrl(largeImg.url) { largeImg.headers }) - .submit() - .get() + .asBitmap() + .load(GlideUrl(largeImg.url) { largeImg.headers }) + .submit() + .get() else null - if(largeBitmap!=null) builder.setStyle( - NotificationCompat - .BigPictureStyle() - .bigPicture(largeBitmap) - .bigLargeIcon(bitmap) - ) + if (largeBitmap != null) builder.setStyle( + NotificationCompat + .BigPictureStyle() + .bigPicture(largeBitmap) + .bigLargeIcon(bitmap) + ) builder.setLargeIcon(bitmap) } else builder diff --git a/app/src/main/java/ani/dantotsu/subcriptions/Subscription.kt b/app/src/main/java/ani/dantotsu/subcriptions/Subscription.kt index 45925ba4..c28b5a49 100644 --- a/app/src/main/java/ani/dantotsu/subcriptions/Subscription.kt +++ b/app/src/main/java/ani/dantotsu/subcriptions/Subscription.kt @@ -40,7 +40,8 @@ class Subscription { val index = subscriptions.map { i++; it.key to i }.toMap() val notificationManager = NotificationManagerCompat.from(context) - val progressEnabled = loadData("subscription_checking_notifications", context) ?: true + val progressEnabled = + loadData("subscription_checking_notifications", context) ?: true val progressNotification = if (progressEnabled) getProgressNotification( context, subscriptions.size @@ -69,23 +70,26 @@ class Subscription { subscriptions.toList().map { val media = it.second val text = if (media.isAnime) { - val parser = SubscriptionHelper.getAnimeParser(context, media.isAdult, media.id) + val parser = + SubscriptionHelper.getAnimeParser(context, media.isAdult, media.id) progress(index[it.first]!!, parser.name, media.name) - val ep: Episode? = SubscriptionHelper.getEpisode(context, parser, media.id, media.isAdult) - if (ep != null) currActivity()!!.getString(R.string.episode)+"${ep.number}${ + val ep: Episode? = + SubscriptionHelper.getEpisode(context, parser, media.id, media.isAdult) + if (ep != null) currActivity()!!.getString(R.string.episode) + "${ep.number}${ if (ep.title != null) " : ${ep.title}" else "" }${ if (ep.isFiller) " [Filler]" else "" - } "+ currActivity()!!.getString(R.string.just_released) to ep.thumbnail + } " + currActivity()!!.getString(R.string.just_released) to ep.thumbnail else null } else { - val parser = SubscriptionHelper.getMangaParser(context, media.isAdult, media.id) + val parser = + SubscriptionHelper.getMangaParser(context, media.isAdult, media.id) progress(index[it.first]!!, parser.name, media.name) val ep: MangaChapter? = SubscriptionHelper.getChapter(context, parser, media.id, media.isAdult) - if (ep != null) currActivity()!!.getString(R.string.chapter)+"${ep.number}${ + if (ep != null) currActivity()!!.getString(R.string.chapter) + "${ep.number}${ if (ep.title != null) " : ${ep.title}" else "" - } "+ currActivity()!!.getString(R.string.just_released) to null + } " + currActivity()!!.getString(R.string.just_released) to null else null } ?: return@map createNotification(context.applicationContext, media, text.first, text.second) @@ -96,7 +100,8 @@ class Subscription { } } - fun getChannelId(isAnime: Boolean, mediaId: Int) = "${if (isAnime) "anime" else "manga"}_${mediaId}" + fun getChannelId(isAnime: Boolean, mediaId: Int) = + "${if (isAnime) "anime" else "manga"}_${mediaId}" private suspend fun createNotification( context: Context, @@ -124,7 +129,10 @@ class Subscription { private const val progressNotificationId = 100 - private fun getProgressNotification(context: Context, size: Int): NotificationCompat.Builder { + private fun getProgressNotification( + context: Context, + size: Int + ): NotificationCompat.Builder { return Notifications.getNotification( context, null, diff --git a/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt b/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt index 5f2636b6..14ee72b0 100644 --- a/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt +++ b/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionHelper.kt @@ -1,26 +1,41 @@ package ani.dantotsu.subcriptions import android.content.Context +import ani.dantotsu.R import ani.dantotsu.currContext import ani.dantotsu.loadData import ani.dantotsu.media.Media import ani.dantotsu.media.Selected -import ani.dantotsu.parsers.* +import ani.dantotsu.media.manga.MangaNameAdapter +import ani.dantotsu.parsers.AnimeParser +import ani.dantotsu.parsers.AnimeSources +import ani.dantotsu.parsers.Episode +import ani.dantotsu.parsers.HAnimeSources +import ani.dantotsu.parsers.HMangaSources +import ani.dantotsu.parsers.MangaChapter +import ani.dantotsu.parsers.MangaParser +import ani.dantotsu.parsers.MangaSources import ani.dantotsu.saveData import ani.dantotsu.tryWithSuspend -import ani.dantotsu.R -import ani.dantotsu.media.manga.MangaNameAdapter import kotlinx.coroutines.withTimeoutOrNull class SubscriptionHelper { companion object { - private fun loadSelected(context: Context, mediaId: Int, isAdult: Boolean, isAnime: Boolean): Selected { + private fun loadSelected( + context: Context, + mediaId: Int, + isAdult: Boolean, + isAnime: Boolean + ): Selected { val sharedPreferences = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) val data = loadData("${mediaId}-select", context) ?: Selected().let { it.sourceIndex = if (isAdult) 0 - else if (isAnime) {sharedPreferences.getInt("settings_def_anime_source_s_r",0)} - else {sharedPreferences.getInt("settings_def_manga_source_s_r",0)} + else if (isAnime) { + sharedPreferences.getInt("settings_def_anime_source_s_r", 0) + } else { + sharedPreferences.getInt("settings_def_manga_source_s_r", 0) + } it.preferDub = loadData("settings_prefer_dub", context) ?: false it } @@ -39,15 +54,27 @@ class SubscriptionHelper { return parser } - suspend fun getEpisode(context: Context, parser: AnimeParser, id: Int, isAdult: Boolean): Episode? { + suspend fun getEpisode( + context: Context, + parser: AnimeParser, + id: Int, + isAdult: Boolean + ): Episode? { val selected = loadSelected(context, id, isAdult, true) val ep = withTimeoutOrNull(10 * 1000) { tryWithSuspend { - val show = parser.loadSavedShowResponse(id) ?: throw Exception(currContext()?.getString(R.string.failed_to_load_data, id)) + val show = parser.loadSavedShowResponse(id) ?: throw Exception( + currContext()?.getString( + R.string.failed_to_load_data, + id + ) + ) show.sAnime?.let { - parser.getLatestEpisode(show.link, show.extra, - it, selected.latest) + parser.getLatestEpisode( + show.link, show.extra, + it, selected.latest + ) } } } @@ -64,14 +91,26 @@ class SubscriptionHelper { return sources[selected.sourceIndex] } - suspend fun getChapter(context: Context, parser: MangaParser, id: Int, isAdult: Boolean): MangaChapter? { + suspend fun getChapter( + context: Context, + parser: MangaParser, + id: Int, + isAdult: Boolean + ): MangaChapter? { val selected = loadSelected(context, id, isAdult, true) val chp = withTimeoutOrNull(10 * 1000) { tryWithSuspend { - val show = parser.loadSavedShowResponse(id) ?: throw Exception(currContext()?.getString(R.string.failed_to_load_data, id)) + val show = parser.loadSavedShowResponse(id) ?: throw Exception( + currContext()?.getString( + R.string.failed_to_load_data, + id + ) + ) show.sManga?.let { - parser.getLatestChapter(show.link, show.extra, - it, selected.latest) + parser.getLatestChapter( + show.link, show.extra, + it, selected.latest + ) } } } @@ -91,8 +130,9 @@ class SubscriptionHelper { ) : java.io.Serializable private const val subscriptions = "subscriptions" - fun getSubscriptions(context: Context): Map = loadData(subscriptions, context) - ?: mapOf().also { saveData(subscriptions, it, context) } + fun getSubscriptions(context: Context): Map = + loadData(subscriptions, context) + ?: mapOf().also { saveData(subscriptions, it, context) } fun saveSubscription(context: Context, media: Media, subscribed: Boolean) { val data = loadData>(subscriptions, context)!!.toMutableMap() diff --git a/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionWorker.kt b/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionWorker.kt index 8481ed88..bed58b21 100644 --- a/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionWorker.kt +++ b/app/src/main/java/ani/dantotsu/subcriptions/SubscriptionWorker.kt @@ -1,18 +1,25 @@ package ani.dantotsu.subcriptions import android.content.Context -import androidx.work.* +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters import ani.dantotsu.loadData import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime import ani.dantotsu.subcriptions.Subscription.Companion.timeMinutes import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.util.concurrent.* +import java.util.concurrent.TimeUnit -class SubscriptionWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { +class SubscriptionWorker(val context: Context, params: WorkerParameters) : + CoroutineWorker(context, params) { override suspend fun doWork(): Result { - withContext(Dispatchers.IO){ + withContext(Dispatchers.IO) { Subscription.perform(context) } return Result.success() @@ -23,8 +30,9 @@ class SubscriptionWorker(val context: Context, params: WorkerParameters) : Corou private const val SUBSCRIPTION_WORK_NAME = "work_subscription" fun enqueue(context: Context) { val curTime = loadData("subscriptions_time_s") ?: defaultTime - if(timeMinutes[curTime]>0L) { - val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() + if (timeMinutes[curTime] > 0L) { + val constraints = + Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() val periodicSyncDataWork = PeriodicWorkRequest.Builder( SubscriptionWorker::class.java, 6, TimeUnit.HOURS ).apply { @@ -32,7 +40,9 @@ class SubscriptionWorker(val context: Context, params: WorkerParameters) : Corou setConstraints(constraints) }.build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( - SUBSCRIPTION_WORK_NAME, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, periodicSyncDataWork + SUBSCRIPTION_WORK_NAME, + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + periodicSyncDataWork ) } } diff --git a/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt b/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt index 8b4a5e61..2029177b 100644 --- a/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt +++ b/app/src/main/java/ani/dantotsu/themes/ThemeManager.kt @@ -1,16 +1,45 @@ package ani.dantotsu.themes +import android.app.Activity import android.content.Context import android.content.res.Configuration +import android.graphics.Bitmap import ani.dantotsu.R +import com.google.android.material.color.DynamicColors +import com.google.android.material.color.DynamicColorsOptions + class ThemeManager(private val context: Context) { - fun applyTheme() { - val useOLED = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_oled", false) && isDarkThemeActive(context) - if(context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("use_material_you", false)){ - return + fun applyTheme(fromImage: Bitmap? = null) { + val useOLED = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + .getBoolean("use_oled", false) && isDarkThemeActive(context) + val useCustomTheme = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + .getBoolean("use_custom_theme", false) + val customTheme = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + .getInt("custom_theme_int", 16712221) + val useSource = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + .getBoolean("use_source_theme", false) + val useMaterial = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + .getBoolean("use_material_you", false) + if (useSource) { + val returnedEarly = applyDynamicColors( + useMaterial, + context, + useOLED, + fromImage, + useCustom = if (useCustomTheme) customTheme else null + ) + if (!returnedEarly) return + } else if (useCustomTheme) { + val returnedEarly = + applyDynamicColors(useMaterial, context, useOLED, useCustom = customTheme) + if (!returnedEarly) return + } else { + val returnedEarly = applyDynamicColors(useMaterial, context, useOLED, useCustom = null) + if (!returnedEarly) return } - val theme = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getString("theme", "PURPLE")!! + val theme = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) + .getString("theme", "PURPLE")!! val themeToApply = when (theme) { "PURPLE" -> if (useOLED) R.style.Theme_Dantotsu_PurpleOLED else R.style.Theme_Dantotsu_Purple @@ -27,6 +56,47 @@ class ThemeManager(private val context: Context) { context.setTheme(themeToApply) } + private fun applyDynamicColors( + useMaterialYou: Boolean, + context: Context, + useOLED: Boolean, + bitmap: Bitmap? = null, + useCustom: Int? = null + ): Boolean { + val builder = DynamicColorsOptions.Builder() + var needMaterial = true + + // Set content-based source if a bitmap is provided + if (bitmap != null) { + builder.setContentBasedSource(bitmap) + needMaterial = false + } else if (useCustom != null) { + builder.setContentBasedSource(useCustom) + needMaterial = false + } + + if (useOLED) { + builder.setThemeOverlay(R.style.AppTheme_Amoled) + } + if (needMaterial && !useMaterialYou) return true + + // Build the options + val options = builder.build() + + // Apply the dynamic colors to the activity + val activity = context as Activity + DynamicColors.applyToActivityIfAvailable(activity, options) + + if (useOLED) { + val options2 = DynamicColorsOptions.Builder() + .setThemeOverlay(R.style.AppTheme_Amoled) + .build() + DynamicColors.applyToActivityIfAvailable(activity, options2) + } + + return false + } + private fun isDarkThemeActive(context: Context): Boolean { return when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES -> true @@ -37,7 +107,7 @@ class ThemeManager(private val context: Context) { } - companion object{ + companion object { enum class Theme(val theme: String) { PURPLE("PURPLE"), BLUE("BLUE"), 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 68bcfd88..277afa71 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.snackString import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi @@ -116,7 +117,7 @@ class AnimeExtensionManager( api.findExtensions() } catch (e: Exception) { logcat(LogPriority.ERROR, e) - withUIContext { context.toast("Failed to get extensions list") } + withUIContext { snackString("Failed to get extensions list") } emptyList() } 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 549ba550..260af47c 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.snackString import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi @@ -116,7 +117,7 @@ class MangaExtensionManager( api.findExtensions() } catch (e: Exception) { logcat(LogPriority.ERROR, e) - withUIContext { context.toast("Failed to get manga extensions") } + withUIContext { snackString("Failed to get manga extensions") } emptyList() } 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 b0efde9f..fa56778b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -1,11 +1,10 @@ package eu.kanade.tachiyomi.network import android.content.Context -import eu.kanade.tachiyomi.network.AndroidCookieJar -import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE -import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE -import eu.kanade.tachiyomi.network.dohCloudflare -import eu.kanade.tachiyomi.network.dohGoogle +import android.os.Build +import ani.dantotsu.Mapper +import ani.dantotsu.defaultHeaders +import com.lagradost.nicehttp.Requests import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor @@ -32,13 +31,13 @@ class NetworkHelper( CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider) } - private val baseClientBuilder: OkHttpClient.Builder - get() { + private fun baseClientBuilder(callTimout: Int = 2): OkHttpClient.Builder + { val builder = OkHttpClient.Builder() .cookieJar(cookieJar) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) - .callTimeout(2, TimeUnit.MINUTES) + .callTimeout(callTimout.toLong(), TimeUnit.MINUTES) .addInterceptor(UncaughtExceptionInterceptor()) .addInterceptor(userAgentInterceptor) @@ -68,7 +67,10 @@ class NetworkHelper( return builder } - val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() } + + + val client by lazy { baseClientBuilder().cache(Cache(cacheDir, cacheSize)).build() } + val downloadClient by lazy { baseClientBuilder(20).build() } @Suppress("UNUSED") val cloudflareClient by lazy { @@ -77,5 +79,17 @@ class NetworkHelper( .build() } + val requestClient = Requests( + client, + mapOf( + "User-Agent" to + defaultUserAgentProvider() + .format(Build.VERSION.RELEASE, Build.MODEL) + ), + defaultCacheTime = 6, + defaultCacheTimeUnit = TimeUnit.HOURS, + responseParser = Mapper + ) + fun defaultUserAgentProvider() = preferences.defaultUserAgent().get().trim() } diff --git a/app/src/main/res/color/chip_text_color.xml b/app/src/main/res/color/chip_text_color.xml index 7cc1a4af..c9f34f3a 100644 --- a/app/src/main/res/color/chip_text_color.xml +++ b/app/src/main/res/color/chip_text_color.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/ic_palette.xml b/app/src/main/res/drawable/ic_palette.xml new file mode 100644 index 00000000..581c3ba9 --- /dev/null +++ b/app/src/main/res/drawable/ic_palette.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_round_color_picker_24.xml b/app/src/main/res/drawable/ic_round_color_picker_24.xml new file mode 100644 index 00000000..b18b2010 --- /dev/null +++ b/app/src/main/res/drawable/ic_round_color_picker_24.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_extensions.xml b/app/src/main/res/layout/activity_extensions.xml index 30e5de8d..19d12fb8 100644 --- a/app/src/main/res/layout/activity_extensions.xml +++ b/app/src/main/res/layout/activity_extensions.xml @@ -9,7 +9,7 @@ + android:text="@string/installed_anime"/> + android:text="@string/available_anime"/> + android:text="@string/installed_manga"/> + android:layout_weight="1" /> + android:visibility="gone" + android:layout_gravity="bottom" + android:layout_weight="1"> - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_image_search.xml b/app/src/main/res/layout/activity_image_search.xml index 10bb08a6..775ebf93 100644 --- a/app/src/main/res/layout/activity_image_search.xml +++ b/app/src/main/res/layout/activity_image_search.xml @@ -2,7 +2,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="fill_parent"> + android:layout_height="fill_parent" + android:fitsSystemWindows="true"> - + + + + + + + + + + + + +