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