560 lines
23 KiB
Kotlin
560 lines
23 KiB
Kotlin
package ani.dantotsu
|
|
|
|
import android.animation.ObjectAnimator
|
|
import android.annotation.SuppressLint
|
|
import android.content.Intent
|
|
import android.content.res.Configuration
|
|
import android.graphics.drawable.Animatable
|
|
import android.graphics.drawable.GradientDrawable
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import android.provider.Settings
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.view.animation.AnticipateInterpolator
|
|
import android.widget.TextView
|
|
import androidx.activity.addCallback
|
|
import androidx.activity.viewModels
|
|
import androidx.annotation.OptIn
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.core.animation.doOnEnd
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.core.view.doOnAttach
|
|
import androidx.core.view.updateLayoutParams
|
|
import androidx.core.view.updateMargins
|
|
import androidx.documentfile.provider.DocumentFile
|
|
import androidx.fragment.app.Fragment
|
|
import androidx.fragment.app.FragmentManager
|
|
import androidx.lifecycle.Lifecycle
|
|
import androidx.lifecycle.lifecycleScope
|
|
import androidx.media3.common.util.UnstableApi
|
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
|
import ani.dantotsu.addons.torrent.TorrentServerService
|
|
import ani.dantotsu.connections.anilist.Anilist
|
|
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
|
import ani.dantotsu.databinding.ActivityMainBinding
|
|
import ani.dantotsu.databinding.DialogUserAgentBinding
|
|
import ani.dantotsu.databinding.SplashScreenBinding
|
|
import ani.dantotsu.home.AnimeFragment
|
|
import ani.dantotsu.home.HomeFragment
|
|
import ani.dantotsu.home.LoginFragment
|
|
import ani.dantotsu.home.MangaFragment
|
|
import ani.dantotsu.home.NoInternet
|
|
import ani.dantotsu.media.MediaDetailsActivity
|
|
import ani.dantotsu.notifications.TaskScheduler
|
|
import ani.dantotsu.others.CustomBottomDialog
|
|
import ani.dantotsu.others.calc.CalcActivity
|
|
import ani.dantotsu.profile.ProfileActivity
|
|
import ani.dantotsu.profile.activity.FeedActivity
|
|
import ani.dantotsu.profile.notification.NotificationActivity
|
|
import ani.dantotsu.settings.AddRepositoryBottomSheet
|
|
import ani.dantotsu.settings.ExtensionsActivity
|
|
import ani.dantotsu.settings.saving.PrefManager
|
|
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
|
|
import ani.dantotsu.settings.saving.PrefName
|
|
import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
|
|
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
|
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
|
import ani.dantotsu.themes.ThemeManager
|
|
import ani.dantotsu.util.AudioHelper
|
|
import ani.dantotsu.util.Logger
|
|
import ani.dantotsu.util.customAlertDialog
|
|
import com.google.android.material.snackbar.BaseTransientBottomBar
|
|
import com.google.android.material.snackbar.Snackbar
|
|
import eu.kanade.domain.source.service.SourcePreferences
|
|
import io.noties.markwon.Markwon
|
|
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
import nl.joery.animatedbottombar.AnimatedBottomBar
|
|
import tachiyomi.core.util.lang.launchIO
|
|
import uy.kohesive.injekt.Injekt
|
|
import uy.kohesive.injekt.api.get
|
|
import java.io.Serializable
|
|
|
|
|
|
class MainActivity : AppCompatActivity() {
|
|
private lateinit var binding: ActivityMainBinding
|
|
private lateinit var incognitoLiveData: SharedPreferenceBooleanLiveData
|
|
private val scope = lifecycleScope
|
|
private var load = false
|
|
|
|
|
|
@kotlin.OptIn(DelicateCoroutinesApi::class)
|
|
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
|
@OptIn(UnstableApi::class)
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
ThemeManager(this).applyTheme()
|
|
|
|
super.onCreate(savedInstanceState)
|
|
|
|
//get FRAGMENT_CLASS_NAME from intent
|
|
val fragment = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
|
|
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
setContentView(binding.root)
|
|
|
|
TaskScheduler.scheduleSingleWork(this)
|
|
if (!CalcActivity.hasPermission) {
|
|
val pin: String = PrefManager.getVal(PrefName.AppPassword)
|
|
if (pin.isNotEmpty()) {
|
|
ContextCompat.startActivity(
|
|
this@MainActivity,
|
|
Intent(this@MainActivity, CalcActivity::class.java)
|
|
.putExtra("code", pin)
|
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK),
|
|
null
|
|
)
|
|
finish()
|
|
return
|
|
}
|
|
}
|
|
|
|
if (Intent.ACTION_VIEW == intent.action) {
|
|
handleViewIntent(intent)
|
|
}
|
|
|
|
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
|
|
val backgroundDrawable = bottomNavBar.background as GradientDrawable
|
|
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
|
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
|
backgroundDrawable.setColor(semiTransparentColor)
|
|
bottomNavBar.background = backgroundDrawable
|
|
}
|
|
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
|
|
|
val offset = try {
|
|
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
|
resources.getDimensionPixelSize(statusBarHeightId)
|
|
} catch (e: Exception) {
|
|
statusBarHeight
|
|
}
|
|
val layoutParams = binding.incognito.layoutParams as ViewGroup.MarginLayoutParams
|
|
layoutParams.topMargin = 11 * offset / 12
|
|
binding.incognito.layoutParams = layoutParams
|
|
incognitoLiveData = PrefManager.getLiveVal(
|
|
PrefName.Incognito,
|
|
false
|
|
).asLiveBool()
|
|
incognitoLiveData.observe(this) {
|
|
if (it) {
|
|
val slideDownAnim = ObjectAnimator.ofFloat(
|
|
binding.incognito,
|
|
View.TRANSLATION_Y,
|
|
-(binding.incognito.height.toFloat() + statusBarHeight),
|
|
0f
|
|
)
|
|
slideDownAnim.duration = 200
|
|
slideDownAnim.start()
|
|
binding.incognito.visibility = View.VISIBLE
|
|
} else {
|
|
val slideUpAnim = ObjectAnimator.ofFloat(
|
|
binding.incognito,
|
|
View.TRANSLATION_Y,
|
|
0f,
|
|
-(binding.incognito.height.toFloat() + statusBarHeight)
|
|
)
|
|
slideUpAnim.duration = 200
|
|
slideUpAnim.start()
|
|
//wait for animation to finish
|
|
Handler(Looper.getMainLooper()).postDelayed(
|
|
{ binding.incognito.visibility = View.GONE },
|
|
200
|
|
)
|
|
}
|
|
}
|
|
incognitoNotification(this)
|
|
|
|
var doubleBackToExitPressedOnce = false
|
|
onBackPressedDispatcher.addCallback(this) {
|
|
if (doubleBackToExitPressedOnce) {
|
|
finish()
|
|
}
|
|
doubleBackToExitPressedOnce = true
|
|
snackString(this@MainActivity.getString(R.string.back_to_exit)).apply {
|
|
this?.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
|
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
|
super.onDismissed(transientBottomBar, event)
|
|
doubleBackToExitPressedOnce = false
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
binding.root.isMotionEventSplittingEnabled = false
|
|
|
|
lifecycleScope.launch {
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
val splash = SplashScreenBinding.inflate(layoutInflater)
|
|
binding.root.addView(splash.root)
|
|
(splash.splashImage.drawable as Animatable).start()
|
|
|
|
delay(1200)
|
|
|
|
ObjectAnimator.ofFloat(
|
|
splash.root,
|
|
View.TRANSLATION_Y,
|
|
0f,
|
|
-splash.root.height.toFloat()
|
|
).apply {
|
|
interpolator = AnticipateInterpolator()
|
|
duration = 200L
|
|
doOnEnd { binding.root.removeView(splash.root) }
|
|
start()
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
splashScreen.setOnExitAnimationListener { splashScreenView ->
|
|
ObjectAnimator.ofFloat(
|
|
splashScreenView,
|
|
View.TRANSLATION_Y,
|
|
0f,
|
|
-splashScreenView.height.toFloat()
|
|
).apply {
|
|
interpolator = AnticipateInterpolator()
|
|
duration = 200L
|
|
doOnEnd { splashScreenView.remove() }
|
|
start()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
binding.root.doOnAttach {
|
|
initActivity(this)
|
|
val preferences: SourcePreferences = Injekt.get()
|
|
if (preferences.animeExtensionUpdatesCount()
|
|
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
|
|
) {
|
|
snackString(R.string.extension_updates_available)
|
|
?.setDuration(Snackbar.LENGTH_SHORT)
|
|
?.setAction(R.string.review) {
|
|
startActivity(Intent(this, ExtensionsActivity::class.java))
|
|
}
|
|
}
|
|
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
|
selectedOption = if (fragment != null) {
|
|
when (fragment) {
|
|
AnimeFragment::class.java.name -> 0
|
|
HomeFragment::class.java.name -> 1
|
|
MangaFragment::class.java.name -> 2
|
|
else -> 1
|
|
}
|
|
} else {
|
|
PrefManager.getVal(PrefName.DefaultStartUpTab)
|
|
}
|
|
val navbar = binding.includedNavbar.navbar
|
|
bottomBar = navbar
|
|
navbar.visibility = View.VISIBLE
|
|
binding.mainProgressBar.visibility = View.GONE
|
|
val mainViewPager = binding.viewpager
|
|
mainViewPager.isUserInputEnabled = false
|
|
mainViewPager.adapter =
|
|
ViewPagerAdapter(supportFragmentManager, lifecycle)
|
|
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
|
|
navbar.selectTabAt(selectedOption)
|
|
navbar.setOnTabSelectListener(object :
|
|
AnimatedBottomBar.OnTabSelectListener {
|
|
override fun onTabSelected(
|
|
lastIndex: Int,
|
|
lastTab: AnimatedBottomBar.Tab?,
|
|
newIndex: Int,
|
|
newTab: AnimatedBottomBar.Tab
|
|
) {
|
|
navbar.animate().translationZ(12f).setDuration(200).start()
|
|
selectedOption = newIndex
|
|
mainViewPager.setCurrentItem(newIndex, false)
|
|
}
|
|
})
|
|
if (mainViewPager.currentItem != selectedOption) {
|
|
mainViewPager.post {
|
|
mainViewPager.setCurrentItem(
|
|
selectedOption,
|
|
false
|
|
)
|
|
}
|
|
}
|
|
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
bottomMargin = navBarHeight
|
|
}
|
|
}
|
|
|
|
var launched = false
|
|
intent.extras?.let { extras ->
|
|
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
|
|
val mediaId = extras.getInt("mediaId", -1)
|
|
val commentId = extras.getInt("commentId", -1)
|
|
val activityId = extras.getInt("activityId", -1)
|
|
|
|
if (fragmentToLoad != null && mediaId != -1 && commentId != -1) {
|
|
val detailIntent = Intent(this, MediaDetailsActivity::class.java).apply {
|
|
putExtra("FRAGMENT_TO_LOAD", fragmentToLoad)
|
|
putExtra("mediaId", mediaId)
|
|
putExtra("commentId", commentId)
|
|
}
|
|
launched = true
|
|
startActivity(detailIntent)
|
|
} else if (fragmentToLoad == "FEED" && activityId != -1) {
|
|
val feedIntent = Intent(this, FeedActivity::class.java).apply {
|
|
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
|
|
putExtra("activityId", activityId)
|
|
|
|
}
|
|
launched = true
|
|
startActivity(feedIntent)
|
|
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
|
|
Logger.log("MainActivity, onCreate: $activityId")
|
|
val notificationIntent = Intent(this, NotificationActivity::class.java).apply {
|
|
putExtra("activityId", activityId)
|
|
}
|
|
launched = true
|
|
startActivity(notificationIntent)
|
|
}
|
|
}
|
|
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
|
if (!isOnline(this)) {
|
|
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
|
startActivity(Intent(this, NoInternet::class.java))
|
|
} else {
|
|
if (offlineMode) {
|
|
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
|
startActivity(Intent(this, NoInternet::class.java))
|
|
} else {
|
|
val model: AnilistHomeViewModel by viewModels()
|
|
|
|
//Load Data
|
|
if (!load && !launched) {
|
|
scope.launch(Dispatchers.IO) {
|
|
model.loadMain(this@MainActivity)
|
|
val id = intent.extras?.getInt("mediaId", 0)
|
|
val isMAL = intent.extras?.getBoolean("mal") ?: false
|
|
val cont = intent.extras?.getBoolean("continue") ?: false
|
|
if (id != null && id != 0) {
|
|
val media = withContext(Dispatchers.IO) {
|
|
Anilist.query.getMedia(id, isMAL)
|
|
}
|
|
if (media != null) {
|
|
media.cameFromContinue = cont
|
|
startActivity(
|
|
Intent(this@MainActivity, MediaDetailsActivity::class.java)
|
|
.putExtra("media", media as Serializable)
|
|
)
|
|
} else {
|
|
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
|
}
|
|
}
|
|
val username = intent.extras?.getString("username")
|
|
if (username != null) {
|
|
val nameInt = username.toIntOrNull()
|
|
if (nameInt != null) {
|
|
startActivity(
|
|
Intent(this@MainActivity, ProfileActivity::class.java)
|
|
.putExtra("userId", nameInt)
|
|
)
|
|
} else {
|
|
startActivity(
|
|
Intent(this@MainActivity, ProfileActivity::class.java)
|
|
.putExtra("username", username)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
load = true
|
|
}
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
if (!(PrefManager.getVal(PrefName.AllowOpeningLinks) as Boolean)) {
|
|
CustomBottomDialog.newInstance().apply {
|
|
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
|
|
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
|
|
addView(TextView(this@MainActivity).apply {
|
|
val markWon =
|
|
Markwon.builder(this@MainActivity)
|
|
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
|
markWon.setMarkdown(this, md)
|
|
})
|
|
|
|
setNegativeButton(this@MainActivity.getString(R.string.no)) {
|
|
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
|
|
dismiss()
|
|
}
|
|
|
|
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
|
|
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
|
|
tryWith(true) {
|
|
startActivity(
|
|
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
|
.setData(Uri.parse("package:$packageName"))
|
|
)
|
|
}
|
|
dismiss()
|
|
}
|
|
}.show(supportFragmentManager, "dialog")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (PrefManager.getVal(PrefName.OC)) {
|
|
AudioHelper.run(this, R.raw.audio)
|
|
PrefManager.setVal(PrefName.OC, false)
|
|
}
|
|
val torrentManager = Injekt.get<TorrentAddonManager>()
|
|
fun startTorrent() {
|
|
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
|
|
launchIO {
|
|
if (!TorrentServerService.isRunning()) {
|
|
TorrentServerService.start()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (torrentManager.isInitialized.value == false) {
|
|
torrentManager.isInitialized.observe(this) {
|
|
if (it) {
|
|
startTorrent()
|
|
}
|
|
}
|
|
} else {
|
|
startTorrent()
|
|
}
|
|
}
|
|
|
|
override fun onRestart() {
|
|
super.onRestart()
|
|
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
|
}
|
|
|
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
super.onConfigurationChanged(newConfig)
|
|
val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
|
|
val params: ViewGroup.MarginLayoutParams =
|
|
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
|
|
params.updateMargins(bottom = margin.toPx)
|
|
}
|
|
|
|
private fun handleViewIntent(intent: Intent) {
|
|
val uri: Uri? = intent.data
|
|
try {
|
|
if (uri == null) {
|
|
throw Exception("Uri is null")
|
|
}
|
|
if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi") && uri.host == "add-repo") {
|
|
val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import")
|
|
val prefName = if (uri.scheme == "tachiyomi") {
|
|
PrefName.MangaExtensionRepos
|
|
} else {
|
|
PrefName.AnimeExtensionRepos
|
|
}
|
|
val savedRepos: Set<String> = PrefManager.getVal(prefName)
|
|
val newRepos = savedRepos.toMutableSet()
|
|
AddRepositoryBottomSheet.addRepoWarning(this) {
|
|
newRepos.add(url)
|
|
PrefManager.setVal(prefName, newRepos)
|
|
toast("${if (uri.scheme == "tachiyomi") "Manga" else "Anime"} Extension Repo added")
|
|
}
|
|
return
|
|
}
|
|
if (intent.type == null) return
|
|
val jsonString =
|
|
contentResolver.openInputStream(uri)?.readBytes()
|
|
?: throw Exception("Error reading file")
|
|
val name =
|
|
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
|
|
//.sani is encrypted, .ani is not
|
|
if (name.endsWith(".sani")) {
|
|
passwordAlertDialog { password ->
|
|
if (password != null) {
|
|
val salt = jsonString.copyOfRange(0, 16)
|
|
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
|
val decryptedJson = try {
|
|
PreferenceKeystore.decryptWithPassword(
|
|
password,
|
|
encrypted,
|
|
salt
|
|
)
|
|
} catch (e: Exception) {
|
|
toast("Incorrect password")
|
|
return@passwordAlertDialog
|
|
}
|
|
if (PreferencePackager.unpack(decryptedJson)) {
|
|
val intent = Intent(this, this.javaClass)
|
|
this.finish()
|
|
startActivity(intent)
|
|
}
|
|
} else {
|
|
toast("Password cannot be empty")
|
|
}
|
|
}
|
|
} else if (name.endsWith(".ani")) {
|
|
val decryptedJson = jsonString.toString(Charsets.UTF_8)
|
|
if (PreferencePackager.unpack(decryptedJson)) {
|
|
val intent = Intent(this, this.javaClass)
|
|
this.finish()
|
|
startActivity(intent)
|
|
}
|
|
} else {
|
|
toast("Invalid file type")
|
|
}
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
toast("Error importing settings")
|
|
}
|
|
}
|
|
|
|
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
|
|
val password = CharArray(16).apply { fill('0') }
|
|
|
|
// Inflate the dialog layout
|
|
val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
|
|
userAgentTextBox.hint = "Password"
|
|
subtitle.visibility = View.VISIBLE
|
|
subtitle.text = getString(R.string.enter_password_to_decrypt_file)
|
|
}
|
|
customAlertDialog().apply {
|
|
setTitle("Enter Password")
|
|
setCustomView(dialogView.root)
|
|
setPosButton(R.string.yes) {
|
|
val editText = dialogView.userAgentTextBox
|
|
if (editText.text?.isNotBlank() == true) {
|
|
editText.text?.toString()?.trim()?.toCharArray(password)
|
|
callback(password)
|
|
} else {
|
|
toast("Password cannot be empty")
|
|
}
|
|
}
|
|
setNegButton(R.string.cancel) {
|
|
password.fill('0')
|
|
callback(null)
|
|
}
|
|
show()
|
|
}
|
|
}
|
|
|
|
//ViewPager
|
|
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
|
FragmentStateAdapter(fragmentManager, lifecycle) {
|
|
|
|
override fun getItemCount(): Int = 3
|
|
|
|
override fun createFragment(position: Int): Fragment {
|
|
when (position) {
|
|
0 -> return AnimeFragment()
|
|
1 -> return if (Anilist.token != null) HomeFragment() else LoginFragment()
|
|
2 -> return MangaFragment()
|
|
}
|
|
return LoginFragment()
|
|
}
|
|
}
|
|
|
|
}
|