Compare commits

..

5 Commits

Author SHA1 Message Date
rebel onion
59784de727 Merge pull request #269 from rebelonion/dev
Dev
2024-03-20 14:54:01 -05:00
rebel onion
0f9bf3c5b1 chore: Update stable.md 2024-03-20 01:33:54 -05:00
rebel onion
4035aee1f9 Merge pull request #264 from rebelonion/dev
Dev
2024-03-20 01:32:15 -05:00
rebel onion
d778cd4350 Merge pull request #187 from rebelonion/dev
Dev
2024-02-08 09:56:12 -06:00
rebel onion
ab199a3502 Merge pull request #186 from rebelonion/dev
Dev
2024-02-08 07:35:44 -06:00
292 changed files with 9112 additions and 11861 deletions

View File

@@ -127,4 +127,4 @@ jobs:
pre-release-keep-count: 3
pre-release-drop-tag: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,10 +6,6 @@ plugins {
id 'com.google.devtools.ksp'
}
def gitCommitHash = providers.exec {
commandLine("git", "rev-parse", "--verify", "--short", "HEAD")
}.standardOutput.asText.get().trim()
android {
compileSdk 34
@@ -21,14 +17,6 @@ android {
versionName "3.0.0"
versionCode 300000000
signingConfig signingConfigs.debug
splits {
abi {
enable true
reset()
include 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
}
}
}
flavorDimensions += "store"
@@ -50,7 +38,7 @@ android {
buildTypes {
alpha {
applicationIdSuffix ".beta" // keep as beta by popular request
versionNameSuffix "-alpha01-" + gitCommitHash
versionNameSuffix "-alpha01"
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
debuggable System.getenv("CI") == null
@@ -58,7 +46,7 @@ android {
}
debug {
applicationIdSuffix ".beta"
versionNameSuffix "-beta02"
versionNameSuffix "-beta01"
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
debuggable false
@@ -87,11 +75,11 @@ android {
dependencies {
// FireBase
// FireBase
googleImplementation platform('com.google.firebase:firebase-bom:32.7.4')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.1'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.2'
// Core
// Core
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.core:core-ktx:1.12.0'
@@ -107,9 +95,8 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.10.0'
implementation "com.anggrayudi:storage:1.5.5"
// Glide
// Glide
ext.glide_version = '4.16.0'
api "com.github.bumptech.glide:glide:$glide_version"
implementation "com.github.bumptech.glide:glide:$glide_version"
@@ -117,7 +104,7 @@ dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer
// Exoplayer
ext.exo_version = '1.3.0'
implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version"
@@ -125,13 +112,14 @@ dependencies {
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
implementation "androidx.media3:media3-session:$exo_version"
// Media3 Casting
//media3 casting
implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.7.0"
implementation "androidx.mediarouter:mediarouter:1.6.0"
// UI
// UI
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.github.RepoDevil:AnimatedBottomBar:7fcb9af'
//implementation 'nl.joery.animatedbottombar:library:1.1.0'
implementation 'com.github.rebelonion:AnimatedBottomBar:v1.1.0'
implementation 'com.flaviofaria:kenburnsview:1.0.7'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.alexvasilkov:gesture-views:2.8.3'
@@ -140,7 +128,7 @@ dependencies {
implementation 'com.github.eltos:simpledialogfragments:v3.7'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:93972bc'
// Markwon
// Markwon
ext.markwon_version = '4.6.2'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:editor:$markwon_version"
@@ -150,18 +138,15 @@ dependencies {
implementation "io.noties.markwon:html:$markwon_version"
implementation "io.noties.markwon:image-glide:$markwon_version"
// Groupie
// Groupie
ext.groupie_version = '2.10.1'
implementation "com.github.lisawray.groupie:groupie:$groupie_version"
implementation "com.github.lisawray.groupie:groupie-viewbinding:$groupie_version"
// String Matching
// string matching
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation group: 'com.arthenica', name: 'ffmpeg-kit-full-gpl', version: '6.0-2.LTS'
//implementation 'com.github.yausername.youtubedl-android:library:0.15.0'
// Aniyomi
// Aniyomi
implementation 'io.reactivex:rxjava:1.3.8'
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'

View File

@@ -19,7 +19,7 @@
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> <!-- For background jobs -->
@@ -49,7 +49,6 @@
android:name=".App"
android:allowBackup="true"
android:banner="@mipmap/ic_banner_foreground"
android:enableOnBackInvokedCallback="true"
android:icon="${icon_placeholder}"
android:label="@string/app_name"
android:largeHeap="true"
@@ -58,30 +57,9 @@
android:supportsRtl="true"
android:theme="@style/Theme.Dantotsu"
android:usesCleartextTraffic="true"
tools:ignore="AllowBackup"
tools:targetApi="tiramisu">
tools:ignore="AllowBackup">
<receiver
android:name=".widgets.upcoming.UpcomingWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/upcoming_widget_info" />
</receiver>
<activity
android:name=".widgets.upcoming.UpcomingWidgetConfigure"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver
android:name=".widgets.statistics.ProfileStatsWidget"
android:name=".widgets.CurrentlyAiringWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -89,10 +67,11 @@
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/statistics_widget_info" />
android:resource="@xml/currently_airing_widget_info" />
</receiver>
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
<activity
android:name=".media.novel.novelreader.NovelReaderActivity"
android:configChanges="orientation|screenSize"
@@ -125,33 +104,26 @@
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.ExtensionsActivity"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name=".widgets.statistics.ProfileStatsConfigure"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
android:windowSoftInputMode="adjustResize|stateHidden"
android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.ProfileActivity"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
android:windowSoftInputMode="adjustResize|stateHidden"
android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.FollowActivity"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
android:windowSoftInputMode="adjustResize|stateHidden"
android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.activity.FeedActivity"
android:configChanges="orientation|screenSize|screenLayout"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
android:parentActivityName=".MainActivity" >
</activity>
<activity
android:name=".profile.activity.NotificationActivity"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
android:parentActivityName=".MainActivity" >
</activity>
<activity
android:name=".others.imagesearch.ImageSearchActivity"
android:parentActivityName=".MainActivity" />
@@ -164,9 +136,8 @@
android:name=".media.CalendarActivity"
android:parentActivityName=".MainActivity" />
<activity android:name=".media.user.ListActivity" />
<activity
android:name=".profile.SingleStatActivity"
android:parentActivityName=".profile.ProfileActivity" />
<activity android:name=".profile.SingleStatActivity"
android:parentActivityName=".profile.ProfileActivity"/>
<activity
android:name=".media.manga.mangareader.MangaReaderActivity"
android:excludeFromRecents="true"
@@ -178,7 +149,7 @@
android:name=".media.MediaDetailsActivity"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.Dantotsu.NeverCutout"
android:windowSoftInputMode="adjustResize|stateHidden" />
android:windowSoftInputMode="adjustResize|stateHidden"/>
<activity android:name=".media.CharacterDetailsActivity" />
<activity android:name=".home.NoInternet" />
<activity
@@ -260,6 +231,7 @@
<data android:host="discord.dantotsu.com" />
</intent-filter>
</activity>
<activity
android:name=".connections.anilist.UrlMedia"
android:configChanges="orientation|screenSize|layoutDirection"
@@ -318,9 +290,7 @@
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" />
@@ -329,27 +299,30 @@
</intent-filter>
</activity>
<activity
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<receiver
android:name=".notifications.AlarmPermissionStateReceiver"
<receiver android:name=".notifications.AlarmPermissionStateReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".notifications.BootCompletedReceiver"
<receiver android:name=".notifications.BootCompletedReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver" />
<receiver android:name=".notifications.comment.CommentNotificationReceiver" />
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver" />
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver"/>
<receiver android:name=".notifications.comment.CommentNotificationReceiver"/>
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver"/>
<meta-data
android:name="preloaded_fonts"
@@ -367,11 +340,25 @@
</provider>
<service
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
android:name=".widgets.CurrentlyAiringRemoteViewsService"
android:exported="true"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
android:name=".download.video.ExoplayerDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync">
<intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<service
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service

View File

@@ -86,7 +86,7 @@ class App : MultiDexApplication() {
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
Logger.log("App: Logging started")
initializeNetwork()
initializeNetwork(baseContext)
setupNotificationChannels()
if (!LogcatLogger.isInstalled) {
@@ -121,9 +121,7 @@ class App : MultiDexApplication() {
}
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
val scheduler = TaskScheduler.create(this, useAlarmManager)
scheduler.scheduleAllTasks(this)
scheduler.scheduleSingleWork(this)
TaskScheduler.create(this, useAlarmManager).scheduleAllTasks(this)
}
private fun setupNotificationChannels() {
@@ -154,10 +152,6 @@ class App : MultiDexApplication() {
companion object {
private var instance: App? = null
/** Reference to the application context.
*
* USE WITH EXTREME CAUTION!**/
var context: Context? = null
fun currentContext(): Context? {
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context

View File

@@ -10,13 +10,11 @@ import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
import android.content.res.Resources.getSystem
import android.graphics.Bitmap
import android.graphics.Color
@@ -184,10 +182,6 @@ fun currActivity(): Activity? {
var loadMedia: Int? = null
var loadIsMAL = false
val Int.toPx get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics
).toInt()
fun initActivity(a: Activity) {
val window = a.window
WindowCompat.setDecorFitsSystemWindows(window, false)
@@ -207,15 +201,13 @@ fun initActivity(a: Activity) {
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
?.apply {
navBarHeight = this.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
}
}
WindowInsetsControllerCompat(
window,
window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
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) {
window.decorView.rootWindowInsets?.displayCutout?.apply {
if (boundingRects.size > 0) {
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
@@ -230,7 +222,6 @@ fun initActivity(a: Activity) {
statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
navBarHeight =
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
}
}
if (a !is MainActivity) a.setNavigationTheme()
@@ -271,55 +262,6 @@ fun Activity.setNavigationTheme() {
}
}
/**
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
*
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
*/
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar) {
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
clipToPadding = false
setPadding(paddingLeft, paddingTop, paddingRight, navBarHeight + navBar.measuredHeight)
}
/**
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
*
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
*/
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar, overlayView: View) {
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
overlayView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
clipToPadding = false
setPadding(paddingLeft, paddingTop, paddingRight, navBarHeight + navBar.measuredHeight + overlayView.measuredHeight)
}
fun Activity.reloadActivity() {
Refresh.all()
finish()
startActivity(Intent(this, this::class.java))
initActivity(this)
}
fun Context.restartApp(view: View) {
val mainIntent = Intent.makeRestartActivityTask(
packageManager.getLaunchIntentForPackage(this.packageName)!!.component
)
val component = ComponentName(this@restartApp.packageName, this@restartApp::class.qualifiedName!!)
Snackbar.make(view, R.string.restart_app, Snackbar.LENGTH_INDEFINITE).apply {
setAction(R.string.do_it) {
this.dismiss()
try {
startActivity(Intent().setComponent(component))
} catch (anything: Exception) {
startActivity(mainIntent)
}
Runtime.getRuntime().exit(0)
}
show()
}
}
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onStart() {
super.onStart()
@@ -417,7 +359,7 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g
dialog.setButton(
DialogInterface.BUTTON_NEUTRAL,
activity.getString(R.string.remove)
) { _, which ->
) { dialog, which ->
if (which == DialogInterface.BUTTON_NEUTRAL) {
date = FuzzyDate()
}
@@ -452,6 +394,7 @@ class InputFilterMinMax(
return ""
}
@SuppressLint("SetTextI18n")
private fun isInRange(a: Double, b: Double, c: Double): Boolean {
val statusStrings = currContext()!!.resources.getStringArray(R.array.status_manga)[2]
@@ -620,14 +563,9 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
if (file?.url?.isNotEmpty() == true) {
tryWith {
if (file.url.startsWith("content://")) {
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
.override(size).into(this)
} else {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
}
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
}
}
}
@@ -774,23 +712,6 @@ fun openLinkInBrowser(link: String?) {
}
}
fun openLinkInYouTube(link: String?) {
link?.let {
try {
val videoIntent = Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(link)
setPackage("com.google.android.youtube")
}
currContext()!!.startActivity(videoIntent)
} catch (e: ActivityNotFoundException) {
openLinkInBrowser(link)
} catch (e: Exception) {
Logger.log(e)
}
}
}
fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Activity) {
FileProvider.getUriForFile(
context,
@@ -882,6 +803,31 @@ fun savePrefs(
}
}
fun downloadsPermission(activity: AppCompatActivity): Boolean {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
val requiredPermissions = permissions.filter {
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
}.toTypedArray()
return if (requiredPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(
activity,
requiredPermissions,
DOWNLOADS_PERMISSION_REQUEST_CODE
)
false
} else {
true
}
}
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
val contentUri = FileProvider.getUriForFile(
@@ -951,9 +897,9 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
}
}
@SuppressLint("SetTextI18n")
fun countDown(media: Media, view: ViewGroup) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null
&& (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
@@ -1054,10 +1000,6 @@ class EmptyAdapter(private val count: Int) : RecyclerView.Adapter<RecyclerView.V
inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
}
fun getAppString(res: Int): String {
return currContext()?.getString(res) ?: ""
}
fun toast(string: String?) {
if (string != null) {
Logger.log(string)
@@ -1068,10 +1010,6 @@ fun toast(string: String?) {
}
}
fun toast(res: Int) {
toast(getAppString(res))
}
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null): Snackbar? {
try { //I have no idea why this sometimes crashes for some people...
if (s != null) {
@@ -1112,10 +1050,6 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
return null
}
fun snackString(r: Int, activity: Activity? = null, clipboard: String? = null): Snackbar? {
return snackString(getAppString(r), activity, clipboard)
}
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 {
@@ -1302,7 +1236,7 @@ fun blurImage(imageView: ImageView, banner: String?) {
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
Glide.with(context as Context)
.load(GlideUrl(url))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
.into(imageView)
}

View File

@@ -3,9 +3,9 @@ package ani.dantotsu
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.drawable.Animatable
import android.graphics.drawable.GradientDrawable
import android.net.Uri
@@ -14,13 +14,14 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator
import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
@@ -55,7 +56,6 @@ import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity
import ani.dantotsu.profile.activity.NotificationActivity
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
import ani.dantotsu.settings.saving.PrefName
@@ -161,16 +161,16 @@ class MainActivity : AppCompatActivity() {
}
}
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val backgroundDrawable = bottomNavBar.background as GradientDrawable
val backgroundDrawable = _bottomBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
backgroundDrawable.setColor(semiTransparentColor)
bottomNavBar.background = backgroundDrawable
_bottomBar.background = backgroundDrawable
}
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
val offset = try {
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
@@ -230,6 +230,17 @@ class MainActivity : AppCompatActivity() {
}
}
val preferences: SourcePreferences = Injekt.get()
if (preferences.animeExtensionUpdatesCount()
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
Toast.makeText(
this,
"You have extension updates available!",
Toast.LENGTH_LONG
).show()
}
binding.root.isMotionEventSplittingEnabled = false
lifecycleScope.launch {
@@ -273,16 +284,6 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach {
initActivity(this)
val preferences: SourcePreferences = Injekt.get()
if (preferences.animeExtensionUpdatesCount()
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
snackString(R.string.extension_updates_available)
?.setDuration(Snackbar.LENGTH_LONG)
?.setAction(R.string.review) {
startActivity(Intent(this, ExtensionsActivity::class.java))
}
}
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
selectedOption = if (fragment != null) {
when (fragment) {
@@ -338,7 +339,7 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(this, NoInternet::class.java))
} else {
val model: AnilistHomeViewModel by viewModels()
model.genres.observe(this) {
model.genres.observe(this) { it ->
if (it != null) {
if (it) {
val navbar = binding.includedNavbar.navbar
@@ -363,7 +364,7 @@ class MainActivity : AppCompatActivity() {
mainViewPager.setCurrentItem(newIndex, false)
}
})
if (mainViewPager.currentItem != selectedOption) {
if (mainViewPager.getCurrentItem() != selectedOption) {
navbar.selectTabAt(selectedOption)
mainViewPager.post {
mainViewPager.setCurrentItem(
@@ -449,7 +450,7 @@ class MainActivity : AppCompatActivity() {
}
}
}
/*lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
val index = Helper.downloadManager(this@MainActivity).downloadIndex
val downloadCursor = index.getDownloads()
while (downloadCursor.moveToNext()) {
@@ -458,7 +459,7 @@ class MainActivity : AppCompatActivity() {
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
}
}
}*/ //TODO: remove this
}
}
override fun onRestart() {
@@ -466,12 +467,18 @@ class MainActivity : AppCompatActivity() {
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
}
private val Int.toPx get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics
).toInt()
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
val params : ViewGroup.MarginLayoutParams =
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
params.updateMargins(bottom = margin.toPx)
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
params.updateMargins(bottom = 8.toPx)
else
params.updateMargins(bottom = 32.toPx)
}
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
@@ -483,7 +490,7 @@ class MainActivity : AppCompatActivity() {
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
subtitleTextView?.visibility = View.VISIBLE
subtitleTextView?.text = getString(R.string.enter_password_to_decrypt_file)
subtitleTextView?.text = "Enter your password to decrypt the file"
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Enter Password")

View File

@@ -1,5 +1,6 @@
package ani.dantotsu
import android.content.Context
import android.os.Build
import androidx.fragment.app.FragmentActivity
import ani.dantotsu.others.webview.CloudFlare
@@ -9,7 +10,6 @@ import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import com.lagradost.nicehttp.addGenericDns
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
@@ -35,13 +35,13 @@ lateinit var defaultHeaders: Map<String, String>
lateinit var okHttpClient: OkHttpClient
lateinit var client: Requests
fun initializeNetwork() {
fun initializeNetwork(context: Context) {
val networkHelper = Injekt.get<NetworkHelper>()
defaultHeaders = mapOf(
"User-Agent" to
defaultUserAgentProvider()
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
.format(Build.VERSION.RELEASE, Build.MODEL)
)

View File

@@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.source.anime.service.AnimeSourceManager
@@ -30,7 +29,6 @@ import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule {
@kotlin.OptIn(ExperimentalSerializationApi::class)
@OptIn(UnstableApi::class)
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)

View File

@@ -7,7 +7,6 @@ import android.util.Log
import androidx.browser.customtabs.CustomTabsIntent
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.currContext
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager
@@ -41,54 +40,20 @@ object Anilist {
"SCORE_DESC",
"POPULARITY_DESC",
"TRENDING_DESC",
"START_DATE_DESC",
"TITLE_ENGLISH",
"TITLE_ENGLISH_DESC",
"SCORE"
)
val source = listOf(
"ORIGINAL",
"MANGA",
"LIGHT NOVEL",
"VISUAL NOVEL",
"VIDEO GAME",
"OTHER",
"NOVEL",
"DOUJINSHI",
"ANIME",
"WEB NOVEL",
"LIVE ACTION",
"GAME",
"COMIC",
"MULTIMEDIA PROJECT",
"PICTURE BOOK"
)
val animeStatus = listOf(
"FINISHED",
"RELEASING",
"NOT YET RELEASED",
"CANCELLED"
)
val mangaStatus = listOf(
"FINISHED",
"RELEASING",
"NOT YET RELEASED",
"HIATUS",
"CANCELLED"
)
val seasons = listOf(
"WINTER", "SPRING", "SUMMER", "FALL"
)
val animeFormats = listOf(
val anime_formats = listOf(
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
)
val mangaFormats = listOf(
val manga_formats = listOf(
"MANGA", "NOVEL", "ONE SHOT"
)
@@ -152,9 +117,6 @@ object Anilist {
episodesWatched = null
chapterRead = null
PrefManager.removeVal(PrefName.AnilistToken)
//logout from comments api
CommentsAPI.logout()
}
suspend inline fun <reified T : Any> executeQuery(

View File

@@ -72,7 +72,7 @@ class AnilistQueries {
media.cameFromContinue = false
val query =
"""{Media(id:${media.id}){id favourites popularity mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
"""{Media(id:${media.id}){id mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
runBlocking {
val anilist = async {
var response = executeQuery<Query.Media>(query, force = true, show = true)
@@ -83,8 +83,7 @@ class AnilistQueries {
media.source = fetchedMedia.source?.toString()
media.countryOfOrigin = fetchedMedia.countryOfOrigin
media.format = fetchedMedia.format?.toString()
media.favourites = fetchedMedia.favourites
media.popularity = fetchedMedia.popularity
media.startDate = fetchedMedia.startDate
media.endDate = fetchedMedia.endDate
@@ -387,7 +386,6 @@ class AnilistQueries {
returnArray.addAll(map.values)
return returnArray
}
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
@@ -545,7 +543,6 @@ class AnilistQueries {
returnMap["current$type"] = returnArray
return
}
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
@@ -575,7 +572,6 @@ class AnilistQueries {
subMap[m.id] = m
}
}
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
@@ -737,7 +733,7 @@ class AnilistQueries {
}
sorted["All"] = all
val listSort: String? = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
val listSort: String = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
else PrefManager.getVal(PrefName.MangaListSortOrder)
val sort = listSort ?: sortOrder ?: options?.rowOrder
for (i in sorted.keys) {
@@ -885,23 +881,18 @@ class AnilistQueries {
sort: String? = null,
genres: MutableList<String>? = null,
tags: MutableList<String>? = null,
status: String? = null,
source: String? = null,
format: String? = null,
countryOfOrigin: String? = null,
isAdult: Boolean = false,
onList: Boolean? = null,
excludedGenres: MutableList<String>? = null,
excludedTags: MutableList<String>? = null,
startYear: Int? = null,
seasonYear: Int? = null,
season: String? = null,
id: Int? = null,
hd: Boolean = false,
adultOnly: Boolean = false
): SearchResults? {
val query = """
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) {
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
pageInfo {
total
@@ -946,19 +937,14 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
}
""".replace("\n", " ").replace(""" """, "")
val variables = """{"type":"$type","isAdult":$isAdult
${if (adultOnly) ""","isAdult":true""" else ""}
${if (onList != null) ""","onList":$onList""" else ""}
${if (page != null) ""","page":"$page"""" else ""}
${if (id != null) ""","id":"$id"""" else ""}
${if (type == "ANIME" && seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
${if (type == "MANGA" && startYear != null) ""","yearGreater":${startYear}0000,"yearLesser":${startYear + 1}0000""" else ""}
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
${if (season != null) ""","season":"$season"""" else ""}
${if (search != null) ""","search":"$search"""" else ""}
${if (source != null) ""","source":"$source"""" else ""}
${if (sort != null) ""","sort":"$sort"""" else ""}
${if (status != null) ""","status":"$status"""" else ""}
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
${if (countryOfOrigin != null) ""","countryOfOrigin":"$countryOfOrigin"""" else ""}
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
${
if (excludedGenres?.isNotEmpty() == true)
@@ -990,6 +976,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
else ""
}
}""".replace("\n", " ").replace(""" """, "")
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page
if (response?.media != null) {
val responseArray = arrayListOf<Media>()
@@ -1021,11 +1008,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
excludedGenres = excludedGenres,
tags = tags,
excludedTags = excludedTags,
status = status,
source = source,
format = format,
countryOfOrigin = countryOfOrigin,
startYear = startYear,
seasonYear = seasonYear,
season = season,
results = responseArray,
@@ -1035,60 +1018,9 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
}
return null
}
private val onListAnime = (if(PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace("\"", "")
private val isAdult = (if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "")
private fun recentAnimeUpdates(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${System.currentTimeMillis() / 1000 - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}"""
}
private fun trendingMovies(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: ANIME, format: MOVIE, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun topRatedAnime(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun mostFavAnime(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
suspend fun loadAnimeList(): Query.AnimeList?{
return executeQuery<Query.AnimeList>(
"""{
recentUpdates:${recentAnimeUpdates()}
trendingMovies:${trendingMovies()}
topRated:${topRatedAnime()}
mostFav:${mostFavAnime()}
}""".trimIndent(), force = true
)
}
private val onListManga = (if(PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace("\"", "")
private fun trendingManga(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA,countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun trendingManhwa(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, countryOfOrigin:KR, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun trendingNovel(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, format: NOVEL, countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun topRatedManga(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun mostFavManga(): String{
return """Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
suspend fun loadMangaList(): Query.MangaList?{
return executeQuery<Query.MangaList>(
"""{
trendingManga:${trendingManga()}
trendingManhwa:${trendingManhwa()}
trendingNovel:${trendingNovel()}
topRated:${topRatedManga()}
mostFav:${mostFavManga()}
}""".trimIndent(), force = true
)
}
suspend fun recentlyUpdated(
smaller: Boolean = true,
greater: Long = 0,
lesser: Long = System.currentTimeMillis() / 1000 - 10000
): MutableList<Media>? {
@@ -1138,6 +1070,21 @@ Page(page:$page,perPage:50) {
}""".replace("\n", " ").replace(""" """, "")
return executeQuery<Query.Page>(query, force = true)?.data?.page
}
if (smaller) {
val response = execute()?.airingSchedules ?: return null
val idArr = mutableListOf<Int>()
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
return response.mapNotNull { i ->
i.media?.let {
if (!idArr.contains(it.id))
if (!listOnly && (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) || (listOnly && it.mediaListEntry != null)) {
idArr.add(it.id)
Media(it)
} else null
else null
}
}.toMutableList()
} else {
var i = 1
val list = mutableListOf<Media>()
var res: Page? = null
@@ -1157,6 +1104,7 @@ Page(page:$page,perPage:50) {
i++
}
return list.reversed().toMutableList()
}
}
suspend fun getCharacterDetails(character: Character): Character {
@@ -1442,10 +1390,15 @@ Page(page:$page,perPage:50) {
"""{
favoriteAnime:${userFavMediaQuery(true, 1, id)}
favoriteManga:${userFavMediaQuery(false, 1, id)}
animeMediaList:${bannerImageQuery("ANIME", id)}
mangaMediaList:${bannerImageQuery("MANGA", id)}
}""".trimIndent(), force = true
)
}
private fun bannerImageQuery(type: String, id: Int?): String {
return """MediaListCollection(userId: ${id}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } }"""
}
suspend fun getNotifications(id: Int, page: Int = 1, resetNotification: Boolean = true): NotificationResponse? {
val reset = if (resetNotification) "true" else "false"
@@ -1473,24 +1426,6 @@ Page(page:$page,perPage:50) {
)
}
suspend fun getUpcomingAnime(id: String): List<Media> {
val res = executeQuery<Query.MediaListCollection>(
"""{MediaListCollection(userId:$id,type:ANIME){lists{name entries{media{id,isFavourite,title{userPreferred,romaji}coverImage{medium}nextAiringEpisode{timeUntilAiring}}}}}}""",
force = true
)
val list = mutableListOf<Media>()
res?.data?.mediaListCollection?.lists?.forEach { listEntry ->
listEntry.entries?.forEach { entry ->
entry.media?.nextAiringEpisode?.timeUntilAiring?.let {
list.add(Media(entry.media!!))
}
}
}
return list.sortedBy { it.timeUntilAiring }
.distinctBy { it.id }
.filter { it.timeUntilAiring != null }
}
suspend fun isUserFav(favType: AnilistMutations.FavType, id: Int): Boolean { //anilist isFavourite is broken, so we need to check it manually
val res = getUserProfile(Anilist.userid?: return false)
return when (favType) {

View File

@@ -5,8 +5,6 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.webkit.internal.ApiFeature.P
import androidx.webkit.internal.StartupApiFeature
import ani.dantotsu.BuildConfig
import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord
@@ -60,36 +58,45 @@ class AnilistHomeViewModel : ViewModel() {
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
private val animeFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
private val animePlanned: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
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)
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
private val mangaFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
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)
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
suspend fun initHomePage() {
val res = Anilist.query.initHomePage()
@@ -105,8 +112,8 @@ class AnilistHomeViewModel : ViewModel() {
suspend fun loadMain(context: FragmentActivity) {
Anilist.getSavedToken()
MAL.getSavedToken()
Discord.getSavedToken()
MAL.getSavedToken(context)
Discord.getSavedToken(context)
if (!BuildConfig.FLAVOR.contains("fdroid")) {
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
}
@@ -137,19 +144,22 @@ class AnilistAnimeViewModel : ViewModel() {
sort = Anilist.sortBy[2],
season = season,
seasonYear = year,
hd = true,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
hd = true
)?.results
)
}
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
private val animePopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = animePopular
suspend fun loadPopular(
type: String,
searchVal: String? = null,
search_val: String? = null,
genres: ArrayList<String>? = null,
sort: String = Anilist.sortBy[1],
onList: Boolean = true,
@@ -157,11 +167,10 @@ class AnilistAnimeViewModel : ViewModel() {
animePopular.postValue(
Anilist.query.search(
type,
search = searchVal,
search = search_val,
onList = if (onList) null else false,
sort = sort,
genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
genres = genres
)
)
}
@@ -176,60 +185,13 @@ class AnilistAnimeViewModel : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
adultOnly = PrefManager.getVal(PrefName.AdultOnly),
r.onList
)
)
var loaded: Boolean = false
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated
private val popularMovies: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMovies(): LiveData<MutableList<Media>> = popularMovies
private val topRatedAnime: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTopRated(): LiveData<MutableList<Media>> = topRatedAnime
private val mostFavAnime: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMostFav(): LiveData<MutableList<Media>> = mostFavAnime
suspend fun loadAll() {
val res = Anilist.query.loadAnimeList()?.data
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly)
res?.apply{
val idArr = mutableListOf<Int>()
updated.postValue(recentUpdates?.airingSchedules?.mapNotNull {i ->
i.media?.let {
if (!idArr.contains(it.id))
if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
idArr.add(it.id)
Media(it)
}else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)){
idArr.add(it.id)
Media(it)
}else if ((listOnly && it.mediaListEntry != null)) {
idArr.add(it.id)
Media(it)
}else null
else null
}
}?.toMutableList() ?: arrayListOf())
popularMovies.postValue(trendingMovies?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
topRatedAnime.postValue(topRated?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
mostFavAnime.postValue(mostFav?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
}
}
}
class AnilistMangaViewModel : ViewModel() {
@@ -247,17 +209,29 @@ class AnilistMangaViewModel : ViewModel() {
type,
perPage = 10,
sort = Anilist.sortBy[2],
hd = true,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
hd = true
)?.results
)
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
suspend fun loadTrendingNovel() =
updated.postValue(
Anilist.query.search(
type,
perPage = 10,
sort = Anilist.sortBy[2],
format = "NOVEL"
)?.results
)
private val mangaPopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = mangaPopular
suspend fun loadPopular(
type: String,
searchVal: String? = null,
search_val: String? = null,
genres: ArrayList<String>? = null,
sort: String = Anilist.sortBy[1],
onList: Boolean = true,
@@ -265,11 +239,10 @@ class AnilistMangaViewModel : ViewModel() {
mangaPopular.postValue(
Anilist.query.search(
type,
search = searchVal,
search = search_val,
onList = if (onList) null else false,
sort = sort,
genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
genres = genres
)
)
}
@@ -284,53 +257,17 @@ class AnilistMangaViewModel : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
r.season
)
)
var loaded: Boolean = false
private val popularManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularManga(): LiveData<MutableList<Media>> = popularManga
private val popularManhwa: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularManhwa(): LiveData<MutableList<Media>> = popularManhwa
private val popularNovel: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularNovel(): LiveData<MutableList<Media>> = popularNovel
private val topRatedManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTopRated(): LiveData<MutableList<Media>> = topRatedManga
private val mostFavManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMostFav(): LiveData<MutableList<Media>> = mostFavManga
suspend fun loadAll() {
val response = Anilist.query.loadMangaList()?.data
response?.apply {
popularManga.postValue(trendingManga?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
popularManhwa.postValue(trendingManhwa?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
popularNovel.postValue(trendingNovel?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
topRatedManga.postValue(topRated?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
mostFavManga.postValue(mostFav?.media?.map { Media(it) }?.toMutableList() ?: arrayListOf())
}
}
}
class AnilistSearch : ViewModel() {
@@ -349,17 +286,13 @@ class AnilistSearch : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season,
r.season
)
)
@@ -372,15 +305,11 @@ class AnilistSearch : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season
)
@@ -418,6 +347,11 @@ class ProfileViewModel : ViewModel() {
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
private val listImages: MutableLiveData<ArrayList<String?>> =
MutableLiveData<ArrayList<String?>>(arrayListOf())
fun getListImages(): LiveData<ArrayList<String?>> = listImages
suspend fun setData(id: Int) {
val res = Anilist.query.initProfilePage(id)
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
@@ -433,11 +367,30 @@ class ProfileViewModel : ViewModel() {
}
animeFav.postValue(ArrayList(animeList ?: arrayListOf()))
val bannerImages = arrayListOf<String?>(null, null)
val animeRandom = res?.data?.animeMediaList?.lists?.mapNotNull {
it.entries?.mapNotNull { entry ->
val imageUrl = entry.media?.bannerImage
if (imageUrl != null && imageUrl != "null") imageUrl
else null
}
}?.flatten()?.randomOrNull()
bannerImages[0] = animeRandom
val mangaRandom = res?.data?.mangaMediaList?.lists?.mapNotNull {
it.entries?.mapNotNull { entry ->
val imageUrl = entry.media?.bannerImage
if (imageUrl != null && imageUrl != "null") imageUrl
else null
}
}?.flatten()?.randomOrNull()
bannerImages[1] = mangaRandom
listImages.postValue(bannerImages)
}
fun refresh() {
mangaFav.postValue(mangaFav.value)
animeFav.postValue(animeFav.value)
listImages.postValue(listImages.value)
}
}

View File

@@ -11,17 +11,13 @@ data class SearchResults(
var onList: Boolean? = null,
var perPage: Int? = null,
var search: String? = null,
var countryOfOrigin :String? = null,
var sort: String? = null,
var genres: MutableList<String>? = null,
var excludedGenres: MutableList<String>? = null,
var tags: MutableList<String>? = null,
var excludedTags: MutableList<String>? = null,
var status: String? = null,
var source: String? = null,
var format: String? = null,
var seasonYear: Int? = null,
var startYear: Int? = null,
var season: String? = null,
var page: Int = 1,
var results: MutableList<Media>,
@@ -41,24 +37,12 @@ data class SearchResults(
)
)
}
status?.let {
list.add(SearchChip("STATUS", currContext()!!.getString(R.string.filter_status, it)))
}
source?.let {
list.add(SearchChip("SOURCE", currContext()!!.getString(R.string.filter_source, it)))
}
format?.let {
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
}
countryOfOrigin?.let {
list.add(SearchChip("COUNTRY", currContext()!!.getString(R.string.filter_country, it)))
}
season?.let {
list.add(SearchChip("SEASON", it))
}
startYear?.let {
list.add(SearchChip("START_YEAR", it.toString()))
}
seasonYear?.let {
list.add(SearchChip("SEASON_YEAR", it.toString()))
}
@@ -90,12 +74,8 @@ data class SearchResults(
fun removeChip(chip: SearchChip) {
when (chip.type) {
"SORT" -> sort = null
"STATUS" -> status = null
"SOURCE" -> source = null
"FORMAT" -> format = null
"COUNTRY" -> countryOfOrigin = null
"SEASON" -> season = null
"START_YEAR" -> startYear = null
"SEASON_YEAR" -> seasonYear = null
"GENRE" -> genres?.remove(chip.text)
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)

View File

@@ -147,35 +147,12 @@ class Query {
@Serializable
data class Data(
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?)
}
@Serializable
data class AnimeList(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
)
}
@Serializable
data class MangaList(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?,
@SerialName("animeMediaList") val animeMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("mangaMediaList") val mangaMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?
)
}
@Serializable
data class ToggleFollow(
@SerialName("data")

View File

@@ -24,7 +24,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object CommentsAPI {
private const val ADDRESS: String = "https://1224665.xyz:443"
val address: String = "https://1224665.xyz:443"
var authToken: String? = null
var userId: String? = null
var isBanned: Boolean = false
@@ -33,7 +33,7 @@ object CommentsAPI {
var totalVotes: Int = 0
suspend fun getCommentsForId(id: Int, page: Int = 1, tag: Int?, sort: String?): CommentResponse? {
var url = "$ADDRESS/comments/$id/$page"
var url = "$address/comments/$id/$page"
val request = requestBuilder()
tag?.let {
url += "?tag=$it"
@@ -61,7 +61,7 @@ object CommentsAPI {
}
suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? {
val url = "$ADDRESS/comments/parent/$id/$page"
val url = "$address/comments/parent/$id/$page"
val request = requestBuilder()
val json = try {
request.get(url)
@@ -83,7 +83,7 @@ object CommentsAPI {
}
suspend fun getSingleComment(id: Int): Comment? {
val url = "$ADDRESS/comments/$id"
val url = "$address/comments/$id"
val request = requestBuilder()
val json = try {
request.get(url)
@@ -105,7 +105,7 @@ object CommentsAPI {
}
suspend fun vote(commentId: Int, voteType: Int): Boolean {
val url = "$ADDRESS/comments/vote/$commentId/$voteType"
val url = "$address/comments/vote/$commentId/$voteType"
val request = requestBuilder()
val json = try {
request.post(url)
@@ -121,7 +121,7 @@ object CommentsAPI {
}
suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? {
val url = "$ADDRESS/comments"
val url = "$address/comments"
val body = FormBody.Builder()
.add("user_id", userId ?: return null)
.add("media_id", mediaId.toString())
@@ -169,7 +169,7 @@ object CommentsAPI {
}
suspend fun deleteComment(commentId: Int): Boolean {
val url = "$ADDRESS/comments/$commentId"
val url = "$address/comments/$commentId"
val request = requestBuilder()
val json = try {
request.delete(url)
@@ -185,7 +185,7 @@ object CommentsAPI {
}
suspend fun editComment(commentId: Int, content: String): Boolean {
val url = "$ADDRESS/comments/$commentId"
val url = "$address/comments/$commentId"
val body = FormBody.Builder()
.add("content", content)
.build()
@@ -204,7 +204,7 @@ object CommentsAPI {
}
suspend fun banUser(userId: String): Boolean {
val url = "$ADDRESS/ban/$userId"
val url = "$address/ban/$userId"
val request = requestBuilder()
val json = try {
request.post(url)
@@ -225,7 +225,7 @@ object CommentsAPI {
mediaTitle: String,
reportedId: String
): Boolean {
val url = "$ADDRESS/report/$commentId"
val url = "$address/report/$commentId"
val body = FormBody.Builder()
.add("username", username)
.add("mediaName", mediaTitle)
@@ -247,7 +247,7 @@ object CommentsAPI {
}
suspend fun getNotifications(client: OkHttpClient): NotificationResponse? {
val url = "$ADDRESS/notification/reply"
val url = "$address/notification/reply"
val request = requestBuilder(client)
val json = try {
request.get(url)
@@ -268,7 +268,7 @@ object CommentsAPI {
}
private suspend fun getUserDetails(client: OkHttpClient? = null): User? {
val url = "$ADDRESS/user"
val url = "$address/user"
val request = if (client != null) requestBuilder(client) else requestBuilder()
val json = try {
request.get(url)
@@ -310,7 +310,7 @@ object CommentsAPI {
}
}
val url = "$ADDRESS/authenticate"
val url = "$address/authenticate"
val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return
repeat(MAX_RETRIES) {
try {
@@ -348,17 +348,6 @@ object CommentsAPI {
snackString("Failed to login after multiple attempts")
}
fun logout() {
PrefManager.removeVal(PrefName.CommentAuthResponse)
PrefManager.removeVal(PrefName.CommentTokenExpiry)
authToken = null
userId = null
isBanned = false
isAdmin = false
isMod = false
totalVotes = 0
}
private suspend fun authRequest(
token: String,
url: String,

View File

@@ -20,14 +20,14 @@ object Discord {
var avatar: String? = null
fun getSavedToken(): Boolean {
fun getSavedToken(context: Context): Boolean {
token = PrefManager.getVal(
PrefName.DiscordToken, null as String?
)
return token != null
}
fun saveToken(token: String) {
fun saveToken(context: Context, token: String) {
PrefManager.setVal(PrefName.DiscordToken, token)
}

View File

@@ -5,12 +5,16 @@ 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 androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@@ -33,6 +37,7 @@ 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
@@ -44,7 +49,6 @@ class DiscordService : Service() {
private lateinit var heartbeatThread: Thread
private lateinit var client: OkHttpClient
private lateinit var wakeLock: PowerManager.WakeLock
private val shouldLog = false
var presenceStore = ""
val json = Json {
encodeDefaults = true
@@ -63,7 +67,7 @@ class DiscordService : Service() {
PowerManager.PARTIAL_WAKE_LOCK,
"discordRPC:backgroundPresence"
)
wakeLock.acquire(30*60*1000L /*30 minutes*/)
wakeLock.acquire()
log("WakeLock Acquired")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
@@ -158,8 +162,8 @@ class DiscordService : Service() {
inner class DiscordWebSocketListener : WebSocketListener() {
private var retryAttempts = 0
private val maxRetryAttempts = 10
var retryAttempts = 0
val maxRetryAttempts = 10
override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
this@DiscordService.webSocket = webSocket
@@ -228,7 +232,7 @@ class DiscordService : Service() {
resume()
resume = false
} else {
identify(webSocket)
identify(webSocket, baseContext)
log("WebSocket: Identified")
}
}
@@ -241,13 +245,13 @@ class DiscordService : Service() {
}
}
private fun identify(webSocket: WebSocket) {
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())
d.addProperty("token", getToken(context))
d.addProperty("intents", 0)
d.add("properties", properties)
val payload = JsonObject()
@@ -266,7 +270,7 @@ class DiscordService : Service() {
retryAttempts++
if (retryAttempts >= maxRetryAttempts) {
log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
errorNotification("Timeout setting presence", "Max Retry Attempts")
errorNotification("Could not set the presence", "Max Retry Attempts")
return
}
}
@@ -307,7 +311,7 @@ class DiscordService : Service() {
}
}
fun getToken(): String {
fun getToken(context: Context): String {
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
return if (token == null) {
log("WebSocket: Token not found")
@@ -345,13 +349,13 @@ class DiscordService : Service() {
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
//TODO: Request permission
return
}
notificationManager.notify(2, builder.build())
log("Error Notified")
}
@Suppress("unused")
fun saveSimpleTestPresence() {
val file = File(baseContext.cacheDir, "payload")
//fill with test payload
@@ -371,22 +375,20 @@ class DiscordService : Service() {
log("WebSocket: Simple Test Presence Saved")
}
fun setPresence(string: String) {
fun setPresence(String: String) {
log("WebSocket: Sending Presence payload")
log(string)
webSocket.send(string)
log(String)
webSocket.send(String)
}
fun log(string: String) {
if (shouldLog) {
Logger.log(string)
}
//Logger.log(string)
}
fun resume() {
log("Sending Resume payload")
val d = JsonObject()
d.addProperty("token", getToken())
d.addProperty("token", getToken(baseContext))
d.addProperty("session_id", sessionId)
d.addProperty("seq", sequence)
val json = JsonObject()
@@ -402,7 +404,8 @@ class DiscordService : Service() {
Thread.sleep(heartbeat.toLong())
heartbeatSend(webSocket, sequence)
log("WebSocket: Heartbeat Sent")
} catch (ignored: InterruptedException) { }
} catch (e: InterruptedException) {
}
}
}

View File

@@ -75,7 +75,7 @@ class Login : AppCompatActivity() {
}
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
finish()
saveToken(token)
saveToken(this, token)
startMainActivity(this@Login)
}

View File

@@ -1,84 +0,0 @@
package ani.dantotsu.connections.github
import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.getAppString
import ani.dantotsu.settings.Developer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
class Contributors {
fun getContributors(): Array<Developer> {
var developers = arrayOf<Developer>()
runBlocking(Dispatchers.IO) {
val repo = getAppString(R.string.repo)
val res = client.get("https://api.github.com/repos/$repo/contributors")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
res.find { it.login == "rebelonion"}?.let { first ->
developers = developers.plus(
Developer(
first.login,
first.avatarUrl,
"Owner and Maintainer",
first.htmlUrl
)
).plus(arrayOf(
Developer(
"Wai What",
"https://avatars.githubusercontent.com/u/149729762?v=4",
"Icon Designer",
"https://github.com/WaiWhat"
),
Developer(
"MarshMeadow",
"https://avatars.githubusercontent.com/u/88599122?v=4",
"Beta Icon Designer",
"https://github.com/MarshMeadow?tab=repositories"
),
Developer(
"Zaxx69",
"https://avatars.githubusercontent.com/u/138523882?v=4",
"Telegram Admin",
"https://github.com/Zaxx69"
),
Developer(
"Arif Alam",
"https://avatars.githubusercontent.com/u/70383209?v=4",
"Head Discord Moderator",
"https://youtube.com/watch?v=dQw4w9WgXcQ"
)
))
}
res.filter {it.login != "rebelonion"}.forEach {
developers = developers.plus(
Developer(
it.login,
it.avatarUrl,
"Contributor",
it.htmlUrl
)
)
}
}
return developers
}
@Serializable
data class GithubResponse(
@SerialName("login")
val login: String,
@SerialName("avatar_url")
val avatarUrl: String,
@SerialName("html_url")
val htmlUrl: String
)
}

View File

@@ -1,55 +0,0 @@
package ani.dantotsu.connections.github
import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.getAppString
import ani.dantotsu.settings.Developer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
class Forks {
fun getForks(): Array<Developer> {
var forks = arrayOf<Developer>()
runBlocking(Dispatchers.IO) {
val res = client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
res.forEach {
forks = forks.plus(
Developer(
it.name,
it.owner.avatarUrl,
it.owner.login,
it.htmlUrl
)
)
}
}
return forks
}
@Serializable
data class GithubResponse(
@SerialName("name")
val name: String,
val owner: Owner,
@SerialName("html_url")
val htmlUrl: String,
) {
@Serializable
data class Owner(
@SerialName("login")
val login: String,
@SerialName("avatar_url")
val avatarUrl: String
)
}
}

View File

@@ -5,6 +5,7 @@ import android.content.Context
import android.net.Uri
import android.util.Base64
import androidx.browser.customtabs.CustomTabsIntent
import androidx.fragment.app.FragmentActivity
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.currContext
@@ -63,7 +64,7 @@ object MAL {
}
suspend fun getSavedToken(): Boolean {
suspend fun getSavedToken(context: FragmentActivity): Boolean {
return tryWithSuspend(false) {
var res: ResponseToken =
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
@@ -76,7 +77,7 @@ object MAL {
} ?: false
}
fun removeSavedToken() {
fun removeSavedToken(context: Context) {
token = null
username = null
userid = null

View File

@@ -1,25 +1,13 @@
package ani.dantotsu.download
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.media.MediaType
import android.os.Environment
import android.widget.Toast
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.findFolder
import com.anggrayudi.storage.file.moveFileTo
import com.anggrayudi.storage.file.moveFolderTo
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.Serializable
class DownloadsManager(private val context: Context) {
@@ -27,11 +15,11 @@ class DownloadsManager(private val context: Context) {
private val downloadsList = loadDownloads().toMutableList()
val mangaDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == MediaType.MANGA }
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
val animeDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == MediaType.ANIME }
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
val novelDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == MediaType.NOVEL }
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
private fun saveDownloads() {
val jsonString = gson.toJson(downloadsList)
@@ -53,66 +41,84 @@ class DownloadsManager(private val context: Context) {
saveDownloads()
}
fun removeDownload(downloadedType: DownloadedType, onFinished: () -> Unit) {
fun removeDownload(downloadedType: DownloadedType) {
downloadsList.remove(downloadedType)
CoroutineScope(Dispatchers.IO).launch {
removeDirectory(downloadedType)
withContext(Dispatchers.Main) {
onFinished()
}
}
removeDirectory(downloadedType)
saveDownloads()
}
fun removeMedia(title: String, type: MediaType) {
val baseDirectory = getBaseDirectory(context, type)
val directory = baseDirectory?.findFolder(title)
if (directory?.exists() == true) {
val deleted = directory.deleteRecursively(context, false)
fun removeMedia(title: String, type: DownloadedType.Type) {
val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga"
} else if (type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory/$title"
)
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (deleted) {
snackString("Successfully deleted")
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
} else {
snackString("Failed to delete directory")
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
}
} else {
snackString("Directory does not exist")
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
cleanDownloads()
}
when (type) {
MediaType.MANGA -> {
downloadsList.removeAll { it.title == title && it.type == MediaType.MANGA }
DownloadedType.Type.MANGA -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
}
MediaType.ANIME -> {
downloadsList.removeAll { it.title == title && it.type == MediaType.ANIME }
DownloadedType.Type.ANIME -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
}
MediaType.NOVEL -> {
downloadsList.removeAll { it.title == title && it.type == MediaType.NOVEL }
DownloadedType.Type.NOVEL -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
}
}
saveDownloads()
}
private fun cleanDownloads() {
cleanDownload(MediaType.MANGA)
cleanDownload(MediaType.ANIME)
cleanDownload(MediaType.NOVEL)
cleanDownload(DownloadedType.Type.MANGA)
cleanDownload(DownloadedType.Type.ANIME)
cleanDownload(DownloadedType.Type.NOVEL)
}
private fun cleanDownload(type: MediaType) {
private fun cleanDownload(type: DownloadedType.Type) {
// remove all folders that are not in the downloads list
val directory = getBaseDirectory(context, type)
val downloadsSubLists = when (type) {
MediaType.MANGA -> mangaDownloadedTypes
MediaType.ANIME -> animeDownloadedTypes
else -> novelDownloadedTypes
val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga"
} else if (type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
if (directory?.exists() == true && directory.isDirectory) {
val directory = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory"
)
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
mangaDownloadedTypes
} else if (type == DownloadedType.Type.ANIME) {
animeDownloadedTypes
} else {
novelDownloadedTypes
}
if (directory.exists()) {
val files = directory.listFiles()
for (file in files) {
if (!downloadsSubLists.any { it.title == file.name }) {
file.deleteRecursively(context, false)
if (files != null) {
for (file in files) {
if (!downloadsSubLists.any { it.title == file.name }) {
val deleted = file.deleteRecursively()
}
}
}
}
@@ -120,64 +126,34 @@ class DownloadsManager(private val context: Context) {
val iterator = downloadsList.iterator()
while (iterator.hasNext()) {
val download = iterator.next()
val downloadDir = directory?.findFolder(download.title)
if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) {
val downloadDir = File(directory, download.title)
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
iterator.remove()
}
}
}
fun moveDownloadsDir(context: Context, oldUri: Uri, newUri: Uri, finished: (Boolean, String) -> Unit) {
try {
if (oldUri == newUri) {
finished(false, "Source and destination are the same")
return
}
CoroutineScope(Dispatchers.IO).launch {
val oldBase =
DocumentFile.fromTreeUri(context, oldUri) ?: throw Exception("Old base is null")
val newBase =
DocumentFile.fromTreeUri(context, newUri) ?: throw Exception("New base is null")
val folder =
oldBase.findFolder(BASE_LOCATION) ?: throw Exception("Base folder not found")
folder.moveFolderTo(context, newBase, false, BASE_LOCATION, object:
FolderCallback() {
override fun onFailed(errorCode: ErrorCode) {
when (errorCode) {
ErrorCode.CANCELED -> finished(false, "Move canceled")
ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> finished(false, "Cannot create file in target")
ErrorCode.INVALID_TARGET_FOLDER -> finished(true, "Invalid target folder") // seems to still work
ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> finished(false, "No space left on target path")
ErrorCode.UNKNOWN_IO_ERROR -> finished(false, "Unknown IO error")
ErrorCode.SOURCE_FOLDER_NOT_FOUND -> finished(false, "Source folder not found")
ErrorCode.STORAGE_PERMISSION_DENIED -> finished(false, "Storage permission denied")
ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> finished(false, "Target folder cannot have same path with source folder")
else -> finished(false, "Failed to move downloads: $errorCode")
}
Logger.log("Failed to move downloads: $errorCode")
super.onFailed(errorCode)
}
override fun onCompleted(result: Result) {
finished(true, "Successfully moved downloads")
super.onCompleted(result)
}
})
}
} catch (e: Exception) {
snackString("Error: ${e.message}")
finished(false, "Failed to move downloads: ${e.message}")
return
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
{
val jsonString = gson.toJson(downloadsList)
val file = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/downloads.json"
)
if (file.parentFile?.exists() == false) {
file.parentFile?.mkdirs()
}
if (!file.exists()) {
file.createNewFile()
}
file.writeText(jsonString)
}
fun queryDownload(downloadedType: DownloadedType): Boolean {
return downloadsList.contains(downloadedType)
}
fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean {
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
return if (type == null) {
downloadsList.any { it.title == title && it.chapter == chapter }
} else {
@@ -186,35 +162,86 @@ class DownloadsManager(private val context: Context) {
}
private fun removeDirectory(downloadedType: DownloadedType) {
val baseDirectory = getBaseDirectory(context, downloadedType.type)
val directory =
baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
)
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
// Check if the directory exists and delete it recursively
if (directory?.exists() == true) {
val deleted = directory.deleteRecursively(context, false)
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (deleted) {
snackString("Successfully deleted")
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
} else {
snackString("Failed to delete directory")
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
}
} else {
snackString("Directory does not exist")
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
}
fun purgeDownloads(type: MediaType) {
val directory = getBaseDirectory(context, type)
if (directory?.exists() == true) {
val deleted = directory.deleteRecursively(context, false)
if (deleted) {
snackString("Successfully deleted")
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
)
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
val destination = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
)
if (directory.exists()) {
val copied = directory.copyRecursively(destination, true)
if (copied) {
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
} else {
snackString("Failed to delete directory")
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
}
} else {
snackString("Directory does not exist")
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
}
fun purgeDownloads(type: DownloadedType.Type) {
val directory = if (type == DownloadedType.Type.MANGA) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
} else if (type == DownloadedType.Type.ANIME) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
}
if (directory.exists()) {
val deleted = directory.deleteRecursively()
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.type == type }
@@ -222,95 +249,62 @@ class DownloadsManager(private val context: Context) {
}
companion object {
private const val BASE_LOCATION = "Dantotsu"
private const val MANGA_SUB_LOCATION = "Manga"
private const val ANIME_SUB_LOCATION = "Anime"
private const val NOVEL_SUB_LOCATION = "Novel"
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
const val novelLocation = "Dantotsu/Novel"
const val mangaLocation = "Dantotsu/Manga"
const val animeLocation = "Dantotsu/Anime"
fun String?.findValidName(): String {
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
}
/**
* Get and create a base directory for the given type
* @param context the context
* @param type the type of media
* @return the base directory
*/
private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null
var base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
base = base.findOrCreateFolder(BASE_LOCATION, false) ?: return null
return when (type) {
MediaType.MANGA -> {
base.findOrCreateFolder(MANGA_SUB_LOCATION, false)
}
MediaType.ANIME -> {
base.findOrCreateFolder(ANIME_SUB_LOCATION, false)
}
else -> {
base.findOrCreateFolder(NOVEL_SUB_LOCATION, false)
}
}
}
/**
* Get and create a subdirectory for the given type
* @param context the context
* @param type the type of media
* @param title the title of the media
* @param chapter the chapter of the media
* @return the subdirectory
*/
fun getSubDirectory(
fun getDirectory(
context: Context,
type: MediaType,
overwrite: Boolean,
type: DownloadedType.Type,
title: String,
chapter: String? = null
): DocumentFile? {
val baseDirectory = getBaseDirectory(context, type) ?: return null
return if (chapter != null) {
baseDirectory.findOrCreateFolder(title, false)
?.findOrCreateFolder(chapter, overwrite)
): File {
return if (type == DownloadedType.Type.MANGA) {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$mangaLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$mangaLocation/$title"
)
}
} else if (type == DownloadedType.Type.ANIME) {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title"
)
}
} else {
baseDirectory.findOrCreateFolder(title, overwrite)
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title"
)
}
}
}
}
fun getDirSize(context: Context, type: MediaType, title: String, chapter: String? = null): Long {
val directory = getSubDirectory(context, type, false, title, chapter) ?: return 0
var size = 0L
directory.listFiles().forEach {
size += it.length()
}
return size
}
private fun DocumentFile.findOrCreateFolder(
name: String, overwrite: Boolean
): DocumentFile? {
return if (overwrite) {
findFolder(name.findValidName())?.delete()
createDirectory(name.findValidName())
} else {
findFolder(name.findValidName()) ?: createDirectory(name.findValidName())
}
}
}
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
enum class Type {
MANGA,
ANIME,
NOVEL
}
}
data class DownloadedType(
val pTitle: String, val pChapter: String, val type: MediaType
) : Serializable {
val title: String
get() = pTitle.findValidName()
val chapter: String
get() = pChapter.findValidName()
}

View File

@@ -9,36 +9,32 @@ 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 androidx.documentfile.provider.DocumentFile
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.download.video.Helper
import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.FFmpegKitConfig
import com.arthenica.ffmpegkit.FFprobeKit
import com.arthenica.ffmpegkit.SessionState
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
@@ -49,21 +45,25 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
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 tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
class AnimeDownloaderService : Service() {
private lateinit var notificationManager: NotificationManagerCompat
@@ -89,7 +89,6 @@ class AnimeDownloaderService : Service() {
setSmallIcon(R.drawable.ic_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
setProgress(100, 0, false)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
@@ -158,14 +157,27 @@ class AnimeDownloaderService : Service() {
@UnstableApi
fun cancelDownload(taskName: String) {
val sessionIds =
AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
.map { it.sessionId }.toMutableList()
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
sessionIds.forEach {
FFmpegKit.cancel(it)
val url =
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
if (url.isEmpty()) {
snackString("Failed to cancel download")
return
}
currentTasks.removeAll { it.getTaskName() == taskName }
DownloadService.sendSetStopReason(
this@AnimeDownloaderService,
ExoplayerDownloadService::class.java,
url,
androidx.media3.exoplayer.offline.Download.STATE_STOPPED,
false
)
DownloadService.sendRemoveDownload(
this@AnimeDownloaderService,
ExoplayerDownloadService::class.java,
url,
false
)
CoroutineScope(Dispatchers.Default).launch {
mutex.withLock {
downloadJobs[taskName]?.cancel()
@@ -198,7 +210,7 @@ class AnimeDownloaderService : Service() {
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) {
try {
//val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
@@ -209,80 +221,18 @@ class AnimeDownloaderService : Service() {
true
}
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
builder.setContentText("Downloading ${task.title} - ${task.episode}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
val outputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title,
task.episode
) ?: throw Exception("Failed to create output directory")
outputDir.findFile("${task.getTaskName()}.mp4")?.delete()
val outputFile = outputDir.createFile("video/mp4", "${task.getTaskName()}.mp4")
?: throw Exception("Failed to create output file")
var percent = 0
var totalLength = 0.0
val path = FFmpegKitConfig.getSafParameterForWrite(
this@AnimeDownloaderService,
outputFile.uri
)
val headersStringBuilder = StringBuilder().append(" ")
task.video.file.headers.forEach {
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
currActivity()?.let {
Helper.downloadVideo(
it,
task.video,
task.subtitle
)
}
headersStringBuilder.append(" ")
FFprobeKit.executeAsync(
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\"",
{
Logger.log("FFprobeKit: $it")
}, {
if (it.message.toDoubleOrNull() != null) {
totalLength = it.message.toDouble()
}
})
var request = "-headers"
val headers = headersStringBuilder.toString()
if (task.video.file.headers.isNotEmpty()) {
request += headers
}
request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
println("Request: $request")
val ffTask =
FFmpegKit.executeAsync(request,
{ session ->
val state: SessionState = session.state
val returnCode = session.returnCode
// CALLED WHEN SESSION IS EXECUTED
Logger.log(
java.lang.String.format(
"FFmpeg process exited with state %s and rc %s.%s",
state,
returnCode,
session.failStackTrace
)
)
}, {
// CALLED WHEN SESSION PRINTS LOGS
Logger.log(it.message)
}) {
// CALLED WHEN SESSION GENERATES STATISTICS
val timeInMilliseconds = it.time
if (timeInMilliseconds > 0 && totalLength > 0) {
percent = ((it.time / 1000) / totalLength * 100).toInt()
}
Logger.log("Statistics: $it")
}
task.sessionId = ffTask.sessionId
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
ffTask.sessionId
saveMediaInfo(task)
task.subtitle?.let {
@@ -292,119 +242,90 @@ class AnimeDownloaderService : Service() {
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
DownloadedType.Type.ANIME,
)
)
}
val downloadStarted =
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
if (!downloadStarted) {
Logger.log("Download failed to start")
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed to start")
broadcastDownloadFailed(task.episode)
return@withContext
}
// periodically check if the download is complete
while (ffTask.state != SessionState.COMPLETED) {
if (ffTask.state == SessionState.FAILED) {
Logger.log("Download failed")
builder.setContentText(
"${
getTaskName(
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) {
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
Logger.log("Download failed")
builder.setContentText("${task.title} - ${task.episode} Download failed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed")
Logger.log("Download failed: ${download.failureReason}")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode
task.episode,
DownloadedType.Type.ANIME,
)
} Download failed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download failed")
Logger.log("Download failed: ${ffTask.failStackTrace}")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${getTaskName(task.title, task.episode)}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${download.failureReason}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
Logger.log("Download completed")
builder.setContentText("${task.title} - ${task.episode} Download completed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
Logger.log("Download stopped")
builder.setContentText("${task.title} - ${task.episode} Download stopped")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download stopped")
break
}
broadcastDownloadProgress(
task.episode,
download.percentDownloaded.toInt()
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
break
}
builder.setProgress(
100, percent.coerceAtMost(99),
false
)
broadcastDownloadProgress(
task.episode,
percent.coerceAtMost(99)
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
kotlinx.coroutines.delay(2000)
}
if (ffTask.state == SessionState.COMPLETED) {
if (ffTask.returnCode.isValueError) {
Logger.log("Download failed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode
)
} Download failed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download failed")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${getTaskName(task.title, task.episode)}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
return@withContext
}
Logger.log("Download completed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode
)
} Download completed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
} else throw Exception("Download failed")
}
} catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
@@ -417,24 +338,34 @@ class AnimeDownloaderService : Service() {
}
}
private fun saveMediaInfo(task: AnimeDownloadTask) {
CoroutineScope(Dispatchers.IO).launch {
val directory =
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
val episodeDirectory =
getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title,
task.episode
)
?: throw Exception("Directory not found")
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun hasDownloadStarted(
downloadManager: DownloadManager,
task: AnimeDownloadTask,
timeout: Long
): Boolean {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < timeout) {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) {
return true
}
// Delay between each poll
kotlinx.coroutines.delay(500)
}
return false
}
private fun saveMediaInfo(task: AnimeDownloadTask) {
launchIO {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.animeLocation}/${task.title}"
)
val episodeDirectory = File(directory, task.episode)
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -468,25 +399,14 @@ class AnimeDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
try {
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@AnimeDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
file.writeText(jsonString)
}
}
}
}
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
@@ -497,16 +417,13 @@ class AnimeDownloaderService : Service() {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
val file = File(directory, name)
FileOutputStream(file).use { output ->
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.uri.toString()
return@withContext file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
@@ -573,15 +490,14 @@ class AnimeDownloaderService : Service() {
val episodeImage: String? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
var sessionId: Long = -1
) {
fun getTaskName(): String {
return "${title.replace("/", "")}/${episode.replace("/", "")}"
return "$title - $episode"
}
companion object {
fun getTaskName(title: String, episode: String): String {
return "${title.replace("/", "")}/${episode.replace("/", "")}"
return "$title - $episode"
}
}
}
@@ -595,6 +511,7 @@ class AnimeDownloaderService : Service() {
object AnimeServiceDataSingleton {
var video: Video? = null
var sourceMedia: Media? = null
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
@Volatile

View File

@@ -1,6 +1,7 @@
package ani.dantotsu.download.anime
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
@@ -37,6 +38,7 @@ class OfflineAnimeAdapter(
return position.toLong()
}
@SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: View = convertView ?: when (style) {
@@ -49,27 +51,28 @@ class OfflineAnimeAdapter(
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
val totalEpisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
val totalepisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
val typeimage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
if (style == 0) {
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
val episodes = view.findViewById<TextView>(R.id.itemTotal)
episodes.text = context.getString(R.string.episodes)
bannerView.setImageURI(item.banner ?: item.image)
totalEpisodes.text = item.totalEpisodeList
episodes.text = " Episodes"
bannerView.setImageURI(item.banner)
totalepisodes.text = item.totalEpisodeList
} else if (style == 1) {
val watchedEpisodes =
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
watchedEpisodes.text = item.watchedEpisode
totalEpisodes.text = context.getString(R.string.total_divider, item.totalEpisode)
totalepisodes.text = " | " + item.totalEpisode
}
// Bind item data to the views
typeImage.setImageResource(R.drawable.ic_round_movie_filter_24)
typeimage.setImageResource(R.drawable.ic_round_movie_filter_24)
type.text = item.type
typeView.visibility = View.VISIBLE
imageView.setImageURI(item.image)

View File

@@ -4,6 +4,7 @@ package ani.dantotsu.download.anime
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
@@ -21,10 +22,8 @@ import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import ani.dantotsu.R
import ani.dantotsu.bottomBar
@@ -33,19 +32,16 @@ import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.initActivity
import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout
@@ -57,13 +53,9 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@@ -72,7 +64,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private lateinit var gridView: GridView
private lateinit var adapter: OfflineAnimeAdapter
private lateinit var total: TextView
private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -119,10 +110,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
})
var style: Int = PrefManager.getVal(PrefName.OfflineView)
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
val layoutCompact = view.findViewById<ImageView>(R.id.downloadedGrid)
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
var selected = when (style) {
0 -> layoutList
1 -> layoutCompact
1 -> layoutcompact
else -> layoutList
}
selected.alpha = 1f
@@ -143,7 +134,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
grid()
}
layoutCompact.setOnClickListener {
layoutcompact.setOnClickListener {
selected(it as ImageView)
style = 1
PrefManager.setVal(PrefName.OfflineView, style)
@@ -163,11 +154,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@OptIn(UnstableApi::class)
private fun grid() {
gridView.visibility = View.VISIBLE
getDownloads()
val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
getDownloads()
gridView.adapter = adapter
gridView.scheduleLayoutAnimation()
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
@@ -175,22 +166,20 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val media =
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title.findValidName() }
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
media?.let {
lifecycleScope.launch {
val mediaModel = getMedia(it)
if (mediaModel == null) {
snackString("Error loading media.json")
return@launch
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
val mediaModel = getMedia(it)
if (mediaModel == null) {
snackString("Error loading media.json")
return@let
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
} ?: run {
snackString("no media found")
}
@@ -198,7 +187,8 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
gridView.setOnItemLongClickListener { _, _, position, _ ->
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val type: MediaType = MediaType.ANIME
val type: DownloadedType.Type =
DownloadedType.Type.ANIME
// Alert dialog to confirm deletion
val builder =
@@ -213,7 +203,13 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
if (mediaIds.isEmpty()) {
snackString("No media found") // if this happens, terrible things have happened
}
for (mediaId in mediaIds) {
ani.dantotsu.download.video.Helper.downloadManager(requireContext())
.removeDownload(mediaId.toString())
}
getDownloads()
adapter.setItems(downloads)
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
@@ -241,6 +237,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
}
override fun onScroll(
@@ -253,7 +250,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val visibility = first != null && first.top < 0
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
scrollTop.isVisible = visibility
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
}
})
initActivity(requireActivity())
@@ -263,6 +260,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -282,39 +280,29 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private fun getDownloads() {
downloads = listOf()
if (downloadsJob.isActive) {
downloadsJob.cancel()
}
downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) {
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineAnimeModel = loadOfflineAnimeModel(download)
newAnimeDownloads += offlineAnimeModel
}
downloads = newAnimeDownloads
withContext(Dispatchers.Main) {
adapter.setItems(downloads)
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
adapter.notifyDataSetChanged()
}
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) {
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineAnimeModel = loadOfflineAnimeModel(download)
newAnimeDownloads += offlineAnimeModel
}
downloads = newAnimeDownloads
}
/**
* Load media.json file from the directory and convert it to Media class
* @param downloadedType DownloadedType object
* @return Media object
*/
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
private fun getMedia(downloadedType: DownloadedType): Media? {
val type = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
DownloadedType.Type.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
return try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.title
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -326,13 +314,8 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
})
.create()
val media = directory?.findFile("media.json")
?: return null
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
}
?: return null
val media = File(directory, "media.json")
val mediaJson = media.readText()
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
@@ -342,26 +325,26 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
}
}
/**
* Load OfflineAnimeModel from the directory
* @param downloadedType DownloadedType object
* @return OfflineAnimeModel object
*/
private suspend fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
val type = downloadedType.type.asText()
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
val type = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
DownloadedType.Type.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.title
)
val mediaModel = getMedia(downloadedType)!!
val cover = directory?.findFile("cover.jpg")
val coverUri: Uri? = if (cover?.exists() == true) {
cover.uri
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else null
val banner = directory?.findFile("banner.jpg")
val bannerUri: Uri? = if (banner?.exists() == true) {
banner.uri
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore

View File

@@ -10,20 +10,19 @@ import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
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 androidx.documentfile.provider.DocumentFile
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
@@ -31,10 +30,6 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROG
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
@@ -44,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
@@ -55,6 +51,8 @@ import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Queue
@@ -191,20 +189,13 @@ class MangaDownloaderService : Service() {
true
}
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title,
task.chapter
)?.deleteRecursively(this@MangaDownloaderService)
// Loop through each ImageData object from the task
var farthest = 0
for ((index, image) in task.imageData.withIndex()) {
@@ -220,7 +211,8 @@ class MangaDownloaderService : Service() {
while (bitmap == null && retryCount < task.retries) {
bitmap = image.fetchAndProcessImage(
image.page,
image.source
image.source,
this@MangaDownloaderService
)
retryCount++
}
@@ -254,7 +246,7 @@ class MangaDownloaderService : Service() {
DownloadedType(
task.title,
task.chapter,
MediaType.MANGA
DownloadedType.Type.MANGA
)
)
broadcastDownloadFinished(task.chapter)
@@ -272,18 +264,24 @@ class MangaDownloaderService : Service() {
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
try {
// Define the directory within the private external storage space
val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
?: throw Exception("Directory not found")
directory.findFile(fileName)?.forceDelete(this)
// Create a file reference within that directory for the image
val file =
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
val directory = File(
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$title/$chapter"
)
if (!directory.exists()) {
directory.mkdirs()
}
// Create a file reference within that directory for your image
val file = File(directory, fileName)
// Use a FileOutputStream to write the bitmap to the file
file.openOutputStream(this, false).use { outputStream ->
if (outputStream == null) throw Exception("Output stream is null")
FileOutputStream(file).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
}
} catch (e: Exception) {
println("Exception while saving image: ${e.message}")
snackString("Exception while saving image: ${e.message}")
@@ -294,12 +292,13 @@ class MangaDownloaderService : Service() {
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) {
launchIO {
val directory =
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${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
@@ -314,10 +313,7 @@ class MangaDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
try {
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
file.writeText(jsonString)
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
@@ -332,7 +328,7 @@ class MangaDownloaderService : Service() {
}
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
@@ -342,16 +338,14 @@ class MangaDownloaderService : Service() {
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
directory.findFile(name)?.forceDelete(this@MangaDownloaderService)
val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
val file = File(directory, name)
FileOutputStream(file).use { output ->
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.uri.toString()
return@withContext file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.download.manga
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
@@ -36,6 +37,7 @@ class OfflineMangaAdapter(
return position.toLong()
}
@SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: View = convertView ?: when (style) {
@@ -48,6 +50,7 @@ class OfflineMangaAdapter(
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
val totalChapter = view.findViewById<TextView>(R.id.itemCompactTotal)
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
@@ -57,14 +60,14 @@ class OfflineMangaAdapter(
if (style == 0) {
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
val chapters = view.findViewById<TextView>(R.id.itemTotal)
chapters.text = context.getString(R.string.chapters)
bannerView.setImageURI(item.banner ?: item.image)
chapters.text = " Chapters"
bannerView.setImageURI(item.banner)
totalChapter.text = item.totalChapter
} else if (style == 1) {
val readChapter =
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
readChapter.text = item.readChapter
totalChapter.text = context.getString(R.string.total_divider, item.totalChapter)
totalChapter.text = " | " + item.totalChapter
}
// Bind item data to the views

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.download.manga
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
@@ -19,10 +20,8 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
@@ -30,19 +29,16 @@ import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.initActivity
import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout
@@ -50,13 +46,9 @@ import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
@@ -65,7 +57,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
private lateinit var total: TextView
private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -155,11 +146,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun grid() {
gridView.visibility = View.VISIBLE
getDownloads()
val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
getDownloads()
gridView.adapter = adapter
gridView.scheduleLayoutAnimation()
total.text =
@@ -171,15 +162,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
media?.let {
lifecycleScope.launch {
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(it))
.putExtra("download", true),
null
)
}
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(it))
.putExtra("download", true),
null
)
} ?: run {
snackString("no media found")
}
@@ -188,11 +178,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
gridView.setOnItemLongClickListener { _, _, position, _ ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val type: MediaType =
val type: DownloadedType.Type =
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
MediaType.MANGA
DownloadedType.Type.MANGA
} else {
MediaType.NOVEL
DownloadedType.Type.NOVEL
}
// Alert dialog to confirm deletion
val builder =
@@ -202,6 +192,9 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
getDownloads()
adapter.setItems(downloads)
total.text =
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
@@ -230,6 +223,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
}
override fun onScroll(
@@ -240,7 +234,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
) {
val first = view.getChildAt(0)
val visibility = first != null && first.top < 0
scrollTop.isVisible = visibility
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
}
@@ -252,6 +246,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -271,62 +266,46 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun getDownloads() {
downloads = listOf()
if (downloadsJob.isActive) {
downloadsJob.cancel()
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
}
downloads = listOf()
downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
}
downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
}
downloads += newNovelDownloads
withContext(Dispatchers.Main) {
adapter.setItems(downloads)
total.text =
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
adapter.notifyDataSetChanged()
}
downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
}
downloads += newNovelDownloads
}
/**
* Load media.json file from the directory and convert it to Media class
* @param downloadedType DownloadedType object
* @return Media object
*/
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
private fun getMedia(downloadedType: DownloadedType): Media? {
val type = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
DownloadedType.Type.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
return try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.title
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
val media = directory?.findFile("media.json")
?: return null
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
}
val media = File(directory, "media.json")
val mediaJson = media.readText()
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
@@ -336,22 +315,26 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
}
}
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = downloadedType.type.asText()
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
DownloadedType.Type.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.title
)
val mediaModel = getMedia(downloadedType)!!
val cover = directory?.findFile("cover.jpg")
val coverUri: Uri? = if (cover?.exists() == true) {
cover.uri
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else null
val banner = directory?.findFile("banner.jpg")
val bannerUri: Uri? = if (banner?.exists() == true) {
banner.uri
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
@@ -359,14 +342,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val isOngoing =
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0
val readChapter = (mediaModel.userProgress ?: "~").toString()
val totalChapter = "${mediaModel.manga?.totalChapters ?: "??"}"
val readchapter = (mediaModel.userProgress ?: "~").toString()
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
val chapters = " Chapters"
return OfflineMangaModel(
title,
score,
totalChapter,
readChapter,
totalchapter,
readchapter,
type,
chapters,
isOngoing,

View File

@@ -16,19 +16,14 @@ import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications
@@ -38,6 +33,7 @@ import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
@@ -68,7 +64,7 @@ class NovelDownloaderService : Service() {
private val mutex = Mutex()
private var isCurrentlyProcessing = false
private val networkHelper = Injekt.get<NetworkHelper>()
val networkHelper = Injekt.get<NetworkHelper>()
override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services.
@@ -251,28 +247,27 @@ class NovelDownloaderService : Service() {
networkHelper.downloadClient.newCall(request).execute().use { response ->
// Ensure the response is successful and has a body
if (!response.isSuccessful) {
if (!response.isSuccessful || response.body == null) {
throw IOException("Failed to download file: ${response.message}")
}
val directory = getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title,
task.chapter
) ?: throw Exception("Directory not found")
directory.findFile("0.epub")?.forceDelete(this@NovelDownloaderService)
val file = directory.createFile("application/epub+zip", "0.epub")
?: throw Exception("File not created")
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 outputStream = this@NovelDownloaderService.contentResolver.openOutputStream(file.uri) ?: throw Exception("Could not open OutputStream")
val sink = outputStream.sink().buffer()
val sink = file.sink().buffer()
val responseBody = response.body
val totalBytes = responseBody.contentLength()
var downloadedBytes = 0L
@@ -340,7 +335,7 @@ class NovelDownloaderService : Service() {
DownloadedType(
task.title,
task.chapter,
MediaType.NOVEL
DownloadedType.Type.NOVEL
)
)
broadcastDownloadFinished(task.originalLink)
@@ -357,16 +352,13 @@ class NovelDownloaderService : Service() {
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) {
launchIO {
val directory =
DownloadsManager.getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title
) ?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
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
@@ -380,47 +372,33 @@ class NovelDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
try {
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@NovelDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
file.writeText(jsonString)
}
}
}
}
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
withContext(
Dispatchers.IO
) {
var connection: HttpURLConnection? = null
Logger.log("Downloading url $url")
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}")
}
directory.findFile(name)?.forceDelete(this@NovelDownloaderService)
val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
val file = File(directory, name)
FileOutputStream(file).use { output ->
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.uri.toString()
return@withContext file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
@@ -495,6 +473,7 @@ class NovelDownloaderService : Service() {
}
object NovelServiceDataSingleton {
var sourceMedia: Media? = null
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
@Volatile

View File

@@ -0,0 +1,37 @@
package ani.dantotsu.download.video
import android.app.Notification
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
import androidx.media3.exoplayer.offline.DownloadService
import androidx.media3.exoplayer.scheduler.PlatformScheduler
import androidx.media3.exoplayer.scheduler.Scheduler
import ani.dantotsu.R
@UnstableApi
class ExoplayerDownloadService :
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
companion object {
private const val JOB_ID = 1
private const val FOREGROUND_NOTIFICATION_ID = 1
}
override fun getDownloadManager(): DownloadManager = Helper.downloadManager(this)
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
override fun getForegroundNotification(
downloads: MutableList<Download>,
notMetRequirements: Int
): Notification =
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
this,
R.drawable.mono,
null,
null,
downloads,
notMetRequirements
)
}

View File

@@ -9,6 +9,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.OptIn
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@@ -36,7 +37,6 @@ import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
import ani.dantotsu.logError
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.okHttpClient
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType
@@ -49,10 +49,146 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.IOException
import java.util.concurrent.Executors
import java.util.concurrent.*
@SuppressLint("UnsafeOptInUsageError")
object Helper {
private var simpleCache: SimpleCache? = null
@SuppressLint("UnsafeOptInUsageError")
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
val dataSourceFactory = DataSource.Factory {
val dataSource: HttpDataSource =
OkHttpDataSource.Factory(okHttpClient).createDataSource()
defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
video.file.headers.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
dataSource
}
val mimeType = when (video.format) {
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
VideoType.DASH -> MimeTypes.APPLICATION_MPD
else -> MimeTypes.APPLICATION_MP4
}
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
var sub: MediaItem.SubtitleConfiguration? = null
if (subtitle != null) {
sub = MediaItem.SubtitleConfiguration
.Builder(Uri.parse(subtitle.file.url))
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setMimeType(
when (subtitle.type) {
SubtitleType.VTT -> MimeTypes.TEXT_VTT
SubtitleType.ASS -> MimeTypes.TEXT_SSA
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
SubtitleType.UNKNOWN -> MimeTypes.TEXT_SSA
}
)
.build()
}
if (sub != null) builder.setSubtitleConfigurations(mutableListOf(sub))
val mediaItem = builder.build()
val downloadHelper = DownloadHelper.forMediaItem(
context,
mediaItem,
DefaultRenderersFactory(context),
dataSourceFactory
)
downloadHelper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) {
helper.getDownloadRequest(null).let {
DownloadService.sendAddDownload(
context,
ExoplayerDownloadService::class.java,
it,
false
)
}
}
override fun onPrepareError(helper: DownloadHelper, e: IOException) {
logError(e)
}
})
}
private var download: DownloadManager? = null
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
@Synchronized
@UnstableApi
fun downloadManager(context: Context): DownloadManager {
return download ?: let {
val database = Injekt.get<StandaloneDatabaseProvider>()
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val dataSourceFactory = DataSource.Factory {
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
val networkHelper = Injekt.get<NetworkHelper>()
val okHttpClient = networkHelper.client
val dataSource: HttpDataSource =
OkHttpDataSource.Factory(okHttpClient).createDataSource()
defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
dataSource
}
val threadPoolSize = Runtime.getRuntime().availableProcessors()
val executorService = Executors.newFixedThreadPool(threadPoolSize)
val downloadManager = DownloadManager(
context,
database,
getSimpleCache(context),
dataSourceFactory,
executorService
).apply {
requirements =
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
maxParallelDownloads = 3
}
downloadManager.addListener( //for testing
object : DownloadManager.Listener {
override fun onDownloadChanged(
downloadManager: DownloadManager,
download: Download,
finalException: Exception?
) {
if (download.state == Download.STATE_COMPLETED) {
Logger.log("Download Completed")
} else if (download.state == Download.STATE_FAILED) {
Logger.log("Download Failed")
} else if (download.state == Download.STATE_STOPPED) {
Logger.log("Download Stopped")
} else if (download.state == Download.STATE_QUEUED) {
Logger.log("Download Queued")
} else if (download.state == Download.STATE_DOWNLOADING) {
Logger.log("Download Downloading")
}
}
}
)
downloadManager
}
}
private var downloadDirectory: File? = null
@Synchronized
private fun getDownloadDirectory(context: Context): File {
if (downloadDirectory == null) {
downloadDirectory = context.getExternalFilesDir(null)
if (downloadDirectory == null) {
downloadDirectory = context.filesDir
}
}
return downloadDirectory!!
}
@OptIn(UnstableApi::class)
fun startAnimeDownloadService(
context: Context,
@@ -84,13 +220,22 @@ object Helper {
val downloadsManger = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManger
.queryDownload(title, episode, MediaType.ANIME)
.queryDownload(title, episode, DownloadedType.Type.ANIME)
if (downloadCheck) {
AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Exists")
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
.setPositiveButton("Yes") { _, _ ->
DownloadService.sendRemoveDownload(
context,
ExoplayerDownloadService::class.java,
PrefManager.getAnimeDownloadPreferences().getString(
animeDownloadTask.getTaskName(),
""
) ?: "",
false
)
PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName())
.apply()
@@ -98,15 +243,14 @@ object Helper {
DownloadedType(
title,
episode,
MediaType.ANIME
DownloadedType.Type.ANIME
)
) {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
)
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
.setNegativeButton("No") { _, _ -> }
@@ -121,6 +265,18 @@ object Helper {
}
}
@OptIn(UnstableApi::class)
fun getSimpleCache(context: Context): SimpleCache {
return if (simpleCache == null) {
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val database = Injekt.get<StandaloneDatabaseProvider>()
simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
simpleCache!!
} else {
simpleCache!!
}
}
private fun isNotificationPermissionGranted(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return ActivityCompat.checkSelfPermission(

View File

@@ -207,21 +207,6 @@ class AnimeFragment : Fragment() {
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()))
}
}
model.getMovies().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateMovies(MediaAdaptor(0, it, requireActivity()))
}
}
model.getTopRated().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
}
}
model.getMostFav().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
}
}
if (animePageAdapter.trendingViewPager != null) {
animePageAdapter.updateHeight()
model.getTrending().observe(viewLifecycleOwner) {
@@ -278,7 +263,7 @@ class AnimeFragment : Fragment() {
}
model.loaded = true
model.loadTrending(1)
model.loadAll()
model.loadUpdated()
model.loadPopular(
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularAnimeList

View File

@@ -4,14 +4,12 @@ import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.LayoutAnimationController
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -23,7 +21,6 @@ import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemAnimePageBinding
import ani.dantotsu.databinding.LayoutTrendingBinding
import ani.dantotsu.loadImage
import ani.dantotsu.media.CalendarActivity
import ani.dantotsu.media.GenreActivity
@@ -44,7 +41,6 @@ import com.google.android.material.textfield.TextInputLayout
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
val ready = MutableLiveData(false)
lateinit var binding: ItemAnimePageBinding
private lateinit var trendingBinding: LayoutTrendingBinding
private var trendHandler: Handler? = null
private lateinit var trendRun: Runnable
var trendingViewPager: ViewPager2? = null
@@ -57,15 +53,14 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
override fun onBindViewHolder(holder: AnimePageViewHolder, position: Int) {
binding = holder.binding
trendingBinding = LayoutTrendingBinding.bind(binding.root)
trendingViewPager = trendingBinding.trendingViewPager
trendingViewPager = binding.animeTrendingViewPager
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
@@ -74,16 +69,16 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
if (PrefManager.getVal(PrefName.SmallView)) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = (-108f).px
}
updateAvatar()
trendingBinding.searchBar.hint = "ANIME"
trendingBinding.searchBarText.setOnClickListener {
binding.animeSearchBar.hint = "ANIME"
binding.animeSearchBarText.setOnClickListener {
ContextCompat.startActivity(
it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
@@ -91,13 +86,16 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
)
}
trendingBinding.userAvatar.setSafeOnClickListener {
binding.animeSearchBar.setEndIconOnClickListener {
binding.animeSearchBarText.performClick()
}
binding.animeUserAvatar.setSafeOnClickListener {
val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
trendingBinding.userAvatar.setOnLongClickListener { view ->
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
binding.animeUserAvatar.setOnLongClickListener { view ->
ContextCompat.startActivity(
view.context,
Intent(view.context, ProfileActivity::class.java)
@@ -106,12 +104,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
false
}
trendingBinding.searchBar.setEndIconOnClickListener {
trendingBinding.searchBar.performClick()
}
trendingBinding.notificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
binding.animeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.animeNotificationCount.text = Anilist.unreadNotificationCount.toString()
listOf(
binding.animePreviousSeason,
@@ -140,7 +134,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
)
}
binding.animeIncludeList.isVisible = Anilist.userid != null
binding.animeIncludeList.visibility =
if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
@@ -164,30 +159,30 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
}
fun updateTrending(adaptor: MediaAdaptor) {
trendingBinding.trendingProgressBar.visibility = View.GONE
trendingBinding.trendingViewPager.adapter = adaptor
trendingBinding.trendingViewPager.offscreenPageLimit = 3
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
binding.animeTrendingProgressBar.visibility = View.GONE
binding.animeTrendingViewPager.adapter = adaptor
binding.animeTrendingViewPager.offscreenPageLimit = 3
binding.animeTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
binding.animeTrendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable {
trendingBinding.trendingViewPager.currentItem += 1
binding.animeTrendingViewPager.currentItem =
binding.animeTrendingViewPager.currentItem + 1
}
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
binding.animeTrendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
trendHandler?.removeCallbacks(trendRun)
if (PrefManager.getVal(PrefName.TrendingScroller)) {
trendHandler!!.postDelayed(trendRun, 4000)
}
trendHandler!!.removeCallbacks(trendRun)
trendHandler!!.postDelayed(trendRun, 4000)
}
}
)
trendingBinding.trendingViewPager.layoutAnimation =
binding.animeTrendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
trendingBinding.titleContainer.startAnimation(setSlideUp())
binding.animeTitleContainer.startAnimation(setSlideUp())
binding.animeListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.animeSeasonsCont.layoutAnimation =
@@ -195,79 +190,36 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
}
fun updateRecent(adaptor: MediaAdaptor) {
binding.apply{
init(
adaptor,
animeUpdatedRecyclerView,
animeUpdatedProgressBar,
animeRecently
)
animePopular.visibility = View.VISIBLE
animePopular.startAnimation(setSlideUp())
if (adaptor.itemCount == 0) {
animeRecentlyContainer.visibility = View.GONE
}
}
}
fun updateMovies(adaptor: MediaAdaptor) {
binding.apply{
init(
adaptor,
animeMoviesRecyclerView,
animeMoviesProgressBar,
animeMovies
)
}
}
fun updateTopRated(adaptor: MediaAdaptor) {
binding.apply{
init(
adaptor,
animeTopRatedRecyclerView,
animeTopRatedProgressBar,
animeTopRated
)
}
}
fun updateMostFav(adaptor: MediaAdaptor) {
binding.apply{
init(
adaptor,
animeMostFavRecyclerView,
animeMostFavProgressBar,
animeMostFav
)
}
}
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =
binding.animeUpdatedProgressBar.visibility = View.GONE
binding.animeUpdatedRecyclerView.adapter = adaptor
binding.animeUpdatedRecyclerView.layoutManager =
LinearLayoutManager(
recyclerView.context,
binding.animeUpdatedRecyclerView.context,
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
title.startAnimation(setSlideUp())
recyclerView.layoutAnimation =
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
binding.animeRecently.visibility = View.VISIBLE
binding.animeRecently.startAnimation(setSlideUp())
binding.animeUpdatedRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.animePopular.visibility = View.VISIBLE
binding.animePopular.startAnimation(setSlideUp())
}
fun updateAvatar() {
if (Anilist.avatar != null && ready.value == true) {
trendingBinding.userAvatar.loadImage(Anilist.avatar)
trendingBinding.userAvatar.imageTintList = null
binding.animeUserAvatar.loadImage(Anilist.avatar)
binding.animeUserAvatar.imageTintList = null
}
}
fun updateNotificationCount() {
if (this::binding.isInitialized) {
trendingBinding.notificationCount.visibility =
binding.animeNotificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
binding.animeNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
}

View File

@@ -5,13 +5,11 @@ import android.content.Intent
import android.graphics.drawable.Animatable
import android.os.Build
import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.LayoutAnimationController
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
@@ -80,10 +78,10 @@ class HomeFragment : Fragment() {
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
binding.homeUserAvatar.loadImage(Anilist.avatar)
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage(if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen, Anilist.bg)
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
blurImage(binding.homeUserBg, Anilist.bg)
binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
binding.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener {
@@ -125,7 +123,6 @@ class HomeFragment : Fragment() {
)
}
binding.homeUserAvatarContainer.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity(
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
.putExtra("userId", Anilist.userid),null
@@ -137,7 +134,6 @@ class HomeFragment : Fragment() {
bottomMargin = navBarHeight
}
binding.homeUserBg.updateLayoutParams { height += statusBarHeight }
binding.homeUserBgNoKen.updateLayoutParams { height += statusBarHeight }
binding.homeTopContainer.updatePadding(top = statusBarHeight)
var reached = false
@@ -379,7 +375,7 @@ class HomeFragment : Fragment() {
override fun onResume() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) {
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
binding.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
super.onResume()

View File

@@ -160,31 +160,11 @@ class MangaFragment : Fragment() {
})
mangaPageAdapter.ready.observe(viewLifecycleOwner) { i ->
if (i == true) {
model.getPopularNovel().observe(viewLifecycleOwner) {
model.getTrendingNovel().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()))
}
}
model.getPopularManga().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()))
}
}
model.getPopularManhwa().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTrendingManhwa(MediaAdaptor(0, it, requireActivity()))
}
}
model.getTopRated().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
}
}
model.getMostFav().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
}
}
if (mangaPageAdapter.trendingViewPager != null) {
mangaPageAdapter.updateHeight()
model.getTrending().observe(viewLifecycleOwner) {
@@ -257,7 +237,7 @@ class MangaFragment : Fragment() {
}
model.loaded = true
model.loadTrending()
model.loadAll()
model.loadTrendingNovel()
model.loadPopular(
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularMangaList

View File

@@ -4,14 +4,12 @@ import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.LayoutAnimationController
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -23,7 +21,6 @@ import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemMangaPageBinding
import ani.dantotsu.databinding.LayoutTrendingBinding
import ani.dantotsu.loadImage
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.MediaAdaptor
@@ -43,7 +40,6 @@ import com.google.android.material.textfield.TextInputLayout
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
val ready = MutableLiveData(false)
lateinit var binding: ItemMangaPageBinding
private lateinit var trendingBinding: LayoutTrendingBinding
private var trendHandler: Handler? = null
private lateinit var trendRun: Runnable
var trendingViewPager: ViewPager2? = null
@@ -56,34 +52,33 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
override fun onBindViewHolder(holder: MangaPageViewHolder, position: Int) {
binding = holder.binding
trendingBinding = LayoutTrendingBinding.bind(binding.root)
trendingViewPager = trendingBinding.trendingViewPager
trendingViewPager = binding.mangaTrendingViewPager
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
if (PrefManager.getVal(PrefName.SmallView)) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = (-108f).px
}
updateAvatar()
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
trendingBinding.searchBar.hint = "MANGA"
trendingBinding.searchBarText.setOnClickListener {
binding.mangaNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.mangaSearchBar.hint = "MANGA"
binding.mangaSearchBarText.setOnClickListener {
ContextCompat.startActivity(
it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"),
@@ -91,13 +86,12 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
)
}
trendingBinding.userAvatar.setSafeOnClickListener {
binding.mangaUserAvatar.setSafeOnClickListener {
val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
trendingBinding.userAvatar.setOnLongClickListener { view ->
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
binding.mangaUserAvatar.setOnLongClickListener { view ->
ContextCompat.startActivity(
view.context,
Intent(view.context, ProfileActivity::class.java)
@@ -106,8 +100,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
false
}
trendingBinding.searchBar.setEndIconOnClickListener {
trendingBinding.searchBarText.performClick()
binding.mangaSearchBar.setEndIconOnClickListener {
binding.mangaSearchBarText.performClick()
}
binding.mangaGenreImage.loadImage("https://s4.anilist.co/file/anilistcdn/media/manga/banner/105778-wk5qQ7zAaTGl.jpg")
@@ -131,7 +125,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
)
}
binding.mangaIncludeList.isVisible = Anilist.userid != null
binding.mangaIncludeList.visibility =
if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
@@ -153,115 +148,63 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
}
fun updateTrending(adaptor: MediaAdaptor) {
trendingBinding.trendingProgressBar.visibility = View.GONE
trendingBinding.trendingViewPager.adapter = adaptor
trendingBinding.trendingViewPager.offscreenPageLimit = 3
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
binding.mangaTrendingProgressBar.visibility = View.GONE
binding.mangaTrendingViewPager.adapter = adaptor
binding.mangaTrendingViewPager.offscreenPageLimit = 3
binding.mangaTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable {
trendingBinding.trendingViewPager.currentItem += 1
binding.mangaTrendingViewPager.currentItem += 1
}
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
trendHandler?.removeCallbacks(trendRun)
if (PrefManager.getVal(PrefName.TrendingScroller))
trendHandler!!.postDelayed(trendRun, 4000)
trendHandler!!.removeCallbacks(trendRun)
trendHandler!!.postDelayed(trendRun, 4000)
}
}
)
trendingBinding.trendingViewPager.layoutAnimation =
binding.mangaTrendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
trendingBinding.titleContainer.startAnimation(setSlideUp())
binding.mangaTitleContainer.startAnimation(setSlideUp())
binding.mangaListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
}
fun updateTrendingManga(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTrendingMangaRecyclerView,
mangaTrendingMangaProgressBar,
mangaTrendingManga
)
}
}
fun updateTrendingManhwa(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTrendingManhwaRecyclerView,
mangaTrendingManhwaProgressBar,
mangaTrendingManhwa
)
}
}
fun updateNovel(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaNovelRecyclerView,
mangaNovelProgressBar,
mangaNovel
)
}
}
fun updateTopRated(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTopRatedRecyclerView,
mangaTopRatedProgressBar,
mangaTopRated
)
}
}
fun updateMostFav(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaMostFavRecyclerView,
mangaMostFavProgressBar,
mangaMostFav
)
mangaPopular.visibility = View.VISIBLE
mangaPopular.startAnimation(setSlideUp())
}
}
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =
binding.mangaNovelProgressBar.visibility = View.GONE
binding.mangaNovelRecyclerView.adapter = adaptor
binding.mangaNovelRecyclerView.layoutManager =
LinearLayoutManager(
recyclerView.context,
binding.mangaNovelRecyclerView.context,
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
title.startAnimation(setSlideUp())
recyclerView.layoutAnimation =
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
binding.mangaNovel.visibility = View.VISIBLE
binding.mangaNovel.startAnimation(setSlideUp())
binding.mangaNovelRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.mangaPopular.visibility = View.VISIBLE
binding.mangaPopular.startAnimation(setSlideUp())
}
fun updateAvatar() {
if (Anilist.avatar != null && ready.value == true) {
trendingBinding.userAvatar.loadImage(Anilist.avatar)
trendingBinding.userAvatar.imageTintList = null
binding.mangaUserAvatar.loadImage(Anilist.avatar)
binding.mangaUserAvatar.imageTintList = null
}
}
fun updateNotificationCount() {
if (this::binding.isInitialized) {
trendingBinding.notificationCount.visibility =
binding.mangaNotificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
}

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater
@@ -23,6 +24,7 @@ class AuthorAdapter(
return AuthorViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder:AuthorViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root)

View File

@@ -1,10 +1,12 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
@@ -14,8 +16,8 @@ import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.hideSystemBarsExtendView
import ani.dantotsu.media.user.ListViewPagerAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
@@ -32,6 +34,7 @@ class CalendarActivity : AppCompatActivity() {
private var selectedTabIdx = 1
private val model: OtherDetailsViewModel by viewModels()
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -42,6 +45,13 @@ class CalendarActivity : AppCompatActivity() {
val typedValue = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
val primaryColor = typedValue.data
val typedValue2 = TypedValue()
theme.resolveAttribute(
com.google.android.material.R.attr.colorOnBackground,
typedValue2,
true
)
val titleTextColor = typedValue2.data
val typedValue3 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
val primaryTextColor = typedValue3.data
@@ -64,7 +74,10 @@ class CalendarActivity : AppCompatActivity() {
} else {
binding.root.fitsSystemWindows = false
requestWindowFeature(Window.FEATURE_NO_TITLE)
hideSystemBarsExtendView()
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater
@@ -23,12 +24,12 @@ class CharacterAdapter(
return CharacterViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root)
val character = characterList[position]
val whitespace = "${character.role} "
binding.itemCompactRelation.text = whitespace
binding.itemCompactRelation.text = character.role + " "
binding.itemCompactImage.loadImage(character.image)
binding.itemCompactTitle.text = character.name
}

View File

@@ -8,7 +8,6 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.isGone
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -153,7 +152,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
}
override fun onResume() {
binding.characterProgress.isGone = loaded
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
super.onResume()
}

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity
import android.view.LayoutInflater
import android.view.ViewGroup
@@ -19,16 +20,15 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
return GenreViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
val binding = holder.binding
val desc =
(if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") +
(if (character.dateOfBirth.toString() != "")
"${currActivity()!!.getString(R.string.birthday)} ${character.dateOfBirth.toString()}" else "") +
(if (character.gender != "null")
currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
currActivity()!!.getString(R.string.male) -> currActivity()!!.getString(R.string.male)
currActivity()!!.getString(R.string.female) -> currActivity()!!.getString(R.string.female)
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
"Male" -> currActivity()!!.getString(R.string.male)
"Female" -> currActivity()!!.getString(R.string.female)
else -> character.gender
} else "") + "\n" + character.description

View File

@@ -25,7 +25,7 @@ data class Media(
var cover: String? = null,
var banner: String? = null,
var relation: String? = null,
var favourites: Int? = null,
var popularity: Int? = null,
var isAdult: Boolean,
var isFav: Boolean = false,
@@ -56,9 +56,6 @@ data class Media(
var trailer: String? = null,
var startDate: FuzzyDate? = null,
var endDate: FuzzyDate? = null,
var popularity: Int? = null,
var timeUntilAiring: Long? = null,
var characters: ArrayList<Character>? = null,
var staff: ArrayList<Author>? = null,
@@ -86,7 +83,7 @@ data class Media(
name = apiMedia.title!!.english,
nameRomaji = apiMedia.title!!.romaji,
userPreferredName = apiMedia.title!!.userPreferred,
cover = apiMedia.coverImage?.large ?: apiMedia.coverImage?.medium,
cover = apiMedia.coverImage?.large,
banner = apiMedia.bannerImage,
status = apiMedia.status.toString(),
isFav = apiMedia.isFavourite!!,
@@ -98,8 +95,6 @@ data class Media(
meanScore = apiMedia.meanScore,
startDate = apiMedia.startDate,
endDate = apiMedia.endDate,
favourites = apiMedia.favourites,
timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000},
anime = if (apiMedia.type == MediaType.ANIME) Anime(
totalEpisodes = apiMedia.episodes,
nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1)

View File

@@ -1,6 +1,8 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
@@ -13,25 +15,25 @@ import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.blurImage
import ani.dantotsu.currActivity
import ani.dantotsu.*
import ani.dantotsu.databinding.ItemMediaCompactBinding
import ani.dantotsu.databinding.ItemMediaLargeBinding
import ani.dantotsu.databinding.ItemMediaPageBinding
import ani.dantotsu.databinding.ItemMediaPageSmallBinding
import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.request.RequestOptions
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import jp.wasabeef.glide.transformations.BlurTransformation
import java.io.Serializable
@@ -83,7 +85,7 @@ class MediaAdaptor(
}
@SuppressLint("ClickableViewAccessibility")
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (type) {
0 -> {
@@ -92,8 +94,8 @@ class MediaAdaptor(
val media = mediaList?.getOrNull(position)
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore
@@ -138,8 +140,8 @@ class MediaAdaptor(
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore
@@ -149,22 +151,25 @@ class MediaAdaptor(
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
)
if (media.anime != null) {
val itemTotal = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(R.string.episode_singular)
b.itemTotal.text = itemTotal
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
?: 0) != 1
) currActivity()!!.getString(R.string.episode_plural)
else currActivity()!!.getString(R.string.episode_singular)
b.itemCompactTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
?: "??").toString()) else (media.anime.totalEpisodes
?: "??").toString()
} else if (media.manga != null) {
val itemTotal = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(R.string.chapter_singular)
b.itemTotal.text = itemTotal
b.itemTotal.text = " " + if ((media.manga.totalChapters
?: 0) != 1
) currActivity()!!.getString(R.string.chapter_plural)
else currActivity()!!.getString(R.string.chapter_singular)
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
}
@SuppressLint("NotifyDataSetChanged")
if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post {
val start = mediaList.size
mediaList.addAll(mediaList)
val end = mediaList.size - start
notifyItemRangeInserted(start, end)
notifyDataSetChanged()
}
}
}
@@ -173,7 +178,6 @@ class MediaAdaptor(
val b = (holder as MediaPageViewHolder).binding
val media = mediaList?.get(position)
if (media != null) {
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
b.itemCompactImage.loadImage(media.cover)
if (bannerAnimations)
@@ -183,9 +187,9 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator()
)
)
blurImage(if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen , media.banner ?: media.cover)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore
@@ -232,9 +236,9 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator()
)
)
blurImage(if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen , media.banner ?: media.cover)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore

View File

@@ -2,8 +2,9 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Rect
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.util.TypedValue
@@ -11,19 +12,18 @@ import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.color
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
@@ -54,7 +54,6 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.LauncherWrapper
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.CoroutineScope
@@ -63,21 +62,20 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
import kotlin.math.abs
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
lateinit var launcher: LauncherWrapper
lateinit var binding: ActivityMediaBinding
private val scope = lifecycleScope
private val model: MediaDetailsViewModel by viewModels()
lateinit var tabLayout: TripleNavAdapter
var selected = 0
lateinit var navBar: AnimatedBottomBar
var anime = true
private var adult = false
@SuppressLint("ClickableViewAccessibility")
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
@@ -85,7 +83,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
if (id != -1) {
runBlocking {
withContext(Dispatchers.IO) {
media = Anilist.query.getMedia(id, false) ?: emptyMedia()
media =
Anilist.query.getMedia(id, false) ?: emptyMedia()
}
}
}
@@ -94,9 +93,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
onBackPressedDispatcher.onBackPressed()
return
}
val contract = ActivityResultContracts.OpenDocumentTree()
launcher = LauncherWrapper(this, contract)
mediaSingleton = null
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
MediaSingleton.bitmap = null
@@ -104,34 +100,26 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat()
navBar = binding.mediaBottomBar
// Ui init
val isVertical = resources.configuration.orientation
//Ui init
initActivity(this)
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin = navBarHeight }
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val oldMargin = binding.mediaViewPager.marginBottom
AndroidBug5497Workaround.assistActivity(this) {
if (it) {
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = 0
}
navBar.visibility = View.GONE
binding.mediaTabContainer.visibility = View.GONE
} else {
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = oldMargin
}
navBar.visibility = View.VISIBLE
binding.mediaTabContainer.visibility = View.VISIBLE
}
}
val navBarRightMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE) navBarHeight else 0
val navBarBottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = navBarRightMargin
bottomMargin = navBarBottomMargin
}
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
@@ -159,6 +147,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
val banner =
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
val viewPager = binding.mediaViewPager
//tabLayout = binding.mediaTab as AnimatedBottomBar
viewPager.isUserInputEnabled = false
viewPager.setPageTransformer(ZoomOutPageTransformer())
@@ -168,10 +157,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaCoverImage.loadImage(media.cover)
binding.mediaCoverImage.setOnLongClickListener {
val coverTitle = "${media.userPreferredName}[Cover]"
ImageViewDialog.newInstance(
this,
coverTitle,
media.userPreferredName + "[Cover]",
media.cover
)
}
@@ -188,10 +176,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
override fun onLongClick(event: MotionEvent) {
val bannerTitle = "${media.userPreferredName}[Banner]"
ImageViewDialog.newInstance(
this@MediaDetailsActivity,
bannerTitle,
media.userPreferredName + "[Banner]",
media.banner ?: media.cover
)
banner.performClick()
@@ -199,8 +186,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
})
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
if (PrefManager.getVal(PrefName.Incognito)) {
val mediaTitle = " ${media.userPreferredName}"
binding.mediaTitle.text = mediaTitle
binding.mediaTitle.text = " ${media.userPreferredName}"
binding.incognito.visibility = View.VISIBLE
} else {
binding.mediaTitle.text = media.userPreferredName
@@ -224,6 +210,20 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
R.drawable.ic_round_favorite_24
)
)
val typedValue = TypedValue()
this.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue,
true
)
val color = typedValue.data
val typedValue2 = TypedValue()
this.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue2,
true
)
val color2 = typedValue.data
PopImageButton(
scope,
@@ -231,7 +231,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
R.drawable.ic_round_favorite_24,
R.drawable.ic_round_favorite_border_24,
R.color.bg_opp,
R.color.violet_400,
R.color.violet_400,//TODO: Change to colorSecondary
media.isFav
) {
media.isFav = it
@@ -246,13 +246,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("ResourceType")
fun total() {
val text = SpannableStringBuilder().apply {
val mediaTypedValue = TypedValue()
val typedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorOnBackground,
mediaTypedValue,
typedValue,
true
)
val white = mediaTypedValue.data
val white = typedValue.data
if (media.userStatus != null) {
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
val typedValue = TypedValue()
@@ -342,6 +342,14 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
progress()
}
}
tabLayout = TripleNavAdapter(
binding.mediaTab1,
binding.mediaTab2,
binding.mediaTab3,
media.anime != null,
media.format ?: "",
isVertical == 1
)
adult = media.isAdult
if (media.anime != null) {
viewPager.adapter =
@@ -357,46 +365,31 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
anime = false
}
selected = media.selected!!.window
binding.mediaTitle.translationX = -screenWidth
val infoTab = navBar.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
val watchTab = if (anime) {
navBar.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
} else if (media.format == "NOVEL") {
navBar.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
} else {
navBar.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
tabLayout.selectionListener = { selected, newId ->
binding.commentInputLayout.visibility = if (selected == 2) View.VISIBLE else View.GONE
this.selected = selected
selectFromID(newId)
viewPager.setCurrentItem(selected, false)
val sel = model.loadSelected(media, isDownload)
sel.window = selected
model.saveSelected(media.id, sel)
}
val commentTab = navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
navBar.addTab(infoTab)
navBar.addTab(watchTab)
navBar.addTab(commentTab)
tabLayout.selectTab(selected)
selectFromID(tabLayout.selected)
viewPager.setCurrentItem(selected, false)
if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
selected = 1
}
if (intent.getStringExtra("FRAGMENT_TO_LOAD") != null) selected = 2
if (viewPager.currentItem != selected) viewPager.post {
viewPager.setCurrentItem(selected, false)
val frag = intent.getStringExtra("FRAGMENT_TO_LOAD")
if (frag != null) {
selected = 2
}
binding.commentInputLayout.isVisible = selected == 2
navBar.selectTabAt(selected)
navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
selected = newIndex
binding.commentInputLayout.isVisible = selected == 2
viewPager.setCurrentItem(selected, true)
val sel = model.loadSelected(media, isDownload)
sel.window = selected
model.saveSelected(media.id, sel)
}
})
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) {
@@ -409,19 +402,40 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val rightMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE) navBarHeight else 0
val bottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight
val params : ViewGroup.MarginLayoutParams =
navBar.layoutParams as ViewGroup.MarginLayoutParams
params.updateMargins(right = rightMargin, bottom = bottomMargin)
private fun selectFromID(id: Int) {
when (id) {
R.id.info -> {
selected = 0
}
R.id.watch, R.id.read -> {
selected = 1
}
R.id.comment -> {
selected = 2
}
}
}
private fun idFromSelect(): Int {
if (anime) when (selected) {
0 -> return R.id.info
1 -> return R.id.watch
2 -> return R.id.comment
}
else when (selected) {
0 -> return R.id.info
1 -> return R.id.read
2 -> return R.id.comment
}
return R.id.info
}
override fun onResume() {
navBar.selectTabAt(selected)
if (this::tabLayout.isInitialized) {
tabLayout.selectTab(selected)
}
super.onResume()
}
@@ -429,7 +443,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ANIME, MANGA, NOVEL
}
// ViewPager
//ViewPager
private class ViewPagerAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle,
@@ -475,6 +489,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaCover.visibility =
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
val typedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue,
true
)
val color = typedValue.data
if (percentage >= percent && !isCollapsed) {
isCollapsed = true
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)

View File

@@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.util.Logger
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter
@@ -28,7 +29,6 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
@@ -52,21 +52,26 @@ class MediaDetailsViewModel : ViewModel() {
it
}
if (isDownload) {
data.sourceIndex = when {
media.anime != null -> {
AnimeSources.list.size - 1
}
media.format == "MANGA" || media.format == "ONE_SHOT" -> {
MangaSources.list.size - 1
}
else -> {
NovelSources.list.size - 1
}
data.sourceIndex = if (media.anime != null) {
AnimeSources.list.size - 1
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
MangaSources.list.size - 1
} else {
NovelSources.list.size - 1
}
}
return data
}
fun loadSelectedStringLocation(sourceName: String): Int {
//find the location of the source in the list
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
if (location == -1) {
location = 0
}
return location
}
var continueMedia: Boolean? = null
private var loading = false
@@ -147,10 +152,10 @@ class MediaDetailsViewModel : ViewModel() {
watchSources?.get(i)?.apply {
if (!post && !allowsPreloading) return@apply
ep.sEpisode?.let {
loadByVideoServers(link, ep.extra, it) { extractor ->
if (extractor.videos.isNotEmpty()) {
list.add(extractor)
ep.extractorCallback?.invoke(extractor)
loadByVideoServers(link, ep.extra, it) {
if (it.videos.isNotEmpty()) {
list.add(it)
ep.extractorCallback?.invoke(it)
}
}
}
@@ -286,6 +291,7 @@ class MediaDetailsViewModel : ViewModel() {
suspend fun loadMangaChapterImages(
chapter: MangaChapter,
selected: Selected,
series: String,
post: Boolean = true
): Boolean {

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
@@ -15,33 +16,16 @@ import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.GenresViewModel
import ani.dantotsu.copyToClipboard
import ani.dantotsu.countDown
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.ActivityGenreBinding
import ani.dantotsu.databinding.FragmentMediaInfoBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemQuelsBinding
import ani.dantotsu.databinding.ItemTitleChipgroupBinding
import ani.dantotsu.databinding.ItemTitleRecyclerBinding
import ani.dantotsu.databinding.ItemTitleSearchBinding
import ani.dantotsu.databinding.ItemTitleTextBinding
import ani.dantotsu.databinding.ItemTitleTrailerBinding
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.databinding.*
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import io.noties.markwon.Markwon
@@ -53,6 +37,7 @@ import java.io.Serializable
import java.net.URLEncoder
@SuppressLint("SetTextI18n")
class MediaInfoFragment : Fragment() {
private var _binding: FragmentMediaInfoBinding? = null
private val binding get() = _binding!!
@@ -61,8 +46,6 @@ class MediaInfoFragment : Fragment() {
private var type = "ANIME"
private val genreModel: GenresViewModel by activityViewModels()
private val tripleTab = "\t\t\t"
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -80,8 +63,8 @@ class MediaInfoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels()
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
binding.mediaInfoProgressBar.isGone = loaded
binding.mediaInfoContainer.isVisible = loaded
binding.mediaInfoProgressBar.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 }
model.scrolledToTop.observe(viewLifecycleOwner) {
@@ -92,18 +75,17 @@ class MediaInfoFragment : Fragment() {
if (media != null && !loaded) {
loaded = true
binding.mediaInfoProgressBar.visibility = View.GONE
binding.mediaInfoContainer.visibility = View.VISIBLE
val infoName = tripleTab + (media.name ?: media.nameRomaji)
binding.mediaInfoName.text = infoName
binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
binding.mediaInfoName.setOnLongClickListener {
copyToClipboard(media.name ?: media.nameRomaji)
true
}
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
View.VISIBLE
val infoNameRomanji = tripleTab + media.nameRomaji
binding.mediaInfoNameRomaji.text = infoNameRomanji
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
binding.mediaInfoNameRomaji.setOnLongClickListener {
copyToClipboard(media.nameRomaji)
true
@@ -115,8 +97,6 @@ class MediaInfoFragment : Fragment() {
binding.mediaInfoSource.text = media.source
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
binding.mediaInfoPopularity.text = media.popularity.toString()
binding.mediaInfoFavorites.text = media.favourites.toString()
if (media.anime != null) {
val episodeDuration = media.anime.episodeDuration
@@ -145,9 +125,8 @@ class MediaInfoFragment : Fragment() {
}
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
val seasonInfo = "${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}"
binding.mediaInfoSeason.text = seasonInfo
binding.mediaInfoSeason.text =
(media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??")
if (media.anime.mainStudio != null) {
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
@@ -181,12 +160,9 @@ class MediaInfoFragment : Fragment() {
}
}
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
val infoTotal = if (media.anime.nextAiringEpisode != null)
"${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}"
else
(media.anime.totalEpisodes ?: "~").toString()
binding.mediaInfoTotal.text = infoTotal
binding.mediaInfoTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes
?: "~").toString()) else (media.anime.totalEpisodes ?: "~").toString()
} else if (media.manga != null) {
type = "MANGA"
binding.mediaInfoTotalTitle.setText(R.string.total_chaps)
@@ -213,9 +189,8 @@ class MediaInfoFragment : Fragment() {
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
val infoDesc = tripleTab + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
binding.mediaInfoDescription.text = infoDesc
binding.mediaInfoDescription.text =
"\t\t\t" + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
binding.mediaInfoDescription.setOnClickListener {
if (binding.mediaInfoDescription.maxLines == 5) {
ObjectAnimator.ofInt(binding.mediaInfoDescription, "maxLines", 100)
@@ -438,138 +413,113 @@ class MediaInfoFragment : Fragment() {
if (!media.relations.isNullOrEmpty() && !offline) {
if (media.sequel != null || media.prequel != null) {
ItemQuelsBinding.inflate(
val bind = ItemQuelsBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
)
if (media.sequel != null) {
mediaInfoSequel.visibility = View.VISIBLE
mediaInfoSequelImage.loadImage(
media.sequel!!.banner ?: media.sequel!!.cover
)
mediaInfoSequel.setSafeOnClickListener {
ContextCompat.startActivity(
if (media.sequel != null) {
bind.mediaInfoSequel.visibility = View.VISIBLE
bind.mediaInfoSequelImage.loadImage(
media.sequel!!.banner ?: media.sequel!!.cover
)
bind.mediaInfoSequel.setSafeOnClickListener {
ContextCompat.startActivity(
requireContext(),
Intent(
requireContext(),
Intent(
requireContext(),
MediaDetailsActivity::class.java
).putExtra(
"media",
media.sequel as Serializable
), null
)
}
}
if (media.prequel != null) {
mediaInfoPrequel.visibility = View.VISIBLE
mediaInfoPrequelImage.loadImage(
media.prequel!!.banner ?: media.prequel!!.cover
MediaDetailsActivity::class.java
).putExtra(
"media",
media.sequel as Serializable
), null
)
mediaInfoPrequel.setSafeOnClickListener {
ContextCompat.startActivity(
}
}
if (media.prequel != null) {
bind.mediaInfoPrequel.visibility = View.VISIBLE
bind.mediaInfoPrequelImage.loadImage(
media.prequel!!.banner ?: media.prequel!!.cover
)
bind.mediaInfoPrequel.setSafeOnClickListener {
ContextCompat.startActivity(
requireContext(),
Intent(
requireContext(),
Intent(
requireContext(),
MediaDetailsActivity::class.java
).putExtra(
"media",
media.prequel as Serializable
), null
)
}
MediaDetailsActivity::class.java
).putExtra(
"media",
media.prequel as Serializable
), null
)
}
parent.addView(root)
}
ItemTitleSearchBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
titleSearchImage.loadImage(media.banner ?: media.cover)
titleSearchText.text =
getString(R.string.search_title, media.mainName())
titleSearchCard.setSafeOnClickListener {
val query = Intent(requireContext(), SearchActivity::class.java)
.putExtra("type", "ANIME")
.putExtra("query", media.mainName())
.putExtra("search", true)
ContextCompat.startActivity(requireContext(), query, null)
}
parent.addView(root)
}
parent.addView(bind.root)
}
ItemTitleRecyclerBinding.inflate(
val bindi = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
)
itemRecycler.adapter =
MediaAdaptor(0, media.relations!!, requireActivity())
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
bindi.itemRecycler.adapter =
MediaAdaptor(0, media.relations!!, requireActivity())
bindi.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bindi.root)
}
if (!media.characters.isNullOrEmpty() && !offline) {
ItemTitleRecyclerBinding.inflate(
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
itemTitle.setText(R.string.characters)
itemRecycler.adapter =
CharacterAdapter(media.characters!!)
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
)
bind.itemTitle.setText(R.string.characters)
bind.itemRecycler.adapter =
CharacterAdapter(media.characters!!)
bind.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bind.root)
}
if (!media.staff.isNullOrEmpty() && !offline) {
ItemTitleRecyclerBinding.inflate(
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
itemTitle.setText(R.string.staff)
itemRecycler.adapter =
AuthorAdapter(media.staff!!)
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
)
bind.itemTitle.setText(R.string.staff)
bind.itemRecycler.adapter =
AuthorAdapter(media.staff!!)
bind.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bind.root)
}
if (!media.recommendations.isNullOrEmpty() && !offline) {
ItemTitleRecyclerBinding.inflate(
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
itemTitle.setText(R.string.recommended)
itemRecycler.adapter =
MediaAdaptor(0, media.recommendations!!, requireActivity())
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
)
bind.itemTitle.setText(R.string.recommended)
bind.itemRecycler.adapter =
MediaAdaptor(0, media.recommendations!!, requireActivity())
bind.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bind.root)
}
}
}
@@ -594,12 +544,11 @@ class MediaInfoFragment : Fragment() {
}
}
}
super.onViewCreated(view, null)
}
override fun onResume() {
binding.mediaInfoProgressBar.isGone = loaded
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
super.onResume()
}

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.InputFilter.LengthFilter
import android.view.Gravity
@@ -10,18 +11,11 @@ import android.widget.ArrayAdapter
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.DatePickerFragment
import ani.dantotsu.InputFilterMinMax
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListBinding
import ani.dantotsu.navBarHeight
import ani.dantotsu.snackString
import ani.dantotsu.tryWith
import com.google.android.material.materialswitch.MaterialSwitch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -42,6 +36,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
return binding.root
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
var media: Media?
@@ -173,10 +168,9 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
val init =
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
.toInt() else 0
if (init < (total ?: 5000)) {
val progressText = "${init + 1}"
binding.mediaListProgress.setText(progressText)
}
if (init < (total
?: 5000)
) binding.mediaListProgress.setText((init + 1).toString())
if (init + 1 == (total ?: 5000)) {
binding.mediaListStatus.setText(statusStrings[2], false)
onComplete()

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.InputFilter.LengthFilter
import android.view.Gravity
@@ -9,16 +10,11 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.InputFilterMinMax
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.getSerialized
import ani.dantotsu.snackString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -58,6 +54,7 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = viewLifecycleOwner.lifecycleScope
@@ -71,7 +68,7 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
MAL.query.deleteList(media.anime != null, media.idMAL)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
snackString(getString(R.string.delete_fail_reason, e.message))
snackString("Failed to delete because of... ${e.message}")
}
return@withContext
}
@@ -157,10 +154,7 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
val init =
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
.toInt() else 0
if (init < (total ?: 5000)) {
val progressText = "${init + 1}"
binding.mediaListProgress.setText(progressText)
}
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
if (init + 1 == (total ?: 5000)) {
binding.mediaListStatus.setText(statusStrings[2], false)
}

View File

@@ -1,146 +0,0 @@
package ani.dantotsu.media
import java.util.Locale
import java.util.regex.Matcher
import java.util.regex.Pattern
object MediaNameAdapter {
private const val REGEX_ITEM = "[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
private const val REGEX_PART_NUMBER = "(?<!part\\s)\\b(\\d+)\\b"
private const val REGEX_EPISODE =
"(episode|episodio|ep|e)${REGEX_ITEM}\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
private const val REGEX_SEASON = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
private const val REGEX_SUBDUB = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
private const val REGEX_CHAPTER = "(chapter|chap|ch|c)${REGEX_ITEM}"
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val soft = subdubMatcher.group(1)
val subdub = subdubMatcher.group(2)
val bed = subdubMatcher.group(3) ?: ""
val toggled = when (typeToSetTo) {
SubDubType.SUB -> "sub"
SubDubType.DUB -> "dub"
SubDubType.NULL -> ""
}
val toggledCasePreserved =
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
?.isUpperCase() == true
) toggled.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.ROOT
) else it.toString()
} else toggled
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
} else {
null
}
}
fun getSubDub(text: String): SubDubType {
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
when (subdub) {
"sub" -> SubDubType.SUB
"dub" -> SubDubType.DUB
else -> SubDubType.NULL
}
} else {
SubDubType.NULL
}
}
enum class SubDubType {
SUB, DUB, NULL
}
fun findSeasonNumber(text: String): Int? {
val seasonPattern: Pattern = Pattern.compile(REGEX_SEASON, Pattern.CASE_INSENSITIVE)
val seasonMatcher: Matcher = seasonPattern.matcher(text)
return if (seasonMatcher.find()) {
seasonMatcher.group(2)?.toInt()
} else {
null
}
}
fun findEpisodeNumber(text: String): Float? {
val episodePattern: Pattern = Pattern.compile(REGEX_EPISODE, Pattern.CASE_INSENSITIVE)
val episodeMatcher: Matcher = episodePattern.matcher(text)
return if (episodeMatcher.find()) {
if (episodeMatcher.group(2) != null) {
episodeMatcher.group(2)?.toFloat()
} else {
val failedEpisodeNumberPattern: Pattern =
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
val failedEpisodeNumberMatcher: Matcher =
failedEpisodeNumberPattern.matcher(text)
if (failedEpisodeNumberMatcher.find()) {
failedEpisodeNumberMatcher.group(1)?.toFloat()
} else {
null
}
}
} else {
null
}
}
fun removeEpisodeNumber(text: String): String {
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "").ifEmpty {
text
}
val letterPattern = Regex("[a-zA-Z]")
return if (letterPattern.containsMatchIn(removedNumber)) {
removedNumber
} else {
text
}
}
fun removeEpisodeNumberCompletely(text: String): String {
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "")
return if (removedNumber.equals(text, true)) { // if nothing was removed
val failedEpisodeNumberPattern =
Regex(REGEX_PART_NUMBER, RegexOption.IGNORE_CASE)
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
mr.value.replaceFirst(mr.groupValues[1], "")
}
} else {
removedNumber
}
}
fun findChapterNumber(text: String): Float? {
val pattern: Pattern = Pattern.compile(REGEX_CHAPTER, Pattern.CASE_INSENSITIVE)
val matcher: Matcher = pattern.matcher(text)
return if (matcher.find()) {
matcher.group(2)?.toFloat()
} else {
val failedChapterNumberPattern: Pattern =
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
val failedChapterNumberMatcher: Matcher =
failedChapterNumberPattern.matcher(text)
if (failedChapterNumberMatcher.find()) {
failedChapterNumberMatcher.group(1)?.toFloat()
} else {
null
}
}
}
}

View File

@@ -1,26 +0,0 @@
package ani.dantotsu.media
enum class MediaType {
ANIME,
MANGA,
NOVEL;
fun asText(): String {
return when (this) {
ANIME -> "Anime"
MANGA -> "Manga"
NOVEL -> "Novel"
}
}
companion object {
fun fromText(string : String): MediaType {
return when (string) {
"Anime" -> ANIME
"Manga" -> MANGA
"Novel" -> NOVEL
else -> { ANIME }
}
}
}
}

View File

@@ -30,7 +30,7 @@ class OtherDetailsViewModel : ViewModel() {
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
suspend fun loadCalendar() {
val curr = System.currentTimeMillis() / 1000
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
val df = DateFormat.getDateInstance(DateFormat.FULL)
val map = mutableMapOf<String, MutableList<Media>>()
val idMap = mutableMapOf<String, MutableList<Int>>()

View File

@@ -27,7 +27,7 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
return ProgressViewHolder(binding)
}
@SuppressLint("ClickableViewAccessibility")
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onBindViewHolder(holder: ProgressViewHolder, position: Int) {
val progressBar = holder.binding.root
bar = progressBar

View File

@@ -4,30 +4,24 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.databinding.ActivitySearchBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Timer
import java.util.TimerTask
import java.util.*
class SearchActivity : AppCompatActivity() {
private lateinit var binding: ActivitySearchBinding
@@ -70,16 +64,11 @@ class SearchActivity : AppCompatActivity() {
intent.getStringExtra("type") ?: "ANIME",
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
onList = listOnly,
search = intent.getStringExtra("query"),
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
sort = intent.getStringExtra("sortBy"),
status = intent.getStringExtra("status"),
source = intent.getStringExtra("source"),
countryOfOrigin = intent.getStringExtra("country"),
season = intent.getStringExtra("season"),
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
seasonYear = intent.getStringExtra("seasonYear")?.toIntOrNull(),
results = mutableListOf(),
hasNextPage = false
)
@@ -138,12 +127,8 @@ class SearchActivity : AppCompatActivity() {
excludedTags = it.excludedTags
tags = it.tags
season = it.season
startYear = it.startYear
seasonYear = it.seasonYear
status = it.status
source = it.source
format = it.format
countryOfOrigin = it.countryOfOrigin
page = it.page
hasNextPage = it.hasNextPage
}
@@ -152,7 +137,7 @@ class SearchActivity : AppCompatActivity() {
model.searchResults.results.addAll(it.results)
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
progressAdapter.bar?.visibility = if (it.hasNextPage) View.VISIBLE else View.GONE
}
}
@@ -166,10 +151,7 @@ class SearchActivity : AppCompatActivity() {
} else
headerAdaptor.requestFocus?.run()
if (intent.getBooleanExtra("search", false)) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED)
search()
}
if (intent.getBooleanExtra("search", false)) search()
}
}
}

View File

@@ -13,10 +13,9 @@ import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.PopupMenu
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat.startActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
@@ -29,9 +28,7 @@ import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.google.android.material.checkbox.MaterialCheckBox.STATE_CHECKED
import com.google.android.material.checkbox.MaterialCheckBox.STATE_INDETERMINATE
import com.google.android.material.checkbox.MaterialCheckBox.STATE_UNCHECKED
import com.google.android.material.checkbox.MaterialCheckBox.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -47,19 +44,6 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
private lateinit var binding: ItemSearchHeaderBinding
private fun updateFilterTextViewDrawable() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@@ -108,7 +92,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true
binding.searchChipRecycler.adapter = SearchChipAdapter(activity, this).also {
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
activity.updateChips = { it.update() }
}
@@ -118,59 +102,6 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
}
binding.searchFilter.setOnLongClickListener {
val popupMenu = PopupMenu(activity, binding.searchFilter)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
}
true
}
popupMenu.show()
true
}
binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
}
@@ -325,7 +256,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
}
class SearchChipAdapter(val activity: SearchActivity, private val searchAdapter: SearchAdapter) :
class SearchChipAdapter(val activity: SearchActivity) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList()
@@ -342,12 +273,11 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
val chip = chips[position]
holder.binding.root.apply {
text = chip.text.replace("_", " ")
text = chip.text
setOnClickListener {
activity.result.removeChip(chip)
update()
activity.search()
searchAdapter.updateFilterTextViewDrawable()
}
}
}
@@ -356,7 +286,6 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
fun update() {
chips = activity.result.toChipList()
notifyDataSetChanged()
searchAdapter.updateFilterTextViewDrawable()
}
override fun getItemCount(): Int = chips.size

View File

@@ -1,15 +1,11 @@
package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AnimationUtils
import android.widget.ArrayAdapter
import android.widget.PopupMenu
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -21,9 +17,6 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
import ani.dantotsu.databinding.ItemChipBinding
import com.google.android.material.chip.Chip
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Calendar
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
@@ -45,54 +38,6 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
private var exGenres = mutableListOf<String>()
private var selectedTags = mutableListOf<String>()
private var exTags = mutableListOf<String>()
private fun updateChips() {
binding.searchFilterGenres.adapter?.notifyDataSetChanged()
binding.searchFilterTags.adapter?.notifyDataSetChanged()
}
private fun startBounceZoomAnimation(view: View? = null) {
val targetView = view ?: binding.sortByFilter
val bounceZoomAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
targetView.startAnimation(bounceZoomAnimation)
}
private fun setSortByFilterImage() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.sortByFilter.setImageResource(filterDrawable)
}
private fun resetSearchFilter() {
activity.result.sort = null
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
startBounceZoomAnimation(binding.sortByFilter)
activity.result.countryOfOrigin = null
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
selectedGenres.clear()
exGenres.clear()
selectedTags.clear()
exTags.clear()
binding.searchStatus.setText("")
binding.searchSource.setText("")
binding.searchFormat.setText("")
binding.searchSeason.setText("")
binding.searchYear.setText("")
binding.searchStatus.clearFocus()
binding.searchFormat.clearFocus()
binding.searchSeason.clearFocus()
binding.searchYear.clearFocus()
updateChips()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -102,149 +47,14 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
exGenres = activity.result.excludedGenres ?: mutableListOf()
selectedTags = activity.result.tags ?: mutableListOf()
exTags = activity.result.excludedTags ?: mutableListOf()
setSortByFilterImage()
binding.resetSearchFilter.setOnClickListener {
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
resetSearchFilter()
}
binding.resetSearchFilter.setOnLongClickListener {
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
val bounceAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
binding.resetSearchFilter.startAnimation(bounceAnimation)
binding.resetSearchFilter.postDelayed({
resetSearchFilter()
CoroutineScope(Dispatchers.Main).launch {
activity.result.apply {
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null }
season = binding.searchSeason.text.toString().ifBlank { null }
startYear = binding.searchYear.text.toString().toIntOrNull()
seasonYear = binding.searchYear.text.toString().toIntOrNull()
sort = activity.result.sort
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
excludedTags = exTags
}
activity.updateChips.invoke()
activity.search()
dismiss()
}
}, 500)
true
}
binding.sortByFilter.setOnClickListener {
val popupMenu = PopupMenu(requireContext(), it)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24)
startBounceZoomAnimation()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24)
startBounceZoomAnimation()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24)
startBounceZoomAnimation()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24)
startBounceZoomAnimation()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24)
startBounceZoomAnimation()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse)
startBounceZoomAnimation()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24)
startBounceZoomAnimation()
}
}
true
}
popupMenu.show()
}
binding.countryFilter.setOnClickListener {
val popupMenu = PopupMenu(requireContext(), it)
popupMenu.menuInflater.inflate(R.menu.country_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.country_global -> {
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_china -> {
activity.result.countryOfOrigin = "CN"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_south_korea -> {
activity.result.countryOfOrigin = "KR"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_japan -> {
activity.result.countryOfOrigin = "JP"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_taiwan -> {
activity.result.countryOfOrigin = "TW"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
}
true
}
popupMenu.show()
}
binding.searchFilterApply.setOnClickListener {
activity.result.apply {
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null }
sort = binding.searchSortBy.text.toString().ifBlank { null }
?.let { Anilist.sortBy[resources.getStringArray(R.array.sort_by).indexOf(it)] }
season = binding.searchSeason.text.toString().ifBlank { null }
if (activity.result.type == "ANIME") {
seasonYear = binding.searchYear.text.toString().toIntOrNull()
} else {
startYear = binding.searchYear.text.toString().toIntOrNull()
}
sort = activity.result.sort
countryOfOrigin = activity.result.countryOfOrigin
seasonYear = binding.searchYear.text.toString().toIntOrNull()
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
@@ -257,22 +67,15 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
binding.searchFilterCancel.setOnClickListener {
dismiss()
}
val format = if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
binding.searchStatus.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
format
)
)
binding.searchSource.setText(activity.result.source?.replace("_", " "))
binding.searchSource.setAdapter(
binding.searchSortBy.setText(activity.result.sort?.let {
resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
})
binding.searchSortBy.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
Anilist.source.toTypedArray()
resources.getStringArray(R.array.sort_by)
)
)
@@ -281,25 +84,11 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
(if (activity.result.type == "ANIME") Anilist.anime_formats else Anilist.manga_formats).toTypedArray()
)
)
if (activity.result.type == "ANIME") {
binding.searchYear.setText(activity.result.seasonYear?.toString())
} else {
binding.searchYear.setText(activity.result.startYear?.toString())
}
binding.searchYear.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
.reversed().toTypedArray()
)
)
if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE
if (activity.result.type == "MANGA") binding.searchSeasonYearCont.visibility = GONE
else {
binding.searchSeason.setText(activity.result.season)
binding.searchSeason.setAdapter(
@@ -309,6 +98,16 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
Anilist.seasons.toTypedArray()
)
)
binding.searchYear.setText(activity.result.seasonYear?.toString())
binding.searchYear.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
.reversed().toTypedArray()
)
)
}
binding.searchFilterGenres.adapter = FilterChipAdapter(Anilist.genres ?: listOf()) { chip ->

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
@@ -21,6 +22,7 @@ abstract class SourceAdapter(
return SourceViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: SourceViewHolder, position: Int) {
val binding = holder.binding
val character = sources[position]

View File

@@ -65,7 +65,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
i = media!!.selected!!.sourceIndex
val source = if (media!!.anime != null) {
(if (media!!.isAdult) HAnimeSources else AnimeSources)[i!!]
(if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!]
} else {
anime = false
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]

View File

@@ -6,7 +6,6 @@ import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -115,7 +114,7 @@ class StudioActivity : AppCompatActivity() {
}
override fun onResume() {
binding.studioProgressBar.isGone = loaded
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
super.onResume()
}
}

View File

@@ -5,7 +5,6 @@ import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.snackString
import com.anggrayudi.storage.file.openOutputStream
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -18,7 +17,7 @@ class SubtitleDownloader {
companion object {
//doesn't really download the subtitles -\_(o_o)_/-
suspend fun loadSubtitleType(url: String): SubtitleType =
suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
withContext(Dispatchers.IO) {
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
val networkHelper = Injekt.get<NetworkHelper>()
@@ -52,17 +51,21 @@ class SubtitleDownloader {
downloadedType: DownloadedType
) {
try {
val directory = DownloadsManager.getSubDirectory(
val directory = DownloadsManager.getDirectory(
context,
downloadedType.type,
false,
downloadedType.title,
downloadedType.chapter
) ?: throw Exception("Could not create directory")
val type = loadSubtitleType(url)
directory.findFile("subtitle.${type}")?.delete()
val subtitleFile = directory.createFile("*/*", "subtitle.${type}")
?: throw Exception("Could not create subtitle file")
)
if (!directory.exists()) { //just in case
directory.mkdirs()
}
val type = loadSubtitleType(context, url)
val subtiteFile = File(directory, "subtitle.${type}")
if (subtiteFile.exists()) {
subtiteFile.delete()
}
subtiteFile.createNewFile()
val client = Injekt.get<NetworkHelper>().client
val request = Request.Builder().url(url).build()
@@ -74,8 +77,7 @@ class SubtitleDownloader {
}
reponse.body.byteStream().use { input ->
subtitleFile.openOutputStream(context, false).use { output ->
if (output == null) throw Exception("Could not open output stream")
subtiteFile.outputStream().use { output ->
input.copyTo(output)
}
}

View File

@@ -0,0 +1,136 @@
package ani.dantotsu.media
import android.graphics.Color
import android.view.ViewGroup
import androidx.core.view.updateLayoutParams
import ani.dantotsu.R
import ani.dantotsu.navBarHeight
import nl.joery.animatedbottombar.AnimatedBottomBar
class TripleNavAdapter(
private val nav1: AnimatedBottomBar,
private val nav2: AnimatedBottomBar,
private val nav3: AnimatedBottomBar,
anime: Boolean,
format: String,
private val isScreenVertical: Boolean = false
) {
var selected: Int = 0
var selectionListener: ((Int, Int) -> Unit)? = null
init {
nav1.tabs.clear()
nav2.tabs.clear()
nav3.tabs.clear()
val infoTab = nav1.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
val watchTab = if (anime) {
nav2.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
} else if (format == "NOVEL") {
nav2.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
} else {
nav2.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
}
val commentTab = nav3.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
nav1.addTab(infoTab)
nav1.visibility = ViewGroup.VISIBLE
if (isScreenVertical) {
nav2.visibility = ViewGroup.GONE
nav3.visibility = ViewGroup.GONE
nav1.addTab(watchTab)
nav1.addTab(commentTab)
nav1.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
nav2.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
nav3.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
} else {
nav1.indicatorColor = Color.TRANSPARENT
nav2.indicatorColor = Color.TRANSPARENT
nav3.indicatorColor = Color.TRANSPARENT
nav2.visibility = ViewGroup.VISIBLE
nav3.visibility = ViewGroup.VISIBLE
nav2.addTab(watchTab)
nav3.addTab(commentTab)
nav2.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
selected = 1
deselectOthers(selected)
selectionListener?.invoke(selected, newTab.id)
}
})
nav3.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
selected = 2
deselectOthers(selected)
selectionListener?.invoke(selected, newTab.id)
}
})
}
nav1.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
if (!isScreenVertical) {
selected = 0
deselectOthers(selected)
} else selected = newIndex
selectionListener?.invoke(selected, newTab.id)
}
})
}
private fun deselectOthers(selected: Int) {
if (selected == 0) {
nav2.clearSelection()
nav3.clearSelection()
}
if (selected == 1) {
nav1.clearSelection()
nav3.clearSelection()
}
if (selected == 2) {
nav1.clearSelection()
nav2.clearSelection()
}
}
fun selectTab(tab: Int) {
selected = tab
if (!isScreenVertical) {
when (tab) {
0 -> nav1.selectTabAt(0)
1 -> nav2.selectTabAt(0)
2 -> nav3.selectTabAt(0)
}
deselectOthers(selected)
} else {
nav1.selectTabAt(selected)
}
}
fun setVisibility(visibility: Int) {
if (isScreenVertical) {
nav1.visibility = visibility
return
}
nav1.visibility = visibility
nav2.visibility = visibility
nav3.visibility = visibility
}
}

View File

@@ -0,0 +1,127 @@
package ani.dantotsu.media.anime
import java.util.Locale
import java.util.regex.Matcher
import java.util.regex.Pattern
class AnimeNameAdapter {
companion object {
const val episodeRegex =
"(episode|episodio|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
const val failedEpisodeNumberRegex =
"(?<!part\\s)\\b(\\d+)\\b"
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
const val subdubRegex = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val soft = subdubMatcher.group(1)
val subdub = subdubMatcher.group(2)
val bed = subdubMatcher.group(3) ?: ""
val toggled = when (typeToSetTo) {
SubDubType.SUB -> "sub"
SubDubType.DUB -> "dub"
SubDubType.NULL -> ""
}
val toggledCasePreserved =
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
?.isUpperCase() == true
) toggled.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.ROOT
) else it.toString()
} else toggled
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
} else {
null
}
}
fun getSubDub(text: String): SubDubType {
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
when (subdub) {
"sub" -> SubDubType.SUB
"dub" -> SubDubType.DUB
else -> SubDubType.NULL
}
} else {
SubDubType.NULL
}
}
enum class SubDubType {
SUB, DUB, NULL
}
fun findSeasonNumber(text: String): Int? {
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
}
}
fun findEpisodeNumber(text: String): Float? {
val episodePattern: Pattern = Pattern.compile(episodeRegex, Pattern.CASE_INSENSITIVE)
val episodeMatcher: Matcher = episodePattern.matcher(text)
return if (episodeMatcher.find()) {
if (episodeMatcher.group(2) != null) {
episodeMatcher.group(2)?.toFloat()
} else {
val failedEpisodeNumberPattern: Pattern =
Pattern.compile(failedEpisodeNumberRegex, Pattern.CASE_INSENSITIVE)
val failedEpisodeNumberMatcher: Matcher =
failedEpisodeNumberPattern.matcher(text)
if (failedEpisodeNumberMatcher.find()) {
failedEpisodeNumberMatcher.group(1)?.toFloat()
} else {
null
}
}
} else {
null
}
}
fun removeEpisodeNumber(text: String): String {
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "").ifEmpty {
text
}
val letterPattern = Regex("[a-zA-Z]")
return if (letterPattern.containsMatchIn(removedNumber)) {
removedNumber
} else {
text
}
}
fun removeEpisodeNumberCompletely(text: String): String {
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "")
return if (removedNumber.equals(text, true)) { // if nothing was removed
val failedEpisodeNumberPattern: Regex =
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
mr.value.replaceFirst(mr.groupValues[1], "")
}
} else {
removedNumber
}
}
}
}

View File

@@ -1,9 +1,11 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import ani.dantotsu.settings.FAQActivity
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageButton
@@ -11,34 +13,22 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.countDown
import ani.dantotsu.currActivity
import ani.dantotsu.*
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.isOnline
import ani.dantotsu.loadImage
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.openSettings
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.px
import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.toast
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
@@ -52,7 +42,7 @@ class AnimeWatchAdapter(
private val fragment: AnimeWatchFragment,
private val watchSources: WatchSources
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
private var autoSelect = true
var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null
@@ -64,6 +54,7 @@ class AnimeWatchAdapter(
private var nestedDialog: AlertDialog? = null
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
_binding = binding
@@ -106,12 +97,15 @@ class AnimeWatchAdapter(
null
)
}
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
val offline = if (!isOnline(binding.root.context) || PrefManager.getVal(
PrefName.OfflineMode
)
) View.GONE else View.VISIBLE
binding.animeSourceNameContainer.isGone = offline
binding.animeSourceSettings.isGone = offline
binding.animeSourceSearch.isGone = offline
binding.animeSourceTitle.isGone = offline
binding.animeSourceNameContainer.visibility = offline
binding.animeSourceSettings.visibility = offline
binding.animeSourceSearch.visibility = offline
binding.animeSourceTitle.visibility = offline
//Source Selection
var source =
@@ -123,7 +117,8 @@ class AnimeWatchAdapter(
this.selectDub = media.selected!!.preferDub
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
}
}
@@ -142,7 +137,8 @@ class AnimeWatchAdapter(
changing = true
binding.animeSourceDubbed.isChecked = selectDub
changing = false
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
source = i
setLanguageList(0, i)
}
@@ -162,12 +158,14 @@ class AnimeWatchAdapter(
changing = true
binding.animeSourceDubbed.isChecked = selectDub
changing = false
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
setLanguageList(i, source)
}
subscribeButton(false)
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
} ?: run { }
} ?: run {
}
}
//settings
@@ -225,9 +223,9 @@ class AnimeWatchAdapter(
else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.setText(R.string.list)
1 -> dialogBinding.layoutText.setText(R.string.grid)
2 -> dialogBinding.layoutText.setText(R.string.compact)
0 -> dialogBinding.layoutText.text = "List"
1 -> dialogBinding.layoutText.text = "Grid"
2 -> dialogBinding.layoutText.text = "Compact"
else -> dialogBinding.animeSourceList
}
selected.alpha = 1f
@@ -239,24 +237,24 @@ class AnimeWatchAdapter(
dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
dialogBinding.layoutText.setText(R.string.list)
dialogBinding.layoutText.text = "List"
run = true
}
dialogBinding.animeSourceGrid.setOnClickListener {
selected(it as ImageButton)
style = 1
dialogBinding.layoutText.setText(R.string.grid)
dialogBinding.layoutText.text = "Grid"
run = true
}
dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 2
dialogBinding.layoutText.setText(R.string.compact)
dialogBinding.layoutText.text = "Compact"
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast(R.string.webview_not_installed)
toast("WebView not installed")
}
//start CookieCatcher activity
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
@@ -309,6 +307,7 @@ class AnimeWatchAdapter(
}
//Chips
@SuppressLint("SetTextI18n")
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding
if (binding != null) {
@@ -330,8 +329,7 @@ class AnimeWatchAdapter(
0
)
}
val chipText = "${names[limit * (position)]} - ${names[last - 1]}"
chip.text = chipText
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.setTextColor(
ContextCompat.getColorStateList(
fragment.requireContext(),
@@ -365,6 +363,7 @@ class AnimeWatchAdapter(
_binding?.animeSourceChipGroup?.removeAllViews()
}
@SuppressLint("SetTextI18n")
fun handleEpisodes() {
val binding = _binding
if (binding != null) {
@@ -372,9 +371,9 @@ class AnimeWatchAdapter(
val episodes = media.anime.episodes!!.keys.toTypedArray()
val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = PrefManager.getCustomVal<String?>(
"${media.id}_current_ep", ""
)?.toIntOrNull() ?: 1
val appEp =
PrefManager.getCustomVal<String?>("${media.id}_current_ep", "")?.toIntOrNull()
?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
if (episodes.contains(continueEp)) {
@@ -404,25 +403,21 @@ class AnimeWatchAdapter(
}
val ep = media.anime.episodes!![continueEp]!!
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
binding.itemEpisodeImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
)
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text =
currActivity()!!.getString(R.string.continue_episode, ep.number, if (ep.filler)
currActivity()!!.getString(R.string.filler_tag)
else
"", cleanedTitle)
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
binding.animeSourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp)
}
if (fragment.continueEp) {
if (
(binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams)
.weight < PrefManager.getVal<Float>(PrefName.WatchPercentage)
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < PrefManager.getVal<Float>(
PrefName.WatchPercentage
)
) {
binding.animeSourceContinue.performClick()
fragment.continueEp = false
@@ -433,28 +428,13 @@ class AnimeWatchAdapter(
}
binding.animeSourceProgressBar.visibility = View.GONE
val sourceFound = media.anime.episodes!!.isNotEmpty()
binding.animeSourceNotFound.isGone = sourceFound
binding.faqbutton.isGone = sourceFound
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) {
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) {
val nextIndex = media.selected!!.sourceIndex + 1
binding.animeSource.setText(binding.animeSource.adapter
.getItem(nextIndex).toString(), false)
fragment.onSourceChange(nextIndex).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
binding.animeSourceDubbed.isChecked = selectDub
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
setLanguageList(0, nextIndex)
}
subscribeButton(false)
fragment.loadEpisodes(nextIndex, false)
if (media.anime.episodes!!.isNotEmpty()) {
binding.animeSourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE}
else {
binding.animeSourceNotFound.visibility = View.VISIBLE
binding.faqbutton.visibility = View.VISIBLE
}
}
binding.animeSource.setOnClickListener { autoSelect = false }
} else {
binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE

View File

@@ -14,12 +14,9 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@@ -30,33 +27,24 @@ import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.dp
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.setNavigationTheme
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
import com.google.android.material.appbar.AppBarLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
@@ -228,7 +216,7 @@ class AnimeWatchFragment : Fragment() {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc =
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: ""
).isBlank()
) media.anime!!.kitsuEpisodes!![i]?.title
@@ -352,12 +340,16 @@ class AnimeWatchFragment : Fragment() {
val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = activity
if (activity is MediaDetailsActivity && isAdded) {
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).isVisible = show
activity.findViewById<ViewPager2>(R.id.mediaViewPager).isVisible = show
activity.findViewById<CardView>(R.id.mediaCover).isVisible = show
activity.findViewById<CardView>(R.id.mediaClose).isVisible = show
activity.navBar.isVisible = show
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).isGone = show
val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
activity.tabLayout.setVisibility(visibility)
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
}
}
var itemSelected = false
@@ -425,19 +417,7 @@ class AnimeWatchFragment : Fragment() {
}
fun onAnimeEpisodeDownloadClick(i: String) {
activity?.let{
if (!hasDirAccess(it)) {
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
if (success) {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
} else {
snackString("Permission is required to download")
}
}
} else {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
}
}
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
}
fun onAnimeEpisodeStopDownloadClick(i: String) {
@@ -455,11 +435,10 @@ class AnimeWatchFragment : Fragment() {
DownloadedType(
media.mainName(),
i,
MediaType.ANIME
DownloadedType.Type.ANIME
)
) {
episodeAdapter.purgeDownload(i)
}
)
episodeAdapter.purgeDownload(i)
}
@OptIn(UnstableApi::class)
@@ -468,17 +447,22 @@ class AnimeWatchFragment : Fragment() {
DownloadedType(
media.mainName(),
i,
MediaType.ANIME
DownloadedType.Type.ANIME
)
) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
episodeAdapter.deleteDownload(i)
}
)
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
DownloadService.sendRemoveDownload(
requireContext(),
ExoplayerDownloadService::class.java,
id,
true
)
episodeAdapter.deleteDownload(i)
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
@@ -542,7 +526,7 @@ class AnimeWatchFragment : Fragment() {
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
episodeAdapter.notifyItemRangeInserted(0, arr.size)
for (download in downloadManager.animeDownloadedTypes) {
if (download.title == media.mainName().findValidName()) {
if (download.title == media.mainName()) {
episodeAdapter.stopDownload(download.chapter)
}
}

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.view.LayoutInflater
import android.view.View
@@ -7,23 +8,18 @@ import android.view.ViewGroup
import android.view.animation.LinearInterpolator
import android.widget.LinearLayout
import androidx.annotation.OptIn
import androidx.core.view.isVisible
import androidx.lifecycle.coroutineScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadIndex
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.*
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.databinding.ItemEpisodeGridBinding
import ani.dantotsu.databinding.ItemEpisodeListBinding
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getDirSize
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
@@ -57,7 +53,15 @@ class EpisodeAdapter(
var arr: List<Episode> = arrayListOf(),
var offlineMode: Boolean
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val context = fragment.requireContext()
private lateinit var index: DownloadIndex
init {
if (offlineMode) {
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return (when (viewType) {
@@ -93,10 +97,11 @@ class EpisodeAdapter(
return type
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val ep = arr[position]
val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
} else {
ep.number
} ?: ""
@@ -120,7 +125,8 @@ class EpisodeAdapter(
binding.itemEpisodeFiller.visibility = View.GONE
binding.itemEpisodeFillerView.visibility = View.GONE
}
binding.itemEpisodeDesc.isVisible = !ep.desc.isNullOrBlank()
binding.itemEpisodeDesc.visibility =
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = ep.desc ?: ""
holder.bind(ep.number, ep.downloadProgress, ep.desc)
@@ -197,7 +203,8 @@ class EpisodeAdapter(
val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root)
binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeFillerView.isVisible = ep.filler
binding.itemEpisodeFillerView.visibility =
if (ep.filler) View.VISIBLE else View.GONE
if (media.userProgress != null) {
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat())
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
@@ -241,8 +248,17 @@ class EpisodeAdapter(
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
media.mainName(),
episodeNumber
)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
val size = try {
bytesToHuman(getDirSize(context, MediaType.ANIME, media.mainName(), episodeNumber))
val download = index.getDownload(id)
bytesToHuman(download?.bytesDownloaded ?: 0)
} catch (e: Exception) {
null
}
@@ -413,7 +429,7 @@ class EpisodeAdapter(
if (bytes < 0) return null
val unit = 1000
if (bytes < unit) return "$bytes B"
val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt()
val pre = ("KMGTPE")[exp - 1]
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
}

View File

@@ -16,10 +16,7 @@ import android.graphics.Color
import android.graphics.drawable.Animatable
import android.hardware.SensorManager
import android.media.AudioManager
import android.media.AudioManager.AUDIOFOCUS_GAIN
import android.media.AudioManager.AUDIOFOCUS_LOSS
import android.media.AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
import android.media.AudioManager.STREAM_MUSIC
import android.media.AudioManager.*
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -30,18 +27,8 @@ import android.provider.Settings.System
import android.util.AttributeSet
import android.util.Rational
import android.util.TypedValue
import android.view.GestureDetector
import android.view.KeyEvent
import android.view.KeyEvent.ACTION_UP
import android.view.KeyEvent.KEYCODE_B
import android.view.KeyEvent.KEYCODE_DPAD_LEFT
import android.view.KeyEvent.KEYCODE_DPAD_RIGHT
import android.view.KeyEvent.KEYCODE_N
import android.view.KeyEvent.KEYCODE_SPACE
import android.view.MotionEvent
import android.view.OrientationEventListener
import android.view.View
import android.view.ViewGroup
import android.view.*
import android.view.KeyEvent.*
import android.view.animation.AnimationUtils
import android.widget.AdapterView
import android.widget.ImageButton
@@ -54,47 +41,32 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.isVisible
import androidx.core.view.WindowCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.C
import androidx.media3.common.*
import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE
import androidx.media3.common.C.TRACK_TYPE_VIDEO
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.PlaybackException
import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultDataSourceFactory
import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.exoplayer.util.EventLogger
import androidx.media3.session.MediaSession
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.CaptionStyleCompat
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_DEPRESSED
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_NONE
import androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_OUTLINE
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView
import androidx.media3.ui.*
import androidx.media3.ui.CaptionStyleCompat.*
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.mediarouter.app.MediaRouteButton
import ani.dantotsu.GesturesListener
import ani.dantotsu.NoPaddingArrayAdapter
import ani.dantotsu.*
import ani.dantotsu.R
import ani.dantotsu.brightnessConverter
import ani.dantotsu.circularReveal
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.connections.discord.Discord
@@ -103,40 +75,19 @@ import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityExoplayerBinding
import ani.dantotsu.defaultHeaders
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.dp
import ani.dantotsu.getCurrentBrightnessValue
import ani.dantotsu.hideSystemBars
import ani.dantotsu.hideSystemBarsExtendView
import ani.dantotsu.isOnline
import ani.dantotsu.logError
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.okHttpClient
import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.AniSkip.getType
import ani.dantotsu.others.ResettableTimer
import ani.dantotsu.others.getSerialized
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType
import ani.dantotsu.px
import ani.dantotsu.parsers.*
import ani.dantotsu.settings.PlayerSettingsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide
import com.google.android.gms.cast.framework.CastButtonFactory
@@ -152,17 +103,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.*
import java.util.concurrent.*
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@UnstableApi
@SuppressLint("ClickableViewAccessibility")
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityListener {
private val resumeWindow = "resumeWindow"
@@ -227,6 +175,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
private var extractor: VideoExtractor? = null
private var video: Video? = null
private var subtitle: Subtitle? = null
private val player = "player_settings"
private var notchHeight: Int = 0
private var currentWindow = 0
@@ -395,15 +344,15 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
isCastApiAvailable = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
try {
castContext =
CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor()).result
castContext = CastContext.getSharedInstance(this)
castPlayer = CastPlayer(castContext!!)
castPlayer!!.setSessionAvailabilityListener(this)
} catch (e: Exception) {
isCastApiAvailable = false
}
hideSystemBarsExtendView()
WindowCompat.setDecorFitsSystemWindows(window, false)
hideSystemBars()
onBackPressedDispatcher.addCallback(this) {
finishAndRemoveTask()
@@ -444,49 +393,43 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}, AUDIO_CONTENT_TYPE_MOVIE, AUDIOFOCUS_GAIN)
if (System.getInt(contentResolver, System.ACCELEROMETER_ROTATION, 0) != 1) {
if (PrefManager.getVal(PrefName.RotationPlayer)) {
orientationListener =
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
when (orientation) {
in 45..135 -> {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
}
in 225..315 -> {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
in 315..360, in 0..45 -> {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
if (PrefManager.getVal(PrefName.RotationPlayer)) {
orientationListener =
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) {
if (orientation in 45..135) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
} else if (orientation in 225..315) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else if (orientation in 315..360 || orientation in 0..45) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
orientationListener?.enable()
}
}
orientationListener?.enable()
}
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
exoRotate.setOnClickListener {
requestedOrientation = rotation
it.visibility = View.GONE
}
}
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
exoRotate.setOnClickListener {
requestedOrientation = rotation
it.visibility = View.GONE
}
}
setupSubFormatting(playerView)
playerView.subtitleView?.alpha = when (PrefManager.getVal<Boolean>(PrefName.Subtitles)) {
true -> PrefManager.getVal(PrefName.SubAlpha)
true -> 1f
false -> 0f
}
val fontSize = PrefManager.getVal<Int>(PrefName.FontSize).toFloat()
@@ -760,13 +703,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
fun seek(forward: Boolean, event: MotionEvent? = null) {
val seekTime = PrefManager.getVal<Int>(PrefName.SeekTime)
val (card, text) = if (forward) {
val text = "+${seekTime * ++seekTimesF}"
forwardText.text = text
forwardText.text = "+${seekTime * ++seekTimesF}"
handler.post { exoPlayer.seekTo(exoPlayer.currentPosition + seekTime * 1000) }
fastForwardCard to forwardText
} else {
val text = "-${seekTime * ++seekTimesR}"
rewindText.text = text
rewindText.text = "-${seekTime * ++seekTimesR}"
handler.post { exoPlayer.seekTo(exoPlayer.currentPosition - seekTime * 1000) }
fastRewindCard to rewindText
}
@@ -1000,10 +941,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
episodeArr = episodes.keys.toList()
currentEpisodeIndex = episodeArr.indexOf(media.anime!!.selectedEpisode!!)
episodeTitleArr = arrayListOf()
episodeTitleArr = arrayListOf<String>()
episodes.forEach {
val episode = it.value
val cleanedTitle = MediaNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
val cleanedTitle = AnimeNameAdapter.removeEpisodeNumberCompletely(episode.title ?: "")
episodeTitleArr.add("Episode ${episode.number}${if (episode.filler) " [Filler]" else ""}${if (cleanedTitle.isNotBlank() && cleanedTitle != "null") ": $cleanedTitle" else ""}")
}
@@ -1088,48 +1029,35 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if ((isOnline(context) && !offline) && Discord.token != null && !incognito) {
lifecycleScope.launch {
val discordMode = PrefManager.getCustomVal("discord_mode", "dantotsu")
val buttons = when (discordMode) {
"nothing" -> mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
)
"dantotsu" -> mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link("Watch on Dantotsu", getString(R.string.dantotsu))
)
"anilist" -> {
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
val anilistLink = "https://anilist.co/user/$userId/"
mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link("View My AniList", anilistLink)
val presence = RPC.createPresence(RPC.Companion.RPCData(
applicationId = Discord.application_Id,
type = RPC.Type.WATCHING,
activityName = media.userPreferredName,
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
R.string.episode_num,
ep.number
),
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
largeImage = media.cover?.let {
RPC.Link(
media.userPreferredName,
it
)
},
smallImage = RPC.Link(
"Dantotsu",
Discord.small_Image
),
buttons = mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link(
"Stream on Dantotsu",
"https://github.com/rebelonion/Dantotsu/"
)
}
else -> mutableListOf()
}
val presence = RPC.createPresence(
RPC.Companion.RPCData(
applicationId = Discord.application_Id,
type = RPC.Type.WATCHING,
activityName = media.userPreferredName,
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
R.string.episode_num,
ep.number
),
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
largeImage = media.cover?.let {
RPC.Link(
media.userPreferredName,
it
)
},
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
buttons = buttons
)
)
)
val intent = Intent(context, DiscordService::class.java).apply {
putExtra("presence", presence)
}
@@ -1137,6 +1065,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
startService(intent)
}
}
updateProgress()
}
}
@@ -1173,7 +1102,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (PrefManager.getVal(PrefName.Cast)) {
playerView.findViewById<CustomCastButton>(R.id.exo_cast).apply {
visibility = View.VISIBLE
if (PrefManager.getVal(PrefName.UseInternalCast)) {
if(PrefManager.getVal(PrefName.UseInternalCast)) {
try {
CastButtonFactory.setUpMediaRouteButton(context, this)
dialogFactory = CustomCastThemeFactory()
@@ -1335,12 +1264,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
media.anime!!.selectedEpisode!!
)
@Suppress("UNCHECKED_CAST")
val list = (PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>).toMutableList()
val list = (PrefManager.getNullableCustomVal("continueAnimeList", listOf<Int>(), List::class.java) as List<Int>).toMutableList()
if (list.contains(media.id)) list.remove(media.id)
list.add(media.id)
PrefManager.setCustomVal("continueAnimeList", list)
@@ -1369,7 +1293,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}
//Subtitles
exoSubtitle.isVisible = ext.subtitles.isNotEmpty()
exoSubtitle.visibility = if (ext.subtitles.isNotEmpty()) View.VISIBLE else View.GONE
exoSubtitle.setOnClickListener {
subClick()
}
@@ -1377,8 +1301,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (subtitle != null) {
//var localFile: String? = null
if (subtitle?.type == SubtitleType.UNKNOWN) {
val context = this
runBlocking {
val type = SubtitleDownloader.loadSubtitleType(subtitle!!.file.url)
val type = SubtitleDownloader.loadSubtitleType(context, subtitle!!.file.url)
val fileUri = Uri.parse(subtitle!!.file.url)
sub = MediaItem.SubtitleConfiguration
.Builder(fileUri)
@@ -1417,6 +1342,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
ext.onVideoPlayed(video)
}
val simpleCache = VideoCache.getInstance(this)
val httpClient = okHttpClient.newBuilder().apply {
ignoreAllSSLErrors()
followRedirects(true)
@@ -1432,9 +1358,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}
dataSource
}
val dafuckDataSourceFactory = DefaultDataSource.Factory(this)
val dafuckDataSourceFactory =
DefaultDataSourceFactory(this, Util.getUserAgent(this, R.string.app_name.toString()))
cacheFactory = CacheDataSource.Factory().apply {
setCache(VideoCache.getInstance(this@ExoplayerView))
setCache(Helper.getSimpleCache(this@ExoplayerView))
if (ext.server.offline) {
setUpstreamDataSourceFactory(dafuckDataSourceFactory)
} else {
@@ -1451,28 +1378,15 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
val downloadedMediaItem = if (ext.server.offline) {
val key = ext.server.name
val titleName = ext.server.name.split("/").first()
val episodeName = ext.server.name.split("/").last()
val directory = getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName)
if (directory != null) {
val files = directory.listFiles()
println(files)
val docFile = directory.listFiles().firstOrNull {
it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true
}
if (docFile != null) {
val uri = docFile.uri
MediaItem.Builder().setUri(uri).setMimeType(mimeType).build()
} else {
snackString("File not found")
null
}
downloadId = PrefManager.getAnimeDownloadPreferences()
.getString(key, null)
if (downloadId != null) {
Helper.downloadManager(this)
.downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
} else {
snackString("Directory not found")
snackString("Download not found")
null
}
} else null
mediaItem = if (downloadedMediaItem == null) {
@@ -1745,7 +1659,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
aspectRatio = Rational(width, height)
videoInfo.text = getString(R.string.video_quality, height)
videoInfo.text = "Quality: ${height}p"
if (exoPlayer.duration < playbackPosition)
exoPlayer.seekTo(0)
@@ -1821,33 +1735,35 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
timer = null
return
}
timer = object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {
if (new == null) {
if (timer == null) {
timer = object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {
if (new == null){
skipTimeButton.visibility = View.GONE
exoSkip.visibility = View.VISIBLE
disappeared = false
functionstarted = false
cancelTimer()
}
}
override fun onFinish() {
skipTimeButton.visibility = View.GONE
exoSkip.visibility = View.VISIBLE
disappeared = false
disappeared = true
functionstarted = false
cancelTimer()
}
}
override fun onFinish() {
skipTimeButton.visibility = View.GONE
exoSkip.visibility = View.VISIBLE
disappeared = true
functionstarted = false
cancelTimer()
}
timer?.start()
}
timer?.start()
}
if (PrefManager.getVal(PrefName.ShowTimeStampButton)) {
if (!functionstarted && !disappeared && PrefManager.getVal(PrefName.AutoHideTimeStamps)) {
if (!functionstarted && !disappeared && PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)) {
disappearSkip()
} else if (!PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)) {
} else if (!PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)){
skipTimeButton.visibility = View.VISIBLE
exoSkip.visibility = View.GONE
skipTimeText.text = new.skipType.getType()
@@ -2186,16 +2102,16 @@ class CustomCastButton : MediaRouteButton {
fun setCastCallback(castCallback: () -> Unit) {
this.castCallback = castCallback
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet, castCallback: () -> Unit) : super(context, attrs) {
this.castCallback = castCallback
}
override fun performClick(): Boolean {
return if (PrefManager.getVal(PrefName.UseInternalCast)) {

View File

@@ -3,8 +3,6 @@ package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Color
@@ -14,41 +12,25 @@ import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.*
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.copyToClipboard
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.BottomSheetSelectorBinding
import ani.dantotsu.databinding.ItemStreamBinding
import ani.dantotsu.databinding.ItemUrlBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.video.Helper
import ani.dantotsu.hideSystemBars
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.tryWith
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -229,62 +211,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
super.onViewCreated(view, savedInstanceState)
}
private val externalPlayerResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
Logger.log(result.data.toString())
}
private fun exportMagnetIntent(episode: Episode, video: Video) : Intent {
val amnis = "com.amnis"
return Intent(Intent.ACTION_VIEW).apply {
component = ComponentName(amnis, "$amnis.gui.player.PlayerActivity")
data = Uri.parse(video.file.url)
putExtra("title", "${media?.name} - ${episode.title}")
putExtra("position", 0)
putExtra(Intent.EXTRA_RETURN_RESULT, true)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra("secure_uri", true)
val headersArray = arrayOf<String>()
video.file.headers.forEach {
headersArray.plus(arrayOf(it.key, it.value))
}
putExtra("headers", headersArray)
}
}
@SuppressLint("UnsafeOptInUsageError")
fun startExoplayer(media: Media) {
prevEpisode = null
dismiss()
episode?.let { ep ->
val video = ep.extractors?.find {
it.server.name == ep.selectedExtractor
}?.videos?.getOrNull(ep.selectedVideo)
video?.file?.url?.let { url ->
if (url.startsWith("magnet:")) {
try {
externalPlayerResult.launch(exportMagnetIntent(ep, video))
} catch (e: ActivityNotFoundException) {
val amnis = "com.amnis"
try {
startActivity(Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$amnis"))
)
} catch (e: ActivityNotFoundException) {
startActivity(Intent(
Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=$amnis")
))
}
}
return
}
}
}
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
stopAddingToList()
val intent = Intent(activity, ExoplayerView::class.java)
@@ -371,6 +302,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
val binding = holder.binding
val video = extractor.videos[position]
@@ -379,45 +311,6 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} else {
binding.urlDownload.visibility = View.GONE
}
val subtitles = extractor.subtitles
if (subtitles.isNotEmpty()) {
binding.urlSub.visibility = View.VISIBLE
} else {
binding.urlSub.visibility = View.GONE
}
binding.urlSub.setOnClickListener {
if (subtitles.isNotEmpty()) {
val subtitleNames = subtitles.map { it.language }
var subtitleToDownload: Subtitle? = null
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Subtitle")
.setSingleChoiceItems(
subtitleNames.toTypedArray(),
-1
) { _, which ->
subtitleToDownload = subtitles[which]
}
.setPositiveButton("Download") { dialog, _ ->
scope.launch {
if (subtitleToDownload != null) {
SubtitleDownloader.downloadSubtitle(
requireContext(),
subtitleToDownload!!.file.url,
DownloadedType(media!!.mainName(), media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number, MediaType.ANIME)
)
}
}
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
.show()
alertDialog.window?.setDimAmount(0.8f)
} else {
snackString("No Subtitles Available")
}
}
binding.urlDownload.setSafeOnClickListener {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
extractor.server.name
@@ -443,7 +336,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
.setSingleChoiceItems(
subtitleNames.toTypedArray(),
-1
) { _, which ->
) { dialog, which ->
subtitleToDownload = subtitles[which]
}
.setPositiveButton("Download") { _, _ ->
@@ -508,12 +401,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
dismiss()
}
if (video.format == VideoType.CONTAINER) {
binding.urlSize.isVisible = video.size != null
// if video size is null or 0, show "Unknown Size" else show the size in MB
val sizeText = getString(R.string.mb_size, "${if (video.extraNote != null) " : " else ""}${
if (video.size == 0.0) getString(R.string.size_unknown) else DecimalFormat("#.##").format(video.size ?: 0)
}")
binding.urlSize.text = sizeText
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
binding.urlSize.text =
// if video size is null or 0, show "Unknown Size" else show the size in MB
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
"#.##"
).format(video.size ?: 0).toString() + " MB"))
}
binding.urlNote.visibility = View.VISIBLE
binding.urlNote.text = video.format.name

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media.anime
import android.app.Activity
import android.graphics.Color.TRANSPARENT
import android.os.Bundle
import android.view.LayoutInflater
@@ -112,6 +113,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
binding.root.setCardBackgroundColor(TRANSPARENT)
}
}
val activity: Activity = requireActivity() as ExoplayerView
binding.root.setOnClickListener {
episode.selectedSubtitle = position - 1
model.setEpisode(episode, "Subtitle")

View File

@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.view.View
import android.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
@@ -12,7 +11,6 @@ import ani.dantotsu.connections.comments.Comment
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemCommentsBinding
import ani.dantotsu.getAppString
import ani.dantotsu.loadImage
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.profile.ProfileActivity
@@ -30,20 +28,17 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import kotlin.math.abs
import kotlin.math.sqrt
class CommentItem(
val comment: Comment,
private val markwon: Markwon,
val parentSection: Section,
private val commentsFragment: CommentsFragment,
private val backgroundColor: Int,
val commentDepth: Int
) :
BindableItem<ItemCommentsBinding>() {
class CommentItem(val comment: Comment,
private val markwon: Markwon,
val parentSection: Section,
private val commentsFragment: CommentsFragment,
private val backgroundColor: Int,
val commentDepth: Int
) : BindableItem<ItemCommentsBinding>() {
lateinit var binding: ItemCommentsBinding
val adapter = GroupieAdapter()
private var subCommentIds: MutableList<Int> = mutableListOf()
@@ -57,6 +52,7 @@ class CommentItem(
adapter.add(repliesSection)
}
@SuppressLint("SetTextI18n")
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
binding = viewBinding
setAnimation(binding.root.context, binding.root)
@@ -65,6 +61,9 @@ class CommentItem(
val isUserComment = CommentsAPI.userId == comment.userId
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
markwon.setMarkdown(viewBinding.commentText, comment.content)
viewBinding.commentDelete.visibility = if (isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod) View.VISIBLE else View.GONE
viewBinding.commentBanUser.visibility = if ((CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment) View.VISIBLE else View.GONE
viewBinding.commentReport.visibility = if (!isUserComment) View.VISIBLE else View.GONE
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
if (comment.tag == null) {
viewBinding.commentUserTagLayout.visibility = View.GONE
@@ -77,15 +76,8 @@ class CommentItem(
if ((comment.replyCount ?: 0) > 0) {
viewBinding.commentTotalReplies.visibility = View.VISIBLE
viewBinding.commentRepliesDivider.visibility = View.VISIBLE
viewBinding.commentTotalReplies.context.run {
viewBinding.commentTotalReplies.text = if (repliesVisible)
getString(R.string.hide_replies)
else
if (comment.replyCount == 1)
getString(R.string.view_reply)
else
getString(R.string.view_replies_count, comment.replyCount)
}
viewBinding.commentTotalReplies.text = if(repliesVisible) "Hide Replies" else
"View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
} else {
viewBinding.commentTotalReplies.visibility = View.GONE
viewBinding.commentRepliesDivider.visibility = View.GONE
@@ -95,15 +87,10 @@ class CommentItem(
if (repliesVisible) {
repliesSection.clear()
removeSubCommentIds()
viewBinding.commentTotalReplies.context.run {
viewBinding.commentTotalReplies.text = if (comment.replyCount == 1)
getString(R.string.view_reply)
else
getString(R.string.view_replies_count, comment.replyCount)
}
viewBinding.commentTotalReplies.text = "View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
repliesVisible = false
} else {
viewBinding.commentTotalReplies.setText(R.string.hide_replies)
viewBinding.commentTotalReplies.text = "Hide Replies"
repliesSection.clear()
commentsFragment.viewReplyCallback(this)
repliesVisible = true
@@ -140,71 +127,39 @@ class CommentItem(
}
viewBinding.modBadge.visibility = if (comment.isMod == true) View.VISIBLE else View.GONE
viewBinding.adminBadge.visibility = if (comment.isAdmin == true) View.VISIBLE else View.GONE
viewBinding.commentInfo.setOnClickListener {
val popup = PopupMenu(commentsFragment.requireContext(), viewBinding.commentInfo)
popup.menuInflater.inflate(R.menu.profile_details_menu, popup.menu)
popup.menu.findItem(R.id.commentDelete)?.isVisible = isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod
popup.menu.findItem(R.id.commentBanUser)?.isVisible = (CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment
popup.menu.findItem(R.id.commentReport)?.isVisible = !isUserComment
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.commentReport -> {
dialogBuilder(
getAppString(R.string.report_comment),
getAppString(R.string.report_comment_confirm)
) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.reportComment(
comment.commentId,
comment.username,
commentsFragment.mediaName,
comment.userId
)
if (success) {
snackString(R.string.comment_reported)
}
}
}
true
}
R.id.commentDelete -> {
dialogBuilder(
getAppString(R.string.delete_comment),
getAppString(R.string.delete_comment_confirm)
) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.deleteComment(comment.commentId)
if (success) {
snackString(R.string.comment_deleted)
parentSection.remove(this@CommentItem)
}
}
}
true
}
R.id.commentBanUser -> {
dialogBuilder(
getAppString(R.string.ban_user),
getAppString(R.string.ban_user_confirm)
) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.banUser(comment.userId)
if (success) {
snackString(R.string.user_banned)
}
}
}
true
}
else -> {
false
viewBinding.commentDelete.setOnClickListener {
dialogBuilder("Delete Comment", "Are you sure you want to delete this comment?") {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.deleteComment(comment.commentId)
if (success) {
snackString("Comment Deleted")
parentSection.remove(this@CommentItem)
}
}
}
}
viewBinding.commentBanUser.setOnClickListener {
dialogBuilder("Ban User", "Are you sure you want to ban this user?") {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.banUser(comment.userId)
if (success) {
snackString("User Banned")
}
}
}
}
viewBinding.commentReport.setOnClickListener {
dialogBuilder("Report Comment", "Only report comments that violate the rules. Are you sure you want to report this comment?") {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.reportComment(comment.commentId, comment.username, commentsFragment.mediaName, comment.userId)
if (success) {
snackString("Comment Reported")
}
}
}
popup.show()
}
//fill the icon if the user has liked the comment
setVoteButtons(viewBinding)
@@ -240,6 +195,7 @@ class CommentItem(
comment.upvotes -= 1
}
comment.downvotes += if (voteType == -1) 1 else -1
notifyChanged()
}
}
@@ -254,8 +210,7 @@ class CommentItem(
}
comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) }
viewBinding.commentUserName.text = comment.username
val userColor = "[${levelColor.second}]"
viewBinding.commentUserLevel.text = userColor
viewBinding.commentUserLevel.text = "[${levelColor.second}]"
viewBinding.commentUserLevel.setTextColor(levelColor.first)
viewBinding.commentUserTime.text = formatTimestamp(comment.timestamp)
}
@@ -288,7 +243,6 @@ class CommentItem(
private fun removeSubCommentIds(){
subCommentIds.forEach { id ->
@Suppress("UNCHECKED_CAST")
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
val commentToRemove = parentComments.find { it.comment.commentId == id }
commentToRemove?.let {
@@ -318,10 +272,9 @@ class CommentItem(
}
}
@SuppressLint("SimpleDateFormat")
private fun formatTimestamp(timestamp: String): String {
return try {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val parsedDate = dateFormat.parse(timestamp)
val currentDate = Date()
@@ -344,9 +297,8 @@ class CommentItem(
}
companion object {
@SuppressLint("SimpleDateFormat")
fun timestampToMillis(timestamp: String): Long {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val parsedDate = dateFormat.parse(timestamp)
return parsedDate?.time ?: 0

View File

@@ -28,7 +28,6 @@ import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.databinding.FragmentCommentsBinding
import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.setBaseline
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
@@ -74,9 +73,6 @@ class CommentsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity = requireActivity() as MediaDetailsActivity
binding.commentsListContainer.setBaseline(activity.navBar, activity.binding.commentInputLayout)
//get the media id from the intent
val mediaId = arguments?.getInt("mediaId") ?: -1
mediaName = arguments?.getString("mediaName") ?: "unknown"
@@ -527,10 +523,11 @@ class CommentsFragment : Fragment() {
}
@SuppressLint("SetTextI18n")
fun replyTo(comment: CommentItem, username: String) {
if (comment.isReplying) {
activity.binding.commentReplyToContainer.visibility = View.VISIBLE
activity.binding.commentReplyTo.text = getString(R.string.replying_to, username)
activity.binding.commentReplyTo.text = "Replying to $username"
activity.binding.commentReplyToCancel.setOnClickListener {
comment.replying(false)
replyCallback(comment)

View File

@@ -2,6 +2,7 @@ package ani.dantotsu.media.manga
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
@@ -9,8 +10,8 @@ import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.util.LruCache
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import ani.dantotsu.snackString
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers
@@ -24,13 +25,15 @@ data class ImageData(
) {
suspend fun fetchAndProcessImage(
page: Page,
httpSource: HttpSource
httpSource: HttpSource,
context: Context
): Bitmap? {
return withContext(Dispatchers.IO) {
try {
// Fetch the image
val response = httpSource.getImage(page)
Logger.log("Response: ${response.code} - ${response.message}")
Logger.log("Response: ${response.code}")
Logger.log("Response: ${response.message}")
// Convert the Response to an InputStream
val inputStream = response.body.byteStream()

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media.manga
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.util.TypedValue
import android.view.LayoutInflater
@@ -15,13 +16,12 @@ import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemChapterListBinding
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.setAnimation
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MangaChapterAdapter(
private var type: Int,
@@ -262,16 +262,17 @@ class MangaChapterAdapter(
}
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ChapterCompactViewHolder -> {
val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root)
val ep = arr[position]
val parsedNumber = MediaNameAdapter.findChapterNumber(ep.number)?.toInt()
val parsedNumber = MangaNameAdapter.findChapterNumber(ep.number)?.toInt()
binding.itemEpisodeNumber.text = parsedNumber?.toString() ?: ep.number
if (media.userProgress != null) {
if ((MediaNameAdapter.findChapterNumber(ep.number)
if ((MangaNameAdapter.findChapterNumber(ep.number)
?: 9999f) <= media.userProgress!!.toFloat()
)
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
@@ -280,7 +281,7 @@ class MangaChapterAdapter(
binding.itemEpisodeCont.setOnLongClickListener {
updateProgress(
media,
MediaNameAdapter.findChapterNumber(ep.number).toString()
MangaNameAdapter.findChapterNumber(ep.number).toString()
)
true
}
@@ -316,7 +317,7 @@ class MangaChapterAdapter(
} else binding.itemChapterTitle.visibility = View.VISIBLE
if (media.userProgress != null) {
if ((MediaNameAdapter.findChapterNumber(ep.number)
if ((MangaNameAdapter.findChapterNumber(ep.number)
?: 9999f) <= media.userProgress!!.toFloat()
) {
binding.itemEpisodeViewedCover.visibility = View.VISIBLE
@@ -327,7 +328,7 @@ class MangaChapterAdapter(
binding.root.setOnLongClickListener {
updateProgress(
media,
MediaNameAdapter.findChapterNumber(ep.number).toString()
MangaNameAdapter.findChapterNumber(ep.number).toString()
)
true
}

View File

@@ -0,0 +1,29 @@
package ani.dantotsu.media.manga
import java.util.regex.Matcher
import java.util.regex.Pattern
class MangaNameAdapter {
companion object {
const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*"
const val filedChapterNumberRegex = "(?<!part\\s)\\b(\\d+)\\b"
fun findChapterNumber(text: String): Float? {
val pattern: Pattern = Pattern.compile(chapterRegex, Pattern.CASE_INSENSITIVE)
val matcher: Matcher = pattern.matcher(text)
return if (matcher.find()) {
matcher.group(2)?.toFloat()
} else {
val failedChapterNumberPattern: Pattern =
Pattern.compile(filedChapterNumberRegex, Pattern.CASE_INSENSITIVE)
val failedChapterNumberMatcher: Matcher =
failedChapterNumberPattern.matcher(text)
if (failedChapterNumberMatcher.find()) {
failedChapterNumberMatcher.group(1)?.toFloat()
} else {
null
}
}
}
}
}

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media.manga
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.view.LayoutInflater
@@ -12,34 +13,24 @@ import android.widget.LinearLayout
import android.widget.NumberPicker
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.*
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.isOnline
import ani.dantotsu.loadImage
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.media.anime.handleProgress
import ani.dantotsu.openSettings
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.px
import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.toast
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
import eu.kanade.tachiyomi.source.online.HttpSource
@@ -67,6 +58,7 @@ class MangaReadAdapter(
private var nestedDialog: AlertDialog? = null
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
_binding = binding
@@ -85,12 +77,14 @@ class MangaReadAdapter(
null
)
}
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
val offline =
if (!isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
) View.GONE else View.VISIBLE
binding.animeSourceNameContainer.isGone = offline
binding.animeSourceSettings.isGone = offline
binding.animeSourceSearch.isGone = offline
binding.animeSourceTitle.isGone = offline
binding.animeSourceNameContainer.visibility = offline
binding.animeSourceSettings.visibility = offline
binding.animeSourceSearch.visibility = offline
binding.animeSourceTitle.visibility = offline
//Source Selection
var source =
media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
@@ -194,8 +188,8 @@ class MangaReadAdapter(
else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.setText(R.string.list)
1 -> dialogBinding.layoutText.setText(R.string.compact)
0 -> dialogBinding.layoutText.text = "List"
1 -> dialogBinding.layoutText.text = "Compact"
else -> dialogBinding.animeSourceList
}
selected.alpha = 1f
@@ -207,18 +201,18 @@ class MangaReadAdapter(
dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
dialogBinding.layoutText.setText(R.string.list)
dialogBinding.layoutText.text = "List"
run = true
}
dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 1
dialogBinding.layoutText.setText(R.string.compact)
dialogBinding.layoutText.text = "Compact"
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast(R.string.webview_not_installed)
toast("WebView not installed")
}
//start CookieCatcher activity
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
@@ -255,7 +249,8 @@ class MangaReadAdapter(
}
//Scanlator
dialogBinding.animeScanlatorContainer.isVisible = options.count() > 1
dialogBinding.animeScanlatorContainer.visibility =
if (options.count() > 1) View.VISIBLE else View.GONE
dialogBinding.scanlatorNo.text = "${options.count()}"
dialogBinding.animeScanlatorTop.setOnClickListener {
val dialogView2 = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
@@ -364,6 +359,7 @@ class MangaReadAdapter(
}
//Chips
@SuppressLint("SetTextI18n")
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding
if (binding != null) {
@@ -386,8 +382,8 @@ class MangaReadAdapter(
)
}
val startChapter = MediaNameAdapter.findChapterNumber(names[limit * (position)])
val endChapter = MediaNameAdapter.findChapterNumber(names[last - 1])
val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)])
val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1])
val startChapterString = if (startChapter != null) {
"Ch.$startChapter"
} else {
@@ -399,8 +395,7 @@ class MangaReadAdapter(
names[last - 1]
}
//chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
val chipText = "$startChapterString - $endChapterString"
chip.text = chipText
chip.text = "$startChapterString - $endChapterString"
chip.setTextColor(
ContextCompat.getColorStateList(
fragment.requireContext(),
@@ -434,6 +429,7 @@ class MangaReadAdapter(
_binding?.animeSourceChipGroup?.removeAllViews()
}
@SuppressLint("SetTextI18n")
fun handleChapters() {
val binding = _binding
@@ -449,7 +445,7 @@ class MangaReadAdapter(
chapter.scanlator !in hiddenScanlators
}
val formattedChapters = filteredChapters.map {
MediaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
}
if (formattedChapters.contains(continueEp)) {
continueEp = chapters[formattedChapters.indexOf(continueEp)]
@@ -470,7 +466,7 @@ class MangaReadAdapter(
val ep = media.manga.chapters!![continueEp]!!
binding.itemEpisodeImage.loadImage(media.banner ?: media.cover)
binding.animeSourceContinueText.text =
currActivity()!!.getString(R.string.continue_chapter, ep.number, if (!ep.title.isNullOrEmpty()) ep.title else "")
currActivity()!!.getString(R.string.continue_chapter) + "${ep.number}${if (!ep.title.isNullOrEmpty()) "\n${ep.title}" else ""}"
binding.animeSourceContinue.setOnClickListener {
fragment.onMangaChapterClick(continueEp)
}
@@ -485,24 +481,12 @@ class MangaReadAdapter(
binding.animeSourceContinue.visibility = View.GONE
}
binding.animeSourceProgressBar.visibility = View.GONE
val sourceFound = media.manga.chapters!!.isNotEmpty()
binding.animeSourceNotFound.isGone = sourceFound
binding.faqbutton.isGone = sourceFound
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources)) {
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) {
val nextIndex = media.selected!!.sourceIndex + 1
binding.animeSource.setText(binding.animeSource.adapter
.getItem(nextIndex).toString(), false)
fragment.onSourceChange(nextIndex).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
setLanguageList(0, nextIndex)
}
subscribeButton(false)
// invalidate if it's the last source
val invalidate = nextIndex == mangaReadSources.names.size - 1
fragment.loadChapters(nextIndex, invalidate)
}
if (media.manga.chapters!!.isNotEmpty()) {
binding.animeSourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE
} else {
binding.animeSourceNotFound.visibility = View.VISIBLE
binding.faqbutton.visibility = View.VISIBLE
}
} else {
binding.animeSourceContinue.visibility = View.GONE
@@ -535,7 +519,8 @@ class MangaReadAdapter(
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
)
val items = adapter.count
binding?.animeSourceLanguageContainer?.isVisible = items > 1
binding?.animeSourceLanguageContainer?.visibility =
if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguage?.setAdapter(adapter)

View File

@@ -16,13 +16,10 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@@ -31,35 +28,26 @@ import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.MangaServiceDataSingleton
import ani.dantotsu.dp
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.navBarHeight
import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.setNavigationTheme
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
import com.google.android.material.appbar.AppBarLayout
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource
@@ -194,7 +182,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
)
for (download in downloadManager.mangaDownloadedTypes) {
if (download.title == media.mainName().findValidName()) {
if (download.title == media.mainName()) {
chapterAdapter.stopDownload(download.chapter)
}
}
@@ -232,7 +220,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val chapters = media.manga?.chapters?.values?.toList()
//filter by selected language
val progressChapterIndex = (chapters?.indexOfFirst {
MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
} ?: 0) + 1
if (progressChapterIndex < 0 || n < 1 || chapters == null) return
@@ -366,12 +354,14 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = activity
if (activity is MediaDetailsActivity && isAdded) {
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).isVisible = show
activity.findViewById<ViewPager2>(R.id.mediaViewPager).isVisible = show
activity.findViewById<CardView>(R.id.mediaCover).isVisible = show
activity.findViewById<CardView>(R.id.mediaClose).isVisible = show
activity.navBar.isVisible = show
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).isGone = show
val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
activity.tabLayout.setVisibility(visibility)
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
}
}
var itemSelected = false
@@ -438,65 +428,51 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
fun onMangaChapterDownloadClick(i: String) {
activity?.let {
if (!isNotificationPermissionGranted()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
it,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1
if (!isNotificationPermissionGranted()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1
)
}
}
model.continueMedia = false
media.manga?.chapters?.get(i)?.let { chapter ->
val parser =
model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
parser?.let {
CoroutineScope(Dispatchers.IO).launch {
val images = parser.imageList("", chapter.sChapter)
// Create a download task
val downloadTask = MangaDownloaderService.DownloadTask(
title = media.mainName(),
chapter = chapter.title!!,
imageData = images,
sourceMedia = media,
retries = 2,
simultaneousDownloads = 2
)
}
}
fun continueDownload() {
model.continueMedia = false
media.manga?.chapters?.get(i)?.let { chapter ->
val parser =
model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
parser?.let {
CoroutineScope(Dispatchers.IO).launch {
val images = parser.imageList(chapter.sChapter)
// Create a download task
val downloadTask = MangaDownloaderService.DownloadTask(
title = media.mainName(),
chapter = chapter.title!!,
imageData = images,
sourceMedia = media,
retries = 2,
simultaneousDownloads = 2
)
MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
// If the service is not already running, start it
if (!MangaServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, MangaDownloaderService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent)
}
MangaServiceDataSingleton.isServiceRunning = true
}
// Inform the adapter that the download has started
withContext(Dispatchers.Main) {
chapterAdapter.startDownload(i)
}
// If the service is not already running, start it
if (!MangaServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, MangaDownloaderService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent)
}
MangaServiceDataSingleton.isServiceRunning = true
}
// Inform the adapter that the download has started
withContext(Dispatchers.Main) {
chapterAdapter.startDownload(i)
}
}
}
if (!hasDirAccess(it)) {
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
if (success) {
continueDownload()
} else {
snackString("Permission is required to download")
}
}
} else {
continueDownload()
}
}
}
@@ -516,11 +492,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
DownloadedType(
media.mainName(),
i,
MediaType.MANGA
DownloadedType.Type.MANGA
)
) {
chapterAdapter.deleteDownload(i)
}
)
chapterAdapter.deleteDownload(i)
}
fun onMangaChapterStopDownloadClick(i: String) {
@@ -535,11 +510,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
DownloadedType(
media.mainName(),
i,
MediaType.MANGA
DownloadedType.Type.MANGA
)
) {
chapterAdapter.purgeDownload(i)
}
)
chapterAdapter.purgeDownload(i)
}
private val downloadStatusReceiver = object : BroadcastReceiver() {

View File

@@ -13,19 +13,16 @@ import androidx.core.view.GestureDetectorCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.FileUrl
import ani.dantotsu.GesturesListener
import ani.dantotsu.R
import ani.dantotsu.*
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.px
import ani.dantotsu.settings.CurrentReaderSettings
import ani.dantotsu.tryWithSuspend
import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import eu.kanade.tachiyomi.source.model.Page
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -37,18 +34,7 @@ abstract class BaseImageAdapter(
chapter: MangaChapter
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val settings = activity.defaultSettings
private val chapterImages = chapter.images()
var images = chapterImages
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
images = if (settings.layout == CurrentReaderSettings.Layouts.PAGED
&& settings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP) {
chapterImages.reversed()
} else {
chapterImages
}
super.onAttachedToRecyclerView(recyclerView)
}
val images = chapter.images()
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
@@ -132,13 +118,13 @@ abstract class BaseImageAdapter(
abstract suspend fun loadImage(position: Int, parent: View): Boolean
companion object {
suspend fun Context.loadBitmapOld(
suspend fun Context.loadBitmap_old(
link: FileUrl,
transforms: List<BitmapTransformation>
): Bitmap? { //still used in some places
return tryWithSuspend {
withContext(Dispatchers.IO) {
Glide.with(this@loadBitmapOld)
Glide.with(this@loadBitmap_old)
.asBitmap()
.let {
if (link.url.startsWith("file://")) {
@@ -172,26 +158,23 @@ abstract class BaseImageAdapter(
Glide.with(this@loadBitmap)
.asBitmap()
.let {
val fileUri = Uri.fromFile(File(link.url)).toString()
val localFile = File(link.url)
if (localFile.exists()) {
it.load(localFile.absoluteFile)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
} else if (link.url.startsWith("content://")) {
it.load(Uri.parse(link.url))
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
} else {
mangaCache.get(link.url)?.let { imageData ->
val bitmap = imageData.fetchAndProcessImage(
imageData.page,
imageData.source
imageData.source,
context = this@loadBitmap
)
it.load(bitmap)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
}
}
}
?.let {
@@ -224,4 +207,9 @@ abstract class BaseImageAdapter(
return newBitmap
}
}
}
interface ImageFetcher {
suspend fun fetchImage(page: Page): Bitmap?
}

View File

@@ -37,7 +37,7 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
binding.selectorAutoListContainer.visibility = View.VISIBLE
binding.selectorListContainer.visibility = View.GONE
binding.selectorTitle.text = getString(R.string.loading_chap_number, chp.number)
binding.selectorTitle.text = getString(R.string.loading_next_chap)
binding.selectorCancel.setOnClickListener {
dismiss()
}
@@ -49,7 +49,8 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
lifecycleScope.launch(Dispatchers.IO) {
if (model.loadMangaChapterImages(
chp,
m.selected!!
m.selected!!,
m.mainName()
)
) {
val activity = currActivity()

View File

@@ -10,19 +10,8 @@ import android.content.res.Resources
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.KeyEvent
import android.view.KeyEvent.ACTION_DOWN
import android.view.KeyEvent.KEYCODE_DPAD_DOWN
import android.view.KeyEvent.KEYCODE_DPAD_UP
import android.view.KeyEvent.KEYCODE_PAGE_DOWN
import android.view.KeyEvent.KEYCODE_PAGE_UP
import android.view.KeyEvent.KEYCODE_VOLUME_DOWN
import android.view.KeyEvent.KEYCODE_VOLUME_UP
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.*
import android.view.KeyEvent.*
import android.view.animation.OvershootInterpolator
import android.widget.AdapterView
import android.widget.CheckBox
@@ -31,16 +20,13 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.math.MathUtils.clamp
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.GesturesListener
import ani.dantotsu.NoPaddingArrayAdapter
import ani.dantotsu.R
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.connections.discord.Discord
@@ -48,41 +34,25 @@ import ani.dantotsu.connections.discord.DiscordService
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ActivityMangaReaderBinding
import ani.dantotsu.dp
import ani.dantotsu.hideSystemBarsExtendView
import ani.dantotsu.isOnline
import ani.dantotsu.logError
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaSingleton
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.manga.MangaNameAdapter
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaImage
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.CurrentReaderSettings
import ani.dantotsu.settings.CurrentReaderSettings.Companion.applyWebtoon
import ani.dantotsu.settings.CurrentReaderSettings.Directions.BOTTOM_TO_TOP
import ani.dantotsu.settings.CurrentReaderSettings.Directions.LEFT_TO_RIGHT
import ani.dantotsu.settings.CurrentReaderSettings.Directions.RIGHT_TO_LEFT
import ani.dantotsu.settings.CurrentReaderSettings.Directions.TOP_TO_BOTTOM
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.Automatic
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.Force
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.No
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.CONTINUOUS_PAGED
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.PAGED
import ani.dantotsu.settings.CurrentReaderSettings.Directions.*
import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.*
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.showSystemBarsRetractView
import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.tryWith
import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
@@ -95,11 +65,11 @@ import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.util.Timer
import java.util.TimerTask
import java.util.*
import kotlin.math.min
import kotlin.properties.Delegates
@SuppressLint("SetTextI18n")
class MangaReaderActivity : AppCompatActivity() {
private val mangaCache = Injekt.get<MangaCache>()
@@ -118,6 +88,7 @@ class MangaReaderActivity : AppCompatActivity() {
private var isContVisible = false
private var showProgressDialog = true
private var hidescrollbar = false
private var maxChapterPage = 0L
private var currentChapterPage = 0L
@@ -129,11 +100,6 @@ class MangaReaderActivity : AppCompatActivity() {
var sliding = false
var isAnimating = false
private val directionRLBT get() = defaultSettings.direction == RIGHT_TO_LEFT
|| defaultSettings.direction == BOTTOM_TO_TOP
private val directionPagedBT get() = defaultSettings.layout == CurrentReaderSettings.Layouts.PAGED
&& defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP
override fun onAttachedToWindow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) {
val displayCutout = window.decorView.rootWindowInsets.displayCutout
@@ -157,7 +123,7 @@ class MangaReaderActivity : AppCompatActivity() {
}
private fun hideSystemBars() {
if (PrefManager.getVal(PrefName.ShowSystemBars))
if (PrefManager.getVal<Boolean>(PrefName.ShowSystemBars))
showSystemBarsRetractView()
else
hideSystemBarsExtendView()
@@ -185,7 +151,7 @@ class MangaReaderActivity : AppCompatActivity() {
defaultSettings = loadReaderSettings("reader_settings") ?: defaultSettings
onBackPressedDispatcher.addCallback(this) {
val chapter = (MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
val chapter = (MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
?.minus(1L) ?: 0).toString()
if (chapter == "0.0" && PrefManager.getVal(PrefName.ChapterZeroReader)
// Not asking individually or incognito
@@ -229,13 +195,8 @@ class MangaReaderActivity : AppCompatActivity() {
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 }
?: 1))
else
if (defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP ) {
binding.mangaReaderPager.currentItem =
(maxChapterPage.toInt() - value.toInt()) / (dualPage { 2 } ?: 1)
} else {
binding.mangaReaderPager.currentItem =
(value.toInt() - 1) / (dualPage { 2 } ?: 1)
}
binding.mangaReaderPager.currentItem =
(value.toInt() - 1) / (dualPage { 2 } ?: 1)
pageSliderHide()
}
}
@@ -262,7 +223,8 @@ class MangaReaderActivity : AppCompatActivity() {
chapter = chapters[media.manga!!.selectedChapter] ?: return
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
binding.mangaReaderSource.isVisible = PrefManager.getVal(PrefName.ShowSource)
binding.mangaReaderSource.visibility =
if (PrefManager.getVal(PrefName.ShowSource)) View.VISIBLE else View.GONE
if (model.mangaReadSources!!.names.isEmpty()) {
//try to reload sources
try {
@@ -341,7 +303,7 @@ class MangaReaderActivity : AppCompatActivity() {
binding.mangaReaderNextChapter.performClick()
}
binding.mangaReaderNextChapter.setOnClickListener {
if (directionRLBT) {
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
if (currentChapterIndex > 0) change(currentChapterIndex - 1)
else snackString(getString(R.string.first_chapter))
} else {
@@ -354,7 +316,7 @@ class MangaReaderActivity : AppCompatActivity() {
binding.mangaReaderPreviousChapter.performClick()
}
binding.mangaReaderPreviousChapter.setOnClickListener {
if (directionRLBT) {
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
else snackString(getString(R.string.next_chapter_not_found))
} else {
@@ -371,12 +333,16 @@ class MangaReaderActivity : AppCompatActivity() {
PrefManager.setCustomVal("${media.id}_current_chp", chap.number)
currentChapterIndex = chaptersArr.indexOf(chap.number)
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
if (directionRLBT) {
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
binding.mangaReaderNextChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
} else {
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
binding.mangaReaderNextChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
}
applySettings()
val context = this
@@ -384,25 +350,6 @@ class MangaReaderActivity : AppCompatActivity() {
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if ((isOnline(context) && !offline) && Discord.token != null && !incognito) {
lifecycleScope.launch {
val discordMode = PrefManager.getCustomVal("discord_mode", "dantotsu")
val buttons = when (discordMode) {
"nothing" -> mutableListOf(
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
)
"dantotsu" -> mutableListOf(
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
RPC.Link("Read on Dantotsu", getString(R.string.dantotsu))
)
"anilist" -> {
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
val anilistLink = "https://anilist.co/user/$userId/"
mutableListOf(
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
RPC.Link("View My AniList", anilistLink)
)
}
else -> mutableListOf()
}
val presence = RPC.createPresence(
RPC.Companion.RPCData(
applicationId = Discord.application_Id,
@@ -411,9 +358,20 @@ class MangaReaderActivity : AppCompatActivity() {
details = chap.title?.takeIf { it.isNotEmpty() }
?: getString(R.string.chapter_num, chap.number),
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}",
largeImage = media.cover?.let { cover -> RPC.Link(media.userPreferredName, cover) },
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
buttons = buttons
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 {
@@ -429,7 +387,8 @@ class MangaReaderActivity : AppCompatActivity() {
scope.launch(Dispatchers.IO) {
model.loadMangaChapterImages(
chapter,
media.selected!!
media.selected!!,
media.mainName()
)
}
}
@@ -469,11 +428,7 @@ class MangaReaderActivity : AppCompatActivity() {
currentChapterPage = PrefManager.getCustomVal("${media.id}_${chapter.number}", 1L)
val chapImages = if (directionPagedBT) {
chapter.images().reversed()
} else {
chapter.images()
}
val chapImages = chapter.images()
maxChapterPage = 0
if (chapImages.isNotEmpty()) {
@@ -497,11 +452,7 @@ class MangaReaderActivity : AppCompatActivity() {
}
val currentPage = if (directionPagedBT) {
maxChapterPage - currentChapterPage + 1
} else {
currentChapterPage
}.toInt()
val currentPage = currentChapterPage.toInt()
if ((defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == BOTTOM_TO_TOP)) {
binding.mangaReaderSwipy.vertical = true
@@ -530,10 +481,10 @@ class MangaReaderActivity : AppCompatActivity() {
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onTopSwiped = {
binding.mangaReaderPreviousChapter.performClick()
binding.mangaReaderNextChapter.performClick()
}
binding.mangaReaderSwipy.onBottomSwiped = {
binding.mangaReaderNextChapter.performClick()
binding.mangaReaderPreviousChapter.performClick()
}
}
binding.mangaReaderSwipy.topBeingSwiped = { value ->
@@ -642,7 +593,7 @@ class MangaReaderActivity : AppCompatActivity() {
RecyclerView.VERTICAL
else
RecyclerView.HORIZONTAL,
directionRLBT
!(defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == LEFT_TO_RIGHT)
)
manager.preloadItemCount = 5
@@ -659,8 +610,6 @@ class MangaReaderActivity : AppCompatActivity() {
else false
}
manager.setStackFromEnd(defaultSettings.direction == BOTTOM_TO_TOP)
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
defaultSettings.apply {
@@ -715,7 +664,9 @@ class MangaReaderActivity : AppCompatActivity() {
visibility = View.VISIBLE
adapter = imageAdapter
layoutDirection =
if (directionRLBT) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
if (defaultSettings.direction == BOTTOM_TO_TOP || defaultSettings.direction == RIGHT_TO_LEFT)
View.LAYOUT_DIRECTION_RTL
else View.LAYOUT_DIRECTION_LTR
orientation =
if (defaultSettings.direction == LEFT_TO_RIGHT || defaultSettings.direction == RIGHT_TO_LEFT)
ViewPager2.ORIENTATION_HORIZONTAL
@@ -790,12 +741,12 @@ class MangaReaderActivity : AppCompatActivity() {
goneTimer.schedule(timerTask, controllerDuration)
}
enum class PressPos {
enum class pressPos {
LEFT, RIGHT, CENTER
}
fun handleController(shouldShow: Boolean? = null, event: MotionEvent? = null) {
var pressLocation = PressPos.CENTER
var pressLocation = pressPos.CENTER
if (!sliding) {
if (event != null && defaultSettings.layout == PAGED) {
if (event.action != MotionEvent.ACTION_UP) return
@@ -804,24 +755,24 @@ class MangaReaderActivity : AppCompatActivity() {
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
if (screenWidth / 5 in x + 1..<y) {
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT) {
PressPos.RIGHT
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
pressPos.RIGHT
} else {
PressPos.LEFT
pressPos.LEFT
}
}
//if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right
else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) {
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT) {
PressPos.LEFT
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
pressPos.LEFT
} else {
PressPos.RIGHT
pressPos.RIGHT
}
}
}
// if pressLocation is left or right go to previous or next page (paged mode only)
if (pressLocation == PressPos.LEFT) {
if (pressLocation == pressPos.LEFT) {
if (binding.mangaReaderPager.currentItem > 0) {
//if the current images zoomed in, go back to normal before going to previous page
@@ -832,7 +783,7 @@ class MangaReaderActivity : AppCompatActivity() {
return
}
} else if (pressLocation == PressPos.RIGHT) {
} else if (pressLocation == pressPos.RIGHT) {
if (binding.mangaReaderPager.currentItem < maxChapterPage - 1) {
//if the current images zoomed in, go back to normal before going to next page
if (imageAdapter?.isZoomed() == true) {
@@ -908,10 +859,9 @@ class MangaReaderActivity : AppCompatActivity() {
}
}
binding.mangaReaderSlider.layoutDirection =
if (directionRLBT)
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP)
View.LAYOUT_DIRECTION_RTL
else
View.LAYOUT_DIRECTION_LTR
else View.LAYOUT_DIRECTION_LTR
shouldShow?.apply { isContVisible = !this }
if (isContVisible) {
isContVisible = false
@@ -919,7 +869,12 @@ class MangaReaderActivity : AppCompatActivity() {
isAnimating = true
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f)
.setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 0f, 128f)
ObjectAnimator.ofFloat(
binding.mangaReaderBottomLayout,
"translationY",
0f,
128f
)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f)
.apply { interpolator = overshoot;duration = controllerDuration;start() }
@@ -939,11 +894,7 @@ class MangaReaderActivity : AppCompatActivity() {
}
private var loading = false
fun updatePageNumber(pageNumber: Long) {
var page = pageNumber
if (directionPagedBT) {
page = maxChapterPage - pageNumber + 1
}
fun updatePageNumber(page: Long) {
if (currentChapterPage != page) {
currentChapterPage = page
PrefManager.setCustomVal("${media.id}_${chapter.number}", page)
@@ -959,6 +910,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.loadMangaChapterImages(
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
media.selected!!,
media.mainName(),
false
)
loading = false
@@ -991,7 +943,7 @@ class MangaReaderActivity : AppCompatActivity() {
PrefManager.setCustomVal("${media.id}_save_progress", true)
updateProgress(
media,
MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
.toString()
)
dialog.dismiss()
@@ -1009,11 +961,11 @@ class MangaReaderActivity : AppCompatActivity() {
if (!incognito && PrefManager.getCustomVal(
"${media.id}_save_progress",
true
) && if (media.isAdult) PrefManager.getVal(PrefName.UpdateForHReader) else true
) && if (media.isAdult) PrefManager.getVal<Boolean>(PrefName.UpdateForHReader) else true
)
updateProgress(
media,
MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
.toString()
)
runnable.run()
@@ -1108,4 +1060,4 @@ class MangaReaderActivity : AppCompatActivity() {
}
return true
}
}
}

View File

@@ -9,6 +9,7 @@ import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -20,7 +21,6 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
@@ -28,7 +28,6 @@ import ani.dantotsu.download.novel.NovelDownloaderService
import ani.dantotsu.download.novel.NovelServiceDataSingleton
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.novel.novelreader.NovelReaderActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.parsers.ShowResponse
@@ -91,27 +90,27 @@ class NovelReadFragment : Fragment(),
DownloadedType(
media.mainName(),
novel.name,
MediaType.NOVEL
DownloadedType.Type.NOVEL
)
)
) {
try {
val directory =
DownloadsManager.getSubDirectory(context?:currContext()!!, MediaType.NOVEL, false, novel.name)
val file = directory?.findFile(novel.name)
if (file?.exists() == false) return false
val fileUri = file?.uri ?: return false
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
} catch (e: Exception) {
Logger.log(e)
return false
val file = File(
context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.novelLocation}/${media.mainName()}/${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
}
@@ -123,7 +122,7 @@ class NovelReadFragment : Fragment(),
DownloadedType(
media.mainName(),
novel.name,
MediaType.NOVEL
DownloadedType.Type.NOVEL
)
)
}
@@ -134,9 +133,9 @@ class NovelReadFragment : Fragment(),
DownloadedType(
media.mainName(),
novel.name,
MediaType.NOVEL
DownloadedType.Type.NOVEL
)
) {}
)
}
private val downloadStatusReceiver = object : BroadcastReceiver() {

View File

@@ -1,10 +1,11 @@
package ani.dantotsu.media.novel
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemNovelResponseBinding
@@ -70,7 +71,8 @@ class NovelResponseAdapter(
}
binding.itemEpisodeDesc2.text = novel.extra?.get("1") ?: ""
val desc = novel.extra?.get("2")
binding.itemEpisodeDesc.isVisible = !desc.isNullOrBlank()
binding.itemEpisodeDesc.visibility =
if (desc != null && desc.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = desc ?: ""
binding.root.setOnClickListener {

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media.novel
import android.annotation.SuppressLint
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
@@ -30,6 +31,7 @@ class UrlAdapter(
)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
val binding = holder.binding
val url = urls[position]

View File

@@ -59,8 +59,7 @@ import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.util.Timer
import java.util.TimerTask
import java.util.*
import kotlin.math.min
import kotlin.properties.Delegates

View File

@@ -1,10 +1,12 @@
package ani.dantotsu.media.user
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
@@ -15,7 +17,7 @@ import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.hideSystemBarsExtendView
import ani.dantotsu.navBarHeight
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
@@ -31,6 +33,7 @@ class ListActivity : AppCompatActivity() {
private val scope = lifecycleScope
private var selectedTabIdx = 0
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -40,6 +43,13 @@ class ListActivity : AppCompatActivity() {
val typedValue = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
val primaryColor = typedValue.data
val typedValue2 = TypedValue()
theme.resolveAttribute(
com.google.android.material.R.attr.colorOnBackground,
typedValue2,
true
)
val titleTextColor = typedValue2.data
val typedValue3 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
val primaryTextColor = typedValue3.data
@@ -62,7 +72,10 @@ class ListActivity : AppCompatActivity() {
} else {
binding.root.fitsSystemWindows = false
requestWindowFeature(Window.FEATURE_NO_TITLE)
hideSystemBarsExtendView()
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
@@ -70,8 +83,8 @@ class ListActivity : AppCompatActivity() {
setContentView(binding.root)
val anime = intent.getBooleanExtra("anime", true)
binding.listTitle.text = getString(R.string.user_list, intent.getStringExtra("username"),
if (anime) getString(R.string.anime) else getString(R.string.manga))
binding.listTitle.text =
intent.getStringExtra("username") + "'s " + (if (anime) "Anime" else "Manga") + " List"
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
this@ListActivity.selectedTabIdx = tab?.position ?: 0
@@ -176,6 +189,7 @@ class ListActivity : AppCompatActivity() {
//get the current tab
val currentTab =
binding.listTabLayout.getTabAt(binding.listTabLayout.selectedTabPosition)
val currentViewePager = binding.listViewPager.getChildAt(0)
val currentFragment =
supportFragmentManager.findFragmentByTag("f" + currentTab?.position.toString()) as? ListFragment
currentFragment?.randomOptionClick()

View File

@@ -15,7 +15,7 @@ import java.util.concurrent.TimeUnit
class AlarmManagerScheduler(private val context: Context) : TaskScheduler {
override fun scheduleRepeatingTask(taskType: TaskType, interval: Long) {
if (interval * 1000 < TimeUnit.MINUTES.toMillis(15)) {
if (interval < TimeUnit.MINUTES.toMillis(15)) {
cancelTask(taskType)
return
}

View File

@@ -31,28 +31,6 @@ interface TaskScheduler {
}
}
fun scheduleSingleWork(context: Context) {
val workManager = androidx.work.WorkManager.getInstance(context)
workManager.enqueueUniqueWork(
CommentNotificationWorker.WORK_NAME,
androidx.work.ExistingWorkPolicy.REPLACE,
androidx.work.OneTimeWorkRequest.Builder(CommentNotificationWorker::class.java)
.build()
)
workManager.enqueueUniqueWork(
AnilistNotificationWorker.WORK_NAME,
androidx.work.ExistingWorkPolicy.REPLACE,
androidx.work.OneTimeWorkRequest.Builder(AnilistNotificationWorker::class.java)
.build()
)
workManager.enqueueUniqueWork(
SubscriptionNotificationWorker.WORK_NAME,
androidx.work.ExistingWorkPolicy.REPLACE,
androidx.work.OneTimeWorkRequest.Builder(SubscriptionNotificationWorker::class.java)
.build()
)
}
companion object {
fun create(context: Context, useAlarmManager: Boolean): TaskScheduler {
return if (useAlarmManager) {

View File

@@ -10,7 +10,7 @@ import ani.dantotsu.notifications.subscription.SubscriptionNotificationWorker
class WorkManagerScheduler(private val context: Context) : TaskScheduler {
override fun scheduleRepeatingTask(taskType: TaskType, interval: Long) {
if (interval * 1000 < PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS) {
if (interval < PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS) {
cancelTask(taskType)
return
}

View File

@@ -10,11 +10,6 @@ class AnilistNotificationWorker(appContext: Context, workerParams: WorkerParamet
override suspend fun doWork(): Result {
Logger.log("AnilistNotificationWorker: doWork")
if (System.currentTimeMillis() - lastCheck < 60000) {
Logger.log("AnilistNotificationWorker: doWork skipped")
return Result.success()
}
lastCheck = System.currentTimeMillis()
return if (AnilistNotificationTask().execute(applicationContext)) {
Result.success()
} else {
@@ -26,6 +21,5 @@ class AnilistNotificationWorker(appContext: Context, workerParams: WorkerParamet
companion object {
val checkIntervals = arrayOf(0L, 30, 60, 120, 240, 360, 720, 1440)
const val WORK_NAME = "ani.dantotsu.notifications.anilist.AnilistNotificationWorker"
private var lastCheck = 0L
}
}

View File

@@ -46,18 +46,19 @@ class CommentNotificationTask : Task {
)
notifications =
notifications?.filter { !it.type.isGlobal() || it.notificationId > recentGlobal }
notifications?.filter { it.type != 3 || it.notificationId > recentGlobal }
?.toMutableList()
val newRecentGlobal =
notifications?.filter { it.type.isGlobal() }?.maxOfOrNull { it.notificationId }
notifications?.filter { it.type == 3 }?.maxOfOrNull { it.notificationId }
if (newRecentGlobal != null) {
PrefManager.setVal(PrefName.RecentGlobalNotification, newRecentGlobal)
}
if (notifications.isNullOrEmpty()) return@withContext
PrefManager.setVal(
PrefName.UnreadCommentNotifications,
PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications) + (notifications.size)
PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications) + (notifications.size
?: 0)
)
notifications.forEach {
@@ -292,7 +293,6 @@ class CommentNotificationTask : Task {
return notification
}
@Suppress("unused")
private fun getBitmapFromVectorDrawable(context: Context, drawableId: Int): Bitmap? {
val drawable = ContextCompat.getDrawable(context, drawableId) ?: return null
val bitmap = Bitmap.createBitmap(
@@ -313,6 +313,4 @@ class CommentNotificationTask : Task {
null
}
}
private fun Int?.isGlobal() = this == 3 || this == 420
}

View File

@@ -11,11 +11,6 @@ class CommentNotificationWorker(appContext: Context, workerParams: WorkerParamet
CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
Logger.log("CommentNotificationWorker: doWork")
if (System.currentTimeMillis() - lastCheck < 60000) {
Logger.log("CommentNotificationWorker: doWork skipped")
return Result.success()
}
lastCheck = System.currentTimeMillis()
return if (CommentNotificationTask().execute(applicationContext)) {
Result.success()
} else {
@@ -35,6 +30,5 @@ class CommentNotificationWorker(appContext: Context, workerParams: WorkerParamet
companion object {
val checkIntervals = arrayOf(0L, 480, 720, 1440)
const val WORK_NAME = "ani.dantotsu.notifications.comment.CommentNotificationWorker"
private var lastCheck = 0L
}
}

View File

@@ -3,11 +3,13 @@ package ani.dantotsu.notifications.subscription
import ani.dantotsu.R
import ani.dantotsu.currContext
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.Selected
import ani.dantotsu.media.manga.MangaNameAdapter
import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.Episode
import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaChapter
import ani.dantotsu.parsers.MangaParser
import ani.dantotsu.parsers.MangaSources
@@ -103,7 +105,7 @@ class SubscriptionHelper {
}
return chp?.apply {
selected.latest = MediaNameAdapter.findChapterNumber(number) ?: 0f
selected.latest = MangaNameAdapter.findChapterNumber(number) ?: 0f
saveSelected(id, selected)
}
}

View File

@@ -185,7 +185,7 @@ class SubscriptionNotificationTask : Task {
size: Int
): NotificationCompat.Builder {
return NotificationCompat.Builder(context, CHANNEL_SUBSCRIPTION_CHECK_PROGRESS)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(context.getString(R.string.checking_subscriptions_title))
.setProgress(size, 0, false)

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.notifications.subscription
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import ani.dantotsu.notifications.anilist.AnilistNotificationTask
import ani.dantotsu.util.Logger
class SubscriptionNotificationWorker(appContext: Context, workerParams: WorkerParameters) :
@@ -10,12 +11,7 @@ class SubscriptionNotificationWorker(appContext: Context, workerParams: WorkerPa
override suspend fun doWork(): Result {
Logger.log("SubscriptionNotificationWorker: doWork")
if (System.currentTimeMillis() - lastCheck < 60000) {
Logger.log("SubscriptionNotificationWorker: doWork skipped")
return Result.success()
}
lastCheck = System.currentTimeMillis()
return if (SubscriptionNotificationTask().execute(applicationContext)) {
return if (AnilistNotificationTask().execute(applicationContext)) {
Result.success()
} else {
Logger.log("SubscriptionNotificationWorker: doWork failed")
@@ -27,6 +23,5 @@ class SubscriptionNotificationWorker(appContext: Context, workerParams: WorkerPa
val checkIntervals = arrayOf(0L, 480, 720, 1440)
const val WORK_NAME =
"ani.dantotsu.notifications.subscription.SubscriptionNotificationWorker"
private var lastCheck = 0L
}
}

View File

@@ -11,7 +11,7 @@ class AndroidBug5497Workaround private constructor(activity: Activity, private v
private val frameLayoutParams: FrameLayout.LayoutParams
init {
val content: FrameLayout = activity.findViewById(android.R.id.content)
val content = activity.findViewById(android.R.id.content) as FrameLayout
mChildOfContent = content.getChildAt(0)
mChildOfContent.viewTreeObserver.addOnGlobalLayoutListener { possiblyResizeChildOfContent() }
frameLayoutParams = mChildOfContent.layoutParams as FrameLayout.LayoutParams
@@ -42,15 +42,9 @@ class AndroidBug5497Workaround private constructor(activity: Activity, private v
return r.bottom
}
/**
* Fixes windowSoftInputMode adjustResize when used with setDecorFitsSystemWindows(false)
*
* @see <a href="https://issuetracker.google.com/issues/36911528">adjustResize breaks when activity is fullscreen </a>
*/
companion object {
/**
* Called on an Activity after the content view has been set.
*/
// For more information, see https://issuetracker.google.com/issues/36911528
// To use this class, simply invoke assistActivity() on an Activity that already has its content view set.
fun assistActivity(activity: Activity, callback: (Boolean) -> Unit) {
AndroidBug5497Workaround(activity, callback)
}

View File

@@ -36,8 +36,16 @@ object Download {
}
private fun getDownloadDir(context: Context): File {
val direct = File("storage/emulated/0/${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/")
if (!direct.exists()) direct.mkdirs()
val direct: File
if (PrefManager.getVal(PrefName.SdDl)) {
val arrayOfFiles = ContextCompat.getExternalFilesDirs(context, null)
val parentDirectory = arrayOfFiles[1].toString()
direct = File(parentDirectory)
if (!direct.exists()) direct.mkdirs()
} else {
direct = File("storage/emulated/0/${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/")
if (!direct.exists()) direct.mkdirs()
}
return direct
}
@@ -88,10 +96,52 @@ object Download {
when (PrefManager.getVal(PrefName.DownloadManager) as Int) {
1 -> oneDM(context, file, notif ?: fileName)
2 -> adm(context, file, fileName, folder)
else -> oneDM(context, file, notif ?: fileName)
else -> defaultDownload(context, file, fileName, folder, notif ?: fileName)
}
}
private fun defaultDownload(
context: Context,
file: FileUrl,
fileName: String,
folder: String,
notif: String
) {
val manager =
context.getSystemService(AppCompatActivity.DOWNLOAD_SERVICE) as DownloadManager
val request: DownloadManager.Request = DownloadManager.Request(Uri.parse(file.url))
file.headers.forEach {
request.addRequestHeader(it.key, it.value)
}
CoroutineScope(Dispatchers.IO).launch {
try {
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val arrayOfFiles = ContextCompat.getExternalFilesDirs(context, null)
if (PrefManager.getVal(PrefName.SdDl) && arrayOfFiles.size > 1 && arrayOfFiles[0] != null && arrayOfFiles[1] != null) {
val parentDirectory = arrayOfFiles[1].toString() + folder
val direct = File(parentDirectory)
if (!direct.exists()) direct.mkdirs()
request.setDestinationUri(Uri.fromFile(File("$parentDirectory$fileName")))
} else {
val direct = File(Environment.DIRECTORY_DOWNLOADS + "/Dantotsu$folder")
if (!direct.exists()) direct.mkdirs()
request.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
"/Dantotsu$folder$fileName"
)
}
request.setTitle(notif)
manager.enqueue(request)
toast(currContext()?.getString(R.string.started_downloading, notif))
} catch (e: SecurityException) {
toast(currContext()?.getString(R.string.permission_required))
} catch (e: Exception) {
toast(e.toString())
}
}
}
private fun oneDM(context: Context, file: FileUrl, notif: String) {
val appName =
if (isPackageInstalled("idm.internet.download.manager.plus", context.packageManager)) {

View File

@@ -12,8 +12,9 @@ import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetImageBinding
import ani.dantotsu.downloadsPermission
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmapOld
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.loadBitmap_old
import ani.dantotsu.media.manga.mangareader.BaseImageAdapter.Companion.mergeBitmap
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.saveImageToDownloads
@@ -21,7 +22,6 @@ import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.shareImage
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.StoragePermissions.Companion.downloadsPermission
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.ImageSource
import kotlinx.coroutines.launch
@@ -84,9 +84,9 @@ class ImageViewDialog : BottomSheetDialogFragment() {
viewLifecycleOwner.lifecycleScope.launch {
val binding = _binding ?: return@launch
var bitmap = context.loadBitmapOld(image, trans1 ?: listOf())
var bitmap = context.loadBitmap_old(image, trans1 ?: listOf())
var bitmap2 =
if (image2 != null) context.loadBitmapOld(image2, trans2 ?: listOf()) else null
if (image2 != null) context.loadBitmap_old(image2, trans2 ?: listOf()) else null
if (bitmap == null) {
bitmap = context.loadBitmap(image, trans1 ?: listOf())
bitmap2 =

View File

@@ -1,11 +1,9 @@
package ani.dantotsu.others
import android.content.Context
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.util.TypedValue
import androidx.appcompat.widget.AppCompatTextView
import ani.dantotsu.R
@@ -56,14 +54,14 @@ class OutlineTextView : AppCompatTextView {
setStrokeWidth(strokeWidth)
}
private val Float.toPx get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics
)
private fun setStrokeWidth(width: Float) {
strokeWidth = width.toPx
strokeWidth = width.toPx(context)
}
private fun Float.toPx(context: Context) =
(this * context.resources.displayMetrics.scaledDensity + 0.5F)
override fun invalidate() {
if (isDrawing) return
super.invalidate()

View File

@@ -13,7 +13,6 @@ class Xpandable @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {
var expanded: Boolean = false
private var listener: OnChangeListener? = null
init {
context.withStyledAttributes(attrs, R.styleable.Xpandable) {
@@ -38,6 +37,7 @@ class Xpandable @JvmOverloads constructor(
super.onAttachedToWindow()
}
private fun hideAll() {
children.forEach {
if (it != getChildAt(0)) {
@@ -48,10 +48,8 @@ class Xpandable @JvmOverloads constructor(
it.visibility = GONE
}, 300)
}
}
postDelayed({
listener?.onRetract()
}, 300)
}
private fun showAll() {
@@ -63,19 +61,6 @@ class Xpandable @JvmOverloads constructor(
ObjectAnimator.ofFloat(it, "alpha", 0f, 1f).setDuration(200).start()
}
}
postDelayed({
listener?.onExpand()
}, 300)
}
@Suppress("unused")
fun setOnChangeListener(listener: OnChangeListener) {
this.listener = listener
}
interface OnChangeListener {
fun onExpand()
fun onRetract()
}
}

View File

@@ -12,19 +12,19 @@ import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.R
import ani.dantotsu.themes.ThemeManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class CookieCatcher : AppCompatActivity() {
@SuppressLint("SetJavaScriptEnabled")
@Suppress("UNCHECKED_CAST")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
//get url from intent
val url = intent.getStringExtra("url") ?: getString(R.string.cursed_yt)
val headers: Map<String, String> = intent.getSerializableExtraCompat("headers") as? Map<String, String> ?: emptyMap()
val url = intent.getStringExtra("url") ?: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
val headers: Map<String, String> = intent.getSerializableExtra("headers") as? Map<String, String> ?: emptyMap()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = Application.getProcessName()

View File

@@ -90,12 +90,14 @@ abstract class AnimeParser : BaseParser() {
domain = domain.substring(4)
}
return when (domain) {
val extractor: VideoExtractor? = when (domain) {
else -> {
println("$name : No extractor found for: $domain | ${server.embed.url}")
null
}
}
return extractor
}
/**

View File

@@ -1,13 +1,21 @@
package ani.dantotsu.parsers
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import ani.dantotsu.FileUrl
import ani.dantotsu.currContext
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.util.Logger
import ani.dantotsu.media.anime.AnimeNameAdapter
import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimesPage
@@ -27,10 +35,12 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.lang.awaitSingle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
@@ -38,10 +48,20 @@ import kotlinx.coroutines.withContext
import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.io.UnsupportedEncodingException
import java.net.MalformedURLException
import java.net.URL
import java.net.URLDecoder
import java.util.regex.Pattern
class AniyomiAdapter {
fun aniyomiToAnimeParser(extension: AnimeExtension.Installed): DynamicAnimeParser {
return DynamicAnimeParser(extension)
}
}
class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
val extension: AnimeExtension.Installed
@@ -73,27 +93,27 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
configurableSource.getPreferenceKey(),
Context.MODE_PRIVATE
)
sharedPreferences.all.filterValues { MediaNameAdapter.getSubDub(it.toString()) != MediaNameAdapter.SubDubType.NULL }
sharedPreferences.all.filterValues { AnimeNameAdapter.getSubDub(it.toString()) != AnimeNameAdapter.Companion.SubDubType.NULL }
.forEach { value ->
return when (MediaNameAdapter.getSubDub(value.value.toString())) {
MediaNameAdapter.SubDubType.SUB -> false
MediaNameAdapter.SubDubType.DUB -> true
MediaNameAdapter.SubDubType.NULL -> false
return when (AnimeNameAdapter.getSubDub(value.value.toString())) {
AnimeNameAdapter.Companion.SubDubType.SUB -> false
AnimeNameAdapter.Companion.SubDubType.DUB -> true
AnimeNameAdapter.Companion.SubDubType.NULL -> false
}
}
}
return false
}
private fun setDub(setDub: Boolean) {
fun setDub(setDub: Boolean) {
if (sourceLanguage >= extension.sources.size) {
sourceLanguage = extension.sources.size - 1
}
val configurableSource = extension.sources[sourceLanguage] as? ConfigurableAnimeSource
?: return
val type = when (setDub) {
true -> MediaNameAdapter.SubDubType.DUB
false -> MediaNameAdapter.SubDubType.SUB
true -> AnimeNameAdapter.Companion.SubDubType.DUB
false -> AnimeNameAdapter.Companion.SubDubType.SUB
}
currContext()?.let { context ->
val sharedPreferences =
@@ -101,9 +121,9 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
configurableSource.getPreferenceKey(),
Context.MODE_PRIVATE
)
sharedPreferences.all.filterValues { MediaNameAdapter.getSubDub(it.toString()) != MediaNameAdapter.SubDubType.NULL }
sharedPreferences.all.filterValues { AnimeNameAdapter.getSubDub(it.toString()) != AnimeNameAdapter.Companion.SubDubType.NULL }
.forEach { value ->
val setValue = MediaNameAdapter.setSubDub(value.value.toString(), type)
val setValue = AnimeNameAdapter.setSubDub(value.value.toString(), type)
if (setValue != null) {
sharedPreferences.edit().putString(value.key, setValue).apply()
}
@@ -122,9 +142,9 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
Context.MODE_PRIVATE
)
sharedPreferences.all.filterValues {
MediaNameAdapter.setSubDub(
AnimeNameAdapter.setSubDub(
it.toString(),
MediaNameAdapter.SubDubType.NULL
AnimeNameAdapter.Companion.SubDubType.NULL
) != null
}
.forEach { _ -> return true }
@@ -150,7 +170,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
val sortedEpisodes = if (res[0].episode_number == -1f) {
// Find the number in the string and sort by that number
val sortedByStringNumber = res.sortedBy {
val matchResult = MediaNameAdapter.findEpisodeNumber(it.name)
val matchResult = AnimeNameAdapter.findEpisodeNumber(it.name)
val number = matchResult ?: Float.MAX_VALUE
it.episode_number = number // Store the found number in episode_number
number
@@ -171,13 +191,13 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
var episodeCounter = 1f
// Group by season, sort within each season, and then renumber while keeping episode number 0 as is
val seasonGroups =
res.groupBy { MediaNameAdapter.findSeasonNumber(it.name) ?: 0 }
seasonGroups.keys.sortedBy { it }
res.groupBy { AnimeNameAdapter.findSeasonNumber(it.name) ?: 0 }
seasonGroups.keys.sortedBy { it.toInt() }
.flatMap { season ->
seasonGroups[season]?.sortedBy { it.episode_number }?.map { episode ->
if (episode.episode_number != 0f) { // Skip renumbering for episode number 0
val potentialNumber =
MediaNameAdapter.findEpisodeNumber(episode.name)
AnimeNameAdapter.findEpisodeNumber(episode.name)
if (potentialNumber != null) {
episode.episode_number = potentialNumber
} else {
@@ -189,7 +209,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
} ?: emptyList()
}
}
return sortedEpisodes.map { sEpisodeToEpisode(it) }
return sortedEpisodes.map { SEpisodeToEpisode(it) }
} catch (e: Exception) {
Logger.log("Exception: $e")
}
@@ -224,7 +244,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
return try {
val videos = source.getVideoList(sEpisode)
videos.map { videoToVideoServer(it) }
videos.map { VideoToVideoServer(it) }
} catch (e: Exception) {
Logger.log("Exception occurred: ${e.message}")
emptyList()
@@ -267,13 +287,16 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
val name = sAnime.title
val link = sAnime.url
val coverUrl = sAnime.thumbnail_url ?: ""
val otherNames = emptyList<String>() // Populate as needed
val total = 1
val extra: Map<String, String>? = null // Populate as needed
// Create a new ShowResponse
ShowResponse(name, link, coverUrl, sAnime)
}
}
private fun sEpisodeToEpisode(sEpisode: SEpisode): Episode {
private fun SEpisodeToEpisode(sEpisode: SEpisode): Episode {
//if the float episode number is a whole number, convert it to an int
val episodeNumberInt =
if (sEpisode.episode_number % 1 == 0f) {
@@ -301,7 +324,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
)
}
private fun videoToVideoServer(video: Video): VideoServer {
private fun VideoToVideoServer(video: Video): VideoServer {
return VideoServer(
video.quality,
video.url,
@@ -312,7 +335,7 @@ class DynamicAnimeParser(extension: AnimeExtension.Installed) : AnimeParser() {
}
class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
private val mangaCache = Injekt.get<MangaCache>()
val mangaCache = Injekt.get<MangaCache>()
val extension: MangaExtension.Installed
var sourceLanguage = 0
@@ -340,7 +363,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
return try {
val res = source.getChapterList(sManga)
val reversedRes = res.reversed()
val chapterList = reversedRes.map { sChapterToMangaChapter(it) }
val chapterList = reversedRes.map { SChapterToMangaChapter(it) }
Logger.log("chapterList size: ${chapterList.size}")
Logger.log("chapterList: ${chapterList[1].title}")
Logger.log("chapterList: ${chapterList[1].description}")
@@ -359,7 +382,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
sourceLanguage = 0
extension.sources[sourceLanguage]
} as? HttpSource ?: return emptyList()
val imageDataList: MutableList<ImageData> = mutableListOf()
var imageDataList: List<ImageData> = listOf()
val ret = coroutineScope {
try {
Logger.log("source.name " + source.name)
@@ -387,7 +410,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
return ret
}
suspend fun imageList(sChapter: SChapter): List<ImageData> {
suspend fun imageList(chapterLink: String, sChapter: SChapter): List<ImageData> {
val source = try {
extension.sources[sourceLanguage]
} catch (e: Exception) {
@@ -420,6 +443,121 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
}
}
suspend fun fetchAndProcessImage(
page: Page,
httpSource: HttpSource,
context: Context
): Bitmap? {
return withContext(Dispatchers.IO) {
try {
// Fetch the image
val response = httpSource.getImage(page)
Logger.log("Response: ${response.code}")
Logger.log("Response: ${response.message}")
// Convert the Response to an InputStream
val inputStream = response.body.byteStream()
// Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream.close()
ani.dantotsu.media.manga.saveImage(
bitmap,
context.contentResolver,
page.imageUrl!!,
Bitmap.CompressFormat.JPEG,
100
)
return@withContext bitmap
} catch (e: Exception) {
// Handle any exceptions
Logger.log("An error occurred: ${e.message}")
return@withContext null
}
}
}
fun fetchAndSaveImage(page: Page, httpSource: HttpSource, contentResolver: ContentResolver) {
CoroutineScope(Dispatchers.IO).launch {
try {
// Fetch the image
val response = httpSource.getImage(page)
// Convert the Response to an InputStream
val inputStream = response.body.byteStream()
// Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream)
withContext(Dispatchers.IO) {
// Save the Bitmap using MediaStore API
saveImage(
bitmap,
contentResolver,
"image_${System.currentTimeMillis()}.jpg",
Bitmap.CompressFormat.JPEG,
100
)
}
inputStream.close()
} catch (e: Exception) {
// Handle any exceptions
Logger.log("An error occurred: ${e.message}")
}
}
}
fun saveImage(
bitmap: Bitmap,
contentResolver: ContentResolver,
filename: String,
format: Bitmap.CompressFormat,
quality: Int
) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
put(
MediaStore.MediaColumns.RELATIVE_PATH,
"${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Anime"
)
}
val uri: Uri? = contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
uri?.let {
contentResolver.openOutputStream(it)?.use { os ->
bitmap.compress(format, quality, os)
}
}
} else {
val directory =
File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime")
if (!directory.exists()) {
directory.mkdirs()
}
val file = File(directory, filename)
FileOutputStream(file).use { outputStream ->
bitmap.compress(format, quality, outputStream)
}
}
} catch (e: Exception) {
// Handle exception here
Logger.log("Exception while saving image: ${e.message}")
}
}
override suspend fun search(query: String): List<ShowResponse> {
val source = try {
extension.sources[sourceLanguage]
@@ -451,6 +589,9 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val name = sManga.title
val link = sManga.url
val coverUrl = sManga.thumbnail_url ?: ""
val otherNames = emptyList<String>() // Populate as needed
val total = 1
val extra: Map<String, String>? = null // Populate as needed
// Create a new ShowResponse
ShowResponse(name, link, coverUrl, sManga)
@@ -459,10 +600,12 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
private fun pageToMangaImage(page: Page): MangaImage {
var headersMap = mapOf<String, String>()
var urlWithoutHeaders = ""
var url = ""
page.imageUrl?.let {
val splitUrl = it.split("&")
urlWithoutHeaders = splitUrl.getOrNull(0) ?: ""
url = it
headersMap = splitUrl.mapNotNull { part ->
@@ -489,7 +632,7 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
}
private fun sChapterToMangaChapter(sChapter: SChapter): MangaChapter {
private fun SChapterToMangaChapter(sChapter: SChapter): MangaChapter {
return MangaChapter(
sChapter.name,
sChapter.url,
@@ -500,15 +643,41 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
sChapter.date_upload
)
}
fun parseChapterTitle(title: String): Triple<String?, String?, String> {
val volumePattern =
Pattern.compile("(?:vol\\.?|v|volume\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
val chapterPattern =
Pattern.compile("(?:ch\\.?|chapter\\s?)(\\d+)", Pattern.CASE_INSENSITIVE)
val volumeMatcher = volumePattern.matcher(title)
val chapterMatcher = chapterPattern.matcher(title)
val volumeNumber = if (volumeMatcher.find()) volumeMatcher.group(1) else null
val chapterNumber = if (chapterMatcher.find()) chapterMatcher.group(1) else null
var remainingTitle = title
if (volumeNumber != null) {
remainingTitle =
volumeMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
}
if (chapterNumber != null) {
remainingTitle =
chapterMatcher.group(0)?.let { remainingTitle.replace(it, "") }.toString()
}
return Triple(volumeNumber, chapterNumber, remainingTitle.trim())
}
}
class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtractor() {
class VideoServerPassthrough(val videoServer: VideoServer) : VideoExtractor() {
override val server: VideoServer
get() = videoServer
override suspend fun extract(): VideoContainer {
val vidList = listOfNotNull(videoServer.video?.let { aniVideoToSaiVideo(it) })
val subList = videoServer.video?.subtitleTracks?.map { trackToSubtitle(it) } ?: emptyList()
val vidList = listOfNotNull(videoServer.video?.let { AniVideoToSaiVideo(it) })
val subList = videoServer.video?.subtitleTracks?.map { TrackToSubtitle(it) } ?: emptyList()
return if (vidList.isNotEmpty()) {
VideoContainer(vidList, subList)
@@ -517,51 +686,42 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
}
}
private fun aniVideoToSaiVideo(aniVideo: Video): ani.dantotsu.parsers.Video {
private fun AniVideoToSaiVideo(aniVideo: Video): ani.dantotsu.parsers.Video {
// Find the number value from the .quality string
val number = Regex("""\d+""").find(aniVideo.quality)?.value?.toInt() ?: 0
// Check for null video URL
val videoUrl = aniVideo.videoUrl ?: throw Exception("Video URL is null")
var format: VideoType?
val urlObj = URL(videoUrl)
val path = urlObj.path
val query = urlObj.query
try {
val urlObj = URL(videoUrl)
val path = urlObj.path
val query = urlObj.query
var format = getVideoType(path)
format = getVideoType(path)
if (format == null && query != null) {
val queryPairs: List<Pair<String, String>> = query.split("&").map {
val idx = it.indexOf("=")
val key = URLDecoder.decode(it.substring(0, idx), "UTF-8")
val value = URLDecoder.decode(it.substring(idx + 1), "UTF-8")
Pair(key, value)
}
// Assume the file is named under the "file" query parameter
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
format = getVideoType(fileName)
// this solves a problem no one has, so I'm commenting it out for now
//if (format == null) {
// val networkHelper = Injekt.get<NetworkHelper>()
// format = headRequest(videoUrl, networkHelper)
//}
if (format == null && query != null) {
val queryPairs: List<Pair<String, String>> = query.split("&").map {
val idx = it.indexOf("=")
val key = URLDecoder.decode(it.substring(0, idx), "UTF-8")
val value = URLDecoder.decode(it.substring(idx + 1), "UTF-8")
Pair(key, value)
}
// If the format is still undetermined, log an error
if (format == null) {
Logger.log("Unknown video format: $videoUrl")
format = VideoType.CONTAINER
}
} catch (malformed: MalformedURLException) {
if (videoUrl.startsWith("magnet:"))
format = VideoType.CONTAINER
else
throw malformed
// Assume the file is named under the "file" query parameter
val fileName = queryPairs.find { it.first == "file" }?.second ?: ""
format = getVideoType(fileName)
// this solves a problem no one has, so I'm commenting it out for now
//if (format == null) {
// val networkHelper = Injekt.get<NetworkHelper>()
// format = headRequest(videoUrl, networkHelper)
//}
}
// If the format is still undetermined, log an error
if (format == null) {
Logger.log("Unknown video format: $videoUrl")
format = VideoType.CONTAINER
}
val headersMap: Map<String, String> =
aniVideo.headers?.toMultimap()?.mapValues { it.value.joinToString() } ?: mapOf()
@@ -569,7 +729,7 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
return Video(
number,
format!!,
format,
FileUrl(videoUrl, headersMap),
if (aniVideo.totalContentLength == 0L) null else aniVideo.bytesDownloaded.toDouble()
)
@@ -590,7 +750,6 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
return type
}
@Suppress("unused")
private fun headRequest(fileName: String, networkHelper: NetworkHelper): VideoType? {
return try {
Logger.log("attempting head request for $fileName")
@@ -630,9 +789,9 @@ class VideoServerPassthrough(private val videoServer: VideoServer) : VideoExtrac
}
private fun trackToSubtitle(track: Track): Subtitle {
private fun TrackToSubtitle(track: Track): Subtitle {
//use Dispatchers.IO to make a HTTP request to determine the subtitle type
var type: SubtitleType?
var type: SubtitleType? = null
runBlocking {
type = findSubtitleType(track.url)
}

View File

@@ -1,11 +1,11 @@
package ani.dantotsu.parsers
import ani.dantotsu.Lazier
import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.source.model.SManga

View File

@@ -1,7 +1,7 @@
package ani.dantotsu.parsers
import ani.dantotsu.FileUrl
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.manga.MangaNameAdapter
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
@@ -33,9 +33,9 @@ abstract class MangaParser : BaseParser() {
): MangaChapter? {
val chapter = loadChapters(mangaLink, extra, sManga)
val max = chapter
.maxByOrNull { MediaNameAdapter.findChapterNumber(it.number) ?: 0f }
.maxByOrNull { MangaNameAdapter.findChapterNumber(it.number) ?: 0f }
return max
?.takeIf { latest < (MediaNameAdapter.findChapterNumber(it.number) ?: 0.001f) }
?.takeIf { latest < (MangaNameAdapter.findChapterNumber(it.number) ?: 0.001f) }
}
/**

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