Merge pull request #69 from rebelonion/dev

Dev
This commit is contained in:
rebelonion 2023-12-04 22:19:26 -06:00 committed by GitHub
commit 7f92ac686d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
201 changed files with 8057 additions and 2750 deletions

View file

@ -21,18 +21,18 @@ android {
minSdk 23 minSdk 23
targetSdk 34 targetSdk 34
versionCode ((System.currentTimeMillis() / 60000).toInteger()) versionCode ((System.currentTimeMillis() / 60000).toInteger())
versionName "1.0.0-beta03i" versionName "1.0.0-beta03i-2"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
buildTypes { buildTypes {
debug { debug {
applicationIdSuffix ".beta" 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 debuggable true
} }
release { release {
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher"] manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
debuggable false debuggable false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 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-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.8.9' 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 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.webkit:webkit:1.9.0'
// Glide // Glide
ext.glide_version = '4.16.0' ext.glide_version = '4.16.0'
@ -99,6 +100,7 @@ dependencies {
implementation 'com.alexvasilkov:gesture-views:2.8.3' implementation 'com.alexvasilkov:gesture-views:2.8.3'
implementation 'com.github.VipulOG:ebook-reader:0.1.6' implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation "com.github.skydoves:colorpickerview:2.3.0"
// string matching // string matching
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'

View file

@ -52,7 +52,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="${icon_placeholder_round}"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Dantotsu" android:theme="@style/Theme.Dantotsu"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
@ -276,6 +276,14 @@
<service android:name=".download.manga.MangaDownloaderService" <service android:name=".download.manga.MangaDownloaderService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service android:name=".download.novel.NovelDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service android:name=".connections.discord.DiscordService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application> </application>
</manifest> </manifest>

View file

@ -8,14 +8,15 @@ import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import ani.dantotsu.aniyomi.anime.custom.AppModule import ani.dantotsu.aniyomi.anime.custom.AppModule
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule 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.others.DisabledReports
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources 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.android.material.color.DynamicColors
import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase 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.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -25,14 +26,16 @@ import kotlinx.coroutines.launch
import logcat.AndroidLogcatLogger import logcat.AndroidLogcatLogger
import logcat.LogPriority import logcat.LogPriority
import logcat.LogcatLogger import logcat.LogcatLogger
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Locale
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
class App : MultiDexApplication() { class App : MultiDexApplication() {
private lateinit var animeExtensionManager: AnimeExtensionManager private lateinit var animeExtensionManager: AnimeExtensionManager
private lateinit var mangaExtensionManager: MangaExtensionManager private lateinit var mangaExtensionManager: MangaExtensionManager
private lateinit var novelExtensionManager: NovelExtensionManager
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base) super.attachBaseContext(base)
MultiDex.install(this) MultiDex.install(this)
@ -48,17 +51,19 @@ class App : MultiDexApplication() {
super.onCreate() super.onCreate()
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false) val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false)
if(useMaterialYou) { if (useMaterialYou) {
DynamicColors.applyToActivitiesIfAvailable(this) DynamicColors.applyToActivitiesIfAvailable(this)
//TODO: HarmonizedColors
} }
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks) registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports) Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
initializeNetwork(baseContext)
Injekt.importModule(AppModule(this)) Injekt.importModule(AppModule(this))
Injekt.importModule(PreferenceModule(this)) Injekt.importModule(PreferenceModule(this))
initializeNetwork(baseContext)
setupNotificationChannels() setupNotificationChannels()
if (!LogcatLogger.isInstalled) { if (!LogcatLogger.isInstalled) {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
@ -66,6 +71,7 @@ class App : MultiDexApplication() {
animeExtensionManager = Injekt.get() animeExtensionManager = Injekt.get()
mangaExtensionManager = Injekt.get() mangaExtensionManager = Injekt.get()
novelExtensionManager = Injekt.get()
val animeScope = CoroutineScope(Dispatchers.Default) val animeScope = CoroutineScope(Dispatchers.Default)
animeScope.launch { animeScope.launch {
@ -79,9 +85,16 @@ class App : MultiDexApplication() {
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}") logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow) 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() { private fun setupNotificationChannels() {
try { try {
Notifications.createChannels(this) Notifications.createChannels(this)
@ -109,7 +122,7 @@ class App : MultiDexApplication() {
companion object { companion object {
private var instance: App? = null private var instance: App? = null
var context : Context? = null var context: Context? = null
fun currentContext(): Context? { fun currentContext(): Context? {
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context
} }

View file

@ -132,9 +132,10 @@ fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = tr
fun initActivity(a: Activity) { fun initActivity(a: Activity) {
val window = a.window val window = a.window
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false) ?: UserInterfaceSettings().apply { val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false)
saveData("ui_settings", this) ?: UserInterfaceSettings().apply {
} saveData("ui_settings", this)
}
uiSettings.darkMode.apply { uiSettings.darkMode.apply {
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(
when (this) { when (this) {
@ -146,9 +147,10 @@ fun initActivity(a: Activity) {
} }
if (uiSettings.immersiveMode) { if (uiSettings.immersiveMode) {
if (navBarHeight == 0) { if (navBarHeight == 0) {
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))?.apply { ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom ?.apply {
} navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
}
} }
a.hideStatusBar() a.hideStatusBar()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { 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 } else
if (statusBarHeight == 0) { 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) { if (windowInsets != null) {
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
statusBarHeight = insets.top statusBarHeight = insets.top
@ -205,7 +208,8 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
} }
fun isOnline(context: Context): Boolean { 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 { return tryWith {
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return@tryWith if (cap != null) { return@tryWith if (cap != null) {
@ -219,7 +223,7 @@ fun isOnline(context: Context): Boolean {
cap.hasTransport(TRANSPORT_WIFI) || cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
else -> false else -> false
} }
} else false } else false
} ?: 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 { DatePickerDialog.OnDateSetListener {
var dialog: DatePickerDialog 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 { 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 { try {
val input = (dest.toString() + source.toString()).toDouble() val input = (dest.toString() + source.toString()).toDouble()
if (isInRange(min, max, input)) return null 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) { override fun transformPage(view: View, position: Float) {
if (position == 0.0f && uiSettings.layoutAnimations) { if (position == 0.0f && uiSettings.layoutAnimations) {
setAnimation(view.context, view, uiSettings, 300, floatArrayOf(1.3f, 1f, 1.3f, 1f), 0.5f to 0f) setAnimation(
ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f).setDuration((200 * uiSettings.animationSpeed).toLong()).start() 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) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 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 { override fun isPaddingOffsetRequired(): Boolean {
return !clipToPadding return !clipToPadding
@ -414,7 +443,7 @@ fun MutableList<ShowResponse>.sortByTitle(string: String) {
} }
fun String.findBetween(a: String, b: 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 } return string.ifEmpty { null }
} }
@ -423,8 +452,7 @@ fun ImageView.loadImage(url: String?, size: Int = 0) {
val localFile = File(url) val localFile = File(url)
if (localFile.exists()) { if (localFile.exists()) {
loadLocalImage(localFile, size) loadLocalImage(localFile, size)
} } else {
else {
loadImage(FileUrl(url), size) loadImage(FileUrl(url), size)
} }
} }
@ -434,7 +462,8 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
if (file?.url?.isNotEmpty() == true) { if (file?.url?.isNotEmpty() == true) {
tryWith { tryWith {
val glideUrl = GlideUrl(file.url) { file.headers } 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) { fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
if (file?.exists() == true) { if (file?.exists() == true) {
tryWith { 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) 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) onScrollYClick(distanceY)
onScrollXClick(distanceX) onScrollXClick(distanceX)
return super.onScroll(e1, e2, distanceX, distanceY) 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) val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0) view.addView(v.root, 0)
v.mediaCountdownText.text = 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) { override fun onTick(millisUntilFinished: Long) {
val a = millisUntilFinished / 1000 val a = millisUntilFinished / 1000
v.mediaCountdown.text = currActivity()?.getString( v.mediaCountdown.text = currActivity()?.getString(
@ -735,7 +776,8 @@ fun toast(string: String?) {
if (string != null) { if (string != null) {
logger(string) logger(string)
MainScope().launch { 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) { if (s != null) {
(activity ?: currActivity())?.apply { (activity ?: currActivity())?.apply {
runOnUiThread { 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 { snackBar.view.apply {
updateLayoutParams<FrameLayout.LayoutParams> { updateLayoutParams<FrameLayout.LayoutParams> {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
@ -769,7 +815,8 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
} }
} }
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) : ArrayAdapter<T>(context, layoutId, items) { open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
ArrayAdapter<T>(context, layoutId, items) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent) val view = super.getView(position, convertView, parent)
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom) view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
@ -790,16 +837,21 @@ class SpinnerNoSwipe : androidx.appcompat.widget.AppCompatSpinner {
setup() setup()
} }
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
setup() setup()
} }
private fun setup() { private fun setup() {
mGestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { mGestureDetector =
override fun onSingleTapUp(e: MotionEvent): Boolean { GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
return performClick() override fun onSingleTapUp(e: MotionEvent): Boolean {
} return performClick()
}) }
})
} }
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
@ -843,7 +895,11 @@ fun getCurrentBrightnessValue(context: Context): Float {
} }
fun getCur(): 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) return brightnessConverter(getCur() / getMax(), true)
@ -865,12 +921,12 @@ fun checkCountry(context: Context): Boolean {
tz.equals("Asia/Kolkata", ignoreCase = true) tz.equals("Asia/Kolkata", ignoreCase = true)
} }
TelephonyManager.SIM_STATE_READY -> { TelephonyManager.SIM_STATE_READY -> {
val countryCodeValue = telMgr.networkCountryIso val countryCodeValue = telMgr.networkCountryIso
countryCodeValue.equals("in", ignoreCase = true) countryCodeValue.equals("in", ignoreCase = true)
} }
else -> false else -> false
} }
} }

View file

@ -1,15 +1,9 @@
package ani.dantotsu package ani.dantotsu
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent 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.Animatable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -17,18 +11,14 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings import android.provider.Settings
import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator import android.view.animation.AnticipateInterpolator
import android.widget.FrameLayout
import android.widget.TextView import android.widget.TextView
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach import androidx.core.view.doOnAttach
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
@ -37,13 +27,10 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.ItemNavbarBinding
import ani.dantotsu.databinding.SplashScreenBinding import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.home.AnimeFragment import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment import ani.dantotsu.home.LoginFragment
@ -51,27 +38,17 @@ import ani.dantotsu.home.MangaFragment
import ani.dantotsu.home.NoInternet import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.others.CustomBottomDialog import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.others.LangSet
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import ani.dantotsu.themes.ThemeManager 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.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar 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 import java.io.Serializable
@ -82,6 +59,7 @@ class MainActivity : AppCompatActivity() {
private var uiSettings = UserInterfaceSettings() private var uiSettings = UserInterfaceSettings()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
LangSet.setLocale(this) LangSet.setLocale(this)
@ -266,10 +244,6 @@ class MainActivity : AppCompatActivity() {
} }
override fun onResume() {
super.onResume()
}
//ViewPager //ViewPager
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :

View file

@ -8,58 +8,51 @@ import ani.dantotsu.others.webview.WebViewBottomDialog
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser import com.lagradost.nicehttp.ResponseParser
import com.lagradost.nicehttp.addGenericDns import com.lagradost.nicehttp.addGenericDns
import kotlinx.coroutines.* import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import okhttp3.Cache
import okhttp3.OkHttpClient 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.PrintWriter
import java.io.Serializable import java.io.Serializable
import java.io.StringWriter 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.KClass
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
val defaultHeaders = mapOf( lateinit var defaultHeaders: Map<String, String>
"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 okHttpClient: OkHttpClient lateinit var okHttpClient: OkHttpClient
lateinit var client: Requests lateinit var client: Requests
fun initializeNetwork(context: Context) { fun initializeNetwork(context: Context) {
val dns = loadData<Int>("settings_dns")
cache = Cache( val networkHelper = Injekt.get<NetworkHelper>()
File(context.cacheDir, "http_cache"),
5 * 1024L * 1024L // 5 MiB defaultHeaders = mapOf(
"User-Agent" to
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
.format(Build.VERSION.RELEASE, Build.MODEL)
) )
okHttpClient = OkHttpClient.Builder()
.followRedirects(true) okHttpClient = networkHelper.client
.followSslRedirects(true)
.cache(cache)
.apply {
when (dns) {
1 -> addGoogleDns()
2 -> addCloudFlareDns()
3 -> addAdGuardDns()
}
}
.build()
client = Requests( client = Requests(
okHttpClient, networkHelper.client,
defaultHeaders, defaultHeaders,
defaultCacheTime = 6, defaultCacheTime = 6,
defaultCacheTimeUnit = TimeUnit.HOURS, defaultCacheTimeUnit = TimeUnit.HOURS,
responseParser = Mapper responseParser = Mapper
) )
} }
object Mapper : ResponseParser { object Mapper : ResponseParser {
@ -122,7 +115,11 @@ fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T):
} }
} }
suspend fun <T> tryWithSuspend(post: Boolean = false, snackbar: Boolean = true, call: suspend () -> T): T? { suspend fun <T> tryWithSuspend(
post: Boolean = false,
snackbar: Boolean = true,
call: suspend () -> T
): T? {
return try { return try {
call.invoke() call.invoke()
} catch (e: Throwable) { } catch (e: Throwable) {
@ -202,28 +199,29 @@ fun OkHttpClient.Builder.addAdGuardDns() = (
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
suspend fun webViewInterface(webViewDialog: WebViewBottomDialog): Map<String, String>? { suspend fun webViewInterface(webViewDialog: WebViewBottomDialog): Map<String, String>? {
var map : Map<String,String>? = null var map: Map<String, String>? = null
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
webViewDialog.callback = { webViewDialog.callback = {
map = it map = it
latch.countDown() latch.countDown()
} }
val fragmentManager = (currContext() as FragmentActivity?)?.supportFragmentManager ?: return null val fragmentManager =
(currContext() as FragmentActivity?)?.supportFragmentManager ?: return null
webViewDialog.show(fragmentManager, "web-view") webViewDialog.show(fragmentManager, "web-view")
delay(0) delay(0)
latch.await(2,TimeUnit.MINUTES) latch.await(2, TimeUnit.MINUTES)
return map return map
} }
suspend fun webViewInterface(type: String, url: FileUrl): Map<String, String>? { suspend fun webViewInterface(type: String, url: FileUrl): Map<String, String>? {
val webViewDialog: WebViewBottomDialog = when (type) { val webViewDialog: WebViewBottomDialog = when (type) {
"Cloudflare" -> CloudFlare.newInstance(url) "Cloudflare" -> CloudFlare.newInstance(url)
else -> return null else -> return null
} }
return webViewInterface(webViewDialog) return webViewInterface(webViewDialog)
} }
suspend fun webViewInterface(type: String, url: String): Map<String, String>? { suspend fun webViewInterface(type: String, url: String): Map<String, String>? {
return webViewInterface(type,FileUrl(url)) return webViewInterface(type, FileUrl(url))
} }

View file

@ -4,18 +4,20 @@ package ani.dantotsu.aniyomi.anime.custom
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import ani.dantotsu.parsers.novel.NovelExtensionManager
import tachiyomi.core.preference.PreferenceStore
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore 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.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.source.anime.service.AnimeSourceManager import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.domain.source.manga.service.MangaSourceManager import tachiyomi.domain.source.manga.service.MangaSourceManager
import uy.kohesive.injekt.api.InjektModule 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.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import ani.dantotsu.download.DownloadsManager
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
@ -35,6 +36,7 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { AnimeExtensionManager(app) } addSingletonFactory { AnimeExtensionManager(app) }
addSingletonFactory { MangaExtensionManager(app) } addSingletonFactory { MangaExtensionManager(app) }
addSingletonFactory { NovelExtensionManager(app) }
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) } addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) } addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }

View file

@ -16,7 +16,7 @@ fun updateProgress(media: Media, number: String) {
if (Anilist.userid != null) { if (Anilist.userid != null) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val a = number.toFloatOrNull()?.roundToInt() val a = number.toFloatOrNull()?.roundToInt()
if ((a?:0) > (media.userProgress?:0)) { if ((a ?: 0) > (media.userProgress ?: 0)) {
Anilist.mutation.editList( Anilist.mutation.editList(
media.id, media.id,
a, a,

View file

@ -10,7 +10,7 @@ import ani.dantotsu.currContext
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import java.io.File import java.io.File
import java.util.* import java.util.Calendar
object Anilist { object Anilist {
val query: AnilistQueries = AnilistQueries() val query: AnilistQueries = AnilistQueries()
@ -29,7 +29,12 @@ object Anilist {
var tags: Map<Boolean, List<String>>? = null var tags: Map<Boolean, List<String>>? = null
val sortBy = listOf( 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( val seasons = listOf(
@ -51,11 +56,11 @@ object Anilist {
private val cal: Calendar = Calendar.getInstance() private val cal: Calendar = Calendar.getInstance()
private val currentYear = cal.get(Calendar.YEAR) private val currentYear = cal.get(Calendar.YEAR)
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) { private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
0, 1, 2 -> 0 0, 1, 2 -> 0
3, 4, 5 -> 1 3, 4, 5 -> 1
6, 7, 8 -> 2 6, 7, 8 -> 2
9, 10, 11 -> 3 9, 10, 11 -> 3
else -> 0 else -> 0
} }
private fun getSeason(next: Boolean): Pair<String, Int> { private fun getSeason(next: Boolean): Pair<String, Int> {
@ -132,7 +137,12 @@ object Anilist {
if (token != null || force) { if (token != null || force) {
if (token != null && useToken) headers["Authorization"] = "Bearer $token" 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 (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
if (show) println("Response : ${json.text}") if (show) println("Response : ${json.text}")
json.parsed() json.parsed()

View file

@ -20,7 +20,7 @@ class AnilistMutations {
repeat: Int? = null, repeat: Int? = null,
notes: String? = null, notes: String? = null,
status: String? = null, status: String? = null,
private:Boolean? = null, private: Boolean? = null,
startedAt: FuzzyDate? = null, startedAt: FuzzyDate? = null,
completedAt: FuzzyDate? = null, completedAt: FuzzyDate? = null,
customList: List<String>? = null customList: List<String>? = null
@ -41,7 +41,7 @@ class AnilistMutations {
${if (repeat != null) ""","repeat":$repeat""" else ""} ${if (repeat != null) ""","repeat":$repeat""" else ""}
${if (notes != null) ""","notes":"${notes.replace("\n", "\\n")}"""" else ""} ${if (notes != null) ""","notes":"${notes.replace("\n", "\\n")}"""" else ""}
${if (status != null) ""","status":"$status"""" 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(""" """, "") }""".replace("\n", "").replace(""" """, "")
println(variables) println(variables)
executeQuery<JsonObject>(query, variables, show = true) executeQuery<JsonObject>(query, variables, show = true)

View file

@ -2,13 +2,13 @@ package ani.dantotsu.connections.anilist
import android.app.Activity import android.app.Activity
import ani.dantotsu.R 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.authorRoles
import ani.dantotsu.connections.anilist.Anilist.executeQuery import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.Page import ani.dantotsu.connections.anilist.api.Page
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.checkGenreTime
import ani.dantotsu.checkId
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.logError import ani.dantotsu.logError
@ -113,9 +113,13 @@ class AnilistQueries {
name = i.node?.name?.userPreferred, name = i.node?.name?.userPreferred,
image = i.node?.image?.medium, image = i.node?.image?.medium,
banner = media.banner ?: media.cover, banner = media.banner ?: media.cover,
role = when (i.role.toString()){ role = when (i.role.toString()) {
"MAIN" -> currContext()?.getString(R.string.main_role) ?: "MAIN" "MAIN" -> currContext()?.getString(R.string.main_role)
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role) ?: "SUPPORTING" ?: "MAIN"
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role)
?: "SUPPORTING"
else -> i.role.toString() else -> i.role.toString()
} }
) )
@ -129,11 +133,16 @@ class AnilistQueries {
val m = Media(mediaEdge) val m = Media(mediaEdge)
media.relations?.add(m) media.relations?.add(m)
if (m.relation == "SEQUEL") { 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") { } else if (m.relation == "PREQUEL") {
media.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 } 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 -> fetchedMedia.externalLinks?.forEach { i ->
when (i.site.lowercase()) { when (i.site.lowercase()) {
"youtube" -> media.anime.youtube = i.url "youtube" -> media.anime.youtube = i.url
"crunchyroll" -> media.crunchySlug = i.url?.split("/")?.getOrNull(3) "crunchyroll" -> media.crunchySlug =
"vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4) 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 { fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
media.manga.author = Author( media.manga.author = Author(
it.id.toString(), it.id.toString(),
@ -241,10 +252,10 @@ class AnilistQueries {
return media return media
} }
suspend fun continueMedia(type: String,planned:Boolean=false): ArrayList<Media> { suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
val returnArray = arrayListOf<Media>() val returnArray = arrayListOf<Media>()
val map = mutableMapOf<Int, Media>() val map = mutableMapOf<Int, Media>()
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) { suspend fun repeat(status: String) {
val response = val response =
executeQuery<Query.MediaListCollection>(""" { 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 } } } } } } """) executeQuery<Query.MediaListCollection>(""" { 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 hasNextPage = true
var page = 0 var page = 0
suspend fun getNextPage(page:Int): List<Media> { suspend fun getNextPage(page: Int): List<Media> {
val response = val response =
executeQuery<Query.User>("""{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}}}}}}}""") executeQuery<Query.User>("""{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 favourites = response?.data?.user?.favourites
val apiMediaList = if (anime) favourites?.anime else favourites?.manga val apiMediaList = if (anime) favourites?.anime else favourites?.manga
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
return apiMediaList?.edges?.mapNotNull { return apiMediaList?.edges?.mapNotNull {
it.node?.let { i-> it.node?.let { i ->
Media(i).apply { isFav = true } Media(i).apply { isFav = true }
} }
} ?: return listOf() } ?: return listOf()
} }
val responseArray = arrayListOf<Media>() val responseArray = arrayListOf<Media>()
while(hasNextPage){ while (hasNextPage) {
page++ page++
responseArray.addAll(getNextPage(page)) responseArray.addAll(getNextPage(page))
} }
@ -361,7 +372,11 @@ class AnilistQueries {
return default return default
} }
suspend fun getMediaLists(anime: Boolean, userId: Int, sortOrder: String? = null): MutableMap<String, ArrayList<Media>> { suspend fun getMediaLists(
anime: Boolean,
userId: Int,
sortOrder: String? = null
): MutableMap<String, ArrayList<Media>> {
val response = val response =
executeQuery<Query.MediaListCollection>("""{ 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 } } } } }""") executeQuery<Query.MediaListCollection>("""{ 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<String, ArrayList<Media>>() val sorted = mutableMapOf<String, ArrayList<Media>>()
@ -388,7 +403,7 @@ class AnilistQueries {
if (unsorted.containsKey(it)) sorted[it] = unsorted[it]!! if (unsorted.containsKey(it)) sorted[it] = unsorted[it]!!
} }
unsorted.forEach { 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) sorted["Favourites"] = favMedia(anime)
@ -399,11 +414,18 @@ class AnilistQueries {
val sort = sortOrder ?: options?.rowOrder val sort = sortOrder ?: options?.rowOrder
for (i in sorted.keys) { for (i in sorted.keys) {
when (sort) { when (sort) {
"score" -> sorted[i]?.sortWith { b, a -> compareValuesBy(a, b, { it.userScore }, { it.meanScore }) } "score" -> sorted[i]?.sortWith { b, a ->
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName }) compareValuesBy(
a,
b,
{ it.userScore },
{ it.meanScore })
}
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
"updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt }) "updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt })
"release" -> sorted[i]?.sortWith(compareByDescending { it.startDate }) "release" -> sorted[i]?.sortWith(compareByDescending { it.startDate })
"id" -> sorted[i]?.sortWith(compareBy { it.id }) "id" -> sorted[i]?.sortWith(compareBy { it.id })
} }
} }
return sorted return sorted
@ -559,18 +581,36 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""} ${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
${if (season != null) ""","season":"$season"""" else ""} ${if (season != null) ""","season":"$season"""" else ""}
${if (search != null) ""","search":"$search"""" 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 (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""} ${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
${ ${
if (excludedGenres?.isNotEmpty() == true) if (excludedGenres?.isNotEmpty() == true)
""","excludedGenres":[${excludedGenres.joinToString { "\"${it.replace("Not ", "")}\"" }}]""" ""","excludedGenres":[${
excludedGenres.joinToString {
"\"${
it.replace(
"Not ",
""
)
}\""
}
}]"""
else "" else ""
} }
${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""} ${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""}
${ ${
if (excludedTags?.isNotEmpty() == true) if (excludedTags?.isNotEmpty() == true)
""","excludedTags":[${excludedTags.joinToString { "\"${it.replace("Not ", "")}\"" }}]""" ""","excludedTags":[${
excludedTags.joinToString {
"\"${
it.replace(
"Not ",
""
)
}\""
}
}]"""
else "" else ""
} }
}""".replace("\n", " ").replace(""" """, "") }""".replace("\n", " ").replace(""" """, "")
@ -622,7 +662,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
greater: Long = 0, greater: Long = 0,
lesser: Long = System.currentTimeMillis() / 1000 - 10000 lesser: Long = System.currentTimeMillis() / 1000 - 10000
): MutableList<Media>? { ): MutableList<Media>? {
suspend fun execute(page:Int = 1):Page?{ suspend fun execute(page: Int = 1): Page? {
val query = """{ val query = """{
Page(page:$page,perPage:50) { Page(page:$page,perPage:50) {
pageInfo { pageInfo {
@ -668,7 +708,7 @@ Page(page:$page,perPage:50) {
}""".replace("\n", " ").replace(""" """, "") }""".replace("\n", " ").replace(""" """, "")
return executeQuery<Query.Page>(query, force = true)?.data?.page return executeQuery<Query.Page>(query, force = true)?.data?.page
} }
if(smaller) { if (smaller) {
val response = execute()?.airingSchedules ?: return null val response = execute()?.airingSchedules ?: return null
val idArr = mutableListOf<Int>() val idArr = mutableListOf<Int>()
val listOnly = loadData("recently_list_only") ?: false val listOnly = loadData("recently_list_only") ?: false
@ -682,11 +722,11 @@ Page(page:$page,perPage:50) {
else null else null
} }
}.toMutableList() }.toMutableList()
}else{ } else {
var i = 1 var i = 1
val list = mutableListOf<Media>() val list = mutableListOf<Media>()
var res : Page? = null var res: Page? = null
suspend fun next(){ suspend fun next() {
res = execute(i) res = execute(i)
list.addAll(res?.airingSchedules?.mapNotNull { j -> list.addAll(res?.airingSchedules?.mapNotNull { j ->
j.media?.let { j.media?.let {
@ -694,10 +734,10 @@ Page(page:$page,perPage:50) {
Media(it).apply { relation = "${j.episode},${j.airingAt}" } Media(it).apply { relation = "${j.episode},${j.airingAt}" }
} else null } else null
} }
}?: listOf()) } ?: listOf())
} }
next() next()
while (res?.pageInfo?.hasNextPage == true){ while (res?.pageInfo?.hasNextPage == true) {
next() next()
i++ i++
} }
@ -822,19 +862,20 @@ Page(page:$page,perPage:50) {
var page = 0 var page = 0
while (hasNextPage) { while (hasNextPage) {
page++ page++
hasNextPage = executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let { hasNextPage =
it.edges?.forEach { i -> executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
i.node?.apply { it.edges?.forEach { i ->
val status = status.toString() i.node?.apply {
val year = startDate?.year?.toString() ?: "TBA" val status = status.toString()
val title = if (status != "CANCELLED") year else status val year = startDate?.year?.toString() ?: "TBA"
if (!yearMedia.containsKey(title)) val title = if (status != "CANCELLED") year else status
yearMedia[title] = arrayListOf() if (!yearMedia.containsKey(title))
yearMedia[title]?.add(Media(this)) yearMedia[title] = arrayListOf()
yearMedia[title]?.add(Media(this))
}
} }
} it.pageInfo?.hasNextPage == true
it.pageInfo?.hasNextPage == true } ?: false
} ?: false
} }
if (yearMedia.contains("CANCELLED")) { if (yearMedia.contains("CANCELLED")) {
val a = yearMedia["CANCELLED"]!! val a = yearMedia["CANCELLED"]!!
@ -896,7 +937,10 @@ Page(page:$page,perPage:50) {
while (hasNextPage) { while (hasNextPage) {
page++ page++
hasNextPage = executeQuery<Query.Author>(query(page), force = true)?.data?.author?.staffMedia?.let { hasNextPage = executeQuery<Query.Author>(
query(page),
force = true
)?.data?.author?.staffMedia?.let {
it.edges?.forEach { i -> it.edges?.forEach { i ->
i.node?.apply { i.node?.apply {
val status = status.toString() val status = status.toString()

View file

@ -7,8 +7,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.loadData
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.loadData
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.others.AppUpdater import ani.dantotsu.others.AppUpdater
import ani.dantotsu.snackString import ani.dantotsu.snackString
@ -19,9 +19,16 @@ import kotlinx.coroutines.launch
suspend fun getUserId(context: Context, block: () -> Unit) { suspend fun getUserId(context: Context, block: () -> Unit) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
if (Discord.userid == null && Discord.token != null) { val sharedPref = context.getSharedPreferences(
if (!Discord.getUserData()) context.getString(R.string.preference_file_key),
snackString(context.getString(R.string.error_loading_discord_user_data)) 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 } else true
if(anilist) block.invoke() if (anilist) block.invoke()
} }
class AnilistHomeViewModel : ViewModel() { class AnilistHomeViewModel : ViewModel() {
private val listImages: MutableLiveData<ArrayList<String?>> = MutableLiveData<ArrayList<String?>>(arrayListOf()) private val listImages: MutableLiveData<ArrayList<String?>> =
MutableLiveData<ArrayList<String?>>(arrayListOf())
fun getListImages(): LiveData<ArrayList<String?>> = listImages fun getListImages(): LiveData<ArrayList<String?>> = listImages
suspend fun setListImages() = listImages.postValue(Anilist.query.getBannerImages()) suspend fun setListImages() = listImages.postValue(Anilist.query.getBannerImages())
private val animeContinue: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null) private val animeContinue: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME")) suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
private val animeFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null) private val animeFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true)) suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
private val animePlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null) private val animePlanned: MutableLiveData<ArrayList<Media>> =
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned MutableLiveData<ArrayList<Media>>(null)
suspend fun setAnimePlanned() = animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
suspend fun setAnimePlanned() =
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
private val mangaContinue: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA")) suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
private val mangaFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null) private val mangaFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false)) suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
private val mangaPlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null) private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned MutableLiveData<ArrayList<Media>>(null)
suspend fun setMangaPlanned() = mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
suspend fun setMangaPlanned() =
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
private val recommendation: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
private val recommendation: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations()) suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
@ -93,7 +118,9 @@ class AnilistAnimeViewModel : ViewModel() {
var notSet = true var notSet = true
lateinit var searchResults: SearchResults lateinit var searchResults: SearchResults
private val type = "ANIME" private val type = "ANIME"
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null) private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTrending(): LiveData<MutableList<Media>> = trending fun getTrending(): LiveData<MutableList<Media>> = trending
suspend fun loadTrending(i: Int) { suspend fun loadTrending(i: Int) {
val (season, year) = Anilist.currentSeasons[i] val (season, year) = Anilist.currentSeasons[i]
@ -109,7 +136,9 @@ class AnilistAnimeViewModel : ViewModel() {
) )
} }
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null) private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated fun getUpdated(): LiveData<MutableList<Media>> = updated
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated()) suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
@ -157,15 +186,33 @@ class AnilistMangaViewModel : ViewModel() {
var notSet = true var notSet = true
lateinit var searchResults: SearchResults lateinit var searchResults: SearchResults
private val type = "MANGA" private val type = "MANGA"
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null) private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTrending(): LiveData<MutableList<Media>> = trending fun getTrending(): LiveData<MutableList<Media>> = trending
suspend fun loadTrending() = 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<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
suspend fun loadTrendingNovel() = 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<SearchResults?>(null) private val mangaPopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = mangaPopular fun getPopular(): LiveData<SearchResults?> = mangaPopular

View file

@ -6,19 +6,20 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.logError import ani.dantotsu.logError
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.others.LangSet
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class Login : AppCompatActivity() { class Login : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
val data: Uri? = intent?.data val data: Uri? = intent?.data
logger(data.toString()) logger(data.toString())
try { 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" val filename = "anilistToken"
this.openFileOutput(filename, Context.MODE_PRIVATE).use { this.openFileOutput(filename, Context.MODE_PRIVATE).use {
it.write(Anilist.token!!.toByteArray()) it.write(Anilist.token!!.toByteArray())

View file

@ -27,7 +27,15 @@ data class SearchResults(
val list = mutableListOf<SearchChip>() val list = mutableListOf<SearchChip>()
sort?.let { sort?.let {
val c = currContext()!! 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 { format?.let {
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it))) list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
@ -42,27 +50,37 @@ data class SearchResults(
list.add(SearchChip("GENRE", it)) list.add(SearchChip("GENRE", it))
} }
excludedGenres?.forEach { 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 { tags?.forEach {
list.add(SearchChip("TAG", it)) list.add(SearchChip("TAG", it))
} }
excludedTags?.forEach { 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 return list
} }
fun removeChip(chip: SearchChip) { fun removeChip(chip: SearchChip) {
when (chip.type) { when (chip.type) {
"SORT" -> sort = null "SORT" -> sort = null
"FORMAT" -> format = null "FORMAT" -> format = null
"SEASON" -> season = null "SEASON" -> season = null
"SEASON_YEAR" -> seasonYear = null "SEASON_YEAR" -> seasonYear = null
"GENRE" -> genres?.remove(chip.text) "GENRE" -> genres?.remove(chip.text)
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text) "EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
"TAG" -> tags?.remove(chip.text) "TAG" -> tags?.remove(chip.text)
"EXCLUDED_TAG" -> excludedTags?.remove(chip.text) "EXCLUDED_TAG" -> excludedTags?.remove(chip.text)
} }
} }

View file

@ -5,15 +5,15 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import ani.dantotsu.loadMedia import ani.dantotsu.loadMedia
import ani.dantotsu.others.LangSet
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
class UrlMedia : Activity() { class UrlMedia : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0 var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
var isMAL = false var isMAL = false
var continueMedia = true var continueMedia = true
@ -23,6 +23,9 @@ ThemeManager(this).applyTheme()
isMAL = data?.host != "anilist.co" isMAL = data?.host != "anilist.co"
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull() id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
} else loadMedia = id } 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)
)
} }
} }

View file

@ -3,23 +3,24 @@ package ani.dantotsu.connections.anilist.api
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
class Query{ class Query {
@Serializable @Serializable
data class Viewer( data class Viewer(
@SerialName("data") @SerialName("data")
val data : Data? val data: Data?
){ ) {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("Viewer") @SerialName("Viewer")
val user: ani.dantotsu.connections.anilist.api.User? val user: ani.dantotsu.connections.anilist.api.User?
) )
} }
@Serializable @Serializable
data class Media( data class Media(
@SerialName("data") @SerialName("data")
val data : Data? val data: Data?
){ ) {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("Media") @SerialName("Media")
@ -30,12 +31,12 @@ class Query{
@Serializable @Serializable
data class Page( data class Page(
@SerialName("data") @SerialName("data")
val data : Data? val data: Data?
){ ) {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("Page") @SerialName("Page")
val page : ani.dantotsu.connections.anilist.api.Page? val page: ani.dantotsu.connections.anilist.api.Page?
) )
} }
// data class AiringSchedule( // data class AiringSchedule(
@ -49,8 +50,8 @@ class Query{
@Serializable @Serializable
data class Character( data class Character(
@SerialName("data") @SerialName("data")
val data : Data? val data: Data?
){ ) {
@Serializable @Serializable
data class Data( data class Data(
@ -63,7 +64,7 @@ class Query{
data class Studio( data class Studio(
@SerialName("data") @SerialName("data")
val data: Data? val data: Data?
){ ) {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("Studio") @SerialName("Studio")
@ -76,7 +77,7 @@ class Query{
data class Author( data class Author(
@SerialName("data") @SerialName("data")
val data: Data? val data: Data?
){ ) {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("Staff") @SerialName("Staff")
@ -95,8 +96,8 @@ class Query{
@Serializable @Serializable
data class MediaListCollection( data class MediaListCollection(
@SerialName("data") @SerialName("data")
val data : Data? val data: Data?
){ ) {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("MediaListCollection") @SerialName("MediaListCollection")
@ -108,7 +109,7 @@ class Query{
data class GenreCollection( data class GenreCollection(
@SerialName("data") @SerialName("data")
val data: Data val data: Data
){ ) {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("GenreCollection") @SerialName("GenreCollection")
@ -120,7 +121,7 @@ class Query{
data class MediaTagCollection( data class MediaTagCollection(
@SerialName("data") @SerialName("data")
val data: Data val data: Data
){ ) {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("MediaTagCollection") @SerialName("MediaTagCollection")
@ -132,7 +133,7 @@ class Query{
data class User( data class User(
@SerialName("data") @SerialName("data")
val data: Data val data: Data
){ ) {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("User") @SerialName("User")

View file

@ -3,7 +3,7 @@ package ani.dantotsu.connections.anilist.api
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import java.io.Serializable import java.io.Serializable
import java.text.DateFormatSymbols import java.text.DateFormatSymbols
import java.util.* import java.util.Calendar
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class FuzzyDate( data class FuzzyDate(
@ -16,9 +16,11 @@ data class FuzzyDate(
fun isEmpty(): Boolean { fun isEmpty(): Boolean {
return year == null && month == null && day == null return year == null && month == null && day == null
} }
override fun toString(): String { override fun toString(): String {
return if ( isEmpty() ) "??" else toStringOrEmpty() return if (isEmpty()) "??" else toStringOrEmpty()
} }
fun toStringOrEmpty(): String { fun toStringOrEmpty(): String {
return listOfNotNull( return listOfNotNull(
day?.toString(), day?.toString(),
@ -29,16 +31,21 @@ data class FuzzyDate(
fun getToday(): FuzzyDate { fun getToday(): FuzzyDate {
val cal = Calendar.getInstance() 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 { fun toVariableString(): String {
return listOfNotNull( return listOfNotNull(
year?.let {"year:$it"}, year?.let { "year:$it" },
month?.let {"month:$it"}, month?.let { "month:$it" },
day?.let {"day:$it"} day?.let { "day:$it" }
).joinToString(",", "{", "}") ).joinToString(",", "{", "}")
} }
fun toMALString(): String { fun toMALString(): String {
val padding = '0' val padding = '0'
val values = listOf( val values = listOf(
@ -46,7 +53,7 @@ data class FuzzyDate(
month?.toString()?.padStart(2, padding), month?.toString()?.padStart(2, padding),
day?.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 { // fun toInt(): Int {
@ -54,8 +61,8 @@ data class FuzzyDate(
// } // }
override fun compareTo(other: FuzzyDate): Int = when { 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) month != other.month -> (month ?: 0) - (other.month ?: 0)
else -> (day ?: 0) - (other.day ?: 0) else -> (day ?: 0) - (other.day ?: 0)
} }
} }

View file

@ -116,7 +116,7 @@ data class Media(
@SerialName("characters") var characters: CharacterConnection?, @SerialName("characters") var characters: CharacterConnection?,
// The staff who produced the media // The staff who produced the media
@SerialName("staffPreview") var staff: StaffConnection?, @SerialName("staffPreview") var staff: StaffConnection?,
// The companies who produced the media // The companies who produced the media
@SerialName("studios") var studios: StudioConnection?, @SerialName("studios") var studios: StudioConnection?,
@ -292,7 +292,7 @@ data class MediaList(
@SerialName("hiddenFromStatusLists") var hiddenFromStatusLists: Boolean?, @SerialName("hiddenFromStatusLists") var hiddenFromStatusLists: Boolean?,
// Map of booleans for which custom lists the entry are in // Map of booleans for which custom lists the entry are in
@SerialName("customLists") var customLists: Map<String,Boolean>?, @SerialName("customLists") var customLists: Map<String, Boolean>?,
// Map of advanced scores with name keys // Map of advanced scores with name keys
// @SerialName("advancedScores") var advancedScores: Json?, // @SerialName("advancedScores") var advancedScores: Json?,
@ -355,7 +355,7 @@ data class MediaTrailer(
@Serializable @Serializable
data class MediaTagCollection( data class MediaTagCollection(
@SerialName("tags") var tags : List<MediaTag>? @SerialName("tags") var tags: List<MediaTag>?
) )
@Serializable @Serializable

View file

@ -2,6 +2,7 @@ package ani.dantotsu.connections.anilist.api
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Recommendation( data class Recommendation(
// The id of the recommendation // The id of the recommendation
@ -22,6 +23,7 @@ data class Recommendation(
// The user that first created the recommendation // The user that first created the recommendation
@SerialName("user") var user: User?, @SerialName("user") var user: User?,
) )
@Serializable @Serializable
data class RecommendationConnection( data class RecommendationConnection(
//@SerialName("edges") var edges: List<RecommendationEdge>?, //@SerialName("edges") var edges: List<RecommendationEdge>?,

View file

@ -9,7 +9,7 @@ data class Staff(
@SerialName("id") var id: Int, @SerialName("id") var id: Int,
// The names of the staff member // 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 // 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?, @SerialName("languageV2") var languageV2: String?,
@ -80,8 +80,8 @@ data class Staff(
) )
@Serializable @Serializable
data class StaffName ( data class StaffName(
var userPreferred:String? var userPreferred: String?
) )
@Serializable @Serializable
@ -96,6 +96,6 @@ data class StaffConnection(
@Serializable @Serializable
data class StaffEdge( data class StaffEdge(
var role:String?, var role: String?,
var node: Staff? var node: Staff?
) )

View file

@ -80,10 +80,10 @@ data class UserOptions(
@SerialName("displayAdultContent") var displayAdultContent: Boolean?, @SerialName("displayAdultContent") var displayAdultContent: Boolean?,
// Whether the user receives notifications when a show they are watching aires // 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) // Profile highlight color (blue, purple, pink, orange, red, green, gray)
@SerialName("profileColor") var profileColor: String?, @SerialName("profileColor") var profileColor: String?,
// //
// // Notification options // // Notification options
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?, // // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,

View file

@ -5,14 +5,11 @@ import android.content.Intent
import android.widget.TextView import android.widget.TextView
import androidx.core.content.edit import androidx.core.content.edit
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.discord.serializers.User
import ani.dantotsu.others.CustomBottomDialog import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.tryWith import ani.dantotsu.tryWith
import ani.dantotsu.tryWithSuspend
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers
import java.io.File import java.io.File
object Discord { object Discord {
@ -21,7 +18,7 @@ object Discord {
var userid: String? = null var userid: String? = null
var avatar: String? = null var avatar: String? = null
private const val TOKEN = "discord_token" const val TOKEN = "discord_token"
fun getSavedToken(context: Context): Boolean { fun getSavedToken(context: Context): Boolean {
val sharedPref = context.getSharedPreferences( val sharedPref = context.getSharedPreferences(
@ -60,17 +57,7 @@ object Discord {
} }
} }
private var rpc : RPC? = null 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
fun warning(context: Context) = CustomBottomDialog().apply { fun warning(context: Context) = CustomBottomDialog().apply {
@ -97,16 +84,21 @@ object Discord {
context.startActivity(intent) 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 { return token?.let {
RPC(it, Dispatchers.IO).apply { RPC(it, Dispatchers.IO).apply {
applicationId = "1163925779692912771" applicationId = application_Id
smallImage = RPC.Link( smallImage = RPC.Link(
"Dantotsu", "Dantotsu",
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png" small_Image
) )
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/")) buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
} }
} }
} }*/
} }

View file

@ -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<User.Response>(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
}

View file

@ -7,15 +7,13 @@ import android.os.Bundle
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.annotation.RequiresApi import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord.saveToken import ani.dantotsu.connections.discord.Discord.saveToken
import ani.dantotsu.logger import ani.dantotsu.others.LangSet
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import ani.dantotsu.snackString
class Login : AppCompatActivity() { class Login : AppCompatActivity() {
@ -39,17 +37,22 @@ class Login : AppCompatActivity() {
} }
WebView.setWebContentsDebuggingEnabled(true) WebView.setWebContentsDebuggingEnabled(true)
webView.webViewClient = object : WebViewClient() { 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 // Check if the URL is the one expected after a successful login
if (request?.url.toString() != "https://discord.com/login") { if (request?.url.toString() != "https://discord.com/login") {
// Delay the script execution to ensure the page is fully loaded // Delay the script execution to ensure the page is fully loaded
view?.postDelayed({ view?.postDelayed({
view.evaluateJavascript(""" view.evaluateJavascript(
"""
(function() { (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(); 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; return wreq;
})() })()
""".trimIndent()) { result -> """.trimIndent()
) { result ->
login(result.trim('"')) login(result.trim('"'))
} }
}, 2000) }, 2000)
@ -66,12 +69,12 @@ class Login : AppCompatActivity() {
} }
private fun login(token: String) { private fun login(token: String) {
if (token.isEmpty() || token == "null"){ if (token.isEmpty() || token == "null") {
snackString("Failed to retrieve token") Toast.makeText(this, "Failed to retrieve token", Toast.LENGTH_SHORT).show()
finish() finish()
return return
} }
snackString("Logged in successfully") Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
finish() finish()
saveToken(this, token) saveToken(this, token)
startMainActivity(this@Login) startMainActivity(this@Login)

View file

@ -1,26 +1,10 @@
package ani.dantotsu.connections.discord package ani.dantotsu.connections.discord
import ani.dantotsu.connections.discord.serializers.* import ani.dantotsu.connections.discord.serializers.Activity
import kotlinx.coroutines.CoroutineScope import ani.dantotsu.connections.discord.serializers.Presence
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 kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json 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 kotlin.coroutines.CoroutineContext
import ani.dantotsu.client as app import ani.dantotsu.client as app
@ -33,203 +17,73 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
ignoreUnknownKeys = true 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 { enum class Type {
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
} }
var buttons = mutableListOf<Link>()
data class Link(val label: String, val url: String) data class Link(val label: String, val url: String)
private suspend fun createPresence(): String { companion object {
return json.encodeToString(Presence.Response( data class RPCData(
3, val applicationId: String? = null,
Presence( val type: Type? = null,
activities = listOf( val activityName: String? = null,
Activity( val details: String? = null,
name = activityName, val state: String? = null,
state = state, val largeImage: Link? = null,
details = details, val smallImage: Link? = null,
type = type?.ordinal, val status: String? = null,
timestamps = if (startTimestamp != null) val startTimestamp: Long? = null,
Activity.Timestamps(startTimestamp, stopTimestamp) val stopTimestamp: Long? = null,
else null, val buttons: MutableList<Link> = mutableListOf()
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<KizzyApi>()
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
)
) )
webSocket.send(json.encodeToString(response))
}
fun send(block: RPC.() -> Unit) { @Serializable
block.invoke(this) data class KizzyApi(val id: String)
send()
}
var started = false val api = "https://kizzy-api.vercel.app/image?url="
var whenStarted: ((User) -> Unit)? = null private suspend fun String.discordUrl(): String? {
if (startsWith("mp:")) return this
val json = app.get("$api$this").parsedSafe<KizzyApi>()
return json?.id
}
fun send() { suspend fun createPresence(data: RPCData): String {
val send = { val json = Json {
CoroutineScope(coroutineContext).launch { encodeDefaults = true
webSocket.send(createPresence()) allowStructuredMapKeys = true
ignoreUnknownKeys = true
} }
} return json.encodeToString(Presence.Response(
if (!started) whenStarted = { 3,
send.invoke() Presence(
whenStarted = null activities = listOf(
} Activity(
else send.invoke() name = data.activityName,
} state = data.state,
details = data.details,
fun close() { type = data.type?.ordinal,
webSocket.send( timestamps = if (data.startTimestamp != null)
json.encodeToString( Activity.Timestamps(data.startTimestamp, data.stopTimestamp)
Presence.Response( else null,
3, assets = Activity.Assets(
Presence(status = "offline") 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<Unit>)? = 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<Res>(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<User.Response>(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())
}
} }
} }

View file

@ -2,8 +2,9 @@ package ani.dantotsu.connections.discord.serializers
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Activity ( data class Activity(
@SerialName("application_id") @SerialName("application_id")
val applicationId: String? = null, val applicationId: String? = null,
val name: String? = null, val name: String? = null,

View file

@ -12,13 +12,13 @@ data class Identity(
) { ) {
@Serializable @Serializable
data class Response ( data class Response(
val op: Long, val op: Long,
val d: Identity val d: Identity
) )
@Serializable @Serializable
data class Properties ( data class Properties(
@SerialName("\$os") @SerialName("\$os")
val os: String, val os: String,

View file

@ -3,14 +3,14 @@ package ani.dantotsu.connections.discord.serializers
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Presence ( data class Presence(
val activities: List<Activity> = listOf(), val activities: List<Activity> = listOf(),
val afk: Boolean = true, val afk: Boolean = true,
val since: Long? = null, val since: Long? = null,
val status: String? = null val status: String? = null
){ ) {
@Serializable @Serializable
data class Response ( data class Response(
val op: Long, val op: Long,
val d: Presence val d: Presence
) )

View file

@ -1,60 +1,60 @@
package ani.dantotsu.connections.discord.serializers package ani.dantotsu.connections.discord.serializers
import kotlinx.serialization.* import kotlinx.serialization.SerialName
import kotlinx.serialization.descriptors.* import kotlinx.serialization.Serializable
import kotlinx.serialization.encoding.* import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.*
@Serializable @Serializable
data class User ( data class User(
val verified: Boolean, val verified: Boolean? = null,
val username: String, val username: String,
@SerialName("purchased_flags") @SerialName("purchased_flags")
val purchasedFlags: Long, val purchasedFlags: Long? = null,
@SerialName("public_flags") @SerialName("public_flags")
val publicFlags: Long, val publicFlags: Long? = null,
val pronouns: String, val pronouns: String? = null,
@SerialName("premium_type") @SerialName("premium_type")
val premiumType: Long, val premiumType: Long? = null,
val premium: Boolean, val premium: Boolean? = null,
val phone: String, val phone: String? = null,
@SerialName("nsfw_allowed") @SerialName("nsfw_allowed")
val nsfwAllowed: Boolean, val nsfwAllowed: Boolean? = null,
val mobile: Boolean, val mobile: Boolean? = null,
@SerialName("mfa_enabled") @SerialName("mfa_enabled")
val mfaEnabled: Boolean, val mfaEnabled: Boolean? = null,
val id: String, val id: String,
@SerialName("global_name") @SerialName("global_name")
val globalName: String, val globalName: String? = null,
val flags: Long, val flags: Long? = null,
val email: String, val email: String? = null,
val discriminator: String, val discriminator: String? = null,
val desktop: Boolean, val desktop: Boolean? = null,
val bio: String, val bio: String? = null,
@SerialName("banner_color") @SerialName("banner_color")
val bannerColor: String, val bannerColor: String? = null,
val banner: JsonElement? = null, val banner: JsonElement? = null,
@SerialName("avatar_decoration") @SerialName("avatar_decoration")
val avatarDecoration: JsonElement? = null, val avatarDecoration: JsonElement? = null,
val avatar: String, val avatar: String? = null,
@SerialName("accent_color") @SerialName("accent_color")
val accentColor: Long val accentColor: Long? = null
) { ) {
@Serializable @Serializable
data class Response( 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" return "https://cdn.discordapp.com/avatars/$id/$avatar.png"
} }
} }

View file

@ -4,11 +4,17 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope 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.clientId
import ani.dantotsu.connections.mal.MAL.saveResponse 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.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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -16,7 +22,7 @@ class Login : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
try { try {
val data: Uri = intent?.data val data: Uri = intent?.data
?: throw Exception(getString(R.string.mal_login_uri_not_found)) ?: throw Exception(getString(R.string.mal_login_uri_not_found))
@ -46,9 +52,8 @@ ThemeManager(this).applyTheme()
} }
} }
} }
} } catch (e: Exception) {
catch (e:Exception){ logError(e, snackbar = false)
logError(e,snackbar = false)
startMainActivity(this) startMainActivity(this)
} }
} }

View file

@ -6,7 +6,13 @@ import android.net.Uri
import android.util.Base64 import android.util.Base64
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.FragmentActivity 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.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.io.File import java.io.File
@ -94,6 +100,6 @@ object MAL {
@SerialName("expires_in") var expiresIn: Long, @SerialName("expires_in") var expiresIn: Long,
@SerialName("access_token") val accessToken: String, @SerialName("access_token") val accessToken: String,
@SerialName("refresh_token") val refreshToken: String, @SerialName("refresh_token") val refreshToken: String,
): java.io.Serializable ) : java.io.Serializable
} }

View file

@ -1,7 +1,7 @@
package ani.dantotsu.connections.mal package ani.dantotsu.connections.mal
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.client import ani.dantotsu.client
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -43,18 +43,18 @@ class MALQueries {
start: FuzzyDate? = null, start: FuzzyDate? = null,
end: FuzzyDate? = null end: FuzzyDate? = null
) { ) {
if(idMAL==null) return if (idMAL == null) return
val data = mutableMapOf("status" to convertStatus(isAnime, status)) val data = mutableMapOf("status" to convertStatus(isAnime, status))
if (progress != null) if (progress != null)
data[if (isAnime) "num_watched_episodes" else "num_chapters_read"] = progress.toString() data[if (isAnime) "num_watched_episodes" else "num_chapters_read"] = progress.toString()
data[if (isAnime) "is_rewatching" else "is_rereading"] = (status == "REPEATING").toString() data[if (isAnime) "is_rewatching" else "is_rereading"] = (status == "REPEATING").toString()
if (score != null) if (score != null)
data["score"] = score.div(10).toString() data["score"] = score.div(10).toString()
if(rewatch!=null) if (rewatch != null)
data[if(isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString() data[if (isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString()
if(start!=null) if (start != null)
data["start_date"] = start.toMALString() data["start_date"] = start.toMALString()
if(end!=null) if (end != null)
data["finish_date"] = end.toMALString() data["finish_date"] = end.toMALString()
tryWithSuspend { tryWithSuspend {
client.put( client.put(
@ -65,8 +65,8 @@ class MALQueries {
} }
} }
suspend fun deleteList(isAnime: Boolean, idMAL: Int?){ suspend fun deleteList(isAnime: Boolean, idMAL: Int?) {
if(idMAL==null) return if (idMAL == null) return
tryWithSuspend { tryWithSuspend {
client.delete( client.delete(
"$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status", "$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status",

View file

@ -4,15 +4,15 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet import ani.dantotsu.others.LangSet
import ani.dantotsu.themes.ThemeManager
class DownloadContainerActivity : AppCompatActivity() { class DownloadContainerActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
setContentView(R.layout.activity_container) setContentView(R.layout.activity_container)
val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME") val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME")

View file

@ -10,7 +10,8 @@ import java.io.File
import java.io.Serializable import java.io.Serializable
class DownloadsManager(private val context: Context) { 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 gson = Gson()
private val downloadsList = loadDownloads().toMutableList() private val downloadsList = loadDownloads().toMutableList()
@ -18,6 +19,8 @@ class DownloadsManager(private val context: Context) {
get() = downloadsList.filter { it.type == Download.Type.MANGA } get() = downloadsList.filter { it.type == Download.Type.MANGA }
val animeDownloads: List<Download> val animeDownloads: List<Download>
get() = downloadsList.filter { it.type == Download.Type.ANIME } get() = downloadsList.filter { it.type == Download.Type.ANIME }
val novelDownloads: List<Download>
get() = downloadsList.filter { it.type == Download.Type.NOVEL }
private fun saveDownloads() { private fun saveDownloads() {
val jsonString = gson.toJson(downloadsList) val jsonString = gson.toJson(downloadsList)
@ -45,11 +48,52 @@ class DownloadsManager(private val context: Context) {
saveDownloads() saveDownloads()
} }
private fun removeDirectory(download: Download) { fun removeMedia(title: String, type: Download.Type) {
val directory = if (download.type == Download.Type.MANGA){ val subDirectory = if (type == Download.Type.MANGA) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}") "Manga"
} else if (type == Download.Type.ANIME) {
"Anime"
} else { } 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 // 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 fun exportDownloads(download: Download) { //copies to the downloads folder available to the user
val directory = if (download.type == Download.Type.MANGA){ val directory = if (download.type == Download.Type.MANGA) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga/${download.title}/${download.chapter}") 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 { } 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()) { if (directory.exists()) {
val copied = directory.copyRecursively(destination, true) val copied = directory.copyRecursively(destination, true)
if (copied) { if (copied) {
@ -84,11 +142,13 @@ class DownloadsManager(private val context: Context) {
} }
} }
fun purgeDownloads(type: Download.Type){ fun purgeDownloads(type: Download.Type) {
val directory = if (type == Download.Type.MANGA){ val directory = if (type == Download.Type.MANGA) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga") File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
} else { } else if (type == Download.Type.ANIME) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime") File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
} }
if (directory.exists()) { if (directory.exists()) {
val deleted = directory.deleteRecursively() val deleted = directory.deleteRecursively()
@ -105,11 +165,18 @@ class DownloadsManager(private val context: Context) {
saveDownloads() 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 { data class Download(val title: String, val chapter: String, val type: Type) : Serializable {
enum class Type { enum class Type {
MANGA, MANGA,
ANIME ANIME,
NOVEL
} }
} }

View file

@ -9,7 +9,6 @@ import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.IBinder import android.os.IBinder
@ -17,24 +16,13 @@ import android.widget.Toast
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.download.Download import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.manga.ImageData 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_FAILED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROGRESS 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.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator 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.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope 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.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock 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.Queue
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -82,18 +82,27 @@ class MangaDownloaderService : Service() {
setProgress(0, 0, false) setProgress(0, 0, false)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, builder.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) startForeground(
}else{ NOTIFICATION_ID,
builder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
startForeground(NOTIFICATION_ID, builder.build()) 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() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
ServiceDataSingleton.downloadQueue.clear() MangaServiceDataSingleton.downloadQueue.clear()
downloadJobs.clear() downloadJobs.clear()
ServiceDataSingleton.isServiceRunning = false MangaServiceDataSingleton.isServiceRunning = false
unregisterReceiver(cancelReceiver) unregisterReceiver(cancelReceiver)
} }
@ -114,8 +123,8 @@ class MangaDownloaderService : Service() {
private fun processQueue() { private fun processQueue() {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
while (ServiceDataSingleton.downloadQueue.isNotEmpty()) { while (MangaServiceDataSingleton.downloadQueue.isNotEmpty()) {
val task = ServiceDataSingleton.downloadQueue.poll() val task = MangaServiceDataSingleton.downloadQueue.poll()
if (task != null) { if (task != null) {
val job = launch { download(task) } val job = launch { download(task) }
mutex.withLock { mutex.withLock {
@ -127,7 +136,7 @@ class MangaDownloaderService : Service() {
} }
updateNotification() // Update the notification after each task is completed updateNotification() // Update the notification after each task is completed
} }
if (ServiceDataSingleton.downloadQueue.isEmpty()) { if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
stopSelf() // Stop the service when the queue is empty stopSelf() // Stop the service when the queue is empty
} }
@ -141,7 +150,7 @@ class MangaDownloaderService : Service() {
mutex.withLock { mutex.withLock {
downloadJobs[chapter]?.cancel() downloadJobs[chapter]?.cancel()
downloadJobs.remove(chapter) downloadJobs.remove(chapter)
ServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter } MangaServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter }
updateNotification() // Update the notification after cancellation updateNotification() // Update the notification after cancellation
} }
} }
@ -149,7 +158,7 @@ class MangaDownloaderService : Service() {
private fun updateNotification() { private fun updateNotification() {
// Update the notification to reflect the current state of the queue // 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) { val text = if (pendingDownloads > 0) {
"Pending downloads: $pendingDownloads" "Pending downloads: $pendingDownloads"
} else { } else {
@ -167,74 +176,90 @@ class MangaDownloaderService : Service() {
} }
suspend fun download(task: DownloadTask) { suspend fun download(task: DownloadTask) {
withContext(Dispatchers.Main) { try {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { withContext(Dispatchers.Main) {
ContextCompat.checkSelfPermission( val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
this@MangaDownloaderService, ContextCompat.checkSelfPermission(
Manifest.permission.POST_NOTIFICATIONS this@MangaDownloaderService,
) == PackageManager.PERMISSION_GRANTED Manifest.permission.POST_NOTIFICATIONS
} else { ) == PackageManager.PERMISSION_GRANTED
true } else {
} true
val deferredList = mutableListOf<Deferred<Bitmap?>>()
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()
} }
// Download the image and add to deferred list val deferredList = mutableListOf<Deferred<Bitmap?>>()
val deferred = async(Dispatchers.IO) { builder.setContentText("Downloading ${task.title} - ${task.chapter}")
var bitmap: Bitmap? = null if (notifi) {
var retryCount = 0 notificationManager.notify(NOTIFICATION_ID, builder.build())
}
while (bitmap == null && retryCount < task.retries) { // Loop through each ImageData object from the task
bitmap = image.fetchAndProcessImage( var farthest = 0
image.page, for ((index, image) in task.imageData.withIndex()) {
image.source, // Limit the number of simultaneous downloads from the task
this@MangaDownloaderService 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 deferredList.add(deferred)
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) // 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) {
// Wait for any remaining deferred to complete logger("Exception while downloading file: ${e.message}")
deferredList.awaitAll() snackString("Exception while downloading file: ${e.message}")
FirebaseCrashlytics.getInstance().recordException(e)
builder.setContentText("${task.title} - ${task.chapter} Download complete") broadcastDownloadFailed(task.chapter)
.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")
} }
} }
@ -296,33 +321,38 @@ class MangaDownloaderService : Service() {
} }
private suspend fun downloadImage(url: String, directory: File, name: String): String? = withContext(Dispatchers.IO) { private suspend fun downloadImage(url: String, directory: File, name: String): String? =
var connection: HttpURLConnection? = null withContext(Dispatchers.IO) {
println("Downloading url $url") var connection: HttpURLConnection? = null
try { println("Downloading url $url")
connection = URL(url).openConnection() as HttpURLConnection try {
connection.connect() connection = URL(url).openConnection() as HttpURLConnection
if (connection.responseCode != HttpURLConnection.HTTP_OK) { connection.connect()
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}") 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)
} }
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) { private fun broadcastDownloadStarted(chapterNumber: String) {
val intent = Intent(ACTION_DOWNLOAD_STARTED).apply { val intent = Intent(ACTION_DOWNLOAD_STARTED).apply {
@ -381,10 +411,11 @@ class MangaDownloaderService : Service() {
} }
} }
object ServiceDataSingleton { object MangaServiceDataSingleton {
var imageData: List<ImageData> = listOf() var imageData: List<ImageData> = listOf()
var sourceMedia: Media? = null var sourceMedia: Media? = null
var downloadQueue: Queue<MangaDownloaderService.DownloadTask> = ConcurrentLinkedQueue() var downloadQueue: Queue<MangaDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
@Volatile @Volatile
var isServiceRunning: Boolean = false var isServiceRunning: Boolean = false
} }

View file

@ -11,8 +11,15 @@ import androidx.cardview.widget.CardView
import ani.dantotsu.R import ani.dantotsu.R
class OfflineMangaAdapter(private val context: Context, private val items: List<OfflineMangaModel>) : BaseAdapter() { class OfflineMangaAdapter(
private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater private val context: Context,
private var items: List<OfflineMangaModel>,
private val searchListener: OfflineMangaSearchListener
) : BaseAdapter() {
private val inflater: LayoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var originalItems: List<OfflineMangaModel> = items
override fun getCount(): Int { override fun getCount(): Int {
return items.size return items.size
} }
@ -49,4 +56,22 @@ class OfflineMangaAdapter(private val context: Context, private val items: List<
} }
return view 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<OfflineMangaModel>) {
this.items = items
this.originalItems = items
notifyDataSetChanged()
}
} }

View file

@ -7,29 +7,25 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.OvershootInterpolator import android.view.animation.OvershootInterpolator
import android.widget.AutoCompleteTextView
import android.widget.GridView import android.widget.GridView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.view.updatePaddingRelative
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentMangaBinding
import ani.dantotsu.download.Download import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity 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.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.snackString 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.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.firebase.crashlytics.FirebaseCrashlytics 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.GsonBuilder
import com.google.gson.InstanceCreator import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl 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.max
import kotlin.math.min import kotlin.math.min
class OfflineMangaFragment: Fragment() { class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private val downloadManager = Injekt.get<DownloadsManager>() private val downloadManager = Injekt.get<DownloadsManager>()
private var downloads: List<OfflineMangaModel> = listOf() private var downloads: List<OfflineMangaModel> = listOf()
private lateinit var gridView: GridView private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter 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 view = inflater.inflate(R.layout.fragment_manga_offline, container, false)
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar) val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
@ -67,25 +67,44 @@ class OfflineMangaFragment: Fragment() {
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true) requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data val color = typedValue.data
val animeUserAvatar= view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar) val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener { 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) { if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt() textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt()) materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
} }
val searchView = view.findViewById<AutoCompleteTextView>(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) gridView = view.findViewById(R.id.gridView)
getDownloads() getDownloads()
adapter = OfflineMangaAdapter(requireContext(), downloads) adapter = OfflineMangaAdapter(requireContext(), downloads, this)
gridView.adapter = adapter gridView.adapter = adapter
gridView.setOnItemClickListener { parent, view, position, id -> gridView.setOnItemClickListener { parent, view, position, id ->
// Get the OfflineMangaModel that was clicked // Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel 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 { media?.let {
startActivity( startActivity(
Intent(requireContext(), MediaDetailsActivity::class.java) 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 return view
} }
override fun onSearchQuery(query: String) {
adapter.onSearchQuery(query)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
var height = statusBarHeight var height = statusBarHeight
@ -139,9 +186,7 @@ class OfflineMangaFragment: Fragment() {
} }
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
getDownloads() getDownloads()
@ -162,25 +207,44 @@ class OfflineMangaFragment: Fragment() {
super.onStop() super.onStop()
downloads = listOf() downloads = listOf()
} }
private fun getDownloads() { private fun getDownloads() {
val titles = downloadManager.mangaDownloads.map { it.title }.distinct() downloads = listOf()
val newDownloads = mutableListOf<OfflineMangaModel>() val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct()
for (title in titles) { val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val _downloads = downloadManager.mangaDownloads.filter { it.title == title } val _downloads = downloadManager.mangaDownloads.filter { it.title == title }
val download = _downloads.first() val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download) val offlineMangaModel = loadOfflineMangaModel(download)
newDownloads += offlineMangaModel newMangaDownloads += offlineMangaModel
} }
downloads = newDownloads downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
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? { 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( val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}" "Dantotsu/$type/${download.title}"
) )
//load media.json and convert to media class with gson //load media.json and convert to media class with gson
try { return try {
val gson = GsonBuilder() val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> { .registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl SChapterImpl() // Provide an instance of SChapterImpl
@ -188,20 +252,26 @@ class OfflineMangaFragment: Fragment() {
.create() .create()
val media = File(directory, "media.json") val media = File(directory, "media.json")
val mediaJson = media.readText() val mediaJson = media.readText()
return gson.fromJson(mediaJson, Media::class.java) gson.fromJson(mediaJson, Media::class.java)
} } catch (e: Exception) {
catch (e: Exception){
logger("Error loading media.json: ${e.message}") logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace()) logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e) 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( val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}" "Dantotsu/$type/${download.title}"
) )
//load media.json and convert to media class with gson //load media.json and convert to media class with gson
try { try {
@ -214,18 +284,21 @@ class OfflineMangaFragment: Fragment() {
} else { } else {
null null
} }
val title = mediaModel.nameMAL?:"unknown" val title = mediaModel.nameMAL ?: "unknown"
val score = if (mediaModel.userScore != 0) mediaModel.userScore.toString() else val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
if (mediaModel.meanScore == null) "?" else mediaModel.meanScore.toString() ?: 0) else mediaModel.userScore) / 10.0).toString()
val isOngoing = false val isOngoing = false
val isUserScored = mediaModel.userScore != 0 val isUserScored = mediaModel.userScore != 0
return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri) return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri)
} } catch (e: Exception) {
catch (e: Exception){
logger("Error loading media.json: ${e.message}") logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace()) logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e) FirebaseCrashlytics.getInstance().recordException(e)
return OfflineMangaModel("unknown", "0", false, false, null) return OfflineMangaModel("unknown", "0", false, false, null)
} }
} }
}
interface OfflineMangaSearchListener {
fun onSearchQuery(query: String)
} }

View file

@ -2,5 +2,10 @@ package ani.dantotsu.download.manga
import android.net.Uri import android.net.Uri
data class OfflineMangaModel(val title: String, val score: String, val isOngoing: Boolean, val isUserScored: Boolean, val image: Uri?) { data class OfflineMangaModel(
} val title: String,
val score: String,
val isOngoing: Boolean,
val isUserScored: Boolean,
val image: Uri?
)

View file

@ -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<DownloadsManager>()
private val downloadJobs = mutableMapOf<String, Job>()
private val mutex = Mutex()
private var isCurrentlyProcessing = false
val networkHelper = Injekt.get<NetworkHelper>()
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<SChapter> {
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<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
@Volatile
var isServiceRunning: Boolean = false
}

View file

@ -38,9 +38,10 @@ import java.util.concurrent.*
object Helper { object Helper {
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
fun downloadVideo(context : Context, video: Video, subtitle: Subtitle?){ fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
val dataSourceFactory = DataSource.Factory { val dataSourceFactory = DataSource.Factory {
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() val dataSource: HttpDataSource =
OkHttpDataSource.Factory(okHttpClient).createDataSource()
defaultHeaders.forEach { defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value) dataSource.setRequestProperty(it.key, it.value)
} }
@ -52,7 +53,7 @@ object Helper {
val mimeType = when (video.format) { val mimeType = when (video.format) {
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8 VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
VideoType.DASH -> MimeTypes.APPLICATION_MPD VideoType.DASH -> MimeTypes.APPLICATION_MPD
else -> MimeTypes.APPLICATION_MP4 else -> MimeTypes.APPLICATION_MP4
} }
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType) val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
@ -79,12 +80,13 @@ object Helper {
DefaultRenderersFactory(context), DefaultRenderersFactory(context),
dataSourceFactory dataSourceFactory
) )
downloadHelper.prepare(object : DownloadHelper.Callback{ downloadHelper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) { override fun onPrepared(helper: DownloadHelper) {
TrackSelectionDialogBuilder(context,"Select thingy",helper.getTracks(0).groups TrackSelectionDialogBuilder(
context, "Select thingy", helper.getTracks(0).groups
) { _, overrides -> ) { _, overrides ->
val params = TrackSelectionParameters.Builder(context) val params = TrackSelectionParameters.Builder(context)
overrides.forEach{ overrides.forEach {
params.addOverride(it.value) params.addOverride(it.value)
} }
helper.addTrackSelection(0, params.build()) helper.addTrackSelection(0, params.build())
@ -124,7 +126,8 @@ object Helper {
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() //val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
val networkHelper = Injekt.get<NetworkHelper>() val networkHelper = Injekt.get<NetworkHelper>()
val okHttpClient = networkHelper.client val okHttpClient = networkHelper.client
val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() val dataSource: HttpDataSource =
OkHttpDataSource.Factory(okHttpClient).createDataSource()
defaultHeaders.forEach { defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value) dataSource.setRequestProperty(it.key, it.value)
} }
@ -137,7 +140,8 @@ object Helper {
dataSourceFactory, dataSourceFactory,
Executor(Runnable::run) Executor(Runnable::run)
).apply { ).apply {
requirements = Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW) requirements =
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
maxParallelDownloads = 3 maxParallelDownloads = 3
} }
} }

View file

@ -21,7 +21,10 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID) override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification = override fun getForegroundNotification(
downloads: MutableList<Download>,
notMetRequirements: Int
): Notification =
DownloadNotificationHelper(this, "download_service").buildProgressNotification( DownloadNotificationHelper(this, "download_service").buildProgressNotification(
this, this,
R.drawable.mono, R.drawable.mono,

View file

@ -48,7 +48,8 @@ class AnimeFragment : Fragment() {
private var _binding: FragmentAnimeBinding? = null private var _binding: FragmentAnimeBinding? = null
private val binding get() = _binding!! 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() 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()
} }
} }

View file

@ -2,7 +2,6 @@ package ani.dantotsu.home
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.TypedValue import android.util.TypedValue
@ -18,7 +17,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.MediaPageTransformer import ani.dantotsu.MediaPageTransformer
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
@ -27,6 +25,7 @@ import ani.dantotsu.databinding.ItemAnimePageBinding
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.CalendarActivity import ani.dantotsu.media.CalendarActivity
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.SearchActivity import ani.dantotsu.media.SearchActivity
import ani.dantotsu.px import ani.dantotsu.px
@ -45,10 +44,12 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
private var trendHandler: Handler? = null private var trendHandler: Handler? = null
private lateinit var trendRun: Runnable private lateinit var trendRun: Runnable
var trendingViewPager: ViewPager2? = null var trendingViewPager: ViewPager2? = null
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings() private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
val binding = ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return AnimePageViewHolder(binding) return AnimePageViewHolder(binding)
} }
@ -60,14 +61,16 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
val currentColor = textInputLayout.boxBackgroundColor val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt() val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer) val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor) materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue() val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true) currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data 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) { if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt() textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt()) materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
@ -95,7 +98,10 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
} }
binding.animeUserAvatar.setSafeOnClickListener { binding.animeUserAvatar.setSafeOnClickListener {
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.ANIME).show((it.context as AppCompatActivity).supportFragmentManager, "dialog") SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.ANIME).show(
(it.context as AppCompatActivity).supportFragmentManager,
"dialog"
)
} }
listOf( listOf(
@ -125,7 +131,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
) )
} }
binding.animeIncludeList.visibility = if(Anilist.userid!=null) View.VISIBLE else View.GONE binding.animeIncludeList.visibility =
if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked -> binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked ->
onIncludeListClick.invoke(isChecked) onIncludeListClick.invoke(isChecked)
} }
@ -133,9 +140,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
ready.postValue(true) ready.postValue(true)
} }
lateinit var onSeasonClick : ((Int)->Unit) lateinit var onSeasonClick: ((Int) -> Unit)
lateinit var onSeasonLongClick : ((Int)->Boolean) lateinit var onSeasonLongClick: ((Int) -> Boolean)
lateinit var onIncludeListClick : ((Boolean)->Unit) lateinit var onIncludeListClick: ((Boolean) -> Unit)
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
@ -152,7 +159,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
trendHandler = Handler(Looper.getMainLooper()) trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable { trendRun = Runnable {
binding.animeTrendingViewPager.currentItem = binding.animeTrendingViewPager.currentItem + 1 binding.animeTrendingViewPager.currentItem =
binding.animeTrendingViewPager.currentItem + 1
} }
binding.animeTrendingViewPager.registerOnPageChangeCallback( binding.animeTrendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
@ -164,22 +172,30 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
} }
) )
binding.animeTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.animeTrendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings)) binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings))
binding.animeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.animeListContainer.layoutAnimation =
binding.animeSeasonsCont.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.animeSeasonsCont.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
} }
fun updateRecent(adaptor: MediaAdaptor) { fun updateRecent(adaptor: MediaAdaptor) {
binding.animeUpdatedProgressBar.visibility = View.GONE binding.animeUpdatedProgressBar.visibility = View.GONE
binding.animeUpdatedRecyclerView.adapter = adaptor binding.animeUpdatedRecyclerView.adapter = adaptor
binding.animeUpdatedRecyclerView.layoutManager = binding.animeUpdatedRecyclerView.layoutManager =
LinearLayoutManager(binding.animeUpdatedRecyclerView.context, LinearLayoutManager.HORIZONTAL, false) LinearLayoutManager(
binding.animeUpdatedRecyclerView.context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
binding.animeRecently.visibility = View.VISIBLE binding.animeRecently.visibility = View.VISIBLE
binding.animeRecently.startAnimation(setSlideUp(uiSettings)) binding.animeRecently.startAnimation(setSlideUp(uiSettings))
binding.animeUpdatedRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.animeUpdatedRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.animePopular.visibility = View.VISIBLE binding.animePopular.visibility = View.VISIBLE
binding.animePopular.startAnimation(setSlideUp(uiSettings)) binding.animePopular.startAnimation(setSlideUp(uiSettings))
} }
@ -191,5 +207,6 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
} }
} }
inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) : RecyclerView.ViewHolder(binding.root) inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) :
RecyclerView.ViewHolder(binding.root)
} }

View file

@ -31,13 +31,13 @@ import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.media.user.ListActivity import ani.dantotsu.media.user.ListActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn import ani.dantotsu.setSlideIn
import ani.dantotsu.setSlideUp import ani.dantotsu.setSlideUp
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -52,7 +52,11 @@ class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!! 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 = FragmentHomeBinding.inflate(inflater, container, false) _binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -96,10 +100,12 @@ class HomeFragment : Fragment() {
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings)) binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
binding.homeUserDataContainer.visibility = View.VISIBLE binding.homeUserDataContainer.visibility = View.VISIBLE
binding.homeUserDataContainer.layoutAnimation = LayoutAnimationController(setSlideUp(uiSettings), 0.25f) binding.homeUserDataContainer.layoutAnimation =
LayoutAnimationController(setSlideUp(uiSettings), 0.25f)
binding.homeAnimeList.visibility = View.VISIBLE binding.homeAnimeList.visibility = View.VISIBLE
binding.homeMangaList.visibility = View.VISIBLE binding.homeMangaList.visibility = View.VISIBLE
binding.homeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.homeListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
} }
else { else {
snackString(currContext()?.getString(R.string.please_reload)) snackString(currContext()?.getString(R.string.please_reload))
@ -107,7 +113,10 @@ class HomeFragment : Fragment() {
} }
binding.homeUserAvatarContainer.setSafeOnClickListener { binding.homeUserAvatarContainer.setSafeOnClickListener {
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show(parentFragmentManager, "dialog") SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.HOME).show(
parentFragmentManager,
"dialog"
)
} }
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
@ -123,11 +132,13 @@ class HomeFragment : Fragment() {
if (!binding.homeScroll.canScrollVertically(1)) { if (!binding.homeScroll.canScrollVertically(1)) {
reached = true reached = true
bottomBar.animate().translationZ(0f).setDuration(duration).start() 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 { } else {
if (reached) { if (reached) {
bottomBar.animate().translationZ(12f).setDuration(duration).start() 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 != null) {
if (displayCutout.boundingRects.size > 0) { if (displayCutout.boundingRects.size > 0) {
height = 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 false
) )
recyclerView.visibility = View.VISIBLE recyclerView.visibility = View.VISIBLE
recyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
} else { } else {
empty.visibility = View.VISIBLE empty.visibility = View.VISIBLE
@ -313,7 +331,8 @@ class HomeFragment : Fragment() {
live.observe(viewLifecycleOwner) { live.observe(viewLifecycleOwner) {
if (it) { if (it) {
scope.launch { scope.launch {
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings() uiSettings =
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
//Get userData First //Get userData First
getUserId(requireContext()) { getUserId(requireContext()) {

View file

@ -15,7 +15,11 @@ class LoginFragment : Fragment() {
private var _binding: FragmentLoginBinding? = null private var _binding: FragmentLoginBinding? = null
private val binding get() = _binding!! 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) _binding = FragmentLoginBinding.inflate(layoutInflater, container, false)
return binding.root return binding.root
} }

View file

@ -44,11 +44,16 @@ class MangaFragment : Fragment() {
private var _binding: FragmentMangaBinding? = null private var _binding: FragmentMangaBinding? = null
private val binding get() = _binding!! 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() 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) _binding = FragmentMangaBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -100,7 +105,8 @@ class MangaFragment : Fragment() {
} }
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity()) val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
val progressAdaptor = ProgressAdapter(searched = model.searched) val progressAdaptor = ProgressAdapter(searched = model.searched)
binding.mangaPageRecyclerView.adapter = ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor) binding.mangaPageRecyclerView.adapter =
ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
val layout = LinearLayoutManager(requireContext()) val layout = LinearLayoutManager(requireContext())
binding.mangaPageRecyclerView.layoutManager = layout 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()
} }
} }

View file

@ -17,7 +17,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.MediaPageTransformer import ani.dantotsu.MediaPageTransformer
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
@ -25,6 +24,7 @@ import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemMangaPageBinding import ani.dantotsu.databinding.ItemMangaPageBinding
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.SearchActivity import ani.dantotsu.media.SearchActivity
import ani.dantotsu.px import ani.dantotsu.px
@ -43,10 +43,12 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
private var trendHandler: Handler? = null private var trendHandler: Handler? = null
private lateinit var trendRun: Runnable private lateinit var trendRun: Runnable
var trendingViewPager: ViewPager2? = null var trendingViewPager: ViewPager2? = null
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings() private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
val binding = ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MangaPageViewHolder(binding) return MangaPageViewHolder(binding)
} }
@ -58,14 +60,16 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
val currentColor = textInputLayout.boxBackgroundColor val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt() val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView = holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer) val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor) materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue() val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true) currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data 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) { if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt() textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt()) materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
@ -89,7 +93,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
} }
binding.mangaUserAvatar.setSafeOnClickListener { binding.mangaUserAvatar.setSafeOnClickListener {
SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.MANGA).show((it.context as AppCompatActivity).supportFragmentManager, "dialog") SettingsDialogFragment(SettingsDialogFragment.Companion.PageType.MANGA).show(
(it.context as AppCompatActivity).supportFragmentManager,
"dialog"
)
} }
binding.mangaSearchBar.setEndIconOnClickListener { binding.mangaSearchBar.setEndIconOnClickListener {
@ -117,7 +124,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
) )
} }
binding.mangaIncludeList.visibility = if(Anilist.userid!=null) View.VISIBLE else View.GONE binding.mangaIncludeList.visibility =
if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked -> binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked ->
onIncludeListClick.invoke(isChecked) onIncludeListClick.invoke(isChecked)
} }
@ -126,7 +134,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
ready.postValue(true) ready.postValue(true)
} }
lateinit var onIncludeListClick : ((Boolean)->Unit) lateinit var onIncludeListClick: ((Boolean) -> Unit)
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
@ -142,7 +150,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer()) binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper()) trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable { trendRun = Runnable {
binding.mangaTrendingViewPager.currentItem = binding.mangaTrendingViewPager.currentItem + 1 binding.mangaTrendingViewPager.currentItem =
binding.mangaTrendingViewPager.currentItem + 1
} }
binding.mangaTrendingViewPager.registerOnPageChangeCallback( binding.mangaTrendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
@ -154,21 +163,28 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
} }
) )
binding.mangaTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.mangaTrendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings)) binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings))
binding.mangaListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.mangaListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
} }
fun updateNovel(adaptor: MediaAdaptor) { fun updateNovel(adaptor: MediaAdaptor) {
binding.mangaNovelProgressBar.visibility = View.GONE binding.mangaNovelProgressBar.visibility = View.GONE
binding.mangaNovelRecyclerView.adapter = adaptor binding.mangaNovelRecyclerView.adapter = adaptor
binding.mangaNovelRecyclerView.layoutManager = binding.mangaNovelRecyclerView.layoutManager =
LinearLayoutManager(binding.mangaNovelRecyclerView.context, LinearLayoutManager.HORIZONTAL, false) LinearLayoutManager(
binding.mangaNovelRecyclerView.context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.mangaNovelRecyclerView.visibility = View.VISIBLE binding.mangaNovelRecyclerView.visibility = View.VISIBLE
binding.mangaNovel.visibility = View.VISIBLE binding.mangaNovel.visibility = View.VISIBLE
binding.mangaNovel.startAnimation(setSlideUp(uiSettings)) binding.mangaNovel.startAnimation(setSlideUp(uiSettings))
binding.mangaNovelRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.mangaNovelRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.mangaPopular.visibility = View.VISIBLE binding.mangaPopular.visibility = View.VISIBLE
binding.mangaPopular.startAnimation(setSlideUp(uiSettings)) binding.mangaPopular.startAnimation(setSlideUp(uiSettings))
} }
@ -180,5 +196,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
} }
} }
inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) : RecyclerView.ViewHolder(binding.root) inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) :
RecyclerView.ViewHolder(binding.root)
} }

View file

@ -16,20 +16,16 @@ import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.ZoomOutPageTransformer import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ActivityNoInternetBinding import ani.dantotsu.databinding.ActivityNoInternetBinding
import ani.dantotsu.download.manga.OfflineMangaFragment import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.isOnline
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.offline.OfflineFragment import ani.dantotsu.offline.OfflineFragment
import ani.dantotsu.others.LangSet
import ani.dantotsu.selectedOption import ani.dantotsu.selectedOption
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.startMainActivity
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
class NoInternet : AppCompatActivity() { class NoInternet : AppCompatActivity() {
@ -39,7 +35,7 @@ class NoInternet : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityNoInternetBinding.inflate(layoutInflater) binding = ActivityNoInternetBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View file

@ -12,11 +12,17 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.* import ani.dantotsu.EmptyAdapter
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityAuthorBinding import ani.dantotsu.databinding.ActivityAuthorBinding
import ani.dantotsu.others.getSerialized import ani.dantotsu.initActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized
import ani.dantotsu.px
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -31,7 +37,7 @@ class AuthorActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityAuthorBinding.inflate(layoutInflater) binding = ActivityAuthorBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View file

@ -16,9 +16,9 @@ import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.media.user.ListViewPagerAdapter import ani.dantotsu.media.user.ListViewPagerAdapter
import ani.dantotsu.others.LangSet
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -35,7 +35,7 @@ class CalendarActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityListBinding.inflate(layoutInflater) binding = ActivityListBinding.inflate(layoutInflater)
@ -43,7 +43,11 @@ ThemeManager(this).applyTheme()
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true) theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
val primaryColor = typedValue.data val primaryColor = typedValue.data
val typedValue2 = TypedValue() val typedValue2 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue2, true) theme.resolveAttribute(
com.google.android.material.R.attr.colorOnBackground,
typedValue2,
true
)
val titleTextColor = typedValue2.data val titleTextColor = typedValue2.data
val typedValue3 = TypedValue() val typedValue3 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true) theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
@ -56,7 +60,7 @@ ThemeManager(this).applyTheme()
window.navigationBarColor = primaryColor window.navigationBarColor = primaryColor
binding.listTabLayout.setBackgroundColor(primaryColor) binding.listTabLayout.setBackgroundColor(primaryColor)
binding.listAppBar.setBackgroundColor(primaryColor) binding.listAppBar.setBackgroundColor(primaryColor)
binding.listTitle.setTextColor(titleTextColor) binding.listTitle.setTextColor(primaryTextColor)
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor) binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor) binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings() val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
@ -65,10 +69,13 @@ ThemeManager(this).applyTheme()
ContextCompat.getColor(this, R.color.nav_bg_inv) ContextCompat.getColor(this, R.color.nav_bg_inv)
binding.root.fitsSystemWindows = true binding.root.fitsSystemWindows = true
}else{ } else {
binding.root.fitsSystemWindows = false binding.root.fitsSystemWindows = false
requestWindowFeature(Window.FEATURE_NO_TITLE) 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) setContentView(binding.root)
@ -79,14 +86,15 @@ ThemeManager(this).applyTheme()
override fun onTabSelected(tab: TabLayout.Tab?) { override fun onTabSelected(tab: TabLayout.Tab?) {
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1 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) { model.getCalendar().observe(this) {
if (it != null) { if (it != null) {
binding.listProgressBar.visibility = View.GONE 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 keys = it.keys.toList()
val values = it.values.toList() val values = it.values.toList()
val savedTab = this.selectedTabIdx val savedTab = this.selectedTabIdx

View file

@ -21,11 +21,13 @@ class CharacterAdapter(
private val characterList: ArrayList<Character> private val characterList: ArrayList<Character>
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() { ) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder { 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) return CharacterViewHolder(binding)
} }
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings() private val uiSettings =
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) { override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
@ -38,16 +40,23 @@ class CharacterAdapter(
} }
override fun getItemCount(): Int = characterList.size 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 { init {
itemView.setOnClickListener { itemView.setOnClickListener {
val char = characterList[bindingAdapterPosition] val char = characterList[bindingAdapterPosition]
ContextCompat.startActivity( ContextCompat.startActivity(
itemView.context, 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( ActivityOptionsCompat.makeSceneTransitionAnimation(
itemView.context as Activity, itemView.context as Activity,
Pair.create(binding.itemCompactImage, ViewCompat.getTransitionName(binding.itemCompactImage)!!), Pair.create(
binding.itemCompactImage,
ViewCompat.getTransitionName(binding.itemCompactImage)!!
),
).toBundle() ).toBundle()
) )
} }

View file

@ -13,13 +13,20 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.* import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityCharacterBinding 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.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.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 com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -36,15 +43,17 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityCharacterBinding.inflate(layoutInflater) binding = ActivityCharacterBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initActivity(this) initActivity(this)
screenWidth = resources.displayMetrics.run { widthPixels / density } 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 } banner.updateLayoutParams { height += statusBarHeight }
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
@ -61,7 +70,13 @@ ThemeManager(this).applyTheme()
binding.characterTitle.text = character.name binding.characterTitle.text = character.name
banner.loadImage(character.banner) banner.loadImage(character.banner)
binding.characterCoverImage.loadImage(character.image) 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) { model.getCharacter().observe(this) {
if (it != null && !loaded) { if (it != null && !loaded) {
@ -73,14 +88,15 @@ ThemeManager(this).applyTheme()
val roles = character.roles val roles = character.roles
if (roles != null) { if (roles != null) {
val mediaAdaptor = MediaAdaptor(0, roles, this, matchParent = true) 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 gridSize = (screenWidth / 124f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize) val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int { override fun getSpanSize(position: Int): Int {
return when (position) { return when (position) {
0 -> gridSize 0 -> gridSize
else -> 1 else -> 1
} }
} }
@ -118,16 +134,19 @@ ThemeManager(this).applyTheme()
binding.characterCover.scaleY = 1f * cap binding.characterCover.scaleY = 1f * cap
binding.characterCover.cardElevation = 32f * 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) { if (percentage >= percent && !isCollapsed) {
isCollapsed = true 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) binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
} }
if (percentage <= percent && isCollapsed) { if (percentage <= percent && isCollapsed) {
isCollapsed = false 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) binding.characterAppBar.setBackgroundResource(R.color.bg)
} }
} }

View file

@ -15,7 +15,8 @@ import io.noties.markwon.SoftBreakAddsNewLinePlugin
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) : class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() { RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder { 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) return GenreViewHolder(binding)
} }
@ -23,20 +24,22 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) { override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
val desc = 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.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) "Male" -> currActivity()!!.getString(R.string.male)
"Female" -> currActivity()!!.getString(R.string.female) "Female" -> currActivity()!!.getString(R.string.female)
else -> character.gender else -> character.gender
} else "") + "\n" + character.description } else "") + "\n" + character.description
binding.characterDesc.isTextSelectable 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) markWon.setMarkdown(binding.characterDesc, desc)
} }
override fun getItemCount(): Int = 1 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)
} }

View file

@ -14,9 +14,9 @@ import ani.dantotsu.databinding.ActivityGenreBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -28,7 +28,7 @@ class GenreActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityGenreBinding.inflate(layoutInflater) binding = ActivityGenreBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initActivity(this) initActivity(this)
@ -50,7 +50,8 @@ ThemeManager(this).applyTheme()
model.doneListener?.invoke() model.doneListener?.invoke()
} }
binding.mediaInfoGenresRecyclerView.adapter = adapter binding.mediaInfoGenresRecyclerView.adapter = adapter
binding.mediaInfoGenresRecyclerView.layoutManager = GridLayoutManager(this, (screenWidth / 156f).toInt()) binding.mediaInfoGenresRecyclerView.layoutManager =
GridLayoutManager(this, (screenWidth / 156f).toInt())
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) { model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) {

View file

@ -37,7 +37,8 @@ class GenreAdapter(
} }
override fun getItemCount(): Int = genres.size 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 { init {
itemView.setOnClickListener { itemView.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
@ -48,15 +49,15 @@ class GenreAdapter(
.putExtra("sortBy", Anilist.sortBy[2]) .putExtra("sortBy", Anilist.sortBy[2])
.putExtra("search", true) .putExtra("search", true)
.also { .also {
if (pos[bindingAdapterPosition].lowercase() == "hentai") { if (pos[bindingAdapterPosition].lowercase() == "hentai") {
if (!Anilist.adult) Toast.makeText( if (!Anilist.adult) Toast.makeText(
itemView.context, itemView.context,
currActivity()?.getString(R.string.content_18), currActivity()?.getString(R.string.content_18),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
it.putExtra("hentai", true) it.putExtra("hentai", true)
} }
}, },
null null
) )
} }

View file

@ -1,5 +1,6 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.graphics.Bitmap
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.MediaEdge import ani.dantotsu.connections.anilist.api.MediaEdge
import ani.dantotsu.connections.anilist.api.MediaList import ani.dantotsu.connections.anilist.api.MediaList
@ -40,7 +41,7 @@ data class Media(
var userUpdatedAt: Long? = null, var userUpdatedAt: Long? = null,
var userStartedAt: FuzzyDate = FuzzyDate(), var userStartedAt: FuzzyDate = FuzzyDate(),
var userCompletedAt: FuzzyDate = FuzzyDate(), var userCompletedAt: FuzzyDate = FuzzyDate(),
var inCustomListsOf: MutableMap<String, Boolean>?= null, var inCustomListsOf: MutableMap<String, Boolean>? = null,
var userFavOrder: Int? = null, var userFavOrder: Int? = null,
val status: String? = null, val status: String? = null,
@ -69,7 +70,7 @@ data class Media(
var shareLink: String? = null, var shareLink: String? = null,
var selected: Selected? = null, var selected: Selected? = null,
var idKitsu: String?=null, var idKitsu: String? = null,
var cameFromContinue: Boolean = false var cameFromContinue: Boolean = false
) : Serializable { ) : Serializable {
@ -119,4 +120,5 @@ data class Media(
object MediaSingleton { object MediaSingleton {
var media: Media? = null var media: Media? = null
var bitmap: Bitmap? = null
} }

View file

@ -4,10 +4,14 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
@ -37,20 +41,43 @@ class MediaAdaptor(
private val viewPager: ViewPager2? = null, private val viewPager: ViewPager2? = null,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings() private val uiSettings =
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (type) { return when (type) {
0 -> MediaViewHolder(ItemMediaCompactBinding.inflate(LayoutInflater.from(parent.context), parent, false)) 0 -> MediaViewHolder(
1 -> MediaLargeViewHolder(ItemMediaLargeBinding.inflate(LayoutInflater.from(parent.context), parent, false)) ItemMediaCompactBinding.inflate(
2 -> MediaPageViewHolder(ItemMediaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)) LayoutInflater.from(parent.context),
3 -> MediaPageSmallViewHolder( 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( ItemMediaPageSmallBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
false false
) )
) )
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
@ -65,10 +92,12 @@ class MediaAdaptor(
val media = mediaList?.getOrNull(position) val media = mediaList?.getOrNull(position)
if (media != null) { if (media != null) {
b.itemCompactImage.loadImage(media.cover) 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.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = 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.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context, b.root.context,
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score) (if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
@ -100,6 +129,7 @@ class MediaAdaptor(
} }
} }
} }
1 -> { 1 -> {
val b = (holder as MediaLargeViewHolder).binding val b = (holder as MediaLargeViewHolder).binding
setAnimation(activity, b.root, uiSettings) setAnimation(activity, b.root, uiSettings)
@ -107,22 +137,29 @@ class MediaAdaptor(
if (media != null) { if (media != null) {
b.itemCompactImage.loadImage(media.cover) b.itemCompactImage.loadImage(media.cover)
b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400) 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.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = 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.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context, b.root.context,
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score) (if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
) )
if (media.anime != null) { 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
else currActivity()!!.getString(R.string.episode_singular) ?: 0) != 1
) currActivity()!!.getString(R.string.episode_plural)
else currActivity()!!.getString(R.string.episode_singular)
b.itemCompactTotal.text = b.itemCompactTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes 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) { } 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) else currActivity()!!.getString(R.string.chapter_singular)
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}" b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
} }
@ -133,6 +170,7 @@ class MediaAdaptor(
} }
} }
} }
2 -> { 2 -> {
val b = (holder as MediaPageViewHolder).binding val b = (holder as MediaPageViewHolder).binding
val media = mediaList?.get(position) val media = mediaList?.get(position)
@ -145,7 +183,8 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator() 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 val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed) if (!(context as Activity).isDestroyed)
Glide.with(context as Context) Glide.with(context as Context)
@ -153,22 +192,29 @@ class MediaAdaptor(
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400) .diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3))) .apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner) .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.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = 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.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context, b.root.context,
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score) (if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
) )
if (media.anime != null) { 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) else currActivity()!!.getString(R.string.episode_singular)
b.itemCompactTotal.text = b.itemCompactTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes 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) { } 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) else currActivity()!!.getString(R.string.chapter_singular)
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}" b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
} }
@ -180,6 +226,7 @@ class MediaAdaptor(
} }
} }
} }
3 -> { 3 -> {
val b = (holder as MediaPageSmallViewHolder).binding val b = (holder as MediaPageSmallViewHolder).binding
val media = mediaList?.get(position) val media = mediaList?.get(position)
@ -192,7 +239,8 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator() 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 val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed) if (!(context as Activity).isDestroyed)
Glide.with(context as Context) Glide.with(context as Context)
@ -200,10 +248,12 @@ class MediaAdaptor(
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400) .diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3))) .apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner) .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.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = 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.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context, b.root.context,
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score) (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 ?: "" b.itemCompactStatus.text = media.status ?: ""
if (media.anime != null) { 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) else currActivity()!!.getString(R.string.episode_singular)
b.itemCompactTotal.text = b.itemCompactTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes 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) { } 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) else currActivity()!!.getString(R.string.chapter_singular)
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}" b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
} }
@ -245,43 +300,73 @@ class MediaAdaptor(
return type return type
} }
inner class MediaViewHolder(val binding: ItemMediaCompactBinding) : RecyclerView.ViewHolder(binding.root) { inner class MediaViewHolder(val binding: ItemMediaCompactBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
if (matchParent) itemView.updateLayoutParams { width = -1 } if (matchParent) itemView.updateLayoutParams { width = -1 }
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) } itemView.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) } 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 { init {
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) } itemView.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) } itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
} }
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) : RecyclerView.ViewHolder(binding.root) { inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) } binding.itemCompactImage.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
itemView.setOnTouchListener { _, _ -> true } itemView.setOnTouchListener { _, _ -> true }
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) } binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
} }
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) : RecyclerView.ViewHolder(binding.root) { inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) } binding.itemCompactImage.setSafeOnClickListener {
binding.itemCompactTitleContainer.setSafeOnClickListener { clicked(bindingAdapterPosition) } clicked(
bindingAdapterPosition,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
binding.itemCompactTitleContainer.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
itemView.setOnTouchListener { _, _ -> true } itemView.setOnTouchListener { _, _ -> true }
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) } binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
} }
} }
fun clicked(position: Int) { fun clicked(position: Int, bitmap: Bitmap? = null) {
if ((mediaList?.size ?: 0) > position && position != -1) { if ((mediaList?.size ?: 0) > position && position != -1) {
val media = mediaList?.get(position) val media = mediaList?.get(position)
if (bitmap != null) MediaSingleton.bitmap = bitmap
ContextCompat.startActivity( ContextCompat.startActivity(
activity, activity,
Intent(activity, MediaDetailsActivity::class.java).putExtra( Intent(activity, MediaDetailsActivity::class.java).putExtra(
@ -296,10 +381,53 @@ class MediaAdaptor(
if ((mediaList?.size ?: 0) > position && position != -1) { if ((mediaList?.size ?: 0) > position && position != -1) {
val media = mediaList?.get(position) ?: return false val media = mediaList?.get(position) ?: return false
if (activity.supportFragmentManager.findFragmentByTag("list") == null) { if (activity.supportFragmentManager.findFragmentByTag("list") == null) {
MediaListDialogSmallFragment.newInstance(media).show(activity.supportFragmentManager, "list") MediaListDialogSmallFragment.newInstance(media)
.show(activity.supportFragmentManager, "list")
return true return true
} }
} }
return false 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)
}
} }

View file

@ -31,24 +31,24 @@ import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.ZoomOutPageTransformer import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.copyToClipboard import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ActivityMediaBinding import ani.dantotsu.databinding.ActivityMediaBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.media.manga.MangaReadFragment import ani.dantotsu.media.manga.MangaReadFragment
import ani.dantotsu.navBarHeight
import ani.dantotsu.media.novel.NovelReadFragment import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.saveData import ani.dantotsu.saveData
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.flaviofaria.kenburnsview.RandomTransitionGenerator import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
@ -72,9 +72,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("SetTextI18n", "ClickableViewAccessibility") @SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LangSet.setLocale(this) 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) binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat() screenWidth = resources.displayMetrics.widthPixels.toFloat()
@ -119,7 +121,7 @@ ThemeManager(this).applyTheme()
viewPager.isUserInputEnabled = false viewPager.isUserInputEnabled = false
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings)) viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
var media: Media = intent.getSerialized("media") ?: return
val isDownload = intent.getBooleanExtra("download", false) val isDownload = intent.getBooleanExtra("download", false)
media.selected = model.loadSelected(media, isDownload) media.selected = model.loadSelected(media, isDownload)

View file

@ -1,28 +1,29 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Environment
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import ani.dantotsu.FileUrl import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.media.anime.Episode import ani.dantotsu.currContext
import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.logger 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.media.manga.MangaChapter
import ani.dantotsu.others.AniSkip import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.Jikan import ani.dantotsu.others.Jikan
import ani.dantotsu.others.Kitsu import ani.dantotsu.others.Kitsu
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.Book import ani.dantotsu.parsers.Book
import ani.dantotsu.parsers.MangaImage import ani.dantotsu.parsers.MangaImage
import ani.dantotsu.parsers.MangaReadSources import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.parsers.NovelSources import ani.dantotsu.parsers.NovelSources
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoExtractor
@ -30,25 +31,12 @@ import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.saveData import ani.dantotsu.saveData
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend 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 com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
class MediaDetailsViewModel : ViewModel() { class MediaDetailsViewModel : ViewModel() {
val scrolledToTop = MutableLiveData(true) val scrolledToTop = MutableLiveData(true)
@ -62,17 +50,20 @@ class MediaDetailsViewModel : ViewModel() {
val sharedPreferences = Injekt.get<SharedPreferences>() val sharedPreferences = Injekt.get<SharedPreferences>()
val data = loadData<Selected>("${media.id}-select") ?: Selected().let { val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) { it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
true ->sharedPreferences.getInt("settings_def_anime_source_s_r", 0) true -> sharedPreferences.getInt("settings_def_anime_source_s_r", 0)
else ->sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0) else -> sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0)
} }
it.preferDub = loadData("settings_prefer_dub") ?: false it.preferDub = loadData("settings_prefer_dub") ?: false
saveSelected(media.id, it) saveSelected(media.id, it)
it it
} }
if (isDownload) { if (isDownload) {
data.sourceIndex = when (media.anime != null) { data.sourceIndex = if (media.anime != null) {
true -> AnimeSources.list.size - 1 AnimeSources.list.size - 1
else -> MangaSources.list.size - 1 } else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
MangaSources.list.size - 1
} else {
NovelSources.list.size - 1
} }
} }
return data return data
@ -81,7 +72,9 @@ class MediaDetailsViewModel : ViewModel() {
fun loadSelectedStringLocation(sourceName: String): Int { fun loadSelectedStringLocation(sourceName: String): Int {
//find the location of the source in the list //find the location of the source in the list
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0 var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
if (location == -1) {location = 0} if (location == -1) {
location = 0
}
return location return location
} }
@ -106,7 +99,9 @@ class MediaDetailsViewModel : ViewModel() {
//Anime //Anime
private val kitsuEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null) private val kitsuEpisodes: MutableLiveData<Map<String, Episode>> =
MutableLiveData<Map<String, Episode>>(null)
fun getKitsuEpisodes(): LiveData<Map<String, Episode>> = kitsuEpisodes fun getKitsuEpisodes(): LiveData<Map<String, Episode>> = kitsuEpisodes
suspend fun loadKitsuEpisodes(s: Media) { suspend fun loadKitsuEpisodes(s: Media) {
tryWithSuspend { tryWithSuspend {
@ -114,7 +109,9 @@ class MediaDetailsViewModel : ViewModel() {
} }
} }
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null) private val fillerEpisodes: MutableLiveData<Map<String, Episode>> =
MutableLiveData<Map<String, Episode>>(null)
fun getFillerEpisodes(): LiveData<Map<String, Episode>> = fillerEpisodes fun getFillerEpisodes(): LiveData<Map<String, Episode>> = fillerEpisodes
suspend fun loadFillerEpisodes(s: Media) { suspend fun loadFillerEpisodes(s: Media) {
tryWithSuspend { tryWithSuspend {
@ -145,7 +142,8 @@ class MediaDetailsViewModel : ViewModel() {
suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) { suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) {
watchSources?.saveResponse(i, id, source) watchSources?.saveResponse(i, id, source)
epsLoaded[i] = watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return epsLoaded[i] =
watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return
episodes.postValue(epsLoaded) episodes.postValue(epsLoaded)
} }
@ -184,7 +182,12 @@ class MediaDetailsViewModel : ViewModel() {
val timeStamps = MutableLiveData<List<AniSkip.Stamp>?>() val timeStamps = MutableLiveData<List<AniSkip.Stamp>?>()
private val timeStampsMap: MutableMap<Int, List<AniSkip.Stamp>?> = mutableMapOf() private val timeStampsMap: MutableMap<Int, List<AniSkip.Stamp>?> = mutableMapOf()
suspend fun loadTimeStamps(malId: Int?, episodeNum: Int?, duration: Long, useProxyForTimeStamps: Boolean) { suspend fun loadTimeStamps(
malId: Int?,
episodeNum: Int?,
duration: Long,
useProxyForTimeStamps: Boolean
) {
malId ?: return malId ?: return
episodeNum ?: return episodeNum ?: return
if (timeStampsMap.containsKey(episodeNum)) if (timeStampsMap.containsKey(episodeNum))
@ -194,7 +197,11 @@ class MediaDetailsViewModel : ViewModel() {
timeStamps.postValue(result) timeStamps.postValue(result)
} }
suspend fun loadEpisodeSingleVideo(ep: Episode, selected: Selected, post: Boolean = true): Boolean { suspend fun loadEpisodeSingleVideo(
ep: Episode,
selected: Selected,
post: Boolean = true
): Boolean {
if (ep.extractors.isNullOrEmpty()) { if (ep.extractors.isNullOrEmpty()) {
val server = selected.server ?: return false val server = selected.server ?: return false
@ -204,8 +211,10 @@ class MediaDetailsViewModel : ViewModel() {
selected.sourceIndex = selected.sourceIndex selected.sourceIndex = selected.sourceIndex
if (!post && !it.allowsPreloading) null if (!post && !it.allowsPreloading) null
else ep.sEpisode?.let { it1 -> else ep.sEpisode?.let { it1 ->
it.loadSingleVideoServer(server, link, ep.extra, it.loadSingleVideoServer(
it1, post) server, link, ep.extra,
it1, post
)
} }
} ?: return false) } ?: return false)
ep.allStreams = false ep.allStreams = false
@ -228,7 +237,13 @@ class MediaDetailsViewModel : ViewModel() {
} }
val epChanged = MutableLiveData(true) val epChanged = MutableLiveData(true)
fun onEpisodeClick(media: Media, i: String, manager: FragmentManager, launch: Boolean = true, prevEp: String? = null) { fun onEpisodeClick(
media: Media,
i: String,
manager: FragmentManager,
launch: Boolean = true,
prevEp: String? = null
) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) { if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
if (media.anime?.episodes?.get(i) != null) { if (media.anime?.episodes?.get(i) != null) {
@ -238,7 +253,8 @@ class MediaDetailsViewModel : ViewModel() {
return@post return@post
} }
media.selected = this.loadSelected(media) media.selected = this.loadSelected(media)
val selector = SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp) val selector =
SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp)
selector.show(manager, "dialog") selector.show(manager, "dialog")
} }
} }
@ -248,13 +264,17 @@ class MediaDetailsViewModel : ViewModel() {
//Manga //Manga
var mangaReadSources: MangaReadSources? = null var mangaReadSources: MangaReadSources? = null
private val mangaChapters = MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null) private val mangaChapters =
MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>() private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> = mangaChapters fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> =
mangaChapters
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) { suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
logger("Loading Manga Chapters : $mangaLoaded") logger("Loading Manga Chapters : $mangaLoaded")
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend { if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend mangaLoaded[i] =
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
} }
mangaChapters.postValue(mangaLoaded) mangaChapters.postValue(mangaLoaded)
} }
@ -269,11 +289,17 @@ class MediaDetailsViewModel : ViewModel() {
private val mangaChapter = MutableLiveData<MangaChapter?>(null) private val mangaChapter = MutableLiveData<MangaChapter?>(null)
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, series: String, post: Boolean = true): Boolean { suspend fun loadMangaChapterImages(
chapter: MangaChapter,
selected: Selected,
series: String,
post: Boolean = true
): Boolean {
return tryWithSuspend(true) { return tryWithSuspend(true) {
chapter.addImages( chapter.addImages(
mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false mangaReadSources?.get(selected.sourceIndex)
?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
) )
if (post) mangaChapter.postValue(chapter) if (post) mangaChapter.postValue(chapter)
true true
@ -281,7 +307,8 @@ class MediaDetailsViewModel : ViewModel() {
} }
fun loadTransformation(mangaImage: MangaImage, source: Int): BitmapTransformation? { fun loadTransformation(mangaImage: MangaImage, source: Int): BitmapTransformation? {
return if (mangaImage.useTransformation) mangaReadSources?.get(source)?.getTransformation() else null return if (mangaImage.useTransformation) mangaReadSources?.get(source)
?.getTransformation() else null
} }
val novelSources = NovelSources val novelSources = NovelSources
@ -296,7 +323,7 @@ class MediaDetailsViewModel : ViewModel() {
} }
suspend fun autoSearchNovels(media: Media) { suspend fun autoSearchNovels(media: Media) {
val source = novelSources[media.selected?.sourceIndex?:0] val source = novelSources[media.selected?.sourceIndex ?: 0]
tryWithSuspend(post = true) { tryWithSuspend(post = true) {
if (source != null) { if (source != null) {
novelResponses.postValue(source.sortedSearch(media)) novelResponses.postValue(source.sortedSearch(media))
@ -307,7 +334,9 @@ class MediaDetailsViewModel : ViewModel() {
val book: MutableLiveData<Book> = MutableLiveData(null) val book: MutableLiveData<Book> = MutableLiveData(null)
suspend fun loadBook(novel: ShowResponse, i: Int) { suspend fun loadBook(novel: ShowResponse, i: Int) {
tryWithSuspend { tryWithSuspend {
book.postValue(novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend) book.postValue(
novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend
)
} }
} }

View file

@ -43,7 +43,11 @@ class MediaInfoFragment : Fragment() {
private var type = "ANIME" private var type = "ANIME"
private val genreModel: GenresViewModel by activityViewModels() private val genreModel: GenresViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentMediaInfoBinding.inflate(inflater, container, false) _binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -59,8 +63,8 @@ class MediaInfoFragment : Fragment() {
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight } binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
model.scrolledToTop.observe(viewLifecycleOwner){ model.scrolledToTop.observe(viewLifecycleOwner) {
if(it) binding.mediaInfoScroll.scrollTo(0,0) if (it) binding.mediaInfoScroll.scrollTo(0, 0)
} }
model.getMedia().observe(viewLifecycleOwner) { media -> model.getMedia().observe(viewLifecycleOwner) { media ->
@ -68,30 +72,32 @@ class MediaInfoFragment : Fragment() {
loaded = true loaded = true
binding.mediaInfoProgressBar.visibility = View.GONE binding.mediaInfoProgressBar.visibility = View.GONE
binding.mediaInfoContainer.visibility = View.VISIBLE binding.mediaInfoContainer.visibility = View.VISIBLE
binding.mediaInfoName.text = "\t\t\t" + (media.name?:media.nameRomaji) binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
binding.mediaInfoName.setOnLongClickListener { binding.mediaInfoName.setOnLongClickListener {
copyToClipboard(media.name?:media.nameRomaji) copyToClipboard(media.name ?: media.nameRomaji)
true true
} }
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility = View.VISIBLE if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
View.VISIBLE
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
binding.mediaInfoNameRomaji.setOnLongClickListener { binding.mediaInfoNameRomaji.setOnLongClickListener {
copyToClipboard(media.nameRomaji) copyToClipboard(media.nameRomaji)
true true
} }
binding.mediaInfoMeanScore.text = if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??" binding.mediaInfoMeanScore.text =
if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??"
binding.mediaInfoStatus.text = media.status binding.mediaInfoStatus.text = media.status
binding.mediaInfoFormat.text = media.format binding.mediaInfoFormat.text = media.format
binding.mediaInfoSource.text = media.source binding.mediaInfoSource.text = media.source
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??" binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
binding.mediaInfoEnd.text =media.endDate?.toString() ?: "??" binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
if (media.anime != null) { if (media.anime != null) {
binding.mediaInfoDuration.text = binding.mediaInfoDuration.text =
if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??" if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??"
binding.mediaInfoDurationContainer.visibility = View.VISIBLE binding.mediaInfoDurationContainer.visibility = View.VISIBLE
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
binding.mediaInfoSeason.text = binding.mediaInfoSeason.text =
(media.anime.season ?: "??")+ " " + (media.anime.seasonYear ?: "??") (media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??")
if (media.anime.mainStudio != null) { if (media.anime.mainStudio != null) {
binding.mediaInfoStudioContainer.visibility = View.VISIBLE binding.mediaInfoStudioContainer.visibility = View.VISIBLE
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
@ -246,7 +252,12 @@ class MediaInfoFragment : Fragment() {
val end = a.indexOf('"', first).let { if (it != -1) it else return a } val end = a.indexOf('"', first).let { if (it != -1) it else return a }
val name = a.subSequence(first, end).toString() val name = a.subSequence(first, end).toString()
return "${a.subSequence(0, first)}" + return "${a.subSequence(0, first)}" +
"[$name](https://www.youtube.com/results?search_query=${URLEncoder.encode(name, "utf-8")})" + "[$name](https://www.youtube.com/results?search_query=${
URLEncoder.encode(
name,
"utf-8"
)
})" +
"${a.subSequence(end, a.length)}" "${a.subSequence(end, a.length)}"
} }
@ -270,7 +281,11 @@ class MediaInfoFragment : Fragment() {
} }
if (media.anime.op.isNotEmpty()) { if (media.anime.op.isNotEmpty()) {
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false) val bind = ItemTitleTextBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.opening) bind.itemTitle.setText(R.string.opening)
makeText(bind.itemText, media.anime.op) makeText(bind.itemText, media.anime.op)
parent.addView(bind.root) parent.addView(bind.root)
@ -278,7 +293,11 @@ class MediaInfoFragment : Fragment() {
if (media.anime.ed.isNotEmpty()) { if (media.anime.ed.isNotEmpty()) {
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false) val bind = ItemTitleTextBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.ending) bind.itemTitle.setText(R.string.ending)
makeText(bind.itemText, media.anime.ed) makeText(bind.itemText, media.anime.ed)
parent.addView(bind.root) parent.addView(bind.root)
@ -458,7 +477,8 @@ class MediaInfoFragment : Fragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cornerTop = ObjectAnimator.ofFloat(binding.root, "radius", 0f, 32f).setDuration(200) val cornerTop = ObjectAnimator.ofFloat(binding.root, "radius", 0f, 32f).setDuration(200)
val cornerNotTop = ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200) val cornerNotTop =
ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200)
var cornered = true var cornered = true
cornerTop.start() cornerTop.start()
binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ -> binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ ->

View file

@ -14,8 +14,8 @@ import androidx.lifecycle.lifecycleScope
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.databinding.BottomSheetMediaListBinding
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListBinding
import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.switchmaterial.SwitchMaterial
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -27,7 +27,11 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetMediaListBinding? = null private var _binding: BottomSheetMediaListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetMediaListBinding.inflate(inflater, container, false) _binding = BottomSheetMediaListBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -46,9 +50,13 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
binding.mediaListLayout.visibility = View.VISIBLE binding.mediaListLayout.visibility = View.VISIBLE
val statuses: Array<String> = resources.getStringArray(R.array.status) val statuses: Array<String> = resources.getStringArray(R.array.status)
val statusStrings = if (media?.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga) val statusStrings =
val userStatus = if(media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0] if (media?.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
R.array.status_manga
)
val userStatus =
if (media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0]
binding.mediaListStatus.setText(userStatus) binding.mediaListStatus.setText(userStatus)
binding.mediaListStatus.setAdapter( binding.mediaListStatus.setAdapter(
ArrayAdapter( ArrayAdapter(
@ -160,7 +168,9 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
val init = val init =
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString() if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
.toInt() else 0 .toInt() else 0
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString()) if (init < (total
?: 5000)
) binding.mediaListProgress.setText((init + 1).toString())
if (init + 1 == (total ?: 5000)) { if (init + 1 == (total ?: 5000)) {
binding.mediaListStatus.setText(statusStrings[2], false) binding.mediaListStatus.setText(statusStrings[2], false)
onComplete() onComplete()
@ -201,11 +211,15 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
scope.launch { scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (media != null) { if (media != null) {
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull() val progress =
_binding?.mediaListProgress?.text.toString().toIntOrNull()
val score = val score =
(_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt() (_binding?.mediaListScore?.text.toString().toDoubleOrNull()
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())] ?.times(10))?.toInt()
val rewatch = _binding?.mediaListRewatch?.text?.toString()?.toIntOrNull() val status =
statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
val rewatch =
_binding?.mediaListRewatch?.text?.toString()?.toIntOrNull()
val notes = _binding?.mediaListNotes?.text?.toString() val notes = _binding?.mediaListNotes?.text?.toString()
val startD = start.date val startD = start.date
val endD = end.date val endD = end.date
@ -245,7 +259,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
scope.launch { scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Anilist.mutation.deleteList(id) Anilist.mutation.deleteList(id)
MAL.query.deleteList(media?.anime!=null,media?.idMAL) MAL.query.deleteList(media?.anime != null, media?.idMAL)
} }
Refresh.all() Refresh.all()
snackString(getString(R.string.deleted_from_list)) snackString(getString(R.string.deleted_from_list))

View file

@ -12,11 +12,9 @@ import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -46,7 +44,11 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetMediaListSmallBinding? = null private var _binding: BottomSheetMediaListSmallBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetMediaListSmallBinding.inflate(inflater, container, false) _binding = BottomSheetMediaListSmallBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -60,8 +62,12 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
binding.mediaListProgressBar.visibility = View.GONE binding.mediaListProgressBar.visibility = View.GONE
binding.mediaListLayout.visibility = View.VISIBLE binding.mediaListLayout.visibility = View.VISIBLE
val statuses: Array<String> = resources.getStringArray(R.array.status) val statuses: Array<String> = resources.getStringArray(R.array.status)
val statusStrings = if (media.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga) val statusStrings =
val userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0] if (media.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
R.array.status_manga
)
val userStatus =
if (media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
binding.mediaListStatus.setText(userStatus) binding.mediaListStatus.setText(userStatus)
binding.mediaListStatus.setAdapter( binding.mediaListStatus.setAdapter(
@ -130,10 +136,26 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull() val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt() val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())] ?.times(10))?.toInt()
Anilist.mutation.editList(media.id, progress, score, null, null, status, media.isListPrivate) val status =
MAL.query.editList(media.idMAL, media.anime != null, progress, score, status) statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
Anilist.mutation.editList(
media.id,
progress,
score,
null,
null,
status,
media.isListPrivate
)
MAL.query.editList(
media.idMAL,
media.anime != null,
progress,
score,
status
)
} }
} }
Refresh.all() Refresh.all()

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.Date
class OtherDetailsViewModel : ViewModel() { class OtherDetailsViewModel : ViewModel() {
private val character: MutableLiveData<Character> = MutableLiveData(null) private val character: MutableLiveData<Character> = MutableLiveData(null)
@ -19,26 +19,28 @@ class OtherDetailsViewModel : ViewModel() {
suspend fun loadStudio(m: Studio) { suspend fun loadStudio(m: Studio) {
if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m)) if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m))
} }
private val author: MutableLiveData<Author> = MutableLiveData(null) private val author: MutableLiveData<Author> = MutableLiveData(null)
fun getAuthor(): LiveData<Author> = author fun getAuthor(): LiveData<Author> = author
suspend fun loadAuthor(m: Author) { suspend fun loadAuthor(m: Author) {
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m)) if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
} }
private val calendar: MutableLiveData<Map<String,MutableList<Media>>> = MutableLiveData(null)
fun getCalendar(): LiveData<Map<String,MutableList<Media>>> = calendar private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
suspend fun loadCalendar() { suspend fun loadCalendar() {
val curr = System.currentTimeMillis()/1000 val curr = System.currentTimeMillis() / 1000
val res = Anilist.query.recentlyUpdated(false,curr-86400,curr+(86400*6)) val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
val df = DateFormat.getDateInstance(DateFormat.FULL) val df = DateFormat.getDateInstance(DateFormat.FULL)
val map = mutableMapOf<String,MutableList<Media>>() val map = mutableMapOf<String, MutableList<Media>>()
val idMap = mutableMapOf<String,MutableList<Int>>() val idMap = mutableMapOf<String, MutableList<Int>>()
res?.forEach { res?.forEach {
val v = it.relation?.split(",")?.map { i-> i.toLong() }!! val v = it.relation?.split(",")?.map { i -> i.toLong() }!!
val dateInfo = df.format(Date(v[1]*1000)) val dateInfo = df.format(Date(v[1] * 1000))
val list = map.getOrPut(dateInfo) { mutableListOf() } val list = map.getOrPut(dateInfo) { mutableListOf() }
val idList = idMap.getOrPut(dateInfo) { mutableListOf() } val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
it.relation = "Episode ${v[0]}" it.relation = "Episode ${v[0]}"
if(!idList.contains(it.id)) { if (!idList.contains(it.id)) {
idList.add(it.id) idList.add(it.id)
list.add(it) list.add(it)
} }

View file

@ -22,7 +22,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
var bar: ProgressBar? = null var bar: ProgressBar? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder {
val binding = ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ProgressViewHolder(binding) return ProgressViewHolder(binding)
} }
@ -33,7 +34,12 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() { val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() {
override fun onDoubleClick(event: MotionEvent) { override fun onDoubleClick(event: MotionEvent) {
snackString(currContext()?.getString(R.string.cant_wait)) snackString(currContext()?.getString(R.string.cant_wait))
ObjectAnimator.ofFloat(progressBar, "translationX", progressBar.translationX, progressBar.translationX + 100f) ObjectAnimator.ofFloat(
progressBar,
"translationX",
progressBar.translationX,
progressBar.translationX + 100f
)
.setDuration(300).start() .setDuration(300).start()
} }
@ -51,7 +57,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
} }
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class ProgressViewHolder(val binding: ItemProgressbarBinding) : RecyclerView.ViewHolder(binding.root) { inner class ProgressViewHolder(val binding: ItemProgressbarBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 } itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 }
} }

View file

@ -16,8 +16,8 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch import ani.dantotsu.connections.anilist.AnilistSearch
import ani.dantotsu.connections.anilist.SearchResults import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.databinding.ActivitySearchBinding import ani.dantotsu.databinding.ActivitySearchBinding
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet import ani.dantotsu.others.LangSet
import ani.dantotsu.themes.ThemeManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
@ -35,12 +35,12 @@ class SearchActivity : AppCompatActivity() {
private lateinit var concatAdapter: ConcatAdapter private lateinit var concatAdapter: ConcatAdapter
lateinit var result: SearchResults lateinit var result: SearchResults
lateinit var updateChips: (()->Unit) lateinit var updateChips: (() -> Unit)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivitySearchBinding.inflate(layoutInflater) binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initActivity(this) initActivity(this)
@ -83,10 +83,10 @@ ThemeManager(this).applyTheme()
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int { override fun getSpanSize(position: Int): Int {
return when (position) { return when (position) {
0 -> gridSize 0 -> gridSize
concatAdapter.itemCount - 1 -> gridSize concatAdapter.itemCount - 1 -> gridSize
else -> when (style) { else -> when (style) {
0 -> 1 0 -> 1
else -> gridSize else -> gridSize
} }
} }
@ -149,7 +149,7 @@ ThemeManager(this).applyTheme()
} else } else
headerAdaptor.requestFocus?.run() headerAdaptor.requestFocus?.run()
if(intent.getBooleanExtra("search",false)) search() if (intent.getBooleanExtra("search", false)) search()
} }
} }
} }

View file

@ -20,14 +20,16 @@ import ani.dantotsu.saveData
import com.google.android.material.checkbox.MaterialCheckBox.* import com.google.android.material.checkbox.MaterialCheckBox.*
class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() { class SearchAdapter(private val activity: SearchActivity) :
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
private val itemViewType = 6969 private val itemViewType = 6969
var search: Runnable? = null var search: Runnable? = null
var requestFocus: Runnable? = null var requestFocus: Runnable? = null
private var textWatcher: TextWatcher? = null private var textWatcher: TextWatcher? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding = ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchHeaderViewHolder(binding) return SearchHeaderViewHolder(binding)
} }
@ -36,13 +38,15 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
val binding = holder.binding val binding = holder.binding
val imm: InputMethodManager = activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager val imm: InputMethodManager =
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
when (activity.style) { when (activity.style) {
0 -> { 0 -> {
binding.searchResultGrid.alpha = 1f binding.searchResultGrid.alpha = 1f
binding.searchResultList.alpha = 0.33f binding.searchResultList.alpha = 0.33f
} }
1 -> { 1 -> {
binding.searchResultList.alpha = 1f binding.searchResultList.alpha = 1f
binding.searchResultGrid.alpha = 0.33f binding.searchResultGrid.alpha = 0.33f
@ -62,7 +66,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also { binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
activity.updateChips = { it.update() } activity.updateChips = { it.update() }
} }
binding.searchChipRecycler.layoutManager = LinearLayoutManager(binding.root.context, HORIZONTAL, false) binding.searchChipRecycler.layoutManager =
LinearLayoutManager(binding.root.context, HORIZONTAL, false)
binding.searchFilter.setOnClickListener { binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog") SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
@ -70,7 +75,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
fun searchTitle() { fun searchTitle() {
activity.result.apply { activity.result.apply {
search = if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null search =
if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
onList = listOnly onList = listOnly
isAdult = adult isAdult = adult
} }
@ -96,7 +102,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0) imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
true true
} }
else -> false
else -> false
} }
} }
binding.searchBar.setEndIconOnClickListener { searchTitle() } binding.searchBar.setEndIconOnClickListener { searchTitle() }
@ -127,7 +134,7 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
binding.searchList.apply { binding.searchList.apply {
if (Anilist.userid != null) { if (Anilist.userid != null) {
visibility = View.VISIBLE visibility = View.VISIBLE
checkedState = when(listOnly){ checkedState = when (listOnly) {
null -> STATE_UNCHECKED null -> STATE_UNCHECKED
true -> STATE_CHECKED true -> STATE_CHECKED
false -> STATE_INDETERMINATE false -> STATE_INDETERMINATE
@ -135,10 +142,10 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
addOnCheckedStateChangedListener { _, state -> addOnCheckedStateChangedListener { _, state ->
listOnly = when (state) { listOnly = when (state) {
STATE_CHECKED -> true STATE_CHECKED -> true
STATE_INDETERMINATE -> false STATE_INDETERMINATE -> false
STATE_UNCHECKED -> null STATE_UNCHECKED -> null
else -> null else -> null
} }
} }
@ -158,20 +165,24 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : RecyclerView.ViewHolder(binding.root) inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
RecyclerView.ViewHolder(binding.root)
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return itemViewType return itemViewType
} }
class SearchChipAdapter(val activity: SearchActivity) : RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() { class SearchChipAdapter(val activity: SearchActivity) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList() private var chips = activity.result.toChipList()
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root) inner class SearchChipViewHolder(val binding: ItemChipBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchChipViewHolder(binding) return SearchChipViewHolder(binding)
} }

View file

@ -18,13 +18,17 @@ import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
class SearchFilterBottomDialog() : BottomSheetDialogFragment() { class SearchFilterBottomDialog : BottomSheetDialogFragment() {
private var _binding: BottomSheetSearchFilterBinding? = null private var _binding: BottomSheetSearchFilterBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var activity: SearchActivity private lateinit var activity: SearchActivity
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSearchFilterBinding.inflate(inflater, container, false) _binding = BottomSheetSearchFilterBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -99,7 +103,7 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
ArrayAdapter( ArrayAdapter(
binding.root.context, binding.root.context,
R.layout.item_dropdown, R.layout.item_dropdown,
(1970 until 2024).map { it.toString() }.reversed().toTypedArray() (1970 until 2025).map { it.toString() }.reversed().toTypedArray()
) )
) )
} }
@ -129,24 +133,25 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
} }
binding.searchGenresGrid.isChecked = false binding.searchGenresGrid.isChecked = false
binding.searchFilterTags.adapter = FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip -> binding.searchFilterTags.adapter =
val tag = chip.text.toString() FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
chip.isChecked = selectedTags.contains(tag) val tag = chip.text.toString()
chip.isCloseIconVisible = exTags.contains(tag) chip.isChecked = selectedTags.contains(tag)
chip.setOnCheckedChangeListener { _, isChecked -> chip.isCloseIconVisible = exTags.contains(tag)
if (isChecked) { chip.setOnCheckedChangeListener { _, isChecked ->
chip.isCloseIconVisible = false if (isChecked) {
exTags.remove(tag) chip.isCloseIconVisible = false
selectedTags.add(tag) exTags.remove(tag)
} else selectedTags.add(tag)
selectedTags.remove(tag) } else
selectedTags.remove(tag)
}
chip.setOnLongClickListener {
chip.isChecked = false
chip.isCloseIconVisible = true
exTags.add(tag)
}
} }
chip.setOnLongClickListener {
chip.isChecked = false
chip.isCloseIconVisible = true
exTags.add(tag)
}
}
binding.searchTagsGrid.setOnCheckedChangeListener { _, isChecked -> binding.searchTagsGrid.setOnCheckedChangeListener { _, isChecked ->
binding.searchFilterTags.layoutManager = binding.searchFilterTags.layoutManager =
if (!isChecked) LinearLayoutManager(binding.root.context, HORIZONTAL, false) if (!isChecked) LinearLayoutManager(binding.root.context, HORIZONTAL, false)
@ -158,10 +163,12 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
class FilterChipAdapter(val list: List<String>, private val perform: ((Chip) -> Unit)) : class FilterChipAdapter(val list: List<String>, private val perform: ((Chip) -> Unit)) :
RecyclerView.Adapter<FilterChipAdapter.SearchChipViewHolder>() { RecyclerView.Adapter<FilterChipAdapter.SearchChipViewHolder>() {
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root) inner class SearchChipViewHolder(val binding: ItemChipBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchChipViewHolder(binding) return SearchChipViewHolder(binding)
} }

View file

@ -17,7 +17,8 @@ abstract class SourceAdapter(
private val scope: CoroutineScope private val scope: CoroutineScope
) : RecyclerView.Adapter<SourceAdapter.SourceViewHolder>() { ) : RecyclerView.Adapter<SourceAdapter.SourceViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SourceViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SourceViewHolder {
val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SourceViewHolder(binding) return SourceViewHolder(binding)
} }
@ -34,7 +35,8 @@ abstract class SourceAdapter(
abstract suspend fun onItemClick(source: ShowResponse) abstract suspend fun onItemClick(source: ShowResponse)
inner class SourceViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) { inner class SourceViewHolder(val binding: ItemCharacterBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
dialogFragment.dismiss() dialogFragment.dismiss()

View file

@ -13,8 +13,8 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.media.anime.AnimeSourceAdapter
import ani.dantotsu.databinding.BottomSheetSourceSearchBinding import ani.dantotsu.databinding.BottomSheetSourceSearchBinding
import ani.dantotsu.media.anime.AnimeSourceAdapter
import ani.dantotsu.media.manga.MangaSourceAdapter import ani.dantotsu.media.manga.MangaSourceAdapter
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
@ -38,7 +38,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
var id: Int? = null var id: Int? = null
var media: Media? = null var media: Media? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSourceSearchBinding.inflate(inflater, container, false) _binding = BottomSheetSourceSearchBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -47,7 +51,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight } binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = requireActivity().lifecycleScope val scope = requireActivity().lifecycleScope
val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm =
requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
model.getMedia().observe(viewLifecycleOwner) { model.getMedia().observe(viewLifecycleOwner) {
media = it media = it
if (media != null) { if (media != null) {
@ -65,6 +70,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
anime = false anime = false
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!] (if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
} }
fun search() { fun search() {
binding.searchBarText.clearFocus() binding.searchBarText.clearFocus()
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0) imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
@ -86,7 +92,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
search() search()
true true
} }
else -> false
else -> false
} }
} }
binding.searchBar.setEndIconOnClickListener { search() } binding.searchBar.setEndIconOnClickListener { search() }
@ -101,7 +108,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope) else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope)
binding.searchRecyclerView.layoutManager = GridLayoutManager( binding.searchRecyclerView.layoutManager = GridLayoutManager(
requireActivity(), requireActivity(),
clamp(requireActivity().resources.displayMetrics.widthPixels / 124f.px, 1, 4) clamp(
requireActivity().resources.displayMetrics.widthPixels / 124f.px,
1,
4
)
) )
} }
} }

View file

@ -12,11 +12,17 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.* import ani.dantotsu.EmptyAdapter
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityStudioBinding import ani.dantotsu.databinding.ActivityStudioBinding
import ani.dantotsu.others.getSerialized import ani.dantotsu.initActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized
import ani.dantotsu.px
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -31,7 +37,7 @@ class StudioActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityStudioBinding.inflate(layoutInflater) binding = ActivityStudioBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View file

@ -1,18 +1,13 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.content.Context import android.content.Context
import android.os.Environment
import ani.dantotsu.parsers.SubtitleType import ani.dantotsu.parsers.SubtitleType
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Request import okhttp3.Request
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.FileInputStream
class SubtitleDownloader { class SubtitleDownloader {
@ -30,7 +25,7 @@ class SubtitleDownloader {
// Check if response is successful // Check if response is successful
if (response.isSuccessful) { if (response.isSuccessful) {
val responseBody = response.body?.string() val responseBody = response.body.string()
val subtitleType = when { val subtitleType = when {

View file

@ -5,8 +5,10 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemTitleBinding import ani.dantotsu.databinding.ItemTitleBinding
class TitleAdapter(private val text: String) : RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() { class TitleAdapter(private val text: String) :
inner class TitleViewHolder(val binding: ItemTitleBinding) : RecyclerView.ViewHolder(binding.root) RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
inner class TitleViewHolder(val binding: ItemTitleBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder {
val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false)

View file

@ -15,7 +15,7 @@ data class Anime(
var ed: ArrayList<String> = arrayListOf(), var ed: ArrayList<String> = arrayListOf(),
var mainStudio: Studio? = null, var mainStudio: Studio? = null,
var author: Author?=null, var author: Author? = null,
var youtube: String? = null, var youtube: String? = null,
var nextAiringEpisode: Int? = null, var nextAiringEpisode: Int? = null,

View file

@ -0,0 +1,20 @@
package ani.dantotsu.media.anime
import java.util.regex.Matcher
import java.util.regex.Pattern
class AnimeNameAdapter {
companion object {
fun findSeasonNumber(text: String): Int? {
val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)"
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
val seasonMatcher: Matcher = seasonPattern.matcher(text)
return if (seasonMatcher.find()) {
seasonMatcher.group(2)?.toInt()
} else {
null
}
}
}
}

View file

@ -1,23 +1,17 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
@ -27,21 +21,11 @@ import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.IndexOutOfBoundsException
class AnimeWatchAdapter( class AnimeWatchAdapter(
private val media: Media, private val media: Media,
@ -72,23 +56,33 @@ class AnimeWatchAdapter(
} }
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
binding.animeSourceDubbedText.text = if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed) binding.animeSourceDubbedText.text =
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
R.string.subbed
)
//PreferDub //PreferDub
var changing = false var changing = false
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked -> binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
binding.animeSourceDubbedText.text = if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed) binding.animeSourceDubbedText.text =
if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
R.string.subbed
)
if (!changing) fragment.onDubClicked(isChecked) if (!changing) fragment.onDubClicked(isChecked)
} }
//Wrong Title //Wrong Title
binding.animeSourceSearch.setOnClickListener { binding.animeSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null) SourceSearchDialogFragment().show(
fragment.requireActivity().supportFragmentManager,
null
)
} }
//Source Selection //Source Selection
var source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it } var source =
setLanguageList(media.selected!!.langIndex,source) media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
setLanguageList(media.selected!!.langIndex, source)
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) { if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
binding.animeSource.setText(watchSources.names[source]) binding.animeSource.setText(watchSources.names[source])
watchSources[source].apply { watchSources[source].apply {
@ -100,7 +94,13 @@ class AnimeWatchAdapter(
} }
} }
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, watchSources.names)) binding.animeSource.setAdapter(
ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
watchSources.names
)
)
binding.animeSourceTitle.isSelected = true binding.animeSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ -> binding.animeSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply { fragment.onSourceChange(i).apply {
@ -109,9 +109,10 @@ class AnimeWatchAdapter(
changing = true changing = true
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
source = i source = i
setLanguageList(0,i) setLanguageList(0, i)
} }
subscribeButton(false) subscribeButton(false)
fragment.loadEpisodes(i, false) fragment.loadEpisodes(i, false)
@ -124,11 +125,13 @@ class AnimeWatchAdapter(
fragment.onLangChange(i) fragment.onLangChange(i)
fragment.onSourceChange(media.selected!!.sourceIndex).apply { fragment.onSourceChange(media.selected!!.sourceIndex).apply {
binding.animeSourceTitle.text = showUserText binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } }
changing = true changing = true
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
setLanguageList(i, source) setLanguageList(i, source)
} }
subscribeButton(false) subscribeButton(false)
@ -146,7 +149,7 @@ class AnimeWatchAdapter(
//Subscription //Subscription
subscribe = MediaDetailsActivity.PopImageButton( subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope, fragment.lifecycleScope,
binding.animeSourceSubscribe, binding.animeSourceSubscribe,
R.drawable.ic_round_notifications_active_24, R.drawable.ic_round_notifications_active_24,
@ -161,7 +164,7 @@ class AnimeWatchAdapter(
subscribeButton(false) subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener { binding.animeSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(),getChannelId(true,media.id)) openSettings(fragment.requireContext(), getChannelId(true, media.id))
} }
//Icons //Icons
@ -200,12 +203,12 @@ class AnimeWatchAdapter(
style = 2 style = 2
fragment.onIconPressed(style, reversed) fragment.onIconPressed(style, reversed)
} }
binding.animeScanlatorTop.visibility= View.GONE binding.animeScanlatorTop.visibility = View.GONE
//Episode Handling //Episode Handling
handleEpisodes() handleEpisodes()
} }
fun subscribeButton(enabled : Boolean) { fun subscribeButton(enabled: Boolean) {
subscribe?.enabled(enabled) subscribe?.enabled(enabled)
} }
@ -219,14 +222,26 @@ class AnimeWatchAdapter(
for (position in arr.indices) { for (position in arr.indices) {
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1)) val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
val chip = val chip =
ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root ItemChipBinding.inflate(
LayoutInflater.from(fragment.context),
binding.animeSourceChipGroup,
false
).root
chip.isCheckable = true chip.isCheckable = true
fun selected() { fun selected() {
chip.isChecked = true chip.isChecked = true
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0) binding.animeWatchChipScroll.smoothScrollTo(
(chip.left - screenWidth / 2) + (chip.width / 2),
0
)
} }
chip.text = "${names[limit * (position)]} - ${names[last - 1]}" chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color)) chip.setTextColor(
ContextCompat.getColorStateList(
fragment.requireContext(),
R.color.chip_text_color
)
)
chip.setOnClickListener { chip.setOnClickListener {
selected() selected()
@ -239,7 +254,14 @@ class AnimeWatchAdapter(
} }
} }
if (select != null) if (select != null)
binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } } binding.animeWatchChipScroll.apply {
post {
scrollTo(
(select.left - screenWidth / 2) + (select.width / 2),
0
)
}
}
} }
} }
@ -281,7 +303,9 @@ class AnimeWatchAdapter(
} }
} }
val ep = media.anime.episodes!![continueEp]!! val ep = media.anime.episodes!![continueEp]!!
binding.itemEpisodeImage.loadImage(ep.thumb ?: FileUrl[media.banner ?: media.cover], 0) binding.itemEpisodeImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
)
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text = binding.animeSourceContinueText.text =
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}" currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}"
@ -321,10 +345,17 @@ class AnimeWatchAdapter(
} }
try { try {
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang) binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
}catch (e: IndexOutOfBoundsException) { } catch (e: IndexOutOfBoundsException) {
binding?.animeSourceLanguage?.setText(parser.extension.sources.firstOrNull()?.lang ?: "Unknown") binding?.animeSourceLanguage?.setText(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
)
} }
binding?.animeSourceLanguage?.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, parser.extension.sources.map { it.lang })) binding?.animeSourceLanguage?.setAdapter(
ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
parser.extension.sources.map { it.lang })
)
} }
} }
@ -332,7 +363,8 @@ class AnimeWatchAdapter(
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) { inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
//Timer //Timer
countDown(media, binding.animeSourceContainer) countDown(media, binding.animeSourceContainer)

View file

@ -8,8 +8,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.math.MathUtils import androidx.core.math.MathUtils
@ -28,8 +26,6 @@ import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.AnimeParser import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.InstalledAnimeExtensionsFragment
import ani.dantotsu.settings.PlayerSettings import ani.dantotsu.settings.PlayerSettings
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
@ -40,17 +36,12 @@ import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -96,8 +87,10 @@ class AnimeWatchFragment : Fragment() {
maxGridSize = max(4, maxGridSize - (maxGridSize % 2)) maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
playerSettings = playerSettings =
loadData("player_settings", toast = false) ?: PlayerSettings().apply { saveData("player_settings", this) } loadData("player_settings", toast = false)
uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } ?: PlayerSettings().apply { saveData("player_settings", this) }
uiSettings = loadData("ui_settings", toast = false)
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize) val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize)
@ -106,11 +99,11 @@ class AnimeWatchFragment : Fragment() {
val style = episodeAdapter.getItemViewType(position) val style = episodeAdapter.getItemViewType(position)
return when (position) { return when (position) {
0 -> maxGridSize 0 -> maxGridSize
else -> when (style) { else -> when (style) {
0 -> maxGridSize 0 -> maxGridSize
1 -> 2 1 -> 2
2 -> 1 2 -> 1
else -> maxGridSize else -> maxGridSize
} }
} }
@ -129,7 +122,8 @@ class AnimeWatchFragment : Fragment() {
media = it media = it
media.selected = model.loadSelected(media) media.selected = model.loadSelected(media)
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id) subscribed =
SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
style = media.selected!!.recyclerStyle style = media.selected!!.recyclerStyle
reverse = media.selected!!.recyclerReversed reverse = media.selected!!.recyclerReversed
@ -141,9 +135,11 @@ class AnimeWatchFragment : Fragment() {
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!) headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
episodeAdapter = EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this) episodeAdapter =
EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this)
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, episodeAdapter) binding.animeSourceRecycler.adapter =
ConcatAdapter(headerAdapter, episodeAdapter)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
awaitAll( awaitAll(
@ -165,15 +161,20 @@ class AnimeWatchFragment : Fragment() {
episodes.forEach { (i, episode) -> episodes.forEach { (i, episode) ->
if (media.anime?.fillerEpisodes != null) { if (media.anime?.fillerEpisodes != null) {
if (media.anime!!.fillerEpisodes!!.containsKey(i)) { if (media.anime!!.fillerEpisodes!!.containsKey(i)) {
episode.title = episode.title ?: media.anime!!.fillerEpisodes!![i]?.title episode.title =
episode.title ?: media.anime!!.fillerEpisodes!![i]?.title
episode.filler = media.anime!!.fillerEpisodes!![i]?.filler ?: false episode.filler = media.anime!!.fillerEpisodes!![i]?.filler ?: false
} }
} }
if (media.anime?.kitsuEpisodes != null) { if (media.anime?.kitsuEpisodes != null) {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) { if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc = episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc episode.desc =
episode.title = episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc
episode.thumb = episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb ?: FileUrl[media.cover] episode.title =
episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title
episode.thumb =
episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb
?: FileUrl[media.cover]
} }
} }
} }
@ -187,7 +188,7 @@ class AnimeWatchFragment : Fragment() {
val limit = when { val limit = when {
(divisions < 25) -> 25 (divisions < 25) -> 25
(divisions < 50) -> 50 (divisions < 50) -> 50
else -> 100 else -> 100
} }
headerAdapter.clearChips() headerAdapter.clearChips()
if (total > limit) { if (total > limit) {
@ -247,7 +248,12 @@ class AnimeWatchFragment : Fragment() {
selected.preferDub = checked selected.preferDub = checked
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected, requireActivity())
media.selected = selected media.selected = selected
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) } lifecycleScope.launch(Dispatchers.IO) {
model.forceLoadEpisode(
media,
selected.sourceIndex
)
}
} }
fun loadEpisodes(i: Int, invalidate: Boolean) { fun loadEpisodes(i: Int, invalidate: Boolean) {
@ -289,7 +295,8 @@ class AnimeWatchFragment : Fragment() {
else getString(R.string.unsubscribed_notification) else getString(R.string.unsubscribed_notification)
) )
} }
fun openSettings(pkg: AnimeExtension.Installed){
fun openSettings(pkg: AnimeExtension.Installed) {
val changeUIVisibility: (Boolean) -> Unit = { show -> val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = requireActivity() as MediaDetailsActivity val activity = requireActivity() as MediaDetailsActivity
val visibility = if (show) View.VISIBLE else View.GONE val visibility = if (show) View.VISIBLE else View.GONE
@ -297,9 +304,9 @@ class AnimeWatchFragment : Fragment() {
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try{ try {
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
}catch (e: ClassCastException){ } catch (e: ClassCastException) {
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
} }
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility = activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
@ -363,46 +370,46 @@ class AnimeWatchFragment : Fragment() {
} }
} }
fun onEpisodeClick(i: String) { fun onEpisodeClick(i: String) {
model.continueMedia = false model.continueMedia = false
model.saveSelected(media.id, media.selected!!, requireActivity()) model.saveSelected(media.id, media.selected!!, requireActivity())
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager) model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
}
@SuppressLint("NotifyDataSetChanged")
private fun reload() {
val selected = model.loadSelected(media)
//Find latest episode for subscription
selected.latest =
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest =
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleEpisodes()
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
var arr: ArrayList<Episode> = arrayListOf()
if (media.anime!!.episodes != null) {
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null
arr.addAll(
media.anime!!.episodes!!.values.toList()
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1)))
)
if (reverse)
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
} }
episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
episodeAdapter.notifyItemRangeInserted(0, arr.size)
}
@SuppressLint("NotifyDataSetChanged") override fun onDestroy() {
private fun reload() { model.watchSources?.flushText()
val selected = model.loadSelected(media) super.onDestroy()
}
//Find latest episode for subscription var state: Parcelable? = null
selected.latest =
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest =
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleEpisodes()
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
var arr: ArrayList<Episode> = arrayListOf()
if (media.anime!!.episodes != null) {
val end = if (end != null && end!! < media.anime!!.episodes!!.size) end else null
arr.addAll(
media.anime!!.episodes!!.values.toList()
.slice(start..(end ?: (media.anime!!.episodes!!.size - 1)))
)
if (reverse)
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
}
episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
episodeAdapter.notifyItemRangeInserted(0, arr.size)
}
override fun onDestroy() {
model.watchSources?.flushText()
super.onDestroy()
}
var state: Parcelable? = null
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.mediaInfoProgressBar.visibility = progress binding.mediaInfoProgressBar.visibility = progress

View file

@ -14,12 +14,12 @@ data class Episode(
var selectedExtractor: String? = null, var selectedExtractor: String? = null,
var selectedVideo: Int = 0, var selectedVideo: Int = 0,
var selectedSubtitle: Int? = -1, var selectedSubtitle: Int? = -1,
var extractors: MutableList<VideoExtractor>?=null, var extractors: MutableList<VideoExtractor>? = null,
@Transient var extractorCallback: ((VideoExtractor) -> Unit)?=null, @Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
var allStreams: Boolean = false, var allStreams: Boolean = false,
var watched: Long? = null, var watched: Long? = null,
var maxLength: Long? = null, var maxLength: Long? = null,
val extra: Map<String,String>?=null, val extra: Map<String, String>? = null,
val sEpisode: eu.kanade.tachiyomi.animesource.model.SEpisode? = null val sEpisode: eu.kanade.tachiyomi.animesource.model.SEpisode? = null
) : Serializable ) : Serializable

View file

@ -41,15 +41,30 @@ class EpisodeAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return (when (viewType) { return (when (viewType) {
0 -> EpisodeListViewHolder(ItemEpisodeListBinding.inflate(LayoutInflater.from(parent.context), parent, false)) 0 -> EpisodeListViewHolder(
1 -> EpisodeGridViewHolder(ItemEpisodeGridBinding.inflate(LayoutInflater.from(parent.context), parent, false)) ItemEpisodeListBinding.inflate(
2 -> EpisodeCompactViewHolder( LayoutInflater.from(parent.context),
parent,
false
)
)
1 -> EpisodeGridViewHolder(
ItemEpisodeGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
2 -> EpisodeCompactViewHolder(
ItemEpisodeCompactBinding.inflate( ItemEpisodeCompactBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
false false
) )
) )
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
}) })
} }
@ -62,15 +77,21 @@ class EpisodeAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val ep = arr[position] val ep = arr[position]
val title = val title =
"${if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString(R.string.episode_singular)} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}" "${
if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString(
R.string.episode_singular
)
} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}"
when (holder) { when (holder) {
is EpisodeListViewHolder -> { is EpisodeListViewHolder -> {
val binding = holder.binding val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } val thumb =
Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage) ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title binding.itemEpisodeTitle.text = title
@ -81,7 +102,8 @@ class EpisodeAdapter(
binding.itemEpisodeFiller.visibility = View.GONE binding.itemEpisodeFiller.visibility = View.GONE
binding.itemEpisodeFillerView.visibility = View.GONE binding.itemEpisodeFillerView.visibility = View.GONE
} }
binding.itemEpisodeDesc.visibility = if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE binding.itemEpisodeDesc.visibility =
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = ep.desc ?: "" binding.itemEpisodeDesc.text = ep.desc ?: ""
if (media.userProgress != null) { if (media.userProgress != null) {
@ -110,12 +132,14 @@ class EpisodeAdapter(
) )
} }
is EpisodeGridViewHolder -> { is EpisodeGridViewHolder -> {
val binding = holder.binding val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } val thumb =
Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage) ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title binding.itemEpisodeTitle.text = title
@ -155,7 +179,8 @@ class EpisodeAdapter(
val binding = holder.binding val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeFillerView.visibility = if (ep.filler) View.VISIBLE else View.GONE binding.itemEpisodeFillerView.visibility =
if (ep.filler) View.VISIBLE else View.GONE
if (media.userProgress != null) { if (media.userProgress != null) {
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat())
binding.itemEpisodeViewedCover.visibility = View.VISIBLE binding.itemEpisodeViewedCover.visibility = View.VISIBLE
@ -180,7 +205,8 @@ class EpisodeAdapter(
override fun getItemCount(): Int = arr.size override fun getItemCount(): Int = arr.size
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) { inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0) if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
@ -189,7 +215,8 @@ class EpisodeAdapter(
} }
} }
inner class EpisodeGridViewHolder(val binding: ItemEpisodeGridBinding) : RecyclerView.ViewHolder(binding.root) { inner class EpisodeGridViewHolder(val binding: ItemEpisodeGridBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0) if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
@ -198,7 +225,8 @@ class EpisodeAdapter(
} }
} }
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) : RecyclerView.ViewHolder(binding.root) { inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0) if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)

View file

@ -17,7 +17,6 @@ import android.graphics.drawable.Animatable
import android.hardware.SensorManager import android.hardware.SensorManager
import android.media.AudioManager import android.media.AudioManager
import android.media.AudioManager.* import android.media.AudioManager.*
import android.media.PlaybackParams
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -62,6 +61,8 @@ import ani.dantotsu.*
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.discord.Discord import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.discord.DiscordService
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
import ani.dantotsu.connections.discord.RPC import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityExoplayerBinding import ani.dantotsu.databinding.ActivityExoplayerBinding
@ -71,6 +72,7 @@ import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.others.AniSkip import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.AniSkip.getType import ani.dantotsu.others.AniSkip.getType
import ani.dantotsu.others.Download.download import ani.dantotsu.others.Download.download
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.ResettableTimer import ani.dantotsu.others.ResettableTimer
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.parsers.* import ani.dantotsu.parsers.*
@ -78,7 +80,6 @@ import ani.dantotsu.settings.PlayerSettings
import ani.dantotsu.settings.PlayerSettingsActivity import ani.dantotsu.settings.PlayerSettingsActivity
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
@ -813,15 +814,15 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
fun fastForward() { fun fastForward() {
isFastForwarding = true isFastForwarding = true
exoPlayer.setPlaybackSpeed(2f) exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2)
snackString("Playing at 2x speed") snackString("Playing at ${exoPlayer.playbackParameters.speed}x speed")
} }
fun stopFastForward() { fun stopFastForward() {
if (isFastForwarding) { if (isFastForwarding) {
isFastForwarding = false isFastForwarding = false
exoPlayer.setPlaybackSpeed(1f) exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed / 2)
snackString("Playing at normal speed") snackString("Playing at default speed: ${exoPlayer.playbackParameters.speed}x")
} }
} }
@ -862,6 +863,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
override fun onLongClick(event: MotionEvent) { override fun onLongClick(event: MotionEvent) {
if (settings.fastforward) fastForward() if (settings.fastforward) fastForward()
} }
override fun onDoubleClick(event: MotionEvent) { override fun onDoubleClick(event: MotionEvent) {
doubleTap(true, event) doubleTap(true, event)
} }
@ -994,22 +996,40 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
playbackPosition = loadData("${media.id}_${ep.number}", this) ?: 0 playbackPosition = loadData("${media.id}_${ep.number}", this) ?: 0
initPlayer() initPlayer()
preloading = false preloading = false
rpc = Discord.defaultRPC() val context = this
rpc?.send {
type = RPC.Type.WATCHING lifecycleScope.launch {
activityName = media.userPreferredName val presence = RPC.createPresence(RPC.Companion.RPCData(
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString( applicationId = Discord.application_Id,
R.string.episode_num, type = RPC.Type.WATCHING,
ep.number activityName = media.userPreferredName,
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
R.string.episode_num,
ep.number
),
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
largeImage = media.cover?.let { RPC.Link(media.userPreferredName, it) },
smallImage = RPC.Link(
"Dantotsu",
Discord.small_Image
),
buttons = mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link(
"Stream on Dantotsu",
"https://github.com/rebelonion/Dantotsu/"
)
)
) )
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}" )
media.cover?.let { cover ->
largeImage = RPC.Link(media.userPreferredName, cover) val intent = Intent(context, DiscordService::class.java).apply {
} putExtra("presence", presence)
media.shareLink?.let { link ->
buttons.add(0, RPC.Link(getString(R.string.view_anime), link))
} }
DiscordServiceRunningSingleton.running = true
startService(intent)
} }
updateProgress() updateProgress()
} }
} }
@ -1129,7 +1149,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
?: true else false ?: true else false
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true) if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
AlertDialog.Builder(this, R.style.DialogTheme) AlertDialog.Builder(this, R.style.MyPopup)
.setTitle(getString(R.string.auto_update, media.userPreferredName)) .setTitle(getString(R.string.auto_update, media.userPreferredName))
.apply { .apply {
setOnCancelListener { hideSystemBars() } setOnCancelListener { hideSystemBars() }
@ -1278,6 +1298,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
} }
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType) val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
logger("url: ${video!!.file.url}")
logger("mimeType: $mimeType")
if (sub != null) { if (sub != null) {
val listofnotnullsubs = immutableListOf(sub).filterNotNull() val listofnotnullsubs = immutableListOf(sub).filterNotNull()
@ -1301,7 +1323,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
.setAllowMultipleAdaptiveSelections(true) .setAllowMultipleAdaptiveSelections(true)
.setPreferredTextLanguage(subtitle?.language ?: "en") .setPreferredTextLanguage(subtitle?.language ?: "en")
.setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE) .setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE)
.setRendererDisabled(C.TRACK_TYPE_VIDEO, false) .setRendererDisabled(TRACK_TYPE_VIDEO, false)
.setRendererDisabled(C.TRACK_TYPE_AUDIO, false) .setRendererDisabled(C.TRACK_TYPE_AUDIO, false)
.setRendererDisabled(C.TRACK_TYPE_TEXT, false) .setRendererDisabled(C.TRACK_TYPE_TEXT, false)
.setMinVideoSize( .setMinVideoSize(
@ -1310,7 +1332,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
) )
.setMaxVideoSize(1, 1) .setMaxVideoSize(1, 1)
//.setOverrideForType( //.setOverrideForType(
// TrackSelectionOverride(trackSelector, 2)) // TrackSelectionOverride(trackSelector, 2))
) )
if (playbackPosition != 0L && !changingServer && !settings.alwaysContinue) { if (playbackPosition != 0L && !changingServer && !settings.alwaysContinue) {
@ -1329,17 +1351,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
) )
AlertDialog.Builder(this, R.style.DialogTheme) AlertDialog.Builder(this, R.style.DialogTheme)
.setTitle(getString(R.string.continue_from, time)).apply { .setTitle(getString(R.string.continue_from, time)).apply {
setCancelable(false) setCancelable(false)
setPositiveButton(getString(R.string.yes)) { d, _ -> setPositiveButton(getString(R.string.yes)) { d, _ ->
buildExoplayer() buildExoplayer()
d.dismiss() d.dismiss()
} }
setNegativeButton(getString(R.string.no)) { d, _ -> setNegativeButton(getString(R.string.no)) { d, _ ->
playbackPosition = 0L playbackPosition = 0L
buildExoplayer() buildExoplayer()
d.dismiss() d.dismiss()
} }
}.show() }.show()
} else buildExoplayer() } else buildExoplayer()
} }
@ -1404,7 +1426,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
exoPlayer.release() exoPlayer.release()
VideoCache.release() VideoCache.release()
mediaSession?.release() mediaSession?.release()
rpc?.close() val stopIntent = Intent(this, DiscordService::class.java)
DiscordServiceRunningSingleton.running = false
stopService(stopIntent)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -1589,17 +1614,19 @@ class ExoplayerView : AppCompatActivity(), Player.Listener {
println("Track__: ${it.isSelected}") println("Track__: ${it.isSelected}")
println("Track__: ${it.type}") println("Track__: ${it.type}")
println("Track__: ${it.mediaTrackGroup.id}") println("Track__: ${it.mediaTrackGroup.id}")
if (it.type == 3 && it.mediaTrackGroup.id == "1:"){ if (it.type == 3 && it.mediaTrackGroup.id == "1:") {
playerView.player?.trackSelectionParameters = playerView.player?.trackSelectionParameters =
playerView.player?.trackSelectionParameters?.buildUpon() playerView.player?.trackSelectionParameters?.buildUpon()
?.setOverrideForType( ?.setOverrideForType(
TrackSelectionOverride(it.mediaTrackGroup, it.length - 1)) TrackSelectionOverride(it.mediaTrackGroup, it.length - 1)
)
?.build()!! ?.build()!!
}else if(it.type == 3){ } else if (it.type == 3) {
playerView.player?.trackSelectionParameters = playerView.player?.trackSelectionParameters =
playerView.player?.trackSelectionParameters?.buildUpon() playerView.player?.trackSelectionParameters?.buildUpon()
?.addOverride( ?.addOverride(
TrackSelectionOverride(it.mediaTrackGroup, listOf())) TrackSelectionOverride(it.mediaTrackGroup, listOf())
)
?.build()!! ?.build()!!
} }
} }

View file

@ -25,8 +25,6 @@ import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.Download.download import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType import ani.dantotsu.parsers.VideoType
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -54,7 +52,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false) _binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
val window = dialog?.window val window = dialog?.window
window?.statusBarColor = Color.TRANSPARENT window?.statusBarColor = Color.TRANSPARENT
@ -71,7 +73,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
media = m media = m
if (media != null && !loaded) { if (media != null && !loaded) {
loaded = true loaded = true
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode) val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
episode = ep episode = ep
if (ep != null) { if (ep != null) {
@ -92,14 +94,17 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} }
fun load() { fun load() {
val size = ep.extractors?.find { it.server.name == selected }?.videos?.size val size =
if (size!=null && size >= media!!.selected!!.video) { ep.extractors?.find { it.server.name == selected }?.videos?.size
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor = selected if (size != null && size >= media!!.selected!!.video) {
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo = media!!.selected!!.video media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor =
selected
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo =
media!!.selected!!.video
startExoplayer(media!!) startExoplayer(media!!)
} else fail() } else fail()
} }
if (ep.extractors.isNullOrEmpty()) { if (ep.extractors.isNullOrEmpty()) {
model.getEpisode().observe(this) { model.getEpisode().observe(this) {
if (it != null) { if (it != null) {
@ -116,8 +121,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
}) fail() }) fail()
} }
} else load() } else load()
} } else {
else {
binding.selectorRecyclerView.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.selectorRecyclerView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
} }
@ -130,10 +134,14 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
saveData("make_default", makeDefault) saveData("make_default", makeDefault)
} }
binding.selectorRecyclerView.layoutManager = binding.selectorRecyclerView.layoutManager =
LinearLayoutManager(requireActivity(), LinearLayoutManager.VERTICAL, false) LinearLayoutManager(
requireActivity(),
LinearLayoutManager.VERTICAL,
false
)
val adapter = ExtractorAdapter() val adapter = ExtractorAdapter()
binding.selectorRecyclerView.adapter = adapter binding.selectorRecyclerView.adapter = adapter
if (!ep.allStreams ) { if (!ep.allStreams) {
ep.extractorCallback = { ep.extractorCallback = {
scope.launch { scope.launch {
adapter.add(it) adapter.add(it)
@ -141,12 +149,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} }
model.getEpisode().observe(this) { model.getEpisode().observe(this) {
if (it != null) { if (it != null) {
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep) media!!.anime?.episodes?.set(
media!!.anime?.selectedEpisode!!,
ep
)
} }
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex) model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main) {
binding.selectorProgressBar.visibility = View.GONE binding.selectorProgressBar.visibility = View.GONE
} }
} }
@ -175,7 +186,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
ExoplayerView.initialized = true ExoplayerView.initialized = true
startActivity(intent) startActivity(intent)
} else { } else {
model.setEpisode(media.anime!!.episodes!![media.anime.selectedEpisode!!]!!, "startExo no launch") model.setEpisode(
media.anime!!.episodes!![media.anime.selectedEpisode!!]!!,
"startExo no launch"
)
} }
} }
@ -186,54 +200,72 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} }
} }
private inner class ExtractorAdapter : RecyclerView.Adapter<ExtractorAdapter.StreamViewHolder>() { private inner class ExtractorAdapter :
RecyclerView.Adapter<ExtractorAdapter.StreamViewHolder>() {
val links = mutableListOf<VideoExtractor>() val links = mutableListOf<VideoExtractor>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
StreamViewHolder(ItemStreamBinding.inflate(LayoutInflater.from(parent.context), parent, false)) StreamViewHolder(
ItemStreamBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val extractor = links[position] val extractor = links[position]
holder.binding.streamName.text = extractor.server.name holder.binding.streamName.text = extractor.server.name
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext()) holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor) holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
} }
override fun getItemCount(): Int = links.size override fun getItemCount(): Int = links.size
fun add(videoExtractor: VideoExtractor){ fun add(videoExtractor: VideoExtractor) {
if(videoExtractor.videos.isNotEmpty()) { if (videoExtractor.videos.isNotEmpty()) {
links.add(videoExtractor) links.add(videoExtractor)
notifyItemInserted(links.size - 1) notifyItemInserted(links.size - 1)
} }
} }
fun addAll(extractors: List<VideoExtractor>?) { fun addAll(extractors: List<VideoExtractor>?) {
links.addAll(extractors?:return) links.addAll(extractors ?: return)
notifyItemRangeInserted(0,extractors.size) notifyItemRangeInserted(0, extractors.size)
} }
private inner class StreamViewHolder(val binding: ItemStreamBinding) : RecyclerView.ViewHolder(binding.root) private inner class StreamViewHolder(val binding: ItemStreamBinding) :
RecyclerView.ViewHolder(binding.root)
} }
private inner class VideoAdapter(private val extractor : VideoExtractor) : RecyclerView.Adapter<VideoAdapter.UrlViewHolder>() { private inner class VideoAdapter(private val extractor: VideoExtractor) :
RecyclerView.Adapter<VideoAdapter.UrlViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder {
return UrlViewHolder(ItemUrlBinding.inflate(LayoutInflater.from(parent.context), parent, false)) return UrlViewHolder(
ItemUrlBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) { override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
val video = extractor.videos[position] val video = extractor.videos[position]
binding.urlQuality.text = if(video.quality!=null) "${video.quality}p" else "Default Quality" binding.urlQuality.text =
if (video.quality != null) "${video.quality}p" else "Default Quality"
binding.urlNote.text = video.extraNote ?: "" binding.urlNote.text = video.extraNote ?: ""
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
binding.urlDownload.visibility = View.VISIBLE binding.urlDownload.visibility = View.VISIBLE
binding.urlDownload.setSafeOnClickListener { binding.urlDownload.setSafeOnClickListener {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor = extractor.server.name media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo = position extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
position
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
download( download(
requireActivity(), requireActivity(),
@ -245,10 +277,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
if (video.format == VideoType.CONTAINER) { if (video.format == VideoType.CONTAINER) {
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
binding.urlSize.text = binding.urlSize.text =
// if video size is null or 0, show "Unknown Size" else show the size in MB // if video size is null or 0, show "Unknown Size" else show the size in MB
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat("#.##").format(video.size ?: 0).toString()+ " MB")) (if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
} "#.##"
else { ).format(video.size ?: 0).toString() + " MB"))
} else {
binding.urlQuality.text = "Multi Quality" binding.urlQuality.text = "Multi Quality"
if ((loadData<Int>("settings_download_manager") ?: 0) == 0) { if ((loadData<Int>("settings_download_manager") ?: 0) == 0) {
binding.urlDownload.visibility = View.GONE binding.urlDownload.visibility = View.GONE
@ -258,12 +291,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
override fun getItemCount(): Int = extractor.videos.size override fun getItemCount(): Int = extractor.videos.size
private inner class UrlViewHolder(val binding: ItemUrlBinding) : RecyclerView.ViewHolder(binding.root) { private inner class UrlViewHolder(val binding: ItemUrlBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setSafeOnClickListener { itemView.setSafeOnClickListener {
tryWith(true) { tryWith(true) {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor = extractor.server.name media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = bindingAdapterPosition extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo =
bindingAdapterPosition
if (makeDefault) { if (makeDefault) {
media!!.selected!!.server = extractor.server.name media!!.selected!!.server = extractor.server.name
media!!.selected!!.video = bindingAdapterPosition media!!.selected!!.video = bindingAdapterPosition
@ -274,12 +310,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} }
itemView.setOnLongClickListener { itemView.setOnLongClickListener {
val video = extractor.videos[bindingAdapterPosition] val video = extractor.videos[bindingAdapterPosition]
val intent= Intent(Intent.ACTION_VIEW).apply { val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(Uri.parse(video.file.url),"video/*") setDataAndType(Uri.parse(video.file.url), "video/*")
} }
copyToClipboard(video.file.url,true) copyToClipboard(video.file.url, true)
dismiss() dismiss()
startActivity(Intent.createChooser(intent,"Open Video in :")) startActivity(Intent.createChooser(intent, "Open Video in :"))
true true
} }
} }
@ -287,7 +323,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} }
companion object { companion object {
fun newInstance(server: String? = null, la: Boolean = true, prev: String? = null): SelectorDialogFragment = fun newInstance(
server: String? = null,
la: Boolean = true,
prev: String? = null
): SelectorDialogFragment =
SelectorDialogFragment().apply { SelectorDialogFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putString("server", server) putString("server", server)

View file

@ -24,7 +24,11 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
val model: MediaDetailsViewModel by activityViewModels() val model: MediaDetailsViewModel by activityViewModels()
private lateinit var episode: Episode private lateinit var episode: Episode
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false) _binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -34,17 +38,27 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
model.getMedia().observe(viewLifecycleOwner) { media -> model.getMedia().observe(viewLifecycleOwner) { media ->
episode = media?.anime?.episodes?.get(media.anime.selectedEpisode) ?: return@observe episode = media?.anime?.episodes?.get(media.anime.selectedEpisode) ?: return@observe
val currentExtractor = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return@observe val currentExtractor =
episode.extractors?.find { it.server.name == episode.selectedExtractor }
?: return@observe
binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext()) binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext())
binding.subtitlesRecycler.adapter = SubtitleAdapter(currentExtractor.subtitles) binding.subtitlesRecycler.adapter = SubtitleAdapter(currentExtractor.subtitles)
} }
} }
inner class SubtitleAdapter(val subtitles: List<Subtitle>) : RecyclerView.Adapter<SubtitleAdapter.StreamViewHolder>() { inner class SubtitleAdapter(val subtitles: List<Subtitle>) :
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) : RecyclerView.ViewHolder(binding.root) RecyclerView.Adapter<SubtitleAdapter.StreamViewHolder>() {
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
StreamViewHolder(ItemSubtitleTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)) StreamViewHolder(
ItemSubtitleTextBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
@ -60,7 +74,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
binding.root.setOnClickListener { binding.root.setOnClickListener {
episode.selectedSubtitle = null episode.selectedSubtitle = null
model.setEpisode(episode, "Subtitle") model.setEpisode(episode, "Subtitle")
model.getMedia().observe(viewLifecycleOwner){media -> model.getMedia().observe(viewLifecycleOwner) { media ->
val mediaID: Int = media.id val mediaID: Int = media.id
saveData("subLang_${mediaID}", "None", activity) saveData("subLang_${mediaID}", "None", activity)
} }
@ -87,7 +101,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
"pl-PL" -> "[pl-PL] Polish" "pl-PL" -> "[pl-PL] Polish"
"ro-RO" -> "[ro-RO] Romanian" "ro-RO" -> "[ro-RO] Romanian"
"sv-SE" -> "[sv-SE] Swedish" "sv-SE" -> "[sv-SE] Swedish"
else -> if(subtitles[position - 1].language matches Regex("([a-z]{2})-([A-Z]{2}|\\d{3})")) "[${subtitles[position - 1].language}]" else subtitles[position - 1].language else -> if (subtitles[position - 1].language matches Regex("([a-z]{2})-([A-Z]{2}|\\d{3})")) "[${subtitles[position - 1].language}]" else subtitles[position - 1].language
} }
model.getMedia().observe(viewLifecycleOwner) { media -> model.getMedia().observe(viewLifecycleOwner) { media ->
val mediaID: Int = media.id val mediaID: Int = media.id
@ -100,7 +114,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
binding.root.setOnClickListener { binding.root.setOnClickListener {
episode.selectedSubtitle = position - 1 episode.selectedSubtitle = position - 1
model.setEpisode(episode, "Subtitle") model.setEpisode(episode, "Subtitle")
model.getMedia().observe(viewLifecycleOwner){media -> model.getMedia().observe(viewLifecycleOwner) { media ->
val mediaID: Int = media.id val mediaID: Int = media.id
saveData("subLang_${mediaID}", subtitles[position - 1].language, activity) saveData("subLang_${mediaID}", subtitles[position - 1].language, activity)
} }

View file

@ -14,7 +14,10 @@ object VideoCache {
val databaseProvider = StandaloneDatabaseProvider(context) val databaseProvider = StandaloneDatabaseProvider(context)
if (simpleCache == null) if (simpleCache == null)
simpleCache = SimpleCache( simpleCache = SimpleCache(
File(context.cacheDir, "exoplayer").also { it.deleteOnExit() }, // Ensures always fresh file File(
context.cacheDir,
"exoplayer"
).also { it.deleteOnExit() }, // Ensures always fresh file
LeastRecentlyUsedCacheEvictor(300L * 1024L * 1024L), LeastRecentlyUsedCacheEvictor(300L * 1024L * 1024L),
databaseProvider databaseProvider
) )

View file

@ -8,5 +8,5 @@ data class Manga(
var selectedChapter: String? = null, var selectedChapter: String? = null,
var chapters: MutableMap<String, MangaChapter>? = null, var chapters: MutableMap<String, MangaChapter>? = null,
var slug: String? = null, var slug: String? = null,
var author: Author?=null, var author: Author? = null,
) : Serializable ) : Serializable

View file

@ -10,7 +10,6 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.util.LruCache import android.util.LruCache
import android.widget.Toast
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.snackString import ani.dantotsu.snackString
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -23,8 +22,12 @@ import java.io.FileOutputStream
data class ImageData( data class ImageData(
val page: Page, val page: Page,
val source: HttpSource val source: HttpSource
){ ) {
suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? { suspend fun fetchAndProcessImage(
page: Page,
httpSource: HttpSource,
context: Context
): Bitmap? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { try {
// Fetch the image // Fetch the image
@ -52,16 +55,26 @@ data class ImageData(
} }
} }
fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) { fun saveImage(
bitmap: Bitmap,
contentResolver: ContentResolver,
filename: String,
format: Bitmap.CompressFormat,
quality: Int
) {
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply { val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename) put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}") put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga") put(
MediaStore.MediaColumns.RELATIVE_PATH,
"${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga"
)
} }
val uri: Uri? = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) val uri: Uri? =
contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
uri?.let { uri?.let {
contentResolver.openOutputStream(it)?.use { os -> contentResolver.openOutputStream(it)?.use { os ->
@ -69,7 +82,8 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
} }
} }
} else { } else {
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Manga") val directory =
File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Manga")
if (!directory.exists()) { if (!directory.exists()) {
directory.mkdirs() directory.mkdirs()
} }
@ -85,7 +99,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
} }
} }
class MangaCache() { class MangaCache {
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024 / 2).toInt() private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024 / 2).toInt()
private val cache = LruCache<String, ImageData>(maxMemory) private val cache = LruCache<String, ImageData>(maxMemory)

View file

@ -15,7 +15,14 @@ data class MangaChapter(
val scanlator: String? = null, val scanlator: String? = null,
var progress: String? = "" var progress: String? = ""
) : Serializable { ) : Serializable {
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter, chapter.scanlator) constructor(chapter: MangaChapter) : this(
chapter.number,
chapter.link,
chapter.title,
chapter.description,
chapter.sChapter,
chapter.scanlator
)
private val images = mutableListOf<MangaImage>() private val images = mutableListOf<MangaImage>()
fun images(): List<MangaImage> = images fun images(): List<MangaImage> = images

View file

@ -5,16 +5,15 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import androidx.core.content.ContentProviderCompat.requireContext
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemChapterListBinding import ani.dantotsu.databinding.ItemChapterListBinding
import ani.dantotsu.databinding.ItemEpisodeCompactBinding import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.currContext
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -124,7 +123,7 @@ class MangaChapterAdapter(
if (progress != null) { if (progress != null) {
binding.itemChapterTitle.visibility = View.VISIBLE binding.itemChapterTitle.visibility = View.VISIBLE
binding.itemChapterTitle.text = "$progress" binding.itemChapterTitle.text = "$progress"
}else{ } else {
binding.itemChapterTitle.visibility = View.GONE binding.itemChapterTitle.visibility = View.GONE
binding.itemChapterTitle.text = "" binding.itemChapterTitle.text = ""
} }
@ -154,9 +153,10 @@ class MangaChapterAdapter(
// Add chapter number to active coroutines set // Add chapter number to active coroutines set
activeCoroutines.add(chapterNumber) activeCoroutines.add(chapterNumber)
while (activeDownloads.contains(chapterNumber)) { while (activeDownloads.contains(chapterNumber)) {
binding.itemDownload.animate().rotationBy(360f).setDuration(1000).setInterpolator( binding.itemDownload.animate().rotationBy(360f).setDuration(1000)
LinearInterpolator() .setInterpolator(
).start() LinearInterpolator()
).start()
delay(1000) delay(1000)
} }
// Remove chapter number from active coroutines set // Remove chapter number from active coroutines set
@ -171,8 +171,16 @@ class MangaChapterAdapter(
init { init {
val theme = currContext()?.theme val theme = currContext()?.theme
theme?.resolveAttribute(com.google.android.material.R.attr.colorError, typedValue1, true) theme?.resolveAttribute(
theme?.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue2, true) com.google.android.material.R.attr.colorError,
typedValue1,
true
)
theme?.resolveAttribute(
com.google.android.material.R.attr.colorPrimary,
typedValue2,
true
)
itemView.setOnClickListener { itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number) fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)

View file

@ -13,15 +13,12 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.App.Companion.context
import ani.dantotsu.media.anime.handleProgress
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.media.anime.handleProgress
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.MangaReadSources import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
@ -30,7 +27,6 @@ import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.lang.IndexOutOfBoundsException
class MangaReadAdapter( class MangaReadAdapter(
private val media: Media, private val media: Media,
@ -57,12 +53,16 @@ class MangaReadAdapter(
//Wrong Title //Wrong Title
binding.animeSourceSearch.setOnClickListener { binding.animeSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null) SourceSearchDialogFragment().show(
fragment.requireActivity().supportFragmentManager,
null
)
} }
//Source Selection //Source Selection
var source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it } var source =
setLanguageList(media.selected!!.langIndex,source) media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
setLanguageList(media.selected!!.langIndex, source)
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) { if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
binding.animeSource.setText(mangaReadSources.names[source]) binding.animeSource.setText(mangaReadSources.names[source])
mangaReadSources[source].apply { mangaReadSources[source].apply {
@ -70,14 +70,20 @@ class MangaReadAdapter(
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
} }
} }
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, mangaReadSources.names)) binding.animeSource.setAdapter(
ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
mangaReadSources.names
)
)
binding.animeSourceTitle.isSelected = true binding.animeSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ -> binding.animeSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply { fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
source = i source = i
setLanguageList(0,i) setLanguageList(0, i)
} }
subscribeButton(false) subscribeButton(false)
//invalidate if it's the last source //invalidate if it's the last source
@ -92,7 +98,8 @@ class MangaReadAdapter(
fragment.onLangChange(i) fragment.onLangChange(i)
fragment.onSourceChange(media.selected!!.sourceIndex).apply { fragment.onSourceChange(media.selected!!.sourceIndex).apply {
binding.animeSourceTitle.text = showUserText binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } }
setLanguageList(i, source) setLanguageList(i, source)
} }
subscribeButton(false) subscribeButton(false)
@ -139,7 +146,8 @@ class MangaReadAdapter(
} }
binding.animeScanlatorTop.setOnClickListener { binding.animeScanlatorTop.setOnClickListener {
val dialogView = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null) val dialogView =
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
val checkboxContainer = dialogView.findViewById<LinearLayout>(R.id.checkboxContainer) val checkboxContainer = dialogView.findViewById<LinearLayout>(R.id.checkboxContainer)
// Dynamically add checkboxes // Dynamically add checkboxes
@ -149,10 +157,10 @@ class MangaReadAdapter(
text = option text = option
} }
//set checked if it's already selected //set checked if it's already selected
if(media.selected!!.scanlators != null){ if (media.selected!!.scanlators != null) {
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
scanlatorSelectionListener?.onScanlatorsSelected() scanlatorSelectionListener?.onScanlatorsSelected()
}else{ } else {
checkBox.isChecked = true checkBox.isChecked = true
} }
checkboxContainer.addView(checkBox) checkboxContainer.addView(checkBox)
@ -178,8 +186,8 @@ class MangaReadAdapter(
} }
var selected = when (style) { var selected = when (style) {
0 -> binding.animeSourceList 0 -> binding.animeSourceList
1 -> binding.animeSourceCompact 1 -> binding.animeSourceCompact
else -> binding.animeSourceList else -> binding.animeSourceList
} }
selected.alpha = 1f selected.alpha = 1f
@ -217,14 +225,26 @@ class MangaReadAdapter(
for (position in arr.indices) { for (position in arr.indices) {
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1)) val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
val chip = val chip =
ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root ItemChipBinding.inflate(
LayoutInflater.from(fragment.context),
binding.animeSourceChipGroup,
false
).root
chip.isCheckable = true chip.isCheckable = true
fun selected() { fun selected() {
chip.isChecked = true chip.isChecked = true
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0) binding.animeWatchChipScroll.smoothScrollTo(
(chip.left - screenWidth / 2) + (chip.width / 2),
0
)
} }
chip.text = "${names[limit * (position)]} - ${names[last - 1]}" chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.setTextColor(ContextCompat.getColorStateList(fragment.requireContext(), R.color.chip_text_color)) chip.setTextColor(
ContextCompat.getColorStateList(
fragment.requireContext(),
R.color.chip_text_color
)
)
chip.setOnClickListener { chip.setOnClickListener {
selected() selected()
@ -237,7 +257,14 @@ class MangaReadAdapter(
} }
} }
if (select != null) if (select != null)
binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } } binding.animeWatchChipScroll.apply {
post {
scrollTo(
(select.left - screenWidth / 2) + (select.width / 2),
0
)
}
}
} }
} }
@ -259,7 +286,9 @@ class MangaReadAdapter(
val chapter = media.manga.chapters!![chapterKey]!! val chapter = media.manga.chapters!![chapterKey]!!
chapter.scanlator !in hiddenScanlators chapter.scanlator !in hiddenScanlators
} }
val formattedChapters = filteredChapters.map { MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString() } val formattedChapters = filteredChapters.map {
MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
}
if (formattedChapters.contains(continueEp)) { if (formattedChapters.contains(continueEp)) {
continueEp = chapters[formattedChapters.indexOf(continueEp)] continueEp = chapters[formattedChapters.indexOf(continueEp)]
binding.animeSourceContinue.visibility = View.VISIBLE binding.animeSourceContinue.visibility = View.VISIBLE
@ -317,10 +346,17 @@ class MangaReadAdapter(
} }
try { try {
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang) binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
}catch (e: IndexOutOfBoundsException) { } catch (e: IndexOutOfBoundsException) {
binding?.animeSourceLanguage?.setText(parser.extension.sources.firstOrNull()?.lang ?: "Unknown") binding?.animeSourceLanguage?.setText(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
)
} }
binding?.animeSourceLanguage?.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, parser.extension.sources.map { it.lang })) binding?.animeSourceLanguage?.setAdapter(
ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
parser.extension.sources.map { it.lang })
)
} }
} }
@ -328,7 +364,8 @@ class MangaReadAdapter(
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
RecyclerView.ViewHolder(binding.root)
} }
interface ScanlatorSelectionListener { interface ScanlatorSelectionListener {

View file

@ -1,5 +1,6 @@
package ani.dantotsu.media.manga package ani.dantotsu.media.manga
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@ -16,6 +17,7 @@ import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.Toast import android.widget.Toast
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp import androidx.core.math.MathUtils.clamp
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@ -30,11 +32,11 @@ import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.manga.MangaDownloaderService import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.ServiceDataSingleton import ani.dantotsu.download.manga.MangaServiceDataSingleton
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser import ani.dantotsu.parsers.MangaParser
@ -59,10 +61,8 @@ import uy.kohesive.injekt.api.get
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
import android.Manifest
import androidx.core.app.ActivityCompat
open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
private var _binding: FragmentAnimeWatchBinding? = null private var _binding: FragmentAnimeWatchBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels() private val model: MediaDetailsViewModel by activityViewModels()
@ -85,7 +85,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
var continueEp: Boolean = false var continueEp: Boolean = false
var loaded = false var loaded = false
val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } val uiSettings = loadData("ui_settings", toast = false)
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -105,7 +106,12 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
addAction(ACTION_DOWNLOAD_PROGRESS) addAction(ACTION_DOWNLOAD_PROGRESS)
} }
ContextCompat.registerReceiver(requireContext(), downloadStatusReceiver, intentFilter, ContextCompat.RECEIVER_EXPORTED) ContextCompat.registerReceiver(
requireContext(),
downloadStatusReceiver,
intentFilter,
ContextCompat.RECEIVER_EXPORTED
)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp screenWidth = resources.displayMetrics.widthPixels.dp
@ -120,10 +126,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val style = chapterAdapter.getItemViewType(position) val style = chapterAdapter.getItemViewType(position)
return when (position) { return when (position) {
0 -> maxGridSize 0 -> maxGridSize
else -> when (style) { else -> when (style) {
0 -> maxGridSize 0 -> maxGridSize
1 -> 1 1 -> 1
else -> maxGridSize else -> maxGridSize
} }
} }
@ -146,7 +152,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
if (media.format == "MANGA" || media.format == "ONE SHOT") { if (media.format == "MANGA" || media.format == "ONE SHOT") {
media.selected = model.loadSelected(media) media.selected = model.loadSelected(media)
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id) subscribed =
SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
style = media.selected!!.recyclerStyle style = media.selected!!.recyclerStyle
reverse = media.selected!!.recyclerReversed reverse = media.selected!!.recyclerReversed
@ -156,13 +163,15 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!) headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
headerAdapter.scanlatorSelectionListener = this headerAdapter.scanlatorSelectionListener = this
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this) chapterAdapter =
MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
for (download in downloadManager.mangaDownloads){ for (download in downloadManager.mangaDownloads) {
chapterAdapter.stopDownload(download.chapter) chapterAdapter.stopDownload(download.chapter)
} }
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter) binding.animeSourceRecycler.adapter =
ConcatAdapter(headerAdapter, chapterAdapter)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
model.loadMangaChapters(media, media.selected!!.sourceIndex) model.loadMangaChapters(media, media.selected!!.sourceIndex)
@ -173,7 +182,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
} }
} else { } else {
binding.animeNotSupported.visibility = View.VISIBLE binding.animeNotSupported.visibility = View.VISIBLE
binding.animeNotSupported.text = getString(R.string.not_supported, media.format ?: "") binding.animeNotSupported.text =
getString(R.string.not_supported, media.format ?: "")
} }
} }
} }
@ -207,7 +217,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val limit = when { val limit = when {
(divisions < 25) -> 25 (divisions < 25) -> 25
(divisions < 50) -> 50 (divisions < 50) -> 50
else -> 100 else -> 100
} }
headerAdapter.clearChips() headerAdapter.clearChips()
if (total > limit) { if (total > limit) {
@ -302,7 +312,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
) )
} }
fun openSettings(pkg: MangaExtension.Installed){ fun openSettings(pkg: MangaExtension.Installed) {
val changeUIVisibility: (Boolean) -> Unit = { show -> val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = requireActivity() as MediaDetailsActivity val activity = requireActivity() as MediaDetailsActivity
val visibility = if (show) View.VISIBLE else View.GONE val visibility = if (show) View.VISIBLE else View.GONE
@ -310,9 +320,9 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try{ try {
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
}catch (e: ClassCastException){ } catch (e: ClassCastException) {
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
} }
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility = activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
@ -335,7 +345,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
// Move the fragment transaction here // Move the fragment transaction here
val fragment = val fragment =
MangaSourcePreferencesFragment().getInstance(selectedSetting.id){ MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
changeUIVisibility(true) changeUIVisibility(true)
loadChapters(media.selected!!.sourceIndex, true) loadChapters(media.selected!!.sourceIndex, true)
} }
@ -353,7 +363,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
.show() .show()
} else { } else {
// If there's only one setting, proceed with the fragment transaction // If there's only one setting, proceed with the fragment transaction
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id){ val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
changeUIVisibility(true) changeUIVisibility(true)
loadChapters(media.selected!!.sourceIndex, true) loadChapters(media.selected!!.sourceIndex, true)
} }
@ -376,7 +386,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
media.manga?.chapters?.get(i)?.let { media.manga?.chapters?.get(i)?.let {
media.manga?.selectedChapter = i media.manga?.selectedChapter = i
model.saveSelected(media.id, media.selected!!, requireActivity()) model.saveSelected(media.id, media.selected!!, requireActivity())
ChapterLoaderDialog.newInstance(it, true).show(requireActivity().supportFragmentManager, "dialog") ChapterLoaderDialog.newInstance(it, true)
.show(requireActivity().supportFragmentManager, "dialog")
} }
} }
@ -393,7 +404,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
model.continueMedia = false model.continueMedia = false
media.manga?.chapters?.get(i)?.let { chapter -> media.manga?.chapters?.get(i)?.let { chapter ->
val parser = model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser val parser =
model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
parser?.let { parser?.let {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val images = parser.imageList("", chapter.sChapter) val images = parser.imageList("", chapter.sChapter)
@ -408,15 +420,15 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
simultaneousDownloads = 2 simultaneousDownloads = 2
) )
ServiceDataSingleton.downloadQueue.offer(downloadTask) MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
// If the service is not already running, start it // If the service is not already running, start it
if (!ServiceDataSingleton.isServiceRunning) { if (!MangaServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, MangaDownloaderService::class.java) val intent = Intent(context, MangaDownloaderService::class.java)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent) ContextCompat.startForegroundService(requireContext(), intent)
} }
ServiceDataSingleton.isServiceRunning = true MangaServiceDataSingleton.isServiceRunning = true
} }
// Inform the adapter that the download has started // Inform the adapter that the download has started
@ -439,10 +451,17 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
} }
fun onMangaChapterRemoveDownloadClick(i: String){ fun onMangaChapterRemoveDownloadClick(i: String) {
downloadManager.removeDownload(Download(media.nameMAL?:media.nameRomaji, i, Download.Type.MANGA)) downloadManager.removeDownload(
Download(
media.nameMAL ?: media.nameRomaji,
i,
Download.Type.MANGA
)
)
chapterAdapter.deleteDownload(i) chapterAdapter.deleteDownload(i)
} }
fun onMangaChapterStopDownloadClick(i: String) { fun onMangaChapterStopDownloadClick(i: String) {
val cancelIntent = Intent().apply { val cancelIntent = Intent().apply {
action = MangaDownloaderService.ACTION_CANCEL_DOWNLOAD action = MangaDownloaderService.ACTION_CANCEL_DOWNLOAD
@ -451,11 +470,19 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
requireContext().sendBroadcast(cancelIntent) requireContext().sendBroadcast(cancelIntent)
// Remove the download from the manager and update the UI // Remove the download from the manager and update the UI
downloadManager.removeDownload(Download(media.nameMAL?:media.nameRomaji, i, Download.Type.MANGA)) downloadManager.removeDownload(
Download(
media.nameMAL ?: media.nameRomaji,
i,
Download.Type.MANGA
)
)
chapterAdapter.purgeDownload(i) chapterAdapter.purgeDownload(i)
} }
private val downloadStatusReceiver = object : BroadcastReceiver() { private val downloadStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (!this@MangaReadFragment::chapterAdapter.isInitialized) return
when (intent.action) { when (intent.action) {
ACTION_DOWNLOAD_STARTED -> { ACTION_DOWNLOAD_STARTED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER) val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
@ -491,8 +518,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
//Find latest chapter for subscription //Find latest chapter for subscription
selected.latest = media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f selected.latest =
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest =
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleChapters() headerAdapter.handleChapters()
@ -501,7 +530,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
if (media.manga!!.chapters != null) { if (media.manga!!.chapters != null) {
val end = if (end != null && end!! < media.manga!!.chapters!!.size) end else null val end = if (end != null && end!! < media.manga!!.chapters!!.size) end else null
arr.addAll( arr.addAll(
media.manga!!.chapters!!.values.toList().slice(start..(end ?: (media.manga!!.chapters!!.size - 1))) media.manga!!.chapters!!.values.toList()
.slice(start..(end ?: (media.manga!!.chapters!!.size - 1)))
) )
if (reverse) if (reverse)
arr = (arr.reversed() as? ArrayList<MangaChapter>) ?: arr arr = (arr.reversed() as? ArrayList<MangaChapter>) ?: arr

View file

@ -14,8 +14,8 @@ import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.settings.CurrentReaderSettings import ani.dantotsu.settings.CurrentReaderSettings
import com.alexvasilkov.gestures.views.GestureFrameLayout import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@ -23,12 +23,9 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ani.dantotsu.media.manga.MangaCache
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -118,7 +115,10 @@ abstract class BaseImageAdapter(
abstract suspend fun loadImage(position: Int, parent: View): Boolean abstract suspend fun loadImage(position: Int, parent: View): Boolean
companion object { companion object {
suspend fun Context.loadBitmap_old(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? { //still used in some places suspend fun Context.loadBitmap_old(
link: FileUrl,
transforms: List<BitmapTransformation>
): Bitmap? { //still used in some places
return tryWithSuspend { return tryWithSuspend {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Glide.with(this@loadBitmap_old) Glide.with(this@loadBitmap_old)
@ -135,8 +135,7 @@ abstract class BaseImageAdapter(
.let { .let {
if (transforms.isNotEmpty()) { if (transforms.isNotEmpty()) {
it.transform(*transforms.toTypedArray()) it.transform(*transforms.toTypedArray())
} } else {
else {
it it
} }
} }
@ -146,7 +145,10 @@ abstract class BaseImageAdapter(
} }
} }
suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? { suspend fun Context.loadBitmap(
link: FileUrl,
transforms: List<BitmapTransformation>
): Bitmap? {
return tryWithSuspend { return tryWithSuspend {
val mangaCache = uy.kohesive.injekt.Injekt.get<MangaCache>() val mangaCache = uy.kohesive.injekt.Injekt.get<MangaCache>()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -161,7 +163,11 @@ abstract class BaseImageAdapter(
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
} else { } else {
mangaCache.get(link.url)?.let { imageData -> mangaCache.get(link.url)?.let { imageData ->
val bitmap = imageData.fetchAndProcessImage(imageData.page, imageData.source, context = this@loadBitmap) val bitmap = imageData.fetchAndProcessImage(
imageData.page,
imageData.source,
context = this@loadBitmap
)
it.load(bitmap) it.load(bitmap)
.skipMemoryCache(true) .skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)

View file

@ -14,9 +14,9 @@ import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.currActivity import ani.dantotsu.currActivity
import ani.dantotsu.databinding.BottomSheetSelectorBinding import ani.dantotsu.databinding.BottomSheetSelectorBinding
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaSingleton import ani.dantotsu.media.MediaSingleton
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.tryWith import ani.dantotsu.tryWith
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -29,8 +29,8 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
val model: MediaDetailsViewModel by activityViewModels() val model: MediaDetailsViewModel by activityViewModels()
private val launch : Boolean by lazy { arguments?.getBoolean("launch", false) ?: false } private val launch: Boolean by lazy { arguments?.getBoolean("launch", false) ?: false }
private val chp : MangaChapter by lazy { arguments?.getSerialized("next")!! } private val chp: MangaChapter by lazy { arguments?.getSerialized("next")!! }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var loaded = false var loaded = false
@ -47,13 +47,21 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
loaded = true loaded = true
binding.selectorAutoText.text = chp.title binding.selectorAutoText.text = chp.title
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
if(model.loadMangaChapterImages(chp, m.selected!!, m.nameMAL?:m.nameRomaji)) { if (model.loadMangaChapterImages(
chp,
m.selected!!,
m.nameMAL ?: m.nameRomaji
)
) {
val activity = currActivity() val activity = currActivity()
activity?.runOnUiThread { activity?.runOnUiThread {
tryWith { dismiss() } tryWith { dismiss() }
if(launch) { if (launch) {
MediaSingleton.media = m MediaSingleton.media = m
val intent = Intent(activity, MangaReaderActivity::class.java)//.apply { putExtra("media", m) } val intent = Intent(
activity,
MangaReaderActivity::class.java
)//.apply { putExtra("media", m) }
activity.startActivity(intent) activity.startActivity(intent)
} }
} }
@ -63,7 +71,11 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false) _binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
val window = dialog?.window val window = dialog?.window
window?.statusBarColor = Color.TRANSPARENT window?.statusBarColor = Color.TRANSPARENT

View file

@ -30,15 +30,15 @@ open class ImageAdapter(
inner class ImageViewHolder(binding: ItemImageBinding) : RecyclerView.ViewHolder(binding.root) inner class ImageViewHolder(binding: ItemImageBinding) : RecyclerView.ViewHolder(binding.root)
open suspend fun loadBitmap(position: Int, parent: View) : Bitmap? { open suspend fun loadBitmap(position: Int, parent: View): Bitmap? {
val link = images.getOrNull(position)?.url ?: return null val link = images.getOrNull(position)?.url ?: return null
if (link.url.isEmpty()) return null if (link.url.isEmpty()) return null
val transforms = mutableListOf<BitmapTransformation>() val transforms = mutableListOf<BitmapTransformation>()
val parserTransformation = activity.getTransformation(images[position]) val parserTransformation = activity.getTransformation(images[position])
if(parserTransformation!=null) transforms.add(parserTransformation) if (parserTransformation != null) transforms.add(parserTransformation)
if(settings.cropBorders) { if (settings.cropBorders) {
transforms.add(RemoveBordersTransformation(true, settings.cropBorderThreshold)) transforms.add(RemoveBordersTransformation(true, settings.cropBorderThreshold))
transforms.add(RemoveBordersTransformation(false, settings.cropBorderThreshold)) transforms.add(RemoveBordersTransformation(false, settings.cropBorderThreshold))
} }
@ -47,7 +47,8 @@ open class ImageAdapter(
} }
override suspend fun loadImage(position: Int, parent: View): Boolean { override suspend fun loadImage(position: Int, parent: View): Boolean {
val imageView = parent.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures) ?: return false val imageView = parent.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
?: return false
val progress = parent.findViewById<View>(R.id.imgProgProgress) ?: return false val progress = parent.findViewById<View>(R.id.imgProgProgress) ?: return false
imageView.recycle() imageView.recycle()
imageView.visibility = View.GONE imageView.visibility = View.GONE
@ -60,10 +61,12 @@ open class ImageAdapter(
if (settings.layout != PAGED) if (settings.layout != PAGED)
parent.updateLayoutParams { parent.updateLayoutParams {
if (settings.direction != LEFT_TO_RIGHT && settings.direction != RIGHT_TO_LEFT) { if (settings.direction != LEFT_TO_RIGHT && settings.direction != RIGHT_TO_LEFT) {
sHeight = if (settings.wrapImages) bitmap.height else (sWidth * bitmap.height * 1f / bitmap.width).toInt() sHeight =
if (settings.wrapImages) bitmap.height else (sWidth * bitmap.height * 1f / bitmap.width).toInt()
height = sHeight height = sHeight
} else { } else {
sWidth = if (settings.wrapImages) bitmap.width else (sHeight * bitmap.width * 1f / bitmap.height).toInt() sWidth =
if (settings.wrapImages) bitmap.width else (sHeight * bitmap.width * 1f / bitmap.height).toInt()
width = sWidth width = sWidth
} }
} }
@ -73,7 +76,8 @@ open class ImageAdapter(
val parentArea = sWidth * sHeight * 1f val parentArea = sWidth * sHeight * 1f
val bitmapArea = bitmap.width * bitmap.height * 1f val bitmapArea = bitmap.width * bitmap.height * 1f
val scale = if (parentArea < bitmapArea) (bitmapArea / parentArea) else (parentArea / bitmapArea) val scale =
if (parentArea < bitmapArea) (bitmapArea / parentArea) else (parentArea / bitmapArea)
imageView.maxScale = scale * 1.1f imageView.maxScale = scale * 1.1f
imageView.minScale = scale imageView.minScale = scale

View file

@ -3,6 +3,7 @@ package ani.dantotsu.media.manga.mangareader
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
@ -25,6 +26,8 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.discord.Discord import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.discord.DiscordService
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
import ani.dantotsu.connections.discord.RPC import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityMangaReaderBinding import ani.dantotsu.databinding.ActivityMangaReaderBinding
@ -35,7 +38,7 @@ import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.manga.MangaNameAdapter import ani.dantotsu.media.manga.MangaNameAdapter
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.LangSet
import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaImage import ani.dantotsu.parsers.MangaImage
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
@ -46,7 +49,6 @@ import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
import ani.dantotsu.settings.ReaderSettings import ani.dantotsu.settings.ReaderSettings
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import com.alexvasilkov.gestures.views.GestureFrameLayout import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
@ -54,8 +56,6 @@ import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -94,14 +94,17 @@ class MangaReaderActivity : AppCompatActivity() {
var sliding = false var sliding = false
var isAnimating = false var isAnimating = false
private var rpc : RPC? = null private var rpc: RPC? = null
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !settings.showSystemBars) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !settings.showSystemBars) {
val displayCutout = window.decorView.rootWindowInsets.displayCutout val displayCutout = window.decorView.rootWindowInsets.displayCutout
if (displayCutout != null) { if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) { if (displayCutout.boundingRects.size > 0) {
notchHeight = min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height()) notchHeight = min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
checkNotch() checkNotch()
} }
} }
@ -121,14 +124,18 @@ class MangaReaderActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
mangaCache.clear() mangaCache.clear()
rpc?.close() if (isOnline(baseContext)) { //TODO:
DiscordServiceRunningSingleton.running = false
val stopIntent = Intent(this, DiscordService::class.java)
stopService(stopIntent)
}
super.onDestroy() super.onDestroy()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this) LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityMangaReaderBinding.inflate(layoutInflater) binding = ActivityMangaReaderBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@ -140,8 +147,14 @@ ThemeManager(this).applyTheme()
progress { finish() } progress { finish() }
} }
settings = loadData("reader_settings", this) ?: ReaderSettings().apply { saveData("reader_settings", this) } settings = loadData("reader_settings", this)
uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } ?: ReaderSettings().apply { saveData("reader_settings", this) }
uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply {
saveData(
"ui_settings",
this
)
}
controllerDuration = (uiSettings.animationSpeed * 200).toLong() controllerDuration = (uiSettings.animationSpeed * 200).toLong()
hideBars() hideBars()
@ -166,9 +179,11 @@ ThemeManager(this).applyTheme()
if (fromUser) { if (fromUser) {
sliding = true sliding = true
if (settings.default.layout != PAGED) if (settings.default.layout != PAGED)
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 } ?: 1)) binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 }
?: 1))
else else
binding.mangaReaderPager.currentItem = (value.toInt() - 1) / (dualPage { 2 } ?: 1) binding.mangaReaderPager.currentItem =
(value.toInt() - 1) / (dualPage { 2 } ?: 1)
pageSliderHide() pageSliderHide()
} }
} }
@ -194,25 +209,25 @@ ThemeManager(this).applyTheme()
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
binding.mangaReaderSource.visibility = if (settings.showSource) View.VISIBLE else View.GONE binding.mangaReaderSource.visibility = if (settings.showSource) View.VISIBLE else View.GONE
if(model.mangaReadSources!!.names.isEmpty()){ if (model.mangaReadSources!!.names.isEmpty()) {
//try to reload sources //try to reload sources
try { try {
if (media.isAdult) { if (media.isAdult) {
val mangaSources = MangaSources val mangaSources = MangaSources
val scope = lifecycleScope val scope = lifecycleScope
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow) mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
}
model.mangaReadSources = mangaSources
} else {
val mangaSources = HMangaSources
val scope = lifecycleScope
scope.launch(Dispatchers.IO) {
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
}
model.mangaReadSources = mangaSources
} }
model.mangaReadSources = mangaSources } catch (e: Exception) {
}else{
val mangaSources = HMangaSources
val scope = lifecycleScope
scope.launch(Dispatchers.IO) {
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow)
}
model.mangaReadSources = mangaSources
}
}catch (e: Exception){
Firebase.crashlytics.recordException(e) Firebase.crashlytics.recordException(e)
logError(e) logError(e)
} }
@ -221,7 +236,8 @@ ThemeManager(this).applyTheme()
if (media.selected!!.sourceIndex >= model.mangaReadSources!!.names.size) { if (media.selected!!.sourceIndex >= model.mangaReadSources!!.names.size) {
media.selected!!.sourceIndex = 0 media.selected!!.sourceIndex = 0
} }
binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.sourceIndex] binding.mangaReaderSource.text =
model.mangaReadSources!!.names[media.selected!!.sourceIndex]
binding.mangaReaderTitle.text = media.userPreferredName binding.mangaReaderTitle.text = media.userPreferredName
@ -234,39 +250,49 @@ ThemeManager(this).applyTheme()
chaptersTitleArr.add("${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") "" else "Chapter "}${chapter.number}${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") " : " + chapter.title else ""}") chaptersTitleArr.add("${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") "" else "Chapter "}${chapter.number}${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") " : " + chapter.title else ""}")
} }
showProgressDialog = if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") != true else false showProgressDialog =
if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") != true else false
progressDialog = progressDialog =
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true) if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true)
AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.title_update_progress)).apply { AlertDialog.Builder(this, R.style.MyPopup)
setMultiChoiceItems( .setTitle(getString(R.string.title_update_progress)).apply {
arrayOf(getString(R.string.dont_ask_again, media.userPreferredName)), setMultiChoiceItems(
booleanArrayOf(false) arrayOf(getString(R.string.dont_ask_again, media.userPreferredName)),
) { _, _, isChecked -> booleanArrayOf(false)
if (isChecked) progressDialog = null ) { _, _, isChecked ->
saveData("${media.id}_progressDialog", isChecked) if (isChecked) progressDialog = null
showProgressDialog = isChecked saveData("${media.id}_progressDialog", isChecked)
showProgressDialog = isChecked
}
setOnCancelListener { hideBars() }
} }
setOnCancelListener { hideBars() }
}
else null else null
//Chapter Change //Chapter Change
fun change(index: Int) { fun change(index: Int) {
mangaCache.clear() mangaCache.clear()
saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this) saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this)
ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!).show(supportFragmentManager, "dialog") ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!)
.show(supportFragmentManager, "dialog")
} }
//ChapterSelector //ChapterSelector
binding.mangaReaderChapterSelect.adapter = NoPaddingArrayAdapter(this, R.layout.item_dropdown, chaptersTitleArr) binding.mangaReaderChapterSelect.adapter =
NoPaddingArrayAdapter(this, R.layout.item_dropdown, chaptersTitleArr)
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex) binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
binding.mangaReaderChapterSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.mangaReaderChapterSelect.onItemSelectedListener =
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { object : AdapterView.OnItemSelectedListener {
if (position != currentChapterIndex) change(position) override fun onItemSelected(
} p0: AdapterView<*>?,
p1: View?,
position: Int,
p3: Long
) {
if (position != currentChapterIndex) change(position)
}
override fun onNothingSelected(parent: AdapterView<*>) {} override fun onNothingSelected(parent: AdapterView<*>) {}
} }
binding.mangaReaderSettings.setSafeOnClickListener { binding.mangaReaderSettings.setSafeOnClickListener {
ReaderSettingsDialogFragment.newInstance().show(supportFragmentManager, "settings") ReaderSettingsDialogFragment.newInstance().show(supportFragmentManager, "settings")
@ -297,40 +323,69 @@ ThemeManager(this).applyTheme()
saveData("${media.id}_current_chp", chap.number, this) saveData("${media.id}_current_chp", chap.number, this)
currentChapterIndex = chaptersArr.indexOf(chap.number) currentChapterIndex = chaptersArr.indexOf(chap.number)
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex) binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: "" binding.mangaReaderNextChap.text =
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: "" chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
applySettings() applySettings()
rpc?.close() val context = this
rpc = Discord.defaultRPC() if (isOnline(context)) {
rpc?.send { lifecycleScope.launch {
type = RPC.Type.WATCHING val presence = RPC.createPresence(
activityName = media.userPreferredName RPC.Companion.RPCData(
details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number) applicationId = Discord.application_Id,
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}" type = RPC.Type.WATCHING,
media.cover?.let { cover -> activityName = media.userPreferredName,
largeImage = RPC.Link(media.userPreferredName, cover) details = chap.title?.takeIf { it.isNotEmpty() }
} ?: getString(R.string.chapter_num, chap.number),
media.shareLink?.let { link -> state = "${chap.number}/${media.manga?.totalChapters ?: "??"}",
buttons.add(0, RPC.Link(getString(R.string.view_manga), link)) largeImage = media.cover?.let { cover ->
RPC.Link(media.userPreferredName, cover)
},
smallImage = RPC.Link(
"Dantotsu",
Discord.small_Image
),
buttons = mutableListOf(
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
RPC.Link(
"Stream on Dantotsu",
"https://github.com/rebelonion/Dantotsu/"
)
)
)
)
val intent = Intent(context, DiscordService::class.java).apply {
putExtra("presence", presence)
}
DiscordServiceRunningSingleton.running = true
startService(intent)
} }
} }
} }
} }
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!, media.nameMAL?:media.nameRomaji) } scope.launch(Dispatchers.IO) {
model.loadMangaChapterImages(
chapter,
media.selected!!,
media.nameMAL ?: media.nameRomaji
)
}
} }
private val snapHelper = PagerSnapHelper() private val snapHelper = PagerSnapHelper()
fun <T> dualPage(callback: () -> T): T? { fun <T> dualPage(callback: () -> T): T? {
return when (settings.default.dualPageMode) { return when (settings.default.dualPageMode) {
No -> null No -> null
Automatic -> { Automatic -> {
val orientation = resources.configuration.orientation val orientation = resources.configuration.orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) callback.invoke() if (orientation == Configuration.ORIENTATION_LANDSCAPE) callback.invoke()
else null else null
} }
Force -> callback.invoke()
Force -> callback.invoke()
} }
} }
@ -361,7 +416,8 @@ ThemeManager(this).applyTheme()
maxChapterPage = chapImages.size.toLong() maxChapterPage = chapImages.size.toLong()
saveData("${media.id}_${chapter.number}_max", maxChapterPage) saveData("${media.id}_${chapter.number}_max", maxChapterPage)
imageAdapter = dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter) imageAdapter =
dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter)
if (chapImages.size > 1) { if (chapImages.size > 1) {
binding.mangaReaderSlider.apply { binding.mangaReaderSlider.apply {
@ -382,8 +438,10 @@ ThemeManager(this).applyTheme()
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)) { if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)) {
binding.mangaReaderSwipy.vertical = true binding.mangaReaderSwipy.vertical = true
if (settings.default.direction == TOP_TO_BOTTOM) { if (settings.default.direction == TOP_TO_BOTTOM) {
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onTopSwiped = { binding.mangaReaderSwipy.onTopSwiped = {
binding.mangaReaderPreviousChapter.performClick() binding.mangaReaderPreviousChapter.performClick()
} }
@ -391,8 +449,10 @@ ThemeManager(this).applyTheme()
binding.mangaReaderNextChapter.performClick() binding.mangaReaderNextChapter.performClick()
} }
} else { } else {
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onTopSwiped = { binding.mangaReaderSwipy.onTopSwiped = {
binding.mangaReaderNextChapter.performClick() binding.mangaReaderNextChapter.performClick()
} }
@ -415,8 +475,10 @@ ThemeManager(this).applyTheme()
} else { } else {
binding.mangaReaderSwipy.vertical = false binding.mangaReaderSwipy.vertical = false
if (settings.default.direction == RIGHT_TO_LEFT) { if (settings.default.direction == RIGHT_TO_LEFT) {
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onLeftSwiped = { binding.mangaReaderSwipy.onLeftSwiped = {
binding.mangaReaderNextChapter.performClick() binding.mangaReaderNextChapter.performClick()
} }
@ -424,8 +486,10 @@ ThemeManager(this).applyTheme()
binding.mangaReaderPreviousChapter.performClick() binding.mangaReaderPreviousChapter.performClick()
} }
} else { } else {
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onLeftSwiped = { binding.mangaReaderSwipy.onLeftSwiped = {
binding.mangaReaderPreviousChapter.performClick() binding.mangaReaderPreviousChapter.performClick()
} }
@ -450,7 +514,8 @@ ThemeManager(this).applyTheme()
if (settings.default.layout != PAGED) { if (settings.default.layout != PAGED) {
binding.mangaReaderRecyclerContainer.visibility = View.VISIBLE binding.mangaReaderRecyclerContainer.visibility = View.VISIBLE
binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled = settings.default.rotation binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled =
settings.default.rotation
val detector = GestureDetectorCompat(this, object : GesturesListener() { val detector = GestureDetectorCompat(this, object : GesturesListener() {
override fun onLongPress(e: MotionEvent) { override fun onLongPress(e: MotionEvent) {
@ -458,18 +523,31 @@ ThemeManager(this).applyTheme()
child ?: return@let false child ?: return@let false
val pos = binding.mangaReaderRecycler.getChildAdapterPosition(child) val pos = binding.mangaReaderRecycler.getChildAdapterPosition(child)
val callback: (ImageViewDialog) -> Unit = { dialog -> val callback: (ImageViewDialog) -> Unit = { dialog ->
lifecycleScope.launch { imageAdapter?.loadImage(pos, child as GestureFrameLayout) } lifecycleScope.launch {
binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) imageAdapter?.loadImage(
pos,
child as GestureFrameLayout
)
}
binding.mangaReaderRecycler.performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS
)
dialog.dismiss() dialog.dismiss()
} }
dualPage { dualPage {
val page = chapter.dualPages().getOrNull(pos) ?: return@dualPage false val page =
chapter.dualPages().getOrNull(pos) ?: return@dualPage false
val nextPage = page.second val nextPage = page.second
if (settings.default.direction != LEFT_TO_RIGHT && nextPage != null) if (settings.default.direction != LEFT_TO_RIGHT && nextPage != null)
onImageLongClicked(pos * 2, nextPage, page.first, callback) onImageLongClicked(pos * 2, nextPage, page.first, callback)
else else
onImageLongClicked(pos * 2, page.first, nextPage, callback) onImageLongClicked(pos * 2, page.first, nextPage, callback)
} ?: onImageLongClicked(pos, chapImages.getOrNull(pos) ?: return@let false, null, callback) } ?: onImageLongClicked(
pos,
chapImages.getOrNull(pos) ?: return@let false,
null,
callback
)
} }
) binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) ) binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
super.onLongPress(e) super.onLongPress(e)
@ -511,12 +589,16 @@ ThemeManager(this).applyTheme()
&& (!v.canScrollVertically(-1) || !v.canScrollVertically(1))) && (!v.canScrollVertically(-1) || !v.canScrollVertically(1)))
|| ||
((direction == LEFT_TO_RIGHT || direction == RIGHT_TO_LEFT) ((direction == LEFT_TO_RIGHT || direction == RIGHT_TO_LEFT)
&& (!v.canScrollHorizontally(-1) || !v.canScrollHorizontally(1))) && (!v.canScrollHorizontally(-1) || !v.canScrollHorizontally(
1
)))
) { ) {
handleController(true) handleController(true)
} else handleController(false) } else handleController(false)
} }
updatePageNumber(manager.findLastVisibleItemPosition().toLong() * (dualPage { 2 } ?: 1) + 1) updatePageNumber(
manager.findLastVisibleItemPosition().toLong() * (dualPage { 2 }
?: 1) + 1)
super.onScrolled(v, dx, dy) super.onScrolled(v, dx, dy)
} }
}) })
@ -578,7 +660,7 @@ ThemeManager(this).applyTheme()
private var onVolumeDown: (() -> Unit)? = null private var onVolumeDown: (() -> Unit)? = null
override fun dispatchKeyEvent(event: KeyEvent): Boolean { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return when (event.keyCode) { return when (event.keyCode) {
KEYCODE_VOLUME_UP, KEYCODE_DPAD_UP, KEYCODE_PAGE_UP -> { KEYCODE_VOLUME_UP, KEYCODE_DPAD_UP, KEYCODE_PAGE_UP -> {
if (event.keyCode == KEYCODE_VOLUME_UP) if (event.keyCode == KEYCODE_VOLUME_UP)
if (!settings.default.volumeButtons) if (!settings.default.volumeButtons)
return false return false
@ -587,6 +669,7 @@ ThemeManager(this).applyTheme()
true true
} else false } else false
} }
KEYCODE_VOLUME_DOWN, KEYCODE_DPAD_DOWN, KEYCODE_PAGE_DOWN -> { KEYCODE_VOLUME_DOWN, KEYCODE_DPAD_DOWN, KEYCODE_PAGE_DOWN -> {
if (event.keyCode == KEYCODE_VOLUME_DOWN) if (event.keyCode == KEYCODE_VOLUME_DOWN)
if (!settings.default.volumeButtons) if (!settings.default.volumeButtons)
@ -596,7 +679,8 @@ ThemeManager(this).applyTheme()
true true
} else false } else false
} }
else -> {
else -> {
super.dispatchKeyEvent(event) super.dispatchKeyEvent(event)
} }
} }
@ -670,8 +754,14 @@ ThemeManager(this).applyTheme()
isContVisible = false isContVisible = false
if (!isAnimating) { if (!isAnimating) {
isAnimating = true isAnimating = true
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f).setDuration(controllerDuration).start() ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f)
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 0f, 128f) .setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(
binding.mangaReaderBottomLayout,
"translationY",
0f,
128f
)
.apply { interpolator = overshoot;duration = controllerDuration;start() } .apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f) ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f)
.apply { interpolator = overshoot;duration = controllerDuration;start() } .apply { interpolator = overshoot;duration = controllerDuration;start() }
@ -680,7 +770,8 @@ ThemeManager(this).applyTheme()
} else { } else {
isContVisible = true isContVisible = true
binding.mangaReaderCont.visibility = View.VISIBLE binding.mangaReaderCont.visibility = View.VISIBLE
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 0f, 1f).setDuration(controllerDuration).start() ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 0f, 1f)
.setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", -128f, 0f) ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", -128f, 0f)
.apply { interpolator = overshoot;duration = controllerDuration;start() } .apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 128f, 0f) ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 128f, 0f)
@ -706,7 +797,7 @@ ThemeManager(this).applyTheme()
model.loadMangaChapterImages( model.loadMangaChapterImages(
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!, chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
media.selected!!, media.selected!!,
media.nameMAL?:media.nameRomaji, media.nameMAL ?: media.nameRomaji,
false false
) )
loading = false loading = false
@ -719,7 +810,11 @@ ThemeManager(this).applyTheme()
progressDialog?.setCancelable(false) progressDialog?.setCancelable(false)
?.setPositiveButton(getString(R.string.yes)) { dialog, _ -> ?.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
saveData("${media.id}_save_progress", true) saveData("${media.id}_save_progress", true)
updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString()) updateProgress(
media,
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
.toString()
)
dialog.dismiss() dialog.dismiss()
runnable.run() runnable.run()
} }
@ -731,7 +826,11 @@ ThemeManager(this).applyTheme()
progressDialog?.show() progressDialog?.show()
} else { } else {
if (loadData<Boolean>("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true) if (loadData<Boolean>("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true)
updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString()) updateProgress(
media,
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
.toString()
)
runnable.run() runnable.run()
} }
} else { } else {

View file

@ -10,14 +10,15 @@ import kotlin.math.max
class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean) : class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean) :
LinearLayoutManager(context, orientation, reverseLayout) { LinearLayoutManager(context, orientation, reverseLayout) {
private val mOrientationHelper: OrientationHelper = OrientationHelper.createOrientationHelper(this, orientation) private val mOrientationHelper: OrientationHelper =
OrientationHelper.createOrientationHelper(this, orientation)
/** /**
* As [LinearLayoutManager.collectAdjacentPrefetchPositions] will prefetch one view for us, * As [LinearLayoutManager.collectAdjacentPrefetchPositions] will prefetch one view for us,
* we only need to prefetch additional ones. * we only need to prefetch additional ones.
*/ */
var preloadItemCount = 1 var preloadItemCount = 1
set(count){ set(count) {
require(count >= 1) { "preloadItemCount must not be smaller than 1!" } require(count >= 1) { "preloadItemCount must not be smaller than 1!" }
field = count - 1 field = count - 1
} }
@ -37,7 +38,8 @@ class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayo
val currentPosition: Int = getPosition(child ?: return) + layoutDirection val currentPosition: Int = getPosition(child ?: return) + layoutDirection
if (layoutDirection == 1) { if (layoutDirection == 1) {
val scrollingOffset = (mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.endAfterPadding) val scrollingOffset =
(mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.endAfterPadding)
((currentPosition + 1) until (currentPosition + preloadItemCount + 1)).forEach { ((currentPosition + 1) until (currentPosition + preloadItemCount + 1)).forEach {
if (it >= 0 && it < state.itemCount) { if (it >= 0 && it < state.itemCount) {
layoutPrefetchRegistry.addPosition(it, max(0, scrollingOffset)) layoutPrefetchRegistry.addPosition(it, max(0, scrollingOffset))

View file

@ -14,7 +14,11 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetCurrentReaderSettingsBinding? = null private var _binding: BottomSheetCurrentReaderSettingsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetCurrentReaderSettingsBinding.inflate(inflater, container, false) _binding = BottomSheetCurrentReaderSettingsBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -24,11 +28,14 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
val activity = requireActivity() as MangaReaderActivity val activity = requireActivity() as MangaReaderActivity
val settings = activity.settings.default val settings = activity.settings.default
binding.readerDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal] binding.readerDirectionText.text =
resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal]
binding.readerDirection.rotation = 90f * (settings.direction.ordinal) binding.readerDirection.rotation = 90f * (settings.direction.ordinal)
binding.readerDirection.setOnClickListener { binding.readerDirection.setOnClickListener {
settings.direction = Directions[settings.direction.ordinal + 1] ?: Directions.TOP_TO_BOTTOM settings.direction =
binding.readerDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal] Directions[settings.direction.ordinal + 1] ?: Directions.TOP_TO_BOTTOM
binding.readerDirectionText.text =
resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal]
binding.readerDirection.rotation = 90f * (settings.direction.ordinal) binding.readerDirection.rotation = 90f * (settings.direction.ordinal)
activity.applySettings() activity.applySettings()
} }
@ -39,36 +46,39 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
binding.readerContinuous binding.readerContinuous
) )
binding.readerPadding.isEnabled = settings.layout.ordinal!=0 binding.readerPadding.isEnabled = settings.layout.ordinal != 0
fun paddingAvailable(enable:Boolean){ fun paddingAvailable(enable: Boolean) {
binding.readerPadding.isEnabled = enable binding.readerPadding.isEnabled = enable
} }
binding.readerPadding.isChecked = settings.padding binding.readerPadding.isChecked = settings.padding
binding.readerPadding.setOnCheckedChangeListener { _,isChecked -> binding.readerPadding.setOnCheckedChangeListener { _, isChecked ->
settings.padding = isChecked settings.padding = isChecked
activity.applySettings() activity.applySettings()
} }
binding.readerCropBorders.isChecked = settings.cropBorders binding.readerCropBorders.isChecked = settings.cropBorders
binding.readerCropBorders.setOnCheckedChangeListener { _,isChecked -> binding.readerCropBorders.setOnCheckedChangeListener { _, isChecked ->
settings.cropBorders = isChecked settings.cropBorders = isChecked
activity.applySettings() activity.applySettings()
} }
binding.readerLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal] binding.readerLayoutText.text =
resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal]
var selected = list[settings.layout.ordinal] var selected = list[settings.layout.ordinal]
selected.alpha = 1f selected.alpha = 1f
list.forEachIndexed { index , imageButton -> list.forEachIndexed { index, imageButton ->
imageButton.setOnClickListener { imageButton.setOnClickListener {
selected.alpha = 0.33f selected.alpha = 0.33f
selected = imageButton selected = imageButton
selected.alpha = 1f selected.alpha = 1f
settings.layout = CurrentReaderSettings.Layouts[index]?:CurrentReaderSettings.Layouts.CONTINUOUS settings.layout =
binding.readerLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal] CurrentReaderSettings.Layouts[index] ?: CurrentReaderSettings.Layouts.CONTINUOUS
binding.readerLayoutText.text =
resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal]
activity.applySettings() activity.applySettings()
paddingAvailable(settings.layout.ordinal!=0) paddingAvailable(settings.layout.ordinal != 0)
} }
} }
@ -87,7 +97,8 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
selectedDual.alpha = 0.33f selectedDual.alpha = 0.33f
selectedDual = imageButton selectedDual = imageButton
selectedDual.alpha = 1f selectedDual.alpha = 1f
settings.dualPageMode = CurrentReaderSettings.DualPageModes[index] ?: CurrentReaderSettings.DualPageModes.Automatic settings.dualPageMode = CurrentReaderSettings.DualPageModes[index]
?: CurrentReaderSettings.DualPageModes.Automatic
binding.readerDualPageText.text = settings.dualPageMode.toString() binding.readerDualPageText.text = settings.dualPageMode.toString()
activity.applySettings() activity.applySettings()
} }
@ -111,37 +122,37 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
} }
binding.readerKeepScreenOn.isChecked = settings.keepScreenOn binding.readerKeepScreenOn.isChecked = settings.keepScreenOn
binding.readerKeepScreenOn.setOnCheckedChangeListener { _,isChecked -> binding.readerKeepScreenOn.setOnCheckedChangeListener { _, isChecked ->
settings.keepScreenOn = isChecked settings.keepScreenOn = isChecked
activity.applySettings() activity.applySettings()
} }
binding.readerHidePageNumbers.isChecked = settings.hidePageNumbers binding.readerHidePageNumbers.isChecked = settings.hidePageNumbers
binding.readerHidePageNumbers.setOnCheckedChangeListener { _,isChecked -> binding.readerHidePageNumbers.setOnCheckedChangeListener { _, isChecked ->
settings.hidePageNumbers = isChecked settings.hidePageNumbers = isChecked
activity.applySettings() activity.applySettings()
} }
binding.readerOverscroll.isChecked = settings.overScrollMode binding.readerOverscroll.isChecked = settings.overScrollMode
binding.readerOverscroll.setOnCheckedChangeListener { _,isChecked -> binding.readerOverscroll.setOnCheckedChangeListener { _, isChecked ->
settings.overScrollMode = isChecked settings.overScrollMode = isChecked
activity.applySettings() activity.applySettings()
} }
binding.readerVolumeButton.isChecked = settings.volumeButtons binding.readerVolumeButton.isChecked = settings.volumeButtons
binding.readerVolumeButton.setOnCheckedChangeListener { _,isChecked -> binding.readerVolumeButton.setOnCheckedChangeListener { _, isChecked ->
settings.volumeButtons = isChecked settings.volumeButtons = isChecked
activity.applySettings() activity.applySettings()
} }
binding.readerWrapImage.isChecked = settings.wrapImages binding.readerWrapImage.isChecked = settings.wrapImages
binding.readerWrapImage.setOnCheckedChangeListener { _,isChecked -> binding.readerWrapImage.setOnCheckedChangeListener { _, isChecked ->
settings.wrapImages = isChecked settings.wrapImages = isChecked
activity.applySettings() activity.applySettings()
} }
binding.readerLongClickImage.isChecked = settings.longClickImage binding.readerLongClickImage.isChecked = settings.longClickImage
binding.readerLongClickImage.setOnCheckedChangeListener { _,isChecked -> binding.readerLongClickImage.setOnCheckedChangeListener { _, isChecked ->
settings.longClickImage = isChecked settings.longClickImage = isChecked
activity.applySettings() activity.applySettings()
} }
@ -152,7 +163,7 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
super.onDestroy() super.onDestroy()
} }
companion object{ companion object {
fun newInstance() = ReaderSettingsDialogFragment() fun newInstance() = ReaderSettingsDialogFragment()
} }
} }

View file

@ -6,7 +6,8 @@ import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import java.security.MessageDigest import java.security.MessageDigest
class RemoveBordersTransformation(private val white:Boolean, private val threshHold:Int) : BitmapTransformation() { class RemoveBordersTransformation(private val white: Boolean, private val threshHold: Int) :
BitmapTransformation() {
override fun transform( override fun transform(
pool: BitmapPool, pool: BitmapPool,
@ -95,6 +96,6 @@ class RemoveBordersTransformation(private val white:Boolean, private val threshH
private fun isPixelNotWhite(pixel: Int): Boolean { private fun isPixelNotWhite(pixel: Int): Boolean {
val brightness = Color.red(pixel) + Color.green(pixel) + Color.blue(pixel) val brightness = Color.red(pixel) + Color.green(pixel) + Color.blue(pixel)
return if(white) brightness < (255-threshHold) else brightness > threshHold return if (white) brightness < (255 - threshHold) else brightness > threshHold
} }
} }

View file

@ -13,7 +13,7 @@ class Swipy @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) { ) : FrameLayout(context, attrs) {
var dragDivider : Int = 5 var dragDivider: Int = 5
var vertical = true var vertical = true
//public, in case a different sub child needs to be considered //public, in case a different sub child needs to be considered
@ -100,7 +100,7 @@ class Swipy @JvmOverloads constructor(
} }
when (action) { when (action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
activePointerId = ev.getPointerId(0) activePointerId = ev.getPointerId(0)
isBeingDragged = false isBeingDragged = false
pointerIndex = ev.findPointerIndex(activePointerId) pointerIndex = ev.findPointerIndex(activePointerId)
@ -109,7 +109,8 @@ class Swipy @JvmOverloads constructor(
} }
initialDown = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex) initialDown = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
} }
MotionEvent.ACTION_MOVE -> {
MotionEvent.ACTION_MOVE -> {
if (activePointerId == INVALID_POINTER) { if (activePointerId == INVALID_POINTER) {
//("Got ACTION_MOVE event but don't have an active pointer id.") //("Got ACTION_MOVE event but don't have an active pointer id.")
return false return false
@ -121,7 +122,8 @@ class Swipy @JvmOverloads constructor(
val pos = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex) val pos = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
startDragging(pos) startDragging(pos)
} }
MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isBeingDragged = false isBeingDragged = false
activePointerId = INVALID_POINTER activePointerId = INVALID_POINTER
@ -138,11 +140,12 @@ class Swipy @JvmOverloads constructor(
return false return false
} }
when (action) { when (action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
activePointerId = ev.getPointerId(0) activePointerId = ev.getPointerId(0)
isBeingDragged = false isBeingDragged = false
} }
MotionEvent.ACTION_MOVE -> {
MotionEvent.ACTION_MOVE -> {
pointerIndex = ev.findPointerIndex(activePointerId) pointerIndex = ev.findPointerIndex(activePointerId)
if (pointerIndex < 0) { if (pointerIndex < 0) {
//("Got ACTION_MOVE event but have an invalid active pointer id.") //("Got ACTION_MOVE event but have an invalid active pointer id.")
@ -160,16 +163,16 @@ class Swipy @JvmOverloads constructor(
if (overscroll > 0) { if (overscroll > 0) {
parent.requestDisallowInterceptTouchEvent(true) parent.requestDisallowInterceptTouchEvent(true)
if (vertical){ if (vertical) {
val totalDragDistance = Resources.getSystem().displayMetrics.heightPixels / dragDivider val totalDragDistance =
Resources.getSystem().displayMetrics.heightPixels / dragDivider
if (verticalPos == VerticalPosition.Top) if (verticalPos == VerticalPosition.Top)
topBeingSwiped.invoke(overscroll / totalDragDistance) topBeingSwiped.invoke(overscroll / totalDragDistance)
else else
bottomBeingSwiped.invoke(overscroll / totalDragDistance) bottomBeingSwiped.invoke(overscroll / totalDragDistance)
} } else {
val totalDragDistance =
else { Resources.getSystem().displayMetrics.widthPixels / dragDivider
val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider
if (horizontalPos == HorizontalPosition.Left) if (horizontalPos == HorizontalPosition.Left)
leftBeingSwiped.invoke(overscroll / totalDragDistance) leftBeingSwiped.invoke(overscroll / totalDragDistance)
else else
@ -180,6 +183,7 @@ class Swipy @JvmOverloads constructor(
} }
} }
} }
MotionEvent.ACTION_POINTER_DOWN -> { MotionEvent.ACTION_POINTER_DOWN -> {
pointerIndex = ev.actionIndex pointerIndex = ev.actionIndex
if (pointerIndex < 0) { if (pointerIndex < 0) {
@ -188,8 +192,9 @@ class Swipy @JvmOverloads constructor(
} }
activePointerId = ev.getPointerId(pointerIndex) activePointerId = ev.getPointerId(pointerIndex)
} }
MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
MotionEvent.ACTION_UP -> {
if (vertical) { if (vertical) {
topBeingSwiped.invoke(0f) topBeingSwiped.invoke(0f)
bottomBeingSwiped.invoke(0f) bottomBeingSwiped.invoke(0f)
@ -216,7 +221,8 @@ class Swipy @JvmOverloads constructor(
activePointerId = INVALID_POINTER activePointerId = INVALID_POINTER
return false return false
} }
MotionEvent.ACTION_CANCEL -> return false
MotionEvent.ACTION_CANCEL -> return false
} }
return true return true
} }
@ -235,21 +241,20 @@ class Swipy @JvmOverloads constructor(
private fun finishSpinner(overscrollDistance: Float) { private fun finishSpinner(overscrollDistance: Float) {
if (vertical) { if (vertical) {
val totalDragDistance = Resources.getSystem().displayMetrics.heightPixels / dragDivider val totalDragDistance = Resources.getSystem().displayMetrics.heightPixels / dragDivider
if (overscrollDistance > totalDragDistance) if (overscrollDistance > totalDragDistance)
if (verticalPos == VerticalPosition.Top) if (verticalPos == VerticalPosition.Top)
onTopSwiped.invoke() onTopSwiped.invoke()
else else
onBottomSwiped.invoke() onBottomSwiped.invoke()
} } else {
else { val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider
val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider if (overscrollDistance > totalDragDistance)
if (overscrollDistance > totalDragDistance)
if (horizontalPos == HorizontalPosition.Left) if (horizontalPos == HorizontalPosition.Left)
onLeftSwiped.invoke() onLeftSwiped.invoke()
else else
onRightSwiped.invoke() onRightSwiped.invoke()
} }
} }
} }

View file

@ -13,8 +13,6 @@ import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.others.LangSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -25,9 +23,18 @@ class BookDialog : BottomSheetDialogFragment() {
private val viewModel by activityViewModels<MediaDetailsViewModel>() private val viewModel by activityViewModels<MediaDetailsViewModel>()
private lateinit var novelName:String private lateinit var novelName: String
private lateinit var novel: ShowResponse private lateinit var novel: ShowResponse
private var source:Int = 0 private var source: Int = 0
interface Callback {
fun onDownloadTriggered(link: String)
}
private var callback: Callback? = null
fun setCallback(callback: Callback) {
this.callback = callback
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
arguments?.let { arguments?.let {
@ -38,7 +45,11 @@ class BookDialog : BottomSheetDialogFragment() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetBookBinding.inflate(inflater, container, false) _binding = BottomSheetBookBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@ -47,11 +58,11 @@ class BookDialog : BottomSheetDialogFragment() {
binding.bookRecyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.bookRecyclerView.layoutManager = LinearLayoutManager(requireContext())
viewModel.book.observe(viewLifecycleOwner) { viewModel.book.observe(viewLifecycleOwner) {
if(it!=null){ if (it != null) {
binding.itemBookTitle.text = it.name binding.itemBookTitle.text = it.name
binding.itemBookDesc.text = it.description binding.itemBookDesc.text = it.description
binding.itemBookImage.loadImage(it.img) binding.itemBookImage.loadImage(it.img)
binding.bookRecyclerView.adapter = UrlAdapter(it.links, it, novelName) binding.bookRecyclerView.adapter = UrlAdapter(it.links, it, novelName, callback)
} }
} }
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@ -65,7 +76,7 @@ class BookDialog : BottomSheetDialogFragment() {
} }
companion object { companion object {
fun newInstance(novelName:String, novel:ShowResponse, source: Int) : BookDialog{ fun newInstance(novelName: String, novel: ShowResponse, source: Int): BookDialog {
val bundle = Bundle().apply { val bundle = Bundle().apply {
putString("novelName", novelName) putString("novelName", novelName)
putInt("source", source) putInt("source", source)

View file

@ -22,12 +22,14 @@ class NovelReadAdapter(
var progress: View? = null var progress: View? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NovelReadAdapter.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NovelReadAdapter.ViewHolder {
val binding = ItemNovelHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemNovelHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
progress = binding.progress.root progress = binding.progress.root
return ViewHolder(binding) return ViewHolder(binding)
} }
private val imm = fragment.requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager private val imm = fragment.requireContext()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
@ -35,7 +37,8 @@ class NovelReadAdapter(
fun search(): Boolean { fun search(): Boolean {
val query = binding.searchBarText.text.toString() val query = binding.searchBarText.text.toString()
val source = media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it } val source =
media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it }
fragment.source = source fragment.source = source
binding.searchBarText.clearFocus() binding.searchBarText.clearFocus()
@ -44,11 +47,18 @@ class NovelReadAdapter(
return true return true
} }
val source = media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it } val source =
media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it }
if (novelReadSources.names.isNotEmpty() && source in 0 until novelReadSources.names.size) { if (novelReadSources.names.isNotEmpty() && source in 0 until novelReadSources.names.size) {
binding.animeSource.setText(novelReadSources.names[source], false) binding.animeSource.setText(novelReadSources.names[source], false)
} }
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, novelReadSources.names)) binding.animeSource.setAdapter(
ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
novelReadSources.names
)
)
binding.animeSource.setOnItemClickListener { _, _, i, _ -> binding.animeSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i) fragment.onSourceChange(i)
search() search()
@ -58,13 +68,14 @@ class NovelReadAdapter(
binding.searchBarText.setOnEditorActionListener { _, actionId, _ -> binding.searchBarText.setOnEditorActionListener { _, actionId, _ ->
return@setOnEditorActionListener when (actionId) { return@setOnEditorActionListener when (actionId) {
IME_ACTION_SEARCH -> search() IME_ACTION_SEARCH -> search()
else -> false else -> false
} }
} }
binding.searchBar.setEndIconOnClickListener { search() } binding.searchBar.setEndIconOnClickListener { search() }
} }
override fun getItemCount(): Int = 0 override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemNovelHeaderBinding) : RecyclerView.ViewHolder(binding.root) inner class ViewHolder(val binding: ItemNovelHeaderBinding) :
RecyclerView.ViewHolder(binding.root)
} }

View file

@ -1,12 +1,20 @@
package ani.dantotsu.media.novel package ani.dantotsu.media.novel
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Parcelable import android.os.Parcelable
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
@ -14,16 +22,29 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.Download
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.novel.NovelDownloaderService
import ani.dantotsu.download.novel.NovelServiceDataSingleton
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.novel.novelreader.NovelReaderActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.saveData import ani.dantotsu.saveData
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class NovelReadFragment : Fragment() { class NovelReadFragment : Fragment(),
DownloadTriggerCallback,
DownloadedCheckCallback {
private var _binding: FragmentAnimeWatchBinding? = null private var _binding: FragmentAnimeWatchBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -40,11 +61,141 @@ class NovelReadFragment : Fragment() {
private var continueEp: Boolean = false private var continueEp: Boolean = false
var loaded = false var loaded = false
val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } val uiSettings = loadData("ui_settings", toast = false)
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) {
Log.e("downloadTrigger", novelDownloadPackage.link)
val downloadTask = NovelDownloaderService.DownloadTask(
title = media.nameMAL ?: media.nameRomaji,
chapter = novelDownloadPackage.novelName,
downloadLink = novelDownloadPackage.link,
originalLink = novelDownloadPackage.originalLink,
sourceMedia = media,
coverUrl = novelDownloadPackage.coverUrl,
retries = 2,
)
NovelServiceDataSingleton.downloadQueue.offer(downloadTask)
CoroutineScope(Dispatchers.IO).launch {
if (!NovelServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, NovelDownloaderService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent)
}
NovelServiceDataSingleton.isServiceRunning = true
}
}
}
override fun downloadedCheckWithStart(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>()
if (downloadsManager.queryDownload(
Download(
media.nameMAL ?: media.nameRomaji,
novel.name,
Download.Type.NOVEL
)
)
) {
val file = File(
context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.novelLocation}/${media.nameMAL ?: media.nameRomaji}/${novel.name}/0.epub"
)
if (!file.exists()) return false
val fileUri = FileProvider.getUriForFile(
requireContext(),
"${requireContext().packageName}.provider",
file
)
val intent = Intent(context, NovelReaderActivity::class.java).apply {
action = Intent.ACTION_VIEW
setDataAndType(fileUri, "application/epub+zip")
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
startActivity(intent)
return true
} else {
return false
}
}
override fun downloadedCheck(novel: ShowResponse): Boolean {
val downloadsManager = Injekt.get<DownloadsManager>()
return downloadsManager.queryDownload(
Download(
media.nameMAL ?: media.nameRomaji,
novel.name,
Download.Type.NOVEL
)
)
}
override fun deleteDownload(novel: ShowResponse) {
val downloadsManager = Injekt.get<DownloadsManager>()
downloadsManager.removeDownload(
Download(
media.nameMAL ?: media.nameRomaji,
novel.name,
Download.Type.NOVEL
)
)
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!this@NovelReadFragment::novelResponseAdapter.isInitialized) return
when (intent.action) {
ACTION_DOWNLOAD_STARTED -> {
val link = intent.getStringExtra(EXTRA_NOVEL_LINK)
link?.let {
novelResponseAdapter.startDownload(it)
}
}
ACTION_DOWNLOAD_FINISHED -> {
val link = intent.getStringExtra(EXTRA_NOVEL_LINK)
link?.let {
novelResponseAdapter.stopDownload(it)
}
}
ACTION_DOWNLOAD_FAILED -> {
val link = intent.getStringExtra(EXTRA_NOVEL_LINK)
link?.let {
novelResponseAdapter.purgeDownload(it)
}
}
ACTION_DOWNLOAD_PROGRESS -> {
val link = intent.getStringExtra(EXTRA_NOVEL_LINK)
val progress = intent.getIntExtra("progress", 0)
link?.let {
novelResponseAdapter.updateDownloadProgress(it, progress)
}
}
}
}
}
var response: List<ShowResponse>? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val intentFilter = IntentFilter().apply {
addAction(ACTION_DOWNLOAD_STARTED)
addAction(ACTION_DOWNLOAD_FINISHED)
addAction(ACTION_DOWNLOAD_FAILED)
addAction(ACTION_DOWNLOAD_PROGRESS)
}
ContextCompat.registerReceiver(
requireContext(),
downloadStatusReceiver,
intentFilter,
ContextCompat.RECEIVER_EXPORTED
)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
binding.animeSourceRecycler.layoutManager = LinearLayoutManager(requireContext()) binding.animeSourceRecycler.layoutManager = LinearLayoutManager(requireContext())
@ -63,8 +214,13 @@ class NovelReadFragment : Fragment() {
val sel = media.selected val sel = media.selected
searchQuery = sel?.server ?: media.name ?: media.nameRomaji searchQuery = sel?.server ?: media.name ?: media.nameRomaji
headerAdapter = NovelReadAdapter(media, this, model.novelSources) headerAdapter = NovelReadAdapter(media, this, model.novelSources)
novelResponseAdapter = NovelResponseAdapter(this) novelResponseAdapter = NovelResponseAdapter(
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, novelResponseAdapter) this,
this,
this
) // probably a better way to do this but it works
binding.animeSourceRecycler.adapter =
ConcatAdapter(headerAdapter, novelResponseAdapter)
loaded = true loaded = true
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
search(searchQuery, sel?.sourceIndex ?: 0, auto = sel?.server == null) search(searchQuery, sel?.sourceIndex ?: 0, auto = sel?.server == null)
@ -74,6 +230,7 @@ class NovelReadFragment : Fragment() {
} }
model.novelResponses.observe(viewLifecycleOwner) { model.novelResponses.observe(viewLifecycleOwner) {
if (it != null) { if (it != null) {
response = it
searching = false searching = false
novelResponseAdapter.submitList(it) novelResponseAdapter.submitList(it)
headerAdapter.progress?.visibility = View.GONE headerAdapter.progress?.visibility = View.GONE
@ -89,8 +246,9 @@ class NovelReadFragment : Fragment() {
searchQuery = query searchQuery = query
headerAdapter.progress?.visibility = View.VISIBLE headerAdapter.progress?.visibility = View.VISIBLE
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
if (auto || query=="") model.autoSearchNovels(media) if (auto || query == "") model.autoSearchNovels(media)
else model.searchNovels(query, source) //else model.searchNovels(query, source)
else model.autoSearchNovels(media) //testing
} }
searching = true searching = true
if (save) { if (save) {
@ -121,6 +279,7 @@ class NovelReadFragment : Fragment() {
override fun onDestroy() { override fun onDestroy() {
model.mangaReadSources?.flushText() model.mangaReadSources?.flushText()
requireContext().unregisterReceiver(downloadStatusReceiver)
super.onDestroy() super.onDestroy()
} }
@ -135,4 +294,22 @@ class NovelReadFragment : Fragment() {
super.onPause() super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
} }
companion object {
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
const val EXTRA_NOVEL_LINK = "extra_novel_link"
}
}
interface DownloadTriggerCallback {
fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage)
}
interface DownloadedCheckCallback {
fun downloadedCheck(novel: ShowResponse): Boolean
fun downloadedCheckWithStart(novel: ShowResponse): Boolean
fun deleteDownload(novel: ShowResponse)
} }

View file

@ -1,22 +1,32 @@
package ani.dantotsu.media.novel package ani.dantotsu.media.novel
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemNovelResponseBinding import ani.dantotsu.databinding.ItemNovelResponseBinding
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.snackString
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
class NovelResponseAdapter(val fragment: NovelReadFragment) : RecyclerView.Adapter<NovelResponseAdapter.ViewHolder>() { class NovelResponseAdapter(
val fragment: NovelReadFragment,
val downloadTriggerCallback: DownloadTriggerCallback,
val downloadedCheckCallback: DownloadedCheckCallback
) : RecyclerView.Adapter<NovelResponseAdapter.ViewHolder>() {
val list: MutableList<ShowResponse> = mutableListOf() val list: MutableList<ShowResponse> = mutableListOf()
inner class ViewHolder(val binding: ItemNovelResponseBinding) : RecyclerView.ViewHolder(binding.root) inner class ViewHolder(val binding: ItemNovelResponseBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemNovelResponseBinding.inflate(LayoutInflater.from(parent.context), parent, false) val bind =
ItemNovelResponseBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(bind) return ViewHolder(bind)
} }
@ -27,19 +37,142 @@ class NovelResponseAdapter(val fragment: NovelReadFragment) : RecyclerView.Adapt
val novel = list[position] val novel = list[position]
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val cover = GlideUrl(novel.coverUrl.url){ novel.coverUrl.headers } val cover = GlideUrl(novel.coverUrl.url) { novel.coverUrl.headers }
Glide.with(binding.itemEpisodeImage).load(cover).override(400,0).into(binding.itemEpisodeImage) Glide.with(binding.itemEpisodeImage).load(cover).override(400, 0)
.into(binding.itemEpisodeImage)
val typedValue = TypedValue()
fragment.requireContext().theme?.resolveAttribute(com.google.android.material.R.attr.colorOnBackground, typedValue, true)
val color = typedValue.data
binding.itemEpisodeTitle.text = novel.name binding.itemEpisodeTitle.text = novel.name
binding.itemEpisodeFiller.text = novel.extra?.get("0") ?: "" binding.itemEpisodeFiller.text =
if (downloadedCheckCallback.downloadedCheck(novel)) {
"Downloaded"
} else {
novel.extra?.get("0") ?: ""
}
if (binding.itemEpisodeFiller.text.contains("Downloading")) {
binding.itemEpisodeFiller.setTextColor(
fragment.requireContext().getColor(android.R.color.holo_blue_light)
)
} else if (binding.itemEpisodeFiller.text.contains("Downloaded")) {
binding.itemEpisodeFiller.setTextColor(
fragment.requireContext().getColor(android.R.color.holo_green_light)
)
} else {
binding.itemEpisodeFiller.setTextColor(color)
}
binding.itemEpisodeDesc2.text = novel.extra?.get("1") ?: "" binding.itemEpisodeDesc2.text = novel.extra?.get("1") ?: ""
val desc = novel.extra?.get("2") val desc = novel.extra?.get("2")
binding.itemEpisodeDesc.visibility = if (desc != null && desc.trim(' ') != "") View.VISIBLE else View.GONE binding.itemEpisodeDesc.visibility =
if (desc != null && desc.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = desc ?: "" binding.itemEpisodeDesc.text = desc ?: ""
binding.root.setOnClickListener { binding.root.setOnClickListener {
BookDialog.newInstance(fragment.novelName, novel, fragment.source) //make sure the file is not downloading
.show(fragment.parentFragmentManager, "dialog") if (activeDownloads.contains(novel.link)) {
return@setOnClickListener
}
if (downloadedCheckCallback.downloadedCheckWithStart(novel)) {
return@setOnClickListener
}
val bookDialog = BookDialog.newInstance(fragment.novelName, novel, fragment.source)
bookDialog.setCallback(object : BookDialog.Callback {
override fun onDownloadTriggered(link: String) {
downloadTriggerCallback.downloadTrigger(
NovelDownloadPackage(
link,
novel.coverUrl.url,
novel.name,
novel.link
)
)
bookDialog.dismiss()
}
})
bookDialog.show(fragment.parentFragmentManager, "dialog")
}
binding.root.setOnLongClickListener {
val builder = androidx.appcompat.app.AlertDialog.Builder(fragment.requireContext(), R.style.DialogTheme)
builder.setTitle("Delete ${novel.name}?")
builder.setMessage("Are you sure you want to delete ${novel.name}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadedCheckCallback.deleteDownload(novel)
deleteDownload(novel.link)
snackString("Deleted ${novel.name}")
if (binding.itemEpisodeFiller.text.toString().contains("Download", ignoreCase = true)) {
binding.itemEpisodeFiller.text = ""
}
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
builder.show()
true
}
}
private val activeDownloads = mutableSetOf<String>()
private val downloadedChapters = mutableSetOf<String>()
fun startDownload(link: String) {
activeDownloads.add(link)
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
list[position].extra?.remove("0")
list[position].extra?.set("0", "Downloading: 0%")
notifyItemChanged(position)
}
}
fun stopDownload(link: String) {
activeDownloads.remove(link)
downloadedChapters.add(link)
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
list[position].extra?.remove("0")
list[position].extra?.set("0", "Downloaded")
notifyItemChanged(position)
}
}
fun deleteDownload(link: String) { //TODO:
downloadedChapters.remove(link)
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
list[position].extra?.remove("0")
list[position].extra?.set("0", "")
notifyItemChanged(position)
}
}
fun purgeDownload(link: String) {
activeDownloads.remove(link)
downloadedChapters.remove(link)
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
list[position].extra?.remove("0")
list[position].extra?.set("0", "Failed")
notifyItemChanged(position)
}
}
fun updateDownloadProgress(link: String, progress: Int) {
if (!activeDownloads.contains(link)) {
activeDownloads.add(link)
}
val position = list.indexOfFirst { it.link == link }
if (position != -1) {
list[position].extra?.remove("0")
list[position].extra?.set("0", "Downloading: $progress%")
Log.d("NovelResponseAdapter", "updateDownloadProgress: $progress, position: $position")
notifyItemChanged(position)
} }
} }
@ -54,4 +187,11 @@ class NovelResponseAdapter(val fragment: NovelReadFragment) : RecyclerView.Adapt
list.clear() list.clear()
notifyItemRangeRemoved(0, size) notifyItemRangeRemoved(0, size)
} }
} }
data class NovelDownloadPackage(
val link: String,
val coverUrl: String,
val novelName: String,
val originalLink: String
)

Some files were not shown because too many files have changed in this diff Show more