Compare commits

..

2 Commits

Author SHA1 Message Date
rebel onion
d778cd4350 Merge pull request #187 from rebelonion/dev
Dev
2024-02-08 09:56:12 -06:00
rebel onion
ab199a3502 Merge pull request #186 from rebelonion/dev
Dev
2024-02-08 07:35:44 -06:00
506 changed files with 10415 additions and 49516 deletions

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -39,7 +39,7 @@ jobs:
fi
echo "Commits since $LAST_SHA:"
# Accumulate commit logs in a shell variable
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an")
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"%h - %s")
# URL-encode the newline characters for GitHub Actions
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
@@ -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
@@ -66,7 +64,7 @@ jobs:
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Setup JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 17
@@ -77,7 +75,7 @@ jobs:
- name: List files in the directory
run: ls -l
- name: Make gradlew executable
run: chmod +x ./gradlew
@@ -85,31 +83,28 @@ 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@v3.0.0
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
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
shell: bash
run: |
#Discord
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g; s/^/\n/')
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g')
# Truncate commit messages if they are too long
max_length=1900 # Adjust this value as needed
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
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
-F "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
-F "caption=[Alpha-Build: ${VERSION}] Change logs :${commit_messages}" \
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
env:
@@ -117,13 +112,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 }}

6
.gitignore vendored
View File

@@ -8,9 +8,6 @@ local.properties
# Log/OS Files
*.log
# Secrets
apikey.properties
# Android Studio generated files and folders
captures/
.externalNativeBuild/
@@ -31,6 +28,3 @@ output.json
#other
scripts/
#crowdin
crowdin.yml

View File

@@ -15,16 +15,15 @@ android {
defaultConfig {
applicationId "ani.dantotsu"
minSdk 21
minSdk 23
targetSdk 34
versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "3.0.0"
versionCode 300000000
versionName "2.2.0"
versionCode 220000000
signingConfig signingConfigs.debug
}
flavorDimensions += "store"
flavorDimensions "store"
productFlavors {
fdroid {
// F-Droid specific configuration
@@ -43,22 +42,19 @@ android {
buildTypes {
alpha {
applicationIdSuffix ".beta" // keep as beta by popular request
versionNameSuffix "-alpha01-" + gitCommitHash
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
versionNameSuffix "-alpha01"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_alpha", icon_placeholder_round: "@mipmap/ic_launcher_alpha_round"]
debuggable System.getenv("CI") == null
isDefault true
}
debug {
applicationIdSuffix ".beta"
versionNameSuffix "-beta02"
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
versionNameSuffix "-beta01"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
debuggable false
}
release {
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_round"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
debuggable false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
}
@@ -80,13 +76,13 @@ 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'
// Core
// FireBase
googleImplementation platform('com.google.firebase:firebase-bom:32.2.3')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.1'
// Core
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.browser:browser:1.7.0'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
@@ -95,14 +91,13 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:2.9.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.10.0'
implementation "com.anggrayudi:storage:1.5.5"
// Glide
// Glide
ext.glide_version = '4.16.0'
api "com.github.bumptech.glide:glide:$glide_version"
implementation "com.github.bumptech.glide:glide:$glide_version"
@@ -110,48 +105,33 @@ dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer
ext.exo_version = '1.3.1'
// Exoplayer
ext.exo_version = '1.2.1'
implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
implementation "androidx.media3:media3-session:$exo_version"
// Media3 Casting
//media3 casting
implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.7.0"
implementation "androidx.mediarouter:mediarouter:1.6.0"
// UI
// UI
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.github.RepoDevil:AnimatedBottomBar:7fcb9af'
implementation 'nl.joery.animatedbottombar:library:1.1.0'
implementation 'io.noties.markwon:core:4.6.2'
implementation 'com.flaviofaria:kenburnsview:1.0.7'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.alexvasilkov:gesture-views:2.8.3'
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'
// 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"
// Groupie
ext.groupie_version = '2.10.1'
implementation "com.github.lisawray.groupie:groupie:$groupie_version"
implementation "com.github.lisawray.groupie:groupie-viewbinding:$groupie_version"
// String Matching
// string matching
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
// Aniyomi
// Aniyomi
implementation 'io.reactivex:rxjava:1.3.8'
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
@@ -161,10 +141,11 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'com.squareup.okio:okio:3.8.0'
implementation 'com.squareup.okio:okio:3.7.0'
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
implementation 'org.jsoup:jsoup:1.16.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3'
implementation 'ch.acra:acra-http:5.11.3'
implementation 'org.jsoup:jsoup:1.15.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.tachiyomiorg:unifile:17bec43'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'

View File

@@ -43,25 +43,6 @@
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
-keep class ani.dantotsu.** { *; }
-keep class ani.dantotsu.download.DownloadsManager { *; }
-keepattributes Signature
-keep class uy.kohesive.injekt.** { *; }
-keep class eu.kanade.tachiyomi.** { *; }
-keep class kotlin.** { *; }
-dontwarn kotlin.**
-keep class kotlinx.** { *; }
-keepclassmembers class uy.kohesive.injekt.api.FullTypeReference {
<init>(...);
}
-keep class com.google.gson.** { *; }
-keepattributes *Annotation*
-keepattributes EnclosingMethod
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class org.jsoup.** { *; }
-keepclassmembers class org.jsoup.nodes.Document { *; }
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault

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

@@ -11,21 +11,13 @@ import android.net.Uri
import android.os.Environment
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import ani.dantotsu.BuildConfig
import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.buildMarkwon
import ani.dantotsu.client
import ani.dantotsu.currContext
import ani.dantotsu.logError
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.*
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@@ -33,8 +25,9 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.*
object AppUpdater {
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
@@ -46,10 +39,9 @@ object AppUpdater {
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }
.maxByOrNull {
it.timeStamp()
} ?: throw Exception("No Pre Release Found")
val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }.maxByOrNull {
it.timeStamp()
} ?: throw Exception("No Pre Release Found")
val v = r.tagName.substringAfter("v", "")
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
} else {
@@ -58,7 +50,7 @@ object AppUpdater {
res to res.substringAfter("# ").substringBefore("\n")
}
Logger.log("Git Version : $version")
logger("Git Version : $version")
val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread {
CustomBottomDialog.newInstance().apply {
@@ -69,7 +61,8 @@ object AppUpdater {
)
addView(
TextView(activity).apply {
val markWon = buildMarkwon(activity, false)
val markWon = Markwon.builder(activity)
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
markWon.setMarkdown(this, md)
}
)
@@ -85,18 +78,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 +104,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
}
}
@@ -174,8 +161,21 @@ object AppUpdater {
DownloadManager.EXTRA_DOWNLOAD_ID, id
) ?: id
downloadManager.getUriForDownloadedFile(downloadId)?.let {
openApk(this@downloadUpdate, it)
val query = DownloadManager.Query()
query.setFilterById(downloadId)
val c = downloadManager.query(query)
if (c.moveToFirst()) {
val columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS)
if (DownloadManager.STATUS_SUCCESSFUL == c
.getInt(columnIndex)
) {
c.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI)
val uri = Uri.parse(
c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
)
openApk(this@downloadUpdate, uri)
}
}
} catch (e: Exception) {
logError(e)
@@ -190,11 +190,16 @@ object AppUpdater {
private fun openApk(context: Context, uri: Uri) {
try {
uri.path?.let {
val contentUri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".provider",
File(it)
)
val installIntent = Intent(Intent.ACTION_VIEW).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
data = uri
data = contentUri
}
context.startActivity(installIntent)
}

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" />
@@ -18,10 +16,10 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> <!-- For background jobs -->
@@ -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" />
@@ -62,7 +49,6 @@
android:name=".App"
android:allowBackup="true"
android:banner="@mipmap/ic_banner_foreground"
android:enableOnBackInvokedCallback="true"
android:icon="${icon_placeholder}"
android:label="@string/app_name"
android:largeHeap="true"
@@ -71,30 +57,9 @@
android:supportsRtl="true"
android:theme="@style/Theme.Dantotsu"
android:usesCleartextTraffic="true"
tools:ignore="AllowBackup"
tools:targetApi="tiramisu">
tools:ignore="AllowBackup">
<receiver
android:name=".widgets.upcoming.UpcomingWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/upcoming_widget_info" />
</receiver>
<activity
android:name=".widgets.upcoming.UpcomingWidgetConfigure"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver
android:name=".widgets.statistics.ProfileStatsWidget"
android:name=".widgets.CurrentlyAiringWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -102,9 +67,10 @@
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/statistics_widget_info" />
android:resource="@xml/currently_airing_widget_info" />
</receiver>
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
<receiver android:name=".subcriptions.NotificationClickReceiver" />
<activity
android:name=".media.novel.novelreader.NovelReaderActivity"
@@ -136,61 +102,8 @@
<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"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name=".widgets.statistics.ProfileStatsConfigure"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name=".profile.ProfileActivity"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name=".profile.FollowActivity"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name=".profile.activity.FeedActivity"
android:configChanges="orientation|screenSize|screenLayout"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.activity.NotificationActivity"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".others.imagesearch.ImageSearchActivity"
@@ -204,9 +117,6 @@
android:name=".media.CalendarActivity"
android:parentActivityName=".MainActivity" />
<activity android:name=".media.user.ListActivity" />
<activity
android:name=".profile.SingleStatActivity"
android:parentActivityName=".profile.ProfileActivity" />
<activity
android:name=".media.manga.mangareader.MangaReaderActivity"
android:excludeFromRecents="true"
@@ -217,8 +127,7 @@
<activity
android:name=".media.MediaDetailsActivity"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.Dantotsu.NeverCutout"
android:windowSoftInputMode="adjustResize|stateHidden" />
android:theme="@style/Theme.Dantotsu.NeverCutout" />
<activity android:name=".media.CharacterDetailsActivity" />
<activity android:name=".home.NoInternet" />
<activity
@@ -330,17 +239,6 @@
<data android:host="myanimelist.net" />
<data android:pathPrefix="/anime" />
</intent-filter>
<intent-filter android:label="@string/view_profile_in_dantotsu">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="anilist.co" />
<data android:pathPrefix="/user" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
@@ -356,40 +254,24 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" />
<data android:pathPattern=".*\\.sani" />
<data android:host="*" />
</intent-filter>
</activity>
<activity
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<receiver
android:name=".notifications.AlarmPermissionStateReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".notifications.BootCompletedReceiver"
android:name=".subcriptions.AlarmReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="Aani.dantotsu.ACTION_ALARM" />
</intent-filter>
</receiver>
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver" />
<receiver android:name=".notifications.comment.CommentNotificationReceiver" />
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver" />
<meta-data
android:name="preloaded_fonts"
@@ -407,11 +289,25 @@
</provider>
<service
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
android:name=".widgets.CurrentlyAiringRemoteViewsService"
android:exported="true"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
android:name=".download.video.ExoplayerDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync">
<intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<service
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
@@ -434,11 +330,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,13 +6,9 @@ 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
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.notifications.TaskScheduler
import ani.dantotsu.others.DisabledReports
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources
@@ -21,8 +17,6 @@ import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.FinalExceptionHandler
import ani.dantotsu.util.Logger
import com.google.android.material.color.DynamicColors
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
@@ -34,6 +28,7 @@ import kotlinx.coroutines.launch
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -43,9 +38,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)
@@ -87,11 +79,9 @@ class App : MultiDexApplication() {
}
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
Logger.init(this)
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
Logger.log("App: Logging started")
initializeNetwork()
initializeNetwork(baseContext)
setupNotificationChannels()
if (!LogcatLogger.isInstalled) {
@@ -101,49 +91,33 @@ 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 {
animeExtensionManager.findAvailableExtensions()
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
}
val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch {
mangaExtensionManager.findAvailableExtensions()
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
}
val novelScope = CoroutineScope(Dispatchers.Default)
novelScope.launch {
novelExtensionManager.findAvailableExtensions()
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
logger("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()
}
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
val scheduler = TaskScheduler.create(this, useAlarmManager)
scheduler.scheduleAllTasks(this)
scheduler.scheduleSingleWork(this)
}
private fun setupNotificationChannels() {
try {
Notifications.createChannels(this)
} catch (e: Exception) {
Logger.log("Failed to modify notification channels")
Logger.log(e)
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
}
}
@@ -166,10 +140,6 @@ class App : MultiDexApplication() {
companion object {
private var instance: App? = null
/** Reference to the application context.
*
* USE WITH EXTREME CAUTION!**/
var context: Context? = null
fun currentContext(): Context? {
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context

View File

@@ -1,16 +1,13 @@
package ani.dantotsu
import android.Manifest
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.app.DatePickerDialog
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
@@ -19,68 +16,32 @@ import android.content.res.Configuration
import android.content.res.Resources.getSystem
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.Manifest
import android.media.MediaScannerConnection
import android.net.ConnectivityManager
import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
import android.net.NetworkCapabilities.TRANSPORT_LOWPAN
import android.net.NetworkCapabilities.TRANSPORT_USB
import android.net.NetworkCapabilities.TRANSPORT_VPN
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE
import android.net.NetworkCapabilities.*
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.os.SystemClock
import android.os.*
import android.provider.Settings
import android.telephony.TelephonyManager
import android.text.InputFilter
import android.text.Spanned
import android.util.AttributeSet
import android.util.TypedValue
import android.view.GestureDetector
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewAnimationUtils
import android.view.ViewGroup
import android.view.*
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.OvershootInterpolator
import android.view.animation.ScaleAnimation
import android.view.animation.TranslateAnimation
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.DatePicker
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import android.view.animation.*
import android.widget.*
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
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.core.view.*
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView
@@ -88,31 +49,18 @@ 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
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
import ani.dantotsu.others.SpoilerPlugin
import ani.dantotsu.parsers.ShowResponse
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 ani.dantotsu.subcriptions.NotificationClickReceiver
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -120,40 +68,15 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.internal.ViewUtils
import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.data.notification.Notifications
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.ext.tasklist.TaskListPlugin
import io.noties.markwon.html.HtmlPlugin
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 kotlinx.coroutines.*
import nl.joery.animatedbottombar.AnimatedBottomBar
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.io.*
import java.lang.Runnable
import java.lang.reflect.Field
import java.util.Calendar
import java.util.TimeZone
import java.util.Timer
import java.util.TimerTask
import kotlin.collections.set
import kotlin.math.log2
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import java.util.*
import kotlin.math.*
var statusBarHeight = 0
@@ -185,10 +108,11 @@ 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()
fun logger(e: Any?, print: Boolean = true) {
if (print)
println(e)
}
fun initActivity(a: Activity) {
val window = a.window
@@ -208,17 +132,11 @@ fun initActivity(a: Activity) {
if (navBarHeight == 0) {
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
?.apply {
navBarHeight = this.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
}
}
WindowInsetsControllerCompat(
window,
window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0
&& a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
) {
a.hideStatusBar()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
window.decorView.rootWindowInsets?.displayCutout?.apply {
if (boundingRects.size > 0) {
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
@@ -230,124 +148,47 @@ fun initActivity(a: Activity) {
val windowInsets =
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
if (windowInsets != null) {
statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
navBarHeight =
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
statusBarHeight = insets.top
navBarHeight = insets.bottom
}
}
if (a !is MainActivity) a.setNavigationTheme()
}
@Suppress("DEPRECATION")
fun Activity.hideSystemBars() {
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
controller.hide(WindowInsetsCompat.Type.systemBars())
}
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
}
fun Activity.hideSystemBarsExtendView() {
WindowCompat.setDecorFitsSystemWindows(window, false)
hideSystemBars()
}
fun Activity.showSystemBars() {
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
controller.show(WindowInsetsCompat.Type.systemBars())
}
}
fun Activity.showSystemBarsRetractView() {
WindowCompat.setDecorFitsSystemWindows(window, true)
showSystemBars()
}
fun Activity.setNavigationTheme() {
val tv = TypedValue()
theme.resolveAttribute(android.R.attr.colorBackground, tv, true)
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && tv.isColorType)
|| (tv.type >= TypedValue.TYPE_FIRST_COLOR_INT && tv.type <= TypedValue.TYPE_LAST_COLOR_INT)
) {
window.navigationBarColor = tv.data
}
}
/**
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
*
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
*/
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar) {
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
clipToPadding = false
setPadding(paddingLeft, paddingTop, paddingRight, navBarHeight + navBar.measuredHeight)
}
/**
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
*
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
*/
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar, overlayView: View) {
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
overlayView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
clipToPadding = false
setPadding(
paddingLeft,
paddingTop,
paddingRight,
navBarHeight + navBar.measuredHeight + overlayView.measuredHeight
)
}
fun Activity.reloadActivity() {
Refresh.all()
finish()
startActivity(Intent(this, this::class.java))
initActivity(this)
}
fun Activity.restartApp() {
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)
}
finishAndRemoveTask()
PrefManager.setCustomVal("reload", true)
@Suppress("DEPRECATION")
fun Activity.hideStatusBar() {
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onStart() {
super.onStart()
dialog?.window?.let { window ->
WindowCompat.setDecorFitsSystemWindows(window, false)
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
if (immersiveMode) {
WindowInsetsControllerCompat(
window, window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
}
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(
com.google.android.material.R.attr.colorSurface,
typedValue,
true
)
window.navigationBarColor = typedValue.data
val window = dialog?.window
val decorView: View = window?.decorView ?: return
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(
com.google.android.material.R.attr.colorSurface,
typedValue,
true
)
window.navigationBarColor = typedValue.data
}
override fun show(manager: FragmentManager, tag: String?) {
@@ -361,35 +202,21 @@ fun isOnline(context: Context): Boolean {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return tryWith {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return@tryWith if (cap != null) {
when {
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return@tryWith if (cap != null) {
when {
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
else -> false
}
} else false
} else {
@Suppress("DEPRECATION")
return@tryWith connectivityManager.activeNetworkInfo?.run {
type == ConnectivityManager.TYPE_BLUETOOTH ||
type == ConnectivityManager.TYPE_ETHERNET ||
type == ConnectivityManager.TYPE_MOBILE ||
type == ConnectivityManager.TYPE_MOBILE_DUN ||
type == ConnectivityManager.TYPE_MOBILE_HIPRI ||
type == ConnectivityManager.TYPE_WIFI ||
type == ConnectivityManager.TYPE_WIMAX ||
type == ConnectivityManager.TYPE_VPN
} ?: false
}
else -> false
}
} else false
} ?: false
}
@@ -421,7 +248,7 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g
dialog.setButton(
DialogInterface.BUTTON_NEUTRAL,
activity.getString(R.string.remove)
) { _, which ->
) { dialog, which ->
if (which == DialogInterface.BUTTON_NEUTRAL) {
date = FuzzyDate()
}
@@ -451,11 +278,12 @@ class InputFilterMinMax(
val input = (dest.toString() + source.toString()).toDouble()
if (isInRange(min, max, input)) return null
} catch (nfe: NumberFormatException) {
Logger.log(nfe)
logger(nfe.stackTraceToString())
}
return ""
}
@SuppressLint("SetTextI18n")
private fun isInRange(a: Double, b: Double, c: Double): Boolean {
val statusStrings = currContext()!!.resources.getStringArray(R.array.status_manga)[2]
@@ -468,7 +296,7 @@ class InputFilterMinMax(
}
class ZoomOutPageTransformer :
class ZoomOutPageTransformer() :
ViewPager2.PageTransformer {
override fun transformPage(view: View, position: Float) {
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
@@ -621,17 +449,11 @@ fun ImageView.loadImage(url: String?, size: Int = 0) {
}
fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
if (file?.url?.isNotEmpty() == true) {
tryWith {
if (file.url.startsWith("content://")) {
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
.override(size).into(this)
} else {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
}
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
}
}
}
@@ -757,41 +579,9 @@ fun View.circularReveal(ex: Int, ey: Int, subX: Boolean, time: Long) {
}
fun openLinkInBrowser(link: String?) {
link?.let {
try {
val emptyBrowserIntent = Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.fromParts("http", "", null)
}
val sendIntent = Intent().apply {
action = Intent.ACTION_VIEW
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(link)
selector = emptyBrowserIntent
}
currContext()!!.startActivity(sendIntent)
} catch (e: ActivityNotFoundException) {
snackString("No browser found")
} catch (e: Exception) {
Logger.log(e)
}
}
}
fun openLinkInYouTube(link: String?) {
link?.let {
try {
val videoIntent = Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(link)
setPackage("com.google.android.youtube")
}
currContext()!!.startActivity(videoIntent)
} catch (e: ActivityNotFoundException) {
openLinkInBrowser(link)
} catch (e: Exception) {
Logger.log(e)
}
tryWith {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
currContext()?.startActivity(intent)
}
}
@@ -886,6 +676,26 @@ fun savePrefs(
}
}
fun downloadsPermission(activity: AppCompatActivity): Boolean {
val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
val requiredPermissions = permissions.filter {
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
}.toTypedArray()
return if (requiredPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(activity, requiredPermissions, DOWNLOADS_PERMISSION_REQUEST_CODE)
false
} else {
true
}
}
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
val contentUri = FileProvider.getUriForFile(
@@ -918,7 +728,7 @@ fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
private fun scanFile(path: String, context: Context) {
MediaScannerConnection.scanFile(context, arrayOf(path), null) { p, _ ->
Logger.log("Finished scanning $p")
logger("Finished scanning $p")
}
}
@@ -950,15 +760,12 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
val clipboard = getSystemService(activity, ClipboardManager::class.java)
val clip = ClipData.newPlainText("label", string)
clipboard?.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
if (toast) snackString(activity.getString(R.string.copied_text, string))
}
if (toast) snackString(activity.getString(R.string.copied_text, string))
}
@SuppressLint("SetTextI18n")
fun countDown(media: Media, view: ViewGroup) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null
&& (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()
) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
@@ -990,50 +797,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) {
@@ -1103,13 +866,9 @@ class EmptyAdapter(private val count: Int) : RecyclerView.Adapter<RecyclerView.V
inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
}
fun getAppString(res: Int): String {
return currContext()?.getString(res) ?: ""
}
fun toast(string: String?) {
if (string != null) {
Logger.log(string)
logger(string)
MainScope().launch {
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
.show()
@@ -1117,20 +876,16 @@ fun toast(string: String?) {
}
}
fun toast(res: Int) {
toast(getAppString(res))
}
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null): Snackbar? {
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
try { //I have no idea why this sometimes crashes for some people...
if (s != null) {
(activity ?: currActivity())?.apply {
val snackBar = Snackbar.make(
window.decorView.findViewById(android.R.id.content),
s,
Snackbar.LENGTH_SHORT
)
runOnUiThread {
val snackBar = Snackbar.make(
window.decorView.findViewById(android.R.id.content),
s,
Snackbar.LENGTH_SHORT
)
snackBar.view.apply {
updateLayoutParams<FrameLayout.LayoutParams> {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
@@ -1150,19 +905,13 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
}
snackBar.show()
}
return snackBar
}
Logger.log(s)
logger(s)
}
} catch (e: Exception) {
Logger.log(e)
logger(e.stackTraceToString())
Injekt.get<CrashlyticsInterface>().logException(e)
}
return null
}
fun snackString(r: Int, activity: Activity? = null, clipboard: String? = null): Snackbar? {
return snackString(getAppString(r), activity, clipboard)
}
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
@@ -1288,7 +1037,7 @@ fun incognitoNotification(context: Context) {
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if (incognito) {
val intent = Intent(context, IncognitoNotificationClickReceiver::class.java)
val intent = Intent(context, NotificationClickReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_IMMUTABLE
@@ -1306,28 +1055,6 @@ fun incognitoNotification(context: Context) {
}
}
fun hasNotificationPermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
} else {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}
fun openSettings(context: Context, channelId: String?): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intent = Intent(
if (channelId != null) Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
else Settings.ACTION_APP_NOTIFICATION_SETTINGS
).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, channelId)
}
context.startActivity(intent)
true
} else false
}
suspend fun View.pop() {
currActivity()?.runOnUiThread {
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
@@ -1340,104 +1067,3 @@ suspend fun View.pop() {
}
delay(100)
}
fun blurImage(imageView: ImageView, banner: String?) {
if (banner != null) {
val radius = PrefManager.getVal<Float>(PrefName.BlurRadius).toInt()
val sampling = PrefManager.getVal<Float>(PrefName.BlurSampling).toInt()
if (PrefManager.getVal(PrefName.BlurBanners)) {
val context = imageView.context
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)
)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
.into(imageView)
}
} else {
imageView.loadImage(banner)
}
} else {
imageView.setImageResource(R.drawable.linear_gradient_bg)
}
}
/**
* Builds the markwon instance with all the plugins
* @return the markwon instance
*/
fun buildMarkwon(
activity: Context,
userInputContent: Boolean = true,
fragment: Fragment? = null
): Markwon {
val glideContext = fragment?.let { Glide.with(it) } ?: Glide.with(activity)
val markwon = Markwon.builder(activity)
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.linkResolver { _, link ->
copyToClipboard(link, true)
}
}
})
.usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(TablePlugin.create(activity))
.usePlugin(TaskListPlugin.create(activity))
.usePlugin(SpoilerPlugin())
.usePlugin(HtmlPlugin.create { plugin ->
if (userInputContent) {
plugin.addHandler(
TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a")
)
}
})
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
private val requestManager: RequestManager = glideContext.apply {
addDefaultRequestListener(object : RequestListener<Any> {
override fun onResourceReady(
resource: Any,
model: Any,
target: Target<Any>,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
if (resource is GifDrawable) {
resource.start()
}
return false
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Any>,
isFirstResource: Boolean
): Boolean {
Logger.log("Image failed to load: $model")
Logger.log(e as Exception)
return false
}
})
}
override fun load(drawable: AsyncDrawable): RequestBuilder<Drawable> {
Logger.log("Loading image: ${drawable.destination}")
return requestManager.load(drawable.destination)
}
override fun cancel(target: Target<*>) {
Logger.log("Cancelling image load")
requestManager.clear(target)
}
}))
.build()
return markwon
}

View File

@@ -2,9 +2,7 @@ package ani.dantotsu
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.content.res.Configuration
import android.graphics.drawable.Animatable
import android.graphics.drawable.GradientDrawable
import android.net.Uri
@@ -13,11 +11,12 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.view.LayoutInflater
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator
import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.annotation.OptIn
@@ -26,55 +25,40 @@ import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.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.SplashScreenBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment
import ani.dantotsu.home.MangaFragment
import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity
import ani.dantotsu.profile.activity.NotificationActivity
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.Logger
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
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.GlobalScope
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
@@ -87,7 +71,6 @@ class MainActivity : AppCompatActivity() {
private var load = false
@kotlin.OptIn(DelicateCoroutinesApi::class)
@SuppressLint("InternalInsetResource", "DiscouragedApi")
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -101,77 +84,17 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
androidx.work.WorkManager.getInstance(this)
.enqueue(OneTimeWorkRequest.Companion.from(CommentNotificationWorker::class.java))
androidx.work.WorkManager.getInstance(this)
.enqueue(OneTimeWorkRequest.Companion.from(AnilistNotificationWorker::class.java))
val action = intent.action
val type = intent.type
if (Intent.ACTION_VIEW == action && type != null) {
val uri: Uri? = intent.data
try {
if (uri == null) {
throw Exception("Uri is null")
}
val jsonString =
contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name =
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try {
PreferenceKeystore.decryptWithPassword(
password,
encrypted,
salt
)
} catch (e: Exception) {
toast("Incorrect password")
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
toast("Error importing settings")
}
}
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val backgroundDrawable = bottomNavBar.background as GradientDrawable
val backgroundDrawable = _bottomBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
backgroundDrawable.setColor(semiTransparentColor)
bottomNavBar.background = backgroundDrawable
_bottomBar.background = backgroundDrawable
}
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
val offset = try {
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
@@ -221,14 +144,22 @@ class MainActivity : AppCompatActivity() {
finish()
}
doubleBackToExitPressedOnce = true
snackString(this@MainActivity.getString(R.string.back_to_exit)).apply {
this?.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
doubleBackToExitPressedOnce = false
}
})
}
snackString(this@MainActivity.getString(R.string.back_to_exit))
Handler(Looper.getMainLooper()).postDelayed(
{ doubleBackToExitPressedOnce = false },
2000
)
}
val preferences: SourcePreferences = Injekt.get()
if (preferences.animeExtensionUpdatesCount()
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
Toast.makeText(
this,
"You have extension updates available!",
Toast.LENGTH_LONG
).show()
}
binding.root.isMotionEventSplittingEnabled = false
@@ -274,17 +205,6 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach {
initActivity(this)
val preferences: SourcePreferences = Injekt.get()
if (preferences.animeExtensionUpdatesCount()
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
snackString(R.string.extension_updates_available)
?.setDuration(Snackbar.LENGTH_LONG)
?.setAction(R.string.review) {
startActivity(Intent(this, ExtensionsActivity::class.java))
}
}
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
selectedOption = if (fragment != null) {
when (fragment) {
AnimeFragment::class.java.name -> 0
@@ -297,40 +217,7 @@ class MainActivity : AppCompatActivity() {
}
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
}
var launched = false
intent.extras?.let { extras ->
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
val mediaId = extras.getInt("mediaId", -1)
val commentId = extras.getInt("commentId", -1)
val activityId = extras.getInt("activityId", -1)
if (fragmentToLoad != null && mediaId != -1 && commentId != -1) {
val detailIntent = Intent(this, MediaDetailsActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", fragmentToLoad)
putExtra("mediaId", mediaId)
putExtra("commentId", commentId)
}
launched = true
startActivity(detailIntent)
} else if (fragmentToLoad == "FEED" && activityId != -1) {
val feedIntent = Intent(this, FeedActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId)
}
launched = true
startActivity(feedIntent)
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
Logger.log("MainActivity, onCreate: $activityId")
val notificationIntent = Intent(this, NotificationActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId)
}
launched = true
startActivity(notificationIntent)
}
}
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
@@ -343,7 +230,7 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(this, NoInternet::class.java))
} else {
val model: AnilistHomeViewModel by viewModels()
model.genres.observe(this) {
model.genres.observe(this) { it ->
if (it != null) {
if (it) {
val navbar = binding.includedNavbar.navbar
@@ -368,14 +255,12 @@ class MainActivity : AppCompatActivity() {
mainViewPager.setCurrentItem(newIndex, false)
}
})
if (mainViewPager.currentItem != selectedOption) {
navbar.selectTabAt(selectedOption)
mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
navbar.selectTabAt(selectedOption)
mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
} else {
binding.mainProgressBar.visibility = View.GONE
@@ -383,7 +268,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)
@@ -403,21 +288,8 @@ class MainActivity : AppCompatActivity() {
snackString(this@MainActivity.getString(R.string.anilist_not_found))
}
}
val username = intent.extras?.getString("username")
if (username != null) {
val nameInt = username.toIntOrNull()
if (nameInt != null) {
startActivity(
Intent(this@MainActivity, ProfileActivity::class.java)
.putExtra("userId", nameInt)
)
} else {
startActivity(
Intent(this@MainActivity, ProfileActivity::class.java)
.putExtra("username", username)
)
}
}
delay(500)
startSubscription()
}
load = true
}
@@ -454,78 +326,27 @@ class MainActivity : AppCompatActivity() {
}
}
}
//TODO: Remove this
GlobalScope.launch(Dispatchers.IO) {
val index = Helper.downloadManager(this@MainActivity).downloadIndex
val downloadCursor = index.getDownloads()
while (downloadCursor.moveToNext()) {
val download = downloadCursor.download
Log.e("Downloader", download.request.uri.toString())
Log.e("Downloader", download.request.id)
Log.e("Downloader", download.request.mimeType.toString())
Log.e("Downloader", download.request.data.size.toString())
Log.e("Downloader", download.bytesDownloaded.toString())
Log.e("Downloader", download.state.toString())
Log.e("Downloader", download.failureReason.toString())
val torrentManager = Injekt.get<TorrentAddonManager>()
fun startTorrent() {
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
launchIO {
if (!ServerService.isRunning()) {
ServerService.start()
}
if (download.state == Download.STATE_FAILED) { //simple cleanup
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
}
}
}
if (torrentManager.isInitialized.value == false) {
torrentManager.isInitialized.observe(this) {
if (it) {
startTorrent()
}
}
} else {
startTorrent()
}
}
override fun onRestart() {
super.onRestart()
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
val params: ViewGroup.MarginLayoutParams =
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
params.updateMargins(bottom = margin.toPx)
}
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
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)
.setPositiveButton("OK", null)
.setNegativeButton("Cancel") { dialog, _ ->
password.fill('0')
dialog.dismiss()
callback(null)
}
.create()
dialog.window?.setDimAmount(0.8f)
dialog.show()
// Override the positive button here
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
dialog.dismiss()
callback(password)
} else {
toast("Password cannot be empty")
}
}
}
//ViewPager
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :

View File

@@ -1,15 +1,14 @@
package ani.dantotsu
import android.content.Context
import android.os.Build
import androidx.fragment.app.FragmentActivity
import ani.dantotsu.others.webview.CloudFlare
import ani.dantotsu.others.webview.WebViewBottomDialog
import ani.dantotsu.util.Logger
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import com.lagradost.nicehttp.addGenericDns
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
@@ -35,13 +34,13 @@ lateinit var defaultHeaders: Map<String, String>
lateinit var okHttpClient: OkHttpClient
lateinit var client: Requests
fun initializeNetwork() {
fun initializeNetwork(context: Context) {
val networkHelper = Injekt.get<NetworkHelper>()
defaultHeaders = mapOf(
"User-Agent" to
defaultUserAgentProvider()
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
.format(Build.VERSION.RELEASE, Build.MODEL)
)
@@ -105,7 +104,6 @@ fun logError(e: Throwable, post: Boolean = true, snackbar: Boolean = true) {
toast(e.localizedMessage)
}
e.printStackTrace()
Logger.log(e)
}
fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T): T? {
@@ -136,7 +134,7 @@ suspend fun <T> tryWithSuspend(
* A url, which can also have headers
* **/
data class FileUrl(
var url: String,
val url: String,
val headers: Map<String, String> = mapOf()
) : Serializable {
companion object {

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,129 +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())
.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,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,133 +0,0 @@
package ani.dantotsu.addons.download
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import ani.dantotsu.addons.AddonListener
import ani.dantotsu.addons.AddonLoader
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 -> {
if (ExtensionInstallReceiver.isReplacing(intent)) return
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,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,133 +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.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.download.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
@@ -20,7 +18,6 @@ import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.source.anime.service.AnimeSourceManager
@@ -32,7 +29,6 @@ import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule {
@kotlin.OptIn(ExperimentalSerializationApi::class)
@OptIn(UnstableApi::class)
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
@@ -40,13 +36,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

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

View File

@@ -3,17 +3,16 @@ 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
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.currContext
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import ani.dantotsu.tryWithSuspend
import java.util.Calendar
object Anilist {
@@ -28,7 +27,6 @@ object Anilist {
var bg: String? = null
var episodesWatched: Int? = null
var chapterRead: Int? = null
var unreadNotificationCount: Int = 0
var genres: ArrayList<String>? = null
var tags: Map<Boolean, List<String>>? = null
@@ -39,54 +37,20 @@ object Anilist {
"SCORE_DESC",
"POPULARITY_DESC",
"TRENDING_DESC",
"START_DATE_DESC",
"TITLE_ENGLISH",
"TITLE_ENGLISH_DESC",
"SCORE"
)
val source = listOf(
"ORIGINAL",
"MANGA",
"LIGHT NOVEL",
"VISUAL NOVEL",
"VIDEO GAME",
"OTHER",
"NOVEL",
"DOUJINSHI",
"ANIME",
"WEB NOVEL",
"LIVE ACTION",
"GAME",
"COMIC",
"MULTIMEDIA PROJECT",
"PICTURE BOOK"
)
val animeStatus = listOf(
"FINISHED",
"RELEASING",
"NOT YET RELEASED",
"CANCELLED"
)
val mangaStatus = listOf(
"FINISHED",
"RELEASING",
"NOT YET RELEASED",
"HIATUS",
"CANCELLED"
)
val seasons = listOf(
"WINTER", "SPRING", "SUMMER", "FALL"
)
val animeFormats = listOf(
val anime_formats = listOf(
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
)
val mangaFormats = listOf(
val manga_formats = listOf(
"MANGA", "NOVEL", "ONE SHOT"
)
@@ -150,9 +114,6 @@ object Anilist {
episodesWatched = null
chapterRead = null
PrefManager.removeVal(PrefName.AnilistToken)
//logout from comments api
CommentsAPI.logout()
}
suspend inline fun <reified T : Any> executeQuery(
@@ -163,8 +124,7 @@ object Anilist {
show: Boolean = false,
cache: Int? = null
): T? {
return try {
if (show) Logger.log("Anilist Query: $query")
return tryWithSuspend {
if (rateLimitReset > System.currentTimeMillis() / 1000) {
toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
@@ -188,7 +148,7 @@ object Anilist {
cacheTime = cache ?: 10
)
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
Logger.log("Remaining requests: $remaining")
Log.d("AnilistQuery", "Remaining requests: $remaining")
if (json.code == 429) {
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
@@ -199,16 +159,10 @@ 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 (show) Logger.log("Anilist Response: ${json.text}")
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
if (show) println("Response : ${json.text}")
json.parsed()
} else null
} catch (e: Exception) {
if (show) snackString("Error fetching Anilist data: ${e.message}")
Logger.log("Anilist Query Error: ${e.message}")
null
}
}
}

View File

@@ -13,23 +13,6 @@ class AnilistMutations {
executeQuery<JsonObject>(query, variables)
}
suspend fun toggleFav(type: FavType, id: Int): Boolean {
val filter = when (type) {
FavType.ANIME -> "animeId"
FavType.MANGA -> "mangaId"
FavType.CHARACTER -> "characterId"
FavType.STAFF -> "staffId"
FavType.STUDIO -> "studioId"
}
val query = """mutation{ToggleFavourite($filter:$id){anime{pageInfo{total}}}}"""
val result = executeQuery<JsonObject>(query)
return result?.get("errors") == null && result != null
}
enum class FavType {
ANIME, MANGA, CHARACTER, STAFF, STUDIO
}
suspend fun editList(
mediaID: Int,
progress: Int? = null,

View File

@@ -6,12 +6,9 @@ import ani.dantotsu.checkGenreTime
import ani.dantotsu.checkId
import ani.dantotsu.connections.anilist.Anilist.authorRoles
import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FeedResponse
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.NotificationResponse
import ani.dantotsu.connections.anilist.api.Page
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.anilist.api.ToggleLike
import ani.dantotsu.currContext
import ani.dantotsu.isOnline
import ani.dantotsu.logError
@@ -20,11 +17,9 @@ 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
import ani.dantotsu.util.Logger
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
@@ -36,27 +31,23 @@ import java.io.Serializable
import kotlin.system.measureTimeMillis
class AnilistQueries {
suspend fun getUserData(): Boolean {
val response: Query.Viewer?
measureTimeMillis {
response =
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}unreadNotificationCount}}""")
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}}}""")
}.also { println("time : $it") }
val user = response?.data?.user ?: return false
PrefManager.setVal(PrefName.AnilistUserName, user.name)
Anilist.userid = user.id
PrefManager.setVal(PrefName.AnilistUserId, user.id.toString())
Anilist.username = user.name
Anilist.bg = user.bannerImage
Anilist.avatar = user.avatar?.medium
Anilist.episodesWatched = user.statistics?.anime?.episodesWatched
Anilist.chapterRead = user.statistics?.manga?.chaptersRead
Anilist.adult = user.options?.displayAdultContent ?: false
Anilist.unreadNotificationCount = user.unreadNotificationCount ?: 0
val unread = PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications)
Anilist.unreadNotificationCount += unread
return true
}
@@ -73,19 +64,18 @@ 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 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}}}}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 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()
media.favourites = fetchedMedia.favourites
media.popularity = fetchedMedia.popularity
media.startDate = fetchedMedia.startDate
media.endDate = fetchedMedia.endDate
@@ -131,38 +121,6 @@ class AnilistQueries {
name = i.node?.name?.userPreferred,
image = i.node?.image?.medium,
banner = media.banner ?: media.cover,
isFav = i.node?.isFavourite ?: false,
role = when (i.role.toString()) {
"MAIN" -> currContext()?.getString(R.string.main_role)
?: "MAIN"
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role)
?: "SUPPORTING"
else -> i.role.toString()
},
voiceActor = i.voiceActors?.map {
Author(
id = it.id,
name = it.name?.userPreferred,
image = it.image?.large,
role = it.languageV2
)
} as ArrayList<Author>
)
)
}
}
}
if (fetchedMedia.staff != null) {
media.staff = arrayListOf()
fetchedMedia.staff?.edges?.forEach { i ->
i.node?.apply {
media.staff?.add(
Author(
id = id,
name = i.node?.name?.userPreferred,
image = i.node?.image?.large,
role = when (i.role.toString()) {
"MAIN" -> currContext()?.getString(R.string.main_role)
?: "MAIN"
@@ -209,24 +167,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
@@ -270,10 +211,8 @@ class AnilistQueries {
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
media.anime.author = Author(
it.id,
it.name?.userPreferred ?: "N/A",
it.image?.medium,
"AUTHOR"
it.id.toString(),
it.name?.userPreferred ?: "N/A"
)
}
@@ -292,10 +231,8 @@ class AnilistQueries {
} else if (media.manga != null) {
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
media.manga.author = Author(
it.id,
it.name?.userPreferred ?: "N/A",
it.image?.medium,
"AUTHOR"
it.id.toString(),
it.name?.userPreferred ?: "N/A"
)
}
}
@@ -312,7 +249,6 @@ class AnilistQueries {
} else {
if (currContext()?.let { isOnline(it) } == true) {
snackString(currContext()?.getString(R.string.error_getting_data))
} else {
}
}
}
@@ -326,52 +262,6 @@ class AnilistQueries {
return media
}
fun userMediaDetails(media: Media): Media {
val query =
"""{Media(id:${media.id}){id mediaListEntry{id status progress private repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite idMal}}"""
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
if (fetchedMedia.mediaListEntry != null) {
fetchedMedia.mediaListEntry?.apply {
media.userProgress = progress
media.isListPrivate = private ?: false
media.userListId = id
media.userStatus = status?.toString()
media.inCustomListsOf = customLists?.toMutableMap()
media.userRepeat = repeat ?: 0
media.userUpdatedAt = updatedAt?.toString()?.toLong()?.times(1000)
media.userCompletedAt = completedAt ?: FuzzyDate()
media.userStartedAt = startedAt ?: FuzzyDate()
}
} else {
media.isListPrivate = false
media.userStatus = null
media.userListId = null
media.userProgress = null
media.userRepeat = 0
media.userUpdatedAt = null
media.userCompletedAt = FuzzyDate()
media.userStartedAt = FuzzyDate()
}
}
if (response.data?.media != null) parse()
else {
response = executeQuery(query, force = true, useToken = false)
if (response?.data?.media != null) parse()
}
}
}
awaitAll(anilist)
}
return media
}
suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
val returnArray = arrayListOf<Media>()
val map = mutableMapOf<Int, Media>()
@@ -409,18 +299,9 @@ class AnilistQueries {
}
}
}
if (type != "ANIME") {
returnArray.addAll(map.values)
return returnArray
}
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
if (set.isNotEmpty()) {
set.reversed().forEach {
if (map.containsKey(it)) returnArray.add(map[it]!!)
}
for (i in map) {
@@ -434,12 +315,12 @@ class AnilistQueries {
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """
}
suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
suspend fun favMedia(anime: Boolean): ArrayList<Media> {
var hasNextPage = true
var page = 0
suspend fun getNextPage(page: Int): List<Media> {
val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page, id)}}""")
val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page)}}""")
val favourites = response?.data?.user?.favourites
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
@@ -458,8 +339,8 @@ class AnilistQueries {
return responseArray
}
private fun favMediaQuery(anime: Boolean, page: Int, id: Int? = Anilist.userid): String {
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
private fun favMediaQuery(anime: Boolean, page: Int): String {
return """User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
}
suspend fun recommendations(): ArrayList<Media> {
@@ -502,7 +383,7 @@ class AnilistQueries {
}
private fun recommendationPlannedQuery(type: String): String {
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${if (type == "ANIME") ", sort: MEDIA_POPULARITY_DESC" else ""} ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING , sort: MEDIA_POPULARITY_DESC ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
}
suspend fun initHomePage(): Map<String, ArrayList<Media>> {
@@ -542,8 +423,7 @@ class AnilistQueries {
}, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
query += """}""".trimEnd(',')
val response = executeQuery<Query.HomePageMedia>(query, show = true)
Logger.log(response.toString())
val response = executeQuery<Query.HomePageMedia>(query)
val returnMap = mutableMapOf<String, ArrayList<Media>>()
fun current(type: String) {
val subMap = mutableMapOf<Int, Media>()
@@ -566,19 +446,10 @@ class AnilistQueries {
subMap[m.id] = m
}
}
if (type != "Anime") {
returnArray.addAll(subMap.values)
returnMap["current$type"] = returnArray
return
}
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
val set = PrefManager.getCustomVal<Set<Int>>("continue_${type.uppercase()}", setOf())
.toMutableSet()
if (set.isNotEmpty()) {
set.reversed().forEach {
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
}
for (i in subMap) {
@@ -601,14 +472,9 @@ class AnilistQueries {
subMap[m.id] = m
}
}
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
if (set.isNotEmpty()) {
set.reversed().forEach {
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
}
for (i in subMap) {
@@ -692,11 +558,12 @@ class AnilistQueries {
private suspend fun bannerImage(type: String): String? {
val image = BannerImage(
PrefManager.getCustomVal("banner_${type}_url", ""),
//var image = loadData<BannerImage>("banner_$type")
val image: BannerImage? = BannerImage(
PrefManager.getCustomVal("banner_${type}_url", null),
PrefManager.getCustomVal("banner_${type}_time", 0L)
)
if (image.url.isNullOrEmpty() || image.checkTime()) {
if (image == null || image.checkTime()) {
val response =
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """)
val random = response?.data?.mediaListCollection?.lists?.mapNotNull {
@@ -725,7 +592,7 @@ class AnilistQueries {
sortOrder: String? = null
): MutableMap<String, ArrayList<Media>> {
val response =
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage genres meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
val sorted = mutableMapOf<String, ArrayList<Media>>()
val unsorted = mutableMapOf<String, ArrayList<Media>>()
val all = arrayListOf<Media>()
@@ -753,7 +620,7 @@ class AnilistQueries {
if (!sorted.containsKey(it.key)) sorted[it.key] = it.value
}
sorted["Favourites"] = favMedia(anime, userId)
sorted["Favourites"] = favMedia(anime)
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
//favMedia doesn't fill userProgress, so we need to fill it manually by searching :(
sorted["Favourites"]?.forEach { fav ->
@@ -763,7 +630,7 @@ class AnilistQueries {
}
sorted["All"] = all
val listSort: String? = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
val listSort: String = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
else PrefManager.getVal(PrefName.MangaListSortOrder)
val sort = listSort ?: sortOrder ?: options?.rowOrder
for (i in sorted.keys) {
@@ -794,8 +661,8 @@ class AnilistQueries {
PrefManager.getVal<Set<String>>(PrefName.TagsListNonAdult).toMutableList()
var tags = if (adultTags.isEmpty() || nonAdultTags.isEmpty()) null else
mapOf(
true to adultTags.sortedBy { it },
false to nonAdultTags.sortedBy { it }
true to adultTags,
false to nonAdultTags
)
if (genres.isNullOrEmpty()) {
@@ -831,7 +698,7 @@ class AnilistQueries {
}
}
return if (!genres.isNullOrEmpty() && tags != null) {
Anilist.genres = genres?.sortedBy { it }?.toMutableList() as ArrayList<String>
Anilist.genres = genres
Anilist.tags = tags
true
} else false
@@ -911,23 +778,18 @@ class AnilistQueries {
sort: String? = null,
genres: MutableList<String>? = null,
tags: MutableList<String>? = null,
status: String? = null,
source: String? = null,
format: String? = null,
countryOfOrigin: String? = null,
isAdult: Boolean = false,
onList: Boolean? = null,
excludedGenres: MutableList<String>? = null,
excludedTags: MutableList<String>? = null,
startYear: Int? = null,
seasonYear: Int? = null,
season: String? = null,
id: Int? = null,
hd: Boolean = false,
adultOnly: Boolean = false
): SearchResults? {
val query = """
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) {
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
pageInfo {
total
@@ -972,19 +834,14 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
}
""".replace("\n", " ").replace(""" """, "")
val variables = """{"type":"$type","isAdult":$isAdult
${if (adultOnly) ""","isAdult":true""" else ""}
${if (onList != null) ""","onList":$onList""" else ""}
${if (page != null) ""","page":"$page"""" else ""}
${if (id != null) ""","id":"$id"""" else ""}
${if (type == "ANIME" && seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
${if (type == "MANGA" && startYear != null) ""","yearGreater":${startYear}0000,"yearLesser":${startYear + 1}0000""" else ""}
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
${if (season != null) ""","season":"$season"""" else ""}
${if (search != null) ""","search":"$search"""" else ""}
${if (source != null) ""","source":"$source"""" else ""}
${if (sort != null) ""","sort":"$sort"""" else ""}
${if (status != null) ""","status":"$status"""" else ""}
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
${if (countryOfOrigin != null) ""","countryOfOrigin":"$countryOfOrigin"""" else ""}
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
${
if (excludedGenres?.isNotEmpty() == true)
@@ -1016,6 +873,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
else ""
}
}""".replace("\n", " ").replace(""" """, "")
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page
if (response?.media != null) {
val responseArray = arrayListOf<Media>()
@@ -1047,11 +905,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
excludedGenres = excludedGenres,
tags = tags,
excludedTags = excludedTags,
status = status,
source = source,
format = format,
countryOfOrigin = countryOfOrigin,
startYear = startYear,
seasonYear = seasonYear,
season = season,
results = responseArray,
@@ -1062,156 +916,11 @@ 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 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 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 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}}}"""
}
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(
"\"",
""
)
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 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 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 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}}}"""
}
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
}
suspend fun recentlyUpdated(
smaller: Boolean = true,
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 +967,41 @@ 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
if (smaller) {
val response = execute()?.airingSchedules ?: return null
val idArr = mutableListOf<Int>()
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
return response.mapNotNull { i ->
i.media?.let {
if (!idArr.contains(it.id))
if (!listOnly && (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) || (listOnly && it.mediaListEntry != null)) {
idArr.add(it.id)
Media(it)
} else null
else null
}
} ?: listOf())
}
next()
while (res?.pageInfo?.hasNextPage == true) {
}.toMutableList()
} else {
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++
while (res?.pageInfo?.hasNextPage == true) {
next()
i++
}
return list.reversed().toMutableList()
}
return list.reversed().toMutableList()
}
suspend fun getCharacterDetails(character: Character): Character {
@@ -1463,39 +1187,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 +1214,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,155 +1221,8 @@ Page(page:$page,perPage:50) {
yearMedia.remove("CANCELLED")
yearMedia["CANCELLED"] = a
}
author.character = characters
author.yearMedia = yearMedia
return author
}
suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
return executeQuery<Query.ToggleFollow>(
"""mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}"""
)
}
suspend fun toggleLike(id: Int, type: String): ToggleLike? {
return executeQuery<ToggleLike>(
"""mutation Like{ToggleLikeV2(id:$id,type:$type){__typename}}"""
)
}
suspend fun getUserProfile(id: Int): Query.UserProfileResponse? {
return executeQuery<Query.UserProfileResponse>(
"""{followerPage:Page{followers(userId:$id){id}pageInfo{total}}followingPage:Page{following(userId:$id){id}pageInfo{total}}user:User(id:$id){id name about(asHtml:true)avatar{medium large}bannerImage isFollowing isFollower isBlocked favourites{anime{nodes{id coverImage{extraLarge large medium color}}}manga{nodes{id coverImage{extraLarge large medium color}}}characters{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}staff{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}studios{nodes{id name isFavourite}}}statistics{anime{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}manga{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}}siteUrl}}""",
force = true
)
}
suspend fun getUserProfile(username: String): Query.UserProfileResponse? {
val id = getUserId(username) ?: return null
return getUserProfile(id)
}
suspend fun getUserId(username: String): Int? {
return executeQuery<Query.User>(
"""{User(name:"$username"){id}}""",
force = true
)?.data?.user?.id
}
suspend fun getUserStatistics(id: Int, sort: String = "ID"): Query.StatisticsResponse? {
return executeQuery<Query.StatisticsResponse>(
"""{User(id:$id){id name mediaListOptions{scoreFormat}statistics{anime{...UserStatistics}manga{...UserStatistics}}}}fragment UserStatistics on UserStatistics{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead formats(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds format}statuses(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds status}scores(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds score}lengths(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds length}releaseYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds releaseYear}startYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds startYear}genres(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds genre}tags(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds tag{id name}}countries(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds country}voiceActors(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds voiceActor{id name{first middle last full native alternative userPreferred}}characterIds}staff(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds staff{id name{first middle last full native alternative userPreferred}}}studios(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds studio{id name isAnimationStudio}}}""",
force = true,
show = true
)
}
private fun userFavMediaQuery(anime: Boolean, page: Int, id: Int): String {
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
}
suspend fun userFollowing(id: Int): Query.Following? {
return executeQuery<Query.Following>(
"""{Page {following(userId:${id},sort:[USERNAME]){id name avatar{large medium}bannerImage}}}""",
force = true
)
}
suspend fun userFollowers(id: Int): Query.Follower? {
return executeQuery<Query.Follower>(
"""{Page {followers(userId:${id},sort:[USERNAME]){id name avatar{large medium}bannerImage}}}""",
force = true
)
}
suspend fun initProfilePage(id: Int): Query.ProfilePageMedia? {
return executeQuery<Query.ProfilePageMedia>(
"""{
favoriteAnime:${userFavMediaQuery(true, 1, id)}
favoriteManga:${userFavMediaQuery(false, 1, id)}
}""".trimIndent(), force = true
)
}
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,}}}}""",
force = true
)
if (res != null && resetNotification) {
val commentNotifications = PrefManager.getVal(PrefName.UnreadCommentNotifications, 0)
res.data.user.unreadNotificationCount += commentNotifications
PrefManager.setVal(PrefName.UnreadCommentNotifications, 0)
Anilist.unreadNotificationCount = 0
}
return res
}
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,"
else "isFollowing:true,type_not:MESSAGE,"
return executeQuery<FeedResponse>(
"""{Page(page:$page,perPage:$ITEMS_PER_PAGE){activities(${filter}sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id title{english romaji native userPreferred}bannerImage coverImage{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id recipientId messengerId type replyCount likeCount message(asHtml:true)isLocked isSubscribed isLiked isPrivate siteUrl createdAt recipient{id name bannerImage avatar{medium large}}messenger{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}}}}""",
force = true
)
}
suspend fun getUpcomingAnime(id: String): List<Media> {
val res = executeQuery<Query.MediaListCollection>(
"""{MediaListCollection(userId:$id,type:ANIME){lists{name entries{media{id,isFavourite,title{userPreferred,romaji}coverImage{medium}nextAiringEpisode{timeUntilAiring}}}}}}""",
force = true
)
val list = mutableListOf<Media>()
res?.data?.mediaListCollection?.lists?.forEach { listEntry ->
listEntry.entries?.forEach { entry ->
entry.media?.nextAiringEpisode?.timeUntilAiring?.let {
list.add(Media(entry.media!!))
}
}
}
return list.sortedBy { it.timeUntilAiring }
.distinctBy { it.id }
.filter { it.timeUntilAiring != null }
}
suspend fun isUserFav(
favType: AnilistMutations.FavType,
id: Int
): Boolean { //anilist isFavourite is broken, so we need to check it manually
val res = getUserProfile(Anilist.userid ?: return false)
return when (favType) {
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
}
}
companion object {
const val ITEMS_PER_PAGE = 25
}
}

View File

@@ -15,7 +15,6 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -58,40 +57,48 @@ class AnilistHomeViewModel : ViewModel() {
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
private val animeFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
private val animePlanned: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
suspend fun setAnimePlanned() =
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
private val mangaFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
suspend fun setMangaPlanned() =
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
private val recommendation: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
suspend fun initHomePage() {
val res = Anilist.query.initHomePage()
Logger.log("AnilistHomeViewModel : res=$res")
res["currentAnime"]?.let { animeContinue.postValue(it) }
res["favoriteAnime"]?.let { animeFav.postValue(it) }
res["plannedAnime"]?.let { animePlanned.postValue(it) }
@@ -103,8 +110,8 @@ class AnilistHomeViewModel : ViewModel() {
suspend fun loadMain(context: FragmentActivity) {
Anilist.getSavedToken()
MAL.getSavedToken()
Discord.getSavedToken()
MAL.getSavedToken(context)
Discord.getSavedToken(context)
if (!BuildConfig.FLAVOR.contains("fdroid")) {
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
}
@@ -135,19 +142,22 @@ class AnilistAnimeViewModel : ViewModel() {
sort = Anilist.sortBy[2],
season = season,
seasonYear = year,
hd = true,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
hd = true
)?.results
)
}
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
private val animePopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = animePopular
suspend fun loadPopular(
type: String,
searchVal: String? = null,
search_val: String? = null,
genres: ArrayList<String>? = null,
sort: String = Anilist.sortBy[1],
onList: Boolean = true,
@@ -155,11 +165,10 @@ class AnilistAnimeViewModel : ViewModel() {
animePopular.postValue(
Anilist.query.search(
type,
search = searchVal,
search = search_val,
onList = if (onList) null else false,
sort = sort,
genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
genres = genres
)
)
}
@@ -174,43 +183,13 @@ class AnilistAnimeViewModel : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
adultOnly = PrefManager.getVal(PrefName.AdultOnly),
r.onList
)
)
var loaded: Boolean = false
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated
private val popularMovies: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMovies(): LiveData<MutableList<Media>> = popularMovies
private val topRatedAnime: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTopRated(): LiveData<MutableList<Media>> = topRatedAnime
private val mostFavAnime: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMostFav(): LiveData<MutableList<Media>> = mostFavAnime
suspend fun loadAll() {
val list = Anilist.query.loadAnimeList()
updated.postValue(list["recentUpdates"])
popularMovies.postValue(list["trendingMovies"])
topRatedAnime.postValue(list["topRated"])
mostFavAnime.postValue(list["mostFav"])
}
}
class AnilistMangaViewModel : ViewModel() {
@@ -228,17 +207,29 @@ class AnilistMangaViewModel : ViewModel() {
type,
perPage = 10,
sort = Anilist.sortBy[2],
hd = true,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
hd = true
)?.results
)
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
suspend fun loadTrendingNovel() =
updated.postValue(
Anilist.query.search(
type,
perPage = 10,
sort = Anilist.sortBy[2],
format = "NOVEL"
)?.results
)
private val mangaPopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = mangaPopular
suspend fun loadPopular(
type: String,
searchVal: String? = null,
search_val: String? = null,
genres: ArrayList<String>? = null,
sort: String = Anilist.sortBy[1],
onList: Boolean = true,
@@ -246,11 +237,10 @@ class AnilistMangaViewModel : ViewModel() {
mangaPopular.postValue(
Anilist.query.search(
type,
search = searchVal,
search = search_val,
onList = if (onList) null else false,
sort = sort,
genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
genres = genres
)
)
}
@@ -265,55 +255,17 @@ class AnilistMangaViewModel : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
r.season
)
)
var loaded: Boolean = false
private val popularManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularManga(): LiveData<MutableList<Media>> = popularManga
private val popularManhwa: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularManhwa(): LiveData<MutableList<Media>> = popularManhwa
private val popularNovel: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularNovel(): LiveData<MutableList<Media>> = popularNovel
private val topRatedManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTopRated(): LiveData<MutableList<Media>> = topRatedManga
private val mostFavManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMostFav(): LiveData<MutableList<Media>> = mostFavManga
suspend fun loadAll() {
val list = Anilist.query.loadMangaList()
popularManga.postValue(list["trendingManga"])
popularManhwa.postValue(list["trendingManhwa"])
popularNovel.postValue(list["trendingNovel"])
topRatedManga.postValue(list["topRated"])
mostFavManga.postValue(list["mostFav"])
}
}
class AnilistSearch : ViewModel() {
@@ -332,17 +284,13 @@ class AnilistSearch : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season,
r.season
)
)
@@ -355,15 +303,11 @@ class AnilistSearch : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season
)
@@ -387,40 +331,4 @@ class GenresViewModel : ViewModel() {
}
}
}
}
class ProfileViewModel : ViewModel() {
private val mangaFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
private val animeFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
suspend fun setData(id: Int) {
val res = Anilist.query.initProfilePage(id)
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
it.node?.let { i ->
Media(i).apply { isFav = true }
}
}
mangaFav.postValue(ArrayList(mangaList ?: arrayListOf()))
val animeList = res?.data?.favoriteAnime?.favourites?.anime?.edges?.mapNotNull {
it.node?.let { i ->
Media(i).apply { isFav = true }
}
}
animeFav.postValue(ArrayList(animeList ?: arrayListOf()))
}
fun refresh() {
mangaFav.postValue(mangaFav.value)
animeFav.postValue(animeFav.value)
}
}

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.logger
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.startMainActivity
@@ -15,6 +16,7 @@ class Login : AppCompatActivity() {
ThemeManager(this).applyTheme()
val data: Uri? = intent?.data
logger(data.toString())
try {
Anilist.token =
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value

View File

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

View File

@@ -11,25 +11,20 @@ import ani.dantotsu.themes.ThemeManager
class UrlMedia : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
val data: Uri? = intent?.data
val type = data?.pathSegments?.getOrNull(0)
if (type != "user") {
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
var isMAL = false
var continueMedia = true
if (id == 0) {
continueMedia = false
isMAL = data?.host != "anilist.co"
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
} else loadMedia = id
startMainActivity(
this,
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)
)
} else {
val username = data.pathSegments?.getOrNull(1)
startMainActivity(this, bundleOf("username" to username))
}
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
var isMAL = false
var continueMedia = true
if (id == 0) {
continueMedia = false
val data: Uri? = intent?.data
isMAL = data?.host != "anilist.co"
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
} else loadMedia = id
startMainActivity(
this,
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)
)
}
}

View File

@@ -46,7 +46,7 @@ data class Character(
// Notes for site moderators
@SerialName("modNotes") var modNotes: String?,
) : java.io.Serializable
)
@Serializable
data class CharacterConnection(
@@ -55,8 +55,8 @@ data class CharacterConnection(
@SerialName("nodes") var nodes: List<Character>?,
// The pagination information
@SerialName("pageInfo") var pageInfo: PageInfo?,
) : java.io.Serializable
// @SerialName("pageInfo") var pageInfo: PageInfo?,
)
@Serializable
data class CharacterEdge(
@@ -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>?,
@@ -82,7 +82,7 @@ data class CharacterEdge(
// The order the character should be displayed from the users favourites
@SerialName("favouriteOrder") var favouriteOrder: Int?,
) : java.io.Serializable
)
@Serializable
data class CharacterName(
@@ -109,7 +109,7 @@ data class CharacterName(
// The currently authenticated users preferred name language. Default romaji for non-authenticated
@SerialName("userPreferred") var userPreferred: String?,
) : java.io.Serializable
)
@Serializable
data class CharacterImage(
@@ -118,4 +118,4 @@ data class CharacterImage(
// The character's image of media at medium size
@SerialName("medium") var medium: String?,
) : java.io.Serializable
)

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?
)
}
@@ -141,586 +139,41 @@ class Query {
)
}
@Serializable
data class ProfilePageMedia(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?
)
}
@Serializable
data class AnimeList(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("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")
val data: Data?
) {
@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")
val data: Data?
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("ToggleFollow")
val toggleFollow: FollowData
) : java.io.Serializable
}
@Serializable
data class GenreCollection(
@SerialName("data")
val data: Data
) : java.io.Serializable {
) {
@Serializable
data class Data(
@SerialName("GenreCollection")
val genreCollection: List<String>?
) : java.io.Serializable
)
}
@Serializable
data class MediaTagCollection(
@SerialName("data")
val data: Data
) : java.io.Serializable {
) {
@Serializable
data class Data(
@SerialName("MediaTagCollection")
val mediaTagCollection: List<MediaTag>?
) : java.io.Serializable
)
}
@Serializable
data class User(
@SerialName("data")
val data: Data
) : java.io.Serializable {
) {
@Serializable
data class Data(
@SerialName("User")
val user: ani.dantotsu.connections.anilist.api.User?
) : java.io.Serializable
)
}
@Serializable
data class UserProfileResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("followerPage")
val followerPage: UserProfilePage?,
@SerialName("followingPage")
val followingPage: UserProfilePage?,
@SerialName("user")
val user: UserProfile?
) : java.io.Serializable
}
@Serializable
data class UserProfilePage(
@SerialName("pageInfo")
val pageInfo: PageInfo,
) : java.io.Serializable
@Serializable
data class Following(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: FollowingPage?
) : java.io.Serializable
}
@Serializable
data class Follower(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: FollowerPage?
) : java.io.Serializable
}
@Serializable
data class FollowerPage(
@SerialName("followers")
val followers: List<ani.dantotsu.connections.anilist.api.User>?
) : java.io.Serializable
@Serializable
data class FollowingPage(
@SerialName("following")
val following: List<ani.dantotsu.connections.anilist.api.User>?
) : java.io.Serializable
@Serializable
data class UserProfile(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
@SerialName("about")
val about: String?,
@SerialName("avatar")
val avatar: UserAvatar?,
@SerialName("bannerImage")
val bannerImage: String?,
@SerialName("isFollowing")
var isFollowing: Boolean,
@SerialName("isFollower")
val isFollower: Boolean,
@SerialName("isBlocked")
val isBlocked: Boolean,
@SerialName("favourites")
val favourites: UserFavourites?,
@SerialName("statistics")
val statistics: NNUserStatisticTypes,
@SerialName("siteUrl")
val siteUrl: String,
) : java.io.Serializable
@Serializable
data class NNUserStatisticTypes(
@SerialName("anime") var anime: NNUserStatistics,
@SerialName("manga") var manga: NNUserStatistics
) : java.io.Serializable
@Serializable
data class NNUserStatistics(
@SerialName("count") var count: Int,
@SerialName("meanScore") var meanScore: Float,
@SerialName("standardDeviation") var standardDeviation: Float,
@SerialName("minutesWatched") var minutesWatched: Int,
@SerialName("episodesWatched") var episodesWatched: Int,
@SerialName("chaptersRead") var chaptersRead: Int,
@SerialName("volumesRead") var volumesRead: Int,
) : java.io.Serializable
@Serializable
data class UserFavourites(
@SerialName("anime")
val anime: UserMediaFavouritesCollection,
@SerialName("manga")
val manga: UserMediaFavouritesCollection,
@SerialName("characters")
val characters: UserCharacterFavouritesCollection,
@SerialName("staff")
val staff: UserStaffFavouritesCollection,
@SerialName("studios")
val studios: UserStudioFavouritesCollection,
) : java.io.Serializable
@Serializable
data class UserMediaFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserMediaImageFavorite>,
) : java.io.Serializable
@Serializable
data class UserMediaImageFavorite(
@SerialName("id")
val id: Int,
@SerialName("coverImage")
val coverImage: MediaCoverImage
) : java.io.Serializable
@Serializable
data class UserCharacterFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserCharacterImageFavorite>,
) : java.io.Serializable
@Serializable
data class UserCharacterImageFavorite(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: CharacterName,
@SerialName("image")
val image: CharacterImage,
@SerialName("isFavourite")
val isFavourite: Boolean
) : java.io.Serializable
@Serializable
data class UserStaffFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character
) : java.io.Serializable
@Serializable
data class UserStudioFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserStudioFavorite>,
) : java.io.Serializable
@Serializable
data class UserStudioFavorite(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
) : java.io.Serializable
//----------------------------------------
// Statistics
@Serializable
data class StatisticsResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("User")
val user: StatisticsUser?
) : java.io.Serializable
}
@Serializable
data class StatisticsUser(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
@SerialName("mediaListOptions")
val mediaListOptions: MediaListOptions,
@SerialName("statistics")
val statistics: StatisticsTypes
) : java.io.Serializable
@Serializable
data class StatisticsTypes(
@SerialName("anime")
val anime: Statistics,
@SerialName("manga")
val manga: Statistics
) : java.io.Serializable
@Serializable
data class Statistics(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("standardDeviation")
val standardDeviation: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("episodesWatched")
val episodesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("volumesRead")
val volumesRead: Int,
@SerialName("formats")
val formats: List<StatisticsFormat>,
@SerialName("statuses")
val statuses: List<StatisticsStatus>,
@SerialName("scores")
val scores: List<StatisticsScore>,
@SerialName("lengths")
val lengths: List<StatisticsLength>,
@SerialName("releaseYears")
val releaseYears: List<StatisticsReleaseYear>,
@SerialName("startYears")
val startYears: List<StatisticsStartYear>,
@SerialName("genres")
val genres: List<StatisticsGenre>,
@SerialName("tags")
val tags: List<StatisticsTag>,
@SerialName("countries")
val countries: List<StatisticsCountry>,
@SerialName("voiceActors")
val voiceActors: List<StatisticsVoiceActor>,
@SerialName("staff")
val staff: List<StatisticsStaff>,
@SerialName("studios")
val studios: List<StatisticsStudio>
) : java.io.Serializable
@Serializable
data class StatisticsFormat(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("format")
val format: String
) : java.io.Serializable
@Serializable
data class StatisticsStatus(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("status")
val status: String
) : java.io.Serializable
@Serializable
data class StatisticsScore(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("score")
val score: Int
) : java.io.Serializable
@Serializable
data class StatisticsLength(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("length")
val length: String? //can be null for manga
) : java.io.Serializable
@Serializable
data class StatisticsReleaseYear(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("releaseYear")
val releaseYear: Int
) : java.io.Serializable
@Serializable
data class StatisticsStartYear(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("startYear")
val startYear: Int
) : java.io.Serializable
@Serializable
data class StatisticsGenre(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("genre")
val genre: String
) : java.io.Serializable
@Serializable
data class StatisticsTag(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("tag")
val tag: Tag
) : java.io.Serializable
@Serializable
data class Tag(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String
) : java.io.Serializable
@Serializable
data class StatisticsCountry(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("country")
val country: String
) : java.io.Serializable
@Serializable
data class StatisticsVoiceActor(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("voiceActor")
val voiceActor: VoiceActor,
@SerialName("characterIds")
val characterIds: List<Int>
) : java.io.Serializable
@Serializable
data class VoiceActor(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: StaffName
) : java.io.Serializable
@Serializable
data class StaffName(
@SerialName("first")
val first: String?,
@SerialName("middle")
val middle: String?,
@SerialName("last")
val last: String?,
@SerialName("full")
val full: String?,
@SerialName("native")
val native: String?,
@SerialName("alternative")
val alternative: List<String>?,
@SerialName("userPreferred")
val userPreferred: String?
) : java.io.Serializable
@Serializable
data class StatisticsStaff(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("staff")
val staff: VoiceActor
) : java.io.Serializable
@Serializable
data class StatisticsStudio(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("studio")
val studio: StatStudio
) : java.io.Serializable
@Serializable
data class StatStudio(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
@SerialName("isAnimationStudio")
val isAnimationStudio: Boolean
) : java.io.Serializable
}
//data class WhaData(
@@ -750,7 +203,7 @@ class Query {
// // Activity reply query
// val ActivityReply: ActivityReply?,
// // CommentNotificationWorker query
// // Comment query
// val ThreadComment: List<ThreadComment>?,
// // Notification query

View File

@@ -1,114 +0,0 @@
package ani.dantotsu.connections.anilist.api
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class FeedResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: ActivityPage
) : java.io.Serializable
}
@Serializable
data class ActivityPage(
@SerialName("activities")
val activities: List<Activity>
) : java.io.Serializable
@Serializable
data class Activity(
@SerialName("__typename")
val typename: String,
@SerialName("id")
val id: Int,
@SerialName("recipientId")
val recipientId: Int?,
@SerialName("messengerId")
val messengerId: Int?,
@SerialName("userId")
val userId: Int?,
@SerialName("type")
val type: String,
@SerialName("replyCount")
val replyCount: Int,
@SerialName("status")
val status: String?,
@SerialName("progress")
val progress: String?,
@SerialName("text")
val text: String?,
@SerialName("message")
val message: String?,
@SerialName("siteUrl")
val siteUrl: String?,
@SerialName("isLocked")
val isLocked: Boolean,
@SerialName("isSubscribed")
val isSubscribed: Boolean,
@SerialName("likeCount")
var likeCount: Int?,
@SerialName("isLiked")
var isLiked: Boolean?,
@SerialName("isPinned")
val isPinned: Boolean?,
@SerialName("isPrivate")
val isPrivate: Boolean?,
@SerialName("createdAt")
val createdAt: Int,
@SerialName("user")
val user: User?,
@SerialName("recipient")
val recipient: User?,
@SerialName("messenger")
val messenger: User?,
@SerialName("media")
val media: Media?,
@SerialName("replies")
val replies: List<ActivityReply>?,
@SerialName("likes")
val likes: List<User>?,
) : java.io.Serializable
@Serializable
data class ActivityReply(
@SerialName("id")
val id: Int,
@SerialName("userId")
val userId: Int,
@SerialName("text")
val text: String,
@SerialName("likeCount")
val likeCount: Int,
@SerialName("isLiked")
val isLiked: Boolean,
@SerialName("createdAt")
val createdAt: Int,
@SerialName("user")
val user: User,
@SerialName("likes")
val likes: List<User>?,
) : java.io.Serializable
@Serializable
data class ToggleLike(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("ToggleLikeV2")
val toggleLike: LikeData
) : java.io.Serializable
}
@Serializable
data class LikeData(
@SerialName("__typename")
val typename: String
) : java.io.Serializable

View File

@@ -251,7 +251,7 @@ data class MediaCoverImage(
// Average #hex color of cover image
@SerialName("color") var color: String?,
) : java.io.Serializable
)
@Serializable
data class MediaList(
@@ -490,7 +490,7 @@ data class MediaExternalLink(
// isDisabled: Boolean
@SerialName("notes") var notes: String?,
) : java.io.Serializable
)
@Serializable
enum class ExternalLinkType {
@@ -512,13 +512,7 @@ data class MediaListCollection(
// If there is another chunk
@SerialName("hasNextChunk") var hasNextChunk: Boolean?,
) : java.io.Serializable
@Serializable
data class FollowData(
@SerialName("id") var id: Int,
@SerialName("isFollowing") var isFollowing: Boolean,
) : java.io.Serializable
)
@Serializable
data class MediaListGroup(
@@ -532,4 +526,4 @@ data class MediaListGroup(
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
@SerialName("status") var status: MediaListStatus?,
) : java.io.Serializable
)

View File

@@ -1,123 +0,0 @@
package ani.dantotsu.connections.anilist.api
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
enum class NotificationType(val value: String) {
ACTIVITY_MESSAGE("ACTIVITY_MESSAGE"),
ACTIVITY_REPLY("ACTIVITY_REPLY"),
FOLLOWING("FOLLOWING"),
ACTIVITY_MENTION("ACTIVITY_MENTION"),
THREAD_COMMENT_MENTION("THREAD_COMMENT_MENTION"),
THREAD_SUBSCRIBED("THREAD_SUBSCRIBED"),
THREAD_COMMENT_REPLY("THREAD_COMMENT_REPLY"),
AIRING("AIRING"),
ACTIVITY_LIKE("ACTIVITY_LIKE"),
ACTIVITY_REPLY_LIKE("ACTIVITY_REPLY_LIKE"),
THREAD_LIKE("THREAD_LIKE"),
THREAD_COMMENT_LIKE("THREAD_COMMENT_LIKE"),
ACTIVITY_REPLY_SUBSCRIBED("ACTIVITY_REPLY_SUBSCRIBED"),
RELATED_MEDIA_ADDITION("RELATED_MEDIA_ADDITION"),
MEDIA_DATA_CHANGE("MEDIA_DATA_CHANGE"),
MEDIA_MERGE("MEDIA_MERGE"),
MEDIA_DELETION("MEDIA_DELETION"),
//custom
COMMENT_REPLY("COMMENT_REPLY"),
}
@Serializable
data class NotificationResponse(
@SerialName("data")
val data: Data,
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("User")
val user: NotificationUser,
@SerialName("Page")
val page: NotificationPage,
) : java.io.Serializable
}
@Serializable
data class NotificationUser(
@SerialName("unreadNotificationCount")
var unreadNotificationCount: Int,
) : java.io.Serializable
@Serializable
data class NotificationPage(
@SerialName("pageInfo")
val pageInfo: PageInfo,
@SerialName("notifications")
val notifications: List<Notification>,
) : java.io.Serializable
@Serializable
data class Notification(
@SerialName("__typename")
val typename: String,
@SerialName("id")
val id: Int,
@SerialName("userId")
val userId: Int? = null,
@SerialName("CommentId")
val commentId: Int?,
@SerialName("type")
val notificationType: String,
@SerialName("activityId")
val activityId: Int? = null,
@SerialName("animeId")
val mediaId: Int? = null,
@SerialName("episode")
val episode: Int? = null,
@SerialName("contexts")
val contexts: List<String>? = null,
@SerialName("context")
val context: String? = null,
@SerialName("reason")
val reason: String? = null,
@SerialName("deletedMediaTitle")
val deletedMediaTitle: String? = null,
@SerialName("deletedMediaTitles")
val deletedMediaTitles: List<String>? = null,
@SerialName("createdAt")
val createdAt: Int,
@SerialName("media")
val media: Media? = null,
@SerialName("user")
val user: User? = null,
@SerialName("message")
val message: MessageActivity? = null,
@SerialName("activity")
val activity: ActivityUnion? = null,
@SerialName("Thread")
val thread: Thread? = null,
@SerialName("comment")
val comment: ThreadComment? = null,
) : java.io.Serializable
@Serializable
data class MessageActivity(
@SerialName("id")
val id: Int?,
) : java.io.Serializable
@Serializable
data class ActivityUnion(
@SerialName("id")
val id: Int?,
) : java.io.Serializable
@Serializable
data class Thread(
@SerialName("id")
val id: Int?,
) : java.io.Serializable
@Serializable
data class ThreadComment(
@SerialName("id")
val id: Int?,
) : java.io.Serializable

View File

@@ -15,7 +15,7 @@ data class Staff(
@SerialName("languageV2") var languageV2: String?,
// The staff images
@SerialName("image") var image: StaffImage?,
// @SerialName("image") var image: StaffImage?,
// A general description of the staff member
@SerialName("description") var description: String?,
@@ -94,15 +94,6 @@ data class StaffConnection(
// @SerialName("pageInfo") var pageInfo: PageInfo?,
)
@Serializable
data class StaffImage(
// The character's image of media at its largest size
@SerialName("large") var large: String?,
// 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

@@ -46,7 +46,7 @@ data class User(
@SerialName("statistics") var statistics: UserStatisticTypes?,
// The number of unread notifications the user has
@SerialName("unreadNotificationCount") var unreadNotificationCount: Int?,
// @SerialName("unreadNotificationCount") var unreadNotificationCount: Int?,
// The url for the user page on the AniList website
// @SerialName("siteUrl") var siteUrl: String?,
@@ -111,7 +111,7 @@ data class UserAvatar(
// The avatar of user at medium size
@SerialName("medium") var medium: String?,
) : java.io.Serializable
)
@Serializable
data class UserStatisticTypes(
@@ -164,7 +164,7 @@ data class Favourites(
@Serializable
data class MediaListOptions(
// The score format the user is using for media lists
@SerialName("scoreFormat") var scoreFormat: String?,
// @SerialName("scoreFormat") var scoreFormat: ScoreFormat?,
// The default order list rows should be displayed in
@SerialName("rowOrder") var rowOrder: String?,

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

@@ -1,570 +0,0 @@
package ani.dantotsu.connections.comments
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import com.lagradost.nicehttp.NiceResponse
import com.lagradost.nicehttp.Requests
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okio.IOException
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object CommentsAPI {
private const val ADDRESS: String = "https://1224665.xyz:443"
var authToken: String? = null
var userId: String? = null
var isBanned: Boolean = false
var isAdmin: Boolean = false
var isMod: Boolean = false
var totalVotes: Int = 0
suspend fun getCommentsForId(
id: Int,
page: Int = 1,
tag: Int?,
sort: String?
): CommentResponse? {
var url = "$ADDRESS/comments/$id/$page"
val request = requestBuilder()
tag?.let {
url += "?tag=$it"
}
sort?.let {
url += if (tag != null) "&sort=$it" else "?sort=$it"
}
val json = try {
request.get(url)
} catch (e: IOException) {
snackString("Failed to fetch comments")
return null
}
if (!json.text.startsWith("{")) return null
val res = json.code == 200
if (!res && json.code != 404) {
errorReason(json.code, json.text)
}
val parsed = try {
Json.decodeFromString<CommentResponse>(json.text)
} catch (e: Exception) {
return null
}
return parsed
}
suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? {
val url = "$ADDRESS/comments/parent/$id/$page"
val request = requestBuilder()
val json = try {
request.get(url)
} catch (e: IOException) {
snackString("Failed to fetch comments")
return null
}
if (!json.text.startsWith("{")) return null
val res = json.code == 200
if (!res && json.code != 404) {
errorReason(json.code, json.text)
}
val parsed = try {
Json.decodeFromString<CommentResponse>(json.text)
} catch (e: Exception) {
return null
}
return parsed
}
suspend fun getSingleComment(id: Int): Comment? {
val url = "$ADDRESS/comments/$id"
val request = requestBuilder()
val json = try {
request.get(url)
} catch (e: IOException) {
snackString("Failed to fetch comment")
return null
}
if (!json.text.startsWith("{")) return null
val res = json.code == 200
if (!res && json.code != 404) {
errorReason(json.code, json.text)
}
val parsed = try {
Json.decodeFromString<Comment>(json.text)
} catch (e: Exception) {
return null
}
return parsed
}
suspend fun vote(commentId: Int, voteType: Int): Boolean {
val url = "$ADDRESS/comments/vote/$commentId/$voteType"
val request = requestBuilder()
val json = try {
request.post(url)
} catch (e: IOException) {
snackString("Failed to vote")
return false
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
}
return res
}
suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? {
val url = "$ADDRESS/comments"
val body = FormBody.Builder()
.add("user_id", userId ?: return null)
.add("media_id", mediaId.toString())
.add("content", content)
if (tag != null) {
body.add("tag", tag.toString())
}
parentCommentId?.let {
body.add("parent_comment_id", it.toString())
}
val request = requestBuilder()
val json = try {
request.post(url, requestBody = body.build())
} catch (e: IOException) {
snackString("Failed to comment")
return null
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
return null
}
val parsed = try {
Json.decodeFromString<ReturnedComment>(json.text)
} catch (e: Exception) {
snackString("Failed to parse comment")
return null
}
return Comment(
parsed.id,
parsed.userId,
parsed.mediaId,
parsed.parentCommentId,
parsed.content,
parsed.timestamp,
parsed.deleted,
parsed.tag,
0,
0,
null,
Anilist.username ?: "",
Anilist.avatar,
totalVotes = totalVotes
)
}
suspend fun deleteComment(commentId: Int): Boolean {
val url = "$ADDRESS/comments/$commentId"
val request = requestBuilder()
val json = try {
request.delete(url)
} catch (e: IOException) {
snackString("Failed to delete comment")
return false
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
}
return res
}
suspend fun editComment(commentId: Int, content: String): Boolean {
val url = "$ADDRESS/comments/$commentId"
val body = FormBody.Builder()
.add("content", content)
.build()
val request = requestBuilder()
val json = try {
request.put(url, requestBody = body)
} catch (e: IOException) {
snackString("Failed to edit comment")
return false
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
}
return res
}
suspend fun banUser(userId: String): Boolean {
val url = "$ADDRESS/ban/$userId"
val request = requestBuilder()
val json = try {
request.post(url)
} catch (e: IOException) {
snackString("Failed to ban user")
return false
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
}
return res
}
suspend fun reportComment(
commentId: Int,
username: String,
mediaTitle: String,
reportedId: String
): Boolean {
val url = "$ADDRESS/report/$commentId"
val body = FormBody.Builder()
.add("username", username)
.add("mediaName", mediaTitle)
.add("reporter", Anilist.username ?: "unknown")
.add("reportedId", reportedId)
.build()
val request = requestBuilder()
val json = try {
request.post(url, requestBody = body)
} catch (e: IOException) {
snackString("Failed to report comment")
return false
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
}
return res
}
suspend fun getNotifications(client: OkHttpClient): NotificationResponse? {
val url = "$ADDRESS/notification/reply"
val request = requestBuilder(client)
val json = try {
request.get(url)
} catch (e: IOException) {
return null
}
if (!json.text.startsWith("{")) return null
val res = json.code == 200
if (!res) {
return null
}
val parsed = try {
Json.decodeFromString<NotificationResponse>(json.text)
} catch (e: Exception) {
return null
}
return parsed
}
private suspend fun getUserDetails(client: OkHttpClient? = null): User? {
val url = "$ADDRESS/user"
val request = if (client != null) requestBuilder(client) else requestBuilder()
val json = try {
request.get(url)
} catch (e: IOException) {
return null
}
if (json.code == 200) {
val parsed = try {
Json.decodeFromString<UserResponse>(json.text)
} catch (e: Exception) {
e.printStackTrace()
return null
}
isBanned = parsed.user.isBanned ?: false
isAdmin = parsed.user.isAdmin ?: false
isMod = parsed.user.isMod ?: false
totalVotes = parsed.user.totalVotes
return parsed.user
}
return null
}
suspend fun fetchAuthToken(client: OkHttpClient? = null) {
if (authToken != null) return
val MAX_RETRIES = 5
val tokenLifetime: Long = 1000 * 60 * 60 * 24 * 6 // 6 days
val tokenExpiry = PrefManager.getVal<Long>(PrefName.CommentTokenExpiry)
if (tokenExpiry < System.currentTimeMillis() + tokenLifetime) {
val commentResponse =
PrefManager.getNullableVal<AuthResponse>(PrefName.CommentAuthResponse, null)
if (commentResponse != null) {
authToken = commentResponse.authToken
userId = commentResponse.user.id
isBanned = commentResponse.user.isBanned ?: false
isAdmin = commentResponse.user.isAdmin ?: false
isMod = commentResponse.user.isMod ?: false
totalVotes = commentResponse.user.totalVotes
if (getUserDetails(client) != null) return
}
}
val url = "$ADDRESS/authenticate"
val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return
repeat(MAX_RETRIES) {
try {
val json = authRequest(token, url, client)
if (json.code == 200) {
if (!json.text.startsWith("{")) throw IOException("Invalid response")
val parsed = try {
Json.decodeFromString<AuthResponse>(json.text)
} catch (e: Exception) {
snackString("Failed to login to comments API: ${e.printStackTrace()}")
return
}
PrefManager.setVal(PrefName.CommentAuthResponse, parsed)
PrefManager.setVal(
PrefName.CommentTokenExpiry,
System.currentTimeMillis() + tokenLifetime
)
authToken = parsed.authToken
userId = parsed.user.id
isBanned = parsed.user.isBanned ?: false
isAdmin = parsed.user.isAdmin ?: false
isMod = parsed.user.isMod ?: false
totalVotes = parsed.user.totalVotes
return
} else if (json.code != 429) {
errorReason(json.code, json.text)
return
}
} catch (e: IOException) {
snackString("Failed to login to comments API")
return
}
kotlinx.coroutines.delay(60000)
}
snackString("Failed to login after multiple attempts")
}
fun logout() {
PrefManager.removeVal(PrefName.CommentAuthResponse)
PrefManager.removeVal(PrefName.CommentTokenExpiry)
authToken = null
userId = null
isBanned = false
isAdmin = false
isMod = false
totalVotes = 0
}
private suspend fun authRequest(
token: String,
url: String,
client: OkHttpClient? = null
): NiceResponse {
val body: FormBody = FormBody.Builder()
.add("token", token)
.build()
val request = if (client != null) requestBuilder(client) else requestBuilder()
return request.post(url, requestBody = body)
}
private fun headerBuilder(): Map<String, String> {
val map = mutableMapOf(
"appauth" to "6*45Qp%W2RS@t38jkXoSKY588Ynj%n"
)
if (authToken != null) {
map["Authorization"] = authToken!!
}
return map
}
private fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
return Requests(
client,
headerBuilder()
)
}
private fun errorReason(code: Int, reason: String? = null) {
val error = when (code) {
429 -> "Rate limited. :("
else -> "Failed to connect"
}
val parsed = try {
Json.decodeFromString<ErrorResponse>(reason!!)
} catch (e: Exception) {
null
}
val message = parsed?.message ?: reason ?: error
val fullMessage = if (code == 500) message else "$code: $message"
toast(fullMessage)
}
}
@Serializable
data class ErrorResponse(
@SerialName("message")
val message: String
)
@Serializable
data class NotificationResponse(
@SerialName("notifications")
val notifications: List<Notification>
)
@Serializable
data class Notification(
@SerialName("username")
val username: String,
@SerialName("media_id")
val mediaId: Int,
@SerialName("comment_id")
val commentId: Int,
@SerialName("type")
val type: Int? = null,
@SerialName("content")
val content: String? = null,
@SerialName("notification_id")
val notificationId: Int
)
@Serializable
data class AuthResponse(
@SerialName("authToken")
val authToken: String,
@SerialName("user")
val user: User
) : java.io.Serializable {
companion object {
private const val serialVersionUID: Long = 1
}
}
@Serializable
data class UserResponse(
@SerialName("user")
val user: User
)
@Serializable
data class User(
@SerialName("user_id")
val id: String,
@SerialName("username")
val username: String,
@SerialName("profile_picture_url")
val profilePictureUrl: String? = null,
@SerialName("is_banned")
@Serializable(with = NumericBooleanSerializer::class)
val isBanned: Boolean? = null,
@SerialName("is_mod")
@Serializable(with = NumericBooleanSerializer::class)
val isAdmin: Boolean? = null,
@SerialName("is_admin")
@Serializable(with = NumericBooleanSerializer::class)
val isMod: Boolean? = null,
@SerialName("total_votes")
val totalVotes: Int,
@SerialName("warnings")
val warnings: Int
) : java.io.Serializable {
companion object {
private const val serialVersionUID: Long = 1
}
}
@Serializable
data class CommentResponse(
@SerialName("comments")
val comments: List<Comment>,
@SerialName("totalPages")
val totalPages: Int
)
@Serializable
data class Comment(
@SerialName("comment_id")
val commentId: Int,
@SerialName("user_id")
val userId: String,
@SerialName("media_id")
val mediaId: Int,
@SerialName("parent_comment_id")
val parentCommentId: Int?,
@SerialName("content")
var content: String,
@SerialName("timestamp")
var timestamp: String,
@SerialName("deleted")
@Serializable(with = NumericBooleanSerializer::class)
val deleted: Boolean?,
@SerialName("tag")
val tag: Int?,
@SerialName("upvotes")
var upvotes: Int,
@SerialName("downvotes")
var downvotes: Int,
@SerialName("user_vote_type")
var userVoteType: Int?,
@SerialName("username")
val username: String,
@SerialName("profile_picture_url")
val profilePictureUrl: String?,
@SerialName("is_mod")
@Serializable(with = NumericBooleanSerializer::class)
val isMod: Boolean? = null,
@SerialName("is_admin")
@Serializable(with = NumericBooleanSerializer::class)
val isAdmin: Boolean? = null,
@SerialName("reply_count")
val replyCount: Int? = null,
@SerialName("total_votes")
val totalVotes: Int
)
@Serializable
data class ReturnedComment(
@SerialName("id")
var id: Int,
@SerialName("comment_id")
var commentId: Int?,
@SerialName("user_id")
val userId: String,
@SerialName("media_id")
val mediaId: Int,
@SerialName("parent_comment_id")
val parentCommentId: Int? = null,
@SerialName("content")
val content: String,
@SerialName("timestamp")
val timestamp: String,
@SerialName("deleted")
@Serializable(with = NumericBooleanSerializer::class)
val deleted: Boolean?,
@SerialName("tag")
val tag: Int? = null,
)
object NumericBooleanSerializer : KSerializer<Boolean> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("NumericBoolean", PrimitiveKind.INT)
override fun serialize(encoder: Encoder, value: Boolean) {
encoder.encodeInt(if (value) 1 else 0)
}
override fun deserialize(decoder: Decoder): Boolean {
return decoder.decodeInt() != 0
}
}

View File

@@ -1,19 +1,17 @@
package ani.dantotsu.connections.crashlytics
import android.content.Context
import ani.dantotsu.util.Logger
class CrashlyticsStub : CrashlyticsInterface {
override fun initialize(context: Context) {
//no-op
}
override fun logException(e: Throwable) {
Logger.log(e)
//no-op
}
override fun log(message: String) {
Logger.log(message)
//no-op
}
override fun setUserId(id: String) {

View File

@@ -20,14 +20,14 @@ object Discord {
var avatar: String? = null
fun getSavedToken(): Boolean {
fun getSavedToken(context: Context): Boolean {
token = PrefManager.getVal(
PrefName.DiscordToken, null as String?
)
return token != null
}
fun saveToken(token: String) {
fun saveToken(context: Context, token: String) {
PrefManager.setVal(PrefName.DiscordToken, token)
}
@@ -70,7 +70,19 @@ 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"
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png"
/*fun defaultRPC(): RPC? {
return token?.let {
RPC(it, Dispatchers.IO).apply {
applicationId = application_Id
smallImage = RPC.Link(
"Dantotsu",
small_Image
)
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
}
}
}*/
}

View File

@@ -5,12 +5,17 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.IBinder
import android.os.PowerManager
import android.provider.MediaStore
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@@ -21,7 +26,6 @@ import ani.dantotsu.connections.discord.serializers.User
import ani.dantotsu.isOnline
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
@@ -33,6 +37,7 @@ import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.io.File
import java.io.OutputStreamWriter
class DiscordService : Service() {
private var heartbeat: Int = 0
@@ -44,7 +49,6 @@ class DiscordService : Service() {
private lateinit var heartbeatThread: Thread
private lateinit var client: OkHttpClient
private lateinit var wakeLock: PowerManager.WakeLock
private val shouldLog = false
var presenceStore = ""
val json = Json {
encodeDefaults = true
@@ -63,7 +67,7 @@ class DiscordService : Service() {
PowerManager.PARTIAL_WAKE_LOCK,
"discordRPC:backgroundPresence"
)
wakeLock.acquire(30 * 60 * 1000L /*30 minutes*/)
wakeLock.acquire()
log("WakeLock Acquired")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
@@ -158,8 +162,8 @@ class DiscordService : Service() {
inner class DiscordWebSocketListener : WebSocketListener() {
private var retryAttempts = 0
private val maxRetryAttempts = 10
var retryAttempts = 0
val maxRetryAttempts = 10
override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
this@DiscordService.webSocket = webSocket
@@ -228,7 +232,7 @@ class DiscordService : Service() {
resume()
resume = false
} else {
identify(webSocket)
identify(webSocket, baseContext)
log("WebSocket: Identified")
}
}
@@ -241,13 +245,13 @@ class DiscordService : Service() {
}
}
private fun identify(webSocket: WebSocket) {
fun identify(webSocket: WebSocket, context: Context) {
val properties = JsonObject()
properties.addProperty("os", "linux")
properties.addProperty("browser", "unknown")
properties.addProperty("device", "unknown")
val d = JsonObject()
d.addProperty("token", getToken())
d.addProperty("token", getToken(context))
d.addProperty("intents", 0)
d.add("properties", properties)
val payload = JsonObject()
@@ -266,11 +270,11 @@ class DiscordService : Service() {
retryAttempts++
if (retryAttempts >= maxRetryAttempts) {
log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
errorNotification("Timeout setting presence", "Max Retry Attempts")
errorNotification("Could not set the presence", "Max Retry Attempts")
return
}
}
t.message?.let { Logger.log("onFailure() $it") }
t.message?.let { Log.d("WebSocket", "onFailure() $it") }
log("WebSocket: Error, onFailure() reason: ${t.message}")
client = OkHttpClient()
client.newWebSocket(
@@ -285,7 +289,7 @@ class DiscordService : Service() {
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
Logger.log("onClosing() $code $reason")
Log.d("WebSocket", "onClosing() $code $reason")
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
heartbeatThread.interrupt()
}
@@ -293,7 +297,7 @@ class DiscordService : Service() {
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason)
Logger.log("onClosed() $code $reason")
Log.d("WebSocket", "onClosed() $code $reason")
if (code >= 4000) {
log("WebSocket: Error, code: $code reason: $reason")
client = OkHttpClient()
@@ -307,7 +311,7 @@ class DiscordService : Service() {
}
}
fun getToken(): String {
fun getToken(context: Context): String {
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
return if (token == null) {
log("WebSocket: Token not found")
@@ -345,13 +349,13 @@ class DiscordService : Service() {
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
//TODO: Request permission
return
}
notificationManager.notify(2, builder.build())
log("Error Notified")
}
@Suppress("unused")
fun saveSimpleTestPresence() {
val file = File(baseContext.cacheDir, "payload")
//fill with test payload
@@ -371,22 +375,65 @@ class DiscordService : Service() {
log("WebSocket: Simple Test Presence Saved")
}
fun setPresence(string: String) {
fun setPresence(String: String) {
log("WebSocket: Sending Presence payload")
log(string)
webSocket.send(string)
log(String)
webSocket.send(String)
}
fun log(string: String) {
if (shouldLog) {
Logger.log(string)
Log.d("WebSocket_Discord", string)
//log += "${SimpleDateFormat("HH:mm:ss").format(Calendar.getInstance().time)} $string\n"
}
fun saveLogToFile() {
val fileName = "log_${System.currentTimeMillis()}.txt"
// ContentValues to store file metadata
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
}
}
// Inserting the file in the MediaStore
val resolver = baseContext.contentResolver
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
} else {
val directory =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(directory, fileName)
// Make sure the Downloads directory exists
if (!directory.exists()) {
directory.mkdirs()
}
// Use FileProvider to get the URI for the file
val authority =
"${baseContext.packageName}.provider" // Adjust with your app's package name
Uri.fromFile(file)
}
// Writing to the file
uri?.let {
resolver.openOutputStream(it).use { outputStream ->
OutputStreamWriter(outputStream).use { writer ->
writer.write(log)
}
}
} ?: run {
log("Error saving log file")
}
}
fun resume() {
log("Sending Resume payload")
val d = JsonObject()
d.addProperty("token", getToken())
d.addProperty("token", getToken(baseContext))
d.addProperty("session_id", sessionId)
d.addProperty("seq", sequence)
val json = JsonObject()
@@ -402,7 +449,7 @@ class DiscordService : Service() {
Thread.sleep(heartbeat.toLong())
heartbeatSend(webSocket, sequence)
log("WebSocket: Heartbeat Sent")
} catch (ignored: InterruptedException) {
} catch (e: InterruptedException) {
}
}
}

View File

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

View File

@@ -2,8 +2,6 @@ package ani.dantotsu.connections.discord
import ani.dantotsu.connections.discord.serializers.Activity
import ani.dantotsu.connections.discord.serializers.Presence
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -71,8 +69,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(
@@ -83,7 +81,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
),
afk = true,
since = data.startTimestamp,
status = PrefManager.getVal(PrefName.DiscordStatus)
status = data.status
)
))
}

View File

@@ -1,114 +0,0 @@
package ani.dantotsu.connections.github
import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.getAppString
import ani.dantotsu.settings.Developer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
class Contributors {
fun getContributors(): Array<Developer> {
var developers = arrayOf<Developer>()
runBlocking(Dispatchers.IO) {
val repo = getAppString(R.string.repo)
val res = client.get("https://api.github.com/repos/$repo/contributors")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
res.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"
}
developers = developers.plus(
Developer(
it.login,
it.avatarUrl,
role,
it.htmlUrl
)
)
}
developers = developers.plus(
arrayOf(
Developer(
"MarshMeadow",
"https://avatars.githubusercontent.com/u/88599122?v=4",
"Beta Icon Designer & Website Maintainer",
"https://github.com/MarshMeadow?tab=repositories"
),
Developer(
"Zaxx69",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6342562-kxE8m4i7KUMK.png",
"Telegram Admin",
"https://anilist.co/user/6342562"
),
Developer(
"Arif Alam",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6011177-2n994qtayiR9.jpg",
"Discord & Comment Moderator",
"https://anilist.co/user/6011177"
),
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"
),
)
)
}
return developers
}
@Serializable
data class GithubResponse(
@SerialName("login")
val login: String,
@SerialName("avatar_url")
val avatarUrl: String,
@SerialName("html_url")
val htmlUrl: String
)
}

View File

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

View File

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

View File

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

View File

@@ -9,35 +9,32 @@ import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.defaultHeaders
import ani.dantotsu.currActivity
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.download.video.Helper
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
@@ -48,7 +45,9 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
@@ -57,12 +56,13 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
class AnimeDownloaderService : Service() {
private lateinit var notificationManager: NotificationManagerCompat
@@ -73,7 +73,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 +81,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 {
@@ -94,7 +88,6 @@ class AnimeDownloaderService : Service() {
setSmallIcon(R.drawable.ic_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
setProgress(100, 0, false)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
@@ -163,14 +156,27 @@ class AnimeDownloaderService : Service() {
@UnstableApi
fun cancelDownload(taskName: String) {
val sessionIds =
AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
.map { it.sessionId }.toMutableList()
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
sessionIds.forEach {
ffExtension!!.cancelDownload(it)
val url =
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
if (url.isEmpty()) {
snackString("Failed to cancel download")
return
}
currentTasks.removeAll { it.getTaskName() == taskName }
DownloadService.sendSetStopReason(
this@AnimeDownloaderService,
ExoplayerDownloadService::class.java,
url,
androidx.media3.exoplayer.offline.Download.STATE_STOPPED,
false
)
DownloadService.sendRemoveDownload(
this@AnimeDownloaderService,
ExoplayerDownloadService::class.java,
url,
false
)
CoroutineScope(Dispatchers.Default).launch {
mutex.withLock {
downloadJobs[taskName]?.cancel()
@@ -203,6 +209,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(
@@ -213,63 +220,18 @@ class AnimeDownloaderService : Service() {
true
}
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
builder.setContentText("Downloading ${task.title} - ${task.episode}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
val outputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title,
task.episode
) ?: throw Exception("Failed to create output directory")
outputDir.findFile("${task.getTaskName()}.mp4")?.delete()
val outputFile = outputDir.createFile("video/mp4", "${task.getTaskName()}.mp4")
?: throw Exception("Failed to create output file")
var percent = 0
var totalLength = 0.0
val path = ffExtension!!.setDownloadPath(
this@AnimeDownloaderService,
outputFile.uri
)
val headersStringBuilder = StringBuilder()
task.video.file.headers.forEach {
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
currActivity()?.let {
Helper.downloadVideo(
it,
task.video,
task.subtitle
)
}
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()
}
}
val headers = headersStringBuilder.toString()
var request = "-headers $headers "
request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
Logger.log("Request: $request")
val ffTask =
ffExtension.executeFFMpeg(request) {
// CALLED WHEN SESSION GENERATES STATISTICS
val timeInMilliseconds = it
if (timeInMilliseconds > 0 && totalLength > 0) {
percent = ((it / 1000) / totalLength * 100).toInt()
}
Logger.log("Statistics: $it")
}
task.sessionId = ffTask
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
ffTask
saveMediaInfo(task)
task.subtitle?.let {
@@ -279,124 +241,94 @@ class AnimeDownloaderService : Service() {
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
DownloadedType.Type.ANIME,
)
)
}
val downloadStarted =
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
if (!downloadStarted) {
logger("Download failed to start")
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed to start")
broadcastDownloadFailed(task.episode)
return@withContext
}
// periodically check if the download is complete
while (ffExtension.getState(ffTask) != "COMPLETED") {
if (ffExtension.getState(ffTask) == "FAILED") {
Logger.log("Download failed")
builder.setContentText(
"${
getTaskName(
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) {
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
logger("Download failed")
builder.setContentText("${task.title} - ${task.episode} Download failed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed")
logger("Download failed: ${download.failureReason}")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode
task.episode,
DownloadedType.Type.ANIME,
)
} Download failed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
toast("${getTaskName(task.title, task.episode)} Download failed")
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
),
false
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${getTaskName(task.title, task.episode)}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${download.failureReason}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
logger("Download completed")
builder.setContentText("${task.title} - ${task.episode} Download completed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
logger("Download stopped")
builder.setContentText("${task.title} - ${task.episode} Download stopped")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download stopped")
break
}
broadcastDownloadProgress(
task.episode,
download.percentDownloaded.toInt()
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
break
}
builder.setProgress(
100, percent.coerceAtMost(99),
false
)
broadcastDownloadProgress(
task.episode,
percent.coerceAtMost(99)
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
kotlinx.coroutines.delay(2000)
}
if (ffExtension.getState(ffTask) == "COMPLETED") {
if (ffExtension.hadError(ffTask)) {
Logger.log("Download failed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode
)
} Download failed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download failed")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${getTaskName(task.title, task.episode)}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
return@withContext
}
Logger.log("Download completed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode
)
} Download completed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
} else throw Exception("Download failed")
}
} catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
Logger.log("Exception while downloading file: ${e.message}")
logger("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
e.printStackTrace()
Injekt.get<CrashlyticsInterface>().logException(e)
@@ -405,24 +337,36 @@ class AnimeDownloaderService : Service() {
}
}
private fun saveMediaInfo(task: AnimeDownloadTask) {
CoroutineScope(Dispatchers.IO).launch {
val directory =
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
val episodeDirectory =
getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title,
task.episode
)
?: throw Exception("Directory not found")
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun hasDownloadStarted(
downloadManager: DownloadManager,
task: AnimeDownloadTask,
timeout: Long
): Boolean {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < timeout) {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) {
return true
}
// Delay between each poll
kotlinx.coroutines.delay(500)
}
return false
}
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: AnimeDownloadTask) {
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.animeLocation}/${task.title}"
)
val episodeDirectory = File(directory, task.episode)
if (!directory.exists()) directory.mkdirs()
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -456,25 +400,14 @@ class AnimeDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
try {
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@AnimeDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
file.writeText(jsonString)
}
}
}
}
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
@@ -485,16 +418,13 @@ class AnimeDownloaderService : Service() {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
val file = File(directory, name)
FileOutputStream(file).use { output ->
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.uri.toString()
return@withContext file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
@@ -561,15 +491,14 @@ class AnimeDownloaderService : Service() {
val episodeImage: String? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
var sessionId: Long = -1
) {
fun getTaskName(): String {
return "${title.replace("/", "")}/${episode.replace("/", "")}"
return "$title - $episode"
}
companion object {
fun getTaskName(title: String, episode: String): String {
return "${title.replace("/", "")}/${episode.replace("/", "")}"
return "$title - $episode"
}
}
}
@@ -583,6 +512,7 @@ class AnimeDownloaderService : Service() {
object AnimeServiceDataSingleton {
var video: Video? = null
var sourceMedia: Media? = null
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
@Volatile

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,25 +9,21 @@ import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications
@@ -35,8 +31,8 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
@@ -46,9 +42,10 @@ import kotlinx.coroutines.withContext
import okhttp3.Request
import okio.buffer
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
@@ -65,7 +62,7 @@ class NovelDownloaderService : Service() {
private val mutex = Mutex()
private var isCurrentlyProcessing = false
private val networkHelper = Injekt.get<NetworkHelper>()
val networkHelper = Injekt.get<NetworkHelper>()
override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services.
@@ -189,15 +186,15 @@ class NovelDownloaderService : Service() {
val contentType = response.header("Content-Type")
val contentDisposition = response.header("Content-Disposition")
Logger.log("Content-Type: $contentType")
Logger.log("Content-Disposition: $contentDisposition")
logger("Content-Type: $contentType")
logger("Content-Disposition: $contentDisposition")
// Return true if the Content-Type or Content-Disposition indicates an EPUB file
contentType == "application/epub+zip" ||
(contentDisposition?.contains(".epub") == true)
}
} catch (e: Exception) {
Logger.log("Error checking file type: ${e.message}")
logger("Error checking file type: ${e.message}")
false
}
}
@@ -228,12 +225,12 @@ class NovelDownloaderService : Service() {
if (!isEpubFile(task.downloadLink)) {
if (isAlreadyDownloaded(task.originalLink)) {
Logger.log("Already downloaded")
logger("Already downloaded")
broadcastDownloadFinished(task.originalLink)
snackString("Already downloaded")
return@withContext
}
Logger.log("Download link is not an .epub file")
logger("Download link is not an .epub file")
broadcastDownloadFailed(task.originalLink)
snackString("Download link is not an .epub file")
return@withContext
@@ -248,30 +245,27 @@ class NovelDownloaderService : Service() {
networkHelper.downloadClient.newCall(request).execute().use { response ->
// Ensure the response is successful and has a body
if (!response.isSuccessful) {
if (!response.isSuccessful || response.body == null) {
throw IOException("Failed to download file: ${response.message}")
}
val directory = getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title,
task.chapter
) ?: throw Exception("Directory not found")
directory.findFile("0.epub")?.forceDelete(this@NovelDownloaderService)
val file = directory.createFile("application/epub+zip", "0.epub")
?: throw Exception("File not created")
val file = File(
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${task.title}/${task.chapter}/0.epub"
)
// Create directories if they don't exist
file.parentFile?.takeIf { !it.exists() }?.mkdirs()
// Overwrite existing file
if (file.exists()) file.delete()
//download cover
task.coverUrl?.let {
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
}
val outputStream =
this@NovelDownloaderService.contentResolver.openOutputStream(file.uri)
?: throw Exception("Could not open OutputStream")
val sink = outputStream.sink().buffer()
val sink = file.sink().buffer()
val responseBody = response.body
val totalBytes = responseBody.contentLength()
var downloadedBytes = 0L
@@ -307,7 +301,7 @@ class NovelDownloaderService : Service() {
withContext(Dispatchers.Main) {
val progress =
(downloadedBytes * 100 / totalBytes).toInt()
Logger.log("Download progress: $progress")
logger("Download progress: $progress")
broadcastDownloadProgress(task.originalLink, progress)
}
lastBroadcastUpdate = downloadedBytes
@@ -322,7 +316,7 @@ class NovelDownloaderService : Service() {
}
}
} catch (e: Exception) {
Logger.log("Exception while downloading .epub inside request: ${e.message}")
logger("Exception while downloading .epub inside request: ${e.message}")
throw e
}
}
@@ -339,33 +333,29 @@ class NovelDownloaderService : Service() {
DownloadedType(
task.title,
task.chapter,
MediaType.NOVEL
DownloadedType.Type.NOVEL
)
)
broadcastDownloadFinished(task.originalLink)
snackString("${task.title} - ${task.chapter} Download finished")
}
} catch (e: Exception) {
Logger.log("Exception while downloading .epub: ${e.message}")
logger("Exception while downloading .epub: ${e.message}")
snackString("Exception while downloading .epub: ${e.message}")
Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.originalLink)
}
}
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) {
launchIO {
val directory =
getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title
) ?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${task.title}"
)
if (!directory.exists()) directory.mkdirs()
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -379,47 +369,33 @@ class NovelDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
try {
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@NovelDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
file.writeText(jsonString)
}
}
}
}
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
withContext(
Dispatchers.IO
) {
var connection: HttpURLConnection? = null
Logger.log("Downloading url $url")
println("Downloading url $url")
try {
connection = URL(url).openConnection() as HttpURLConnection
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
directory.findFile(name)?.forceDelete(this@NovelDownloaderService)
val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
val file = File(directory, name)
FileOutputStream(file).use { output ->
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.uri.toString()
return@withContext file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
@@ -494,6 +470,7 @@ class NovelDownloaderService : Service() {
}
object NovelServiceDataSingleton {
var sourceMedia: Media? = null
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
@Volatile

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,13 +5,11 @@ import android.content.Intent
import android.graphics.drawable.Animatable
import android.os.Build
import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.LayoutAnimationController
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
@@ -23,7 +21,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.blurImage
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
@@ -35,7 +32,6 @@ import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.user.ListActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn
import ani.dantotsu.setSlideUp
@@ -80,14 +76,9 @@ class HomeFragment : Fragment() {
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
binding.homeUserAvatar.loadImage(Anilist.avatar)
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage(
if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen,
Anilist.bg
)
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
binding.homeUserBg.loadImage(Anilist.bg)
binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener {
ContextCompat.startActivity(
@@ -127,38 +118,26 @@ class HomeFragment : Fragment() {
"dialog"
)
}
binding.homeUserAvatarContainer.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity(
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
.putExtra("userId", Anilist.userid), null
)
false
}
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.homeUserBg.updateLayoutParams { height += statusBarHeight }
binding.homeUserBgNoKen.updateLayoutParams { height += statusBarHeight }
binding.homeTopContainer.updatePadding(top = statusBarHeight)
var reached = false
val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
if (!binding.homeScroll.canScrollVertically(1)) {
reached = true
bottomBar.animate().translationZ(0f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
if (!binding.homeScroll.canScrollVertically(1)) {
reached = true
bottomBar.animate().translationZ(0f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
.start()
} else {
if (reached) {
bottomBar.animate().translationZ(12f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
.start()
} else {
if (reached) {
bottomBar.animate().translationZ(12f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
.start()
}
}
}
}
@@ -326,7 +305,6 @@ class HomeFragment : Fragment() {
}
}
val array = arrayOf(
"AnimeContinue",
"AnimeFav",
@@ -382,10 +360,6 @@ class HomeFragment : Fragment() {
override fun onResume() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) {
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
super.onResume()
}
}

View File

@@ -17,7 +17,6 @@ import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import com.google.android.material.textfield.TextInputEditText
class LoginFragment : Fragment() {
@@ -51,7 +50,7 @@ class LoginFragment : Fragment() {
DocumentFile.fromSingleUri(requireActivity(), uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
passwordAlertDialog() { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
@@ -79,7 +78,7 @@ class LoginFragment : Fragment() {
toast("Invalid file type")
}
} catch (e: Exception) {
Logger.log(e)
e.printStackTrace()
toast("Error importing settings")
}
}

View File

@@ -160,37 +160,11 @@ class MangaFragment : Fragment() {
})
mangaPageAdapter.ready.observe(viewLifecycleOwner) { i ->
if (i == true) {
model.getPopularNovel().observe(viewLifecycleOwner) {
model.getTrendingNovel().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()))
}
}
model.getPopularManga().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()))
}
}
model.getPopularManhwa().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTrendingManhwa(
MediaAdaptor(
0,
it,
requireActivity()
)
)
}
}
model.getTopRated().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
}
}
model.getMostFav().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
}
}
if (mangaPageAdapter.trendingViewPager != null) {
mangaPageAdapter.updateHeight()
model.getTrending().observe(viewLifecycleOwner) {
@@ -263,7 +237,7 @@ class MangaFragment : Fragment() {
}
model.loaded = true
model.loadTrending()
model.loadAll()
model.loadTrendingNovel()
model.loadPopular(
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularMangaList
@@ -284,9 +258,6 @@ class MangaFragment : Fragment() {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
if (this::mangaPageAdapter.isInitialized && _binding != null) {
mangaPageAdapter.updateNotificationCount()
}
super.onResume()
}

View File

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

View File

@@ -3,10 +3,7 @@ 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: String,
val name: 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

@@ -1,58 +0,0 @@
package ani.dantotsu.media
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemCharacterBinding
import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation
import java.io.Serializable
class AuthorAdapter(
private val authorList: ArrayList<Author>,
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
val binding =
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return AuthorViewHolder(binding)
}
override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root)
val author = authorList[position]
binding.itemCompactRelation.text = author.role
binding.itemCompactImage.loadImage(author.image)
binding.itemCompactTitle.text = author.name
}
override fun getItemCount(): Int = authorList.size
inner class AuthorViewHolder(val binding: ItemCharacterBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
val author = authorList[bindingAdapterPosition]
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
AuthorActivity::class.java
).putExtra("author", author as Serializable),
ActivityOptionsCompat.makeSceneTransitionAnimation(
itemView.context as Activity,
Pair.create(
binding.itemCompactImage,
ViewCompat.getTransitionName(binding.itemCompactImage)!!
),
).toBundle()
)
}
}
}
}

View File

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

View File

@@ -9,11 +9,10 @@ data class Character(
val image: String?,
val banner: String?,
val role: String,
var isFav: Boolean,
var description: String? = null,
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

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

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
@@ -8,7 +7,6 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.isGone
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -17,25 +15,20 @@ import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistMutations
import ani.dantotsu.databinding.ActivityCharacterBinding
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized
import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.abs
class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
@@ -55,7 +48,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
initActivity(this)
screenWidth = resources.displayMetrics.run { widthPixels / density }
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.transparent)
ContextCompat.getColor(this, R.color.status)
val banner =
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
@@ -82,40 +75,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
character.image
)
}
val link = "https://anilist.co/character/${character.id}"
binding.characterShare.setOnClickListener {
val i = Intent(Intent.ACTION_SEND)
i.type = "text/plain"
i.putExtra(Intent.EXTRA_TEXT, link)
startActivity(Intent.createChooser(i, character.name))
}
binding.characterShare.setOnLongClickListener {
openLinkInBrowser(link)
true
}
lifecycleScope.launch {
withContext(Dispatchers.IO) {
character.isFav =
Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
}
withContext(Dispatchers.Main) {
binding.characterFav.setImageResource(
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
}
}
binding.characterFav.setOnClickListener {
lifecycleScope.launch {
if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, character.id)) {
character.isFav = !character.isFav
binding.characterFav.setImageResource(
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
} else {
snackString("Failed to toggle favorite")
}
}
}
model.getCharacter().observe(this) {
if (it != null && !loaded) {
character = it
@@ -154,7 +114,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
}
override fun onResume() {
binding.characterProgress.isGone = loaded
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
super.onResume()
}
@@ -179,11 +139,13 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
isCollapsed = true
if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg)
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
}
if (percentage <= percent && isCollapsed) {
isCollapsed = false
if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.transparent)
ContextCompat.getColor(this, R.color.status)
binding.characterAppBar.setBackgroundResource(R.color.bg)
}
}
}

View File

@@ -1,10 +1,9 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
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
@@ -21,36 +20,23 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
return GenreViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
val binding = holder.binding
val desc =
(if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") +
(if (character.dateOfBirth.toString() != "")
"${currActivity()!!.getString(R.string.birthday)} ${character.dateOfBirth.toString()}" else "") +
(if (character.gender != "null")
currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
currActivity()!!.getString(R.string.male) -> currActivity()!!.getString(
R.string.male
)
currActivity()!!.getString(R.string.female) -> currActivity()!!.getString(
R.string.female
)
else -> character.gender
} else "") + "\n" + character.description
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
"Male" -> currActivity()!!.getString(R.string.male)
"Female" -> currActivity()!!.getString(R.string.female)
else -> character.gender
} else "") + "\n" + character.description
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
}
markWon.setMarkdown(binding.characterDesc, desc)
}
override fun getItemCount(): Int = 1

View File

@@ -67,12 +67,11 @@ class GenreActivity : AppCompatActivity() {
private fun loadLocalGenres(): ArrayList<String>? {
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList)
.toMutableList()
return if (genres.isEmpty()) {
.toMutableList() as ArrayList<String>?
return if (genres.isNullOrEmpty()) {
null
} else {
//sort alphabetically
genres.sort().let { genres as ArrayList<String> }
genres
}
}
}

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
@@ -26,7 +25,7 @@ data class Media(
var cover: String? = null,
var banner: String? = null,
var relation: String? = null,
var favourites: Int? = null,
var popularity: Int? = null,
var isAdult: Boolean,
var isFav: Boolean = false,
@@ -57,17 +56,13 @@ data class Media(
var trailer: String? = null,
var startDate: FuzzyDate? = null,
var endDate: FuzzyDate? = null,
var popularity: Int? = null,
var timeUntilAiring: Long? = null,
var characters: ArrayList<Character>? = null,
var staff: ArrayList<Author>? = null,
var prequel: Media? = null,
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,
@@ -87,7 +82,7 @@ data class Media(
name = apiMedia.title!!.english,
nameRomaji = apiMedia.title!!.romaji,
userPreferredName = apiMedia.title!!.userPreferred,
cover = apiMedia.coverImage?.large ?: apiMedia.coverImage?.medium,
cover = apiMedia.coverImage?.large,
banner = apiMedia.bannerImage,
status = apiMedia.status.toString(),
isFav = apiMedia.isFavourite!!,
@@ -99,8 +94,6 @@ data class Media(
meanScore = apiMedia.meanScore,
startDate = apiMedia.startDate,
endDate = apiMedia.endDate,
favourites = apiMedia.favourites,
timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000 },
anime = if (apiMedia.type == MediaType.ANIME) Anime(
totalEpisodes = apiMedia.episodes,
nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1)
@@ -115,8 +108,6 @@ 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()
}
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {

View File

@@ -1,6 +1,8 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
@@ -13,25 +15,25 @@ import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.blurImage
import ani.dantotsu.currActivity
import ani.dantotsu.*
import ani.dantotsu.databinding.ItemMediaCompactBinding
import ani.dantotsu.databinding.ItemMediaLargeBinding
import ani.dantotsu.databinding.ItemMediaPageBinding
import ani.dantotsu.databinding.ItemMediaPageSmallBinding
import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.request.RequestOptions
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import jp.wasabeef.glide.transformations.BlurTransformation
import java.io.Serializable
@@ -41,7 +43,6 @@ class MediaAdaptor(
private val activity: FragmentActivity,
private val matchParent: Boolean = false,
private val viewPager: ViewPager2? = null,
private val fav: Boolean = false,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@@ -83,7 +84,7 @@ class MediaAdaptor(
}
@SuppressLint("ClickableViewAccessibility")
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (type) {
0 -> {
@@ -92,8 +93,8 @@ class MediaAdaptor(
val media = mediaList?.getOrNull(position)
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore
@@ -127,7 +128,6 @@ class MediaAdaptor(
)
b.itemCompactTotal.text = " | ${media.manga.totalChapters ?: "~"}"
}
b.itemCompactProgressContainer.visibility = if (fav) View.GONE else View.VISIBLE
}
}
@@ -137,9 +137,9 @@ class MediaAdaptor(
val media = mediaList?.get(position)
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactBanner.loadImage(media.banner ?: media.cover)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore
@@ -149,30 +149,25 @@ class MediaAdaptor(
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
)
if (media.anime != null) {
val itemTotal = " " + if ((media.anime.totalEpisodes
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
?: 0) != 1
) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(
R.string.episode_singular
)
b.itemTotal.text = itemTotal
) currActivity()!!.getString(R.string.episode_plural)
else currActivity()!!.getString(R.string.episode_singular)
b.itemCompactTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
?: "??").toString()) else (media.anime.totalEpisodes
?: "??").toString()
} else if (media.manga != null) {
val itemTotal = " " + if ((media.manga.totalChapters
b.itemTotal.text = " " + if ((media.manga.totalChapters
?: 0) != 1
) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(
R.string.chapter_singular
)
b.itemTotal.text = itemTotal
) currActivity()!!.getString(R.string.chapter_plural)
else currActivity()!!.getString(R.string.chapter_singular)
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
}
@SuppressLint("NotifyDataSetChanged")
if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post {
val start = mediaList.size
mediaList.addAll(mediaList)
val end = mediaList.size - start
notifyItemRangeInserted(start, end)
notifyDataSetChanged()
}
}
}
@@ -181,7 +176,6 @@ class MediaAdaptor(
val b = (holder as MediaPageViewHolder).binding
val media = mediaList?.get(position)
if (media != null) {
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
b.itemCompactImage.loadImage(media.cover)
if (bannerAnimations)
@@ -191,12 +185,17 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator()
)
)
blurImage(
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
media.banner ?: media.cover
)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
val banner =
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed)
Glide.with(context as Context)
.load(GlideUrl(media.banner ?: media.cover))
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore
@@ -243,12 +242,17 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator()
)
)
blurImage(
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
media.banner ?: media.cover
)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
val banner =
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed)
Glide.with(context as Context)
.load(GlideUrl(media.banner ?: media.cover))
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore

View File

@@ -3,7 +3,6 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.util.TypedValue
@@ -13,40 +12,34 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.color
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.CustomBottomNavBar
import ani.dantotsu.GesturesListener
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.blurImage
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ActivityMediaBinding
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.media.comments.CommentsFragment
import ani.dantotsu.media.manga.MangaReadFragment
import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.AndroidBug5497Workaround
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized
import ani.dantotsu.settings.saving.PrefManager
@@ -54,49 +47,35 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.LauncherWrapper
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
import kotlin.math.abs
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
lateinit var launcher: LauncherWrapper
lateinit var binding: ActivityMediaBinding
private lateinit var binding: ActivityMediaBinding
private val scope = lifecycleScope
private val model: MediaDetailsViewModel by viewModels()
private lateinit var tabLayout: NavigationBarView
var selected = 0
lateinit var navBar: AnimatedBottomBar
var anime = true
private var adult = false
@SuppressLint("ClickableViewAccessibility")
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
val id = intent.getIntExtra("mediaId", -1)
if (id != -1) {
runBlocking {
withContext(Dispatchers.IO) {
media = Anilist.query.getMedia(id, false) ?: emptyMedia()
}
}
}
if (media.name == "No media found") {
snackString(media.name)
onBackPressedDispatcher.onBackPressed()
return
}
val contract = ActivityResultContracts.OpenDocumentTree()
launcher = LauncherWrapper(this, contract)
mediaSingleton = null
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
MediaSingleton.bitmap = null
@@ -104,44 +83,21 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat()
navBar = binding.mediaBottomBar
// Ui init
//Ui init
initActivity(this)
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
val oldMargin = binding.mediaViewPager.marginBottom
AndroidBug5497Workaround.assistActivity(this) {
if (it) {
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = 0
}
navBar.visibility = View.GONE
} else {
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = oldMargin
}
navBar.visibility = View.VISIBLE
}
}
val navBarRightMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) navBarHeight else 0
val navBarBottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = navBarRightMargin
bottomMargin = navBarBottomMargin
}
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.mediaCollapsing.minimumHeight = statusBarHeight
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.mediaTitle.isSelected = true
mMaxScrollSize = binding.mediaAppBar.totalScrollRange
@@ -163,6 +119,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
val banner =
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
val viewPager = binding.mediaViewPager
tabLayout = binding.mediaTab as NavigationBarView
viewPager.isUserInputEnabled = false
viewPager.setPageTransformer(ZoomOutPageTransformer())
@@ -172,15 +129,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaCoverImage.loadImage(media.cover)
binding.mediaCoverImage.setOnLongClickListener {
val coverTitle = "${media.userPreferredName}[Cover]"
ImageViewDialog.newInstance(
this,
coverTitle,
media.userPreferredName + "[Cover]",
media.cover
)
}
blurImage(banner, media.banner ?: media.cover)
banner.loadImage(media.banner ?: media.cover, 400)
val gestureDetector = GestureDetector(this, object : GesturesListener() {
override fun onDoubleClick(event: MotionEvent) {
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
@@ -192,10 +147,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
override fun onLongClick(event: MotionEvent) {
val bannerTitle = "${media.userPreferredName}[Banner]"
ImageViewDialog.newInstance(
this@MediaDetailsActivity,
bannerTitle,
media.userPreferredName + "[Banner]",
media.banner ?: media.cover
)
banner.performClick()
@@ -203,8 +157,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
})
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
if (PrefManager.getVal(PrefName.Incognito)) {
val mediaTitle = " ${media.userPreferredName}"
binding.mediaTitle.text = mediaTitle
binding.mediaTitle.text = " ${media.userPreferredName}"
binding.incognito.visibility = View.VISIBLE
} else {
binding.mediaTitle.text = media.userPreferredName
@@ -228,6 +181,20 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
R.drawable.ic_round_favorite_24
)
)
val typedValue = TypedValue()
this.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue,
true
)
val color = typedValue.data
val typedValue2 = TypedValue()
this.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue2,
true
)
val color2 = typedValue.data
PopImageButton(
scope,
@@ -235,7 +202,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
R.drawable.ic_round_favorite_24,
R.drawable.ic_round_favorite_border_24,
R.color.bg_opp,
R.color.violet_400,
R.color.violet_400,//TODO: Change to colorSecondary
media.isFav
) {
media.isFav = it
@@ -250,13 +217,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("ResourceType")
fun total() {
val text = SpannableStringBuilder().apply {
val mediaTypedValue = TypedValue()
val typedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorOnBackground,
mediaTypedValue,
typedValue,
true
)
val white = mediaTypedValue.data
val white = typedValue.data
if (media.userStatus != null) {
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
val typedValue = TypedValue()
@@ -346,68 +313,49 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
progress()
}
}
adult = media.isAdult
tabLayout.menu.clear()
if (media.anime != null) {
viewPager.adapter =
ViewPagerAdapter(
supportFragmentManager,
lifecycle,
SupportedMedia.ANIME,
media,
intent.getIntExtra("commentId", -1)
)
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME)
tabLayout.inflateMenu(R.menu.anime_menu_detail)
} else if (media.manga != null) {
viewPager.adapter = ViewPagerAdapter(
supportFragmentManager,
lifecycle,
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA,
media,
intent.getIntExtra("commentId", -1)
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
)
if (media.format == "NOVEL") {
tabLayout.inflateMenu(R.menu.novel_menu_detail)
} else {
tabLayout.inflateMenu(R.menu.manga_menu_detail)
}
anime = false
}
selected = media.selected!!.window
binding.mediaTitle.translationX = -screenWidth
tabLayout.visibility = View.VISIBLE
val infoTab = navBar.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
val watchTab = if (anime) {
navBar.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
} else if (media.format == "NOVEL") {
navBar.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
} else {
navBar.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
tabLayout.setOnItemSelectedListener { item ->
selectFromID(item.itemId)
viewPager.setCurrentItem(selected, false)
val sel = model.loadSelected(media, isDownload)
sel.window = selected
model.saveSelected(media.id, sel)
true
}
val commentTab =
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
navBar.addTab(infoTab)
navBar.addTab(watchTab)
navBar.addTab(commentTab)
tabLayout.selectedItemId = idFromSelect()
viewPager.setCurrentItem(selected, false)
if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
selected = 1
}
if (intent.getStringExtra("FRAGMENT_TO_LOAD") != null) selected = 2
if (viewPager.currentItem != selected) viewPager.post {
viewPager.setCurrentItem(selected, false)
}
binding.commentInputLayout.isVisible = selected == 2
navBar.selectTabAt(selected)
navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
selected = newIndex
binding.commentInputLayout.isVisible = selected == 2
viewPager.setCurrentItem(selected, true)
val sel = model.loadSelected(media, isDownload)
sel.window = selected
model.saveSelected(media.id, sel)
}
})
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) {
@@ -420,21 +368,35 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val rightMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) navBarHeight else 0
val bottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight
val params: ViewGroup.MarginLayoutParams =
navBar.layoutParams as ViewGroup.MarginLayoutParams
params.updateMargins(right = rightMargin, bottom = bottomMargin)
private fun selectFromID(id: Int) {
when (id) {
R.id.info -> {
selected = 0
}
R.id.watch, R.id.read -> {
selected = 1
}
}
}
private fun idFromSelect(): Int {
if (anime) when (selected) {
0 -> return R.id.info
1 -> return R.id.watch
}
else when (selected) {
0 -> return R.id.info
1 -> return R.id.read
}
return R.id.info
}
override fun onResume() {
navBar.selectTabAt(selected)
if (this::tabLayout.isInitialized) {
tabLayout.selectedItemId = idFromSelect()
}
super.onResume()
}
@@ -442,36 +404,24 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ANIME, MANGA, NOVEL
}
// ViewPager
//ViewPager
private class ViewPagerAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle,
private val mediaType: SupportedMedia,
private val media: Media,
private val commentId: Int
private val media: SupportedMedia
) :
FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int = 3
override fun getItemCount(): Int = 2
override fun createFragment(position: Int): Fragment = when (position) {
0 -> MediaInfoFragment()
1 -> when (mediaType) {
1 -> when (media) {
SupportedMedia.ANIME -> AnimeWatchFragment()
SupportedMedia.MANGA -> MangaReadFragment()
SupportedMedia.NOVEL -> NovelReadFragment()
}
2 -> {
val fragment = CommentsFragment()
val bundle = Bundle()
bundle.putInt("mediaId", media.id)
bundle.putString("mediaName", media.mainName())
if (commentId != -1) bundle.putInt("commentId", commentId)
fragment.arguments = bundle
fragment
}
else -> MediaInfoFragment()
}
}
@@ -489,6 +439,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaCover.visibility =
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
val typedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue,
true
)
val color = typedValue.data
if (percentage >= percent && !isCollapsed) {
isCollapsed = true
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)
@@ -527,7 +484,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
private val c1: Int,
private val c2: Int,
var clicked: Boolean,
needsInitialClick: Boolean = false,
callback: suspend (Boolean) -> (Unit)
) {
private var disabled = false
@@ -536,11 +492,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
init {
enabled(true)
if (needsInitialClick) {
scope.launch {
clicked()
}
}
image.setOnClickListener {
if (pressable && !disabled) {
pressable = false
@@ -595,4 +546,5 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
companion object {
var mediaSingleton: Media? = null
}
}
}

View File

@@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.logger
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter
@@ -28,7 +29,6 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
@@ -52,23 +52,26 @@ class MediaDetailsViewModel : ViewModel() {
it
}
if (isDownload) {
data.sourceIndex = when {
media.anime != null -> {
AnimeSources.list.size - 1
}
media.format == "MANGA" || media.format == "ONE_SHOT" -> {
MangaSources.list.size - 1
}
else -> {
NovelSources.list.size - 1
}
data.sourceIndex = if (media.anime != null) {
AnimeSources.list.size - 1
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
MangaSources.list.size - 1
} else {
NovelSources.list.size - 1
}
}
return data
}
fun loadSelectedStringLocation(sourceName: String): Int {
//find the location of the source in the list
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
if (location == -1) {
location = 0
}
return location
}
var continueMedia: Boolean? = null
private var loading = false
@@ -149,10 +152,10 @@ class MediaDetailsViewModel : ViewModel() {
watchSources?.get(i)?.apply {
if (!post && !allowsPreloading) return@apply
ep.sEpisode?.let {
loadByVideoServers(link, ep.extra, it) { extractor ->
if (extractor.videos.isNotEmpty()) {
list.add(extractor)
ep.extractorCallback?.invoke(extractor)
loadByVideoServers(link, ep.extra, it) {
if (it.videos.isNotEmpty()) {
list.add(it)
ep.extractorCallback?.invoke(it)
}
}
}
@@ -220,7 +223,7 @@ class MediaDetailsViewModel : ViewModel() {
}
fun setEpisode(ep: Episode?, who: String) {
Logger.log("set episode ${ep?.number} - $who")
logger("set episode ${ep?.number} - $who", false)
episode.postValue(ep)
MainScope().launch(Dispatchers.Main) {
episode.value = null
@@ -267,7 +270,7 @@ class MediaDetailsViewModel : ViewModel() {
mangaChapters
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
Logger.log("Loading Manga Chapters : $mangaLoaded")
logger("Loading Manga Chapters : $mangaLoaded")
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
mangaLoaded[i] =
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
@@ -288,6 +291,7 @@ class MediaDetailsViewModel : ViewModel() {
suspend fun loadMangaChapterImages(
chapter: MangaChapter,
selected: Selected,
series: String,
post: Boolean = true
): Boolean {

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.InputFilter.LengthFilter
import android.view.Gravity
@@ -9,16 +10,11 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.InputFilterMinMax
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.getSerialized
import ani.dantotsu.snackString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -58,43 +54,10 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = viewLifecycleOwner.lifecycleScope
binding.mediaListDelete.setOnClickListener {
var id = media.userListId
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
if (id != null) {
try {
Anilist.mutation.deleteList(id!!)
MAL.query.deleteList(media.anime != null, media.idMAL)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
snackString(getString(R.string.delete_fail_reason, e.message))
}
return@withContext
}
} else {
val profile = Anilist.query.userMediaDetails(media)
profile.userListId?.let { listId ->
id = listId
Anilist.mutation.deleteList(listId)
MAL.query.deleteList(media.anime != null, media.idMAL)
}
}
}
withContext(Dispatchers.Main) {
if (id != null) {
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} else {
snackString(getString(R.string.no_list_id))
}
}
}
}
binding.mediaListProgressBar.visibility = View.GONE
binding.mediaListLayout.visibility = View.VISIBLE
@@ -157,10 +120,7 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
val init =
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
.toInt() else 0
if (init < (total ?: 5000)) {
val progressText = "${init + 1}"
binding.mediaListProgress.setText(progressText)
}
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
if (init + 1 == (total ?: 5000)) {
binding.mediaListStatus.setText(statusStrings[2], false)
}

View File

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

View File

@@ -1,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,56 +0,0 @@
package ani.dantotsu.media
interface Type {
fun asText(): String
}
enum class MediaType : Type {
ANIME,
MANGA,
NOVEL;
override fun asText(): String {
return when (this) {
ANIME -> "Anime"
MANGA -> "Manga"
NOVEL -> "Novel"
}
}
companion object {
fun fromText(string: String): MediaType? {
return when (string) {
"Anime" -> ANIME
"Manga" -> MANGA
"Novel" -> NOVEL
else -> {
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
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.TextWatcher
@@ -13,7 +12,6 @@ import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.PopupMenu
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.LinearLayoutManager
@@ -25,12 +23,9 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.google.android.material.checkbox.MaterialCheckBox.STATE_CHECKED
import com.google.android.material.checkbox.MaterialCheckBox.STATE_INDETERMINATE
import com.google.android.material.checkbox.MaterialCheckBox.STATE_UNCHECKED
import com.google.android.material.checkbox.MaterialCheckBox.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -46,20 +41,6 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
private lateinit var binding: ItemSearchHeaderBinding
private fun updateFilterTextViewDrawable() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@@ -108,78 +89,16 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true
binding.searchChipRecycler.adapter = SearchChipAdapter(activity, this).also {
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
activity.updateChips = { it.update() }
}
binding.searchChipRecycler.layoutManager =
LinearLayoutManager(binding.root.context, HORIZONTAL, false)
binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
}
binding.searchFilter.setOnLongClickListener {
val popupMenu = PopupMenu(activity, binding.searchFilter)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
}
true
}
popupMenu.show()
true
}
binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
}
fun searchTitle() {
activity.result.apply {
search =
@@ -289,16 +208,13 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchHistoryList.startAnimation(fadeInAnimation())
binding.searchResultLayout.visibility = View.GONE
binding.searchHistoryList.visibility = View.VISIBLE
binding.searchByImage.visibility = View.VISIBLE
} else {
if (binding.searchResultLayout.visibility != View.VISIBLE) {
binding.searchResultLayout.startAnimation(fadeInAnimation())
binding.searchHistoryList.startAnimation(fadeOutAnimation())
}
binding.searchResultLayout.visibility = View.VISIBLE
binding.searchHistoryList.visibility = View.GONE
binding.searchByImage.visibility = View.GONE
}
}
@@ -331,10 +247,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
}
class SearchChipAdapter(
val activity: SearchActivity,
private val searchAdapter: SearchAdapter
) :
class SearchChipAdapter(val activity: SearchActivity) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList()
@@ -351,12 +264,11 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
val chip = chips[position]
holder.binding.root.apply {
text = chip.text.replace("_", " ")
text = chip.text
setOnClickListener {
activity.result.removeChip(chip)
update()
activity.search()
searchAdapter.updateFilterTextViewDrawable()
}
}
}
@@ -365,7 +277,6 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
fun update() {
chips = activity.result.toChipList()
notifyDataSetChanged()
searchAdapter.updateFilterTextViewDrawable()
}
override fun getItemCount(): Int = chips.size

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
@@ -11,37 +12,26 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.currActivity
import ani.dantotsu.*
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
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.openSettings
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.px
import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.toast
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@@ -52,7 +42,7 @@ class AnimeWatchAdapter(
private val fragment: AnimeWatchFragment,
private val watchSources: WatchSources
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
private var autoSelect = true
var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null
@@ -64,25 +54,20 @@ class AnimeWatchAdapter(
private var nestedDialog: AlertDialog? = null
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
_binding = binding
binding.faqbutton.setOnClickListener {
startActivity(
fragment.requireContext(),
Intent(fragment.requireContext(), FAQActivity::class.java),
null
)
}
//Youtube
if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
if (media.anime!!.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
binding.animeSourceYT.visibility = View.VISIBLE
binding.animeSourceYT.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
fragment.requireContext().startActivity(intent)
}
}
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
binding.animeSourceDubbedText.text =
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
@@ -106,12 +91,15 @@ class AnimeWatchAdapter(
null
)
}
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
val offline = if (!isOnline(binding.root.context) || PrefManager.getVal(
PrefName.OfflineMode
)
) View.GONE else View.VISIBLE
binding.animeSourceNameContainer.isGone = offline
binding.animeSourceSettings.isGone = offline
binding.animeSourceSearch.isGone = offline
binding.animeSourceTitle.isGone = offline
binding.animeSourceNameContainer.visibility = offline
binding.animeSourceSettings.visibility = offline
binding.animeSourceSearch.visibility = offline
binding.animeSourceTitle.visibility = offline
//Source Selection
var source =
@@ -123,7 +111,8 @@ class AnimeWatchAdapter(
this.selectDub = media.selected!!.preferDub
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
}
}
@@ -142,7 +131,8 @@ class AnimeWatchAdapter(
changing = true
binding.animeSourceDubbed.isChecked = selectDub
changing = false
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
source = i
setLanguageList(0, i)
}
@@ -162,12 +152,14 @@ class AnimeWatchAdapter(
changing = true
binding.animeSourceDubbed.isChecked = selectDub
changing = false
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
setLanguageList(i, source)
}
subscribeButton(false)
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
} ?: run { }
} ?: run {
}
}
//settings
@@ -187,8 +179,7 @@ class AnimeWatchAdapter(
R.drawable.ic_round_notifications_none_24,
R.color.bg_opp,
R.color.violet_400,
fragment.subscribed,
true
fragment.subscribed
) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
}
@@ -196,7 +187,7 @@ class AnimeWatchAdapter(
subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
openSettings(fragment.requireContext(), getChannelId(true, media.id))
}
//Nested Button
@@ -225,9 +216,9 @@ class AnimeWatchAdapter(
else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.setText(R.string.list)
1 -> dialogBinding.layoutText.setText(R.string.grid)
2 -> dialogBinding.layoutText.setText(R.string.compact)
0 -> dialogBinding.layoutText.text = "List"
1 -> dialogBinding.layoutText.text = "Grid"
2 -> dialogBinding.layoutText.text = "Compact"
else -> dialogBinding.animeSourceList
}
selected.alpha = 1f
@@ -239,24 +230,24 @@ class AnimeWatchAdapter(
dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
dialogBinding.layoutText.setText(R.string.list)
dialogBinding.layoutText.text = "List"
run = true
}
dialogBinding.animeSourceGrid.setOnClickListener {
selected(it as ImageButton)
style = 1
dialogBinding.layoutText.setText(R.string.grid)
dialogBinding.layoutText.text = "Grid"
run = true
}
dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 2
dialogBinding.layoutText.setText(R.string.compact)
dialogBinding.layoutText.text = "Compact"
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast(R.string.webview_not_installed)
toast("WebView not installed")
}
//start CookieCatcher activity
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
@@ -309,6 +300,7 @@ class AnimeWatchAdapter(
}
//Chips
@SuppressLint("SetTextI18n")
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding
if (binding != null) {
@@ -330,9 +322,7 @@ class AnimeWatchAdapter(
0
)
}
val chipText = "${names[limit * (position)]} - ${names[last - 1]}"
chip.text = chipText
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.setTextColor(
ContextCompat.getColorStateList(
fragment.requireContext(),
@@ -366,6 +356,7 @@ class AnimeWatchAdapter(
_binding?.animeSourceChipGroup?.removeAllViews()
}
@SuppressLint("SetTextI18n")
fun handleEpisodes() {
val binding = _binding
if (binding != null) {
@@ -373,9 +364,9 @@ class AnimeWatchAdapter(
val episodes = media.anime.episodes!!.keys.toTypedArray()
val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = PrefManager.getCustomVal<String?>(
"${media.id}_current_ep", ""
)?.toIntOrNull() ?: 1
val appEp =
PrefManager.getCustomVal<String?>("${media.id}_current_ep", "")?.toIntOrNull()
?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
if (episodes.contains(continueEp)) {
@@ -405,27 +396,21 @@ class AnimeWatchAdapter(
}
val ep = media.anime.episodes!![continueEp]!!
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
binding.itemEpisodeImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
)
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text =
currActivity()!!.getString(
R.string.continue_episode, ep.number, if (ep.filler)
currActivity()!!.getString(R.string.filler_tag)
else
"", cleanedTitle
)
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
binding.animeSourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp)
}
if (fragment.continueEp) {
if (
(binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams)
.weight < PrefManager.getVal<Float>(PrefName.WatchPercentage)
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < PrefManager.getVal<Float>(
PrefName.WatchPercentage
)
) {
binding.animeSourceContinue.performClick()
fragment.continueEp = false
@@ -436,35 +421,13 @@ class AnimeWatchAdapter(
}
binding.animeSourceProgressBar.visibility = View.GONE
val sourceFound = media.anime.episodes!!.isNotEmpty()
binding.animeSourceNotFound.isGone = sourceFound
binding.faqbutton.isGone = sourceFound
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) {
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) {
val nextIndex = media.selected!!.sourceIndex + 1
binding.animeSource.setText(
binding.animeSource.adapter
.getItem(nextIndex).toString(), false
)
fragment.onSourceChange(nextIndex).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } }
binding.animeSourceDubbed.isChecked = selectDub
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
setLanguageList(0, nextIndex)
}
subscribeButton(false)
fragment.loadEpisodes(nextIndex, false)
}
}
binding.animeSource.setOnClickListener { autoSelect = false }
if (media.anime.episodes!!.isNotEmpty())
binding.animeSourceNotFound.visibility = View.GONE
else
binding.animeSourceNotFound.visibility = View.VISIBLE
} else {
binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE
clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE
}
@@ -506,7 +469,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

@@ -17,46 +17,39 @@ import androidx.annotation.OptIn
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
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
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.dp
import ani.dantotsu.isOnline
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight
import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.setNavigationTheme
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import kotlinx.coroutines.Dispatchers
@@ -199,16 +192,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
@@ -233,7 +220,7 @@ class AnimeWatchFragment : Fragment() {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc =
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: ""
).isBlank()
) media.anime!!.kitsuEpisodes!![i]?.title
@@ -346,7 +333,16 @@ class AnimeWatchFragment : Fragment() {
var subscribed = false
fun onNotificationPressed(subscribed: Boolean, source: String) {
this.subscribed = subscribed
saveSubscription(media, subscribed)
saveSubscription(requireContext(), media, subscribed)
if (!subscribed)
Notifications.deleteChannel(requireContext(), getChannelId(true, media.id))
else
Notifications.createChannel(
requireContext(),
ANIME_GROUP,
getChannelId(true, media.id),
media.userPreferredName
)
snackString(
if (subscribed) getString(R.string.subscribed_notification, source)
else getString(R.string.unsubscribed_notification)
@@ -357,12 +353,18 @@ class AnimeWatchFragment : Fragment() {
val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = activity
if (activity is MediaDetailsActivity && isAdded) {
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).isVisible = show
activity.findViewById<ViewPager2>(R.id.mediaViewPager).isVisible = show
activity.findViewById<CardView>(R.id.mediaCover).isVisible = show
activity.findViewById<CardView>(R.id.mediaClose).isVisible = show
activity.navBar.isVisible = show
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).isGone = show
val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try {
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
} catch (e: ClassCastException) {
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
}
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
}
}
var itemSelected = false
@@ -430,29 +432,7 @@ class AnimeWatchFragment : Fragment() {
}
fun onAnimeEpisodeDownloadClick(i: String) {
activity?.let {
if (!hasDirAccess(it)) {
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
if (success) {
model.onEpisodeClick(
media,
i,
requireActivity().supportFragmentManager,
isDownload = true
)
} else {
snackString(getString(R.string.download_permission_required))
}
}
} else {
model.onEpisodeClick(
media,
i,
requireActivity().supportFragmentManager,
isDownload = true
)
}
}
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
}
fun onAnimeEpisodeStopDownloadClick(i: String) {
@@ -470,11 +450,10 @@ class AnimeWatchFragment : Fragment() {
DownloadedType(
media.mainName(),
i,
MediaType.ANIME
DownloadedType.Type.ANIME
)
) {
episodeAdapter.purgeDownload(i)
}
)
episodeAdapter.purgeDownload(i)
}
@OptIn(UnstableApi::class)
@@ -483,13 +462,22 @@ class AnimeWatchFragment : Fragment() {
DownloadedType(
media.mainName(),
i,
MediaType.ANIME
DownloadedType.Type.ANIME
)
) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
episodeAdapter.deleteDownload(i)
}
)
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
DownloadService.sendRemoveDownload(
requireContext(),
ExoplayerDownloadService::class.java,
id,
true
)
episodeAdapter.deleteDownload(i)
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
@@ -553,7 +541,7 @@ class AnimeWatchFragment : Fragment() {
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
episodeAdapter.notifyItemRangeInserted(0, arr.size)
for (download in downloadManager.animeDownloadedTypes) {
if (media.compareName(download.title)) {
if (download.title == media.mainName()) {
episodeAdapter.stopDownload(download.chapter)
}
}
@@ -573,8 +561,6 @@ class AnimeWatchFragment : Fragment() {
super.onResume()
binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
requireActivity().setNavigationTheme()
}
override fun onPause() {

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