Compare commits

..

8 Commits

Author SHA1 Message Date
rebel onion
61406ebf2c Merge branch 'dev' into custom-download-location 2024-04-04 04:03:34 -05:00
rebelonion
33ed647832 feat: changing the downloads dir 2024-04-04 02:21:43 -05:00
rebelonion
79113cb04a fix: notification incrementing 2024-04-04 01:15:18 -05:00
rebelonion
93172153b7 chore: clean manifest 2024-04-04 01:03:26 -05:00
rebelonion
9e6f71feb2 feat: novel to new system | load freezing 2024-04-04 01:02:37 -05:00
rebelonion
bf73bbb98b fix: offline page for new download system 2024-04-03 23:53:03 -05:00
rebelonion
fb903c56aa fix: send headers to ffmpeg
ffmpeg can be a real bitch to work with
2024-04-02 16:21:48 -05:00
rebelonion
bcbcf1a829 feat: custom downloader (novel broken) 2024-04-02 08:04:24 -05:00
315 changed files with 6063 additions and 10047 deletions

View File

@@ -48,11 +48,9 @@ jobs:
echo "COMMIT_LOG=${COMMIT_LOGS}" >> $GITHUB_ENV
# Debugging: Print the variable to check its content
echo "$COMMIT_LOGS"
echo "$COMMIT_LOGS" > commit_log.txt
shell: /usr/bin/bash -e {0}
env:
CI: true
continue-on-error: true
- name: Save Current SHA for Next Run
run: echo ${{ github.sha }} > last_sha.txt
@@ -77,7 +75,7 @@ jobs:
- name: List files in the directory
run: ls -l
- name: Make gradlew executable
run: chmod +x ./gradlew
@@ -85,11 +83,9 @@ jobs:
run: ./gradlew assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v4.3.1
with:
name: Dantotsu
retention-days: 5
compression-level: 9
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
- name: Upload APK to Discord and Telegram
@@ -103,7 +99,7 @@ jobs:
if [ ${#commit_messages} -gt $max_length ]; then
commit_messages="${commit_messages:0:$max_length}... (truncated)"
fi
contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
contentbody=$( jq -nc --arg msg "Alpha-Build: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
#Telegram
@@ -117,13 +113,18 @@ jobs:
VERSION: ${{ env.VERSION }}
- name: Upload Current SHA as Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: last-sha
path: last_sha.txt
- name: Upload Commit log as Artifact
uses: actions/upload-artifact@v4
- name: Delete Old Pre-Releases
id: delete-pre-releases
uses: sgpublic/delete-release-action@master
with:
name: commit-log
path: commit_log.txt
pre-release-drop: true
pre-release-keep-count: 3
pre-release-drop-tag: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View File

@@ -31,6 +31,3 @@ output.json
#other
scripts/
#crowdin
crowdin.yml

View File

@@ -21,7 +21,14 @@ 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"
@@ -81,9 +88,9 @@ android {
dependencies {
// FireBase
googleImplementation platform('com.google.firebase:firebase-bom:32.8.1')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.6.2'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.4'
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
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.8.0'
@@ -111,7 +118,7 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer
ext.exo_version = '1.3.1'
ext.exo_version = '1.3.0'
implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
@@ -131,17 +138,17 @@ dependencies {
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'com.github.eltos:simpledialogfragments:v3.7'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:93972bc'
// Markwon
ext.markwon_version = '4.6.2'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:editor:$markwon_version"
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
implementation "io.noties.markwon:ext-tables:$markwon_version"
implementation "io.noties.markwon:ext-tasklist:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version"
implementation "io.noties.markwon:image-glide:$markwon_version"
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:editor:$markwon_version"
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
implementation "io.noties.markwon:ext-tables:$markwon_version"
implementation "io.noties.markwon:ext-tasklist:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version"
implementation "io.noties.markwon:image-glide:$markwon_version"
// Groupie
ext.groupie_version = '2.10.1'
@@ -151,6 +158,9 @@ dependencies {
// 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
implementation 'io.reactivex:rxjava:1.3.8'
implementation 'io.reactivex:rxandroid:1.2.1'

View File

@@ -5,12 +5,12 @@ import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.google.firebase.ktx.app
class FirebaseCrashlytics : CrashlyticsInterface {
override fun initialize(context: Context) {
FirebaseApp.initializeApp(context)
}
override fun logException(e: Throwable) {
FirebaseCrashlytics.getInstance().recordException(e)
}

View File

@@ -85,18 +85,13 @@ object AppUpdater {
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
MainScope().launch(Dispatchers.IO) {
try {
val apks =
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
.parsed<GithubResponse>().assets?.filter {
it.browserDownloadURL.endsWith(
".apk"
)
}
val apkToDownload = apks?.first()
apkToDownload?.browserDownloadURL.apply {
if (this != null) activity.downloadUpdate(version, this)
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
}
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
.parsed<GithubResponse>().assets?.find {
it.browserDownloadURL.endsWith("apk")
}?.browserDownloadURL.apply {
if (this != null) activity.downloadUpdate(version, this)
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
}
} catch (e: Exception) {
logError(e)
}
@@ -116,25 +111,24 @@ object AppUpdater {
}
private fun compareVersion(version: String): Boolean {
return when (BuildConfig.BUILD_TYPE) {
"debug" -> BuildConfig.VERSION_NAME != version
"alpha" -> false
else -> {
fun toDouble(list: List<String>): Double {
return list.mapIndexed { i: Int, s: String ->
when (i) {
0 -> s.toDouble() * 100
1 -> s.toDouble() * 10
2 -> s.toDouble()
else -> s.toDoubleOrNull() ?: 0.0
}
}.sum()
}
val new = toDouble(version.split("."))
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
new > curr
if (BuildConfig.DEBUG) {
return BuildConfig.VERSION_NAME != version
} else {
fun toDouble(list: List<String>): Double {
return list.mapIndexed { i: Int, s: String ->
when (i) {
0 -> s.toDouble() * 100
1 -> s.toDouble() * 10
2 -> s.toDouble()
else -> s.toDoubleOrNull() ?: 0.0
}
}.sum()
}
val new = toDouble(version.split("."))
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
return new > curr
}
}

View File

@@ -2,8 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="go.server.gojni" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
@@ -40,17 +38,6 @@
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
tools:ignore="ProtectedPermissions" />
<!-- ExoPlayer: Bluetooth Headsets -->
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- ExoPlayer: Bluetooth Headsets -->
<queries>
<package android:name="idm.internet.download.manager.plus" />
<package android:name="idm.internet.download.manager" />
@@ -92,7 +79,7 @@
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver
android:name=".widgets.statistics.ProfileStatsWidget"
android:exported="false">
@@ -136,33 +123,6 @@
<activity
android:name=".settings.SettingsActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAboutActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAccountActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAnimeActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsCommonActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsExtensionsActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAddonActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsMangaActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsNotificationActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsThemeActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.ExtensionsActivity"
android:parentActivityName=".MainActivity"
@@ -171,7 +131,7 @@
android:name=".widgets.statistics.ProfileStatsConfigure"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
@@ -434,11 +394,6 @@
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".addons.torrent.ServerService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="true" />
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"

View File

@@ -6,8 +6,6 @@ import android.content.Context
import android.os.Bundle
import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.aniyomi.anime.custom.AppModule
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
import ani.dantotsu.connections.comments.CommentsAPI
@@ -43,9 +41,6 @@ class App : MultiDexApplication() {
private lateinit var animeExtensionManager: AnimeExtensionManager
private lateinit var mangaExtensionManager: MangaExtensionManager
private lateinit var novelExtensionManager: NovelExtensionManager
private lateinit var torrentAddonManager: TorrentAddonManager
private lateinit var downloadAddonManager: DownloadAddonManager
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
MultiDex.install(this)
@@ -101,8 +96,6 @@ class App : MultiDexApplication() {
animeExtensionManager = Injekt.get()
mangaExtensionManager = Injekt.get()
novelExtensionManager = Injekt.get()
torrentAddonManager = Injekt.get()
downloadAddonManager = Injekt.get()
val animeScope = CoroutineScope(Dispatchers.Default)
animeScope.launch {
@@ -122,11 +115,6 @@ class App : MultiDexApplication() {
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
}
val addonScope = CoroutineScope(Dispatchers.Default)
addonScope.launch {
torrentAddonManager.init()
downloadAddonManager.init()
}
val commentsScope = CoroutineScope(Dispatchers.Default)
commentsScope.launch {
CommentsAPI.fetchAuthToken()

View File

@@ -16,6 +16,7 @@ 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
@@ -67,9 +68,12 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider
import androidx.core.math.MathUtils.clamp
@@ -88,7 +92,6 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.BuildConfig.APPLICATION_ID
import ani.dantotsu.connections.anilist.Genre
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.bakaupdates.MangaUpdates
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media
@@ -99,7 +102,6 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.util.CountUpTimer
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
@@ -132,12 +134,9 @@ import io.noties.markwon.html.TagHandlerNoOp
import io.noties.markwon.image.AsyncDrawable
import io.noties.markwon.image.glide.GlideImagesPlugin
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -185,10 +184,9 @@ fun currActivity(): Activity? {
var loadMedia: Int? = null
var loadIsMAL = false
val Int.toPx
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), getSystem().displayMetrics
).toInt()
val Int.toPx get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics
).toInt()
fun initActivity(a: Activity) {
val window = a.window
@@ -217,8 +215,7 @@ fun initActivity(a: Activity) {
window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0
&& a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
) {
&& a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
window.decorView.rootWindowInsets?.displayCutout?.apply {
if (boundingRects.size > 0) {
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
@@ -294,12 +291,7 @@ 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
)
setPadding(paddingLeft, paddingTop, paddingRight, navBarHeight + navBar.measuredHeight + overlayView.measuredHeight)
}
fun Activity.reloadActivity() {
@@ -309,19 +301,23 @@ fun Activity.reloadActivity() {
initActivity(this)
}
fun Activity.restartApp() {
fun Context.restartApp(view: View) {
val mainIntent = Intent.makeRestartActivityTask(
packageManager.getLaunchIntentForPackage(this.packageName)!!.component
)
val component =
ComponentName(this@restartApp.packageName, this@restartApp::class.qualifiedName!!)
try {
startActivity(Intent().setComponent(component))
} catch (e: Exception) {
startActivity(mainIntent)
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()
}
finishAndRemoveTask()
PrefManager.setCustomVal("reload", true)
}
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
@@ -468,7 +464,7 @@ class InputFilterMinMax(
}
class ZoomOutPageTransformer :
class ZoomOutPageTransformer() :
ViewPager2.PageTransformer {
override fun transformPage(view: View, position: Float) {
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
@@ -636,23 +632,6 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
}
}
fun ImageView.loadImage(file: FileUrl?, width: Int = 0, height: 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(width, height).into(this)
} else {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(width, height)
.into(this)
}
}
}
}
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
if (file?.exists() == true) {
tryWith {
@@ -974,8 +953,7 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
fun countDown(media: Media, view: ViewGroup) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null
&& (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()
) {
&& (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 =
@@ -1007,50 +985,6 @@ fun countDown(media: Media, view: ViewGroup) {
}
}
fun sinceWhen(media: Media, view: ViewGroup) {
if (media.status != "RELEASING" && media.status != "HIATUS") return
CoroutineScope(Dispatchers.IO).launch {
MangaUpdates().search(media.mangaName(), media.startDate)?.let {
val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
val timeSince = (System.currentTimeMillis() -
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
withContext(Dispatchers.Main) {
val v =
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
object : CountUpTimer(86400000) {
override fun onTick(second: Int) {
val a = second + timeSince
v.mediaCountdown.text = currActivity()?.getString(
R.string.time_format,
a / 86400,
a % 86400 / 3600,
a % 86400 % 3600 / 60,
a % 86400 % 3600 % 60
)
}
override fun onFinish() {
// The legend will never die.
}
}.start()
}
}
}
}
fun displayTimer(media: Media, view: ViewGroup) {
when {
media.anime != null -> countDown(media, view)
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
else -> {} // No timer yet
}
}
fun MutableMap<String, Genre>.checkId(id: Int): Boolean {
this.forEach {
if (it.value.id == id) {
@@ -1367,11 +1301,7 @@ fun blurImage(imageView: ImageView, banner: String?) {
if (!(context as Activity).isDestroyed) {
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
Glide.with(context as Context)
.load(
if (banner.startsWith("http")) GlideUrl(url) else if (banner.startsWith("content://")) Uri.parse(
url
) else File(url)
)
.load(GlideUrl(url))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
.into(imageView)
@@ -1457,4 +1387,4 @@ fun buildMarkwon(
}))
.build()
return markwon
}
}

View File

@@ -3,6 +3,7 @@ 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.graphics.drawable.Animatable
@@ -19,6 +20,7 @@ import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator
import android.widget.TextView
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
@@ -33,15 +35,14 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.work.OneTimeWorkRequest
import ani.dantotsu.addons.torrent.ServerService
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.DialogUserAgentBinding
import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment
@@ -69,13 +70,11 @@ import com.google.android.material.textfield.TextInputEditText
import eu.kanade.domain.source.service.SourcePreferences
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.Serializable
@@ -88,7 +87,6 @@ class MainActivity : AppCompatActivity() {
private var load = false
@kotlin.OptIn(DelicateCoroutinesApi::class)
@SuppressLint("InternalInsetResource", "DiscouragedApi")
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -301,7 +299,6 @@ class MainActivity : AppCompatActivity() {
}
}
var launched = false
intent.extras?.let { extras ->
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
val mediaId = extras.getInt("mediaId", -1)
@@ -314,7 +311,6 @@ class MainActivity : AppCompatActivity() {
putExtra("mediaId", mediaId)
putExtra("commentId", commentId)
}
launched = true
startActivity(detailIntent)
} else if (fragmentToLoad == "FEED" && activityId != -1) {
val feedIntent = Intent(this, FeedActivity::class.java).apply {
@@ -322,7 +318,6 @@ class MainActivity : AppCompatActivity() {
putExtra("activityId", activityId)
}
launched = true
startActivity(feedIntent)
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
Logger.log("MainActivity, onCreate: $activityId")
@@ -330,7 +325,6 @@ class MainActivity : AppCompatActivity() {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId)
}
launched = true
startActivity(notificationIntent)
}
}
@@ -384,7 +378,7 @@ class MainActivity : AppCompatActivity() {
}
}
//Load Data
if (!load && !launched) {
if (!load) {
scope.launch(Dispatchers.IO) {
model.loadMain(this@MainActivity)
val id = intent.extras?.getInt("mediaId", 0)
@@ -455,26 +449,16 @@ class MainActivity : AppCompatActivity() {
}
}
}
val torrentManager = Injekt.get<TorrentAddonManager>()
fun startTorrent() {
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
launchIO {
if (!ServerService.isRunning()) {
ServerService.start()
}
/*lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
val index = Helper.downloadManager(this@MainActivity).downloadIndex
val downloadCursor = index.getDownloads()
while (downloadCursor.moveToNext()) {
val download = downloadCursor.download
if (download.state == Download.STATE_FAILED) {
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
}
}
}
if (torrentManager.isInitialized.value == false) {
torrentManager.isInitialized.observe(this) {
if (it) {
startTorrent()
}
}
} else {
startTorrent()
}
}*/ //TODO: remove this
}
override fun onRestart() {
@@ -485,7 +469,7 @@ class MainActivity : AppCompatActivity() {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
val params: ViewGroup.MarginLayoutParams =
val params : ViewGroup.MarginLayoutParams =
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
params.updateMargins(bottom = margin.toPx)
}
@@ -494,14 +478,16 @@ class MainActivity : AppCompatActivity() {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
dialogView.userAgentTextBox.hint = "Password"
dialogView.subtitle.visibility = View.VISIBLE
dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file)
val dialogView =
LayoutInflater.from(this).inflate(R.layout.dialog_user_agent, null)
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)
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Enter Password")
.setView(dialogView.root)
.setView(dialogView)
.setPositiveButton("OK", null)
.setNegativeButton("Cancel") { dialog, _ ->
password.fill('0')

View File

@@ -1,15 +0,0 @@
package ani.dantotsu.addons
abstract class Addon {
abstract val name: String
abstract val pkgName: String
abstract val versionName: String
abstract val versionCode: Long
abstract class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
) : Addon()
}

View File

@@ -1,128 +0,0 @@
package ani.dantotsu.addons
import android.app.Activity
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.logError
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.AppUpdater
import ani.dantotsu.settings.InstallerSteps
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
import rx.android.schedulers.AndroidSchedulers
class AddonDownloader {
companion object {
private suspend fun check(repo: String): Pair<String, String> {
return try {
val res = client.get("https://api.github.com/repos/$repo/releases")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<AppUpdater.GithubResponse>(it)
}
val r = res.maxByOrNull {
it.timeStamp()
} ?: throw Exception("No Pre Release Found")
val v = r.tagName.substringAfter("v", "")
val md = r.body ?: ""
val version = v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
Logger.log("Git Version : $version")
Pair(md, version)
} catch (e: Exception) {
Logger.log("Error checking for update")
Logger.log(e)
Pair("", "")
}
}
suspend fun hasUpdate(repo: String, currentVersion: String): Boolean {
val (_, version) = check(repo)
return compareVersion(version, currentVersion)
}
suspend fun update(
activity: Activity,
manager: AddonManager<*>,
repo: String,
currentVersion: String
) {
val (_, version) = check(repo)
if (!compareVersion(version, currentVersion)) {
toast(activity.getString(R.string.no_update_found))
return
}
MainScope().launch(Dispatchers.IO) {
try {
val apks =
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
.parsed<AppUpdater.GithubResponse>().assets?.filter {
it.browserDownloadURL.endsWith(
".apk"
)
}
val apkToDownload =
apks?.find { it.browserDownloadURL.contains(getCurrentABI()) }
?: apks?.find { it.browserDownloadURL.contains("universal") }
?: apks?.first()
apkToDownload?.browserDownloadURL.apply {
if (this != null) {
val notificationManager =
activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val installerSteps = InstallerSteps(notificationManager, activity)
manager.install(this)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ installStep -> installerSteps.onInstallStep(installStep) {} },
{ error -> installerSteps.onError(error) {} },
{ installerSteps.onComplete {} }
)
} else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
}
} catch (e: Exception) {
logError(e)
}
}
}
/**
* Returns the ABI that the app is most likely running on.
* @return The primary ABI for the device.
*/
private fun getCurrentABI(): String {
return if (Build.SUPPORTED_ABIS.isNotEmpty()) {
Build.SUPPORTED_ABIS[0]
} else "Unknown"
}
private fun compareVersion(newVersion: String, oldVersion: String): Boolean {
fun toDouble(list: List<String>): Double {
return try {
list.mapIndexed { i: Int, s: String ->
when (i) {
0 -> s.toDouble() * 100
1 -> s.toDouble() * 10
2 -> s.toDouble()
else -> s.toDoubleOrNull() ?: 0.0
}
}.sum()
} catch (e: NumberFormatException) {
0.0
}
}
val new = toDouble(newVersion.split("."))
val curr = toDouble(oldVersion.split("."))
return new > curr
}
}
}

View File

@@ -1,131 +0,0 @@
package ani.dantotsu.addons
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.media.AddonType
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.filter
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.getPackageNameFromIntent
import kotlinx.coroutines.DelicateCoroutinesApi
import tachiyomi.core.util.lang.launchNow
internal class AddonInstallReceiver : BroadcastReceiver() {
private var listener: AddonListener? = null
private var type: AddonType? = null
/**
* Registers this broadcast receiver
*/
fun register(context: Context) {
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
}
fun setListener(listener: AddonListener, type: AddonType): AddonInstallReceiver {
this.listener = listener
this.type = type
return this
}
/**
* Called when one of the events of the [filter] is received. When the package is an extension,
* it's loaded in background and it notifies the [listener] when finished.
*/
@OptIn(DelicateCoroutinesApi::class)
override fun onReceive(context: Context, intent: Intent?) {
if (intent == null) return
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> {
if (ExtensionInstallReceiver.isReplacing(intent)) return
launchNow {
when (type) {
AddonType.DOWNLOAD -> {
getPackageNameFromIntent(intent)?.let { packageName ->
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
listener?.onAddonInstalled(
AddonLoader.loadFromPkgName(
context,
packageName,
AddonType.DOWNLOAD
)
)
}
}
AddonType.TORRENT -> {
getPackageNameFromIntent(intent)?.let { packageName ->
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
listener?.onAddonInstalled(
AddonLoader.loadFromPkgName(
context,
packageName,
AddonType.TORRENT
)
)
}
}
else -> {}
}
}
}
Intent.ACTION_PACKAGE_REPLACED -> {
launchNow {
when (type) {
AddonType.DOWNLOAD -> {
getPackageNameFromIntent(intent)?.let { packageName ->
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
listener?.onAddonUpdated(
AddonLoader.loadFromPkgName(
context,
packageName,
AddonType.DOWNLOAD
)
)
}
}
AddonType.TORRENT -> {
getPackageNameFromIntent(intent)?.let { packageName ->
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
listener?.onAddonUpdated(
AddonLoader.loadFromPkgName(
context,
packageName,
AddonType.TORRENT
)
)
}
}
else -> {}
}
}
}
Intent.ACTION_PACKAGE_REMOVED -> {
if (ExtensionInstallReceiver.isReplacing(intent)) return
getPackageNameFromIntent(intent)?.let { packageName ->
when (type) {
AddonType.DOWNLOAD -> {
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return
listener?.onAddonUninstalled(packageName)
}
AddonType.TORRENT -> {
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return
listener?.onAddonUninstalled(packageName)
}
else -> {}
}
}
}
}
}
}

View File

@@ -1,11 +0,0 @@
package ani.dantotsu.addons
interface AddonListener {
fun onAddonInstalled(result: LoadResult?)
fun onAddonUpdated(result: LoadResult?)
fun onAddonUninstalled(pkgName: String)
enum class ListenerAction {
INSTALL, UPDATE, UNINSTALL
}
}

View File

@@ -1,143 +0,0 @@
package ani.dantotsu.addons
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import ani.dantotsu.addons.download.DownloadAddon
import ani.dantotsu.addons.download.DownloadAddonApi
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.download.DownloadLoadResult
import ani.dantotsu.addons.torrent.TorrentAddon
import ani.dantotsu.addons.torrent.TorrentAddonApi
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.addons.torrent.TorrentLoadResult
import ani.dantotsu.media.AddonType
import ani.dantotsu.util.Logger
import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.util.system.getApplicationIcon
class AddonLoader {
companion object {
fun loadExtension(
context: Context,
packageName: String,
className: String,
type: AddonType
): LoadResult? {
val pkgManager = context.packageManager
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(ExtensionLoader.PACKAGE_FLAGS.toLong()))
} else {
pkgManager.getInstalledPackages(ExtensionLoader.PACKAGE_FLAGS)
}
val extPkgs = installedPkgs.filter {
isPackageAnExtension(
packageName,
it
)
}
if (extPkgs.isEmpty()) return null
if (extPkgs.size > 1) throw IllegalStateException("Multiple extensions with the same package name found")
val pkgName = extPkgs.first().packageName
val pkgInfo = extPkgs.first()
val appInfo = try {
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
} catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point
Logger.log(error)
throw error
}
val extName =
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Dantotsu: ")
val versionName = pkgInfo.versionName
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
if (versionName.isNullOrEmpty()) {
Logger.log("Missing versionName for extension $extName")
throw IllegalStateException("Missing versionName for extension $extName")
}
val classLoader =
PathClassLoader(appInfo.sourceDir, appInfo.nativeLibraryDir, context.classLoader)
val loadedClass = try {
Class.forName(className, false, classLoader)
} catch (e: ClassNotFoundException) {
Logger.log("Extension load error: $extName ($className)")
Logger.log(e)
throw e
} catch (e: NoClassDefFoundError) {
Logger.log("Extension load error: $extName ($className)")
Logger.log(e)
throw e
} catch (e: Exception) {
Logger.log("Extension load error: $extName ($className)")
Logger.log(e)
throw e
}
val instance = loadedClass.getDeclaredConstructor().newInstance()
return when (type) {
AddonType.TORRENT -> {
val extension = instance as? TorrentAddonApi
?: throw IllegalStateException("Extension is not a TorrentAddonApi")
TorrentLoadResult.Success(
TorrentAddon.Installed(
name = extName,
pkgName = pkgName,
versionName = versionName,
versionCode = versionCode,
extension = extension,
icon = context.getApplicationIcon(pkgName),
)
)
}
AddonType.DOWNLOAD -> {
val extension = instance as? DownloadAddonApi
?: throw IllegalStateException("Extension is not a DownloadAddonApi")
DownloadLoadResult.Success(
DownloadAddon.Installed(
name = extName,
pkgName = pkgName,
versionName = versionName,
versionCode = versionCode,
extension = extension,
icon = context.getApplicationIcon(pkgName),
)
)
}
}
}
fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? {
return when (type) {
AddonType.TORRENT -> loadExtension(
context,
packageName,
TorrentAddonManager.TORRENT_CLASS,
type
)
AddonType.DOWNLOAD -> loadExtension(
context,
packageName,
DownloadAddonManager.DOWNLOAD_CLASS,
type
)
}
}
private fun isPackageAnExtension(type: String, pkgInfo: PackageInfo): Boolean {
return pkgInfo.packageName.equals(type)
}
}
}

View File

@@ -1,46 +0,0 @@
package ani.dantotsu.addons
import android.content.Context
import ani.dantotsu.media.AddonType
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import rx.Observable
abstract class AddonManager<T : Addon.Installed>(
private val context: Context
) {
abstract var extension: T?
abstract var name: String
abstract var type: AddonType
protected val installer by lazy { ExtensionInstaller(context) }
var hasUpdate: Boolean = false
protected set
protected var onListenerAction: ((AddonListener.ListenerAction) -> Unit)? = null
abstract suspend fun init()
abstract fun isAvailable(): Boolean
abstract fun getVersion(): String?
abstract fun getPackageName(): String?
abstract fun hadError(context: Context): String?
abstract fun updateInstallStep(id: Long, step: InstallStep)
abstract fun setInstalling(id: Long)
fun uninstall() {
getPackageName()?.let {
installer.uninstallApk(it)
}
}
fun addListenerAction(action: (AddonListener.ListenerAction) -> Unit) {
onListenerAction = action
}
fun removeListenerAction() {
onListenerAction = null
}
fun install(url: String): Observable<InstallStep> {
return installer.downloadAndInstall(url, getPackageName() ?: "", name, type)
}
}

View File

@@ -1,8 +0,0 @@
package ani.dantotsu.addons
abstract class LoadResult {
abstract class Success : LoadResult()
}

View File

@@ -1,18 +0,0 @@
package ani.dantotsu.addons.download
import android.graphics.drawable.Drawable
import ani.dantotsu.addons.Addon
sealed class DownloadAddon : Addon() {
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
val extension: DownloadAddonApi,
val icon: Drawable?,
val hasUpdate: Boolean = false,
) : Addon.Installed(name, pkgName, versionName, versionCode)
}

View File

@@ -1,21 +0,0 @@
package ani.dantotsu.addons.download
import android.content.Context
import android.net.Uri
interface DownloadAddonApi {
fun cancelDownload(sessionId: Long)
fun setDownloadPath(context: Context, uri: Uri): String
suspend fun executeFFProbe(request: String, logCallback: (String) -> Unit)
suspend fun executeFFMpeg(request: String, statCallback: (Double) -> Unit): Long
fun getState(sessionId: Long): String
fun getStackTrace(sessionId: Long): String?
fun hadError(sessionId: Long): Boolean
}

View File

@@ -1,134 +0,0 @@
package ani.dantotsu.addons.download
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import ani.dantotsu.R
import ani.dantotsu.addons.AddonDownloader
import ani.dantotsu.addons.AddonInstallReceiver
import ani.dantotsu.addons.AddonListener
import ani.dantotsu.addons.AddonLoader
import ani.dantotsu.addons.AddonManager
import ani.dantotsu.addons.LoadResult
import ani.dantotsu.media.AddonType
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.InstallStep
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DownloadAddonManager(
private val context: Context
) : AddonManager<DownloadAddon.Installed>(context) {
override var extension: DownloadAddon.Installed? = null
override var name: String = "Download Addon"
override var type = AddonType.DOWNLOAD
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
val isInitialized: LiveData<Boolean> = _isInitialized
private var error: String? = null
override suspend fun init() {
extension = null
error = null
hasUpdate = false
withContext(Dispatchers.Main) {
_isInitialized.value = false
}
AddonInstallReceiver()
.setListener(InstallationListener(), type)
.register(context)
try {
val result = AddonLoader.loadExtension(
context,
DOWNLOAD_PACKAGE,
DOWNLOAD_CLASS,
AddonType.DOWNLOAD
) as? DownloadLoadResult
result?.let {
if (it is DownloadLoadResult.Success) {
extension = it.extension
hasUpdate = AddonDownloader.hasUpdate(REPO, it.extension.versionName)
}
}
withContext(Dispatchers.Main) {
_isInitialized.value = true
}
} catch (e: Exception) {
Logger.log("Error initializing Download extension")
Logger.log(e)
error = e.message
}
}
override fun isAvailable(): Boolean {
return extension?.extension != null
}
override fun getVersion(): String? {
return extension?.versionName
}
override fun getPackageName(): String? {
return extension?.pkgName
}
override fun hadError(context: Context): String? {
return if (isInitialized.value == true) {
if (error != null) {
error
} else if (extension != null) {
context.getString(R.string.loaded_successfully)
} else {
null
}
} else {
null
}
}
private inner class InstallationListener : AddonListener {
override fun onAddonInstalled(result: LoadResult?) {
if (result is DownloadLoadResult.Success) {
extension = result.extension
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
}
}
override fun onAddonUpdated(result: LoadResult?) {
if (result is DownloadLoadResult.Success) {
extension = result.extension
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
}
}
override fun onAddonUninstalled(pkgName: String) {
if (extension?.pkgName == pkgName) {
extension = null
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
}
}
}
override fun updateInstallStep(id: Long, step: InstallStep) {
installer.updateInstallStep(id, step)
}
override fun setInstalling(id: Long) {
installer.updateInstallStep(id, InstallStep.Installing)
}
companion object {
const val DOWNLOAD_PACKAGE = "dantotsu.downloadAddon"
const val DOWNLOAD_CLASS = "ani.dantotsu.downloadAddon.DownloadAddon"
const val REPO = "rebelonion/Dantotsu-Download-Addon"
}
}

View File

@@ -1,7 +0,0 @@
package ani.dantotsu.addons.download
import ani.dantotsu.addons.LoadResult
open class DownloadLoadResult : LoadResult() {
class Success(val extension: DownloadAddon.Installed) : DownloadLoadResult()
}

View File

@@ -1,16 +0,0 @@
package ani.dantotsu.addons.torrent
import android.graphics.drawable.Drawable
import ani.dantotsu.addons.Addon
sealed class TorrentAddon : Addon() {
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
val extension: TorrentAddonApi,
val icon: Drawable?,
val hasUpdate: Boolean = false,
) : Addon.Installed(name, pkgName, versionName, versionCode)
}

View File

@@ -1,24 +0,0 @@
package ani.dantotsu.addons.torrent
import eu.kanade.tachiyomi.data.torrentServer.model.Torrent
interface TorrentAddonApi {
fun startServer(path: String)
fun stopServer()
fun echo(): String
fun removeTorrent(torrent: String)
fun addTorrent(
link: String,
title: String,
poster: String,
data: String,
save: Boolean,
): Torrent
fun getLink(torrent: Torrent, index: Int): String
}

View File

@@ -1,137 +0,0 @@
package ani.dantotsu.addons.torrent
import android.content.Context
import android.os.Build
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import ani.dantotsu.R
import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate
import ani.dantotsu.addons.AddonListener
import ani.dantotsu.addons.AddonLoader
import ani.dantotsu.addons.AddonManager
import ani.dantotsu.addons.LoadResult
import ani.dantotsu.addons.AddonInstallReceiver
import ani.dantotsu.media.AddonType
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.InstallStep
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class TorrentAddonManager(
private val context: Context
) : AddonManager<TorrentAddon.Installed>(context) {
override var extension: TorrentAddon.Installed? = null
override var name: String = "Torrent Addon"
override var type: AddonType = AddonType.TORRENT
var torrentHash: String? = null
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
val isInitialized: LiveData<Boolean> = _isInitialized
private var error: String? = null
override suspend fun init() {
extension = null
error = null
hasUpdate = false
withContext(Dispatchers.Main) {
_isInitialized.value = false
}
if (Build.VERSION.SDK_INT < 23) {
Logger.log("Torrent extension is not supported on this device.")
error = context.getString(R.string.torrent_extension_not_supported)
return
}
AddonInstallReceiver()
.setListener(InstallationListener(), type)
.register(context)
try {
val result = AddonLoader.loadExtension(
context,
TORRENT_PACKAGE,
TORRENT_CLASS,
type
) as TorrentLoadResult?
result?.let {
if (it is TorrentLoadResult.Success) {
extension = it.extension
hasUpdate = hasUpdate(REPO, it.extension.versionName)
}
}
withContext(Dispatchers.Main) {
_isInitialized.value = true
}
} catch (e: Exception) {
Logger.log("Error initializing torrent extension")
Logger.log(e)
error = e.message
}
}
override fun isAvailable(): Boolean {
return extension?.extension != null
}
override fun getVersion(): String? {
return extension?.versionName
}
override fun getPackageName(): String? {
return extension?.pkgName
}
override fun hadError(context: Context): String? {
return if (isInitialized.value == true) {
if (error != null) {
error
} else if (extension != null) {
context.getString(R.string.loaded_successfully)
} else {
null
}
} else {
null
}
}
private inner class InstallationListener : AddonListener {
override fun onAddonInstalled(result: LoadResult?) {
if (result is TorrentLoadResult.Success) {
extension = result.extension
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
}
}
override fun onAddonUpdated(result: LoadResult?) {
if (result is TorrentLoadResult.Success) {
extension = result.extension
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
}
}
override fun onAddonUninstalled(pkgName: String) {
if (pkgName == TORRENT_PACKAGE) {
extension = null
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
}
}
}
override fun updateInstallStep(id: Long, step: InstallStep) {
installer.updateInstallStep(id, step)
}
override fun setInstalling(id: Long) {
installer.updateInstallStep(id, InstallStep.Installing)
}
companion object {
const val TORRENT_PACKAGE = "dantotsu.torrentAddon"
const val TORRENT_CLASS = "ani.dantotsu.torrentAddon.TorrentAddon"
const val REPO = "rebelonion/Dantotsu-Torrent-Addon"
}
}

View File

@@ -1,7 +0,0 @@
package ani.dantotsu.addons.torrent
import ani.dantotsu.addons.LoadResult
open class TorrentLoadResult : LoadResult() {
class Success(val extension: TorrentAddon.Installed) : TorrentLoadResult()
}

View File

@@ -1,168 +0,0 @@
package ani.dantotsu.addons.torrent
import android.app.ActivityManager
import android.app.Application
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import ani.dantotsu.R
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_TORRENT_SERVER
import eu.kanade.tachiyomi.data.notification.Notifications.ID_TORRENT_SERVER
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.notificationBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.coroutines.EmptyCoroutineContext
class ServerService : Service() {
private val serviceScope = CoroutineScope(EmptyCoroutineContext)
private val applicationContext = Injekt.get<Application>()
private val extension = Injekt.get<TorrentAddonManager>().extension!!.extension
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(
intent: Intent?,
flags: Int,
startId: Int,
): Int {
intent?.let {
if (it.action != null) {
when (it.action) {
ACTION_START -> {
startServer()
notification(applicationContext)
return START_STICKY
}
ACTION_STOP -> {
stopServer()
return START_NOT_STICKY
}
}
}
}
return START_NOT_STICKY
}
private fun startServer() {
serviceScope.launch {
val echo = extension.echo()
if (echo == "") {
extension.startServer(filesDir.absolutePath)
}
}
}
private fun stopServer() {
serviceScope.launch {
extension.stopServer()
applicationContext.cancelNotification(ID_TORRENT_SERVER)
stopSelf()
}
}
private fun notification(context: Context) {
val exitPendingIntent =
PendingIntent.getService(
applicationContext,
0,
Intent(applicationContext, ServerService::class.java).apply {
action = ACTION_STOP
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val builder = context.notificationBuilder(CHANNEL_TORRENT_SERVER) {
setSmallIcon(R.drawable.notification_icon)
setContentText("Torrent Server")
setContentTitle("Server is running…")
setAutoCancel(false)
setOngoing(true)
setUsesChronometer(true)
addAction(
R.drawable.ic_circle_cancel,
"Stop",
exitPendingIntent,
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
ID_TORRENT_SERVER,
builder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
} else {
startForeground(ID_TORRENT_SERVER, builder.build())
}
}
companion object {
const val ACTION_START = "start_torrent_server"
const val ACTION_STOP = "stop_torrent_server"
fun isRunning(): Boolean {
with(Injekt.get<Application>().getSystemService(ACTIVITY_SERVICE) as ActivityManager) {
@Suppress("DEPRECATION") // We only need our services
getRunningServices(Int.MAX_VALUE).forEach {
if (ServerService::class.java.name.equals(it.service.className)) {
return true
}
}
}
return false
}
fun start() {
try {
val intent =
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
action = ACTION_START
}
Injekt.get<Application>().startService(intent)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun stop() {
try {
val intent =
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
action = ACTION_STOP
}
Injekt.get<Application>().startService(intent)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun wait(timeout: Int = -1): Boolean {
var count = 0
if (timeout < 0) {
count = -20
}
var echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
while (echo == "") {
Thread.sleep(1000)
count++
if (count > timeout) {
return false
}
echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
}
Logger.log("ServerService: Server started: $echo")
return true
}
}
}

View File

@@ -6,8 +6,6 @@ import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaCache
@@ -40,13 +38,10 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DownloadsManager(app) }
addSingletonFactory { NetworkHelper(app) }
addSingletonFactory { NetworkHelper(app).client }
addSingletonFactory { AnimeExtensionManager(app) }
addSingletonFactory { MangaExtensionManager(app) }
addSingletonFactory { NovelExtensionManager(app) }
addSingletonFactory { TorrentAddonManager(app) }
addSingletonFactory { DownloadAddonManager(app) }
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.connections.anilist
import android.content.ActivityNotFoundException
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.browser.customtabs.CustomTabsIntent
import ani.dantotsu.R
import ani.dantotsu.client
@@ -13,6 +14,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import java.util.Calendar
@@ -199,9 +201,7 @@ object Anilist {
toast("Rate limited. Try after $retry seconds")
throw Exception("Rate limited after $retry seconds")
}
if (!json.text.startsWith("{")) {
throw Exception(currContext()?.getString(R.string.anilist_down))
}
if (!json.text.startsWith("{")) {throw Exception(currContext()?.getString(R.string.anilist_down))}
if (show) Logger.log("Anilist Response: ${json.text}")
json.parsed()
} else null

View File

@@ -20,7 +20,6 @@ import ani.dantotsu.media.Character
import ani.dantotsu.media.Media
import ani.dantotsu.media.Studio
import ani.dantotsu.others.MalScraper
import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
@@ -73,14 +72,14 @@ class AnilistQueries {
media.cameFromContinue = false
val query =
"""{Media(id:${media.id}){id favourites popularity episodes chapters 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 voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } 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}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
"""{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}}}"""
runBlocking {
val anilist = async {
var response = executeQuery<Query.Media>(query, force = true, show = true)
if (response != null) {
fun parse() {
val fetchedMedia = response?.data?.media ?: return
val user = response?.data?.page
media.source = fetchedMedia.source?.toString()
media.countryOfOrigin = fetchedMedia.countryOfOrigin
media.format = fetchedMedia.format?.toString()
@@ -140,15 +139,7 @@ class AnilistQueries {
?: "SUPPORTING"
else -> i.role.toString()
},
voiceActor = i.voiceActors?.map {
Author(
id = it.id,
name = it.name?.userPreferred,
image = it.image?.large,
role = it.languageV2
)
} as ArrayList<Author>
}
)
)
}
@@ -162,7 +153,7 @@ class AnilistQueries {
Author(
id = id,
name = i.node?.name?.userPreferred,
image = i.node?.image?.large,
image = i.node?.image?.medium,
role = when (i.role.toString()) {
"MAIN" -> currContext()?.getString(R.string.main_role)
?: "MAIN"
@@ -209,24 +200,7 @@ class AnilistQueries {
}
}
}
if (user?.mediaList?.isNotEmpty() == true) {
media.users = user.mediaList?.mapNotNull {
it.user?.let { user ->
if (user.id != Anilist.userid) {
User(
user.id,
user.name ?: "Unknown",
user.avatar?.large,
"",
it.status?.toString(),
it.score,
it.progress,
fetchedMedia.episodes ?: fetchedMedia.chapters,
)
} else null
}
}?.toCollection(arrayListOf()) ?: arrayListOf()
}
if (fetchedMedia.mediaListEntry != null) {
fetchedMedia.mediaListEntry?.apply {
media.userProgress = progress
@@ -1061,157 +1035,63 @@ 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(page: Int): String {
return """Page(page:$page,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 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(page: Int): String {
return """Page(page:$page,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 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(page: Int): String {
return """Page(page:$page,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 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(page: Int): String {
return """Page(page:$page,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}}}"""
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(): Map<String, ArrayList<Media>> {
val list = mutableMapOf<String, ArrayList<Media>>()
fun query(): String {
return """{
recentUpdates:${recentAnimeUpdates(1)}
recentUpdates2:${recentAnimeUpdates(2)}
trendingMovies:${trendingMovies(1)}
trendingMovies2:${trendingMovies(2)}
topRated:${topRatedAnime(1)}
topRated2:${topRatedAnime(2)}
mostFav:${mostFavAnime(1)}
mostFav2:${mostFavAnime(2)}
}""".trimIndent()
}
executeQuery<Query.AnimeList>(query(), force = true)?.data?.apply {
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly)
val idArr = mutableListOf<Int>()
list["recentUpdates"] = 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
}
} as ArrayList<Media>
list["trendingMovies"] = trendingMovies?.media?.map { Media(it) } as ArrayList<Media>
list["topRated"] = topRated?.media?.map { Media(it) } as ArrayList<Media>
list["mostFav"] = mostFav?.media?.map { Media(it) } as ArrayList<Media>
list["recentUpdates"]?.addAll(recentUpdates2?.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
}
} as ArrayList<Media>)
list["trendingMovies"]?.addAll(trendingMovies2?.media?.map { Media(it) } as ArrayList<Media>)
list["topRated"]?.addAll(topRated2?.media?.map { Media(it) } as ArrayList<Media>)
list["mostFav"]?.addAll(mostFav2?.media?.map { Media(it) } as ArrayList<Media>)
}
return list
}
private val onListManga =
(if (PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace(
"\"",
""
suspend fun loadAnimeList(): Query.AnimeList?{
return executeQuery<Query.AnimeList>(
"""{
recentUpdates:${recentAnimeUpdates()}
trendingMovies:${trendingMovies()}
topRated:${topRatedAnime()}
mostFav:${mostFavAnime()}
}""".trimIndent(), force = true
)
private fun trendingManga(page: Int): String {
return """Page(page:$page,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(page: Int): String {
return """Page(page:$page,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 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 trendingNovel(page: Int): String {
return """Page(page:$page,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 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 topRatedManga(page: Int): String {
return """Page(page:$page,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 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 mostFavManga(page: Int): String {
return """Page(page:$page,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}}}"""
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}}}"""
}
suspend fun loadMangaList(): Map<String, ArrayList<Media>> {
val list = mutableMapOf<String, ArrayList<Media>>()
fun query(): String {
return """{
trendingManga:${trendingManga(1)}
trendingManga2:${trendingManga(2)}
trendingManhwa:${trendingManhwa(1)}
trendingManhwa2:${trendingManhwa(2)}
trendingNovel:${trendingNovel(1)}
trendingNovel2:${trendingNovel(2)}
topRated:${topRatedManga(1)}
topRated2:${topRatedManga(2)}
mostFav:${mostFavManga(1)}
mostFav2:${mostFavManga(2)}
}""".trimIndent()
}
executeQuery<Query.MangaList>(query(), force = true)?.data?.apply {
list["trendingManga"] = trendingManga?.media?.map { Media(it) } as ArrayList<Media>
list["trendingManhwa"] = trendingManhwa?.media?.map { Media(it) } as ArrayList<Media>
list["trendingNovel"] = trendingNovel?.media?.map { Media(it) } as ArrayList<Media>
list["topRated"] = topRated?.media?.map { Media(it) } as ArrayList<Media>
list["mostFav"] = mostFav?.media?.map { Media(it) } as ArrayList<Media>
list["trendingManga"]?.addAll(trendingManga2?.media?.map { Media(it) } as ArrayList<Media>)
list["trendingManhwa"]?.addAll(trendingManhwa2?.media?.map { Media(it) } as ArrayList<Media>)
list["trendingNovel"]?.addAll(trendingNovel2?.media?.map { Media(it) } as ArrayList<Media>)
list["topRated"]?.addAll(topRated2?.media?.map { Media(it) } as ArrayList<Media>)
list["mostFav"]?.addAll(mostFav2?.media?.map { Media(it) } as ArrayList<Media>)
}
return list
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(
greater: Long = 0,
lesser: Long = System.currentTimeMillis() / 1000 - 10000
): MutableList<Media> {
): MutableList<Media>? {
suspend fun execute(page: Int = 1): Page? {
val query = """{
Page(page:$page,perPage:50) {
@@ -1258,26 +1138,25 @@ Page(page:$page,perPage:50) {
}""".replace("\n", " ").replace(""" """, "")
return executeQuery<Query.Page>(query, force = true)?.data?.page
}
var i = 1
val list = mutableListOf<Media>()
var res: Page? = null
suspend fun next() {
res = execute(i)
list.addAll(res?.airingSchedules?.mapNotNull { j ->
j.media?.let {
if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
} else null
}
} ?: listOf())
}
next()
while (res?.pageInfo?.hasNextPage == true) {
var i = 1
val list = mutableListOf<Media>()
var res: Page? = null
suspend fun next() {
res = execute(i)
list.addAll(res?.airingSchedules?.mapNotNull { j ->
j.media?.let {
if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
} else null
}
} ?: listOf())
}
next()
i++
}
return list.reversed().toMutableList()
while (res?.pageInfo?.hasNextPage == true) {
next()
i++
}
return list.reversed().toMutableList()
}
suspend fun getCharacterDetails(character: Character): Character {
@@ -1463,39 +1342,19 @@ Page(page:$page,perPage:50) {
}
}
}
characters(page: $page,sort:FAVOURITES_DESC) {
pageInfo{
hasNextPage
}
nodes{
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
}
}
}
}""".replace("\n", " ").replace(""" """, "")
var hasNextPage = true
val yearMedia = mutableMapOf<String, ArrayList<Media>>()
var page = 0
val characters = arrayListOf<Character>()
while (hasNextPage) {
page++
val query = executeQuery<Query.Author>(
query(page), force = true
)?.data?.author
hasNextPage = query?.staffMedia?.let {
hasNextPage = executeQuery<Query.Author>(
query(page),
force = true
)?.data?.author?.staffMedia?.let {
it.edges?.forEach { i ->
i.node?.apply {
val status = status.toString()
@@ -1510,20 +1369,6 @@ Page(page:$page,perPage:50) {
}
it.pageInfo?.hasNextPage == true
} ?: false
query?.characters?.let {
it.nodes?.forEach { i ->
characters.add(
Character(
i.id,
i.name?.userPreferred,
i.image?.large,
i.image?.medium,
"",
false
)
)
}
}
}
if (yearMedia.contains("CANCELLED")) {
@@ -1531,7 +1376,6 @@ Page(page:$page,perPage:50) {
yearMedia.remove("CANCELLED")
yearMedia["CANCELLED"] = a
}
author.character = characters
author.yearMedia = yearMedia
return author
}
@@ -1603,11 +1447,7 @@ Page(page:$page,perPage:50) {
}
suspend fun getNotifications(
id: Int,
page: Int = 1,
resetNotification: Boolean = true
): NotificationResponse? {
suspend fun getNotifications(id: Int, page: Int = 1, resetNotification: Boolean = true): NotificationResponse? {
val reset = if (resetNotification) "true" else "false"
val res = executeQuery<NotificationResponse>(
"""{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){pageInfo{currentPage,hasNextPage}notifications(resetNotificationCount:$reset){__typename...on AiringNotification{id,type,animeId,episode,contexts,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}},}...on FollowingNotification{id,userId,type,context,createdAt,user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMessageNotification{id,userId,type,activityId,context,createdAt,message{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMentionNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplySubscribedNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentMentionNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentReplyNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentSubscribedNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentLikeNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadLikeNotification{id,userId,type,threadId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on RelatedMediaAdditionNotification{id,type,context,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDataChangeNotification{id,type,mediaId,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaMergeNotification{id,type,mediaId,deletedMediaTitles,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDeletionNotification{id,type,deletedMediaTitle,context,reason,createdAt,}}}}""",
@@ -1622,12 +1462,7 @@ Page(page:$page,perPage:50) {
return res
}
suspend fun getFeed(
userId: Int?,
global: Boolean = false,
page: Int = 1,
activityId: Int? = null
): FeedResponse? {
suspend fun getFeed(userId: Int?, global: Boolean = false, page: Int = 1, activityId: Int? = null): FeedResponse? {
val filter = if (activityId != null) "id:$activityId,"
else if (userId != null) "userId:$userId,"
else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
@@ -1656,26 +1491,14 @@ Page(page:$page,perPage:50) {
.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)
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) {
AnilistMutations.FavType.ANIME -> res?.data?.user?.favourites?.anime?.nodes?.any { it.id == id }
?: false
AnilistMutations.FavType.MANGA -> res?.data?.user?.favourites?.manga?.nodes?.any { it.id == id }
?: false
AnilistMutations.FavType.CHARACTER -> res?.data?.user?.favourites?.characters?.nodes?.any { it.id == id }
?: false
AnilistMutations.FavType.STAFF -> res?.data?.user?.favourites?.staff?.nodes?.any { it.id == id }
?: false
AnilistMutations.FavType.STUDIO -> res?.data?.user?.favourites?.studios?.nodes?.any { it.id == id }
?: false
AnilistMutations.FavType.ANIME -> res?.data?.user?.favourites?.anime?.nodes?.any { it.id == id } ?: false
AnilistMutations.FavType.MANGA -> res?.data?.user?.favourites?.manga?.nodes?.any { it.id == id } ?: false
AnilistMutations.FavType.CHARACTER -> res?.data?.user?.favourites?.characters?.nodes?.any { it.id == id } ?: false
AnilistMutations.FavType.STAFF -> res?.data?.user?.favourites?.staff?.nodes?.any { it.id == id } ?: false
AnilistMutations.FavType.STUDIO -> res?.data?.user?.favourites?.studios?.nodes?.any { it.id == id } ?: false
}
}

View File

@@ -5,6 +5,8 @@ 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
@@ -187,29 +189,46 @@ class AnilistAnimeViewModel : ViewModel() {
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 list = Anilist.query.loadAnimeList()
updated.postValue(list["recentUpdates"])
popularMovies.postValue(list["trendingMovies"])
topRatedAnime.postValue(list["topRated"])
mostFavAnime.postValue(list["mostFav"])
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())
}
}
}
@@ -284,35 +303,33 @@ class AnilistMangaViewModel : ViewModel() {
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 list = Anilist.query.loadMangaList()
popularManga.postValue(list["trendingManga"])
popularManhwa.postValue(list["trendingManhwa"])
popularNovel.postValue(list["trendingNovel"])
topRatedManga.postValue(list["topRated"])
mostFavManga.postValue(list["mostFav"])
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())
}
}
}

View File

@@ -4,6 +4,7 @@ import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.logError
import ani.dantotsu.util.Logger
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.startMainActivity

View File

@@ -11,7 +11,7 @@ data class SearchResults(
var onList: Boolean? = null,
var perPage: Int? = null,
var search: String? = null,
var countryOfOrigin: String? = null,
var countryOfOrigin :String? = null,
var sort: String? = null,
var genres: MutableList<String>? = null,
var excludedGenres: MutableList<String>? = null,

View File

@@ -55,7 +55,7 @@ data class CharacterConnection(
@SerialName("nodes") var nodes: List<Character>?,
// The pagination information
@SerialName("pageInfo") var pageInfo: PageInfo?,
// @SerialName("pageInfo") var pageInfo: PageInfo?,
) : java.io.Serializable
@Serializable
@@ -72,7 +72,7 @@ data class CharacterEdge(
@SerialName("name") var name: String?,
// The voice actors of the character
@SerialName("voiceActors") var voiceActors: List<Staff>?,
// @SerialName("voiceActors") var voiceActors: List<Staff>?,
// The voice actors of the character with role date
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,

View File

@@ -24,9 +24,7 @@ class Query {
@Serializable
data class Data(
@SerialName("Media")
val media: ani.dantotsu.connections.anilist.api.Media?,
@SerialName("Page")
val page: ani.dantotsu.connections.anilist.api.Page?
val media: ani.dantotsu.connections.anilist.api.Media?
)
}
@@ -149,10 +147,8 @@ 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?
)
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?)
}
@Serializable
data class AnimeList(
@SerialName("data")
@@ -161,16 +157,11 @@ class Query {
@Serializable
data class Data(
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("recentUpdates2") val recentUpdates2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingMovies2") val trendingMovies2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
)
}
@Serializable
data class MangaList(
@SerialName("data")
@@ -179,18 +170,12 @@ class Query {
@Serializable
data class Data(
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManga2") val trendingManga2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManhwa2") val trendingManhwa2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingNovel2") val trendingNovel2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
)
}
@Serializable
data class ToggleFollow(
@SerialName("data")
@@ -321,13 +306,13 @@ class Query {
val statistics: NNUserStatisticTypes,
@SerialName("siteUrl")
val siteUrl: String,
) : java.io.Serializable
): java.io.Serializable
@Serializable
data class NNUserStatisticTypes(
@SerialName("anime") var anime: NNUserStatistics,
@SerialName("manga") var manga: NNUserStatistics
) : java.io.Serializable
): java.io.Serializable
@Serializable
data class NNUserStatistics(
@@ -338,9 +323,9 @@ class Query {
@SerialName("episodesWatched") var episodesWatched: Int,
@SerialName("chaptersRead") var chaptersRead: Int,
@SerialName("volumesRead") var volumesRead: Int,
) : java.io.Serializable
): java.io.Serializable
@Serializable
@Serializable
data class UserFavourites(
@SerialName("anime")
val anime: UserMediaFavouritesCollection,
@@ -352,13 +337,13 @@ class Query {
val staff: UserStaffFavouritesCollection,
@SerialName("studios")
val studios: UserStudioFavouritesCollection,
) : java.io.Serializable
): java.io.Serializable
@Serializable
data class UserMediaFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserMediaImageFavorite>,
) : java.io.Serializable
): java.io.Serializable
@Serializable
data class UserMediaImageFavorite(
@@ -366,13 +351,13 @@ class Query {
val id: Int,
@SerialName("coverImage")
val coverImage: MediaCoverImage
) : java.io.Serializable
): java.io.Serializable
@Serializable
data class UserCharacterFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserCharacterImageFavorite>,
) : java.io.Serializable
): java.io.Serializable
@Serializable
data class UserCharacterImageFavorite(
@@ -384,19 +369,19 @@ class Query {
val image: CharacterImage,
@SerialName("isFavourite")
val isFavourite: Boolean
) : java.io.Serializable
): java.io.Serializable
@Serializable
data class UserStaffFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character
) : java.io.Serializable
): java.io.Serializable
@Serializable
data class UserStudioFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserStudioFavorite>,
) : java.io.Serializable
): java.io.Serializable
@Serializable
data class UserStudioFavorite(
@@ -404,7 +389,7 @@ class Query {
val id: Int,
@SerialName("name")
val name: String,
) : java.io.Serializable
): java.io.Serializable
//----------------------------------------
// Statistics
@@ -413,12 +398,12 @@ class Query {
data class StatisticsResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
): java.io.Serializable {
@Serializable
data class Data(
@SerialName("User")
val user: StatisticsUser?
) : java.io.Serializable
): java.io.Serializable
}
@Serializable

View File

@@ -21,7 +21,6 @@ enum class NotificationType(val value: String) {
MEDIA_DATA_CHANGE("MEDIA_DATA_CHANGE"),
MEDIA_MERGE("MEDIA_MERGE"),
MEDIA_DELETION("MEDIA_DELETION"),
//custom
COMMENT_REPLY("COMMENT_REPLY"),
}
@@ -85,9 +84,9 @@ data class Notification(
@SerialName("createdAt")
val createdAt: Int,
@SerialName("media")
val media: Media? = null,
val media: ani.dantotsu.connections.anilist.api.Media? = null,
@SerialName("user")
val user: User? = null,
val user: ani.dantotsu.connections.anilist.api.User? = null,
@SerialName("message")
val message: MessageActivity? = null,
@SerialName("activity")

View File

@@ -93,7 +93,6 @@ data class StaffConnection(
// The pagination information
// @SerialName("pageInfo") var pageInfo: PageInfo?,
)
@Serializable
data class StaffImage(
// The character's image of media at its largest size
@@ -102,7 +101,6 @@ data class StaffImage(
// The character's image of media at medium size
@SerialName("medium") var medium: String?,
) : java.io.Serializable
@Serializable
data class StaffEdge(
var role: String?,

View File

@@ -111,7 +111,7 @@ data class UserAvatar(
// The avatar of user at medium size
@SerialName("medium") var medium: String?,
) : java.io.Serializable
): java.io.Serializable
@Serializable
data class UserStatisticTypes(

View File

@@ -1,128 +0,0 @@
package ani.dantotsu.connections.bakaupdates
import android.content.Context
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import okio.ByteString.Companion.encode
import org.json.JSONException
import org.json.JSONObject
import java.nio.charset.Charset
class MangaUpdates {
private val Int?.dateFormat get() = String.format("%02d", this)
private val apiUrl = "https://api.mangaupdates.com/v1/releases/search"
suspend fun search(title: String, startDate: FuzzyDate?): MangaUpdatesResponse.Results? {
return tryWithSuspend {
val query = JSONObject().apply {
try {
put("search", title.encode(Charset.forName("UTF-8")))
startDate?.let {
put(
"start_date",
"${it.year}-${it.month.dateFormat}-${it.day.dateFormat}"
)
}
put("include_metadata", true)
} catch (e: JSONException) {
e.printStackTrace()
}
}
val res = client.post(apiUrl, json = query).parsed<MangaUpdatesResponse>()
coroutineScope {
res.results?.map {
async(Dispatchers.IO) {
Logger.log(it.toString())
}
}
}?.awaitAll()
res.results?.first {
it.metadata.series.lastUpdated?.timestamp != null
&& (it.metadata.series.latestChapter != null
|| (it.record.volume.isNullOrBlank() && it.record.chapter != null))
}
}
}
companion object {
fun getLatestChapter(context: Context, results: MangaUpdatesResponse.Results): String {
return results.metadata.series.latestChapter?.let {
context.getString(R.string.chapter_number, it)
} ?: results.record.chapter!!.substringAfterLast("-").trim().let { chapter ->
chapter.takeIf {
it.toIntOrNull() == null
} ?: context.getString(R.string.chapter_number, chapter.toInt())
}
}
}
@Serializable
data class MangaUpdatesResponse(
@SerialName("total_hits")
val totalHits: Int?,
@SerialName("page")
val page: Int?,
@SerialName("per_page")
val perPage: Int?,
val results: List<Results>? = null
) {
@Serializable
data class Results(
val record: Record,
val metadata: MetaData
) {
@Serializable
data class Record(
@SerialName("id")
val id: Int,
@SerialName("title")
val title: String,
@SerialName("volume")
val volume: String?,
@SerialName("chapter")
val chapter: String?,
@SerialName("release_date")
val releaseDate: String
)
@Serializable
data class MetaData(
val series: Series
) {
@Serializable
data class Series(
@SerialName("series_id")
val seriesId: Long?,
@SerialName("title")
val title: String?,
@SerialName("latest_chapter")
val latestChapter: Int?,
@SerialName("last_updated")
val lastUpdated: LastUpdated?
) {
@Serializable
data class LastUpdated(
@SerialName("timestamp")
val timestamp: Long,
@SerialName("as_rfc3339")
val asRfc3339: String,
@SerialName("as_string")
val asString: String
)
}
}
}
}
}

View File

@@ -32,12 +32,7 @@ object CommentsAPI {
var isMod: Boolean = false
var totalVotes: Int = 0
suspend fun getCommentsForId(
id: Int,
page: Int = 1,
tag: Int?,
sort: String?
): CommentResponse? {
suspend fun getCommentsForId(id: Int, page: Int = 1, tag: Int?, sort: String?): CommentResponse? {
var url = "$ADDRESS/comments/$id/$page"
val request = requestBuilder()
tag?.let {
@@ -404,7 +399,7 @@ object CommentsAPI {
null
}
val message = parsed?.message ?: reason ?: error
val fullMessage = if (code == 500) message else "$code: $message"
val fullMessage = if(code == 500) message else "$code: $message"
toast(fullMessage)
}

View File

@@ -7,7 +7,6 @@ class CrashlyticsStub : CrashlyticsInterface {
override fun initialize(context: Context) {
//no-op
}
override fun logException(e: Throwable) {
Logger.log(e)
}

View File

@@ -71,6 +71,4 @@ object Discord {
const val application_Id = "1163925779692912771"
const val small_Image: String =
"mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
const val small_Image_AniList: String =
"mp:external/rHOIjjChluqQtGyL_UHk6Z4oAqiVYlo_B7HSGPLSoUg/%3Fsize%3D128/https/cdn.discordapp.com/icons/210521487378087947/a_f54f910e2add364a3da3bb2f2fce0c72.webp"
}

View File

@@ -63,7 +63,7 @@ class DiscordService : Service() {
PowerManager.PARTIAL_WAKE_LOCK,
"discordRPC:backgroundPresence"
)
wakeLock.acquire(30 * 60 * 1000L /*30 minutes*/)
wakeLock.acquire(30*60*1000L /*30 minutes*/)
log("WakeLock Acquired")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
@@ -402,8 +402,7 @@ class DiscordService : Service() {
Thread.sleep(heartbeat.toLong())
heartbeatSend(webSocket, sequence)
log("WebSocket: Heartbeat Sent")
} catch (ignored: InterruptedException) {
}
} catch (ignored: InterruptedException) { }
}
}

View File

@@ -71,8 +71,8 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
assets = Activity.Assets(
largeImage = data.largeImage?.url?.discordUrl(),
largeText = data.largeImage?.label,
smallImage = if (PrefManager.getVal(PrefName.ShowAniListIcon)) Discord.small_Image_AniList.discordUrl() else Discord.small_Image.discordUrl(),
smallText = if (PrefManager.getVal(PrefName.ShowAniListIcon)) "Anilist" else "Dantotsu",
smallImage = data.smallImage?.url?.discordUrl(),
smallText = data.smallImage?.label
),
buttons = data.buttons.map { it.label },
metadata = Activity.Metadata(

View File

@@ -22,81 +22,51 @@ class Contributors {
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
res.forEach {
if (it.login == "SunglassJerry") return@forEach
val role = when (it.login) {
"rebelonion" -> "Owner & Maintainer"
"sneazy-ibo" -> "Contributor & Comment Moderator"
"WaiWhat" -> "Icon Designer"
else -> "Contributor"
}
res.find { it.login == "rebelonion"}?.let { first ->
developers = developers.plus(
Developer(
it.login,
it.avatarUrl,
role,
it.htmlUrl
first.login,
first.avatarUrl,
"Owner and Maintainer",
first.htmlUrl
)
)
}
developers = developers.plus(
arrayOf(
).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 & Website Maintainer",
"Beta Icon Designer",
"https://github.com/MarshMeadow?tab=repositories"
),
Developer(
"Zaxx69",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6342562-kxE8m4i7KUMK.png",
"https://avatars.githubusercontent.com/u/138523882?v=4",
"Telegram Admin",
"https://anilist.co/user/6342562"
"https://github.com/Zaxx69"
),
Developer(
"Arif Alam",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6011177-2n994qtayiR9.jpg",
"Discord & Comment Moderator",
"https://anilist.co/user/6011177"
),
"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(
"SunglassJeery",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b5804776-FEKfP5wbz2xv.png",
"Head Discord & Comment Moderator",
"https://anilist.co/user/5804776"
),
Developer(
"Excited",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6131921-toSoGWmKbRA1.png",
"Comment Moderator",
"https://anilist.co/user/6131921"
),
Developer(
"Gurjshan",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6363228-rWQ3Pl3WuxzL.png",
"Comment Moderator",
"https://anilist.co/user/6363228"
),
Developer(
"NekoMimi",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6244220-HOpImMGMQAxW.jpg",
"Comment Moderator",
"https://anilist.co/user/6244220"
),
Developer(
"Zaidsenior",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6049773-8cjYeUOFUguv.jpg",
"Comment Moderator",
"https://anilist.co/user/6049773"
),
Developer(
"hastsu",
"https://cdn.discordapp.com/avatars/602422545077108749/20b4a6efa4314550e4ed51cdbe4fef3d.webp?size=160",
"Comment Moderator",
"https://anilist.co/user/6183359"
),
it.login,
it.avatarUrl,
"Contributor",
it.htmlUrl
)
)
)
}
}
return developers
}

View File

@@ -1,7 +1,9 @@
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
@@ -15,11 +17,10 @@ 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?sort=stargazers")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
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(

View File

@@ -1,381 +0,0 @@
package ani.dantotsu.download
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.widget.Toast
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.anime.OfflineAnimeModel
import ani.dantotsu.download.manga.OfflineMangaModel
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.Episode
import ani.dantotsu.parsers.MangaChapter
import ani.dantotsu.parsers.MangaImage
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.util.Logger
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
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 eu.kanade.tachiyomi.source.model.SManga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.Locale
@Deprecated("external storage is deprecated, use SAF instead")
class DownloadCompat {
companion object {
@Deprecated("external storage is deprecated, use SAF instead")
fun loadMediaCompat(downloadedType: DownloadedType): Media? {
val type = when (downloadedType.type) {
MediaType.MANGA -> "Manga"
MediaType.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.titleName}"
)
//load media.json and convert to media class with gson
return try {
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
SAnimeImpl() // Provide an instance of SAnimeImpl
})
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
})
.create()
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}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
null
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun loadOfflineAnimeModelCompat(downloadedType: DownloadedType): OfflineAnimeModel {
val type = when (downloadedType.type) {
MediaType.MANGA -> "Manga"
MediaType.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.titleName}"
)
//load media.json and convert to media class with gson
try {
val mediaModel = loadMediaCompat(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else null
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
?: 0) else mediaModel.userScore) / 10.0).toString()
val isOngoing =
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0
val watchedEpisodes = (mediaModel.userProgress ?: "~").toString()
val totalEpisode =
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString() + " | " + (mediaModel.anime.totalEpisodes
?: "~").toString()) else (mediaModel.anime?.totalEpisodes ?: "~").toString()
val chapters = " Chapters"
val totalEpisodesList =
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString()) else (mediaModel.anime?.totalEpisodes
?: "~").toString()
return OfflineAnimeModel(
title,
score,
totalEpisode,
totalEpisodesList,
watchedEpisodes,
type,
chapters,
isOngoing,
isUserScored,
coverUri,
bannerUri
)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineAnimeModel(
"unknown",
"0",
"??",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun loadOfflineMangaModelCompat(downloadedType: DownloadedType): OfflineMangaModel {
val type = when (downloadedType.type) {
MediaType.MANGA -> "Manga"
MediaType.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.titleName}"
)
//load media.json and convert to media class with gson
try {
val mediaModel = loadMediaCompat(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else null
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
?: 0) else mediaModel.userScore) / 10.0).toString()
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 chapters = " Chapters"
return OfflineMangaModel(
title,
score,
totalchapter,
readchapter,
type,
chapters,
isOngoing,
isUserScored,
coverUri,
bannerUri
)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
@Deprecated("external storage is deprecated, use SAF instead")
suspend fun loadEpisodesCompat(
animeLink: String,
extra: Map<String, String>?,
sAnime: SAnime
): List<Episode> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${animeLocation}/$animeLink"
)
//get all of the folder names and add them to the list
val episodes = mutableListOf<Episode>()
if (directory.exists()) {
directory.listFiles()?.forEach {
//put the title and episdode number in the extra data
val extraData = mutableMapOf<String, String>()
extraData["title"] = animeLink
extraData["episode"] = it.name
if (it.isDirectory) {
val episode = Episode(
it.name,
"$animeLink - ${it.name}",
it.name,
null,
null,
extra = extraData,
sEpisode = SEpisodeImpl()
)
episodes.add(episode)
}
}
episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) }
return episodes
}
return emptyList()
}
@Deprecated("external storage is deprecated, use SAF instead")
suspend fun loadChaptersCompat(
mangaLink: String,
extra: Map<String, String>?,
sManga: SManga
): List<MangaChapter> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$mangaLink"
)
//get all of the folder names and add them to the list
val chapters = mutableListOf<MangaChapter>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isDirectory) {
val chapter = MangaChapter(
it.name,
"$mangaLink/${it.name}",
it.name,
null,
null,
SChapter.create()
)
chapters.add(chapter)
}
}
chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) }
return chapters
}
return emptyList()
}
@Deprecated("external storage is deprecated, use SAF instead")
suspend fun loadImagesCompat(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$chapterLink"
)
val images = mutableListOf<MangaImage>()
val imageNumberRegex = Regex("""(\d+)\.jpg$""")
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isFile) {
val image = MangaImage(it.absolutePath, false, null)
images.add(image)
}
}
images.sortBy { image ->
val matchResult = imageNumberRegex.find(image.url.url)
matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE
}
for (image in images) {
Logger.log("imageNumber: ${image.url.url}")
}
return images
}
return emptyList()
}
@Deprecated("external storage is deprecated, use SAF instead")
fun loadSubtitleCompat(title: String, episode: String): List<Subtitle>? {
currContext()?.let {
File(
it.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title/$episode"
).listFiles()?.forEach {
if (it.name.contains("subtitle")) {
return listOf(
Subtitle(
"Downloaded Subtitle",
Uri.fromFile(it).toString(),
determineSubtitletype(it.absolutePath)
)
)
}
}
}
return null
}
private fun determineSubtitletype(url: String): SubtitleType {
return when {
url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS
url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT
else -> SubtitleType.SRT
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun removeMediaCompat(context: Context, title: String, type: MediaType) {
val subDirectory = if (type == MediaType.MANGA) {
"Manga"
} else if (type == MediaType.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory/$title"
)
if (directory.exists()) {
directory.deleteRecursively()
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun removeDownloadCompat(context: Context, downloadedType: DownloadedType) {
val directory = if (downloadedType.type == MediaType.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${downloadedType.titleName}/${downloadedType.chapterName}"
)
} else if (downloadedType.type == MediaType.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${downloadedType.titleName}/${downloadedType.chapterName}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${downloadedType.titleName}/${downloadedType.chapterName}"
)
}
// Check if the directory exists and delete it recursively
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()
}
}
}
private val animeLocation = "Dantotsu/Anime"
}
}

View File

@@ -3,9 +3,7 @@ package ani.dantotsu.download
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.download.DownloadCompat.Companion.removeDownloadCompat
import ani.dantotsu.download.DownloadCompat.Companion.removeMediaCompat
import ani.dantotsu.media.Media
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.media.MediaType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
@@ -14,6 +12,7 @@ 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
@@ -21,7 +20,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.io.Serializable
class DownloadsManager(private val context: Context) {
@@ -55,15 +53,10 @@ class DownloadsManager(private val context: Context) {
saveDownloads()
}
fun removeDownload(
downloadedType: DownloadedType,
toast: Boolean = true,
onFinished: () -> Unit
) {
removeDownloadCompat(context, downloadedType)
fun removeDownload(downloadedType: DownloadedType, onFinished: () -> Unit) {
downloadsList.remove(downloadedType)
CoroutineScope(Dispatchers.IO).launch {
removeDirectory(downloadedType, toast)
removeDirectory(downloadedType)
withContext(Dispatchers.Main) {
onFinished()
}
@@ -72,7 +65,6 @@ class DownloadsManager(private val context: Context) {
}
fun removeMedia(title: String, type: MediaType) {
removeMediaCompat(context, title, type)
val baseDirectory = getBaseDirectory(context, type)
val directory = baseDirectory?.findFolder(title)
if (directory?.exists() == true) {
@@ -88,15 +80,15 @@ class DownloadsManager(private val context: Context) {
}
when (type) {
MediaType.MANGA -> {
downloadsList.removeAll { it.titleName == title && it.type == MediaType.MANGA }
downloadsList.removeAll { it.title == title && it.type == MediaType.MANGA }
}
MediaType.ANIME -> {
downloadsList.removeAll { it.titleName == title && it.type == MediaType.ANIME }
downloadsList.removeAll { it.title == title && it.type == MediaType.ANIME }
}
MediaType.NOVEL -> {
downloadsList.removeAll { it.titleName == title && it.type == MediaType.NOVEL }
downloadsList.removeAll { it.title == title && it.type == MediaType.NOVEL }
}
}
saveDownloads()
@@ -119,7 +111,7 @@ class DownloadsManager(private val context: Context) {
if (directory?.exists() == true && directory.isDirectory) {
val files = directory.listFiles()
for (file in files) {
if (!downloadsSubLists.any { it.titleName == file.name }) {
if (!downloadsSubLists.any { it.title == file.name }) {
file.deleteRecursively(context, false)
}
}
@@ -128,19 +120,14 @@ class DownloadsManager(private val context: Context) {
val iterator = downloadsList.iterator()
while (iterator.hasNext()) {
val download = iterator.next()
val downloadDir = directory?.findFolder(download.titleName)
if ((downloadDir?.exists() == false && download.type == type) || download.titleName.isBlank()) {
val downloadDir = directory?.findFolder(download.title)
if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) {
iterator.remove()
}
}
}
fun moveDownloadsDir(
context: Context,
oldUri: Uri,
newUri: Uri,
finished: (Boolean, String) -> Unit
) {
fun moveDownloadsDir(context: Context, oldUri: Uri, newUri: Uri, finished: (Boolean, String) -> Unit) {
try {
if (oldUri == newUri) {
finished(false, "Source and destination are the same")
@@ -154,41 +141,18 @@ class DownloadsManager(private val context: Context) {
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 :
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.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"
)
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")
@@ -200,7 +164,7 @@ class DownloadsManager(private val context: Context) {
super.onCompleted(result)
}
})
}
}
} catch (e: Exception) {
snackString("Error: ${e.message}")
@@ -215,23 +179,23 @@ class DownloadsManager(private val context: Context) {
fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean {
return if (type == null) {
downloadsList.any { it.titleName == title && it.chapterName == chapter }
downloadsList.any { it.title == title && it.chapter == chapter }
} else {
downloadsList.any { it.titleName == title && it.chapterName == chapter && it.type == type }
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
}
}
private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) {
private fun removeDirectory(downloadedType: DownloadedType) {
val baseDirectory = getBaseDirectory(context, downloadedType.type)
val directory =
baseDirectory?.findFolder(downloadedType.titleName)
?.findFolder(downloadedType.chapterName)
downloadsList.remove(downloadedType)
baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
// Check if the directory exists and delete it recursively
if (directory?.exists() == true) {
val deleted = directory.deleteRecursively(context, false)
if (deleted) {
if (toast) snackString("Successfully deleted")
snackString("Successfully deleted")
} else {
snackString("Failed to delete directory")
}
@@ -262,7 +226,11 @@ class DownloadsManager(private val context: Context) {
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 = "|\\?*<\":>+[]/'"
fun String?.findValidName(): String {
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
}
/**
* Get and create a base directory for the given type
@@ -270,6 +238,7 @@ class DownloadsManager(private val context: 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
@@ -314,12 +283,7 @@ class DownloadsManager(private val context: Context) {
}
}
fun getDirSize(
context: Context,
type: MediaType,
title: String,
chapter: String? = null
): Long {
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 {
@@ -328,19 +292,6 @@ class DownloadsManager(private val context: Context) {
return size
}
fun addNoMedia(context: Context) {
val baseDirectory = getBaseDirectory(context) ?: return
if (baseDirectory.findFile(".nomedia") == null) {
baseDirectory.createFile("application/octet-stream", ".nomedia")
}
}
private fun getBaseDirectory(context: Context): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null
return DocumentFile.fromTreeUri(context, baseDirectory)
}
private fun DocumentFile.findOrCreateFolder(
name: String, overwrite: Boolean
): DocumentFile? {
@@ -352,38 +303,14 @@ class DownloadsManager(private val context: Context) {
}
}
private const val RATIO_THRESHOLD = 95
fun Media.compareName(name: String): Boolean {
val mainName = mainName().findValidName().lowercase()
val ratio = FuzzySearch.ratio(mainName, name.lowercase())
return ratio > RATIO_THRESHOLD
}
fun String.compareName(name: String): Boolean {
val mainName = findValidName().lowercase()
val compareName = name.findValidName().lowercase()
val ratio = FuzzySearch.ratio(mainName, compareName)
return ratio > RATIO_THRESHOLD
}
}
}
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
fun String?.findValidName(): String {
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
}
data class DownloadedType(
private val pTitle: String?,
private val pChapter: String?,
val type: MediaType,
@Deprecated("use pTitle instead")
private val title: String? = null,
@Deprecated("use pChapter instead")
private val chapter: String? = null
val pTitle: String, val pChapter: String, val type: MediaType
) : Serializable {
val titleName: String
get() = title?:pTitle.findValidName()
val chapterName: String
get() = chapter?:pChapter.findValidName()
val title: String
get() = pTitle.findValidName()
val chapter: String
get() = pChapter.findValidName()
}

View File

@@ -19,9 +19,7 @@ import androidx.documentfile.provider.DocumentFile
import androidx.media3.common.util.UnstableApi
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.defaultHeaders
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
@@ -34,10 +32,13 @@ import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.toast
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
@@ -73,7 +74,6 @@ class AnimeDownloaderService : Service() {
private val mutex = Mutex()
private var isCurrentlyProcessing = false
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
private val ffExtension = Injekt.get<DownloadAddonManager>().extension?.extension
override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services.
@@ -82,11 +82,6 @@ class AnimeDownloaderService : Service() {
override fun onCreate() {
super.onCreate()
if (ffExtension == null) {
toast(getString(R.string.download_addon_not_found))
stopSelf()
return
}
notificationManager = NotificationManagerCompat.from(this)
builder =
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
@@ -168,7 +163,7 @@ class AnimeDownloaderService : Service() {
.map { it.sessionId }.toMutableList()
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
sessionIds.forEach {
ffExtension!!.cancelDownload(it)
FFmpegKit.cancel(it)
}
currentTasks.removeAll { it.getTaskName() == taskName }
CoroutineScope(Dispatchers.Default).launch {
@@ -203,6 +198,7 @@ class AnimeDownloaderService : Service() {
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) {
try {
//val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
@@ -232,44 +228,61 @@ class AnimeDownloaderService : Service() {
var percent = 0
var totalLength = 0.0
val path = ffExtension!!.setDownloadPath(
val path = FFmpegKitConfig.getSafParameterForWrite(
this@AnimeDownloaderService,
outputFile.uri
)
val headersStringBuilder = StringBuilder()
val headersStringBuilder = StringBuilder().append(" ")
task.video.file.headers.forEach {
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
}
if (!task.video.file.headers.containsKey("User-Agent")) { //headers should never be empty now
headersStringBuilder.append("\"").append("User-Agent: ")
.append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'")
}
val probeRequest =
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\""
ffExtension.executeFFProbe(
probeRequest
) {
if (it.toDoubleOrNull() != null) {
totalLength = it.toDouble()
}
}
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()
var request = "-headers $headers "
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"
Logger.log("Request: $request")
println("Request: $request")
val ffTask =
ffExtension.executeFFMpeg(request) {
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
val timeInMilliseconds = it.time
if (timeInMilliseconds > 0 && totalLength > 0) {
percent = ((it / 1000) / totalLength * 100).toInt()
percent = ((it.time / 1000) / totalLength * 100).toInt()
}
Logger.log("Statistics: $it")
}
task.sessionId = ffTask
task.sessionId = ffTask.sessionId
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
ffTask
ffTask.sessionId
saveMediaInfo(task)
task.subtitle?.let {
@@ -285,8 +298,8 @@ class AnimeDownloaderService : Service() {
}
// periodically check if the download is complete
while (ffExtension.getState(ffTask) != "COMPLETED") {
if (ffExtension.getState(ffTask) == "FAILED") {
while (ffTask.state != SessionState.COMPLETED) {
if (ffTask.state == SessionState.FAILED) {
Logger.log("Download failed")
builder.setContentText(
"${
@@ -297,15 +310,14 @@ class AnimeDownloaderService : Service() {
} Download failed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
toast("${getTaskName(task.title, task.episode)} Download failed")
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
snackString("${getTaskName(task.title, task.episode)} Download failed")
Logger.log("Download failed: ${ffTask.failStackTrace}")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
),
false
)
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
@@ -333,8 +345,8 @@ class AnimeDownloaderService : Service() {
}
kotlinx.coroutines.delay(2000)
}
if (ffExtension.getState(ffTask) == "COMPLETED") {
if (ffExtension.hadError(ffTask)) {
if (ffTask.state == SessionState.COMPLETED) {
if (ffTask.returnCode.isValueError) {
Logger.log("Download failed")
builder.setContentText(
"${

View File

@@ -31,12 +31,9 @@ import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
import ani.dantotsu.download.findValidName
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.initActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
@@ -178,7 +175,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val media =
downloadManager.animeDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title.findValidName() }
media?.let {
lifecycleScope.launch {
val mediaModel = getMedia(it)
@@ -290,10 +287,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
}
downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val animeTitles = downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct()
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) {
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.titleName == title }
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineAnimeModel = loadOfflineAnimeModel(download)
newAnimeDownloads += offlineAnimeModel
@@ -316,7 +313,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
return try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
false, downloadedType.title
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
@@ -330,7 +327,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
})
.create()
val media = directory?.findFile("media.json")
?: return loadMediaCompat(downloadedType)
?: return null
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
@@ -355,7 +352,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
false, downloadedType.title
)
val mediaModel = getMedia(downloadedType)!!
val cover = directory?.findFile("cover.jpg")
@@ -366,7 +363,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val bannerUri: Uri? = if (banner?.exists() == true) {
banner.uri
} else null
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
@@ -395,26 +391,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
bannerUri
)
} catch (e: Exception) {
return try {
loadOfflineAnimeModelCompat(downloadedType)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
OfflineAnimeModel(
"unknown",
"0",
"??",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineAnimeModel(
"unknown",
"0",
"??",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
}

View File

@@ -28,13 +28,9 @@ import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineMangaModelCompat
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.findValidName
import ani.dantotsu.initActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
@@ -172,8 +168,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val media =
downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
media?.let {
lifecycleScope.launch {
ContextCompat.startActivity(
@@ -193,7 +189,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val type: MediaType =
if (downloadManager.mangaDownloadedTypes.any { it.titleName == item.title }) {
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
MediaType.MANGA
} else {
MediaType.NOVEL
@@ -281,19 +277,19 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
downloads = listOf()
downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName == title }
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.titleName }.distinct()
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName == title }
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
@@ -318,7 +314,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
return try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
false, downloadedType.title
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
@@ -326,7 +322,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
})
.create()
val media = directory?.findFile("media.json")
?: return DownloadCompat.loadMediaCompat(downloadedType)
?: return null
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
@@ -346,7 +342,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
false, downloadedType.title
)
val mediaModel = getMedia(downloadedType)!!
val cover = directory?.findFile("cover.jpg")
@@ -357,7 +353,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val bannerUri: Uri? = if (banner?.exists() == true) {
banner.uri
} else null
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
@@ -380,25 +375,21 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
bannerUri
)
} catch (e: Exception) {
return try {
loadOfflineMangaModelCompat(downloadedType)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
}

View File

@@ -9,6 +9,7 @@ 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
@@ -49,6 +50,8 @@ import okio.sink
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.io.IOException
import java.net.HttpURLConnection
import java.net.URL
@@ -267,9 +270,7 @@ class NovelDownloaderService : Service() {
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 outputStream = this@NovelDownloaderService.contentResolver.openOutputStream(file.uri) ?: throw Exception("Could not open OutputStream")
val sink = outputStream.sink().buffer()
val responseBody = response.body
@@ -357,7 +358,7 @@ class NovelDownloaderService : Service() {
private fun saveMediaInfo(task: DownloadTask) {
launchIO {
val directory =
getSubDirectory(
DownloadsManager.getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,

View File

@@ -7,10 +7,14 @@ import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.annotation.OptIn
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource
@@ -18,8 +22,11 @@ import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.NoOpCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadHelper
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadService
import androidx.media3.exoplayer.scheduler.Requirements
import ani.dantotsu.R
import ani.dantotsu.defaultHeaders
@@ -27,16 +34,21 @@ import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
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
import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.IOException
import java.util.concurrent.Executors
@SuppressLint("UnsafeOptInUsageError")
@@ -118,92 +130,4 @@ object Helper {
}
return true
}
@Synchronized
@UnstableApi
@Deprecated("exoplayer download manager is no longer used")
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
}
}
@Deprecated("exoplayer download manager is no longer used")
@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!!
}
}
@Synchronized
@Deprecated("exoplayer download manager is no longer used")
private fun getDownloadDirectory(context: Context): File {
if (downloadDirectory == null) {
downloadDirectory = context.getExternalFilesDir(null)
if (downloadDirectory == null) {
downloadDirectory = context.filesDir
}
}
return downloadDirectory!!
}
@Deprecated("exoplayer download manager is no longer used")
private var download: DownloadManager? = null
@Deprecated("exoplayer download manager is no longer used")
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
@Deprecated("exoplayer download manager is no longer used")
private var simpleCache: SimpleCache? = null
@Deprecated("exoplayer download manager is no longer used")
private var downloadDirectory: File? = null
}

View File

@@ -101,7 +101,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
ContextCompat.startActivity(
view.context,
Intent(view.context, ProfileActivity::class.java)
.putExtra("userId", Anilist.userid), null
.putExtra("userId", Anilist.userid),null
)
false
}
@@ -110,8 +110,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
trendingBinding.searchBar.performClick()
}
trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
listOf(
@@ -168,8 +167,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
trendingBinding.trendingProgressBar.visibility = View.GONE
trendingBinding.trendingViewPager.adapter = adaptor
trendingBinding.trendingViewPager.offscreenPageLimit = 3
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
RecyclerView.OVER_SCROLL_NEVER
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable {
@@ -197,7 +195,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
}
fun updateRecent(adaptor: MediaAdaptor) {
binding.apply {
binding.apply{
init(
adaptor,
animeUpdatedRecyclerView,
@@ -212,9 +210,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
}
}
fun updateMovies(adaptor: MediaAdaptor) {
binding.apply {
binding.apply{
init(
adaptor,
animeMoviesRecyclerView,
@@ -225,7 +222,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
}
fun updateTopRated(adaptor: MediaAdaptor) {
binding.apply {
binding.apply{
init(
adaptor,
animeTopRatedRecyclerView,
@@ -234,9 +231,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
)
}
}
fun updateMostFav(adaptor: MediaAdaptor) {
binding.apply {
binding.apply{
init(
adaptor,
animeMostFavRecyclerView,
@@ -245,8 +241,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
)
}
}
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =
@@ -261,7 +256,6 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
}
fun updateAvatar() {
if (Anilist.avatar != null && ready.value == true) {
trendingBinding.userAvatar.loadImage(Anilist.avatar)

View File

@@ -81,10 +81,7 @@ class HomeFragment : Fragment() {
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
)
blurImage(if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen, Anilist.bg)
binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
@@ -131,7 +128,7 @@ class HomeFragment : Fragment() {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity(
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
.putExtra("userId", Anilist.userid), null
.putExtra("userId", Anilist.userid),null
)
false
}
@@ -379,7 +376,6 @@ class HomeFragment : Fragment() {
}
}
}
override fun onResume() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) {

View File

@@ -172,13 +172,7 @@ class MangaFragment : Fragment() {
}
model.getPopularManhwa().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTrendingManhwa(
MediaAdaptor(
0,
it,
requireActivity()
)
)
mangaPageAdapter.updateTrendingManhwa(MediaAdaptor(0, it, requireActivity()))
}
}
model.getTopRated().observe(viewLifecycleOwner) {

View File

@@ -101,7 +101,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
ContextCompat.startActivity(
view.context,
Intent(view.context, ProfileActivity::class.java)
.putExtra("userId", Anilist.userid), null
.putExtra("userId", Anilist.userid),null
)
false
}
@@ -156,8 +156,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
trendingBinding.trendingProgressBar.visibility = View.GONE
trendingBinding.trendingViewPager.adapter = adaptor
trendingBinding.trendingViewPager.offscreenPageLimit = 3
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
RecyclerView.OVER_SCROLL_NEVER
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable {
@@ -192,7 +191,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
)
}
}
fun updateTrendingManhwa(adaptor: MediaAdaptor) {
binding.apply {
init(
@@ -203,7 +201,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
)
}
}
fun updateNovel(adaptor: MediaAdaptor) {
binding.apply {
init(
@@ -215,7 +212,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
}
}
fun updateTopRated(adaptor: MediaAdaptor) {
binding.apply {
init(
@@ -226,7 +222,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
)
}
}
fun updateMostFav(adaptor: MediaAdaptor) {
binding.apply {
init(
@@ -239,8 +234,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
mangaPopular.startAnimation(setSlideUp())
}
}
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
fun init(adaptor: MediaAdaptor,recyclerView: RecyclerView, progress: View, title: View){
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =

View File

@@ -3,10 +3,9 @@ package ani.dantotsu.media
import java.io.Serializable
data class Author(
var id: Int,
var name: String?,
var image: String?,
var role: String?,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
var character: ArrayList<Character>? = null
val id: Int,
val name: String?,
val image: String?,
val role: String?,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
) : Serializable

View File

@@ -12,7 +12,6 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.EmptyAdapter
import ani.dantotsu.R
import ani.dantotsu.Refresh
@@ -33,6 +32,7 @@ class AuthorActivity : AppCompatActivity() {
private val model: OtherDetailsViewModel by viewModels()
private var author: Author? = null
private var loaded = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -55,15 +55,14 @@ class AuthorActivity : AppCompatActivity() {
binding.studioClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
model.getAuthor().observe(this) {
if (it != null) {
author = it
loaded = true
binding.studioProgressBar.visibility = View.GONE
binding.studioRecycler.visibility = View.VISIBLE
if (author!!.yearMedia.isNullOrEmpty()) {
binding.studioRecycler.visibility = View.GONE
}
val titlePosition = arrayListOf<Int>()
val concatAdapter = ConcatAdapter()
val map = author!!.yearMedia ?: return@observe
@@ -90,19 +89,9 @@ class AuthorActivity : AppCompatActivity() {
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
concatAdapter.addAdapter(EmptyAdapter(empty))
}
binding.studioRecycler.adapter = concatAdapter
binding.studioRecycler.layoutManager = gridLayoutManager
binding.charactersRecycler.visibility = View.VISIBLE
binding.charactersText.visibility = View.VISIBLE
binding.charactersRecycler.adapter =
CharacterAdapter(author!!.character ?: arrayListOf())
binding.charactersRecycler.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
if (author!!.character.isNullOrEmpty()) {
binding.charactersRecycler.visibility = View.GONE
binding.charactersText.visibility = View.GONE
}
}
}
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }

View File

@@ -15,7 +15,7 @@ import ani.dantotsu.setAnimation
import java.io.Serializable
class AuthorAdapter(
private val authorList: ArrayList<Author>,
private val authorList: ArrayList<Author>
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
val binding =
@@ -23,7 +23,7 @@ class AuthorAdapter(
return AuthorViewHolder(binding)
}
override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
override fun onBindViewHolder(holder:AuthorViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root)
val author = authorList[position]

View File

@@ -14,6 +14,5 @@ data class Character(
var age: String? = null,
var gender: String? = null,
var dateOfBirth: FuzzyDate? = null,
var roles: ArrayList<Media>? = null,
val voiceActor: ArrayList<Author>? = null,
var roles: ArrayList<Media>? = null
) : Serializable

View File

@@ -28,7 +28,6 @@ class CharacterAdapter(
setAnimation(binding.root.context, holder.binding.root)
val character = characterList[position]
val whitespace = "${character.role} "
character.voiceActor
binding.itemCompactRelation.text = whitespace
binding.itemCompactImage.loadImage(character.image)
binding.itemCompactTitle.text = character.name

View File

@@ -95,8 +95,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
}
lifecycleScope.launch {
withContext(Dispatchers.IO) {
character.isFav =
Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
character.isFav = Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
}
withContext(Dispatchers.Main) {
binding.characterFav.setImageResource(

View File

@@ -2,9 +2,7 @@ package ani.dantotsu.media
import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.currActivity
@@ -29,28 +27,16 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
"${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
)
else -> character.gender
} else "") + "\n" + character.description
currActivity()!!.getString(R.string.male) -> currActivity()!!.getString(R.string.male)
currActivity()!!.getString(R.string.female) -> currActivity()!!.getString(R.string.female)
else -> character.gender
} else "") + "\n" + character.description
binding.characterDesc.isTextSelectable
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(SpoilerPlugin()).build()
markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
binding.voiceActorRecycler.adapter = AuthorAdapter(character.voiceActor ?: arrayListOf())
binding.voiceActorRecycler.layoutManager = LinearLayoutManager(
activity, LinearLayoutManager.HORIZONTAL, false
)
if (binding.voiceActorRecycler.adapter!!.itemCount == 0) {
binding.voiceActorContainer.visibility = View.GONE
}
}
override fun getItemCount(): Int = 1

View File

@@ -7,7 +7,6 @@ import ani.dantotsu.connections.anilist.api.MediaList
import ani.dantotsu.connections.anilist.api.MediaType
import ani.dantotsu.media.anime.Anime
import ani.dantotsu.media.manga.Manga
import ani.dantotsu.profile.User
import java.io.Serializable
import ani.dantotsu.connections.anilist.api.Media as ApiMedia
@@ -67,7 +66,7 @@ data class Media(
var sequel: Media? = null,
var relations: ArrayList<Media>? = null,
var recommendations: ArrayList<Media>? = null,
var users: ArrayList<User>? = null,
var vrvId: String? = null,
var crunchySlug: String? = null,
@@ -100,7 +99,7 @@ data class Media(
startDate = apiMedia.startDate,
endDate = apiMedia.endDate,
favourites = apiMedia.favourites,
timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000 },
timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000},
anime = if (apiMedia.type == MediaType.ANIME) Anime(
totalEpisodes = apiMedia.episodes,
nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1)
@@ -115,8 +114,7 @@ data class Media(
this.userScore = mediaList.score?.toInt() ?: 0
this.userStatus = mediaList.status?.toString()
this.userUpdatedAt = mediaList.updatedAt?.toLong()
this.genres =
mediaList.media?.genres?.toMutableList() as? ArrayList<String>? ?: arrayListOf()
this.genres = mediaList.media?.genres?.toMutableList() as? ArrayList<String>? ?: arrayListOf()
}
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {

View File

@@ -149,22 +149,14 @@ 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
)
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.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
)
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.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
}
@@ -191,10 +183,7 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator()
)
)
blurImage(
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
media.banner ?: media.cover
)
blurImage(if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen , media.banner ?: media.cover)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactTitle.text = media.userPreferredName
@@ -243,10 +232,7 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator()
)
)
blurImage(
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
media.banner ?: media.cover
)
blurImage(if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen , media.banner ?: media.cover)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactTitle.text = media.userPreferredName

View File

@@ -109,9 +109,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
// 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) {
@@ -127,11 +125,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
}
val navBarRightMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) navBarHeight else 0
Configuration.ORIENTATION_LANDSCAPE) navBarHeight else 0
val navBarBottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight
Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = navBarRightMargin
bottomMargin = navBarBottomMargin
@@ -349,13 +345,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
adult = media.isAdult
if (media.anime != null) {
viewPager.adapter =
ViewPagerAdapter(
supportFragmentManager,
lifecycle,
SupportedMedia.ANIME,
media,
intent.getIntExtra("commentId", -1)
)
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME, media, intent.getIntExtra("commentId", -1))
} else if (media.manga != null) {
viewPager.adapter = ViewPagerAdapter(
supportFragmentManager,
@@ -378,8 +368,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} else {
navBar.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
}
val commentTab =
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
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)
@@ -423,12 +412,10 @@ 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
Configuration.ORIENTATION_LANDSCAPE) navBarHeight else 0
val bottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight
val params: ViewGroup.MarginLayoutParams =
Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight
val params : ViewGroup.MarginLayoutParams =
navBar.layoutParams as ViewGroup.MarginLayoutParams
params.updateMargins(right = rightMargin, bottom = bottomMargin)
}
@@ -461,7 +448,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
SupportedMedia.MANGA -> MangaReadFragment()
SupportedMedia.NOVEL -> NovelReadFragment()
}
2 -> {
val fragment = CommentsFragment()
val bundle = Bundle()

View File

@@ -56,11 +56,9 @@ class MediaDetailsViewModel : ViewModel() {
media.anime != null -> {
AnimeSources.list.size - 1
}
media.format == "MANGA" || media.format == "ONE_SHOT" -> {
MangaSources.list.size - 1
}
else -> {
NovelSources.list.size - 1
}

View File

@@ -27,6 +27,7 @@ import ani.dantotsu.R
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
@@ -37,7 +38,6 @@ import ani.dantotsu.databinding.ItemTitleRecyclerBinding
import ani.dantotsu.databinding.ItemTitleSearchBinding
import ani.dantotsu.databinding.ItemTitleTextBinding
import ani.dantotsu.databinding.ItemTitleTrailerBinding
import ani.dantotsu.displayTimer
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
@@ -145,8 +145,7 @@ class MediaInfoFragment : Fragment() {
}
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
val seasonInfo =
"${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}"
val seasonInfo = "${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}"
binding.mediaInfoSeason.text = seasonInfo
if (media.anime.mainStudio != null) {
@@ -183,9 +182,9 @@ class MediaInfoFragment : Fragment() {
}
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
val infoTotal = if (media.anime.nextAiringEpisode != null)
"${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}"
"${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}"
else
(media.anime.totalEpisodes ?: "~").toString()
(media.anime.totalEpisodes ?: "~").toString()
binding.mediaInfoTotal.text = infoTotal
} else if (media.manga != null) {
@@ -214,8 +213,7 @@ 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)
val infoDesc = tripleTab + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
binding.mediaInfoDescription.text = infoDesc
binding.mediaInfoDescription.setOnClickListener {
@@ -227,7 +225,8 @@ class MediaInfoFragment : Fragment() {
.setDuration(400).start()
}
}
displayTimer(media, binding.mediaInfoContainer)
countDown(media, binding.mediaInfoContainer)
val parent = _binding?.mediaInfoContainer!!
val screenWidth = resources.displayMetrics.run { widthPixels / density }
@@ -572,23 +571,6 @@ class MediaInfoFragment : Fragment() {
parent.addView(root)
}
}
if (!media.users.isNullOrEmpty() && !offline) {
ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
itemTitle.setText(R.string.social)
itemRecycler.adapter =
MediaSocialAdapter(media.users!!)
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
}
}
}

View File

@@ -70,7 +70,7 @@ object MediaNameAdapter {
return if (seasonMatcher.find()) {
seasonMatcher.group(2)?.toInt()
} else {
text.toIntOrNull()
null
}
}
@@ -93,7 +93,7 @@ object MediaNameAdapter {
}
}
} else {
text.toFloatOrNull()
null
}
}
@@ -139,7 +139,7 @@ object MediaNameAdapter {
if (failedChapterNumberMatcher.find()) {
failedChapterNumberMatcher.group(1)?.toFloat()
} else {
text.toFloatOrNull()
null
}
}
}

View File

@@ -1,70 +0,0 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemFollowerGridBinding
import ani.dantotsu.loadImage
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.setAnimation
class MediaSocialAdapter(private val user: ArrayList<User>) :
RecyclerView.Adapter<MediaSocialAdapter.DeveloperViewHolder>() {
inner class DeveloperViewHolder(val binding: ItemFollowerGridBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeveloperViewHolder {
return DeveloperViewHolder(
ItemFollowerGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: DeveloperViewHolder, position: Int) {
holder.binding.apply {
val user = user[position]
val score = user.score?.div(10.0) ?: 0.0
setAnimation(root.context, root)
profileUserName.text = user.name
profileInfo.apply {
text = when (user.status) {
"CURRENT" -> "WATCHING"
else -> user.status ?: ""
}
visibility = View.VISIBLE
}
profileCompactUserProgress.text = user.progress.toString()
profileCompactScore.text = score.toString()
profileCompactTotal.text = " | ${user.totalEpisodes ?: "~"}"
profileUserAvatar.loadImage(user.pfp)
val scoreDrawable = if (score == 0.0) R.drawable.score else R.drawable.user_score
profileCompactScoreBG.apply {
visibility = View.VISIBLE
background = ContextCompat.getDrawable(root.context, scoreDrawable)
}
profileCompactProgressContainer.visibility = View.VISIBLE
profileUserAvatar.setOnClickListener {
val intent = Intent(root.context, ProfileActivity::class.java).apply {
putExtra("userId", user.id)
}
ContextCompat.startActivity(root.context, intent, null)
}
}
}
override fun getItemCount(): Int = user.size
}

View File

@@ -1,15 +1,11 @@
package ani.dantotsu.media
interface Type {
fun asText(): String
}
enum class MediaType : Type {
enum class MediaType {
ANIME,
MANGA,
NOVEL;
override fun asText(): String {
fun asText(): String {
return when (this) {
ANIME -> "Anime"
MANGA -> "Manga"
@@ -18,38 +14,12 @@ enum class MediaType : Type {
}
companion object {
fun fromText(string: String): MediaType? {
fun fromText(string : String): MediaType {
return when (string) {
"Anime" -> ANIME
"Manga" -> MANGA
"Novel" -> NOVEL
else -> {
null
}
}
}
}
}
enum class AddonType : Type {
TORRENT,
DOWNLOAD;
override fun asText(): String {
return when (this) {
TORRENT -> "Torrent"
DOWNLOAD -> "Download"
}
}
companion object {
fun fromText(string: String): AddonType? {
return when (string) {
"Torrent" -> TORRENT
"Download" -> DOWNLOAD
else -> {
null
}
else -> { ANIME }
}
}
}

View File

@@ -78,10 +78,8 @@ class SearchActivity : AppCompatActivity() {
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 = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")?.toIntOrNull() else null,
results = mutableListOf(),
hasNextPage = false
)

View File

@@ -13,6 +13,7 @@ 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
@@ -59,7 +60,6 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
}
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)
@@ -129,42 +129,36 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
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()
@@ -305,12 +299,14 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
private fun fadeInAnimation(): Animation {
return AlphaAnimation(0f, 1f).apply {
duration = 150
fillAfter = true
}
}
private fun fadeOutAnimation(): Animation {
return AlphaAnimation(1f, 0f).apply {
duration = 150
fillAfter = true
}
}
@@ -329,10 +325,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, private val searchAdapter: SearchAdapter) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList()

View File

@@ -105,8 +105,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
setSortByFilterImage()
binding.resetSearchFilter.setOnClickListener {
val rotateAnimation =
ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
@@ -114,8 +113,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
}
binding.resetSearchFilter.setOnLongClickListener {
val rotateAnimation =
ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
val rotateAnimation = ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
@@ -127,10 +125,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
CoroutineScope(Dispatchers.Main).launch {
activity.result.apply {
status =
binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source =
binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
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()
@@ -210,25 +206,21 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
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)
@@ -265,8 +257,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
binding.searchFilterCancel.setOnClickListener {
dismiss()
}
val format =
if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
val format = if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
binding.searchStatus.setAdapter(
ArrayAdapter(

View File

@@ -12,6 +12,7 @@ import kotlinx.coroutines.withContext
import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class SubtitleDownloader {
@@ -55,8 +56,8 @@ class SubtitleDownloader {
context,
downloadedType.type,
false,
downloadedType.titleName,
downloadedType.chapterName
downloadedType.title,
downloadedType.chapter
) ?: throw Exception("Could not create directory")
val type = loadSubtitleType(url)
directory.findFile("subtitle.${type}")?.delete()

View File

@@ -17,11 +17,11 @@ 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.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.displayTimer
import ani.dantotsu.isOnline
import ani.dantotsu.loadImage
import ani.dantotsu.media.Media
@@ -330,7 +330,6 @@ class AnimeWatchAdapter(
0
)
}
val chipText = "${names[limit * (position)]} - ${names[last - 1]}"
chip.text = chipText
chip.setTextColor(
@@ -413,12 +412,10 @@ class AnimeWatchAdapter(
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)
currActivity()!!.getString(R.string.filler_tag)
else
"", cleanedTitle)
binding.animeSourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp)
}
@@ -444,14 +441,11 @@ class AnimeWatchAdapter(
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
)
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 } }
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
binding.animeSourceDubbed.isChecked = selectDub
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
setLanguageList(0, nextIndex)
@@ -506,7 +500,8 @@ class AnimeWatchAdapter(
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
displayTimer(media, binding.animeSourceContainer)
//Timer
countDown(media, binding.animeSourceContainer)
}
}
}

View File

@@ -14,6 +14,7 @@ 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
@@ -24,6 +25,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadService
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -33,15 +35,14 @@ import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
import ani.dantotsu.download.DownloadsManager.Companion.findValidName
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.dp
import ani.dantotsu.isOnline
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaNameAdapter
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
@@ -199,16 +200,10 @@ class AnimeWatchFragment : Fragment() {
ConcatAdapter(headerAdapter, episodeAdapter)
lifecycleScope.launch(Dispatchers.IO) {
val offline =
!isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
if (offline) {
media.selected!!.sourceIndex = model.watchSources!!.list.lastIndex
} else {
awaitAll(
async { model.loadKitsuEpisodes(media) },
async { model.loadFillerEpisodes(media) }
)
}
awaitAll(
async { model.loadKitsuEpisodes(media) },
async { model.loadFillerEpisodes(media) }
)
model.loadEpisodes(media, media.selected!!.sourceIndex)
}
loaded = true
@@ -430,27 +425,17 @@ class AnimeWatchFragment : Fragment() {
}
fun onAnimeEpisodeDownloadClick(i: String) {
activity?.let {
activity?.let{
if (!hasDirAccess(it)) {
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
if (success) {
model.onEpisodeClick(
media,
i,
requireActivity().supportFragmentManager,
isDownload = true
)
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
} else {
snackString(getString(R.string.download_permission_required))
snackString("Permission is required to download")
}
}
} else {
model.onEpisodeClick(
media,
i,
requireActivity().supportFragmentManager,
isDownload = true
)
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
}
}
}
@@ -487,6 +472,10 @@ class AnimeWatchFragment : Fragment() {
)
) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
episodeAdapter.deleteDownload(i)
}
@@ -553,8 +542,8 @@ class AnimeWatchFragment : Fragment() {
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
episodeAdapter.notifyItemRangeInserted(0, arr.size)
for (download in downloadManager.animeDownloadedTypes) {
if (media.compareName(download.titleName)) {
episodeAdapter.stopDownload(download.chapterName)
if (download.title == media.mainName().findValidName()) {
episodeAdapter.stopDownload(download.chapter)
}
}
}

View File

@@ -17,7 +17,9 @@ 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.media.Media
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType

View File

@@ -48,8 +48,6 @@ import android.widget.ImageButton
import android.widget.Spinner
import android.widget.TextView
import androidx.activity.addCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
@@ -62,16 +60,12 @@ import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.C
import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE
import androidx.media3.common.C.TRACK_TYPE_AUDIO
import androidx.media3.common.C.TRACK_TYPE_TEXT
import androidx.media3.common.C.TRACK_TYPE_VIDEO
import androidx.media3.common.Format
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.TrackGroup
import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
@@ -111,7 +105,6 @@ import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityExoplayerBinding
import ani.dantotsu.defaultHeaders
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.video.Helper
import ani.dantotsu.dp
import ani.dantotsu.getCurrentBrightnessValue
import ani.dantotsu.hideSystemBars
@@ -135,13 +128,13 @@ 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.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.toPx
import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
@@ -160,12 +153,10 @@ import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
import java.util.Locale
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.collections.set
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@@ -198,7 +189,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
private lateinit var exoSettings: ImageButton
private lateinit var exoSubtitle: ImageButton
private lateinit var exoSubtitleView: SubtitleView
private lateinit var exoAudioTrack: ImageButton
private lateinit var exoRotate: ImageButton
private lateinit var exoSpeed: ImageButton
private lateinit var exoScreen: ImageButton
@@ -221,16 +211,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
private var orientationListener: OrientationEventListener? = null
private var downloadId: String? = null
private var hasExtSubtitles = false
companion object {
var initialized = false
lateinit var media: Media
private const val DEFAULT_MIN_BUFFER_MS = 600000
private const val DEFAULT_MAX_BUFFER_MS = 600000
private const val BUFFER_FOR_PLAYBACK_MS = 2500
private const val BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000
}
private lateinit var episode: Episode
@@ -298,7 +282,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}
}
playerView.findViewById<View>(androidx.media3.ui.R.id.exo_buffering).translationY =
(if (orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else (notchHeight + 8.toPx)).dp
(if (orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else (notchHeight + 8f.px)).dp
exoBrightnessCont.updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginEnd =
if (orientation == Configuration.ORIENTATION_LANDSCAPE) notchHeight else 0
@@ -388,31 +372,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
6 -> ResourcesCompat.getFont(this, R.font.blocky)
else -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
}
val fontSize = PrefManager.getVal<Int>(PrefName.FontSize).toFloat()
playerView.subtitleView?.let { subtitles ->
subtitles.setApplyEmbeddedStyles(false)
subtitles.setApplyEmbeddedFontSizes(false)
subtitles.setStyle(
CaptionStyleCompat(
primaryColor,
subBackground,
subWindow,
outline,
secondaryColor,
font
)
playerView.subtitleView?.setStyle(
CaptionStyleCompat(
primaryColor,
subBackground,
subWindow,
outline,
secondaryColor,
font
)
subtitles.alpha =
when (PrefManager.getVal<Boolean>(PrefName.Subtitles)) {
true -> PrefManager.getVal(PrefName.SubAlpha)
false -> 0f
}
subtitles.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize)
}
)
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -445,7 +414,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
exoSource = playerView.findViewById(R.id.exo_source)
exoSettings = playerView.findViewById(R.id.exo_settings)
exoSubtitle = playerView.findViewById(R.id.exo_sub)
exoAudioTrack = playerView.findViewById(R.id.exo_audio)
exoSubtitleView = playerView.findViewById(androidx.media3.ui.R.id.exo_subtitles)
exoRotate = playerView.findViewById(R.id.exo_rotate)
exoSpeed = playerView.findViewById(androidx.media3.ui.R.id.exo_playback_speed)
@@ -513,8 +481,17 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
it.visibility = View.GONE
}
}
setupSubFormatting(playerView)
playerView.subtitleView?.alpha = when (PrefManager.getVal<Boolean>(PrefName.Subtitles)) {
true -> PrefManager.getVal(PrefName.SubAlpha)
false -> 0f
}
val fontSize = PrefManager.getVal<Int>(PrefName.FontSize).toFloat()
playerView.subtitleView?.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize)
if (savedInstanceState != null) {
currentWindow = savedInstanceState.getInt(resumeWindow)
playbackPosition = savedInstanceState.getLong(resumePosition)
@@ -926,8 +903,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
isFastForwarding = true
exoPlayer.setPlaybackSpeed(exoPlayer.playbackParameters.speed * 2)
fastForward.visibility = View.VISIBLE
val speedText = "${exoPlayer.playbackParameters.speed}x"
fastForward.text = speedText
fastForward.text = "${exoPlayer.playbackParameters.speed}x"
}
fun stopFastForward() {
@@ -1220,7 +1196,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
putExtra("subtitle", subtitle)
}
exoPlayer.pause()
onChangeSettings.launch(intent)
startActivity(intent)
}
//Speed
@@ -1376,6 +1352,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
val ext = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return
extractor = ext
video = ext.videos.getOrNull(episode.selectedVideo) ?: return
subtitle = intent.getSerialized("subtitle")
?: when (val subLang: String? =
PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java)) {
@@ -1392,23 +1369,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}
//Subtitles
hasExtSubtitles = ext.subtitles.isNotEmpty()
if (hasExtSubtitles) {
exoSubtitle.isVisible = hasExtSubtitles
exoSubtitle.setOnClickListener {
subClick()
}
exoSubtitle.isVisible = ext.subtitles.isNotEmpty()
exoSubtitle.setOnClickListener {
subClick()
}
val sub: MutableList<MediaItem.SubtitleConfiguration> =
emptyList<MediaItem.SubtitleConfiguration>().toMutableList()
ext.subtitles.forEach { subtitle ->
val subtitleUrl = if (!hasExtSubtitles) video!!.file.url else subtitle.file.url
var sub: MediaItem.SubtitleConfiguration? = null
if (subtitle != null) {
//var localFile: String? = null
if (subtitle.type == SubtitleType.UNKNOWN) {
if (subtitle?.type == SubtitleType.UNKNOWN) {
runBlocking {
val type = SubtitleDownloader.loadSubtitleType(subtitleUrl)
val fileUri = Uri.parse(subtitleUrl)
sub += MediaItem.SubtitleConfiguration
val type = SubtitleDownloader.loadSubtitleType(subtitle!!.file.url)
val fileUri = Uri.parse(subtitle!!.file.url)
sub = MediaItem.SubtitleConfiguration
.Builder(fileUri)
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setMimeType(
@@ -1420,17 +1392,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}
)
.setId("69")
.setLanguage(subtitle.language)
.build()
}
println("sub: $sub")
} else {
val subUri = Uri.parse(subtitleUrl)
sub += MediaItem.SubtitleConfiguration
val subUri = Uri.parse(subtitle!!.file.url)
sub = MediaItem.SubtitleConfiguration
.Builder(subUri)
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setMimeType(
when (subtitle.type) {
when (subtitle?.type) {
SubtitleType.VTT -> MimeTypes.TEXT_VTT
SubtitleType.ASS -> MimeTypes.TEXT_SSA
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
@@ -1438,7 +1409,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}
)
.setId("69")
.setLanguage(subtitle.language)
.build()
}
}
@@ -1480,60 +1450,53 @@ 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()
downloadId = PrefManager.getAnimeDownloadPreferences()
.getString("$titleName - $episodeName", null) ?:
PrefManager.getAnimeDownloadPreferences()
.getString(ext.server.name, null)
val exoItem = if (downloadId != null) {
Helper.downloadManager(this)
.downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem()
} else null
if (exoItem != null) {
exoItem
} else {
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
}
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("Directory not found")
snackString("File not found")
null
}
} else {
snackString("Directory not found")
null
}
} else null
mediaItem = if (downloadedMediaItem == null) {
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
Logger.log("url: ${video!!.file.url}")
Logger.log("mimeType: $mimeType")
builder.setSubtitleConfigurations(sub)
if (sub != null) {
val listofnotnullsubs = listOfNotNull(sub)
builder.setSubtitleConfigurations(listofnotnullsubs)
}
builder.build()
} else {
if (sub.isNotEmpty()) {
val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon()
val addLanguage = sub[0].buildUpon().setLanguage("en").build()
val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon()
if (sub != null) {
val listofnotnullsubs = listOfNotNull(sub)
val addLanguage = listofnotnullsubs[0].buildUpon().setLanguage("en").build()
addedSubsDownloadedMediaItem.setSubtitleConfigurations(listOf(addLanguage))
episode.selectedSubtitle = 0
addedSubsDownloadedMediaItem.build()
} else {
downloadedMediaItem
}
addedSubsDownloadedMediaItem.build()
}
//Source
exoSource.setOnClickListener {
sourceClick()
@@ -1541,22 +1504,22 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
//Quality Track
trackSelector = DefaultTrackSelector(this)
val parameters = trackSelector.buildUponParameters()
.setAllowVideoMixedMimeTypeAdaptiveness(true)
.setAllowVideoNonSeamlessAdaptiveness(true)
.setSelectUndeterminedTextLanguage(true)
.setAllowAudioMixedMimeTypeAdaptiveness(true)
.setAllowMultipleAdaptiveSelections(true)
.setPreferredTextLanguage(subtitle?.language ?: Locale.getDefault().language)
.setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE)
.setRendererDisabled(TRACK_TYPE_VIDEO, false)
.setRendererDisabled(TRACK_TYPE_AUDIO, false)
.setRendererDisabled(TRACK_TYPE_TEXT, false)
.setMaxVideoSize(1, 1)
// .setOverrideForType(TrackSelectionOverride(trackSelector, TRACK_TYPE_VIDEO))
if (PrefManager.getVal(PrefName.SettingsPreferDub))
parameters.setPreferredAudioLanguage(Locale.getDefault().language)
trackSelector.setParameters(parameters)
trackSelector.setParameters(
trackSelector.buildUponParameters()
.setAllowVideoMixedMimeTypeAdaptiveness(true)
.setAllowVideoNonSeamlessAdaptiveness(true)
.setSelectUndeterminedTextLanguage(true)
.setAllowAudioMixedMimeTypeAdaptiveness(true)
.setAllowMultipleAdaptiveSelections(true)
.setPreferredTextLanguage(subtitle?.language ?: "en")
.setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE)
.setRendererDisabled(TRACK_TYPE_VIDEO, false)
.setRendererDisabled(C.TRACK_TYPE_AUDIO, false)
.setRendererDisabled(C.TRACK_TYPE_TEXT, false)
.setMaxVideoSize(1, 1)
//.setOverrideForType(
// TrackSelectionOverride(trackSelector, 2))
)
if (playbackPosition != 0L && !changingServer && !PrefManager.getVal<Boolean>(PrefName.AlwaysContinue)) {
val time = String.format(
@@ -1586,18 +1549,16 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}
}.show()
dialog.window?.setDimAmount(0.8f)
}
if (!this::exoPlayer.isInitialized) buildExoplayer()
val isDisabled = (subtitle == null && hasExtSubtitles)
exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
.buildUpon()
.setTrackTypeDisabled(TRACK_TYPE_TEXT, isDisabled)
.build()
} else buildExoplayer()
}
private fun buildExoplayer() {
//Player
val DEFAULT_MIN_BUFFER_MS = 600000
val DEFAULT_MAX_BUFFER_MS = 600000
val BUFFER_FOR_PLAYBACK_MS = 2500
val BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000
val loadControl = DefaultLoadControl.Builder()
.setBackBuffer(1000 * 60 * 2, true)
.setBufferDurationsMs(
@@ -1628,6 +1589,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}
playerView.player = exoPlayer
try {
val rightNow = Calendar.getInstance()
mediaSession = MediaSession.Builder(this, exoPlayer)
@@ -1640,11 +1602,32 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
exoPlayer.addListener(this)
exoPlayer.addAnalyticsListener(EventLogger())
isInitialized = true
if (!hasExtSubtitles && !PrefManager.getVal<Boolean>(PrefName.Subtitles)) {
onSetTrackGroupOverride(dummyTrack, TRACK_TYPE_TEXT)
}
}
/*private fun selectSubtitleTrack() { saving this for later
// Get the current track groups
val trackGroups = exoPlayer.currentTrackGroups
// Prepare a track selector parameters builder
val parametersBuilder = DefaultTrackSelector.ParametersBuilder(this)
// Iterate through the track groups to find the subtitle tracks
for (i in 0 until trackGroups.length) {
val trackGroup = trackGroups[i]
for (j in 0 until trackGroup.length) {
val trackMetadata = trackGroup.getFormat(j)
// Check if the track is a subtitle track
if (MimeTypes.isText(trackMetadata.sampleMimeType)) {
parametersBuilder.setRendererDisabled(i, false) // Enable the renderer for this track group
parametersBuilder.setSelectionOverride(i, trackGroups, DefaultTrackSelector.SelectionOverride(j, 0)) // Override to select this track
break
}
}
}
// Apply the track selector parameters to select the subtitle
trackSelector.setParameters(parametersBuilder)
}*/
private fun releasePlayer() {
isPlayerPlaying = exoPlayer.playWhenReady
@@ -1819,8 +1802,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (isInitialized) {
val playerCurrentTime = exoPlayer.currentPosition / 1000
currentTimeStamp = model.timeStamps.value?.find { timestamp ->
timestamp.interval.startTime < playerCurrentTime
&& playerCurrentTime < (timestamp.interval.endTime - 1)
timestamp.interval.startTime < playerCurrentTime && playerCurrentTime < (timestamp.interval.endTime - 1)
}
val new = currentTimeStamp
@@ -1843,7 +1825,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
override fun onTick(millisUntilFinished: Long) {
if (new == null) {
skipTimeButton.visibility = View.GONE
exoSkip.isVisible = PrefManager.getVal<Int>(PrefName.SkipTime) > 0
exoSkip.visibility = View.VISIBLE
disappeared = false
functionstarted = false
cancelTimer()
@@ -1852,7 +1834,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
override fun onFinish() {
skipTimeButton.visibility = View.GONE
exoSkip.isVisible = PrefManager.getVal<Int>(PrefName.SkipTime) > 0
exoSkip.visibility = View.VISIBLE
disappeared = true
functionstarted = false
cancelTimer()
@@ -1873,7 +1855,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
}
}
}
if (PrefManager.getVal(PrefName.AutoSkipOPED) && (new.skipType == "op" || new.skipType == "ed")
&& !skippedTimeStamps.contains(new)
@@ -1881,13 +1862,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
skippedTimeStamps.add(new)
}
if (PrefManager.getVal(PrefName.AutoSkipRecap) && new.skipType == "recap" && !skippedTimeStamps.contains(
new
)
) {
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
skippedTimeStamps.add(new)
}
new.skipType.getType()
} else {
disappeared = false
@@ -1902,95 +1876,43 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}, 500)
}
fun onSetTrackGroupOverride(trackGroup: Tracks.Group, type: @C.TrackType Int, index: Int = 0) {
val isDisabled = trackGroup.getTrackFormat(0).language == "none"
exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
.buildUpon()
.setTrackTypeDisabled(TRACK_TYPE_TEXT, isDisabled)
.setOverrideForType(
TrackSelectionOverride(trackGroup.mediaTrackGroup, index)
)
.build()
if (type == TRACK_TYPE_TEXT) setupSubFormatting(playerView)
playerView.subtitleView?.alpha = when (isDisabled) {
false -> PrefManager.getVal(PrefName.SubAlpha)
true -> 0f
}
}
private val dummyTrack = Tracks.Group(
TrackGroup("Dummy Track", Format.Builder().apply { setLanguage("none") }.build()),
true,
intArrayOf(1),
booleanArrayOf(false)
)
override fun onTracksChanged(tracks: Tracks) {
val audioTracks: ArrayList<Tracks.Group> = arrayListOf()
val subTracks: ArrayList<Tracks.Group> = arrayListOf(dummyTrack)
tracks.groups.forEach {
println(
"Track__: $it\nTrack__: ${it.length}\nTrack__: ${it.isSelected}\n" +
"Track__: ${it.type}\nTrack__: ${it.mediaTrackGroup.id}"
)
when (it.type) {
TRACK_TYPE_AUDIO -> {
if (it.isSupported(true)) audioTracks.add(it)
}
TRACK_TYPE_TEXT -> {
if (!hasExtSubtitles) {
if (it.isSupported(true)) subTracks.add(it)
return@forEach
}
}
println("Track__: $it")
println("Track__: ${it.length}")
println("Track__: ${it.isSelected}")
println("Track__: ${it.type}")
println("Track__: ${it.mediaTrackGroup.id}")
if (it.type == 3 && it.mediaTrackGroup.id == "1:") {
playerView.player?.trackSelectionParameters =
playerView.player?.trackSelectionParameters?.buildUpon()
?.setOverrideForType(
TrackSelectionOverride(it.mediaTrackGroup, it.length - 1)
)
?.build()!!
} else if (it.type == 3) {
playerView.player?.trackSelectionParameters =
playerView.player?.trackSelectionParameters?.buildUpon()
?.addOverride(
TrackSelectionOverride(it.mediaTrackGroup, listOf())
)
?.build()!!
}
}
exoAudioTrack.isVisible = audioTracks.size > 1
exoAudioTrack.setOnClickListener {
TrackGroupDialogFragment(this, audioTracks, TRACK_TYPE_AUDIO)
.show(supportFragmentManager, "dialog")
}
if (!hasExtSubtitles) {
exoSubtitle.isVisible = subTracks.size > 1
exoSubtitle.setOnClickListener {
TrackGroupDialogFragment(this, subTracks, TRACK_TYPE_TEXT)
.show(supportFragmentManager, "dialog")
}
}
}
private val onChangeSettings = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { _: ActivityResult ->
if (!hasExtSubtitles) {
exoPlayer.currentTracks.groups.forEach { trackGroup ->
when (trackGroup.type) {
TRACK_TYPE_TEXT -> {
if (PrefManager.getVal(PrefName.Subtitles)) {
onSetTrackGroupOverride(trackGroup, TRACK_TYPE_TEXT)
} else {
onSetTrackGroupOverride(dummyTrack, TRACK_TYPE_TEXT)
}
}
else -> {}
}
}
}
if (isInitialized) exoPlayer.play()
println("Track: ${tracks.groups.size}")
}
override fun onPlayerError(error: PlaybackException) {
when (error.errorCode) {
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED -> {
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED
-> {
toast("Source Exception : ${error.message}")
isPlayerPlaying = true
sourceClick()
}
else -> {
else
-> {
toast("Player Error ${error.errorCode} (${error.errorCodeName}) : ${error.message}")
Injekt.get<CrashlyticsInterface>().logException(error)
}
@@ -2000,6 +1922,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
private var isBuffering = true
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == ExoPlayer.STATE_READY) {
exoPlayer.play()
if (episodeLength == 0f) {
episodeLength = exoPlayer.duration.toFloat()
@@ -2054,7 +1977,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}
}
@SuppressLint("UnsafeIntentLaunch")
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
finishAndRemoveTask()
@@ -2084,11 +2006,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
// Cast
private fun cast() {
val videoURL = video?.file?.url ?: return
val subtitleUrl = if (!hasExtSubtitles) video!!.file.url else subtitle!!.file.url
val shareVideo = Intent(Intent.ACTION_VIEW)
shareVideo.setDataAndType(Uri.parse(videoURL), "video/*")
shareVideo.setPackage("com.instantbits.cast.webvideo")
if (subtitle != null) shareVideo.putExtra("subtitle", subtitleUrl)
if (subtitle != null) shareVideo.putExtra("subtitle", subtitle!!.file.url)
shareVideo.putExtra(
"title",
media.userPreferredName + " : Ep " + episodeTitleArr[currentEpisodeIndex]
@@ -2284,4 +2205,4 @@ class CustomCastButton : MediaRouteButton {
true
}
}
}
}

View File

@@ -24,8 +24,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.copyToClipboard
import ani.dantotsu.currActivity
@@ -49,16 +47,12 @@ import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.tryWith
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.DecimalFormat
@@ -236,12 +230,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
}
private val externalPlayerResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult ->
ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
Logger.log(result.data.toString())
}
private fun exportMagnetIntent(episode: Episode, video: Video): Intent {
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")
@@ -259,70 +252,32 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
}
}
@OptIn(DelicateCoroutinesApi::class)
@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:") || url.endsWith(".torrent")) {
val torrentExtension = Injekt.get<TorrentAddonManager>()
if (torrentExtension.isAvailable()) {
val activity = currActivity() ?: requireActivity()
launchIO {
val extension = torrentExtension.extension!!.extension
torrentExtension.torrentHash?.let {
extension.removeTorrent(it)
}
val index = if (url.contains("index=")) {
url.substringAfter("index=").toIntOrNull() ?: 0
} else 0
Logger.log("Sending: ${url}, ${video.quality}, $index")
val currentTorrent = extension.addTorrent(
url, video.quality.toString(), "", "", false
)
torrentExtension.torrentHash = currentTorrent.hash
video.file.url = extension.getLink(currentTorrent, index)
Logger.log("Received: ${video.file.url}")
if (launch == true) {
Intent(activity, ExoplayerView::class.java).apply {
ExoplayerView.media = media
ExoplayerView.initialized = true
startActivity(this)
}
} else {
model.setEpisode(
media.anime!!.episodes!![media.anime.selectedEpisode!!]!!,
"startExo no launch"
)
}
dismiss()
}
} else {
if (url.startsWith("magnet:")) {
try {
externalPlayerResult.launch(exportMagnetIntent(ep, video))
} catch (e: ActivityNotFoundException) {
val amnis = "com.amnis"
try {
externalPlayerResult.launch(exportMagnetIntent(ep, video))
startActivity(Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$amnis"))
)
} catch (e: ActivityNotFoundException) {
val amnis = "com.amnis"
try {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$amnis")
)
)
dismiss()
} catch (e: ActivityNotFoundException) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=$amnis")
)
)
}
startActivity(Intent(
Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=$amnis")
))
}
}
return
@@ -330,7 +285,6 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
}
}
dismiss()
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
stopAddingToList()
val intent = Intent(activity, ExoplayerView::class.java)
@@ -449,11 +403,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
SubtitleDownloader.downloadSubtitle(
requireContext(),
subtitleToDownload!!.file.url,
DownloadedType(
media!!.mainName(),
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number,
MediaType.ANIME
)
DownloadedType(media!!.mainName(), media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number, MediaType.ANIME)
)
}
}
@@ -480,45 +430,13 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
media!!.userPreferredName
)
} else {
val downloadAddonManager: DownloadAddonManager = Injekt.get()
if (!downloadAddonManager.isAvailable()){
toast("Download Extension not available")
return@setSafeOnClickListener
}
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
val selectedVideo =
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
val subtitles = extractor.subtitles
val subtitleNames = subtitles.map { it.language }
var subtitleToDownload: Subtitle? = null
val activity = currActivity() ?: requireActivity()
selectedVideo?.file?.url?.let { url ->
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
val torrentExtension = Injekt.get<TorrentAddonManager>()
if (!torrentExtension.isAvailable()) {
toast("Torrent Extension not available")
return@setSafeOnClickListener
}
runBlocking {
withContext(Dispatchers.IO) {
val extension = torrentExtension.extension!!.extension
torrentExtension.torrentHash?.let {
extension.removeTorrent(it)
}
val index = if (url.contains("index=")) {
url.substringAfter("index=").toIntOrNull() ?: 0
} else 0
Logger.log("Sending: ${url}, ${selectedVideo.quality}, $index")
val currentTorrent = extension.addTorrent(
url, selectedVideo.quality.toString(), "", "", false
)
torrentExtension.torrentHash = currentTorrent.hash
selectedVideo.file.url =
extension.getLink(currentTorrent, index)
Logger.log("Received: ${selectedVideo.file.url}")
}
}
}
}
val activity = currActivity()?:requireActivity()
if (subtitles.isNotEmpty()) {
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Subtitle")
@@ -592,13 +510,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
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
)
}"
)
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.urlNote.visibility = View.VISIBLE

View File

@@ -67,11 +67,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
binding.subtitleTitle.setText(R.string.none)
model.getMedia().observe(viewLifecycleOwner) { media ->
val mediaID: Int = media.id
val selSubs = PrefManager.getNullableCustomVal(
"subLang_${mediaID}",
null,
String::class.java
)
val selSubs = PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java)
if (episode.selectedSubtitle != null && selSubs != "None") {
binding.root.setCardBackgroundColor(TRANSPARENT)
}
@@ -111,11 +107,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
model.getMedia().observe(viewLifecycleOwner) { media ->
val mediaID: Int = media.id
val selSubs: String? =
PrefManager.getNullableCustomVal(
"subLang_${mediaID}",
null,
String::class.java
)
PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java)
if (episode.selectedSubtitle != position - 1 && selSubs != subtitles[position - 1].language) {
binding.root.setCardBackgroundColor(TRANSPARENT)
}

View File

@@ -1,119 +0,0 @@
package ani.dantotsu.media.anime
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.media3.common.C.TRACK_TYPE_AUDIO
import androidx.media3.common.C.TrackType
import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetSubtitlesBinding
import ani.dantotsu.databinding.ItemSubtitleTextBinding
import java.util.Locale
@OptIn(UnstableApi::class)
class TrackGroupDialogFragment(
instance: ExoplayerView, trackGroups: ArrayList<Tracks.Group>, type: @TrackType Int
) : BottomSheetDialogFragment() {
private var _binding: BottomSheetSubtitlesBinding? = null
private val binding get() = _binding!!
private var instance: ExoplayerView
private var trackGroups: ArrayList<Tracks.Group>
private var type: @TrackType Int
init {
this.instance = instance
this.trackGroups = trackGroups
this.type = type
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (type == TRACK_TYPE_AUDIO) binding.selectionTitle.text = getString(R.string.audio_tracks)
binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext())
binding.subtitlesRecycler.adapter = TrackGroupAdapter()
}
inner class TrackGroupAdapter : RecyclerView.Adapter<TrackGroupAdapter.StreamViewHolder>() {
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
StreamViewHolder(
ItemSubtitleTextBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
@OptIn(UnstableApi::class)
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val binding = holder.binding
trackGroups[position].let { trackGroup ->
when (val language = trackGroup.getTrackFormat(0).language?.lowercase()) {
null -> {
binding.subtitleTitle.text =
getString(R.string.unknown_track, "Track $position")
}
"none" -> {
binding.subtitleTitle.text = getString(R.string.disabled_track)
}
else -> {
val locale = if (language.contains("-")) {
val parts = language.split("-")
try {
Locale(parts[0], parts[1])
} catch (ignored: Exception) {
null
}
} else {
try {
Locale(language)
} catch (ignored: Exception) {
null
}
}
binding.subtitleTitle.text = locale?.let {
"[${it.language}] ${it.displayName}"
} ?: getString(R.string.unknown_track, language)
}
}
if (trackGroup.isSelected) {
val selected = "${binding.subtitleTitle.text}"
binding.subtitleTitle.text = selected
}
binding.root.setOnClickListener {
dismiss()
instance.onSetTrackGroupOverride(trackGroup, type)
}
}
}
override fun getItemCount(): Int = trackGroups.size
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
}

View File

@@ -60,8 +60,7 @@ class CommentItem(
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
binding = viewBinding
setAnimation(binding.root.context, binding.root)
viewBinding.commentRepliesList.layoutManager =
LinearLayoutManager(commentsFragment.activity)
viewBinding.commentRepliesList.layoutManager = LinearLayoutManager(commentsFragment.activity)
viewBinding.commentRepliesList.adapter = adapter
val isUserComment = CommentsAPI.userId == comment.userId
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
@@ -113,20 +112,16 @@ class CommentItem(
viewBinding.commentUserName.setOnClickListener {
ContextCompat.startActivity(
commentsFragment.activity,
Intent(commentsFragment.activity, ProfileActivity::class.java)
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
.putExtra("userId", comment.userId.toInt())
.putExtra("userLVL", "[${levelColor.second}]"),
null
.putExtra("userLVL","[${levelColor.second}]"), null
)
}
viewBinding.commentUserAvatar.setOnClickListener {
ContextCompat.startActivity(
commentsFragment.activity,
Intent(commentsFragment.activity, ProfileActivity::class.java)
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
.putExtra("userId", comment.userId.toInt())
.putExtra("userLVL", "[${levelColor.second}]"),
null
.putExtra("userLVL","[${levelColor.second}]"), null
)
}
viewBinding.commentText.setOnLongClickListener {
@@ -148,10 +143,8 @@ class CommentItem(
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.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) {
@@ -280,16 +273,12 @@ class CommentItem(
}
fun replying(isReplying: Boolean) {
binding.commentReply.text =
if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply"
binding.commentReply.text = if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply"
this.isReplying = isReplying
}
fun editing(isEditing: Boolean) {
binding.commentEdit.text =
if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(
R.string.edit
)
binding.commentEdit.text = if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(R.string.edit)
this.isEditing = isEditing
}
@@ -297,7 +286,7 @@ class CommentItem(
subCommentIds.add(id)
}
private fun removeSubCommentIds() {
private fun removeSubCommentIds(){
subCommentIds.forEach { id ->
@Suppress("UNCHECKED_CAST")
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
@@ -317,13 +306,11 @@ class CommentItem(
viewBinding.commentUpVote.alpha = 1f
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
}
-1 -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
viewBinding.commentDownVote.alpha = 1f
}
else -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
@@ -368,8 +355,7 @@ class CommentItem(
private fun getAvatarColor(voteCount: Int, backgroundColor: Int): Pair<Int, Int> {
val level = if (voteCount < 0) 0 else sqrt(abs(voteCount.toDouble()) / 0.8).toInt()
val colorString =
if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level]
val colorString = if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level]
var color = Color.parseColor(colorString)
val ratio = getContrastRatio(color, backgroundColor)
if (ratio < 4.5) {
@@ -387,17 +373,16 @@ class CommentItem(
* @param callback the callback to call when the user clicks yes
*/
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
val alertDialog =
android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
.setTitle(title)
.setMessage(message)
.setPositiveButton("Yes") { dialog, _ ->
callback()
dialog.dismiss()
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}
val alertDialog = android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
.setTitle(title)
.setMessage(message)
.setPositiveButton("Yes") { dialog, _ ->
callback()
dialog.dismiss()
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
}

View File

@@ -75,10 +75,7 @@ class CommentsFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
activity = requireActivity() as MediaDetailsActivity
binding.commentsListContainer.setBaseline(
activity.navBar,
activity.binding.commentInputLayout
)
binding.commentsListContainer.setBaseline(activity.navBar, activity.binding.commentInputLayout)
//get the media id from the intent
val mediaId = arguments?.getInt("mediaId") ?: -1
@@ -121,6 +118,7 @@ class CommentsFragment : Fragment() {
}
}
} else {
toast("Not logged in")
activity.binding.commentMessageContainer.visibility = View.GONE
}
@@ -303,7 +301,7 @@ class CommentsFragment : Fragment() {
activity.binding.commentLabel.setOnClickListener {
//alert dialog to enter a number, with a cancel and ok button
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup)
.setTitle("Enter a chapter/episode number tag")
.setView(R.layout.dialog_edittext)
.setPositiveButton("OK") { dialog, _ ->
@@ -579,7 +577,7 @@ class CommentsFragment : Fragment() {
* Called when the user tries to comment for the first time
*/
private fun showCommentRulesDialog() {
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup)
.setTitle("Commenting Rules")
.setMessage(
"I WILL BAN YOU WITHOUT HESITATION\n" +

View File

@@ -343,7 +343,6 @@ class MangaChapterAdapter(
fun updateType(t: Int) {
type = t
}
private fun formatDate(timestamp: Long?): String {
timestamp ?: return "" // Return empty string if timestamp is null
@@ -367,7 +366,6 @@ class MangaChapterAdapter(
else -> "Just now"
}
}
1L -> "1 day ago"
in 2..6 -> "$daysDifference days ago"
else -> SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(targetDate)

View File

@@ -229,7 +229,7 @@ class MangaReadAdapter(
refresh = true
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
startActivity(fragment.requireContext(), intent, null)
ContextCompat.startActivity(fragment.requireContext(), intent, null)
}
}
}
@@ -258,10 +258,8 @@ class MangaReadAdapter(
dialogBinding.animeScanlatorContainer.isVisible = options.count() > 1
dialogBinding.scanlatorNo.text = "${options.count()}"
dialogBinding.animeScanlatorTop.setOnClickListener {
val dialogView2 =
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
val checkboxContainer =
dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
val dialogView2 = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
val checkboxContainer = dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
val tickAllButton = dialogView2.findViewById<ImageButton>(R.id.toggleButton)
// Function to get the right image resource for the toggle button
@@ -443,11 +441,7 @@ class MangaReadAdapter(
if (media.manga?.chapters != null) {
val chapters = media.manga.chapters!!.keys.toTypedArray()
val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = PrefManager.getNullableCustomVal(
"${media.id}_current_chp",
null,
String::class.java
)
val appEp = PrefManager.getNullableCustomVal("${media.id}_current_chp", null, String::class.java)
?.toIntOrNull() ?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
val filteredChapters = chapters.filter { chapterKey ->
@@ -476,11 +470,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()) ep.title else "")
binding.animeSourceContinue.setOnClickListener {
fragment.onMangaChapterClick(continueEp)
}
@@ -501,14 +491,11 @@ class MangaReadAdapter(
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
)
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 } }
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
setLanguageList(0, nextIndex)
}
subscribeButton(false)

View File

@@ -16,6 +16,7 @@ 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
@@ -34,16 +35,15 @@ import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
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.isOnline
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaNameAdapter
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
@@ -194,8 +194,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
)
for (download in downloadManager.mangaDownloadedTypes) {
if (media.compareName(download.titleName)) {
chapterAdapter.stopDownload(download.chapterName)
if (download.title == media.mainName().findValidName()) {
chapterAdapter.stopDownload(download.chapter)
}
}
@@ -203,10 +203,6 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
ConcatAdapter(headerAdapter, chapterAdapter)
lifecycleScope.launch(Dispatchers.IO) {
val offline =
!isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
if (offline) media.selected!!.sourceIndex =
model.mangaReadSources!!.list.lastIndex
model.loadMangaChapters(media, media.selected!!.sourceIndex)
}
loaded = true
@@ -495,7 +491,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
if (success) {
continueDownload()
} else {
snackString(getString(R.string.download_permission_required))
snackString("Permission is required to download")
}
}
} else {

View File

@@ -42,8 +42,7 @@ abstract class BaseImageAdapter(
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
images = if (settings.layout == CurrentReaderSettings.Layouts.PAGED
&& settings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP
) {
&& settings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP) {
chapterImages.reversed()
} else {
chapterImages

View File

@@ -56,8 +56,8 @@ import ani.dantotsu.isOnline
import ani.dantotsu.logError
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaNameAdapter
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.others.ImageViewDialog
@@ -129,12 +129,10 @@ 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
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)) {
@@ -231,7 +229,7 @@ class MangaReaderActivity : AppCompatActivity() {
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 }
?: 1))
else
if (defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP) {
if (defaultSettings.direction == CurrentReaderSettings.Directions.BOTTOM_TO_TOP ) {
binding.mangaReaderPager.currentItem =
(maxChapterPage.toInt() - value.toInt()) / (dualPage { 2 } ?: 1)
} else {
@@ -347,11 +345,7 @@ class MangaReaderActivity : AppCompatActivity() {
if (currentChapterIndex > 0) change(currentChapterIndex - 1)
else snackString(getString(R.string.first_chapter))
} else {
if (chaptersArr.size > currentChapterIndex + 1) progress {
change(
currentChapterIndex + 1
)
}
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
else snackString(getString(R.string.next_chapter_not_found))
}
}
@@ -361,11 +355,7 @@ class MangaReaderActivity : AppCompatActivity() {
}
binding.mangaReaderPreviousChapter.setOnClickListener {
if (directionRLBT) {
if (chaptersArr.size > currentChapterIndex + 1) progress {
change(
currentChapterIndex + 1
)
}
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
else snackString(getString(R.string.next_chapter_not_found))
} else {
if (currentChapterIndex > 0) change(currentChapterIndex - 1)
@@ -382,15 +372,11 @@ class MangaReaderActivity : AppCompatActivity() {
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) ?: ""
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
@@ -403,12 +389,10 @@ class MangaReaderActivity : AppCompatActivity() {
"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/"
@@ -417,7 +401,6 @@ class MangaReaderActivity : AppCompatActivity() {
RPC.Link("View My AniList", anilistLink)
)
}
else -> mutableListOf()
}
val presence = RPC.createPresence(
@@ -428,12 +411,8 @@ 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
)
},
largeImage = media.cover?.let { cover -> RPC.Link(media.userPreferredName, cover) },
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
buttons = buttons
)
)
@@ -940,12 +919,7 @@ 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() }

View File

@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
@@ -12,13 +13,13 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
@@ -26,22 +27,19 @@ import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.novel.NovelDownloaderService
import ani.dantotsu.download.novel.NovelServiceDataSingleton
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
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
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import ani.dantotsu.util.StoragePermissions
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class NovelReadFragment : Fragment(),
DownloadTriggerCallback,
@@ -64,40 +62,26 @@ class NovelReadFragment : Fragment(),
override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) {
Logger.log("novel link: ${novelDownloadPackage.link}")
activity?.let {
fun continueDownload() {
val downloadTask = NovelDownloaderService.DownloadTask(
title = media.mainName(),
chapter = novelDownloadPackage.novelName,
downloadLink = novelDownloadPackage.link,
originalLink = novelDownloadPackage.originalLink,
sourceMedia = media,
coverUrl = novelDownloadPackage.coverUrl,
retries = 2,
)
NovelServiceDataSingleton.downloadQueue.offer(downloadTask)
CoroutineScope(Dispatchers.IO).launch {
val downloadTask = NovelDownloaderService.DownloadTask(
title = media.mainName(),
chapter = novelDownloadPackage.novelName,
downloadLink = novelDownloadPackage.link,
originalLink = novelDownloadPackage.originalLink,
sourceMedia = media,
coverUrl = novelDownloadPackage.coverUrl,
retries = 2,
)
NovelServiceDataSingleton.downloadQueue.offer(downloadTask)
CoroutineScope(Dispatchers.IO).launch {
if (!NovelServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, NovelDownloaderService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent)
}
NovelServiceDataSingleton.isServiceRunning = true
}
if (!NovelServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, NovelDownloaderService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent)
}
NovelServiceDataSingleton.isServiceRunning = true
}
if (!StoragePermissions.hasDirAccess(it)) {
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
if (success) {
continueDownload()
} else {
snackString(getString(R.string.download_permission_required))
}
}
} else {
continueDownload()
}
}
}
@@ -113,14 +97,8 @@ class NovelReadFragment : Fragment(),
) {
try {
val directory =
DownloadsManager.getSubDirectory(
context ?: currContext()!!,
MediaType.NOVEL,
false,
media.mainName(),
novel.name
)
val file = directory?.findFile("0.epub")
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 {

View File

@@ -8,7 +8,6 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemNovelResponseBinding
import ani.dantotsu.loadImage
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.setAnimation
import ani.dantotsu.snackString
@@ -38,7 +37,10 @@ class NovelResponseAdapter(
val binding = holder.binding
val novel = list[position]
setAnimation(fragment.requireContext(), holder.binding.root)
binding.itemEpisodeImage.loadImage(novel.coverUrl, 400, 0)
val cover = GlideUrl(novel.coverUrl.url) { novel.coverUrl.headers }
Glide.with(binding.itemEpisodeImage).load(cover).override(400, 0)
.into(binding.itemEpisodeImage)
val typedValue = TypedValue()
fragment.requireContext().theme?.resolveAttribute(
@@ -179,7 +181,7 @@ class NovelResponseAdapter(
if (position != -1) {
list[position].extra?.remove("0")
list[position].extra?.set("0", "Downloading: $progress%")
Logger.log("updateDownloadProgress: $progress, position: $position")
Logger.log( "updateDownloadProgress: $progress, position: $position")
notifyItemChanged(position)
}
}

View File

@@ -292,11 +292,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
applySettings()
}
val cfi = PrefManager.getNullableCustomVal(
"${sanitizedBookId}_progress",
null,
String::class.java
)
val cfi = PrefManager.getNullableCustomVal("${sanitizedBookId}_progress", null, String::class.java)
cfi?.let { binding.bookReader.goto(it) }
binding.progress.visibility = View.GONE

View File

@@ -70,10 +70,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 = getString(R.string.user_list, intent.getStringExtra("username"),
if (anime) getString(R.string.anime) else getString(R.string.manga))
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
this@ListActivity.selectedTabIdx = tab?.position ?: 0
@@ -160,8 +158,7 @@ class ListActivity : AppCompatActivity() {
}
binding.filter.setOnClickListener {
val genres =
PrefManager.getVal<Set<String>>(PrefName.GenresList).toMutableSet().sorted()
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList).toMutableSet().sorted()
val popup = PopupMenu(this, it)
popup.menu.add("All")
genres.forEach { genre ->

View File

@@ -5,9 +5,9 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import ani.dantotsu.notifications.TaskScheduler.TaskType
import ani.dantotsu.notifications.anilist.AnilistNotificationReceiver
import ani.dantotsu.notifications.comment.CommentNotificationReceiver
import ani.dantotsu.notifications.TaskScheduler.TaskType
import ani.dantotsu.notifications.subscription.SubscriptionNotificationReceiver
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName

View File

@@ -5,9 +5,9 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import ani.dantotsu.notifications.TaskScheduler.TaskType
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.notifications.TaskScheduler.TaskType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger

View File

@@ -21,16 +21,11 @@ interface TaskScheduler {
for (taskType in TaskType.entries) {
val interval = when (taskType) {
TaskType.COMMENT_NOTIFICATION -> CommentNotificationWorker.checkIntervals[PrefManager.getVal(
PrefName.CommentNotificationInterval
)]
PrefName.CommentNotificationInterval)]
TaskType.ANILIST_NOTIFICATION -> AnilistNotificationWorker.checkIntervals[PrefManager.getVal(
PrefName.AnilistNotificationInterval
)]
PrefName.AnilistNotificationInterval)]
TaskType.SUBSCRIPTION_NOTIFICATION -> SubscriptionNotificationWorker.checkIntervals[PrefManager.getVal(
PrefName.SubscriptionNotificationInterval
)]
PrefName.SubscriptionNotificationInterval)]
}
scheduleRepeatingTask(taskType, interval)
}
@@ -67,7 +62,6 @@ interface TaskScheduler {
}
}
}
enum class TaskType {
COMMENT_NOTIFICATION,
ANILIST_NOTIFICATION,

View File

@@ -66,7 +66,6 @@ class MediaNameFetch {
val type = object : TypeToken<MediaResponse>() {}.type
return gson.fromJson(response, type)
}
data class ReturnedData(val title: String, val coverImage: String, val color: String)
data class MediaResponse(val data: Map<String, MediaItem>)

View File

@@ -32,7 +32,7 @@ class SubscriptionHelper {
return data
}
private fun saveSelected(mediaId: Int, data: Selected) {
private fun saveSelected( mediaId: Int, data: Selected) {
PrefManager.setCustomVal("${mediaId}-select", data)
}

View File

@@ -5,10 +5,7 @@ import android.graphics.Rect
import android.view.View
import android.widget.FrameLayout
class AndroidBug5497Workaround private constructor(
activity: Activity,
private val callback: (Boolean) -> Unit
) {
class AndroidBug5497Workaround private constructor(activity: Activity, private val callback: (Boolean) -> Unit) {
private val mChildOfContent: View
private var usableHeightPrevious = 0
private val frameLayoutParams: FrameLayout.LayoutParams

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.others
import android.app.DownloadManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -7,6 +8,7 @@ import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import ani.dantotsu.FileUrl
import ani.dantotsu.R
@@ -17,6 +19,9 @@ import ani.dantotsu.parsers.Book
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
object Download {

View File

@@ -2,10 +2,10 @@ package ani.dantotsu.others
import ani.dantotsu.FileUrl
import ani.dantotsu.client
import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import com.google.gson.Gson
import com.lagradost.nicehttp.NiceResponse
import kotlinx.serialization.SerialName

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