Merge pull request #264 from rebelonion/dev

Dev
This commit is contained in:
rebel onion 2024-03-20 01:32:15 -05:00 committed by GitHub
commit 4035aee1f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
281 changed files with 14581 additions and 1948 deletions

View file

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

3
.gitignore vendored
View file

@ -8,6 +8,9 @@ local.properties
# Log/OS Files # Log/OS Files
*.log *.log
# Secrets
apikey.properties
# Android Studio generated files and folders # Android Studio generated files and folders
captures/ captures/
.externalNativeBuild/ .externalNativeBuild/

View file

@ -6,24 +6,20 @@ plugins {
id 'com.google.devtools.ksp' id 'com.google.devtools.ksp'
} }
def gitCommitHash = providers.exec {
commandLine("git", "rev-parse", "--verify", "--short", "HEAD")
}.standardOutput.asText.get().trim()
android { android {
compileSdk 34 compileSdk 34
defaultConfig { defaultConfig {
applicationId "ani.dantotsu" applicationId "ani.dantotsu"
minSdk 23 minSdk 21
targetSdk 34 targetSdk 34
versionCode((System.currentTimeMillis() / 60000).toInteger()) versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "2.2.0" versionName "3.0.0"
versionCode 220000000 versionCode 220000000
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
flavorDimensions "store" flavorDimensions += "store"
productFlavors { productFlavors {
fdroid { fdroid {
// F-Droid specific configuration // F-Droid specific configuration
@ -43,18 +39,21 @@ android {
alpha { alpha {
applicationIdSuffix ".beta" // keep as beta by popular request applicationIdSuffix ".beta" // keep as beta by popular request
versionNameSuffix "-alpha01" versionNameSuffix "-alpha01"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_alpha", icon_placeholder_round: "@mipmap/ic_launcher_alpha_round"] manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
debuggable System.getenv("CI") == null debuggable System.getenv("CI") == null
isDefault true isDefault true
} }
debug { debug {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
versionNameSuffix "-beta01" versionNameSuffix "-beta01"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"] manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
debuggable false debuggable false
} }
release { release {
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"] manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_round"
debuggable false debuggable false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
} }
@ -77,12 +76,12 @@ android {
dependencies { dependencies {
// FireBase // FireBase
googleImplementation platform('com.google.firebase:firebase-bom:32.2.3') googleImplementation platform('com.google.firebase:firebase-bom:32.7.4')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.0' googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.1'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.1' googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.2'
// Core // Core
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.7.0' implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.fragment:fragment-ktx:1.6.2' implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
@ -91,9 +90,9 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:2.9.0" implementation "androidx.work:work-runtime-ktx:2.9.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.10' implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.github.Blatzar:NiceHttp:0.4.4' implementation 'com.github.Blatzar:NiceHttp:0.4.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.10.0' implementation 'androidx.webkit:webkit:1.10.0'
@ -106,7 +105,7 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer // Exoplayer
ext.exo_version = '1.2.1' ext.exo_version = '1.3.0'
implementation "androidx.media3:media3-exoplayer:$exo_version" implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version" implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version" implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
@ -119,14 +118,30 @@ dependencies {
// UI // UI
implementation 'com.google.android.material:material:1.11.0' implementation 'com.google.android.material:material:1.11.0'
implementation 'nl.joery.animatedbottombar:library:1.1.0' //implementation 'nl.joery.animatedbottombar:library:1.1.0'
implementation 'io.noties.markwon:core:4.6.2' implementation 'com.github.rebelonion:AnimatedBottomBar:v1.1.0'
implementation 'com.flaviofaria:kenburnsview:1.0.7' implementation 'com.flaviofaria:kenburnsview:1.0.7'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.alexvasilkov:gesture-views:2.8.3' implementation 'com.alexvasilkov:gesture-views:2.8.3'
implementation 'com.github.VipulOG:ebook-reader:0.1.6' implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'com.github.eltos:simpledialogfragments:v3.7' implementation 'com.github.eltos:simpledialogfragments:v3.7'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:93972bc'
// Markwon
ext.markwon_version = '4.6.2'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:editor:$markwon_version"
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
implementation "io.noties.markwon:ext-tables:$markwon_version"
implementation "io.noties.markwon:ext-tasklist:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version"
implementation "io.noties.markwon:image-glide:$markwon_version"
// Groupie
ext.groupie_version = '2.10.1'
implementation "com.github.lisawray.groupie:groupie:$groupie_version"
implementation "com.github.lisawray.groupie:groupie-viewbinding:$groupie_version"
// string matching // string matching
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'
@ -141,11 +156,10 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12' 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:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'com.squareup.okio:okio:3.7.0' implementation 'com.squareup.okio:okio:3.8.0'
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12' implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
implementation 'ch.acra:acra-http:5.11.3' implementation 'org.jsoup:jsoup:1.16.1'
implementation 'org.jsoup:jsoup:1.15.4' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0' implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.tachiyomiorg:unifile:17bec43' implementation 'com.github.tachiyomiorg:unifile:17bec43'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'

View file

@ -43,6 +43,25 @@
public static <1> INSTANCE; public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...); 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. # @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault -keepattributes RuntimeVisibleAnnotations,AnnotationDefault

View file

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

View file

@ -16,7 +16,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
@ -69,7 +69,7 @@
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/currently_airing_widget_info" /> android:resource="@xml/currently_airing_widget_info" />
</receiver> </receiver>
<receiver android:name=".subcriptions.NotificationClickReceiver" /> <receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
<activity <activity
@ -104,7 +104,26 @@
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".settings.ExtensionsActivity" android:name=".settings.ExtensionsActivity"
android:windowSoftInputMode="adjustResize|stateHidden"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.ProfileActivity"
android:windowSoftInputMode="adjustResize|stateHidden"
android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.FollowActivity"
android:windowSoftInputMode="adjustResize|stateHidden"
android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.activity.FeedActivity"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" >
</activity>
<activity
android:name=".profile.activity.NotificationActivity"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" >
</activity>
<activity <activity
android:name=".others.imagesearch.ImageSearchActivity" android:name=".others.imagesearch.ImageSearchActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
@ -117,6 +136,8 @@
android:name=".media.CalendarActivity" android:name=".media.CalendarActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity android:name=".media.user.ListActivity" /> <activity android:name=".media.user.ListActivity" />
<activity android:name=".profile.SingleStatActivity"
android:parentActivityName=".profile.ProfileActivity"/>
<activity <activity
android:name=".media.manga.mangareader.MangaReaderActivity" android:name=".media.manga.mangareader.MangaReaderActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
@ -127,7 +148,8 @@
<activity <activity
android:name=".media.MediaDetailsActivity" android:name=".media.MediaDetailsActivity"
android:parentActivityName=".MainActivity" android:parentActivityName=".MainActivity"
android:theme="@style/Theme.Dantotsu.NeverCutout" /> android:theme="@style/Theme.Dantotsu.NeverCutout"
android:windowSoftInputMode="adjustResize|stateHidden"/>
<activity android:name=".media.CharacterDetailsActivity" /> <activity android:name=".media.CharacterDetailsActivity" />
<activity android:name=".home.NoInternet" /> <activity android:name=".home.NoInternet" />
<activity <activity
@ -209,6 +231,7 @@
<data android:host="discord.dantotsu.com" /> <data android:host="discord.dantotsu.com" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".connections.anilist.UrlMedia" android:name=".connections.anilist.UrlMedia"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
@ -239,6 +262,17 @@
<data android:host="myanimelist.net" /> <data android:host="myanimelist.net" />
<data android:pathPrefix="/anime" /> <data android:pathPrefix="/anime" />
</intent-filter> </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>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@ -254,6 +288,15 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </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>
<activity <activity
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity" android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
@ -264,14 +307,22 @@
android:exported="false" android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<receiver <receiver android:name=".notifications.AlarmPermissionStateReceiver"
android:name=".subcriptions.AlarmReceiver" 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:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="Aani.dantotsu.ACTION_ALARM" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver"/>
<receiver android:name=".notifications.comment.CommentNotificationReceiver"/>
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver"/>
<meta-data <meta-data
android:name="preloaded_fonts" android:name="preloaded_fonts"

View file

@ -8,7 +8,9 @@ import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import ani.dantotsu.aniyomi.anime.custom.AppModule import ani.dantotsu.aniyomi.anime.custom.AppModule
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.notifications.TaskScheduler
import ani.dantotsu.others.DisabledReports import ani.dantotsu.others.DisabledReports
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
@ -17,6 +19,8 @@ import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.settings.SettingsActivity import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.FinalExceptionHandler
import ani.dantotsu.util.Logger
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
@ -28,7 +32,6 @@ import kotlinx.coroutines.launch
import logcat.AndroidLogcatLogger import logcat.AndroidLogcatLogger
import logcat.LogPriority import logcat.LogPriority
import logcat.LogcatLogger import logcat.LogcatLogger
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -79,7 +82,9 @@ class App : MultiDexApplication() {
} }
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo()) crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
Logger.init(this)
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
Logger.log("App: Logging started")
initializeNetwork(baseContext) initializeNetwork(baseContext)
@ -95,29 +100,36 @@ class App : MultiDexApplication() {
val animeScope = CoroutineScope(Dispatchers.Default) val animeScope = CoroutineScope(Dispatchers.Default)
animeScope.launch { animeScope.launch {
animeExtensionManager.findAvailableExtensions() animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}") Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow) AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
} }
val mangaScope = CoroutineScope(Dispatchers.Default) val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch { mangaScope.launch {
mangaExtensionManager.findAvailableExtensions() mangaExtensionManager.findAvailableExtensions()
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}") Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow) MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
} }
val novelScope = CoroutineScope(Dispatchers.Default) val novelScope = CoroutineScope(Dispatchers.Default)
novelScope.launch { novelScope.launch {
novelExtensionManager.findAvailableExtensions() novelExtensionManager.findAvailableExtensions()
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}") Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow) NovelSources.init(novelExtensionManager.installedExtensionsFlow)
} }
} val commentsScope = CoroutineScope(Dispatchers.Default)
commentsScope.launch {
CommentsAPI.fetchAuthToken()
}
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
TaskScheduler.create(this, useAlarmManager).scheduleAllTasks(this)
}
private fun setupNotificationChannels() { private fun setupNotificationChannels() {
try { try {
Notifications.createChannels(this) Notifications.createChannels(this)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" } Logger.log("Failed to modify notification channels")
Logger.log(e)
} }
} }

View file

@ -1,11 +1,13 @@
package ani.dantotsu package ani.dantotsu
import android.Manifest
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.DatePickerDialog import android.app.DatePickerDialog
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
@ -16,32 +18,71 @@ import android.content.res.Configuration
import android.content.res.Resources.getSystem import android.content.res.Resources.getSystem
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.Manifest import android.graphics.drawable.Drawable
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities.* 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.Uri import android.net.Uri
import android.os.* 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.provider.Settings import android.provider.Settings
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.text.InputFilter import android.text.InputFilter
import android.text.Spanned import android.text.Spanned
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.* 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.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.animation.* import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.* 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 androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.math.MathUtils.clamp import androidx.core.math.MathUtils.clamp
import androidx.core.view.* 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.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -52,15 +93,26 @@ import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
import ani.dantotsu.others.SpoilerPlugin
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.internal.PreferenceKeystore import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.subcriptions.NotificationClickReceiver import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide 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.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade 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.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -68,15 +120,37 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.internal.ViewUtils import com.google.android.material.internal.ViewUtils
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.* 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.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.* import java.io.File
import java.lang.Runnable import java.io.FileOutputStream
import java.io.OutputStream
import java.lang.reflect.Field import java.lang.reflect.Field
import java.util.* import java.util.Calendar
import kotlin.math.* 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
var statusBarHeight = 0 var statusBarHeight = 0
@ -108,12 +182,6 @@ fun currActivity(): Activity? {
var loadMedia: Int? = null var loadMedia: Int? = null
var loadIsMAL = false var loadIsMAL = false
fun logger(e: Any?, print: Boolean = true) {
if (print)
println(e)
}
fun initActivity(a: Activity) { fun initActivity(a: Activity) {
val window = a.window val window = a.window
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
@ -132,10 +200,13 @@ fun initActivity(a: Activity) {
if (navBarHeight == 0) { if (navBarHeight == 0) {
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content)) ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
?.apply { ?.apply {
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom navBarHeight = this.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
} }
} }
a.hideStatusBar() WindowInsetsControllerCompat(
window,
window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
window.decorView.rootWindowInsets?.displayCutout?.apply { window.decorView.rootWindowInsets?.displayCutout?.apply {
if (boundingRects.size > 0) { if (boundingRects.size > 0) {
@ -148,47 +219,73 @@ fun initActivity(a: Activity) {
val windowInsets = val windowInsets =
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content)) ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
if (windowInsets != null) { if (windowInsets != null) {
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
statusBarHeight = insets.top navBarHeight =
navBarHeight = insets.bottom windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
} }
} }
if (a !is MainActivity) a.setNavigationTheme()
} }
@Suppress("DEPRECATION")
fun Activity.hideSystemBars() { fun Activity.hideSystemBars() {
window.decorView.systemUiVisibility = ( WindowInsetsControllerCompat(window, window.decorView).let { controller ->
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY controller.systemBarsBehavior =
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
or View.SYSTEM_UI_FLAG_FULLSCREEN controller.hide(WindowInsetsCompat.Type.systemBars())
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION }
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
} }
@Suppress("DEPRECATION") fun Activity.hideSystemBarsExtendView() {
fun Activity.hideStatusBar() { WindowCompat.setDecorFitsSystemWindows(window, false)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 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
}
} }
open class BottomSheetDialogFragment : BottomSheetDialogFragment() { open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val window = dialog?.window dialog?.window?.let { window ->
val decorView: View = window?.decorView ?: return WindowCompat.setDecorFitsSystemWindows(window, false)
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) { if (immersiveMode) {
val behavior = BottomSheetBehavior.from(requireView().parent as View) WindowInsetsControllerCompat(
behavior.state = BottomSheetBehavior.STATE_EXPANDED 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 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?) { override fun show(manager: FragmentManager, tag: String?) {
@ -202,21 +299,35 @@ fun isOnline(context: Context): Boolean {
val connectivityManager = val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return tryWith { return tryWith {
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return@tryWith if (cap != null) { val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
when { return@tryWith if (cap != null) {
cap.hasTransport(TRANSPORT_BLUETOOTH) || when {
cap.hasTransport(TRANSPORT_CELLULAR) || cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_ETHERNET) || cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_LOWPAN) || cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_USB) || cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_VPN) || cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_WIFI) || cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
else -> false else -> false
} }
} 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
}
} ?: false } ?: false
} }
@ -278,7 +389,7 @@ class InputFilterMinMax(
val input = (dest.toString() + source.toString()).toDouble() val input = (dest.toString() + source.toString()).toDouble()
if (isInRange(min, max, input)) return null if (isInRange(min, max, input)) return null
} catch (nfe: NumberFormatException) { } catch (nfe: NumberFormatException) {
logger(nfe.stackTraceToString()) Logger.log(nfe)
} }
return "" return ""
} }
@ -449,6 +560,7 @@ fun ImageView.loadImage(url: String?, size: Int = 0) {
} }
fun ImageView.loadImage(file: FileUrl?, 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) { if (file?.url?.isNotEmpty() == true) {
tryWith { tryWith {
val glideUrl = GlideUrl(file.url) { file.headers } val glideUrl = GlideUrl(file.url) { file.headers }
@ -579,9 +691,24 @@ fun View.circularReveal(ex: Int, ey: Int, subX: Boolean, time: Long) {
} }
fun openLinkInBrowser(link: String?) { fun openLinkInBrowser(link: String?) {
tryWith { link?.let {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) try {
currContext()?.startActivity(intent) 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)
}
} }
} }
@ -677,6 +804,7 @@ fun savePrefs(
} }
fun downloadsPermission(activity: AppCompatActivity): Boolean { fun downloadsPermission(activity: AppCompatActivity): Boolean {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
val permissions = arrayOf( val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE Manifest.permission.READ_EXTERNAL_STORAGE
@ -687,7 +815,11 @@ fun downloadsPermission(activity: AppCompatActivity): Boolean {
}.toTypedArray() }.toTypedArray()
return if (requiredPermissions.isNotEmpty()) { return if (requiredPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(activity, requiredPermissions, DOWNLOADS_PERMISSION_REQUEST_CODE) ActivityCompat.requestPermissions(
activity,
requiredPermissions,
DOWNLOADS_PERMISSION_REQUEST_CODE
)
false false
} else { } else {
true true
@ -728,7 +860,7 @@ fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
private fun scanFile(path: String, context: Context) { private fun scanFile(path: String, context: Context) {
MediaScannerConnection.scanFile(context, arrayOf(path), null) { p, _ -> MediaScannerConnection.scanFile(context, arrayOf(path), null) { p, _ ->
logger("Finished scanning $p") Logger.log("Finished scanning $p")
} }
} }
@ -760,7 +892,9 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
val clipboard = getSystemService(activity, ClipboardManager::class.java) val clipboard = getSystemService(activity, ClipboardManager::class.java)
val clip = ClipData.newPlainText("label", string) val clip = ClipData.newPlainText("label", string)
clipboard?.setPrimaryClip(clip) clipboard?.setPrimaryClip(clip)
if (toast) snackString(activity.getString(R.string.copied_text, string)) if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
if (toast) snackString(activity.getString(R.string.copied_text, string))
}
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@ -868,7 +1002,7 @@ class EmptyAdapter(private val count: Int) : RecyclerView.Adapter<RecyclerView.V
fun toast(string: String?) { fun toast(string: String?) {
if (string != null) { if (string != null) {
logger(string) Logger.log(string)
MainScope().launch { MainScope().launch {
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT) Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
.show() .show()
@ -876,16 +1010,16 @@ fun toast(string: String?) {
} }
} }
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) { fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null): Snackbar? {
try { //I have no idea why this sometimes crashes for some people... try { //I have no idea why this sometimes crashes for some people...
if (s != null) { if (s != null) {
(activity ?: currActivity())?.apply { (activity ?: currActivity())?.apply {
val snackBar = Snackbar.make(
window.decorView.findViewById(android.R.id.content),
s,
Snackbar.LENGTH_SHORT
)
runOnUiThread { runOnUiThread {
val snackBar = Snackbar.make(
window.decorView.findViewById(android.R.id.content),
s,
Snackbar.LENGTH_SHORT
)
snackBar.view.apply { snackBar.view.apply {
updateLayoutParams<FrameLayout.LayoutParams> { updateLayoutParams<FrameLayout.LayoutParams> {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
@ -905,13 +1039,15 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
} }
snackBar.show() snackBar.show()
} }
return snackBar
} }
logger(s) Logger.log(s)
} }
} catch (e: Exception) { } catch (e: Exception) {
logger(e.stackTraceToString()) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
} }
return null
} }
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) : open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
@ -1037,7 +1173,7 @@ fun incognitoNotification(context: Context) {
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito) val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if (incognito) { if (incognito) {
val intent = Intent(context, NotificationClickReceiver::class.java) val intent = Intent(context, IncognitoNotificationClickReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast( val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent, context, 0, intent,
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
@ -1055,6 +1191,28 @@ 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() { suspend fun View.pop() {
currActivity()?.runOnUiThread { currActivity()?.runOnUiThread {
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start() ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
@ -1067,3 +1225,100 @@ suspend fun View.pop() {
} }
delay(100) 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(GlideUrl(url))
.diskCacheStrategy(DiskCacheStrategy.ALL).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,7 +2,10 @@ package ani.dantotsu
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.net.Uri import android.net.Uri
@ -11,7 +14,8 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator import android.view.animation.AnticipateInterpolator
@ -25,6 +29,8 @@ import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach import androidx.core.view.doOnAttach
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -32,6 +38,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download import androidx.media3.exoplayer.offline.Download
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.work.OneTimeWorkRequest
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding import ani.dantotsu.databinding.ActivityMainBinding
@ -43,18 +50,27 @@ import ani.dantotsu.home.LoginFragment
import ani.dantotsu.home.MangaFragment import ani.dantotsu.home.MangaFragment
import ani.dantotsu.home.NoInternet import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.others.CustomBottomDialog 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.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool import ani.dantotsu.settings.saving.PrefManager.asLiveBool
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.themes.ThemeManager 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 eu.kanade.domain.source.service.SourcePreferences
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -84,6 +100,67 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) 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 _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar) val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@ -95,7 +172,6 @@ class MainActivity : AppCompatActivity() {
} }
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray) _bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
val offset = try { val offset = try {
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android") val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
resources.getDimensionPixelSize(statusBarHeightId) resources.getDimensionPixelSize(statusBarHeightId)
@ -144,11 +220,14 @@ class MainActivity : AppCompatActivity() {
finish() finish()
} }
doubleBackToExitPressedOnce = true doubleBackToExitPressedOnce = true
snackString(this@MainActivity.getString(R.string.back_to_exit)) snackString(this@MainActivity.getString(R.string.back_to_exit)).apply {
Handler(Looper.getMainLooper()).postDelayed( this?.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
{ doubleBackToExitPressedOnce = false }, override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
2000 super.onDismissed(transientBottomBar, event)
) doubleBackToExitPressedOnce = false
}
})
}
} }
val preferences: SourcePreferences = Injekt.get() val preferences: SourcePreferences = Injekt.get()
@ -205,6 +284,7 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach { binding.root.doOnAttach {
initActivity(this) initActivity(this)
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
selectedOption = if (fragment != null) { selectedOption = if (fragment != null) {
when (fragment) { when (fragment) {
AnimeFragment::class.java.name -> 0 AnimeFragment::class.java.name -> 0
@ -217,7 +297,36 @@ class MainActivity : AppCompatActivity() {
} }
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
}
}
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)
}
startActivity(detailIntent)
} else if (fragmentToLoad == "FEED" && activityId != -1) {
val feedIntent = Intent(this, FeedActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId)
}
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)
}
startActivity(notificationIntent)
} }
} }
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode) val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
@ -255,12 +364,14 @@ class MainActivity : AppCompatActivity() {
mainViewPager.setCurrentItem(newIndex, false) mainViewPager.setCurrentItem(newIndex, false)
} }
}) })
navbar.selectTabAt(selectedOption) if (mainViewPager.getCurrentItem() != selectedOption) {
mainViewPager.post { navbar.selectTabAt(selectedOption)
mainViewPager.setCurrentItem( mainViewPager.post {
selectedOption, mainViewPager.setCurrentItem(
false selectedOption,
) false
)
}
} }
} else { } else {
binding.mainProgressBar.visibility = View.GONE binding.mainProgressBar.visibility = View.GONE
@ -288,8 +399,21 @@ class MainActivity : AppCompatActivity() {
snackString(this@MainActivity.getString(R.string.anilist_not_found)) snackString(this@MainActivity.getString(R.string.anilist_not_found))
} }
} }
delay(500) val username = intent.extras?.getString("username")
startSubscription() if (username != null) {
val nameInt = username.toIntOrNull()
if (nameInt != null) {
startActivity(
Intent(this@MainActivity, ProfileActivity::class.java)
.putExtra("userId", nameInt)
)
} else {
startActivity(
Intent(this@MainActivity, ProfileActivity::class.java)
.putExtra("username", username)
)
}
}
} }
load = true load = true
} }
@ -326,27 +450,74 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
//TODO: Remove this lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
GlobalScope.launch(Dispatchers.IO) {
val index = Helper.downloadManager(this@MainActivity).downloadIndex val index = Helper.downloadManager(this@MainActivity).downloadIndex
val downloadCursor = index.getDownloads() val downloadCursor = index.getDownloads()
while (downloadCursor.moveToNext()) { while (downloadCursor.moveToNext()) {
val download = downloadCursor.download val download = downloadCursor.download
Log.e("Downloader", download.request.uri.toString()) if (download.state == Download.STATE_FAILED) {
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())
if (download.state == Download.STATE_FAILED) { //simple cleanup
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id) Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
} }
} }
} }
} }
override fun onRestart() {
super.onRestart()
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
}
private val Int.toPx get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics
).toInt()
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val params : ViewGroup.MarginLayoutParams =
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
params.updateMargins(bottom = 8.toPx)
else
params.updateMargins(bottom = 32.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 = "Enter your password to decrypt the 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 //ViewPager
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :

View file

@ -5,6 +5,7 @@ import android.os.Build
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import ani.dantotsu.others.webview.CloudFlare import ani.dantotsu.others.webview.CloudFlare
import ani.dantotsu.others.webview.WebViewBottomDialog import ani.dantotsu.others.webview.WebViewBottomDialog
import ani.dantotsu.util.Logger
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser import com.lagradost.nicehttp.ResponseParser
import com.lagradost.nicehttp.addGenericDns import com.lagradost.nicehttp.addGenericDns
@ -104,6 +105,7 @@ fun logError(e: Throwable, post: Boolean = true, snackbar: Boolean = true) {
toast(e.localizedMessage) toast(e.localizedMessage)
} }
e.printStackTrace() e.printStackTrace()
Logger.log(e)
} }
fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T): T? { fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T): T? {
@ -134,7 +136,7 @@ suspend fun <T> tryWithSuspend(
* A url, which can also have headers * A url, which can also have headers
* **/ * **/
data class FileUrl( data class FileUrl(
val url: String, var url: String,
val headers: Map<String, String> = mapOf() val headers: Map<String, String> = mapOf()
) : Serializable { ) : Serializable {
companion object { companion object {

View file

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

View file

@ -11,8 +11,10 @@ import ani.dantotsu.currContext
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import java.util.Calendar import java.util.Calendar
object Anilist { object Anilist {
@ -27,6 +29,7 @@ object Anilist {
var bg: String? = null var bg: String? = null
var episodesWatched: Int? = null var episodesWatched: Int? = null
var chapterRead: Int? = null var chapterRead: Int? = null
var unreadNotificationCount: Int = 0
var genres: ArrayList<String>? = null var genres: ArrayList<String>? = null
var tags: Map<Boolean, List<String>>? = null var tags: Map<Boolean, List<String>>? = null
@ -124,7 +127,8 @@ object Anilist {
show: Boolean = false, show: Boolean = false,
cache: Int? = null cache: Int? = null
): T? { ): T? {
return tryWithSuspend { return try {
if (show) Logger.log("Anilist Query: $query")
if (rateLimitReset > System.currentTimeMillis() / 1000) { if (rateLimitReset > System.currentTimeMillis() / 1000) {
toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds") toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds") throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
@ -148,7 +152,7 @@ object Anilist {
cacheTime = cache ?: 10 cacheTime = cache ?: 10
) )
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1 val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
Log.d("AnilistQuery", "Remaining requests: $remaining") Logger.log("Remaining requests: $remaining")
if (json.code == 429) { if (json.code == 429) {
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1 val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0 val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
@ -159,10 +163,14 @@ object Anilist {
toast("Rate limited. Try after $retry seconds") toast("Rate limited. Try after $retry seconds")
throw Exception("Rate limited after $retry seconds") throw Exception("Rate limited after $retry seconds")
} }
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down)) if (!json.text.startsWith("{")) {throw Exception(currContext()?.getString(R.string.anilist_down))}
if (show) println("Response : ${json.text}") if (show) Logger.log("Anilist Response: ${json.text}")
json.parsed() json.parsed()
} else null } 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,6 +13,23 @@ class AnilistMutations {
executeQuery<JsonObject>(query, variables) 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( suspend fun editList(
mediaID: Int, mediaID: Int,
progress: Int? = null, progress: Int? = null,

View file

@ -6,9 +6,12 @@ import ani.dantotsu.checkGenreTime
import ani.dantotsu.checkId import ani.dantotsu.checkId
import ani.dantotsu.connections.anilist.Anilist.authorRoles import ani.dantotsu.connections.anilist.Anilist.authorRoles
import ani.dantotsu.connections.anilist.Anilist.executeQuery import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FeedResponse
import ani.dantotsu.connections.anilist.api.FuzzyDate 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.Page
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.anilist.api.ToggleLike
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.isOnline import ani.dantotsu.isOnline
import ani.dantotsu.logError import ani.dantotsu.logError
@ -20,6 +23,7 @@ import ani.dantotsu.others.MalScraper
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -31,23 +35,27 @@ import java.io.Serializable
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class AnilistQueries { class AnilistQueries {
suspend fun getUserData(): Boolean { suspend fun getUserData(): Boolean {
val response: Query.Viewer? val response: Query.Viewer?
measureTimeMillis { measureTimeMillis {
response = response =
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}}}""") executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}unreadNotificationCount}}""")
}.also { println("time : $it") } }.also { println("time : $it") }
val user = response?.data?.user ?: return false val user = response?.data?.user ?: return false
PrefManager.setVal(PrefName.AnilistUserName, user.name) PrefManager.setVal(PrefName.AnilistUserName, user.name)
Anilist.userid = user.id Anilist.userid = user.id
PrefManager.setVal(PrefName.AnilistUserId, user.id.toString())
Anilist.username = user.name Anilist.username = user.name
Anilist.bg = user.bannerImage Anilist.bg = user.bannerImage
Anilist.avatar = user.avatar?.medium Anilist.avatar = user.avatar?.medium
Anilist.episodesWatched = user.statistics?.anime?.episodesWatched Anilist.episodesWatched = user.statistics?.anime?.episodesWatched
Anilist.chapterRead = user.statistics?.manga?.chaptersRead Anilist.chapterRead = user.statistics?.manga?.chaptersRead
Anilist.adult = user.options?.displayAdultContent ?: false Anilist.adult = user.options?.displayAdultContent ?: false
Anilist.unreadNotificationCount = user.unreadNotificationCount ?: 0
val unread = PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications)
Anilist.unreadNotificationCount += unread
return true return true
} }
@ -64,7 +72,7 @@ class AnilistQueries {
media.cameFromContinue = false media.cameFromContinue = false
val query = val query =
"""{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}}}""" """{Media(id:${media.id}){id mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
runBlocking { runBlocking {
val anilist = async { val anilist = async {
var response = executeQuery<Query.Media>(query, force = true, show = true) var response = executeQuery<Query.Media>(query, force = true, show = true)
@ -121,6 +129,30 @@ class AnilistQueries {
name = i.node?.name?.userPreferred, name = i.node?.name?.userPreferred,
image = i.node?.image?.medium, image = i.node?.image?.medium,
banner = media.banner ?: media.cover, banner = media.banner ?: media.cover,
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()
}
)
)
}
}
}
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?.medium,
role = when (i.role.toString()) { role = when (i.role.toString()) {
"MAIN" -> currContext()?.getString(R.string.main_role) "MAIN" -> currContext()?.getString(R.string.main_role)
?: "MAIN" ?: "MAIN"
@ -211,8 +243,10 @@ class AnilistQueries {
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let { fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
media.anime.author = Author( media.anime.author = Author(
it.id.toString(), it.id,
it.name?.userPreferred ?: "N/A" it.name?.userPreferred ?: "N/A",
it.image?.medium,
"AUTHOR"
) )
} }
@ -231,8 +265,10 @@ class AnilistQueries {
} else if (media.manga != null) { } else if (media.manga != null) {
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let { fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
media.manga.author = Author( media.manga.author = Author(
it.id.toString(), it.id,
it.name?.userPreferred ?: "N/A" it.name?.userPreferred ?: "N/A",
it.image?.medium,
"AUTHOR"
) )
} }
} }
@ -249,6 +285,7 @@ class AnilistQueries {
} else { } else {
if (currContext()?.let { isOnline(it) } == true) { if (currContext()?.let { isOnline(it) } == true) {
snackString(currContext()?.getString(R.string.error_getting_data)) snackString(currContext()?.getString(R.string.error_getting_data))
} else {
} }
} }
} }
@ -262,6 +299,52 @@ class AnilistQueries {
return media 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> { suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
val returnArray = arrayListOf<Media>() val returnArray = arrayListOf<Media>()
val map = mutableMapOf<Int, Media>() val map = mutableMapOf<Int, Media>()
@ -299,9 +382,17 @@ class AnilistQueries {
} }
} }
} }
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet() if (type != "ANIME") {
if (set.isNotEmpty()) { returnArray.addAll(map.values)
set.reversed().forEach { return returnArray
}
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
if (map.containsKey(it)) returnArray.add(map[it]!!) if (map.containsKey(it)) returnArray.add(map[it]!!)
} }
for (i in map) { for (i in map) {
@ -315,12 +406,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 } } } } } """ 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): ArrayList<Media> { suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
var hasNextPage = true var hasNextPage = true
var page = 0 var page = 0
suspend fun getNextPage(page: Int): List<Media> { suspend fun getNextPage(page: Int): List<Media> {
val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page)}}""") val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page, id)}}""")
val favourites = response?.data?.user?.favourites val favourites = response?.data?.user?.favourites
val apiMediaList = if (anime) favourites?.anime else favourites?.manga val apiMediaList = if (anime) favourites?.anime else favourites?.manga
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
@ -339,8 +430,8 @@ class AnilistQueries {
return responseArray return responseArray
} }
private fun favMediaQuery(anime: Boolean, page: Int): String { private fun favMediaQuery(anime: Boolean, page: Int, id: Int? = Anilist.userid): 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}}}}}}""" 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 recommendations(): ArrayList<Media> { suspend fun recommendations(): ArrayList<Media> {
@ -383,7 +474,7 @@ class AnilistQueries {
} }
private fun recommendationPlannedQuery(type: String): String { private fun recommendationPlannedQuery(type: String): String {
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 } } } } }""" 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 } } } } }"""
} }
suspend fun initHomePage(): Map<String, ArrayList<Media>> { suspend fun initHomePage(): Map<String, ArrayList<Media>> {
@ -423,7 +514,8 @@ class AnilistQueries {
}, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}""" }, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
query += """}""".trimEnd(',') query += """}""".trimEnd(',')
val response = executeQuery<Query.HomePageMedia>(query) val response = executeQuery<Query.HomePageMedia>(query, show = true)
Logger.log(response.toString())
val returnMap = mutableMapOf<String, ArrayList<Media>>() val returnMap = mutableMapOf<String, ArrayList<Media>>()
fun current(type: String) { fun current(type: String) {
val subMap = mutableMapOf<Int, Media>() val subMap = mutableMapOf<Int, Media>()
@ -446,10 +538,18 @@ class AnilistQueries {
subMap[m.id] = m subMap[m.id] = m
} }
} }
val set = PrefManager.getCustomVal<Set<Int>>("continue_${type.uppercase()}", setOf()) if (type != "Anime") {
.toMutableSet() returnArray.addAll(subMap.values)
if (set.isNotEmpty()) { returnMap["current$type"] = returnArray
set.reversed().forEach { return
}
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!) if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
} }
for (i in subMap) { for (i in subMap) {
@ -472,9 +572,13 @@ class AnilistQueries {
subMap[m.id] = m subMap[m.id] = m
} }
} }
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet() val list = PrefManager.getNullableCustomVal(
if (set.isNotEmpty()) { "continueAnimeList",
set.reversed().forEach { listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!) if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
} }
for (i in subMap) { for (i in subMap) {
@ -558,12 +662,11 @@ class AnilistQueries {
private suspend fun bannerImage(type: String): String? { private suspend fun bannerImage(type: String): String? {
//var image = loadData<BannerImage>("banner_$type") val image = BannerImage(
val image: BannerImage? = BannerImage( PrefManager.getCustomVal("banner_${type}_url", ""),
PrefManager.getCustomVal("banner_${type}_url", null),
PrefManager.getCustomVal("banner_${type}_time", 0L) PrefManager.getCustomVal("banner_${type}_time", 0L)
) )
if (image == null || image.checkTime()) { if (image.url.isNullOrEmpty() || image.checkTime()) {
val response = 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 } } } } } """) 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 { val random = response?.data?.mediaListCollection?.lists?.mapNotNull {
@ -592,7 +695,7 @@ class AnilistQueries {
sortOrder: String? = null sortOrder: String? = null
): MutableMap<String, ArrayList<Media>> { ): MutableMap<String, ArrayList<Media>> {
val response = val response =
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""") executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage genres meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
val sorted = mutableMapOf<String, ArrayList<Media>>() val sorted = mutableMapOf<String, ArrayList<Media>>()
val unsorted = mutableMapOf<String, ArrayList<Media>>() val unsorted = mutableMapOf<String, ArrayList<Media>>()
val all = arrayListOf<Media>() val all = arrayListOf<Media>()
@ -620,7 +723,7 @@ class AnilistQueries {
if (!sorted.containsKey(it.key)) sorted[it.key] = it.value if (!sorted.containsKey(it.key)) sorted[it.key] = it.value
} }
sorted["Favourites"] = favMedia(anime) sorted["Favourites"] = favMedia(anime, userId)
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder }) sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
//favMedia doesn't fill userProgress, so we need to fill it manually by searching :( //favMedia doesn't fill userProgress, so we need to fill it manually by searching :(
sorted["Favourites"]?.forEach { fav -> sorted["Favourites"]?.forEach { fav ->
@ -661,8 +764,8 @@ class AnilistQueries {
PrefManager.getVal<Set<String>>(PrefName.TagsListNonAdult).toMutableList() PrefManager.getVal<Set<String>>(PrefName.TagsListNonAdult).toMutableList()
var tags = if (adultTags.isEmpty() || nonAdultTags.isEmpty()) null else var tags = if (adultTags.isEmpty() || nonAdultTags.isEmpty()) null else
mapOf( mapOf(
true to adultTags, true to adultTags.sortedBy { it },
false to nonAdultTags false to nonAdultTags.sortedBy { it }
) )
if (genres.isNullOrEmpty()) { if (genres.isNullOrEmpty()) {
@ -698,7 +801,7 @@ class AnilistQueries {
} }
} }
return if (!genres.isNullOrEmpty() && tags != null) { return if (!genres.isNullOrEmpty() && tags != null) {
Anilist.genres = genres Anilist.genres = genres?.sortedBy { it }?.toMutableList() as ArrayList<String>
Anilist.tags = tags Anilist.tags = tags
true true
} else false } else false
@ -1225,4 +1328,116 @@ Page(page:$page,perPage:50) {
return author 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)}
animeMediaList:${bannerImageQuery("ANIME", id)}
mangaMediaList:${bannerImageQuery("MANGA", id)}
}""".trimIndent(), force = true
)
}
private fun bannerImageQuery(type: String, id: Int?): String {
return """MediaListCollection(userId: ${id}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } }"""
}
suspend fun getNotifications(id: Int, page: Int = 1, resetNotification: Boolean = true): NotificationResponse? {
val reset = if (resetNotification) "true" else "false"
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 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,6 +15,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -99,6 +100,7 @@ class AnilistHomeViewModel : ViewModel() {
suspend fun initHomePage() { suspend fun initHomePage() {
val res = Anilist.query.initHomePage() val res = Anilist.query.initHomePage()
Logger.log("AnilistHomeViewModel : res=$res")
res["currentAnime"]?.let { animeContinue.postValue(it) } res["currentAnime"]?.let { animeContinue.postValue(it) }
res["favoriteAnime"]?.let { animeFav.postValue(it) } res["favoriteAnime"]?.let { animeFav.postValue(it) }
res["plannedAnime"]?.let { animePlanned.postValue(it) } res["plannedAnime"]?.let { animePlanned.postValue(it) }
@ -331,4 +333,64 @@ 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
private val listImages: MutableLiveData<ArrayList<String?>> =
MutableLiveData<ArrayList<String?>>(arrayListOf())
fun getListImages(): LiveData<ArrayList<String?>> = listImages
suspend fun setData(id: Int) {
val res = Anilist.query.initProfilePage(id)
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
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()))
val bannerImages = arrayListOf<String?>(null, null)
val animeRandom = res?.data?.animeMediaList?.lists?.mapNotNull {
it.entries?.mapNotNull { entry ->
val imageUrl = entry.media?.bannerImage
if (imageUrl != null && imageUrl != "null") imageUrl
else null
}
}?.flatten()?.randomOrNull()
bannerImages[0] = animeRandom
val mangaRandom = res?.data?.mangaMediaList?.lists?.mapNotNull {
it.entries?.mapNotNull { entry ->
val imageUrl = entry.media?.bannerImage
if (imageUrl != null && imageUrl != "null") imageUrl
else null
}
}?.flatten()?.randomOrNull()
bannerImages[1] = mangaRandom
listImages.postValue(bannerImages)
}
fun refresh() {
mangaFav.postValue(mangaFav.value)
animeFav.postValue(animeFav.value)
listImages.postValue(listImages.value)
}
} }

View file

@ -4,7 +4,7 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.logError import ani.dantotsu.logError
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
@ -16,7 +16,6 @@ class Login : AppCompatActivity() {
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
val data: Uri? = intent?.data val data: Uri? = intent?.data
logger(data.toString())
try { try {
Anilist.token = Anilist.token =
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value

View file

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

View file

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

View file

@ -139,41 +139,550 @@ 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?,
@SerialName("animeMediaList") val animeMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("mangaMediaList") val mangaMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?
)
}
@Serializable
data class ToggleFollow(
@SerialName("data")
val data: Data?
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("ToggleFollow")
val toggleFollow: FollowData
) : java.io.Serializable
}
@Serializable @Serializable
data class GenreCollection( data class GenreCollection(
@SerialName("data") @SerialName("data")
val data: Data val data: Data
) { ) : java.io.Serializable {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("GenreCollection") @SerialName("GenreCollection")
val genreCollection: List<String>? val genreCollection: List<String>?
) ) : java.io.Serializable
} }
@Serializable @Serializable
data class MediaTagCollection( data class MediaTagCollection(
@SerialName("data") @SerialName("data")
val data: Data val data: Data
) { ) : java.io.Serializable {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("MediaTagCollection") @SerialName("MediaTagCollection")
val mediaTagCollection: List<MediaTag>? val mediaTagCollection: List<MediaTag>?
) ) : java.io.Serializable
} }
@Serializable @Serializable
data class User( data class User(
@SerialName("data") @SerialName("data")
val data: Data val data: Data
) { ) : java.io.Serializable {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("User") @SerialName("User")
val user: ani.dantotsu.connections.anilist.api.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( //data class WhaData(
@ -203,7 +712,7 @@ class Query {
// // Activity reply query // // Activity reply query
// val ActivityReply: ActivityReply?, // val ActivityReply: ActivityReply?,
// // Comment query // // CommentNotificationWorker query
// val ThreadComment: List<ThreadComment>?, // val ThreadComment: List<ThreadComment>?,
// // Notification query // // Notification query

View file

@ -0,0 +1,114 @@
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 // Average #hex color of cover image
@SerialName("color") var color: String?, @SerialName("color") var color: String?,
) ) : java.io.Serializable
@Serializable @Serializable
data class MediaList( data class MediaList(
@ -490,7 +490,7 @@ data class MediaExternalLink(
// isDisabled: Boolean // isDisabled: Boolean
@SerialName("notes") var notes: String?, @SerialName("notes") var notes: String?,
) ) : java.io.Serializable
@Serializable @Serializable
enum class ExternalLinkType { enum class ExternalLinkType {
@ -512,7 +512,13 @@ data class MediaListCollection(
// If there is another chunk // If there is another chunk
@SerialName("hasNextChunk") var hasNextChunk: Boolean?, @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 @Serializable
data class MediaListGroup( data class MediaListGroup(
@ -526,4 +532,4 @@ data class MediaListGroup(
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?, @SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
@SerialName("status") var status: MediaListStatus?, @SerialName("status") var status: MediaListStatus?,
) ) : java.io.Serializable

View file

@ -0,0 +1,122 @@
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: ani.dantotsu.connections.anilist.api.Media? = null,
@SerialName("user")
val user: ani.dantotsu.connections.anilist.api.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?, @SerialName("languageV2") var languageV2: String?,
// The staff images // The staff images
// @SerialName("image") var image: StaffImage?, @SerialName("image") var image: StaffImage?,
// A general description of the staff member // A general description of the staff member
@SerialName("description") var description: String?, @SerialName("description") var description: String?,
@ -93,7 +93,14 @@ data class StaffConnection(
// The pagination information // The pagination information
// @SerialName("pageInfo") var pageInfo: PageInfo?, // @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 @Serializable
data class StaffEdge( data class StaffEdge(
var role: String?, var role: String?,

View file

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

View file

@ -0,0 +1,554 @@
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 {
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")
}
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,17 +1,18 @@
package ani.dantotsu.connections.crashlytics package ani.dantotsu.connections.crashlytics
import android.content.Context import android.content.Context
import ani.dantotsu.util.Logger
class CrashlyticsStub : CrashlyticsInterface { class CrashlyticsStub : CrashlyticsInterface {
override fun initialize(context: Context) { override fun initialize(context: Context) {
//no-op //no-op
} }
override fun logException(e: Throwable) { override fun logException(e: Throwable) {
//no-op Logger.log(e)
} }
override fun log(message: String) { override fun log(message: String) {
//no-op Logger.log(message)
} }
override fun setUserId(id: String) { override fun setUserId(id: String) {

View file

@ -70,19 +70,5 @@ object Discord {
const val application_Id = "1163925779692912771" const val application_Id = "1163925779692912771"
const val small_Image: String = const val small_Image: String =
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png" "mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.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

@ -15,7 +15,6 @@ import android.os.Environment
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@ -26,6 +25,7 @@ import ani.dantotsu.connections.discord.serializers.User
import ani.dantotsu.isOnline import ani.dantotsu.isOnline
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
@ -274,7 +274,7 @@ class DiscordService : Service() {
return return
} }
} }
t.message?.let { Log.d("WebSocket", "onFailure() $it") } t.message?.let { Logger.log("onFailure() $it") }
log("WebSocket: Error, onFailure() reason: ${t.message}") log("WebSocket: Error, onFailure() reason: ${t.message}")
client = OkHttpClient() client = OkHttpClient()
client.newWebSocket( client.newWebSocket(
@ -289,7 +289,7 @@ class DiscordService : Service() {
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason) super.onClosing(webSocket, code, reason)
Log.d("WebSocket", "onClosing() $code $reason") Logger.log("onClosing() $code $reason")
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) { if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
heartbeatThread.interrupt() heartbeatThread.interrupt()
} }
@ -297,7 +297,7 @@ class DiscordService : Service() {
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason) super.onClosed(webSocket, code, reason)
Log.d("WebSocket", "onClosed() $code $reason") Logger.log("onClosed() $code $reason")
if (code >= 4000) { if (code >= 4000) {
log("WebSocket: Error, code: $code reason: $reason") log("WebSocket: Error, code: $code reason: $reason")
client = OkHttpClient() client = OkHttpClient()
@ -382,52 +382,7 @@ class DiscordService : Service() {
} }
fun log(string: String) { fun log(string: String) {
Log.d("WebSocket_Discord", string) //Logger.log(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() { fun resume() {

View file

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

View file

@ -27,7 +27,7 @@ import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.video.ExoplayerDownloadService import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.download.video.Helper import ani.dantotsu.download.video.Helper
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.SubtitleDownloader import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.media.anime.AnimeWatchFragment import ani.dantotsu.media.anime.AnimeWatchFragment
@ -54,6 +54,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -249,7 +250,7 @@ class AnimeDownloaderService : Service() {
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
if (!downloadStarted) { if (!downloadStarted) {
logger("Download failed to start") Logger.log("Download failed to start")
builder.setContentText("${task.title} - ${task.episode} Download failed to start") builder.setContentText("${task.title} - ${task.episode} Download failed to start")
notificationManager.notify(NOTIFICATION_ID, builder.build()) notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed to start") snackString("${task.title} - ${task.episode} Download failed to start")
@ -263,11 +264,11 @@ class AnimeDownloaderService : Service() {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url) val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) { if (download != null) {
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) { if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
logger("Download failed") Logger.log("Download failed")
builder.setContentText("${task.title} - ${task.episode} Download failed") builder.setContentText("${task.title} - ${task.episode} Download failed")
notificationManager.notify(NOTIFICATION_ID, builder.build()) notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed") snackString("${task.title} - ${task.episode} Download failed")
logger("Download failed: ${download.failureReason}") Logger.log("Download failed: ${download.failureReason}")
downloadsManager.removeDownload( downloadsManager.removeDownload(
DownloadedType( DownloadedType(
task.title, task.title,
@ -289,7 +290,7 @@ class AnimeDownloaderService : Service() {
break break
} }
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) { if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
logger("Download completed") Logger.log("Download completed")
builder.setContentText("${task.title} - ${task.episode} Download completed") builder.setContentText("${task.title} - ${task.episode} Download completed")
notificationManager.notify(NOTIFICATION_ID, builder.build()) notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download completed") snackString("${task.title} - ${task.episode} Download completed")
@ -309,7 +310,7 @@ class AnimeDownloaderService : Service() {
break break
} }
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) { if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
logger("Download stopped") Logger.log("Download stopped")
builder.setContentText("${task.title} - ${task.episode} Download stopped") builder.setContentText("${task.title} - ${task.episode} Download stopped")
notificationManager.notify(NOTIFICATION_ID, builder.build()) notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download stopped") snackString("${task.title} - ${task.episode} Download stopped")
@ -328,7 +329,7 @@ class AnimeDownloaderService : Service() {
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut if (e.message?.contains("Coroutine was cancelled") == false) { //wut
logger("Exception while downloading file: ${e.message}") Logger.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}") snackString("Exception while downloading file: ${e.message}")
e.printStackTrace() e.printStackTrace()
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
@ -355,15 +356,13 @@ class AnimeDownloaderService : Service() {
return false return false
} }
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: AnimeDownloadTask) { private fun saveMediaInfo(task: AnimeDownloadTask) {
GlobalScope.launch(Dispatchers.IO) { launchIO {
val directory = File( val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.animeLocation}/${task.title}" "${DownloadsManager.animeLocation}/${task.title}"
) )
val episodeDirectory = File(directory, task.episode) val episodeDirectory = File(directory, task.episode)
if (!directory.exists()) directory.mkdirs()
if (!episodeDirectory.exists()) episodeDirectory.mkdirs() if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
val file = File(directory, "media.json") val file = File(directory, "media.json")

View file

@ -33,7 +33,7 @@ import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
@ -318,8 +318,8 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val mediaJson = media.readText() val mediaJson = media.readText()
gson.fromJson(mediaJson, Media::class.java) gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) { } catch (e: Exception) {
logger("Error loading media.json: ${e.message}") Logger.log("Error loading media.json: ${e.message}")
logger(e.printStackTrace()) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
null null
} }
@ -374,8 +374,8 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
bannerUri bannerUri
) )
} catch (e: Exception) { } catch (e: Exception) {
logger("Error loading media.json: ${e.message}") Logger.log("Error loading media.json: ${e.message}")
logger(e.printStackTrace()) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineAnimeModel( return OfflineAnimeModel(
"unknown", "unknown",

View file

@ -21,7 +21,7 @@ import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.manga.ImageData 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_FAILED
@ -37,6 +37,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -47,6 +48,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -251,7 +253,7 @@ class MangaDownloaderService : Service() {
snackString("${task.title} - ${task.chapter} Download finished") snackString("${task.title} - ${task.chapter} Download finished")
} }
} catch (e: Exception) { } catch (e: Exception) {
logger("Exception while downloading file: ${e.message}") Logger.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}") snackString("Exception while downloading file: ${e.message}")
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.chapter) broadcastDownloadFailed(task.chapter)
@ -287,8 +289,9 @@ class MangaDownloaderService : Service() {
} }
} }
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) { private fun saveMediaInfo(task: DownloadTask) {
GlobalScope.launch(Dispatchers.IO) { launchIO {
val directory = File( val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${task.title}" "Dantotsu/Manga/${task.title}"

View file

@ -30,7 +30,7 @@ import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
@ -308,8 +308,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val mediaJson = media.readText() val mediaJson = media.readText()
gson.fromJson(mediaJson, Media::class.java) gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) { } catch (e: Exception) {
logger("Error loading media.json: ${e.message}") Logger.log("Error loading media.json: ${e.message}")
logger(e.printStackTrace()) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
null null
} }
@ -358,8 +358,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
bannerUri bannerUri
) )
} catch (e: Exception) { } catch (e: Exception) {
logger("Error loading media.json: ${e.message}") Logger.log("Error loading media.json: ${e.message}")
logger(e.printStackTrace()) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel( return OfflineMangaModel(
"unknown", "unknown",

View file

@ -20,7 +20,7 @@ import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.novel.NovelReadFragment import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.snackString import ani.dantotsu.snackString
@ -31,6 +31,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -42,6 +43,7 @@ import kotlinx.coroutines.withContext
import okhttp3.Request import okhttp3.Request
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -186,15 +188,15 @@ class NovelDownloaderService : Service() {
val contentType = response.header("Content-Type") val contentType = response.header("Content-Type")
val contentDisposition = response.header("Content-Disposition") val contentDisposition = response.header("Content-Disposition")
logger("Content-Type: $contentType") Logger.log("Content-Type: $contentType")
logger("Content-Disposition: $contentDisposition") Logger.log("Content-Disposition: $contentDisposition")
// Return true if the Content-Type or Content-Disposition indicates an EPUB file // Return true if the Content-Type or Content-Disposition indicates an EPUB file
contentType == "application/epub+zip" || contentType == "application/epub+zip" ||
(contentDisposition?.contains(".epub") == true) (contentDisposition?.contains(".epub") == true)
} }
} catch (e: Exception) { } catch (e: Exception) {
logger("Error checking file type: ${e.message}") Logger.log("Error checking file type: ${e.message}")
false false
} }
} }
@ -225,12 +227,12 @@ class NovelDownloaderService : Service() {
if (!isEpubFile(task.downloadLink)) { if (!isEpubFile(task.downloadLink)) {
if (isAlreadyDownloaded(task.originalLink)) { if (isAlreadyDownloaded(task.originalLink)) {
logger("Already downloaded") Logger.log("Already downloaded")
broadcastDownloadFinished(task.originalLink) broadcastDownloadFinished(task.originalLink)
snackString("Already downloaded") snackString("Already downloaded")
return@withContext return@withContext
} }
logger("Download link is not an .epub file") Logger.log("Download link is not an .epub file")
broadcastDownloadFailed(task.originalLink) broadcastDownloadFailed(task.originalLink)
snackString("Download link is not an .epub file") snackString("Download link is not an .epub file")
return@withContext return@withContext
@ -301,7 +303,7 @@ class NovelDownloaderService : Service() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val progress = val progress =
(downloadedBytes * 100 / totalBytes).toInt() (downloadedBytes * 100 / totalBytes).toInt()
logger("Download progress: $progress") Logger.log("Download progress: $progress")
broadcastDownloadProgress(task.originalLink, progress) broadcastDownloadProgress(task.originalLink, progress)
} }
lastBroadcastUpdate = downloadedBytes lastBroadcastUpdate = downloadedBytes
@ -316,7 +318,7 @@ class NovelDownloaderService : Service() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
logger("Exception while downloading .epub inside request: ${e.message}") Logger.log("Exception while downloading .epub inside request: ${e.message}")
throw e throw e
} }
} }
@ -340,15 +342,16 @@ class NovelDownloaderService : Service() {
snackString("${task.title} - ${task.chapter} Download finished") snackString("${task.title} - ${task.chapter} Download finished")
} }
} catch (e: Exception) { } catch (e: Exception) {
logger("Exception while downloading .epub: ${e.message}") Logger.log("Exception while downloading .epub: ${e.message}")
snackString("Exception while downloading .epub: ${e.message}") snackString("Exception while downloading .epub: ${e.message}")
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.originalLink) broadcastDownloadFailed(task.originalLink)
} }
} }
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) { private fun saveMediaInfo(task: DownloadTask) {
GlobalScope.launch(Dispatchers.IO) { launchIO {
val directory = File( val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${task.title}" "Dantotsu/Novel/${task.title}"

View file

@ -43,6 +43,7 @@ import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.parsers.Video import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoType import ani.dantotsu.parsers.VideoType
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -157,15 +158,15 @@ object Helper {
finalException: Exception? finalException: Exception?
) { ) {
if (download.state == Download.STATE_COMPLETED) { if (download.state == Download.STATE_COMPLETED) {
Log.e("Downloader", "Download Completed") Logger.log("Download Completed")
} else if (download.state == Download.STATE_FAILED) { } else if (download.state == Download.STATE_FAILED) {
Log.e("Downloader", "Download Failed") Logger.log("Download Failed")
} else if (download.state == Download.STATE_STOPPED) { } else if (download.state == Download.STATE_STOPPED) {
Log.e("Downloader", "Download Stopped") Logger.log("Download Stopped")
} else if (download.state == Download.STATE_QUEUED) { } else if (download.state == Download.STATE_QUEUED) {
Log.e("Downloader", "Download Queued") Logger.log("Download Queued")
} else if (download.state == Download.STATE_DOWNLOADING) { } else if (download.state == Download.STATE_DOWNLOADING) {
Log.e("Downloader", "Download Downloading") Logger.log("Download Downloading")
} }
} }
} }

View file

@ -283,7 +283,6 @@ class AnimeFragment : Fragment() {
binding.root.requestApplyInsets() binding.root.requestApplyInsets()
binding.root.requestLayout() binding.root.requestLayout()
} }
super.onResume() super.onResume()
} }
} }

View file

@ -26,6 +26,7 @@ import ani.dantotsu.media.CalendarActivity
import ani.dantotsu.media.GenreActivity import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.SearchActivity import ani.dantotsu.media.SearchActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn import ani.dantotsu.setSlideIn
@ -94,6 +95,17 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME) SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog") dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
} }
binding.animeUserAvatar.setOnLongClickListener { view ->
ContextCompat.startActivity(
view.context,
Intent(view.context, ProfileActivity::class.java)
.putExtra("userId", Anilist.userid),null
)
false
}
binding.animeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.animeNotificationCount.text = Anilist.unreadNotificationCount.toString()
listOf( listOf(
binding.animePreviousSeason, binding.animePreviousSeason,

View file

@ -21,6 +21,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.blurImage
import ani.dantotsu.bottomBar import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel import ani.dantotsu.connections.anilist.AnilistHomeViewModel
@ -32,6 +33,7 @@ import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.user.ListActivity import ani.dantotsu.media.user.ListActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn import ani.dantotsu.setSlideIn
import ani.dantotsu.setSlideUp import ani.dantotsu.setSlideUp
@ -77,8 +79,10 @@ class HomeFragment : Fragment() {
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString() binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
binding.homeUserAvatar.loadImage(Anilist.avatar) binding.homeUserAvatar.loadImage(Anilist.avatar)
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause() if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
binding.homeUserBg.loadImage(Anilist.bg) blurImage(binding.homeUserBg, Anilist.bg)
binding.homeUserDataProgressBar.visibility = View.GONE binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener { binding.homeAnimeList.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
@ -118,6 +122,13 @@ class HomeFragment : Fragment() {
"dialog" "dialog"
) )
} }
binding.homeUserAvatarContainer.setOnLongClickListener {
ContextCompat.startActivity(
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
.putExtra("userId", Anilist.userid),null
)
false
}
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
@ -127,17 +138,20 @@ class HomeFragment : Fragment() {
var reached = false var reached = false
val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong() val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
if (!binding.homeScroll.canScrollVertically(1)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
reached = true binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
bottomBar.animate().translationZ(0f).setDuration(duration).start() if (!binding.homeScroll.canScrollVertically(1)) {
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration) reached = true
.start() bottomBar.animate().translationZ(0f).setDuration(duration).start()
} else { ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
if (reached) {
bottomBar.animate().translationZ(12f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
.start() .start()
} else {
if (reached) {
bottomBar.animate().translationZ(12f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
.start()
}
} }
} }
} }
@ -305,6 +319,7 @@ class HomeFragment : Fragment() {
} }
} }
val array = arrayOf( val array = arrayOf(
"AnimeContinue", "AnimeContinue",
"AnimeFav", "AnimeFav",
@ -357,9 +372,12 @@ class HomeFragment : Fragment() {
} }
} }
} }
override fun onResume() { override fun onResume() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true) if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) {
binding.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
super.onResume() super.onResume()
} }
} }

View file

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

View file

@ -25,6 +25,7 @@ import ani.dantotsu.loadImage
import ani.dantotsu.media.GenreActivity import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.SearchActivity import ani.dantotsu.media.SearchActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn import ani.dantotsu.setSlideIn
@ -74,7 +75,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
} }
updateAvatar() updateAvatar()
binding.mangaNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.mangaSearchBar.hint = "MANGA" binding.mangaSearchBar.hint = "MANGA"
binding.mangaSearchBarText.setOnClickListener { binding.mangaSearchBarText.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
@ -89,6 +91,14 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA) SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog") dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
} }
binding.mangaUserAvatar.setOnLongClickListener { view ->
ContextCompat.startActivity(
view.context,
Intent(view.context, ProfileActivity::class.java)
.putExtra("userId", Anilist.userid),null
)
false
}
binding.mangaSearchBar.setEndIconOnClickListener { binding.mangaSearchBar.setEndIconOnClickListener {
binding.mangaSearchBarText.performClick() binding.mangaSearchBarText.performClick()
@ -145,8 +155,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer()) binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper()) trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable { trendRun = Runnable {
binding.mangaTrendingViewPager.currentItem = binding.mangaTrendingViewPager.currentItem += 1
binding.mangaTrendingViewPager.currentItem + 1
} }
binding.mangaTrendingViewPager.registerOnPageChangeCallback( binding.mangaTrendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {

View file

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

View file

@ -0,0 +1,60 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
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)
}
@SuppressLint("SetTextI18n")
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

@ -80,14 +80,13 @@ class CalendarActivity : AppCompatActivity() {
) )
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight topMargin = statusBarHeight
bottomMargin = navBarHeight
} }
} }
setContentView(binding.root) setContentView(binding.root)
binding.listTitle.setText(R.string.release_calendar) binding.listTitle.setText(R.string.release_calendar)
binding.listSort.visibility = View.GONE binding.listSort.visibility = View.GONE
binding.random.visibility = View.GONE
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) { override fun onTabSelected(tab: TabLayout.Tab?) {
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1 this@CalendarActivity.selectedTabIdx = tab?.position ?: 1

View file

@ -9,7 +9,7 @@ data class Character(
val image: String?, val image: String?,
val banner: String?, val banner: String?,
val role: String, val role: String,
var isFav: Boolean,
var description: String? = null, var description: String? = null,
var age: String? = null, var age: String? = null,
var gender: String? = null, var gender: String? = null,

View file

@ -1,5 +1,6 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -15,20 +16,25 @@ import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistMutations
import ani.dantotsu.databinding.ActivityCharacterBinding import ani.dantotsu.databinding.ActivityCharacterBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.abs import kotlin.math.abs
class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener { class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
@ -48,7 +54,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
initActivity(this) initActivity(this)
screenWidth = resources.displayMetrics.run { widthPixels / density } screenWidth = resources.displayMetrics.run { widthPixels / density }
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor = if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.status) ContextCompat.getColor(this, R.color.transparent)
val banner = val banner =
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
@ -75,7 +81,39 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
character.image 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) { model.getCharacter().observe(this) {
if (it != null && !loaded) { if (it != null && !loaded) {
character = it character = it
@ -139,13 +177,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
isCollapsed = true isCollapsed = true
if (immersiveMode) this.window.statusBarColor = if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg) ContextCompat.getColor(this, R.color.nav_bg)
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
} }
if (percentage <= percent && isCollapsed) { if (percentage <= percent && isCollapsed) {
isCollapsed = false isCollapsed = false
if (immersiveMode) this.window.statusBarColor = if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.status) ContextCompat.getColor(this, R.color.transparent)
binding.characterAppBar.setBackgroundResource(R.color.bg)
} }
} }
} }

View file

@ -35,7 +35,7 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
binding.characterDesc.isTextSelectable binding.characterDesc.isTextSelectable
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()) val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(SpoilerPlugin()).build() .usePlugin(SpoilerPlugin()).build()
markWon.setMarkdown(binding.characterDesc, desc) markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
} }

View file

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

View file

@ -58,6 +58,7 @@ data class Media(
var endDate: FuzzyDate? = null, var endDate: FuzzyDate? = null,
var characters: ArrayList<Character>? = null, var characters: ArrayList<Character>? = null,
var staff: ArrayList<Author>? = null,
var prequel: Media? = null, var prequel: Media? = null,
var sequel: Media? = null, var sequel: Media? = null,
var relations: ArrayList<Media>? = null, var relations: ArrayList<Media>? = null,
@ -108,6 +109,7 @@ data class Media(
this.userScore = mediaList.score?.toInt() ?: 0 this.userScore = mediaList.score?.toInt() ?: 0
this.userStatus = mediaList.status?.toString() this.userStatus = mediaList.status?.toString()
this.userUpdatedAt = mediaList.updatedAt?.toLong() this.userUpdatedAt = mediaList.updatedAt?.toLong()
this.genres = mediaList.media?.genres?.toMutableList() as? ArrayList<String>? ?: arrayListOf()
} }
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) { constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {

View file

@ -43,6 +43,7 @@ class MediaAdaptor(
private val activity: FragmentActivity, private val activity: FragmentActivity,
private val matchParent: Boolean = false, private val matchParent: Boolean = false,
private val viewPager: ViewPager2? = null, private val viewPager: ViewPager2? = null,
private val fav: Boolean = false,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@ -128,6 +129,7 @@ class MediaAdaptor(
) )
b.itemCompactTotal.text = " | ${media.manga.totalChapters ?: "~"}" b.itemCompactTotal.text = " | ${media.manga.totalChapters ?: "~"}"
} }
b.itemCompactProgressContainer.visibility = if (fav) View.GONE else View.VISIBLE
} }
} }
@ -137,7 +139,7 @@ class MediaAdaptor(
val media = mediaList?.get(position) val media = mediaList?.get(position)
if (media != null) { if (media != null) {
b.itemCompactImage.loadImage(media.cover) b.itemCompactImage.loadImage(media.cover)
b.itemCompactBanner.loadImage(media.banner ?: media.cover) blurImage(b.itemCompactBanner, media.banner ?: media.cover)
b.itemCompactOngoing.visibility = b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
@ -185,15 +187,7 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator() AccelerateDecelerateInterpolator()
) )
) )
val banner = blurImage(b.itemCompactBanner, media.banner ?: media.cover)
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 = b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
@ -242,15 +236,7 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator() AccelerateDecelerateInterpolator()
) )
) )
val banner = blurImage(b.itemCompactBanner, media.banner ?: media.cover)
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 = b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName

View file

@ -2,7 +2,9 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.util.TypedValue import android.util.TypedValue
@ -10,7 +12,9 @@ import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -18,6 +22,7 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.bold import androidx.core.text.bold
import androidx.core.text.color import androidx.core.text.color
import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
@ -25,21 +30,23 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.CustomBottomNavBar
import ani.dantotsu.GesturesListener import ani.dantotsu.GesturesListener
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.ZoomOutPageTransformer import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.blurImage
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.copyToClipboard import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ActivityMediaBinding import ani.dantotsu.databinding.ActivityMediaBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.anime.AnimeWatchFragment import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.media.comments.CommentsFragment
import ani.dantotsu.media.manga.MangaReadFragment import ani.dantotsu.media.manga.MangaReadFragment
import ani.dantotsu.media.novel.NovelReadFragment import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.AndroidBug5497Workaround
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
@ -49,20 +56,21 @@ import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import com.flaviofaria.kenburnsview.RandomTransitionGenerator import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.math.abs import kotlin.math.abs
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener { class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
private lateinit var binding: ActivityMediaBinding lateinit var binding: ActivityMediaBinding
private val scope = lifecycleScope private val scope = lifecycleScope
private val model: MediaDetailsViewModel by viewModels() private val model: MediaDetailsViewModel by viewModels()
private lateinit var tabLayout: NavigationBarView lateinit var tabLayout: TripleNavAdapter
var selected = 0 var selected = 0
var anime = true var anime = true
private var adult = false private var adult = false
@ -71,6 +79,15 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia() 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") { if (media.name == "No media found") {
snackString(media.name) snackString(media.name)
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
@ -84,20 +101,31 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
setContentView(binding.root) setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat() screenWidth = resources.displayMetrics.widthPixels.toFloat()
val isVertical = resources.configuration.orientation
//Ui init //Ui init
initActivity(this) 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
}
binding.mediaTabContainer.visibility = View.GONE
} else {
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = oldMargin
}
binding.mediaTabContainer.visibility = View.VISIBLE
}
}
binding.mediaBanner.updateLayoutParams { height += statusBarHeight } binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight } binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.mediaCollapsing.minimumHeight = statusBarHeight binding.mediaCollapsing.minimumHeight = statusBarHeight
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.mediaTitle.isSelected = true binding.mediaTitle.isSelected = true
mMaxScrollSize = binding.mediaAppBar.totalScrollRange mMaxScrollSize = binding.mediaAppBar.totalScrollRange
@ -119,7 +147,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
val banner = val banner =
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
val viewPager = binding.mediaViewPager val viewPager = binding.mediaViewPager
tabLayout = binding.mediaTab as NavigationBarView //tabLayout = binding.mediaTab as AnimatedBottomBar
viewPager.isUserInputEnabled = false viewPager.isUserInputEnabled = false
viewPager.setPageTransformer(ZoomOutPageTransformer()) viewPager.setPageTransformer(ZoomOutPageTransformer())
@ -135,7 +163,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
media.cover media.cover
) )
} }
banner.loadImage(media.banner ?: media.cover, 400)
blurImage(banner, media.banner ?: media.cover)
val gestureDetector = GestureDetector(this, object : GesturesListener() { val gestureDetector = GestureDetector(this, object : GesturesListener() {
override fun onDoubleClick(event: MotionEvent) { override fun onDoubleClick(event: MotionEvent) {
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
@ -313,49 +342,54 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
progress() progress()
} }
} }
tabLayout = TripleNavAdapter(
binding.mediaTab1,
binding.mediaTab2,
binding.mediaTab3,
media.anime != null,
media.format ?: "",
isVertical == 1
)
adult = media.isAdult adult = media.isAdult
tabLayout.menu.clear()
if (media.anime != null) { if (media.anime != null) {
viewPager.adapter = viewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME) ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME, media, intent.getIntExtra("commentId", -1))
tabLayout.inflateMenu(R.menu.anime_menu_detail)
} else if (media.manga != null) { } else if (media.manga != null) {
viewPager.adapter = ViewPagerAdapter( viewPager.adapter = ViewPagerAdapter(
supportFragmentManager, supportFragmentManager,
lifecycle, lifecycle,
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA,
media,
intent.getIntExtra("commentId", -1)
) )
if (media.format == "NOVEL") {
tabLayout.inflateMenu(R.menu.novel_menu_detail)
} else {
tabLayout.inflateMenu(R.menu.manga_menu_detail)
}
anime = false anime = false
} }
selected = media.selected!!.window selected = media.selected!!.window
binding.mediaTitle.translationX = -screenWidth binding.mediaTitle.translationX = -screenWidth
tabLayout.visibility = View.VISIBLE
tabLayout.setOnItemSelectedListener { item -> tabLayout.selectionListener = { selected, newId ->
selectFromID(item.itemId) binding.commentInputLayout.visibility = if (selected == 2) View.VISIBLE else View.GONE
this.selected = selected
selectFromID(newId)
viewPager.setCurrentItem(selected, false) viewPager.setCurrentItem(selected, false)
val sel = model.loadSelected(media, isDownload) val sel = model.loadSelected(media, isDownload)
sel.window = selected sel.window = selected
model.saveSelected(media.id, sel) model.saveSelected(media.id, sel)
true
} }
tabLayout.selectTab(selected)
selectFromID(tabLayout.selected)
tabLayout.selectedItemId = idFromSelect()
viewPager.setCurrentItem(selected, false) viewPager.setCurrentItem(selected, false)
if (model.continueMedia == null && media.cameFromContinue) { if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia) model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
selected = 1 selected = 1
} }
val frag = intent.getStringExtra("FRAGMENT_TO_LOAD")
if (frag != null) {
selected = 2
}
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) } val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) { live.observe(this) {
@ -368,7 +402,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} }
} }
private fun selectFromID(id: Int) { private fun selectFromID(id: Int) {
when (id) { when (id) {
R.id.info -> { R.id.info -> {
@ -378,6 +411,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
R.id.watch, R.id.read -> { R.id.watch, R.id.read -> {
selected = 1 selected = 1
} }
R.id.comment -> {
selected = 2
}
} }
} }
@ -385,17 +422,19 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
if (anime) when (selected) { if (anime) when (selected) {
0 -> return R.id.info 0 -> return R.id.info
1 -> return R.id.watch 1 -> return R.id.watch
2 -> return R.id.comment
} }
else when (selected) { else when (selected) {
0 -> return R.id.info 0 -> return R.id.info
1 -> return R.id.read 1 -> return R.id.read
2 -> return R.id.comment
} }
return R.id.info return R.id.info
} }
override fun onResume() { override fun onResume() {
if (this::tabLayout.isInitialized) { if (this::tabLayout.isInitialized) {
tabLayout.selectedItemId = idFromSelect() tabLayout.selectTab(selected)
} }
super.onResume() super.onResume()
} }
@ -408,19 +447,30 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
private class ViewPagerAdapter( private class ViewPagerAdapter(
fragmentManager: FragmentManager, fragmentManager: FragmentManager,
lifecycle: Lifecycle, lifecycle: Lifecycle,
private val media: SupportedMedia private val mediaType: SupportedMedia,
private val media: Media,
private val commentId: Int
) : ) :
FragmentStateAdapter(fragmentManager, lifecycle) { FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int = 2 override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment = when (position) { override fun createFragment(position: Int): Fragment = when (position) {
0 -> MediaInfoFragment() 0 -> MediaInfoFragment()
1 -> when (media) { 1 -> when (mediaType) {
SupportedMedia.ANIME -> AnimeWatchFragment() SupportedMedia.ANIME -> AnimeWatchFragment()
SupportedMedia.MANGA -> MangaReadFragment() SupportedMedia.MANGA -> MangaReadFragment()
SupportedMedia.NOVEL -> NovelReadFragment() 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() else -> MediaInfoFragment()
} }
@ -484,6 +534,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
private val c1: Int, private val c1: Int,
private val c2: Int, private val c2: Int,
var clicked: Boolean, var clicked: Boolean,
needsInitialClick: Boolean = false,
callback: suspend (Boolean) -> (Unit) callback: suspend (Boolean) -> (Unit)
) { ) {
private var disabled = false private var disabled = false
@ -492,6 +543,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
init { init {
enabled(true) enabled(true)
if (needsInitialClick) {
scope.launch {
clicked()
}
}
image.setOnClickListener { image.setOnClickListener {
if (pressable && !disabled) { if (pressable && !disabled) {
pressable = false pressable = false
@ -546,5 +602,4 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
companion object { companion object {
var mediaSingleton: Media? = null var mediaSingleton: Media? = null
} }
} }

View file

@ -9,7 +9,7 @@ import androidx.lifecycle.ViewModel
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.media.anime.Episode import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
@ -223,7 +223,7 @@ class MediaDetailsViewModel : ViewModel() {
} }
fun setEpisode(ep: Episode?, who: String) { fun setEpisode(ep: Episode?, who: String) {
logger("set episode ${ep?.number} - $who", false) Logger.log("set episode ${ep?.number} - $who")
episode.postValue(ep) episode.postValue(ep)
MainScope().launch(Dispatchers.Main) { MainScope().launch(Dispatchers.Main) {
episode.value = null episode.value = null
@ -270,7 +270,7 @@ class MediaDetailsViewModel : ViewModel() {
mangaChapters mangaChapters
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) { suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
logger("Loading Manga Chapters : $mangaLoaded") Logger.log("Loading Manga Chapters : $mangaLoaded")
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend { if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
mangaLoaded[i] = mangaLoaded[i] =
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend

View file

@ -3,6 +3,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.CountDownTimer import android.os.CountDownTimer
@ -73,6 +74,8 @@ class MediaInfoFragment : Fragment() {
model.getMedia().observe(viewLifecycleOwner) { media -> model.getMedia().observe(viewLifecycleOwner) { media ->
if (media != null && !loaded) { if (media != null && !loaded) {
loaded = true loaded = true
binding.mediaInfoProgressBar.visibility = View.GONE binding.mediaInfoProgressBar.visibility = View.GONE
binding.mediaInfoContainer.visibility = View.VISIBLE binding.mediaInfoContainer.visibility = View.VISIBLE
binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji) binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
@ -408,23 +411,6 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) 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.relations.isNullOrEmpty() && !offline) {
if (media.sequel != null || media.prequel != null) { if (media.sequel != null || media.prequel != null) {
val bind = ItemQuelsBinding.inflate( val bind = ItemQuelsBinding.inflate(
@ -487,7 +473,38 @@ class MediaInfoFragment : Fragment() {
) )
parent.addView(bindi.root) parent.addView(bindi.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.staff.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.staff)
bind.itemRecycler.adapter =
AuthorAdapter(media.staff!!)
bind.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bind.root)
}
if (!media.recommendations.isNullOrEmpty() && !offline) { if (!media.recommendations.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate( val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),

View file

@ -254,20 +254,28 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
} }
binding.mediaListDelete.setOnClickListener { binding.mediaListDelete.setOnClickListener {
val id = media!!.userListId var id = media!!.userListId
if (id != null) { scope.launch {
scope.launch { withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { if (id != null) {
Anilist.mutation.deleteList(id) Anilist.mutation.deleteList(id!!)
MAL.query.deleteList(media?.anime != null, media?.idMAL) 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)
}
} }
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} }
}
if (id != null) {
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} else { } else {
snackString(getString(R.string.no_list_id)) snackString(getString(R.string.no_list_id))
Refresh.all()
} }
} }
} }

View file

@ -58,6 +58,40 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight } binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = viewLifecycleOwner.lifecycleScope 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("Failed to delete because of... ${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.mediaListProgressBar.visibility = View.GONE
binding.mediaListLayout.visibility = View.VISIBLE binding.mediaListLayout.visibility = View.VISIBLE

View file

@ -199,7 +199,9 @@ class SearchActivity : AppCompatActivity() {
var state: Parcelable? = null var state: Parcelable? = null
override fun onPause() { override fun onPause() {
headerAdaptor.addHistory() if (this::headerAdaptor.isInitialized) {
headerAdaptor.addHistory()
}
super.onPause() super.onPause()
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState() state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
} }

View file

@ -1,6 +1,7 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
@ -14,6 +15,7 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat.startActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
@ -23,6 +25,7 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import com.google.android.material.checkbox.MaterialCheckBox.* import com.google.android.material.checkbox.MaterialCheckBox.*
@ -92,13 +95,16 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also { binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
activity.updateChips = { it.update() } activity.updateChips = { it.update() }
} }
binding.searchChipRecycler.layoutManager = binding.searchChipRecycler.layoutManager =
LinearLayoutManager(binding.root.context, HORIZONTAL, false) LinearLayoutManager(binding.root.context, HORIZONTAL, false)
binding.searchFilter.setOnClickListener { binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog") SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
} }
binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
}
fun searchTitle() { fun searchTitle() {
activity.result.apply { activity.result.apply {
search = search =
@ -208,13 +214,16 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchHistoryList.startAnimation(fadeInAnimation()) binding.searchHistoryList.startAnimation(fadeInAnimation())
binding.searchResultLayout.visibility = View.GONE binding.searchResultLayout.visibility = View.GONE
binding.searchHistoryList.visibility = View.VISIBLE binding.searchHistoryList.visibility = View.VISIBLE
binding.searchByImage.visibility = View.VISIBLE
} else { } else {
if (binding.searchResultLayout.visibility != View.VISIBLE) { if (binding.searchResultLayout.visibility != View.VISIBLE) {
binding.searchResultLayout.startAnimation(fadeInAnimation()) binding.searchResultLayout.startAnimation(fadeInAnimation())
binding.searchHistoryList.startAnimation(fadeOutAnimation()) binding.searchHistoryList.startAnimation(fadeOutAnimation())
} }
binding.searchResultLayout.visibility = View.VISIBLE binding.searchResultLayout.visibility = View.VISIBLE
binding.searchHistoryList.visibility = View.GONE binding.searchHistoryList.visibility = View.GONE
binding.searchByImage.visibility = View.GONE
} }
} }

View file

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

View file

@ -7,7 +7,7 @@ import java.util.regex.Pattern
class AnimeNameAdapter { class AnimeNameAdapter {
companion object { companion object {
const val episodeRegex = const val episodeRegex =
"(episode|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*" "(episode|episodio|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
const val failedEpisodeNumberRegex = const val failedEpisodeNumberRegex =
"(?<!part\\s)\\b(\\d+)\\b" "(?<!part\\s)\\b(\\d+)\\b"
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*" const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"

View file

@ -5,6 +5,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import ani.dantotsu.settings.FAQActivity
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ImageButton import android.widget.ImageButton
@ -28,10 +29,9 @@ import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource 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 eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -59,15 +59,21 @@ class AnimeWatchAdapter(
val binding = holder.binding val binding = holder.binding
_binding = binding _binding = binding
binding.faqbutton.setOnClickListener {
startActivity(
fragment.requireContext(),
Intent(fragment.requireContext(), FAQActivity::class.java),
null
)
}
//Youtube //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.visibility = View.VISIBLE
binding.animeSourceYT.setOnClickListener { binding.animeSourceYT.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
fragment.requireContext().startActivity(intent) fragment.requireContext().startActivity(intent)
} }
} }
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
binding.animeSourceDubbedText.text = binding.animeSourceDubbedText.text =
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString( if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
@ -179,7 +185,8 @@ class AnimeWatchAdapter(
R.drawable.ic_round_notifications_none_24, R.drawable.ic_round_notifications_none_24,
R.color.bg_opp, R.color.bg_opp,
R.color.violet_400, R.color.violet_400,
fragment.subscribed fragment.subscribed,
true
) { ) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString()) fragment.onNotificationPressed(it, binding.animeSource.text.toString())
} }
@ -187,7 +194,7 @@ class AnimeWatchAdapter(
subscribeButton(false) subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener { binding.animeSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(), getChannelId(true, media.id)) openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
} }
//Nested Button //Nested Button
@ -421,13 +428,17 @@ class AnimeWatchAdapter(
} }
binding.animeSourceProgressBar.visibility = View.GONE binding.animeSourceProgressBar.visibility = View.GONE
if (media.anime.episodes!!.isNotEmpty()) if (media.anime.episodes!!.isNotEmpty()) {
binding.animeSourceNotFound.visibility = View.GONE binding.animeSourceNotFound.visibility = View.GONE
else binding.faqbutton.visibility = View.GONE}
else {
binding.animeSourceNotFound.visibility = View.VISIBLE binding.animeSourceNotFound.visibility = View.VISIBLE
binding.faqbutton.visibility = View.VISIBLE
}
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE binding.animeSourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE
clearChips() clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE binding.animeSourceProgressBar.visibility = View.VISIBLE
} }

View file

@ -43,13 +43,9 @@ import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.subcriptions.Notifications import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
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.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -333,16 +329,7 @@ class AnimeWatchFragment : Fragment() {
var subscribed = false var subscribed = false
fun onNotificationPressed(subscribed: Boolean, source: String) { fun onNotificationPressed(subscribed: Boolean, source: String) {
this.subscribed = subscribed this.subscribed = subscribed
saveSubscription(requireContext(), media, subscribed) saveSubscription(media, subscribed)
if (!subscribed)
Notifications.deleteChannel(requireContext(), getChannelId(true, media.id))
else
Notifications.createChannel(
requireContext(),
ANIME_GROUP,
getChannelId(true, media.id),
media.userPreferredName
)
snackString( snackString(
if (subscribed) getString(R.string.subscribed_notification, source) if (subscribed) getString(R.string.subscribed_notification, source)
else getString(R.string.unsubscribed_notification) else getString(R.string.unsubscribed_notification)
@ -358,11 +345,9 @@ class AnimeWatchFragment : Fragment() {
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try {
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility activity.tabLayout.setVisibility(visibility)
} catch (e: ClassCastException) {
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
}
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility = activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE if (show) View.GONE else View.VISIBLE
} }
@ -561,6 +546,8 @@ class AnimeWatchFragment : Fragment() {
super.onResume() super.onResume()
binding.mediaInfoProgressBar.visibility = progress binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state) binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
requireActivity().setNavigationTheme()
} }
override fun onPause() { override fun onPause() {

View file

@ -20,6 +20,7 @@ import android.media.AudioManager.*
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.CountDownTimer
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings.System import android.provider.Settings.System
@ -39,6 +40,7 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.math.MathUtils.clamp import androidx.core.math.MathUtils.clamp
import androidx.core.view.isVisible
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -61,6 +63,7 @@ import androidx.media3.exoplayer.util.EventLogger
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.ui.* import androidx.media3.ui.*
import androidx.media3.ui.CaptionStyleCompat.* import androidx.media3.ui.CaptionStyleCompat.*
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.mediarouter.app.MediaRouteButton import androidx.mediarouter.app.MediaRouteButton
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.R import ani.dantotsu.R
@ -85,6 +88,7 @@ import ani.dantotsu.settings.PlayerSettingsActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastContext
@ -113,6 +117,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
private val resumePosition = "resumePosition" private val resumePosition = "resumePosition"
private val playerFullscreen = "playerFullscreen" private val playerFullscreen = "playerFullscreen"
private val playerOnPlay = "playerOnPlay" private val playerOnPlay = "playerOnPlay"
private var disappeared: Boolean = false
private var functionstarted: Boolean = false
private lateinit var exoPlayer: ExoPlayer private lateinit var exoPlayer: ExoPlayer
private var castPlayer: CastPlayer? = null private var castPlayer: CastPlayer? = null
@ -311,7 +317,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
2 -> ResourcesCompat.getFont(this, R.font.poppins) 2 -> ResourcesCompat.getFont(this, R.font.poppins)
3 -> ResourcesCompat.getFont(this, R.font.poppins_thin) 3 -> ResourcesCompat.getFont(this, R.font.poppins_thin)
4 -> ResourcesCompat.getFont(this, R.font.century_gothic_regular) 4 -> ResourcesCompat.getFont(this, R.font.century_gothic_regular)
5 -> ResourcesCompat.getFont(this, R.font.century_gothic_bold) 5 -> ResourcesCompat.getFont(this, R.font.levenim_mt_bold)
6 -> ResourcesCompat.getFont(this, R.font.blocky)
else -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold) else -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
} }
playerView.subtitleView?.setStyle( playerView.subtitleView?.setStyle(
@ -386,30 +393,37 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
}, AUDIO_CONTENT_TYPE_MOVIE, AUDIOFOCUS_GAIN) }, AUDIO_CONTENT_TYPE_MOVIE, AUDIOFOCUS_GAIN)
if (System.getInt(contentResolver, System.ACCELEROMETER_ROTATION, 0) != 1) { if (System.getInt(contentResolver, System.ACCELEROMETER_ROTATION, 0) != 1) {
if (PrefManager.getVal(PrefName.RotationPlayer)) { if (PrefManager.getVal(PrefName.RotationPlayer)) {
orientationListener = orientationListener =
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) { object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
override fun onOrientationChanged(orientation: Int) { override fun onOrientationChanged(orientation: Int) {
if (orientation in 45..135) { if (orientation in 45..135) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) exoRotate.visibility = if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
View.VISIBLE exoRotate.visibility = View.VISIBLE
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
} else if (orientation in 225..315) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) exoRotate.visibility =
View.VISIBLE
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
} }
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
} else if (orientation in 225..315) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else if (orientation in 315..360 || orientation in 0..45) {
if (rotation != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
exoRotate.visibility = View.VISIBLE
}
rotation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} }
orientationListener?.enable() }
} }
orientationListener?.enable()
}
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
exoRotate.setOnClickListener { exoRotate.setOnClickListener {
requestedOrientation = rotation requestedOrientation = rotation
it.visibility = View.GONE it.visibility = View.GONE
} }
} }
setupSubFormatting(playerView) setupSubFormatting(playerView)
@ -963,7 +977,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
episodeTitle.setSelection(currentEpisodeIndex) episodeTitle.setSelection(currentEpisodeIndex)
episodeTitle.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { episodeTitle.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) {
if (position != currentEpisodeIndex) change(position) if (position != currentEpisodeIndex) {
disappeared = false
functionstarted = false
change(position)
}
} }
override fun onNothingSelected(parent: AdapterView<*>) {} override fun onNothingSelected(parent: AdapterView<*>) {}
@ -975,6 +993,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (isInitialized) { if (isInitialized) {
nextEpisode { i -> nextEpisode { i ->
updateAniProgress() updateAniProgress()
disappeared = false
functionstarted = false
change(currentEpisodeIndex + i) change(currentEpisodeIndex + i)
} }
} }
@ -983,6 +1003,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
exoPrev = playerView.findViewById(R.id.exo_prev_ep) exoPrev = playerView.findViewById(R.id.exo_prev_ep)
exoPrev.setOnClickListener { exoPrev.setOnClickListener {
if (currentEpisodeIndex > 0) { if (currentEpisodeIndex > 0) {
disappeared = false
change(currentEpisodeIndex - 1) change(currentEpisodeIndex - 1)
} else } else
snackString(getString(R.string.first_episode)) snackString(getString(R.string.first_episode))
@ -1243,10 +1264,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
media.anime!!.selectedEpisode!! media.anime!!.selectedEpisode!!
) )
val list = PrefManager.getVal<Set<Int>>(PrefName.ContinuedAnime).toMutableList() val list = (PrefManager.getNullableCustomVal("continueAnimeList", listOf<Int>(), List::class.java) as List<Int>).toMutableList()
if (list.contains(media.id)) list.remove(media.id) if (list.contains(media.id)) list.remove(media.id)
list.add(media.id) list.add(media.id)
PrefManager.setVal(PrefName.ContinuedAnime, list.toList()) PrefManager.setCustomVal("continueAnimeList", list)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
extractor?.onVideoStopped(video) extractor?.onVideoStopped(video)
@ -1258,7 +1279,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
subtitle = intent.getSerialized("subtitle") subtitle = intent.getSerialized("subtitle")
?: when (val subLang: String? = ?: when (val subLang: String? =
PrefManager.getCustomVal("subLang_${media.id}", null as String?)) { PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java)) {
null -> { null -> {
when (episode.selectedSubtitle) { when (episode.selectedSubtitle) {
null -> null null -> null
@ -1370,8 +1391,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
mediaItem = if (downloadedMediaItem == null) { mediaItem = if (downloadedMediaItem == null) {
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType) val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
logger("url: ${video!!.file.url}") Logger.log("url: ${video!!.file.url}")
logger("mimeType: $mimeType") Logger.log("mimeType: $mimeType")
if (sub != null) { if (sub != null) {
val listofnotnullsubs = listOfNotNull(sub) val listofnotnullsubs = listOfNotNull(sub)
@ -1447,10 +1468,26 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
private fun buildExoplayer() { private fun buildExoplayer() {
//Player //Player
val DEFAULT_MIN_BUFFER_MS = 600000
val DEFAULT_MAX_BUFFER_MS = 600000
val BUFFER_FOR_PLAYBACK_MS = 2500
val BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000
val loadControl = DefaultLoadControl.Builder()
.setBackBuffer(1000 * 60 * 2, true)
.setBufferDurationsMs(
DEFAULT_MIN_BUFFER_MS,
DEFAULT_MAX_BUFFER_MS,
BUFFER_FOR_PLAYBACK_MS,
BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
)
.build()
hideSystemBars() hideSystemBars()
exoPlayer = ExoPlayer.Builder(this) exoPlayer = ExoPlayer.Builder(this)
.setMediaSourceFactory(DefaultMediaSourceFactory(cacheFactory)) .setMediaSourceFactory(DefaultMediaSourceFactory(cacheFactory))
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.setLoadControl(loadControl)
.build().apply { .build().apply {
playWhenReady = true playWhenReady = true
this.playbackParameters = this@ExoplayerView.playbackParameters this.playbackParameters = this@ExoplayerView.playbackParameters
@ -1509,6 +1546,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
private fun releasePlayer() { private fun releasePlayer() {
isPlayerPlaying = exoPlayer.playWhenReady isPlayerPlaying = exoPlayer.playWhenReady
playbackPosition = exoPlayer.currentPosition playbackPosition = exoPlayer.currentPosition
disappeared = false
functionstarted = false
exoPlayer.release() exoPlayer.release()
VideoCache.release() VideoCache.release()
mediaSession?.release() mediaSession?.release()
@ -1682,26 +1721,69 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
val new = currentTimeStamp val new = currentTimeStamp
timeStampText.text = if (new != null) { timeStampText.text = if (new != null) {
if (PrefManager.getVal(PrefName.ShowTimeStampButton)) { fun disappearSkip() {
functionstarted = true
skipTimeButton.visibility = View.VISIBLE skipTimeButton.visibility = View.VISIBLE
exoSkip.visibility = View.GONE exoSkip.visibility = View.GONE
skipTimeText.text = new.skipType.getType() skipTimeText.text = new.skipType.getType()
skipTimeButton.setOnClickListener { skipTimeButton.setOnClickListener {
exoPlayer.seekTo((new.interval.endTime * 1000).toLong()) exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
} }
var timer: CountDownTimer? = null
fun cancelTimer() {
timer?.cancel()
timer = null
return
}
if (timer == null) {
timer = object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {
if (new == null){
skipTimeButton.visibility = View.GONE
exoSkip.visibility = View.VISIBLE
disappeared = false
functionstarted = false
cancelTimer()
}
}
override fun onFinish() {
skipTimeButton.visibility = View.GONE
exoSkip.visibility = View.VISIBLE
disappeared = true
functionstarted = false
cancelTimer()
}
}
timer?.start()
}
} }
if (PrefManager.getVal(PrefName.AutoSkipOPED) && (new.skipType == "op" || new.skipType == "ed") && !skippedTimeStamps.contains( if (PrefManager.getVal(PrefName.ShowTimeStampButton)) {
new
) if (!functionstarted && !disappeared && PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)) {
disappearSkip()
} else if (!PrefManager.getVal<Boolean>(PrefName.AutoHideTimeStamps)){
skipTimeButton.visibility = View.VISIBLE
exoSkip.visibility = View.GONE
skipTimeText.text = new.skipType.getType()
skipTimeButton.setOnClickListener {
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
}
}
}
if (PrefManager.getVal(PrefName.AutoSkipOPED) && (new.skipType == "op" || new.skipType == "ed")
&& !skippedTimeStamps.contains(new)
) { ) {
exoPlayer.seekTo((new.interval.endTime * 1000).toLong()) exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
skippedTimeStamps.add(new) skippedTimeStamps.add(new)
} }
new.skipType.getType() new.skipType.getType()
} else { } else {
disappeared = false
functionstarted = false
skipTimeButton.visibility = View.GONE skipTimeButton.visibility = View.GONE
if (PrefManager.getVal<Int>(PrefName.SkipTime) > 0) exoSkip.visibility = exoSkip.isVisible = PrefManager.getVal<Int>(PrefName.SkipTime) > 0
View.VISIBLE
"" ""
} }
} }
@ -1772,17 +1854,23 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
private fun updateAniProgress() { private fun updateAniProgress() {
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito) val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if (!incognito && exoPlayer.currentPosition / episodeLength > PrefManager.getVal<Float>( val episodeEnd = exoPlayer.currentPosition / episodeLength > PrefManager.getVal<Float>(
PrefName.WatchPercentage PrefName.WatchPercentage
) && Anilist.userid != null )
val episode0 = currentEpisodeIndex == 0 && PrefManager.getVal(PrefName.ChapterZeroPlayer)
if (!incognito && (episodeEnd || episode0) && Anilist.userid != null
) )
if (PrefManager.getCustomVal( if (PrefManager.getCustomVal(
"${media.id}_save_progress", "${media.id}_save_progress",
true true
) && (if (media.isAdult) PrefManager.getVal(PrefName.UpdateForHPlayer) else true) ) && (if (media.isAdult) PrefManager.getVal(PrefName.UpdateForHPlayer) else true)
) { ) {
media.anime!!.selectedEpisode?.apply { if (episode0) {
updateProgress(media, this) updateProgress(media, "0")
} else {
media.anime!!.selectedEpisode?.apply {
updateProgress(media, this)
}
} }
} }
} }
@ -1822,6 +1910,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
if (isInitialized) { if (isInitialized) {
updateAniProgress() updateAniProgress()
disappeared = false
functionstarted = false
releasePlayer() releasePlayer()
} }
@ -2031,4 +2121,4 @@ class CustomCastButton : MediaRouteButton {
true true
} }
} }
} }

View file

@ -181,11 +181,23 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex) model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.selectorProgressBar.visibility = View.GONE binding.selectorProgressBar.visibility = View.GONE
if (adapter.itemCount == 0) {
snackString(getString(R.string.stream_selection_empty))
tryWith {
dismiss()
}
}
} }
} }
} else { } else {
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep) media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
adapter.addAll(ep.extractors) adapter.addAll(ep.extractors)
if (ep.extractors?.size == 0) {
snackString(getString(R.string.stream_selection_empty))
tryWith {
dismiss()
}
}
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) { if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
adapter.performClick(0) adapter.performClick(0)
} }

View file

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

View file

@ -0,0 +1,394 @@
package ani.dantotsu.media.comments
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.connections.comments.Comment
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemCommentsBinding
import ani.dantotsu.loadImage
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.setAnimation
import ani.dantotsu.snackString
import ani.dantotsu.util.ColorEditor.Companion.adjustColorForContrast
import ani.dantotsu.util.ColorEditor.Companion.getContrastRatio
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.BindableItem
import io.noties.markwon.Markwon
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.TimeZone
import kotlin.math.abs
import kotlin.math.sqrt
class CommentItem(val comment: Comment,
private val markwon: Markwon,
val parentSection: Section,
private val commentsFragment: CommentsFragment,
private val backgroundColor: Int,
val commentDepth: Int
) : BindableItem<ItemCommentsBinding>() {
lateinit var binding: ItemCommentsBinding
val adapter = GroupieAdapter()
private var subCommentIds: MutableList<Int> = mutableListOf()
val repliesSection = Section()
private var isEditing = false
var isReplying = false
private var repliesVisible = false
var MAX_DEPTH = 3
init {
adapter.add(repliesSection)
}
@SuppressLint("SetTextI18n")
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
binding = viewBinding
setAnimation(binding.root.context, binding.root)
viewBinding.commentRepliesList.layoutManager = LinearLayoutManager(commentsFragment.activity)
viewBinding.commentRepliesList.adapter = adapter
val isUserComment = CommentsAPI.userId == comment.userId
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
markwon.setMarkdown(viewBinding.commentText, comment.content)
viewBinding.commentDelete.visibility = if (isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod) View.VISIBLE else View.GONE
viewBinding.commentBanUser.visibility = if ((CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment) View.VISIBLE else View.GONE
viewBinding.commentReport.visibility = if (!isUserComment) View.VISIBLE else View.GONE
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
if (comment.tag == null) {
viewBinding.commentUserTagLayout.visibility = View.GONE
} else {
viewBinding.commentUserTagLayout.visibility = View.VISIBLE
viewBinding.commentUserTag.text = comment.tag.toString()
}
replying(isReplying) //sets default text
editing(isEditing)
if ((comment.replyCount ?: 0) > 0) {
viewBinding.commentTotalReplies.visibility = View.VISIBLE
viewBinding.commentRepliesDivider.visibility = View.VISIBLE
viewBinding.commentTotalReplies.text = if(repliesVisible) "Hide Replies" else
"View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
} else {
viewBinding.commentTotalReplies.visibility = View.GONE
viewBinding.commentRepliesDivider.visibility = View.GONE
}
viewBinding.commentReply.visibility = View.VISIBLE
viewBinding.commentTotalReplies.setOnClickListener {
if (repliesVisible) {
repliesSection.clear()
removeSubCommentIds()
viewBinding.commentTotalReplies.text = "View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
repliesVisible = false
} else {
viewBinding.commentTotalReplies.text = "Hide Replies"
repliesSection.clear()
commentsFragment.viewReplyCallback(this)
repliesVisible = true
}
}
viewBinding.commentUserName.setOnClickListener {
ContextCompat.startActivity(
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
.putExtra("userId", comment.userId.toInt())
.putExtra("userLVL","[${levelColor.second}]"), null
)
}
viewBinding.commentUserAvatar.setOnClickListener {
ContextCompat.startActivity(
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
.putExtra("userId", comment.userId.toInt())
.putExtra("userLVL","[${levelColor.second}]"), null
)
}
viewBinding.commentText.setOnLongClickListener {
copyToClipboard(comment.content)
true
}
viewBinding.commentEdit.setOnClickListener {
editing(!isEditing)
commentsFragment.editCallback(this)
}
viewBinding.commentReply.setOnClickListener {
replying(!isReplying)
commentsFragment.replyTo(this, comment.username)
commentsFragment.replyCallback(this)
}
viewBinding.modBadge.visibility = if (comment.isMod == true) View.VISIBLE else View.GONE
viewBinding.adminBadge.visibility = if (comment.isAdmin == true) View.VISIBLE else View.GONE
viewBinding.commentDelete.setOnClickListener {
dialogBuilder("Delete Comment", "Are you sure you want to delete this comment?") {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.deleteComment(comment.commentId)
if (success) {
snackString("Comment Deleted")
parentSection.remove(this@CommentItem)
}
}
}
}
viewBinding.commentBanUser.setOnClickListener {
dialogBuilder("Ban User", "Are you sure you want to ban this user?") {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.banUser(comment.userId)
if (success) {
snackString("User Banned")
}
}
}
}
viewBinding.commentReport.setOnClickListener {
dialogBuilder("Report Comment", "Only report comments that violate the rules. Are you sure you want to report this comment?") {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.reportComment(comment.commentId, comment.username, commentsFragment.mediaName, comment.userId)
if (success) {
snackString("Comment Reported")
}
}
}
}
//fill the icon if the user has liked the comment
setVoteButtons(viewBinding)
viewBinding.commentUpVote.setOnClickListener {
val voteType = if (comment.userVoteType == 1) 0 else 1
val previousVoteType = comment.userVoteType
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.vote(comment.commentId, voteType)
if (success) {
comment.userVoteType = voteType
if (previousVoteType == -1) {
comment.downvotes -= 1
}
comment.upvotes += if (voteType == 1) 1 else -1
notifyChanged()
}
}
}
viewBinding.commentDownVote.setOnClickListener {
val voteType = if (comment.userVoteType == -1) 0 else -1
val previousVoteType = comment.userVoteType
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.vote(comment.commentId, voteType)
if (success) {
comment.userVoteType = voteType
if (previousVoteType == 1) {
comment.upvotes -= 1
}
comment.downvotes += if (voteType == -1) 1 else -1
notifyChanged()
}
}
}
viewBinding.commentTotalVotes.text = (comment.upvotes - comment.downvotes).toString()
viewBinding.commentUserAvatar.setOnLongClickListener {
ImageViewDialog.newInstance(
commentsFragment.activity,
"${comment.username}'s [Cover]",
comment.profilePictureUrl
)
}
comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) }
viewBinding.commentUserName.text = comment.username
viewBinding.commentUserLevel.text = "[${levelColor.second}]"
viewBinding.commentUserLevel.setTextColor(levelColor.first)
viewBinding.commentUserTime.text = formatTimestamp(comment.timestamp)
}
override fun getLayout(): Int {
return R.layout.item_comments
}
fun containsGif(): Boolean {
return comment.content.contains(".gif")
}
override fun initializeViewBinding(view: View): ItemCommentsBinding {
return ItemCommentsBinding.bind(view)
}
fun replying(isReplying: Boolean) {
binding.commentReply.text = if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply"
this.isReplying = isReplying
}
fun editing(isEditing: Boolean) {
binding.commentEdit.text = if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(R.string.edit)
this.isEditing = isEditing
}
fun registerSubComment(id: Int) {
subCommentIds.add(id)
}
private fun removeSubCommentIds(){
subCommentIds.forEach { id ->
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
val commentToRemove = parentComments.find { it.comment.commentId == id }
commentToRemove?.let {
it.removeSubCommentIds()
parentSection.remove(it)
}
}
subCommentIds.clear()
}
private fun setVoteButtons(viewBinding: ItemCommentsBinding) {
when (comment.userVoteType) {
1 -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_active_24)
viewBinding.commentUpVote.alpha = 1f
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
}
-1 -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
viewBinding.commentDownVote.alpha = 1f
}
else -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
}
}
}
private fun formatTimestamp(timestamp: String): String {
return try {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val parsedDate = dateFormat.parse(timestamp)
val currentDate = Date()
val diff = currentDate.time - (parsedDate?.time ?: 0)
val days = diff / (24 * 60 * 60 * 1000)
val hours = diff / (60 * 60 * 1000) % 24
val minutes = diff / (60 * 1000) % 60
return when {
days > 0 -> "${days}d"
hours > 0 -> "${hours}h"
minutes > 0 -> "${minutes}m"
else -> "now"
}
} catch (e: Exception) {
"now"
}
}
companion object {
fun timestampToMillis(timestamp: String): Long {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val parsedDate = dateFormat.parse(timestamp)
return parsedDate?.time ?: 0
}
}
private fun getAvatarColor(voteCount: Int, backgroundColor: Int): Pair<Int, Int> {
val level = if (voteCount < 0) 0 else sqrt(abs(voteCount.toDouble()) / 0.8).toInt()
val colorString = if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level]
var color = Color.parseColor(colorString)
val ratio = getContrastRatio(color, backgroundColor)
if (ratio < 4.5) {
color = adjustColorForContrast(color, backgroundColor)
}
return Pair(color, level)
}
/**
* Builds the dialog for yes/no confirmation
* no doesn't do anything, yes calls the callback
* @param title the title of the dialog
* @param message the message of the dialog
* @param callback the callback to call when the user clicks yes
*/
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
val alertDialog = android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
.setTitle(title)
.setMessage(message)
.setPositiveButton("Yes") { dialog, _ ->
callback()
dialog.dismiss()
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
}
private val usernameColors: Array<String> = arrayOf(
"#9932cc",
"#a020f0",
"#8b008b",
"#7b68ee",
"#da70d6",
"#dda0dd",
"#ffe4b5",
"#f0e68c",
"#ffb6c1",
"#fa8072",
"#b03060",
"#ff1493",
"#ff00ff",
"#ff69b4",
"#dc143c",
"#8b0000",
"#ff0000",
"#a0522d",
"#f4a460",
"#b8860b",
"#ffa500",
"#d2691e",
"#ff6347",
"#808000",
"#ffd700",
"#ffff54",
"#8fbc8f",
"#3cb371",
"#008000",
"#00fa9a",
"#98fb98",
"#00ff00",
"#adff2f",
"#32cd32",
"#556b2f",
"#9acd32",
"#7fffd4",
"#2f4f4f",
"#5f9ea0",
"#87ceeb",
"#00bfff",
"#00ffff",
"#1e90ff",
"#4682b4",
"#0000ff",
"#0000cd",
"#00008b",
"#191970",
"#ffffff",
)
}

View file

@ -0,0 +1,706 @@
package ani.dantotsu.media.comments
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context.INPUT_METHOD_SERVICE
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.appcompat.widget.PopupMenu
import androidx.core.animation.doOnEnd
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.buildMarkwon
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.comments.Comment
import ani.dantotsu.connections.comments.CommentResponse
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.databinding.FragmentCommentsBinding
import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaDetailsActivity
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 com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section
import io.noties.markwon.editor.MarkwonEditor
import io.noties.markwon.editor.MarkwonEditorTextWatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@SuppressLint("ClickableViewAccessibility")
class CommentsFragment : Fragment() {
lateinit var binding: FragmentCommentsBinding
lateinit var activity: MediaDetailsActivity
private var interactionState = InteractionState.NONE
private var commentWithInteraction: CommentItem? = null
private val section = Section()
private val adapter = GroupieAdapter()
private var tag: Int? = null
private var filterTag: Int? = null
private var mediaId: Int = -1
var mediaName: String = ""
private var backgroundColor: Int = 0
var pagesLoaded = 1
var totalPages = 1
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentCommentsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity = requireActivity() as MediaDetailsActivity
//get the media id from the intent
val mediaId = arguments?.getInt("mediaId") ?: -1
mediaName = arguments?.getString("mediaName") ?: "unknown"
if (mediaId == -1) {
snackString("Invalid Media ID")
return
}
this.mediaId = mediaId
backgroundColor = (binding.root.background as? ColorDrawable)?.color ?: 0
val markwon = buildMarkwon(activity, fragment = this@CommentsFragment)
activity.binding.commentUserAvatar.loadImage(Anilist.avatar)
val markwonEditor = MarkwonEditor.create(markwon)
activity.binding.commentInput.addTextChangedListener(
MarkwonEditorTextWatcher.withProcess(
markwonEditor
)
)
binding.commentsRefresh.setOnRefreshListener {
lifecycleScope.launch {
loadAndDisplayComments()
binding.commentsRefresh.isRefreshing = false
}
activity.binding.commentReplyToContainer.visibility = View.GONE
}
binding.commentsList.adapter = adapter
binding.commentsList.layoutManager = LinearLayoutManager(activity)
if (CommentsAPI.authToken != null) {
lifecycleScope.launch {
val commentId = arguments?.getInt("commentId")
if (commentId != null && commentId > 0) {
loadSingleComment(commentId)
} else {
loadAndDisplayComments()
}
}
} else {
toast("Not logged in")
activity.binding.commentMessageContainer.visibility = View.GONE
}
binding.commentSort.setOnClickListener { sortView ->
fun sortComments(sortOrder: String) {
val groups = section.groups
when (sortOrder) {
"newest" -> groups.sortByDescending { CommentItem.timestampToMillis((it as CommentItem).comment.timestamp) }
"oldest" -> groups.sortBy { CommentItem.timestampToMillis((it as CommentItem).comment.timestamp) }
"highest_rated" -> groups.sortByDescending { (it as CommentItem).comment.upvotes - it.comment.downvotes }
"lowest_rated" -> groups.sortBy { (it as CommentItem).comment.upvotes - it.comment.downvotes }
}
section.update(groups)
}
val popup = PopupMenu(activity, sortView)
popup.setOnMenuItemClickListener { item ->
val sortOrder = when (item.itemId) {
R.id.comment_sort_newest -> "newest"
R.id.comment_sort_oldest -> "oldest"
R.id.comment_sort_highest_rated -> "highest_rated"
R.id.comment_sort_lowest_rated -> "lowest_rated"
else -> return@setOnMenuItemClickListener false
}
PrefManager.setVal(PrefName.CommentSortOrder, sortOrder)
if (totalPages > pagesLoaded) {
lifecycleScope.launch {
loadAndDisplayComments()
activity.binding.commentReplyToContainer.visibility = View.GONE
}
} else {
sortComments(sortOrder)
}
binding.commentsList.scrollToPosition(0)
true
}
popup.inflate(R.menu.comments_sort_menu)
popup.show()
}
binding.commentFilter.setOnClickListener {
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
.setTitle("Enter a chapter/episode number tag")
.setView(R.layout.dialog_edittext)
.setPositiveButton("OK") { dialog, _ ->
val editText =
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText)
val text = editText?.text.toString()
filterTag = text.toIntOrNull()
lifecycleScope.launch {
loadAndDisplayComments()
}
dialog.dismiss()
}
.setNeutralButton("Clear") { dialog, _ ->
filterTag = null
lifecycleScope.launch {
loadAndDisplayComments()
}
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
filterTag = null
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
}
var isFetching = false
binding.commentsList.setOnTouchListener(
object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_UP) {
if (!binding.commentsList.canScrollVertically(1) && !isFetching &&
(binding.commentsList.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() == (binding.commentsList.adapter!!.itemCount - 1)
) {
if (pagesLoaded < totalPages && totalPages > 1) {
binding.commentBottomRefresh.visibility = View.VISIBLE
loadMoreComments()
lifecycleScope.launch {
kotlinx.coroutines.delay(1000)
withContext(Dispatchers.Main) {
binding.commentBottomRefresh.visibility = View.GONE
}
}
} else {
//snackString("No more comments") fix spam?
Logger.log("No more comments")
}
}
}
return false
}
private fun loadMoreComments() {
isFetching = true
lifecycleScope.launch {
val comments = fetchComments()
comments?.comments?.forEach { comment ->
updateUIWithComment(comment)
}
totalPages = comments?.totalPages ?: 1
pagesLoaded++
isFetching = false
}
}
private suspend fun fetchComments(): CommentResponse? {
return withContext(Dispatchers.IO) {
CommentsAPI.getCommentsForId(
mediaId,
pagesLoaded + 1,
filterTag,
PrefManager.getVal(PrefName.CommentSortOrder, "newest")
)
}
}
//adds additional comments to the section
private suspend fun updateUIWithComment(comment: Comment) {
withContext(Dispatchers.Main) {
section.add(
CommentItem(
comment,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
0
)
)
}
}
})
activity.binding.commentInput.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: android.text.Editable?) {
if ((activity.binding.commentInput.text.length) > 300) {
activity.binding.commentInput.text.delete(
300,
activity.binding.commentInput.text.length
)
snackString("Comment cannot be longer than 300 characters")
}
}
})
activity.binding.commentInput.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
val targetWidth = activity.binding.commentInputLayout.width -
activity.binding.commentLabel.width -
activity.binding.commentSend.width -
activity.binding.commentUserAvatar.width - 12 + 16
val anim = ValueAnimator.ofInt(activity.binding.commentInput.width, targetWidth)
anim.addUpdateListener { valueAnimator ->
val layoutParams = activity.binding.commentInput.layoutParams
layoutParams.width = valueAnimator.animatedValue as Int
activity.binding.commentInput.layoutParams = layoutParams
}
anim.duration = 300
anim.start()
anim.doOnEnd {
activity.binding.commentLabel.visibility = View.VISIBLE
activity.binding.commentSend.visibility = View.VISIBLE
activity.binding.commentLabel.animate().translationX(0f).setDuration(300)
.start()
activity.binding.commentSend.animate().translationX(0f).setDuration(300).start()
}
}
activity.binding.commentLabel.setOnClickListener {
//alert dialog to enter a number, with a cancel and ok button
val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup)
.setTitle("Enter a chapter/episode number tag")
.setView(R.layout.dialog_edittext)
.setPositiveButton("OK") { dialog, _ ->
val editText =
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText)
val text = editText?.text.toString()
tag = text.toIntOrNull()
if (tag == null) {
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_off_24,
null
)
} else {
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_24,
null
)
}
dialog.dismiss()
}
.setNeutralButton("Clear") { dialog, _ ->
tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_off_24,
null
)
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_off_24,
null
)
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
}
}
activity.binding.commentSend.setOnClickListener {
if (CommentsAPI.isBanned) {
snackString("You are banned from commenting :(")
return@setOnClickListener
}
if (PrefManager.getVal(PrefName.FirstComment)) {
showCommentRulesDialog()
} else {
processComment()
}
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onStart() {
super.onStart()
}
@SuppressLint("NotifyDataSetChanged")
override fun onResume() {
super.onResume()
tag = null
section.groups.forEach {
if (it is CommentItem && it.containsGif()) {
it.notifyChanged()
}
}
}
enum class InteractionState {
NONE, EDIT, REPLY
}
/**
* Loads and displays the comments
* Called when the activity is created
* Or when the user refreshes the comments
*/
private suspend fun loadAndDisplayComments() {
binding.commentsProgressBar.visibility = View.VISIBLE
binding.commentsList.visibility = View.GONE
adapter.clear()
section.clear()
val comments = withContext(Dispatchers.IO) {
CommentsAPI.getCommentsForId(
mediaId,
tag = filterTag,
sort = PrefManager.getVal(PrefName.CommentSortOrder, "newest")
)
}
val sortedComments = sortComments(comments?.comments)
sortedComments.forEach {
withContext(Dispatchers.Main) {
section.add(
CommentItem(
it,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
0
)
)
}
}
totalPages = comments?.totalPages ?: 1
binding.commentsProgressBar.visibility = View.GONE
binding.commentsList.visibility = View.VISIBLE
adapter.add(section)
}
private suspend fun loadSingleComment(commentId: Int) {
binding.commentsProgressBar.visibility = View.VISIBLE
binding.commentsList.visibility = View.GONE
adapter.clear()
section.clear()
val comment = withContext(Dispatchers.IO) {
CommentsAPI.getSingleComment(commentId)
}
if (comment != null) {
withContext(Dispatchers.Main) {
section.add(
CommentItem(
comment,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
0
)
)
}
}
binding.commentsProgressBar.visibility = View.GONE
binding.commentsList.visibility = View.VISIBLE
adapter.add(section)
}
private fun sortComments(comments: List<Comment>?): List<Comment> {
if (comments == null) return emptyList()
return when (PrefManager.getVal(PrefName.CommentSortOrder, "newest")) {
"newest" -> comments.sortedByDescending { CommentItem.timestampToMillis(it.timestamp) }
"oldest" -> comments.sortedBy { CommentItem.timestampToMillis(it.timestamp) }
"highest_rated" -> comments.sortedByDescending { it.upvotes - it.downvotes }
"lowest_rated" -> comments.sortedBy { it.upvotes - it.downvotes }
else -> comments
}
}
/**
* Resets the old state of the comment input
* @return the old state
*/
private fun resetOldState(): InteractionState {
val oldState = interactionState
interactionState = InteractionState.NONE
return when (oldState) {
InteractionState.EDIT -> {
activity.binding.commentReplyToContainer.visibility = View.GONE
activity.binding.commentInput.setText("")
val imm = activity.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(activity.binding.commentInput.windowToken, 0)
commentWithInteraction?.editing(false)
InteractionState.EDIT
}
InteractionState.REPLY -> {
activity.binding.commentReplyToContainer.visibility = View.GONE
activity.binding.commentInput.setText("")
val imm = activity.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(activity.binding.commentInput.windowToken, 0)
commentWithInteraction?.replying(false)
InteractionState.REPLY
}
else -> {
InteractionState.NONE
}
}
}
/**
* Callback from the comment item to edit the comment
* Called every time the edit button is clicked
* @param comment the comment to edit
*/
fun editCallback(comment: CommentItem) {
if (resetOldState() == InteractionState.EDIT) return
commentWithInteraction = comment
activity.binding.commentInput.setText(comment.comment.content)
activity.binding.commentInput.requestFocus()
activity.binding.commentInput.setSelection(activity.binding.commentInput.text.length)
val imm = activity.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(activity.binding.commentInput, InputMethodManager.SHOW_IMPLICIT)
interactionState = InteractionState.EDIT
}
/**
* Callback from the comment item to reply to the comment
* Called every time the reply button is clicked
* @param comment the comment to reply to
*/
fun replyCallback(comment: CommentItem) {
if (resetOldState() == InteractionState.REPLY) return
commentWithInteraction = comment
activity.binding.commentReplyToContainer.visibility = View.VISIBLE
activity.binding.commentInput.requestFocus()
activity.binding.commentInput.setSelection(activity.binding.commentInput.text.length)
val imm = activity.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(activity.binding.commentInput, InputMethodManager.SHOW_IMPLICIT)
interactionState = InteractionState.REPLY
}
@SuppressLint("SetTextI18n")
fun replyTo(comment: CommentItem, username: String) {
if (comment.isReplying) {
activity.binding.commentReplyToContainer.visibility = View.VISIBLE
activity.binding.commentReplyTo.text = "Replying to $username"
activity.binding.commentReplyToCancel.setOnClickListener {
comment.replying(false)
replyCallback(comment)
activity.binding.commentReplyToContainer.visibility = View.GONE
}
} else {
activity.binding.commentReplyToContainer.visibility = View.GONE
}
}
/**
* Callback from the comment item to view the replies to the comment
* @param comment the comment to view the replies of
*/
fun viewReplyCallback(comment: CommentItem) {
lifecycleScope.launch {
val replies = withContext(Dispatchers.IO) {
CommentsAPI.getRepliesFromId(comment.comment.commentId)
}
replies?.comments?.forEach {
val depth =
if (comment.commentDepth + 1 > comment.MAX_DEPTH) comment.commentDepth else comment.commentDepth + 1
val section =
if (comment.commentDepth + 1 > comment.MAX_DEPTH) comment.parentSection else comment.repliesSection
if (depth >= comment.MAX_DEPTH) comment.registerSubComment(it.commentId)
val newCommentItem = CommentItem(
it,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
depth
)
section.add(newCommentItem)
}
}
}
/**
* Shows the comment rules dialog
* Called when the user tries to comment for the first time
*/
private fun showCommentRulesDialog() {
val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup)
.setTitle("Commenting Rules")
.setMessage(
"I WILL BAN YOU WITHOUT HESITATION\n" +
"1. No racism\n" +
"2. No hate speech\n" +
"3. No spam\n" +
"4. No NSFW content\n" +
"6. ENGLISH ONLY\n" +
"7. No self promotion\n" +
"8. No impersonation\n" +
"9. No harassment\n" +
"10. No illegal content\n" +
"11. Anything you know you shouldn't comment\n"
)
.setPositiveButton("I Understand") { dialog, _ ->
dialog.dismiss()
PrefManager.setVal(PrefName.FirstComment, false)
processComment()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
}
private fun processComment() {
val commentText = activity.binding.commentInput.text.toString()
if (commentText.isEmpty()) {
snackString("Comment cannot be empty")
return
}
activity.binding.commentInput.text.clear()
lifecycleScope.launch {
if (interactionState == InteractionState.EDIT) {
handleEditComment(commentText)
} else {
handleNewComment(commentText)
tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_off_24,
null
)
}
resetOldState()
}
}
private suspend fun handleEditComment(commentText: String) {
val success = withContext(Dispatchers.IO) {
CommentsAPI.editComment(
commentWithInteraction?.comment?.commentId ?: return@withContext false, commentText
)
}
if (success) {
updateCommentInSection(commentText)
}
}
private fun updateCommentInSection(commentText: String) {
val groups = section.groups
groups.forEach { item ->
if (item is CommentItem && item.comment.commentId == commentWithInteraction?.comment?.commentId) {
updateCommentItem(item, commentText)
snackString("Comment edited")
}
}
}
private fun updateCommentItem(item: CommentItem, commentText: String) {
item.comment.content = commentText
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
item.comment.timestamp = dateFormat.format(System.currentTimeMillis())
item.notifyChanged()
}
/**
* Handles the new user-added comment
* @param commentText the text of the comment
*/
private suspend fun handleNewComment(commentText: String) {
val success = withContext(Dispatchers.IO) {
CommentsAPI.comment(
mediaId,
if (interactionState == InteractionState.REPLY) commentWithInteraction?.comment?.commentId else null,
commentText,
tag
)
}
success?.let {
if (interactionState == InteractionState.REPLY) {
if (commentWithInteraction == null) return@let
val section =
if (commentWithInteraction!!.commentDepth + 1 > commentWithInteraction!!.MAX_DEPTH) commentWithInteraction?.parentSection else commentWithInteraction?.repliesSection
val depth =
if (commentWithInteraction!!.commentDepth + 1 > commentWithInteraction!!.MAX_DEPTH) commentWithInteraction!!.commentDepth else commentWithInteraction!!.commentDepth + 1
if (depth >= commentWithInteraction!!.MAX_DEPTH) commentWithInteraction!!.registerSubComment(
it.commentId
)
section?.add(
if (commentWithInteraction!!.commentDepth + 1 > commentWithInteraction!!.MAX_DEPTH) 0 else section.itemCount,
CommentItem(
it,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
depth
)
)
} else {
section.add(
0,
CommentItem(
it,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
0
)
)
}
}
}
}

View file

@ -10,7 +10,7 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.util.LruCache import android.util.LruCache
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.snackString import ani.dantotsu.snackString
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@ -32,8 +32,8 @@ data class ImageData(
try { try {
// Fetch the image // Fetch the image
val response = httpSource.getImage(page) val response = httpSource.getImage(page)
logger("Response: ${response.code}") Logger.log("Response: ${response.code}")
logger("Response: ${response.message}") Logger.log("Response: ${response.message}")
// Convert the Response to an InputStream // Convert the Response to an InputStream
val inputStream = response.body.byteStream() val inputStream = response.body.byteStream()
@ -47,7 +47,7 @@ data class ImageData(
return@withContext bitmap return@withContext bitmap
} catch (e: Exception) { } catch (e: Exception) {
// Handle any exceptions // Handle any exceptions
logger("An error occurred: ${e.message}") Logger.log("An error occurred: ${e.message}")
snackString("An error occurred: ${e.message}") snackString("An error occurred: ${e.message}")
return@withContext null return@withContext null
} }

View file

@ -13,6 +13,7 @@ data class MangaChapter(
var description: String? = null, var description: String? = null,
var sChapter: SChapter, var sChapter: SChapter,
val scanlator: String? = null, val scanlator: String? = null,
val date: Long? = null,
var progress: String? = "" var progress: String? = ""
) : Serializable { ) : Serializable {
constructor(chapter: MangaChapter) : this( constructor(chapter: MangaChapter) : this(
@ -21,7 +22,8 @@ data class MangaChapter(
chapter.title, chapter.title,
chapter.description, chapter.description,
chapter.sChapter, chapter.sChapter,
chapter.scanlator chapter.scanlator,
chapter.date
) )
private val images = mutableListOf<MangaImage>() private val images = mutableListOf<MangaImage>()

View file

@ -1,5 +1,6 @@
package ani.dantotsu.media.manga package ani.dantotsu.media.manga
import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
@ -16,6 +17,9 @@ import ani.dantotsu.databinding.ItemChapterListBinding
import ani.dantotsu.databinding.ItemEpisodeCompactBinding import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -258,6 +262,7 @@ class MangaChapterAdapter(
} }
} }
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) { when (holder) {
is ChapterCompactViewHolder -> { is ChapterCompactViewHolder -> {
@ -290,6 +295,23 @@ class MangaChapterAdapter(
holder.bind(ep.number, ep.progress) holder.bind(ep.number, ep.progress)
setAnimation(fragment.requireContext(), holder.binding.root) setAnimation(fragment.requireContext(), holder.binding.root)
binding.itemChapterNumber.text = ep.number binding.itemChapterNumber.text = ep.number
if (ep.date != null) {
binding.itemChapterDateLayout.visibility = View.VISIBLE
binding.itemChapterDate.text = formatDate(ep.date)
}
if (ep.scanlator != null) {
binding.itemChapterDateLayout.visibility = View.VISIBLE
binding.itemChapterScan.text = ep.scanlator.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.ROOT
) else it.toString()
}
}
if (formatDate(ep.date) == "" || ep.scanlator == null) {
binding.itemChapterDateDivider.visibility = View.GONE
} else binding.itemChapterDateDivider.visibility = View.VISIBLE
if (ep.progress.isNullOrEmpty()) { if (ep.progress.isNullOrEmpty()) {
binding.itemChapterTitle.visibility = View.GONE binding.itemChapterTitle.visibility = View.GONE
} else binding.itemChapterTitle.visibility = View.VISIBLE } else binding.itemChapterTitle.visibility = View.VISIBLE
@ -322,6 +344,33 @@ class MangaChapterAdapter(
fun updateType(t: Int) { fun updateType(t: Int) {
type = t type = t
} }
private fun formatDate(timestamp: Long?): String {
timestamp ?: return "" // Return empty string if timestamp is null
val targetDate = Date(timestamp)
if (targetDate < Date(946684800000L)) { // January 1, 2000 (who want dates before that?)
return ""
}
val currentDate = Date()
val difference = currentDate.time - targetDate.time
return when (val daysDifference = difference / (1000 * 60 * 60 * 24)) {
0L -> {
val hoursDifference = difference / (1000 * 60 * 60)
val minutesDifference = (difference / (1000 * 60)) % 60
when {
hoursDifference > 0 -> "$hoursDifference hour${if (hoursDifference > 1) "s" else ""} ago"
minutesDifference > 0 -> "$minutesDifference minute${if (minutesDifference > 1) "s" else ""} ago"
else -> "Just now"
}
}
1L -> "1 day ago"
in 2..6 -> "$daysDifference days ago"
else -> SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(targetDate)
}
}
} }

View file

@ -12,6 +12,7 @@ import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.NumberPicker import android.widget.NumberPicker
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
@ -27,11 +28,11 @@ import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.MangaReadSources import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
@ -63,6 +64,12 @@ class MangaReadAdapter(
_binding = binding _binding = binding
binding.sourceTitle.setText(R.string.chaps) binding.sourceTitle.setText(R.string.chaps)
//Fuck u launch
binding.faqbutton.setOnClickListener {
val intent = Intent(fragment.requireContext(), FAQActivity::class.java)
startActivity(fragment.requireContext(), intent, null)
}
//Wrong Title //Wrong Title
binding.animeSourceSearch.setOnClickListener { binding.animeSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show( SourceSearchDialogFragment().show(
@ -142,7 +149,8 @@ class MangaReadAdapter(
R.drawable.ic_round_notifications_none_24, R.drawable.ic_round_notifications_none_24,
R.color.bg_opp, R.color.bg_opp,
R.color.violet_400, R.color.violet_400,
fragment.subscribed fragment.subscribed,
true
) { ) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString()) fragment.onNotificationPressed(it, binding.animeSource.text.toString())
} }
@ -150,7 +158,7 @@ class MangaReadAdapter(
subscribeButton(false) subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener { binding.animeSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(), getChannelId(true, media.id)) openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
} }
binding.animeNestedButton.setOnClickListener { binding.animeNestedButton.setOnClickListener {
@ -245,17 +253,41 @@ class MangaReadAdapter(
if (options.count() > 1) View.VISIBLE else View.GONE if (options.count() > 1) View.VISIBLE else View.GONE
dialogBinding.scanlatorNo.text = "${options.count()}" dialogBinding.scanlatorNo.text = "${options.count()}"
dialogBinding.animeScanlatorTop.setOnClickListener { dialogBinding.animeScanlatorTop.setOnClickListener {
val dialogView2 = val dialogView2 = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null) val checkboxContainer = dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
val checkboxContainer = val tickAllButton = dialogView2.findViewById<ImageButton>(R.id.toggleButton)
dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
// Function to get the right image resource for the toggle button
fun getToggleImageResource(container: ViewGroup): Int {
var allChecked = true
var allUnchecked = true
for (i in 0 until container.childCount) {
val checkBox = container.getChildAt(i) as CheckBox
if (!checkBox.isChecked) {
allChecked = false
} else {
allUnchecked = false
}
}
return when {
allChecked -> R.drawable.untick_all_boxes
allUnchecked -> R.drawable.tick_all_boxes
else -> R.drawable.invert_all_boxes
}
}
// Dynamically add checkboxes // Dynamically add checkboxes
options.forEach { option -> options.forEach { option ->
val checkBox = CheckBox(currContext()).apply { val checkBox = CheckBox(currContext()).apply {
text = option text = option
setOnCheckedChangeListener { _, _ ->
// Update image resource when you change a checkbox
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
}
} }
//set checked if it's already selected
// Set checked if its already selected
if (media.selected!!.scanlators != null) { if (media.selected!!.scanlators != null) {
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
scanlatorSelectionListener?.onScanlatorsSelected() scanlatorSelectionListener?.onScanlatorsSelected()
@ -269,7 +301,6 @@ class MangaReadAdapter(
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup) val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
.setView(dialogView2) .setView(dialogView2)
.setPositiveButton("OK") { _, _ -> .setPositiveButton("OK") { _, _ ->
//add unchecked to hidden
hiddenScanlators.clear() hiddenScanlators.clear()
for (i in 0 until checkboxContainer.childCount) { for (i in 0 until checkboxContainer.childCount) {
val checkBox = checkboxContainer.getChildAt(i) as CheckBox val checkBox = checkboxContainer.getChildAt(i) as CheckBox
@ -283,6 +314,21 @@ class MangaReadAdapter(
.setNegativeButton("Cancel", null) .setNegativeButton("Cancel", null)
.show() .show()
dialog.window?.setDimAmount(0.8f) dialog.window?.setDimAmount(0.8f)
// Standard image resource
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
// Listens to ticked checkboxes and changes image resource accordingly
tickAllButton.setOnClickListener {
// Toggle checkboxes
for (i in 0 until checkboxContainer.childCount) {
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
checkBox.isChecked = !checkBox.isChecked
}
// Update image resource
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
}
} }
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup) nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
@ -391,7 +437,7 @@ class MangaReadAdapter(
if (media.manga?.chapters != null) { if (media.manga?.chapters != null) {
val chapters = media.manga.chapters!!.keys.toTypedArray() val chapters = media.manga.chapters!!.keys.toTypedArray()
val anilistEp = (media.userProgress ?: 0).plus(1) val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = PrefManager.getCustomVal<String?>("${media.id}_current_chp", null) val appEp = PrefManager.getNullableCustomVal("${media.id}_current_chp", null, String::class.java)
?.toIntOrNull() ?: 1 ?.toIntOrNull() ?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString() var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
val filteredChapters = chapters.filter { chapterKey -> val filteredChapters = chapters.filter { chapterKey ->
@ -435,13 +481,17 @@ class MangaReadAdapter(
binding.animeSourceContinue.visibility = View.GONE binding.animeSourceContinue.visibility = View.GONE
} }
binding.animeSourceProgressBar.visibility = View.GONE binding.animeSourceProgressBar.visibility = View.GONE
if (media.manga.chapters!!.isNotEmpty()) if (media.manga.chapters!!.isNotEmpty()) {
binding.animeSourceNotFound.visibility = View.GONE binding.animeSourceNotFound.visibility = View.GONE
else binding.faqbutton.visibility = View.GONE
} else {
binding.animeSourceNotFound.visibility = View.VISIBLE binding.animeSourceNotFound.visibility = View.VISIBLE
binding.faqbutton.visibility = View.VISIBLE
}
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE binding.animeSourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE
clearChips() clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE binding.animeSourceProgressBar.visibility = View.VISIBLE
} }

View file

@ -46,13 +46,9 @@ import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.subcriptions.Notifications import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
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.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -347,16 +343,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
var subscribed = false var subscribed = false
fun onNotificationPressed(subscribed: Boolean, source: String) { fun onNotificationPressed(subscribed: Boolean, source: String) {
this.subscribed = subscribed this.subscribed = subscribed
saveSubscription(requireContext(), media, subscribed) saveSubscription(media, subscribed)
if (!subscribed)
Notifications.deleteChannel(requireContext(), getChannelId(true, media.id))
else
Notifications.createChannel(
requireContext(),
MANGA_GROUP,
getChannelId(true, media.id),
media.userPreferredName
)
snackString( snackString(
if (subscribed) getString(R.string.subscribed_notification, source) if (subscribed) getString(R.string.subscribed_notification, source)
else getString(R.string.unsubscribed_notification) else getString(R.string.unsubscribed_notification)
@ -372,11 +359,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try { activity.tabLayout.setVisibility(visibility)
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 = activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE if (show) View.GONE else View.VISIBLE
} }
@ -605,6 +588,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
super.onResume() super.onResume()
binding.mediaInfoProgressBar.visibility = progress binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state) binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
requireActivity().setNavigationTheme()
} }
override fun onPause() { override fun onPause() {

View file

@ -88,6 +88,7 @@ class MangaReaderActivity : AppCompatActivity() {
private var isContVisible = false private var isContVisible = false
private var showProgressDialog = true private var showProgressDialog = true
private var hidescrollbar = false
private var maxChapterPage = 0L private var maxChapterPage = 0L
private var currentChapterPage = 0L private var currentChapterPage = 0L
@ -121,8 +122,11 @@ class MangaReaderActivity : AppCompatActivity() {
} }
} }
private fun hideBars() { private fun hideSystemBars() {
if (!PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) hideSystemBars() if (PrefManager.getVal<Boolean>(PrefName.ShowSystemBars))
showSystemBarsRetractView()
else
hideSystemBarsExtendView()
} }
override fun onDestroy() { override fun onDestroy() {
@ -147,12 +151,26 @@ class MangaReaderActivity : AppCompatActivity() {
defaultSettings = loadReaderSettings("reader_settings") ?: defaultSettings defaultSettings = loadReaderSettings("reader_settings") ?: defaultSettings
onBackPressedDispatcher.addCallback(this) { onBackPressedDispatcher.addCallback(this) {
progress { finish() } val chapter = (MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
?.minus(1L) ?: 0).toString()
if (chapter == "0.0" && PrefManager.getVal(PrefName.ChapterZeroReader)
// Not asking individually or incognito
&& !showProgressDialog && !PrefManager.getVal<Boolean>(PrefName.Incognito)
// Not ...opted out ...already? Somehow?
&& PrefManager.getCustomVal("${media.id}_save_progress", true)
// Allowing Doujin updates or not one
&& if (media.isAdult) PrefManager.getVal(PrefName.UpdateForHReader) else true
) {
updateProgress(media, chapter)
finish()
} else {
progress { finish() }
}
} }
controllerDuration = (PrefManager.getVal<Float>(PrefName.AnimationSpeed) * 200).toLong() controllerDuration = (PrefManager.getVal<Float>(PrefName.AnimationSpeed) * 200).toLong()
hideBars() hideSystemBars()
var pageSliderTimer = Timer() var pageSliderTimer = Timer()
fun pageSliderHide() { fun pageSliderHide() {
@ -285,16 +303,26 @@ class MangaReaderActivity : AppCompatActivity() {
binding.mangaReaderNextChapter.performClick() binding.mangaReaderNextChapter.performClick()
} }
binding.mangaReaderNextChapter.setOnClickListener { binding.mangaReaderNextChapter.setOnClickListener {
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) } if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
else snackString(getString(R.string.next_chapter_not_found)) if (currentChapterIndex > 0) change(currentChapterIndex - 1)
else snackString(getString(R.string.first_chapter))
} else {
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
else snackString(getString(R.string.next_chapter_not_found))
}
} }
//Prev Chapter //Prev Chapter
binding.mangaReaderPrevChap.setOnClickListener { binding.mangaReaderPrevChap.setOnClickListener {
binding.mangaReaderPreviousChapter.performClick() binding.mangaReaderPreviousChapter.performClick()
} }
binding.mangaReaderPreviousChapter.setOnClickListener { binding.mangaReaderPreviousChapter.setOnClickListener {
if (currentChapterIndex > 0) change(currentChapterIndex - 1) if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
else snackString(getString(R.string.first_chapter)) if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
else snackString(getString(R.string.next_chapter_not_found))
} else {
if (currentChapterIndex > 0) change(currentChapterIndex - 1)
else snackString(getString(R.string.first_chapter))
}
} }
model.getMangaChapter().observe(this) { chap -> model.getMangaChapter().observe(this) { chap ->
@ -305,10 +333,17 @@ class MangaReaderActivity : AppCompatActivity() {
PrefManager.setCustomVal("${media.id}_current_chp", chap.number) PrefManager.setCustomVal("${media.id}_current_chp", chap.number)
currentChapterIndex = chaptersArr.indexOf(chap.number) currentChapterIndex = chaptersArr.indexOf(chap.number)
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex) binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
binding.mangaReaderNextChap.text = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: "" binding.mangaReaderNextChap.text =
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: "" binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
} else {
binding.mangaReaderNextChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
}
applySettings() applySettings()
val context = this val context = this
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode) val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
@ -377,7 +412,7 @@ class MangaReaderActivity : AppCompatActivity() {
fun applySettings() { fun applySettings() {
saveReaderSettings("${media.id}_current_settings", defaultSettings) saveReaderSettings("${media.id}_current_settings", defaultSettings)
hideBars() hideSystemBars()
//true colors //true colors
SubsamplingScaleImageView.setPreferredBitmapConfig( SubsamplingScaleImageView.setPreferredBitmapConfig(
@ -422,6 +457,10 @@ class MangaReaderActivity : AppCompatActivity() {
if ((defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == BOTTOM_TO_TOP)) { if ((defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == BOTTOM_TO_TOP)) {
binding.mangaReaderSwipy.vertical = true binding.mangaReaderSwipy.vertical = true
if (defaultSettings.direction == TOP_TO_BOTTOM) { if (defaultSettings.direction == TOP_TO_BOTTOM) {
binding.mangaReaderNextChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
@ -433,6 +472,10 @@ class MangaReaderActivity : AppCompatActivity() {
binding.mangaReaderNextChapter.performClick() binding.mangaReaderNextChapter.performClick()
} }
} else { } else {
binding.mangaReaderNextChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
@ -459,27 +502,26 @@ class MangaReaderActivity : AppCompatActivity() {
} else { } else {
binding.mangaReaderSwipy.vertical = false binding.mangaReaderSwipy.vertical = false
if (defaultSettings.direction == RIGHT_TO_LEFT) { if (defaultSettings.direction == RIGHT_TO_LEFT) {
binding.mangaReaderNextChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onLeftSwiped = {
binding.mangaReaderNextChapter.performClick()
}
binding.mangaReaderSwipy.onRightSwiped = {
binding.mangaReaderPreviousChapter.performClick()
}
} else { } else {
binding.mangaReaderNextChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onLeftSwiped = { }
binding.mangaReaderPreviousChapter.performClick() binding.mangaReaderSwipy.onLeftSwiped = {
} binding.mangaReaderPreviousChapter.performClick()
binding.mangaReaderSwipy.onRightSwiped = {
binding.mangaReaderNextChapter.performClick()
}
} }
binding.mangaReaderSwipy.leftBeingSwiped = { value -> binding.mangaReaderSwipy.leftBeingSwiped = { value ->
binding.LeftSwipeContainer.apply { binding.LeftSwipeContainer.apply {
@ -487,6 +529,9 @@ class MangaReaderActivity : AppCompatActivity() {
translationX = -width.dp * (1 - min(value, 1f)) translationX = -width.dp * (1 - min(value, 1f))
} }
} }
binding.mangaReaderSwipy.onRightSwiped = {
binding.mangaReaderNextChapter.performClick()
}
binding.mangaReaderSwipy.rightBeingSwiped = { value -> binding.mangaReaderSwipy.rightBeingSwiped = { value ->
binding.RightSwipeContainer.apply { binding.RightSwipeContainer.apply {
alpha = value alpha = value
@ -710,7 +755,7 @@ class MangaReaderActivity : AppCompatActivity() {
val screenWidth = Resources.getSystem().displayMetrics.widthPixels val screenWidth = Resources.getSystem().displayMetrics.widthPixels
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left //if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
if (screenWidth / 5 in x + 1..<y) { if (screenWidth / 5 in x + 1..<y) {
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT) { pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
pressPos.RIGHT pressPos.RIGHT
} else { } else {
pressPos.LEFT pressPos.LEFT
@ -718,7 +763,7 @@ class MangaReaderActivity : AppCompatActivity() {
} }
//if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right //if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right
else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) { else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) {
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT) { pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
pressPos.LEFT pressPos.LEFT
} else { } else {
pressPos.RIGHT pressPos.RIGHT
@ -751,9 +796,41 @@ class MangaReaderActivity : AppCompatActivity() {
} }
if (!PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) { if (!PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) {
hideBars() hideSystemBars()
checkNotch() checkNotch()
} }
// Hide the scrollbar completely
if (defaultSettings.hideScrollBar) {
binding.mangaReaderSliderContainer.visibility = View.GONE
} else {
if (defaultSettings.horizontalScrollBar) {
binding.mangaReaderSliderContainer.updateLayoutParams {
height = ViewGroup.LayoutParams.WRAP_CONTENT
width = ViewGroup.LayoutParams.WRAP_CONTENT
}
binding.mangaReaderSlider.apply {
updateLayoutParams<ViewGroup.MarginLayoutParams> {
width = ViewGroup.LayoutParams.MATCH_PARENT
}
rotation = 0f
}
} else {
binding.mangaReaderSliderContainer.updateLayoutParams {
height = ViewGroup.LayoutParams.MATCH_PARENT
width = 48f.px
}
binding.mangaReaderSlider.apply {
updateLayoutParams {
width = binding.mangaReaderSliderContainer.height - 16f.px
}
rotation = 90f
}
}
binding.mangaReaderSliderContainer.visibility = View.VISIBLE
}
//horizontal scrollbar //horizontal scrollbar
if (defaultSettings.horizontalScrollBar) { if (defaultSettings.horizontalScrollBar) {
binding.mangaReaderSliderContainer.updateLayoutParams { binding.mangaReaderSliderContainer.updateLayoutParams {
@ -877,7 +954,7 @@ class MangaReaderActivity : AppCompatActivity() {
dialog.dismiss() dialog.dismiss()
runnable.run() runnable.run()
} }
.setOnCancelListener { hideBars() } .setOnCancelListener { hideSystemBars() }
.create() .create()
.show() .show()
} else { } else {

View file

@ -127,6 +127,12 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
activity.applySettings() activity.applySettings()
} }
binding.readerHideScrollBar.isChecked = settings.hideScrollBar
binding.readerHideScrollBar.setOnCheckedChangeListener { _, isChecked ->
settings.hideScrollBar = isChecked
activity.applySettings()
}
binding.readerHidePageNumbers.isChecked = settings.hidePageNumbers binding.readerHidePageNumbers.isChecked = settings.hidePageNumbers
binding.readerHidePageNumbers.setOnCheckedChangeListener { _, isChecked -> binding.readerHidePageNumbers.setOnCheckedChangeListener { _, isChecked ->
settings.hidePageNumbers = isChecked settings.hidePageNumbers = isChecked

View file

@ -31,6 +31,7 @@ import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.novel.novelreader.NovelReaderActivity import ani.dantotsu.media.novel.novelreader.NovelReaderActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -59,7 +60,7 @@ class NovelReadFragment : Fragment(),
var loaded = false var loaded = false
override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) { override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) {
Log.e("downloadTrigger", novelDownloadPackage.link) Logger.log("novel link: ${novelDownloadPackage.link}")
val downloadTask = NovelDownloaderService.DownloadTask( val downloadTask = NovelDownloaderService.DownloadTask(
title = media.mainName(), title = media.mainName(),
chapter = novelDownloadPackage.novelName, chapter = novelDownloadPackage.novelName,

View file

@ -5,12 +5,14 @@ import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.databinding.ItemNovelResponseBinding import ani.dantotsu.databinding.ItemNovelResponseBinding
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
@ -58,11 +60,11 @@ class NovelResponseAdapter(
} }
if (binding.itemEpisodeFiller.text.contains("Downloading")) { if (binding.itemEpisodeFiller.text.contains("Downloading")) {
binding.itemEpisodeFiller.setTextColor( binding.itemEpisodeFiller.setTextColor(
fragment.requireContext().getColor(android.R.color.holo_blue_light) ContextCompat.getColor(fragment.requireContext(), android.R.color.holo_blue_light)
) )
} else if (binding.itemEpisodeFiller.text.contains("Downloaded")) { } else if (binding.itemEpisodeFiller.text.contains("Downloaded")) {
binding.itemEpisodeFiller.setTextColor( binding.itemEpisodeFiller.setTextColor(
fragment.requireContext().getColor(android.R.color.holo_green_light) ContextCompat.getColor(fragment.requireContext(), android.R.color.holo_green_light)
) )
} else { } else {
binding.itemEpisodeFiller.setTextColor(color) binding.itemEpisodeFiller.setTextColor(color)
@ -181,7 +183,7 @@ class NovelResponseAdapter(
if (position != -1) { if (position != -1) {
list[position].extra?.remove("0") list[position].extra?.remove("0")
list[position].extra?.set("0", "Downloading: $progress%") list[position].extra?.set("0", "Downloading: $progress%")
Log.d("NovelResponseAdapter", "updateDownloadProgress: $progress, position: $position") Logger.log( "updateDownloadProgress: $progress, position: $position")
notifyItemChanged(position) notifyItemChanged(position)
} }
} }

View file

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

View file

@ -78,7 +78,6 @@ class ListActivity : AppCompatActivity() {
) )
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight topMargin = statusBarHeight
bottomMargin = navBarHeight
} }
} }
setContentView(binding.root) setContentView(binding.root)
@ -171,6 +170,21 @@ class ListActivity : AppCompatActivity() {
popup.show() popup.show()
} }
binding.filter.setOnClickListener {
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList).toMutableSet().sorted()
val popup = PopupMenu(this, it)
popup.menu.add("All")
genres.forEach { genre ->
popup.menu.add(genre)
}
popup.setOnMenuItemClickListener { menuItem ->
val selectedGenre = menuItem.title.toString()
model.filterLists(selectedGenre)
true
}
popup.show()
}
binding.random.setOnClickListener { binding.random.setOnClickListener {
//get the current tab //get the current tab
val currentTab = val currentTab =

View file

@ -13,10 +13,29 @@ class ListViewModel : ViewModel() {
var grid = MutableLiveData(PrefManager.getVal<Boolean>(PrefName.ListGrid)) var grid = MutableLiveData(PrefManager.getVal<Boolean>(PrefName.ListGrid))
private val lists = MutableLiveData<MutableMap<String, ArrayList<Media>>>() private val lists = MutableLiveData<MutableMap<String, ArrayList<Media>>>()
private val unfilteredLists = MutableLiveData<MutableMap<String, ArrayList<Media>>>()
fun getLists(): LiveData<MutableMap<String, ArrayList<Media>>> = lists fun getLists(): LiveData<MutableMap<String, ArrayList<Media>>> = lists
suspend fun loadLists(anime: Boolean, userId: Int, sortOrder: String? = null) { suspend fun loadLists(anime: Boolean, userId: Int, sortOrder: String? = null) {
tryWithSuspend { tryWithSuspend {
lists.postValue(Anilist.query.getMediaLists(anime, userId, sortOrder)) val res = Anilist.query.getMediaLists(anime, userId, sortOrder)
lists.postValue(res)
unfilteredLists.postValue(res)
} }
} }
fun filterLists(genre: String) {
if (genre == "All") {
lists.postValue(unfilteredLists.value)
return
}
val currentLists = unfilteredLists.value ?: return
val filteredLists = currentLists.mapValues { entry ->
entry.value.filter { media ->
genre in media.genres
} as ArrayList<Media>
}.toMutableMap()
lists.postValue(filteredLists)
}
} }

View file

@ -0,0 +1,88 @@
package ani.dantotsu.notifications
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import ani.dantotsu.notifications.anilist.AnilistNotificationReceiver
import ani.dantotsu.notifications.comment.CommentNotificationReceiver
import ani.dantotsu.notifications.TaskScheduler.TaskType
import ani.dantotsu.notifications.subscription.SubscriptionNotificationReceiver
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import java.util.concurrent.TimeUnit
class AlarmManagerScheduler(private val context: Context) : TaskScheduler {
override fun scheduleRepeatingTask(taskType: TaskType, interval: Long) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = when (taskType) {
TaskType.COMMENT_NOTIFICATION -> Intent(
context,
CommentNotificationReceiver::class.java
)
TaskType.ANILIST_NOTIFICATION -> Intent(
context,
AnilistNotificationReceiver::class.java
)
TaskType.SUBSCRIPTION_NOTIFICATION -> Intent(
context,
SubscriptionNotificationReceiver::class.java
)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
taskType.ordinal,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val triggerAtMillis = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(interval)
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerAtMillis,
pendingIntent
)
} else {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent)
}
} catch (e: SecurityException) {
PrefManager.setVal(PrefName.UseAlarmManager, false)
TaskScheduler.create(context, true).cancelAllTasks()
TaskScheduler.create(context, false).scheduleAllTasks(context)
}
}
override fun cancelTask(taskType: TaskType) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = when (taskType) {
TaskType.COMMENT_NOTIFICATION -> Intent(
context,
CommentNotificationReceiver::class.java
)
TaskType.ANILIST_NOTIFICATION -> Intent(
context,
AnilistNotificationReceiver::class.java
)
TaskType.SUBSCRIPTION_NOTIFICATION -> Intent(
context,
SubscriptionNotificationReceiver::class.java
)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
taskType.ordinal,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
}
}

View file

@ -0,0 +1,60 @@
package ani.dantotsu.notifications
import android.app.AlarmManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.notifications.TaskScheduler.TaskType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
class BootCompletedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
val scheduler = AlarmManagerScheduler(context)
PrefManager.init(context)
Logger.init(context)
Logger.log("Starting Dantotsu Subscription Service on Boot")
if (PrefManager.getVal(PrefName.UseAlarmManager)) {
val commentInterval =
CommentNotificationWorker.checkIntervals[PrefManager.getVal(PrefName.CommentNotificationInterval)]
val anilistInterval =
AnilistNotificationWorker.checkIntervals[PrefManager.getVal(PrefName.AnilistNotificationInterval)]
scheduler.scheduleRepeatingTask(
TaskType.COMMENT_NOTIFICATION,
commentInterval
)
scheduler.scheduleRepeatingTask(
TaskType.ANILIST_NOTIFICATION,
anilistInterval
)
}
}
}
}
class AlarmPermissionStateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED) {
PrefManager.init(context)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val canScheduleExactAlarms = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
alarmManager.canScheduleExactAlarms()
} else {
true
}
if (canScheduleExactAlarms) {
TaskScheduler.create(context, false).cancelAllTasks()
TaskScheduler.create(context, true).scheduleAllTasks(context)
} else {
TaskScheduler.create(context, true).cancelAllTasks()
TaskScheduler.create(context, false).scheduleAllTasks(context)
}
PrefManager.setVal(PrefName.UseAlarmManager, canScheduleExactAlarms)
}
}
}

View file

@ -1,4 +1,4 @@
package ani.dantotsu.subcriptions package ani.dantotsu.notifications
import android.app.NotificationManager import android.app.NotificationManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
@ -9,7 +9,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
class NotificationClickReceiver : BroadcastReceiver() { class IncognitoNotificationClickReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
PrefManager.setVal(PrefName.Incognito, false) PrefManager.setVal(PrefName.Incognito, false)

View file

@ -0,0 +1,52 @@
package ani.dantotsu.notifications
import android.content.Context
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.notifications.subscription.SubscriptionNotificationWorker
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
interface TaskScheduler {
fun scheduleRepeatingTask(taskType: TaskType, interval: Long)
fun cancelTask(taskType: TaskType)
fun cancelAllTasks() {
for (taskType in TaskType.entries) {
cancelTask(taskType)
}
}
fun scheduleAllTasks(context: Context) {
for (taskType in TaskType.entries) {
val interval = when (taskType) {
TaskType.COMMENT_NOTIFICATION -> CommentNotificationWorker.checkIntervals[PrefManager.getVal(
PrefName.CommentNotificationInterval)]
TaskType.ANILIST_NOTIFICATION -> AnilistNotificationWorker.checkIntervals[PrefManager.getVal(
PrefName.AnilistNotificationInterval)]
TaskType.SUBSCRIPTION_NOTIFICATION -> SubscriptionNotificationWorker.checkIntervals[PrefManager.getVal(
PrefName.SubscriptionNotificationInterval)]
}
scheduleRepeatingTask(taskType, interval)
}
}
companion object {
fun create(context: Context, useAlarmManager: Boolean): TaskScheduler {
return if (useAlarmManager) {
AlarmManagerScheduler(context)
} else {
WorkManagerScheduler(context)
}
}
}
enum class TaskType {
COMMENT_NOTIFICATION,
ANILIST_NOTIFICATION,
SUBSCRIPTION_NOTIFICATION
}
}
interface Task {
suspend fun execute(context: Context): Boolean
}

View file

@ -0,0 +1,89 @@
package ani.dantotsu.notifications
import android.content.Context
import androidx.work.Constraints
import androidx.work.PeriodicWorkRequest
import ani.dantotsu.notifications.TaskScheduler.TaskType
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.notifications.subscription.SubscriptionNotificationWorker
class WorkManagerScheduler(private val context: Context) : TaskScheduler {
override fun scheduleRepeatingTask(taskType: TaskType, interval: Long) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(androidx.work.NetworkType.CONNECTED)
.build()
when (taskType) {
TaskType.COMMENT_NOTIFICATION -> {
val recurringWork = PeriodicWorkRequest.Builder(
CommentNotificationWorker::class.java,
interval,
java.util.concurrent.TimeUnit.MINUTES,
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS,
java.util.concurrent.TimeUnit.MINUTES
)
.setConstraints(constraints)
.build()
androidx.work.WorkManager.getInstance(context).enqueueUniquePeriodicWork(
CommentNotificationWorker.WORK_NAME,
androidx.work.ExistingPeriodicWorkPolicy.UPDATE,
recurringWork
)
}
TaskType.ANILIST_NOTIFICATION -> {
val recurringWork = PeriodicWorkRequest.Builder(
AnilistNotificationWorker::class.java,
interval,
java.util.concurrent.TimeUnit.MINUTES,
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS,
java.util.concurrent.TimeUnit.MINUTES
)
.setConstraints(constraints)
.build()
androidx.work.WorkManager.getInstance(context).enqueueUniquePeriodicWork(
AnilistNotificationWorker.WORK_NAME,
androidx.work.ExistingPeriodicWorkPolicy.UPDATE,
recurringWork
)
}
TaskType.SUBSCRIPTION_NOTIFICATION -> {
val recurringWork = PeriodicWorkRequest.Builder(
SubscriptionNotificationWorker::class.java,
interval,
java.util.concurrent.TimeUnit.MINUTES,
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS,
java.util.concurrent.TimeUnit.MINUTES
)
.setConstraints(constraints)
.build()
androidx.work.WorkManager.getInstance(context).enqueueUniquePeriodicWork(
SubscriptionNotificationWorker.WORK_NAME,
androidx.work.ExistingPeriodicWorkPolicy.UPDATE,
recurringWork
)
}
}
}
override fun cancelTask(taskType: TaskType) {
when (taskType) {
TaskType.COMMENT_NOTIFICATION -> {
androidx.work.WorkManager.getInstance(context)
.cancelUniqueWork(CommentNotificationWorker.WORK_NAME)
}
TaskType.ANILIST_NOTIFICATION -> {
androidx.work.WorkManager.getInstance(context)
.cancelUniqueWork(AnilistNotificationWorker.WORK_NAME)
}
TaskType.SUBSCRIPTION_NOTIFICATION -> {
androidx.work.WorkManager.getInstance(context)
.cancelUniqueWork(SubscriptionNotificationWorker.WORK_NAME)
}
}
}
}

View file

@ -0,0 +1,26 @@
package ani.dantotsu.notifications.anilist
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import ani.dantotsu.notifications.AlarmManagerScheduler
import ani.dantotsu.notifications.TaskScheduler
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import kotlinx.coroutines.runBlocking
class AnilistNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
Logger.log("AnilistNotificationReceiver: onReceive")
runBlocking {
AnilistNotificationTask().execute(context)
}
val anilistInterval =
AnilistNotificationWorker.checkIntervals[PrefManager.getVal(PrefName.AnilistNotificationInterval)]
AlarmManagerScheduler(context).scheduleRepeatingTask(
TaskScheduler.TaskType.ANILIST_NOTIFICATION,
anilistInterval
)
}
}

View file

@ -0,0 +1,109 @@
package ani.dantotsu.notifications.anilist
import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import ani.dantotsu.MainActivity
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.notifications.Task
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AnilistNotificationTask : Task {
override suspend fun execute(context: Context): Boolean {
try {
withContext(Dispatchers.IO) {
PrefManager.init(context) //make sure prefs are initialized
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
if (userId.isNotEmpty()) {
Anilist.getSavedToken()
val res = Anilist.query.getNotifications(
userId.toInt(),
resetNotification = false
)
val unreadNotificationCount = res?.data?.user?.unreadNotificationCount ?: 0
if (unreadNotificationCount > 0) {
val unreadNotifications =
res?.data?.page?.notifications?.sortedBy { it.id }
?.takeLast(unreadNotificationCount)
val lastId = PrefManager.getVal<Int>(PrefName.LastAnilistNotificationId)
val newNotifications = unreadNotifications?.filter { it.id > lastId }
val filteredTypes =
PrefManager.getVal<Set<String>>(PrefName.AnilistFilteredTypes)
newNotifications?.forEach {
if (!filteredTypes.contains(it.notificationType)) {
val content = ActivityItemBuilder.getContent(it)
val notification = createNotification(context, content, it.id)
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
NotificationManagerCompat.from(context)
.notify(
Notifications.CHANNEL_ANILIST,
System.currentTimeMillis().toInt(),
notification
)
}
}
}
if (newNotifications?.isNotEmpty() == true) {
PrefManager.setVal(
PrefName.LastAnilistNotificationId,
newNotifications.last().id
)
}
}
}
}
return true
} catch (e: Exception) {
Logger.log("AnilistNotificationTask: ${e.message}")
Logger.log(e)
return false
}
}
private fun createNotification(
context: Context,
content: String,
notificationId: Int? = null
): android.app.Notification {
val title = "New Anilist Notification"
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
if (notificationId != null) {
Logger.log("notificationId: $notificationId")
putExtra("activityId", notificationId)
}
}
val pendingIntent = PendingIntent.getActivity(
context,
notificationId ?: 0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(context, Notifications.CHANNEL_ANILIST)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(title)
.setContentText(content)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
}
}

View file

@ -0,0 +1,25 @@
package ani.dantotsu.notifications.anilist
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import ani.dantotsu.util.Logger
class AnilistNotificationWorker(appContext: Context, workerParams: WorkerParameters) :
CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
Logger.log("AnilistNotificationWorker: doWork")
return if (AnilistNotificationTask().execute(applicationContext)) {
Result.success()
} else {
Logger.log("AnilistNotificationWorker: doWork failed")
Result.retry()
}
}
companion object {
val checkIntervals = arrayOf(0L, 30, 60, 120, 240, 360, 720, 1440)
const val WORK_NAME = "ani.dantotsu.notifications.anilist.AnilistNotificationWorker"
}
}

View file

@ -0,0 +1,16 @@
package ani.dantotsu.notifications.comment
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import ani.dantotsu.util.Logger
import kotlinx.coroutines.runBlocking
class CommentNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
Logger.log("CommentNotificationReceiver: onReceive")
runBlocking {
CommentNotificationTask().execute(context)
}
}
}

View file

@ -0,0 +1,316 @@
package ani.dantotsu.notifications.comment
import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.MainActivity
import ani.dantotsu.R
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.notifications.Task
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
class CommentNotificationTask : Task {
override suspend fun execute(context: Context): Boolean {
try {
withContext(Dispatchers.IO) {
PrefManager.init(context) //make sure prefs are initialized
val client = OkHttpClient()
CommentsAPI.fetchAuthToken(client)
val notificationResponse = CommentsAPI.getNotifications(client)
var notifications = notificationResponse?.notifications?.toMutableList()
//if we have at least one reply notification, we need to fetch the media titles
var names = emptyMap<Int, MediaNameFetch.Companion.ReturnedData>()
if (notifications?.any { it.type == 1 || it.type == null } == true) {
val mediaIds =
notifications.filter { it.type == 1 || it.type == null }.map { it.mediaId }
names = MediaNameFetch.fetchMediaTitles(mediaIds)
}
val recentGlobal = PrefManager.getVal<Int>(
PrefName.RecentGlobalNotification
)
notifications =
notifications?.filter { it.type != 3 || it.notificationId > recentGlobal }
?.toMutableList()
val newRecentGlobal =
notifications?.filter { it.type == 3 }?.maxOfOrNull { it.notificationId }
if (newRecentGlobal != null) {
PrefManager.setVal(PrefName.RecentGlobalNotification, newRecentGlobal)
}
if (notifications.isNullOrEmpty()) return@withContext
PrefManager.setVal(
PrefName.UnreadCommentNotifications,
PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications) + (notifications.size
?: 0)
)
notifications.forEach {
val type: CommentNotificationWorker.NotificationType = when (it.type) {
1 -> CommentNotificationWorker.NotificationType.COMMENT_REPLY
2 -> CommentNotificationWorker.NotificationType.COMMENT_WARNING
3 -> CommentNotificationWorker.NotificationType.APP_GLOBAL
420 -> CommentNotificationWorker.NotificationType.NO_NOTIFICATION
else -> CommentNotificationWorker.NotificationType.UNKNOWN
}
val notification = when (type) {
CommentNotificationWorker.NotificationType.COMMENT_WARNING -> {
val title = "You received a warning"
val message = it.content ?: "Be more thoughtful with your comments"
val commentStore = CommentStore(
title,
message,
it.mediaId,
it.commentId
)
addNotificationToStore(commentStore)
createNotification(
context,
CommentNotificationWorker.NotificationType.COMMENT_WARNING,
message,
title,
it.mediaId,
it.commentId,
"",
""
)
}
CommentNotificationWorker.NotificationType.COMMENT_REPLY -> {
val title = "New Comment Reply"
val mediaName = names[it.mediaId]?.title ?: "Unknown"
val message = "${it.username} replied to your comment in $mediaName"
val commentStore = CommentStore(
title,
message,
it.mediaId,
it.commentId
)
addNotificationToStore(commentStore)
createNotification(
context,
CommentNotificationWorker.NotificationType.COMMENT_REPLY,
message,
title,
it.mediaId,
it.commentId,
names[it.mediaId]?.color ?: "#222222",
names[it.mediaId]?.coverImage ?: ""
)
}
CommentNotificationWorker.NotificationType.APP_GLOBAL -> {
val title = "Update from Dantotsu"
val message = it.content ?: "New feature available"
val commentStore = CommentStore(
title,
message,
null,
null
)
addNotificationToStore(commentStore)
createNotification(
context,
CommentNotificationWorker.NotificationType.APP_GLOBAL,
message,
title,
0,
0,
"",
""
)
}
CommentNotificationWorker.NotificationType.NO_NOTIFICATION -> {
PrefManager.removeCustomVal("genre_thumb")
PrefManager.removeCustomVal("banner_ANIME_time")
PrefManager.removeCustomVal("banner_MANGA_time")
PrefManager.setVal(PrefName.ImageUrl, it.content ?: "")
null
}
CommentNotificationWorker.NotificationType.UNKNOWN -> {
null
}
}
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
if (notification != null) {
NotificationManagerCompat.from(context)
.notify(
type.id,
System.currentTimeMillis().toInt(),
notification
)
}
}
}
}
return true
} catch (e: Exception) {
Logger.log("CommentNotificationTask: ${e.message}")
Logger.log(e)
return false
}
}
private fun addNotificationToStore(notification: CommentStore) {
val notificationStore = PrefManager.getNullableVal<List<CommentStore>>(
PrefName.CommentNotificationStore,
null
) ?: listOf()
val newStore = notificationStore.toMutableList()
if (newStore.size > 10) {
newStore.remove(newStore.minByOrNull { it.time })
}
if (newStore.any { it.content == notification.content }) {
return
}
newStore.add(notification)
PrefManager.setVal(PrefName.CommentNotificationStore, newStore)
}
private fun createNotification(
context: Context,
notificationType: CommentNotificationWorker.NotificationType,
message: String,
title: String,
mediaId: Int,
commentId: Int,
color: String,
imageUrl: String
): android.app.Notification? {
Logger.log(
"Creating notification of type $notificationType" +
", message: $message, title: $title, mediaId: $mediaId, commentId: $commentId"
)
val notification = when (notificationType) {
CommentNotificationWorker.NotificationType.COMMENT_WARNING -> {
val intent = Intent(context, MainActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "COMMENTS")
putExtra("mediaId", mediaId)
putExtra("commentId", commentId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context,
commentId,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(context, notificationType.id)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.notification_icon)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
builder.build()
}
CommentNotificationWorker.NotificationType.COMMENT_REPLY -> {
val intent = Intent(context, MainActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "COMMENTS")
putExtra("mediaId", mediaId)
putExtra("commentId", commentId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context,
commentId,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(context, notificationType.id)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.notification_icon)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
if (imageUrl.isNotEmpty()) {
val bitmap = getBitmapFromUrl(imageUrl)
if (bitmap != null) {
builder.setLargeIcon(bitmap)
}
}
if (color.isNotEmpty()) {
builder.color = Color.parseColor(color)
}
builder.build()
}
CommentNotificationWorker.NotificationType.APP_GLOBAL -> {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context,
System.currentTimeMillis().toInt(),
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(context, notificationType.id)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.notification_icon)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
builder.build()
}
else -> {
null
}
}
return notification
}
private fun getBitmapFromVectorDrawable(context: Context, drawableId: Int): Bitmap? {
val drawable = ContextCompat.getDrawable(context, drawableId) ?: return null
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
private fun getBitmapFromUrl(url: String): Bitmap? {
return try {
val inputStream = java.net.URL(url).openStream()
BitmapFactory.decodeStream(inputStream)
} catch (e: Exception) {
null
}
}
}

View file

@ -0,0 +1,34 @@
package ani.dantotsu.notifications.comment
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.data.notification.Notifications
class CommentNotificationWorker(appContext: Context, workerParams: WorkerParameters) :
CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
Logger.log("CommentNotificationWorker: doWork")
return if (CommentNotificationTask().execute(applicationContext)) {
Result.success()
} else {
Logger.log("CommentNotificationWorker: doWork failed")
Result.retry()
}
}
enum class NotificationType(val id: String) {
COMMENT_REPLY(Notifications.CHANNEL_COMMENTS),
COMMENT_WARNING(Notifications.CHANNEL_COMMENT_WARING),
APP_GLOBAL(Notifications.CHANNEL_APP_GLOBAL),
NO_NOTIFICATION("no_notification"),
UNKNOWN("unknown")
}
companion object {
val checkIntervals = arrayOf(0L, 480, 720, 1440)
const val WORK_NAME = "ani.dantotsu.notifications.comment.CommentNotificationWorker"
}
}

View file

@ -0,0 +1,20 @@
package ani.dantotsu.notifications.comment
import kotlinx.serialization.Serializable
@Suppress("INAPPROPRIATE_CONST_NAME")
@Serializable
data class CommentStore(
val title: String,
val content: String,
val mediaId: Int? = null,
val commentId: Int? = null,
val time: Long = System.currentTimeMillis(),
) : java.io.Serializable {
companion object {
@Suppress("INAPPROPRIATE_CONST_NAME")
private const val serialVersionUID = 1L
}
}

View file

@ -0,0 +1,82 @@
package ani.dantotsu.notifications.comment
import ani.dantotsu.client
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class MediaNameFetch {
companion object {
private fun queryBuilder(mediaIds: List<Int>): String {
var query = "{"
mediaIds.forEachIndexed { index, mediaId ->
query += """
media$index: Media(id: $mediaId) {
coverImage {
medium
color
}
id
title {
romaji
}
}
""".trimIndent()
}
query += "}"
return query
}
suspend fun fetchMediaTitles(ids: List<Int>): Map<Int, ReturnedData> {
return try {
val url = "https://graphql.anilist.co/"
val data = mapOf(
"query" to queryBuilder(ids),
)
withContext(Dispatchers.IO) {
val response = client.post(
url,
headers = mapOf(
"Content-Type" to "application/json",
"Accept" to "application/json"
),
data = data
)
val mediaResponse = parseMediaResponseWithGson(response.text)
val mediaMap = mutableMapOf<Int, ReturnedData>()
mediaResponse.data.forEach { (_, mediaItem) ->
mediaMap[mediaItem.id] = ReturnedData(
mediaItem.title.romaji,
mediaItem.coverImage.medium,
mediaItem.coverImage.color
)
}
mediaMap
}
} catch (e: Exception) {
val errorMap = mutableMapOf<Int, ReturnedData>()
ids.forEach { errorMap[it] = ReturnedData("Unknown", "", "#222222") }
errorMap
}
}
private fun parseMediaResponseWithGson(response: String): MediaResponse {
val gson = Gson()
val type = object : TypeToken<MediaResponse>() {}.type
return gson.fromJson(response, type)
}
data class ReturnedData(val title: String, val coverImage: String, val color: String)
data class MediaResponse(val data: Map<String, MediaItem>)
data class MediaItem(
val coverImage: MediaCoverImage,
val id: Int,
val title: MediaTitle
)
data class MediaTitle(val romaji: String)
data class MediaCoverImage(val medium: String, val color: String)
}
}

View file

@ -1,6 +1,5 @@
package ani.dantotsu.subcriptions package ani.dantotsu.notifications.subscription
import android.content.Context
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
@ -17,15 +16,13 @@ import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
class SubscriptionHelper { class SubscriptionHelper {
companion object { companion object {
private fun loadSelected( private fun loadSelected(
context: Context, mediaId: Int
mediaId: Int,
isAdult: Boolean,
isAnime: Boolean
): Selected { ): Selected {
val data = val data =
PrefManager.getNullableCustomVal("${mediaId}-select", null, Selected::class.java) PrefManager.getNullableCustomVal("${mediaId}-select", null, Selected::class.java)
@ -37,26 +34,25 @@ class SubscriptionHelper {
return data return data
} }
private fun saveSelected(context: Context, mediaId: Int, data: Selected) { private fun saveSelected( mediaId: Int, data: Selected) {
PrefManager.setCustomVal("${mediaId}-select", data) PrefManager.setCustomVal("${mediaId}-select", data)
} }
fun getAnimeParser(context: Context, isAdult: Boolean, id: Int): AnimeParser { fun getAnimeParser(id: Int): AnimeParser {
val sources = if (isAdult) HAnimeSources else AnimeSources val sources = AnimeSources
val selected = loadSelected(context, id, isAdult, true) Logger.log("getAnimeParser size: ${sources.list.size}")
val selected = loadSelected(id)
val parser = sources[selected.sourceIndex] val parser = sources[selected.sourceIndex]
parser.selectDub = selected.preferDub parser.selectDub = selected.preferDub
return parser return parser
} }
suspend fun getEpisode( suspend fun getEpisode(
context: Context,
parser: AnimeParser, parser: AnimeParser,
id: Int, id: Int
isAdult: Boolean
): Episode? { ): Episode? {
val selected = loadSelected(context, id, isAdult, true) val selected = loadSelected(id)
val ep = withTimeoutOrNull(10 * 1000) { val ep = withTimeoutOrNull(10 * 1000) {
tryWithSuspend { tryWithSuspend {
val show = parser.loadSavedShowResponse(id) ?: throw Exception( val show = parser.loadSavedShowResponse(id) ?: throw Exception(
@ -76,23 +72,21 @@ class SubscriptionHelper {
return ep?.apply { return ep?.apply {
selected.latest = number.toFloat() selected.latest = number.toFloat()
saveSelected(context, id, selected) saveSelected(id, selected)
} }
} }
fun getMangaParser(context: Context, isAdult: Boolean, id: Int): MangaParser { fun getMangaParser(id: Int): MangaParser {
val sources = if (isAdult) HMangaSources else MangaSources val sources = MangaSources
val selected = loadSelected(context, id, isAdult, false) val selected = loadSelected(id)
return sources[selected.sourceIndex] return sources[selected.sourceIndex]
} }
suspend fun getChapter( suspend fun getChapter(
context: Context,
parser: MangaParser, parser: MangaParser,
id: Int, id: Int
isAdult: Boolean
): MangaChapter? { ): MangaChapter? {
val selected = loadSelected(context, id, isAdult, true) val selected = loadSelected(id)
val chp = withTimeoutOrNull(10 * 1000) { val chp = withTimeoutOrNull(10 * 1000) {
tryWithSuspend { tryWithSuspend {
val show = parser.loadSavedShowResponse(id) ?: throw Exception( val show = parser.loadSavedShowResponse(id) ?: throw Exception(
@ -112,7 +106,7 @@ class SubscriptionHelper {
return chp?.apply { return chp?.apply {
selected.latest = MangaNameAdapter.findChapterNumber(number) ?: 0f selected.latest = MangaNameAdapter.findChapterNumber(number) ?: 0f
saveSelected(context, id, selected) saveSelected(id, selected)
} }
} }
@ -124,21 +118,21 @@ class SubscriptionHelper {
val image: String? val image: String?
) : java.io.Serializable ) : java.io.Serializable
private const val subscriptions = "subscriptions" private const val SUBSCRIPTIONS = "subscriptions"
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun getSubscriptions(): Map<Int, SubscribeMedia> = fun getSubscriptions(): Map<Int, SubscribeMedia> =
(PrefManager.getNullableCustomVal( (PrefManager.getNullableCustomVal(
subscriptions, SUBSCRIPTIONS,
null, null,
Map::class.java Map::class.java
) as? Map<Int, SubscribeMedia>) ) as? Map<Int, SubscribeMedia>)
?: mapOf<Int, SubscribeMedia>().also { PrefManager.setCustomVal(subscriptions, it) } ?: mapOf<Int, SubscribeMedia>().also { PrefManager.setCustomVal(SUBSCRIPTIONS, it) }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun saveSubscription(context: Context, media: Media, subscribed: Boolean) { fun saveSubscription(media: Media, subscribed: Boolean) {
val data = PrefManager.getNullableCustomVal( val data = PrefManager.getNullableCustomVal(
subscriptions, SUBSCRIPTIONS,
null, null,
Map::class.java Map::class.java
) as? MutableMap<Int, SubscribeMedia> ) as? MutableMap<Int, SubscribeMedia>
@ -157,7 +151,7 @@ class SubscriptionHelper {
} else { } else {
data.remove(media.id) data.remove(media.id)
} }
PrefManager.setCustomVal(subscriptions, data) PrefManager.setCustomVal(SUBSCRIPTIONS, data)
} }
} }
} }

View file

@ -0,0 +1,26 @@
package ani.dantotsu.notifications.subscription
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import ani.dantotsu.notifications.AlarmManagerScheduler
import ani.dantotsu.notifications.TaskScheduler
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import kotlinx.coroutines.runBlocking
class SubscriptionNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
Logger.log("SubscriptionNotificationReceiver: onReceive")
runBlocking {
SubscriptionNotificationTask().execute(context)
}
val subscriptionInterval =
SubscriptionNotificationWorker.checkIntervals[PrefManager.getVal(PrefName.SubscriptionNotificationInterval)]
AlarmManagerScheduler(context).scheduleRepeatingTask(
TaskScheduler.TaskType.SUBSCRIPTION_NOTIFICATION,
subscriptionInterval
)
}
}

View file

@ -0,0 +1,222 @@
package ani.dantotsu.notifications.subscription
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import ani.dantotsu.App
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.UrlMedia
import ani.dantotsu.hasNotificationPermission
import ani.dantotsu.notifications.Task
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.Episode
import ani.dantotsu.parsers.MangaChapter
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK_PROGRESS
import eu.kanade.tachiyomi.data.notification.Notifications.ID_SUBSCRIPTION_CHECK_PROGRESS
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SubscriptionNotificationTask : Task {
private var currentlyPerforming = false
@SuppressLint("MissingPermission")
override suspend fun execute(context: Context): Boolean {
if (!currentlyPerforming) {
try {
withContext(Dispatchers.IO) {
PrefManager.init(context)
currentlyPerforming = true
App.context = context
Logger.log("SubscriptionNotificationTask: execute")
var timeout = 15_000L
do {
delay(1000)
timeout -= 1000
} while (timeout > 0 && !AnimeSources.isInitialized && !MangaSources.isInitialized)
Logger.log("SubscriptionNotificationTask: timeout: $timeout")
if (timeout <= 0) {
currentlyPerforming = false
return@withContext
}
val subscriptions = SubscriptionHelper.getSubscriptions()
var i = 0
val index = subscriptions.map { i++; it.key to i }.toMap()
val notificationManager = NotificationManagerCompat.from(context)
val progressEnabled: Boolean =
PrefManager.getVal(PrefName.SubscriptionCheckingNotifications)
val progressNotification = if (progressEnabled) getProgressNotification(
context,
subscriptions.size
) else null
if (progressNotification != null && hasNotificationPermission(context)) {
notificationManager.notify(
ID_SUBSCRIPTION_CHECK_PROGRESS,
progressNotification.build()
)
//Seems like if the parent coroutine scope gets cancelled, the notification stays
//So adding this as a safeguard? dk if this will be useful
CoroutineScope(Dispatchers.Main).launch {
delay(5 * subscriptions.size * 1000L)
notificationManager.cancel(ID_SUBSCRIPTION_CHECK_PROGRESS)
}
}
fun progress(progress: Int, parser: String, media: String) {
if (progressNotification != null && hasNotificationPermission(context))
notificationManager.notify(
ID_SUBSCRIPTION_CHECK_PROGRESS,
progressNotification
.setProgress(subscriptions.size, progress, false)
.setContentText("$media on $parser")
.build()
)
}
subscriptions.toList().map {
val media = it.second
val text = if (media.isAnime) {
val parser =
SubscriptionHelper.getAnimeParser(media.id)
progress(index[it.first]!!, parser.name, media.name)
val ep: Episode? =
SubscriptionHelper.getEpisode(
parser,
media.id
)
if (ep != null) context.getString(R.string.episode) + "${ep.number}${
if (ep.title != null) " : ${ep.title}" else ""
}${
if (ep.isFiller) " [Filler]" else ""
} " + context.getString(R.string.just_released) to ep.thumbnail
else null
} else {
val parser =
SubscriptionHelper.getMangaParser(media.id)
progress(index[it.first]!!, parser.name, media.name)
val ep: MangaChapter? =
SubscriptionHelper.getChapter(
parser,
media.id
)
if (ep != null) ep.number + " " + context.getString(R.string.just_released) to null
else null
} ?: return@map
val notification = createNotification(
context.applicationContext,
media,
text.first,
text.second
)
if (hasNotificationPermission(context)) {
NotificationManagerCompat.from(context)
.notify(
CHANNEL_SUBSCRIPTION_CHECK,
System.currentTimeMillis().toInt(),
notification
)
}
}
if (progressNotification != null) notificationManager.cancel(
ID_SUBSCRIPTION_CHECK_PROGRESS
)
currentlyPerforming = false
}
return true
} catch (e: Exception) {
Logger.log("SubscriptionNotificationTask: ${e.message}")
Logger.log(e)
return false
}
} else {
return false
}
}
@SuppressLint("MissingPermission")
private fun createNotification(
context: Context,
media: SubscriptionHelper.Companion.SubscribeMedia,
text: String,
thumbnail: FileUrl?
): android.app.Notification {
val pendingIntent = getIntent(context, media.id)
val icon =
if (media.isAnime) R.drawable.ic_round_movie_filter_24 else R.drawable.ic_round_menu_book_24
val builder = NotificationCompat.Builder(context, CHANNEL_SUBSCRIPTION_CHECK)
.setSmallIcon(icon)
.setContentTitle(media.name)
.setContentText(text)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
if (thumbnail != null) {
val bitmap = getBitmapFromUrl(thumbnail.url)
if (bitmap != null) {
builder.setLargeIcon(bitmap)
}
}
return builder.build()
}
private fun getProgressNotification(
context: Context,
size: Int
): NotificationCompat.Builder {
return NotificationCompat.Builder(context, CHANNEL_SUBSCRIPTION_CHECK_PROGRESS)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(context.getString(R.string.checking_subscriptions_title))
.setProgress(size, 0, false)
.setOngoing(true)
.setAutoCancel(false)
}
private fun getBitmapFromUrl(url: String): Bitmap? {
return try {
val inputStream = java.net.URL(url).openStream()
BitmapFactory.decodeStream(inputStream)
} catch (e: Exception) {
null
}
}
private fun getIntent(context: Context, mediaId: Int): PendingIntent {
val notifyIntent = Intent(context, UrlMedia::class.java)
.putExtra("media", mediaId)
.setAction(mediaId.toString())
.apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
return PendingIntent.getActivity(
context, mediaId, notifyIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT
} else {
PendingIntent.FLAG_ONE_SHOT
}
)
}
}

View file

@ -0,0 +1,27 @@
package ani.dantotsu.notifications.subscription
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import ani.dantotsu.notifications.anilist.AnilistNotificationTask
import ani.dantotsu.util.Logger
class SubscriptionNotificationWorker(appContext: Context, workerParams: WorkerParameters) :
CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
Logger.log("SubscriptionNotificationWorker: doWork")
return if (AnilistNotificationTask().execute(applicationContext)) {
Result.success()
} else {
Logger.log("SubscriptionNotificationWorker: doWork failed")
Result.retry()
}
}
companion object {
val checkIntervals = arrayOf(0L, 480, 720, 1440)
const val WORK_NAME =
"ani.dantotsu.notifications.subscription.SubscriptionNotificationWorker"
}
}

View file

@ -0,0 +1,53 @@
package ani.dantotsu.others
import android.app.Activity
import android.graphics.Rect
import android.view.View
import android.widget.FrameLayout
class AndroidBug5497Workaround private constructor(activity: Activity, private val callback: (Boolean) -> Unit) {
private val mChildOfContent: View
private var usableHeightPrevious = 0
private val frameLayoutParams: FrameLayout.LayoutParams
init {
val content = activity.findViewById(android.R.id.content) as FrameLayout
mChildOfContent = content.getChildAt(0)
mChildOfContent.viewTreeObserver.addOnGlobalLayoutListener { possiblyResizeChildOfContent() }
frameLayoutParams = mChildOfContent.layoutParams as FrameLayout.LayoutParams
}
private fun possiblyResizeChildOfContent() {
val usableHeightNow = computeUsableHeight()
if (usableHeightNow != usableHeightPrevious) {
val usableHeightSansKeyboard = mChildOfContent.rootView.height
val heightDifference = usableHeightSansKeyboard - usableHeightNow
if (heightDifference > usableHeightSansKeyboard / 4) {
// keyboard probably just became visible
callback.invoke(true)
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference
} else {
// keyboard probably just became hidden
callback.invoke(false)
frameLayoutParams.height = usableHeightSansKeyboard
}
mChildOfContent.requestLayout()
usableHeightPrevious = usableHeightNow
}
}
private fun computeUsableHeight(): Int {
val r = Rect()
mChildOfContent.getWindowVisibleDisplayFrame(r)
return r.bottom
}
companion object {
// For more information, see https://issuetracker.google.com/issues/36911528
// To use this class, simply invoke assistActivity() on an Activity that already has its content view set.
fun assistActivity(activity: Activity, callback: (Boolean) -> Unit) {
AndroidBug5497Workaround(activity, callback)
}
}
}

View file

@ -1,6 +1,6 @@
package ani.dantotsu.others package ani.dantotsu.others
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.pow import kotlin.math.pow
@ -64,7 +64,7 @@ class JsUnpacker(packedJS: String?) {
return decoded.toString() return decoded.toString()
} }
} catch (e: Exception) { } catch (e: Exception) {
logger(e) Logger.log(e)
} }
return null return null
} }

View file

@ -2,7 +2,7 @@ package ani.dantotsu.others
import ani.dantotsu.FileUrl import ani.dantotsu.FileUrl
import ani.dantotsu.client import ani.dantotsu.client
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.anime.Episode import ani.dantotsu.media.anime.Episode
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
@ -41,8 +41,7 @@ object Kitsu {
} }
suspend fun getKitsuEpisodesDetails(media: Media): Map<String, Episode>? { suspend fun getKitsuEpisodesDetails(media: Media): Map<String, Episode>? {
val print = false Logger.log("Kitsu : title=${media.mainName()}")
logger("Kitsu : title=${media.mainName()}", print)
val query = val query =
""" """
query { query {
@ -70,7 +69,7 @@ query {
val result = getKitsuData(query) ?: return null val result = getKitsuData(query) ?: return null
logger("Kitsu : result=$result", print) //Logger.log("Kitsu : result=$result")
media.idKitsu = result.data?.lookupMapping?.id media.idKitsu = result.data?.lookupMapping?.id
val a = (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep -> val a = (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep ->
val num = ep?.number?.toString() ?: return@mapNotNull null val num = ep?.number?.toString() ?: return@mapNotNull null
@ -81,7 +80,7 @@ query {
thumb = FileUrl[ep.thumbnail?.original?.url], thumb = FileUrl[ep.thumbnail?.original?.url],
) )
}.toMap() }.toMap()
logger("Kitsu : a=$a", print) //Logger.log("Kitsu : a=$a")
return a return a
} }

View file

@ -44,7 +44,7 @@ class SpoilerPlugin : AbstractMarkwonPlugin() {
} }
companion object { companion object {
private val RE = Pattern.compile("~!.+?!~") private val RE = Pattern.compile("\\|\\|.+?\\|\\|")
private fun applySpoilerSpans(spannable: Spannable) { private fun applySpoilerSpans(spannable: Spannable) {
val text = spannable.toString() val text = spannable.toString()
val matcher = RE.matcher(text) val matcher = RE.matcher(text)

View file

@ -11,7 +11,7 @@ class CloudFlare(override val location: FileUrl) : WebViewBottomDialog() {
override var title = "Cloudflare Bypass" override var title = "Cloudflare Bypass"
override val webViewClient = object : WebViewClient() { override val webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
val cookie = cookies.getCookie(url.toString()) val cookie = cookies?.getCookie(url.toString())
if (cookie?.contains(cfTag) == true) { if (cookie?.contains(cfTag) == true) {
val clearance = cookie.substringAfter("$cfTag=").substringBefore(";") val clearance = cookie.substringAfter("$cfTag=").substringBefore(";")
privateCallback.invoke(mapOf(cfTag to clearance)) privateCallback.invoke(mapOf(cfTag to clearance))

View file

@ -34,8 +34,8 @@ class CookieCatcher : AppCompatActivity() {
val webView = findViewById<WebView>(R.id.discordWebview) val webView = findViewById<WebView>(R.id.discordWebview)
val cookies: CookieManager = Injekt.get<NetworkHelper>().cookieJar.manager val cookies: CookieManager? = Injekt.get<NetworkHelper>().cookieJar.manager
cookies.setAcceptThirdPartyCookies(webView, true) cookies?.setAcceptThirdPartyCookies(webView, true)
webView.apply { webView.apply {
settings.javaScriptEnabled = true settings.javaScriptEnabled = true

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