commit
4035aee1f9
281 changed files with 14581 additions and 1948 deletions
13
.github/workflows/beta.yml
vendored
13
.github/workflows/beta.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
@ -39,7 +39,7 @@ jobs:
|
|||
fi
|
||||
echo "Commits since $LAST_SHA:"
|
||||
# Accumulate commit logs in a shell variable
|
||||
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"%h - %s")
|
||||
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an")
|
||||
# URL-encode the newline characters for GitHub Actions
|
||||
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
|
||||
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
|
||||
|
@ -64,7 +64,7 @@ jobs:
|
|||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
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 }}
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v3.0.0
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: Dantotsu
|
||||
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
|
||||
|
||||
- name: Upload APK to Discord and Telegram
|
||||
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
|
||||
shell: bash
|
||||
run: |
|
||||
#Discord
|
||||
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g')
|
||||
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g; s/^/\n/')
|
||||
# Truncate commit messages if they are too long
|
||||
max_length=1900 # Adjust this value as needed
|
||||
if [ ${#commit_messages} -gt $max_length ]; then
|
||||
|
@ -104,7 +105,7 @@ jobs:
|
|||
#Telegram
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
|
||||
-F "caption=[Alpha-Build: ${VERSION}] Change logs :${commit_messages}" \
|
||||
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
|
||||
env:
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -8,6 +8,9 @@ local.properties
|
|||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Secrets
|
||||
apikey.properties
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
|
|
|
@ -6,24 +6,20 @@ plugins {
|
|||
id 'com.google.devtools.ksp'
|
||||
}
|
||||
|
||||
def gitCommitHash = providers.exec {
|
||||
commandLine("git", "rev-parse", "--verify", "--short", "HEAD")
|
||||
}.standardOutput.asText.get().trim()
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "ani.dantotsu"
|
||||
minSdk 23
|
||||
minSdk 21
|
||||
targetSdk 34
|
||||
versionCode((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "2.2.0"
|
||||
versionName "3.0.0"
|
||||
versionCode 220000000
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
|
||||
flavorDimensions "store"
|
||||
flavorDimensions += "store"
|
||||
productFlavors {
|
||||
fdroid {
|
||||
// F-Droid specific configuration
|
||||
|
@ -43,18 +39,21 @@ android {
|
|||
alpha {
|
||||
applicationIdSuffix ".beta" // keep as beta by popular request
|
||||
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
|
||||
isDefault true
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".beta"
|
||||
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
|
||||
}
|
||||
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
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
|
||||
}
|
||||
|
@ -77,12 +76,12 @@ android {
|
|||
dependencies {
|
||||
|
||||
// FireBase
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.1'
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:32.7.4')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.1'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.2'
|
||||
// Core
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.browser:browser:1.7.0'
|
||||
implementation 'androidx.browser:browser:1.8.0'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
|
@ -91,9 +90,9 @@ dependencies {
|
|||
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
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.webkit:webkit:1.10.0'
|
||||
|
||||
|
@ -106,7 +105,7 @@ dependencies {
|
|||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.2.1'
|
||||
ext.exo_version = '1.3.0'
|
||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||
implementation "androidx.media3:media3-ui:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||
|
@ -119,14 +118,30 @@ dependencies {
|
|||
|
||||
// UI
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
//implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
||||
implementation 'com.github.rebelonion:AnimatedBottomBar:v1.1.0'
|
||||
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||
implementation 'com.github.AAChartModel:AAChartCore-Kotlin: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
|
||||
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:okhttp:5.0.0-alpha.12'
|
||||
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 'ch.acra:acra-http:5.11.3'
|
||||
implementation 'org.jsoup:jsoup:1.15.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
|
||||
implementation 'org.jsoup:jsoup:1.16.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3'
|
||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
implementation 'com.github.tachiyomiorg:unifile:17bec43'
|
||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
|
|
19
app/proguard-rules.pro
vendored
19
app/proguard-rules.pro
vendored
|
@ -43,6 +43,25 @@
|
|||
public static <1> INSTANCE;
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
-keep class ani.dantotsu.** { *; }
|
||||
-keep class ani.dantotsu.download.DownloadsManager { *; }
|
||||
-keepattributes Signature
|
||||
-keep class uy.kohesive.injekt.** { *; }
|
||||
-keep class eu.kanade.tachiyomi.** { *; }
|
||||
-keep class kotlin.** { *; }
|
||||
-dontwarn kotlin.**
|
||||
-keep class kotlinx.** { *; }
|
||||
-keepclassmembers class uy.kohesive.injekt.api.FullTypeReference {
|
||||
<init>(...);
|
||||
}
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes EnclosingMethod
|
||||
-keep class com.google.gson.reflect.TypeToken { *; }
|
||||
-keep class org.jsoup.** { *; }
|
||||
-keepclassmembers class org.jsoup.nodes.Document { *; }
|
||||
|
||||
|
||||
|
||||
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||
|
|
|
@ -11,13 +11,21 @@ import android.net.Uri
|
|||
import android.os.Environment
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.*
|
||||
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 io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -25,9 +33,8 @@ import kotlinx.serialization.SerialName
|
|||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
object AppUpdater {
|
||||
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
|
||||
|
@ -39,7 +46,8 @@ object AppUpdater {
|
|||
.parsed<JsonArray>().map {
|
||||
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") }
|
||||
.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v", "")
|
||||
|
@ -50,7 +58,7 @@ object AppUpdater {
|
|||
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)
|
||||
if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread {
|
||||
CustomBottomDialog.newInstance().apply {
|
||||
|
@ -61,8 +69,7 @@ object AppUpdater {
|
|||
)
|
||||
addView(
|
||||
TextView(activity).apply {
|
||||
val markWon = Markwon.builder(activity)
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||
val markWon = buildMarkwon(activity, false)
|
||||
markWon.setMarkdown(this, md)
|
||||
}
|
||||
)
|
||||
|
@ -161,21 +168,8 @@ object AppUpdater {
|
|||
DownloadManager.EXTRA_DOWNLOAD_ID, id
|
||||
) ?: id
|
||||
|
||||
val query = DownloadManager.Query()
|
||||
query.setFilterById(downloadId)
|
||||
val c = downloadManager.query(query)
|
||||
|
||||
if (c.moveToFirst()) {
|
||||
val columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||
if (DownloadManager.STATUS_SUCCESSFUL == c
|
||||
.getInt(columnIndex)
|
||||
) {
|
||||
c.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI)
|
||||
val uri = Uri.parse(
|
||||
c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||
)
|
||||
openApk(this@downloadUpdate, uri)
|
||||
}
|
||||
downloadManager.getUriForDownloadedFile(downloadId)?.let {
|
||||
openApk(this@downloadUpdate, it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
|
@ -190,16 +184,11 @@ object AppUpdater {
|
|||
private fun openApk(context: Context, uri: Uri) {
|
||||
try {
|
||||
uri.path?.let {
|
||||
val contentUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
BuildConfig.APPLICATION_ID + ".provider",
|
||||
File(it)
|
||||
)
|
||||
val installIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
|
||||
data = contentUri
|
||||
data = uri
|
||||
}
|
||||
context.startActivity(installIntent)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
@ -69,7 +69,7 @@
|
|||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/currently_airing_widget_info" />
|
||||
</receiver>
|
||||
<receiver android:name=".subcriptions.NotificationClickReceiver" />
|
||||
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
|
||||
|
||||
|
||||
<activity
|
||||
|
@ -104,7 +104,26 @@
|
|||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.ExtensionsActivity"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden"
|
||||
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
|
||||
android:name=".others.imagesearch.ImageSearchActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
|
@ -117,6 +136,8 @@
|
|||
android:name=".media.CalendarActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity android:name=".media.user.ListActivity" />
|
||||
<activity android:name=".profile.SingleStatActivity"
|
||||
android:parentActivityName=".profile.ProfileActivity"/>
|
||||
<activity
|
||||
android:name=".media.manga.mangareader.MangaReaderActivity"
|
||||
android:excludeFromRecents="true"
|
||||
|
@ -127,7 +148,8 @@
|
|||
<activity
|
||||
android:name=".media.MediaDetailsActivity"
|
||||
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=".home.NoInternet" />
|
||||
<activity
|
||||
|
@ -209,6 +231,7 @@
|
|||
<data android:host="discord.dantotsu.com" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".connections.anilist.UrlMedia"
|
||||
android:configChanges="orientation|screenSize|layoutDirection"
|
||||
|
@ -239,6 +262,17 @@
|
|||
<data android:host="myanimelist.net" />
|
||||
<data android:pathPrefix="/anime" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/view_profile_in_dantotsu">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="anilist.co" />
|
||||
<data android:pathPrefix="/user" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
@ -254,6 +288,15 @@
|
|||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:pathPattern=".*\\.ani" />
|
||||
<data android:pathPattern=".*\\.sani" />
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
||||
|
@ -264,14 +307,22 @@
|
|||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<receiver
|
||||
android:name=".subcriptions.AlarmReceiver"
|
||||
<receiver android:name=".notifications.AlarmPermissionStateReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.BootCompletedReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="Aani.dantotsu.ACTION_ALARM" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver"/>
|
||||
<receiver android:name=".notifications.comment.CommentNotificationReceiver"/>
|
||||
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver"/>
|
||||
|
||||
<meta-data
|
||||
android:name="preloaded_fonts"
|
||||
|
|
|
@ -8,7 +8,9 @@ import androidx.multidex.MultiDex
|
|||
import androidx.multidex.MultiDexApplication
|
||||
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
||||
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.notifications.TaskScheduler
|
||||
import ani.dantotsu.others.DisabledReports
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
|
@ -17,6 +19,8 @@ import ani.dantotsu.parsers.novel.NovelExtensionManager
|
|||
import ani.dantotsu.settings.SettingsActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.util.FinalExceptionHandler
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
|
@ -28,7 +32,6 @@ import kotlinx.coroutines.launch
|
|||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
@ -79,7 +82,9 @@ class App : MultiDexApplication() {
|
|||
}
|
||||
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
|
||||
|
||||
|
||||
Logger.init(this)
|
||||
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
|
||||
Logger.log("App: Logging started")
|
||||
|
||||
initializeNetwork(baseContext)
|
||||
|
||||
|
@ -95,29 +100,36 @@ class App : MultiDexApplication() {
|
|||
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||
animeScope.launch {
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
val mangaScope = CoroutineScope(Dispatchers.Default)
|
||||
mangaScope.launch {
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
val novelScope = CoroutineScope(Dispatchers.Default)
|
||||
novelScope.launch {
|
||||
novelExtensionManager.findAvailableExtensions()
|
||||
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
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() {
|
||||
try {
|
||||
Notifications.createChannels(this)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
|
||||
Logger.log("Failed to modify notification channels")
|
||||
Logger.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package ani.dantotsu
|
||||
|
||||
import android.Manifest
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.DatePickerDialog
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
|
@ -16,32 +18,71 @@ import android.content.res.Configuration
|
|||
import android.content.res.Resources.getSystem
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.Manifest
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.MediaScannerConnection
|
||||
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.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.telephony.TelephonyManager
|
||||
import android.text.InputFilter
|
||||
import android.text.Spanned
|
||||
import android.util.AttributeSet
|
||||
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.animation.*
|
||||
import android.widget.*
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationSet
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.view.animation.ScaleAnimation
|
||||
import android.view.animation.TranslateAnimation
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.DatePicker
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
import androidx.core.view.*
|
||||
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.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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.databinding.ItemCountDownBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
|
||||
import ani.dantotsu.others.SpoilerPlugin
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
||||
import ani.dantotsu.subcriptions.NotificationClickReceiver
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
|
||||
import com.bumptech.glide.load.resource.gif.GifDrawable
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
|
@ -68,15 +120,37 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|||
import com.google.android.material.internal.ViewUtils
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import 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 uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.*
|
||||
import java.lang.Runnable
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
import java.util.*
|
||||
import kotlin.math.*
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.log2
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
|
||||
|
||||
var statusBarHeight = 0
|
||||
|
@ -108,12 +182,6 @@ fun currActivity(): Activity? {
|
|||
var loadMedia: Int? = null
|
||||
var loadIsMAL = false
|
||||
|
||||
fun logger(e: Any?, print: Boolean = true) {
|
||||
if (print)
|
||||
println(e)
|
||||
}
|
||||
|
||||
|
||||
fun initActivity(a: Activity) {
|
||||
val window = a.window
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
@ -132,10 +200,13 @@ fun initActivity(a: Activity) {
|
|||
if (navBarHeight == 0) {
|
||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||
?.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) {
|
||||
window.decorView.rootWindowInsets?.displayCutout?.apply {
|
||||
if (boundingRects.size > 0) {
|
||||
|
@ -148,35 +219,60 @@ fun initActivity(a: Activity) {
|
|||
val windowInsets =
|
||||
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
|
||||
if (windowInsets != null) {
|
||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
statusBarHeight = insets.top
|
||||
navBarHeight = insets.bottom
|
||||
statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
||||
navBarHeight =
|
||||
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
||||
}
|
||||
}
|
||||
if (a !is MainActivity) a.setNavigationTheme()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun Activity.hideSystemBars() {
|
||||
window.decorView.systemUiVisibility = (
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
)
|
||||
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
|
||||
controller.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
controller.hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun Activity.hideStatusBar() {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
fun Activity.hideSystemBarsExtendView() {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
hideSystemBars()
|
||||
}
|
||||
|
||||
fun Activity.showSystemBars() {
|
||||
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
|
||||
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
||||
controller.show(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.showSystemBarsRetractView() {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||
showSystemBars()
|
||||
}
|
||||
|
||||
fun Activity.setNavigationTheme() {
|
||||
val tv = TypedValue()
|
||||
theme.resolveAttribute(android.R.attr.colorBackground, tv, true)
|
||||
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && tv.isColorType)
|
||||
|| (tv.type >= TypedValue.TYPE_FIRST_COLOR_INT && tv.type <= TypedValue.TYPE_LAST_COLOR_INT)
|
||||
) {
|
||||
window.navigationBarColor = tv.data
|
||||
}
|
||||
}
|
||||
|
||||
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val window = dialog?.window
|
||||
val decorView: View = window?.decorView ?: return
|
||||
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
dialog?.window?.let { window ->
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
|
||||
if (immersiveMode) {
|
||||
WindowInsetsControllerCompat(
|
||||
window, window.decorView
|
||||
).hide(WindowInsetsCompat.Type.statusBars())
|
||||
}
|
||||
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
|
||||
val behavior = BottomSheetBehavior.from(requireView().parent as View)
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
|
@ -190,6 +286,7 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
|
|||
)
|
||||
window.navigationBarColor = typedValue.data
|
||||
}
|
||||
}
|
||||
|
||||
override fun show(manager: FragmentManager, tag: String?) {
|
||||
val ft = manager.beginTransaction()
|
||||
|
@ -202,6 +299,7 @@ fun isOnline(context: Context): Boolean {
|
|||
val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
return tryWith {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
return@tryWith if (cap != null) {
|
||||
when {
|
||||
|
@ -217,6 +315,19 @@ fun isOnline(context: Context): Boolean {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -278,7 +389,7 @@ class InputFilterMinMax(
|
|||
val input = (dest.toString() + source.toString()).toDouble()
|
||||
if (isInRange(min, max, input)) return null
|
||||
} catch (nfe: NumberFormatException) {
|
||||
logger(nfe.stackTraceToString())
|
||||
Logger.log(nfe)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
@ -449,6 +560,7 @@ fun ImageView.loadImage(url: String?, size: Int = 0) {
|
|||
}
|
||||
|
||||
fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
|
||||
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
|
||||
if (file?.url?.isNotEmpty() == true) {
|
||||
tryWith {
|
||||
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?) {
|
||||
tryWith {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
|
||||
currContext()?.startActivity(intent)
|
||||
link?.let {
|
||||
try {
|
||||
val emptyBrowserIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
data = Uri.fromParts("http", "", null)
|
||||
}
|
||||
val sendIntent = Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
data = Uri.parse(link)
|
||||
selector = emptyBrowserIntent
|
||||
}
|
||||
currContext()!!.startActivity(sendIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
snackString("No browser found")
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -677,6 +804,7 @@ fun savePrefs(
|
|||
}
|
||||
|
||||
fun downloadsPermission(activity: AppCompatActivity): Boolean {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
|
@ -687,7 +815,11 @@ fun downloadsPermission(activity: AppCompatActivity): Boolean {
|
|||
}.toTypedArray()
|
||||
|
||||
return if (requiredPermissions.isNotEmpty()) {
|
||||
ActivityCompat.requestPermissions(activity, requiredPermissions, DOWNLOADS_PERMISSION_REQUEST_CODE)
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
requiredPermissions,
|
||||
DOWNLOADS_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
|
@ -728,7 +860,7 @@ fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
|
|||
|
||||
private fun scanFile(path: String, context: Context) {
|
||||
MediaScannerConnection.scanFile(context, arrayOf(path), null) { p, _ ->
|
||||
logger("Finished scanning $p")
|
||||
Logger.log("Finished scanning $p")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -760,8 +892,10 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
|
|||
val clipboard = getSystemService(activity, ClipboardManager::class.java)
|
||||
val clip = ClipData.newPlainText("label", string)
|
||||
clipboard?.setPrimaryClip(clip)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
if (toast) snackString(activity.getString(R.string.copied_text, string))
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun countDown(media: Media, view: ViewGroup) {
|
||||
|
@ -868,7 +1002,7 @@ class EmptyAdapter(private val count: Int) : RecyclerView.Adapter<RecyclerView.V
|
|||
|
||||
fun toast(string: String?) {
|
||||
if (string != null) {
|
||||
logger(string)
|
||||
Logger.log(string)
|
||||
MainScope().launch {
|
||||
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
|
||||
.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...
|
||||
if (s != null) {
|
||||
(activity ?: currActivity())?.apply {
|
||||
runOnUiThread {
|
||||
val snackBar = Snackbar.make(
|
||||
window.decorView.findViewById(android.R.id.content),
|
||||
s,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
runOnUiThread {
|
||||
snackBar.view.apply {
|
||||
updateLayoutParams<FrameLayout.LayoutParams> {
|
||||
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
|
||||
|
@ -905,13 +1039,15 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
|
|||
}
|
||||
snackBar.show()
|
||||
}
|
||||
return snackBar
|
||||
}
|
||||
logger(s)
|
||||
Logger.log(s)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger(e.stackTraceToString())
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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
|
||||
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
||||
if (incognito) {
|
||||
val intent = Intent(context, NotificationClickReceiver::class.java)
|
||||
val intent = Intent(context, IncognitoNotificationClickReceiver::class.java)
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
context, 0, intent,
|
||||
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() {
|
||||
currActivity()?.runOnUiThread {
|
||||
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
|
||||
|
@ -1067,3 +1225,100 @@ suspend fun View.pop() {
|
|||
}
|
||||
delay(100)
|
||||
}
|
||||
|
||||
fun blurImage(imageView: ImageView, banner: String?) {
|
||||
if (banner != null) {
|
||||
val radius = PrefManager.getVal<Float>(PrefName.BlurRadius).toInt()
|
||||
val sampling = PrefManager.getVal<Float>(PrefName.BlurSampling).toInt()
|
||||
if (PrefManager.getVal(PrefName.BlurBanners)) {
|
||||
val context = imageView.context
|
||||
if (!(context as Activity).isDestroyed) {
|
||||
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
|
||||
Glide.with(context as Context)
|
||||
.load(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
|
||||
}
|
|
@ -2,7 +2,10 @@ package ani.dantotsu
|
|||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.net.Uri
|
||||
|
@ -11,7 +14,8 @@ import android.os.Bundle
|
|||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
|
@ -25,6 +29,8 @@ import androidx.core.animation.doOnEnd
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updateMargins
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
|
@ -32,6 +38,7 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.databinding.ActivityMainBinding
|
||||
|
@ -43,18 +50,27 @@ import ani.dantotsu.home.LoginFragment
|
|||
import ani.dantotsu.home.MangaFragment
|
||||
import ani.dantotsu.home.NoInternet
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
|
||||
import ani.dantotsu.notifications.comment.CommentNotificationWorker
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.activity.FeedActivity
|
||||
import ani.dantotsu.profile.activity.NotificationActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
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.util.Logger
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -84,6 +100,67 @@ class MainActivity : AppCompatActivity() {
|
|||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
|
||||
androidx.work.WorkManager.getInstance(this)
|
||||
.enqueue(OneTimeWorkRequest.Companion.from(CommentNotificationWorker::class.java))
|
||||
|
||||
androidx.work.WorkManager.getInstance(this)
|
||||
.enqueue(OneTimeWorkRequest.Companion.from(AnilistNotificationWorker::class.java))
|
||||
|
||||
val action = intent.action
|
||||
val type = intent.type
|
||||
if (Intent.ACTION_VIEW == action && type != null) {
|
||||
val uri: Uri? = intent.data
|
||||
try {
|
||||
if (uri == null) {
|
||||
throw Exception("Uri is null")
|
||||
}
|
||||
val jsonString =
|
||||
contentResolver.openInputStream(uri)?.readBytes()
|
||||
?: throw Exception("Error reading file")
|
||||
val name =
|
||||
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
|
||||
//.sani is encrypted, .ani is not
|
||||
if (name.endsWith(".sani")) {
|
||||
passwordAlertDialog { password ->
|
||||
if (password != null) {
|
||||
val salt = jsonString.copyOfRange(0, 16)
|
||||
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
||||
val decryptedJson = try {
|
||||
PreferenceKeystore.decryptWithPassword(
|
||||
password,
|
||||
encrypted,
|
||||
salt
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
toast("Incorrect password")
|
||||
return@passwordAlertDialog
|
||||
}
|
||||
if (PreferencePackager.unpack(decryptedJson)) {
|
||||
val intent = Intent(this, this.javaClass)
|
||||
this.finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
} else if (name.endsWith(".ani")) {
|
||||
val decryptedJson = jsonString.toString(Charsets.UTF_8)
|
||||
if (PreferencePackager.unpack(decryptedJson)) {
|
||||
val intent = Intent(this, this.javaClass)
|
||||
this.finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
toast("Invalid file type")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast("Error importing settings")
|
||||
}
|
||||
}
|
||||
|
||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
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)
|
||||
|
||||
|
||||
val offset = try {
|
||||
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
resources.getDimensionPixelSize(statusBarHeightId)
|
||||
|
@ -144,11 +220,14 @@ class MainActivity : AppCompatActivity() {
|
|||
finish()
|
||||
}
|
||||
doubleBackToExitPressedOnce = true
|
||||
snackString(this@MainActivity.getString(R.string.back_to_exit))
|
||||
Handler(Looper.getMainLooper()).postDelayed(
|
||||
{ doubleBackToExitPressedOnce = false },
|
||||
2000
|
||||
)
|
||||
snackString(this@MainActivity.getString(R.string.back_to_exit)).apply {
|
||||
this?.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
||||
super.onDismissed(transientBottomBar, event)
|
||||
doubleBackToExitPressedOnce = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
val preferences: SourcePreferences = Injekt.get()
|
||||
|
@ -205,6 +284,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
binding.root.doOnAttach {
|
||||
initActivity(this)
|
||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
||||
selectedOption = if (fragment != null) {
|
||||
when (fragment) {
|
||||
AnimeFragment::class.java.name -> 0
|
||||
|
@ -217,7 +297,36 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
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)
|
||||
|
@ -255,6 +364,7 @@ class MainActivity : AppCompatActivity() {
|
|||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
if (mainViewPager.getCurrentItem() != selectedOption) {
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post {
|
||||
mainViewPager.setCurrentItem(
|
||||
|
@ -262,6 +372,7 @@ class MainActivity : AppCompatActivity() {
|
|||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
}
|
||||
|
@ -288,8 +399,21 @@ class MainActivity : AppCompatActivity() {
|
|||
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
||||
}
|
||||
}
|
||||
delay(500)
|
||||
startSubscription()
|
||||
val username = intent.extras?.getString("username")
|
||||
if (username != null) {
|
||||
val nameInt = username.toIntOrNull()
|
||||
if (nameInt != null) {
|
||||
startActivity(
|
||||
Intent(this@MainActivity, ProfileActivity::class.java)
|
||||
.putExtra("userId", nameInt)
|
||||
)
|
||||
} else {
|
||||
startActivity(
|
||||
Intent(this@MainActivity, ProfileActivity::class.java)
|
||||
.putExtra("username", username)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
load = true
|
||||
}
|
||||
|
@ -326,27 +450,74 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
//TODO: Remove this
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
|
||||
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||
val downloadCursor = index.getDownloads()
|
||||
while (downloadCursor.moveToNext()) {
|
||||
val download = downloadCursor.download
|
||||
Log.e("Downloader", download.request.uri.toString())
|
||||
Log.e("Downloader", download.request.id)
|
||||
Log.e("Downloader", download.request.mimeType.toString())
|
||||
Log.e("Downloader", download.request.data.size.toString())
|
||||
Log.e("Downloader", download.bytesDownloaded.toString())
|
||||
Log.e("Downloader", download.state.toString())
|
||||
Log.e("Downloader", download.failureReason.toString())
|
||||
|
||||
if (download.state == Download.STATE_FAILED) { //simple cleanup
|
||||
if (download.state == Download.STATE_FAILED) {
|
||||
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
|
||||
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.os.Build
|
|||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.others.webview.CloudFlare
|
||||
import ani.dantotsu.others.webview.WebViewBottomDialog
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import com.lagradost.nicehttp.addGenericDns
|
||||
|
@ -104,6 +105,7 @@ fun logError(e: Throwable, post: Boolean = true, snackbar: Boolean = true) {
|
|||
toast(e.localizedMessage)
|
||||
}
|
||||
e.printStackTrace()
|
||||
Logger.log(e)
|
||||
}
|
||||
|
||||
fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T): T? {
|
||||
|
@ -134,7 +136,7 @@ suspend fun <T> tryWithSuspend(
|
|||
* A url, which can also have headers
|
||||
* **/
|
||||
data class FileUrl(
|
||||
val url: String,
|
||||
var url: String,
|
||||
val headers: Map<String, String> = mapOf()
|
||||
) : Serializable {
|
||||
companion object {
|
||||
|
|
|
@ -19,7 +19,7 @@ fun updateProgress(media: Media, number: String) {
|
|||
if (Anilist.userid != null) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val a = number.toFloatOrNull()?.toInt()
|
||||
if ((a ?: 0) > (media.userProgress ?: 0)) {
|
||||
if ((a ?: 0) > (media.userProgress ?: -1)) {
|
||||
Anilist.mutation.editList(
|
||||
media.id,
|
||||
a,
|
||||
|
|
|
@ -11,8 +11,10 @@ import ani.dantotsu.currContext
|
|||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import java.util.Calendar
|
||||
|
||||
object Anilist {
|
||||
|
@ -27,6 +29,7 @@ object Anilist {
|
|||
var bg: String? = null
|
||||
var episodesWatched: Int? = null
|
||||
var chapterRead: Int? = null
|
||||
var unreadNotificationCount: Int = 0
|
||||
|
||||
var genres: ArrayList<String>? = null
|
||||
var tags: Map<Boolean, List<String>>? = null
|
||||
|
@ -124,7 +127,8 @@ object Anilist {
|
|||
show: Boolean = false,
|
||||
cache: Int? = null
|
||||
): T? {
|
||||
return tryWithSuspend {
|
||||
return try {
|
||||
if (show) Logger.log("Anilist Query: $query")
|
||||
if (rateLimitReset > System.currentTimeMillis() / 1000) {
|
||||
toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
||||
throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
||||
|
@ -148,7 +152,7 @@ object Anilist {
|
|||
cacheTime = cache ?: 10
|
||||
)
|
||||
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
|
||||
Log.d("AnilistQuery", "Remaining requests: $remaining")
|
||||
Logger.log("Remaining requests: $remaining")
|
||||
if (json.code == 429) {
|
||||
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
|
||||
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
|
||||
|
@ -159,10 +163,14 @@ object Anilist {
|
|||
toast("Rate limited. Try after $retry seconds")
|
||||
throw Exception("Rate limited after $retry seconds")
|
||||
}
|
||||
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||
if (show) println("Response : ${json.text}")
|
||||
if (!json.text.startsWith("{")) {throw Exception(currContext()?.getString(R.string.anilist_down))}
|
||||
if (show) Logger.log("Anilist Response: ${json.text}")
|
||||
json.parsed()
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
if (show) snackString("Error fetching Anilist data: ${e.message}")
|
||||
Logger.log("Anilist Query Error: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,23 @@ class AnilistMutations {
|
|||
executeQuery<JsonObject>(query, variables)
|
||||
}
|
||||
|
||||
suspend fun toggleFav(type: FavType, id: Int): Boolean {
|
||||
val filter = when (type) {
|
||||
FavType.ANIME -> "animeId"
|
||||
FavType.MANGA -> "mangaId"
|
||||
FavType.CHARACTER -> "characterId"
|
||||
FavType.STAFF -> "staffId"
|
||||
FavType.STUDIO -> "studioId"
|
||||
}
|
||||
val query = """mutation{ToggleFavourite($filter:$id){anime{pageInfo{total}}}}"""
|
||||
val result = executeQuery<JsonObject>(query)
|
||||
return result?.get("errors") == null && result != null
|
||||
}
|
||||
|
||||
enum class FavType {
|
||||
ANIME, MANGA, CHARACTER, STAFF, STUDIO
|
||||
}
|
||||
|
||||
suspend fun editList(
|
||||
mediaID: Int,
|
||||
progress: Int? = null,
|
||||
|
|
|
@ -6,9 +6,12 @@ import ani.dantotsu.checkGenreTime
|
|||
import ani.dantotsu.checkId
|
||||
import ani.dantotsu.connections.anilist.Anilist.authorRoles
|
||||
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
||||
import ani.dantotsu.connections.anilist.api.FeedResponse
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.anilist.api.NotificationResponse
|
||||
import ani.dantotsu.connections.anilist.api.Page
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.connections.anilist.api.ToggleLike
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.logError
|
||||
|
@ -20,6 +23,7 @@ import ani.dantotsu.others.MalScraper
|
|||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -31,23 +35,27 @@ import java.io.Serializable
|
|||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class AnilistQueries {
|
||||
|
||||
suspend fun getUserData(): Boolean {
|
||||
val response: Query.Viewer?
|
||||
measureTimeMillis {
|
||||
response =
|
||||
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}}}""")
|
||||
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") }
|
||||
val user = response?.data?.user ?: return false
|
||||
|
||||
PrefManager.setVal(PrefName.AnilistUserName, user.name)
|
||||
|
||||
Anilist.userid = user.id
|
||||
PrefManager.setVal(PrefName.AnilistUserId, user.id.toString())
|
||||
Anilist.username = user.name
|
||||
Anilist.bg = user.bannerImage
|
||||
Anilist.avatar = user.avatar?.medium
|
||||
Anilist.episodesWatched = user.statistics?.anime?.episodesWatched
|
||||
Anilist.chapterRead = user.statistics?.manga?.chaptersRead
|
||||
Anilist.adult = user.options?.displayAdultContent ?: false
|
||||
Anilist.unreadNotificationCount = user.unreadNotificationCount ?: 0
|
||||
val unread = PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications)
|
||||
Anilist.unreadNotificationCount += unread
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -64,7 +72,7 @@ class AnilistQueries {
|
|||
media.cameFromContinue = false
|
||||
|
||||
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 {
|
||||
val anilist = async {
|
||||
var response = executeQuery<Query.Media>(query, force = true, show = true)
|
||||
|
@ -121,6 +129,30 @@ class AnilistQueries {
|
|||
name = i.node?.name?.userPreferred,
|
||||
image = i.node?.image?.medium,
|
||||
banner = media.banner ?: media.cover,
|
||||
isFav = i.node?.isFavourite ?: false,
|
||||
role = when (i.role.toString()) {
|
||||
"MAIN" -> currContext()?.getString(R.string.main_role)
|
||||
?: "MAIN"
|
||||
|
||||
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role)
|
||||
?: "SUPPORTING"
|
||||
|
||||
else -> i.role.toString()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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()) {
|
||||
"MAIN" -> currContext()?.getString(R.string.main_role)
|
||||
?: "MAIN"
|
||||
|
@ -211,8 +243,10 @@ class AnilistQueries {
|
|||
|
||||
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
||||
media.anime.author = Author(
|
||||
it.id.toString(),
|
||||
it.name?.userPreferred ?: "N/A"
|
||||
it.id,
|
||||
it.name?.userPreferred ?: "N/A",
|
||||
it.image?.medium,
|
||||
"AUTHOR"
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -231,8 +265,10 @@ class AnilistQueries {
|
|||
} else if (media.manga != null) {
|
||||
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
||||
media.manga.author = Author(
|
||||
it.id.toString(),
|
||||
it.name?.userPreferred ?: "N/A"
|
||||
it.id,
|
||||
it.name?.userPreferred ?: "N/A",
|
||||
it.image?.medium,
|
||||
"AUTHOR"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -249,6 +285,7 @@ class AnilistQueries {
|
|||
} else {
|
||||
if (currContext()?.let { isOnline(it) } == true) {
|
||||
snackString(currContext()?.getString(R.string.error_getting_data))
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -262,6 +299,52 @@ class AnilistQueries {
|
|||
return media
|
||||
}
|
||||
|
||||
fun userMediaDetails(media: Media): Media {
|
||||
val query =
|
||||
"""{Media(id:${media.id}){id mediaListEntry{id status progress private repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite idMal}}"""
|
||||
runBlocking {
|
||||
val anilist = async {
|
||||
var response = executeQuery<Query.Media>(query, force = true, show = true)
|
||||
if (response != null) {
|
||||
fun parse() {
|
||||
val fetchedMedia = response?.data?.media ?: return
|
||||
|
||||
if (fetchedMedia.mediaListEntry != null) {
|
||||
fetchedMedia.mediaListEntry?.apply {
|
||||
media.userProgress = progress
|
||||
media.isListPrivate = private ?: false
|
||||
media.userListId = id
|
||||
media.userStatus = status?.toString()
|
||||
media.inCustomListsOf = customLists?.toMutableMap()
|
||||
media.userRepeat = repeat ?: 0
|
||||
media.userUpdatedAt = updatedAt?.toString()?.toLong()?.times(1000)
|
||||
media.userCompletedAt = completedAt ?: FuzzyDate()
|
||||
media.userStartedAt = startedAt ?: FuzzyDate()
|
||||
}
|
||||
} else {
|
||||
media.isListPrivate = false
|
||||
media.userStatus = null
|
||||
media.userListId = null
|
||||
media.userProgress = null
|
||||
media.userRepeat = 0
|
||||
media.userUpdatedAt = null
|
||||
media.userCompletedAt = FuzzyDate()
|
||||
media.userStartedAt = FuzzyDate()
|
||||
}
|
||||
}
|
||||
|
||||
if (response.data?.media != null) parse()
|
||||
else {
|
||||
response = executeQuery(query, force = true, useToken = false)
|
||||
if (response?.data?.media != null) parse()
|
||||
}
|
||||
}
|
||||
}
|
||||
awaitAll(anilist)
|
||||
}
|
||||
return media
|
||||
}
|
||||
|
||||
suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
|
||||
val returnArray = arrayListOf<Media>()
|
||||
val map = mutableMapOf<Int, Media>()
|
||||
|
@ -299,9 +382,17 @@ class AnilistQueries {
|
|||
}
|
||||
}
|
||||
}
|
||||
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
|
||||
if (set.isNotEmpty()) {
|
||||
set.reversed().forEach {
|
||||
if (type != "ANIME") {
|
||||
returnArray.addAll(map.values)
|
||||
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]!!)
|
||||
}
|
||||
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 } } } } } """
|
||||
}
|
||||
|
||||
suspend fun favMedia(anime: Boolean): ArrayList<Media> {
|
||||
suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
|
||||
var hasNextPage = true
|
||||
var page = 0
|
||||
|
||||
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 apiMediaList = if (anime) favourites?.anime else favourites?.manga
|
||||
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
|
||||
|
@ -339,8 +430,8 @@ class AnilistQueries {
|
|||
return responseArray
|
||||
}
|
||||
|
||||
private fun favMediaQuery(anime: Boolean, page: Int): String {
|
||||
return """User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
|
||||
private fun favMediaQuery(anime: Boolean, page: Int, id: Int? = Anilist.userid): String {
|
||||
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
|
||||
}
|
||||
|
||||
suspend fun recommendations(): ArrayList<Media> {
|
||||
|
@ -383,7 +474,7 @@ class AnilistQueries {
|
|||
}
|
||||
|
||||
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>> {
|
||||
|
@ -423,7 +514,8 @@ class AnilistQueries {
|
|||
}, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
|
||||
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>>()
|
||||
fun current(type: String) {
|
||||
val subMap = mutableMapOf<Int, Media>()
|
||||
|
@ -446,10 +538,18 @@ class AnilistQueries {
|
|||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
val set = PrefManager.getCustomVal<Set<Int>>("continue_${type.uppercase()}", setOf())
|
||||
.toMutableSet()
|
||||
if (set.isNotEmpty()) {
|
||||
set.reversed().forEach {
|
||||
if (type != "Anime") {
|
||||
returnArray.addAll(subMap.values)
|
||||
returnMap["current$type"] = returnArray
|
||||
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]!!)
|
||||
}
|
||||
for (i in subMap) {
|
||||
|
@ -472,9 +572,13 @@ class AnilistQueries {
|
|||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
|
||||
if (set.isNotEmpty()) {
|
||||
set.reversed().forEach {
|
||||
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]!!)
|
||||
}
|
||||
for (i in subMap) {
|
||||
|
@ -558,12 +662,11 @@ class AnilistQueries {
|
|||
|
||||
|
||||
private suspend fun bannerImage(type: String): String? {
|
||||
//var image = loadData<BannerImage>("banner_$type")
|
||||
val image: BannerImage? = BannerImage(
|
||||
PrefManager.getCustomVal("banner_${type}_url", null),
|
||||
val image = BannerImage(
|
||||
PrefManager.getCustomVal("banner_${type}_url", ""),
|
||||
PrefManager.getCustomVal("banner_${type}_time", 0L)
|
||||
)
|
||||
if (image == null || image.checkTime()) {
|
||||
if (image.url.isNullOrEmpty() || image.checkTime()) {
|
||||
val response =
|
||||
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """)
|
||||
val random = response?.data?.mediaListCollection?.lists?.mapNotNull {
|
||||
|
@ -592,7 +695,7 @@ class AnilistQueries {
|
|||
sortOrder: String? = null
|
||||
): MutableMap<String, ArrayList<Media>> {
|
||||
val response =
|
||||
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage 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 unsorted = mutableMapOf<String, ArrayList<Media>>()
|
||||
val all = arrayListOf<Media>()
|
||||
|
@ -620,7 +723,7 @@ class AnilistQueries {
|
|||
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 })
|
||||
//favMedia doesn't fill userProgress, so we need to fill it manually by searching :(
|
||||
sorted["Favourites"]?.forEach { fav ->
|
||||
|
@ -661,8 +764,8 @@ class AnilistQueries {
|
|||
PrefManager.getVal<Set<String>>(PrefName.TagsListNonAdult).toMutableList()
|
||||
var tags = if (adultTags.isEmpty() || nonAdultTags.isEmpty()) null else
|
||||
mapOf(
|
||||
true to adultTags,
|
||||
false to nonAdultTags
|
||||
true to adultTags.sortedBy { it },
|
||||
false to nonAdultTags.sortedBy { it }
|
||||
)
|
||||
|
||||
if (genres.isNullOrEmpty()) {
|
||||
|
@ -698,7 +801,7 @@ class AnilistQueries {
|
|||
}
|
||||
}
|
||||
return if (!genres.isNullOrEmpty() && tags != null) {
|
||||
Anilist.genres = genres
|
||||
Anilist.genres = genres?.sortedBy { it }?.toMutableList() as ArrayList<String>
|
||||
Anilist.tags = tags
|
||||
true
|
||||
} else false
|
||||
|
@ -1225,4 +1328,116 @@ Page(page:$page,perPage:50) {
|
|||
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
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
|||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -99,6 +100,7 @@ class AnilistHomeViewModel : ViewModel() {
|
|||
|
||||
suspend fun initHomePage() {
|
||||
val res = Anilist.query.initHomePage()
|
||||
Logger.log("AnilistHomeViewModel : res=$res")
|
||||
res["currentAnime"]?.let { animeContinue.postValue(it) }
|
||||
res["favoriteAnime"]?.let { animeFav.postValue(it) }
|
||||
res["plannedAnime"]?.let { animePlanned.postValue(it) }
|
||||
|
@ -332,3 +334,63 @@ 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)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ import android.net.Uri
|
|||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.startMainActivity
|
||||
|
@ -16,7 +16,6 @@ class Login : AppCompatActivity() {
|
|||
|
||||
ThemeManager(this).applyTheme()
|
||||
val data: Uri? = intent?.data
|
||||
logger(data.toString())
|
||||
try {
|
||||
Anilist.token =
|
||||
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
||||
|
|
|
@ -11,14 +11,15 @@ import ani.dantotsu.themes.ThemeManager
|
|||
class UrlMedia : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
val data: Uri? = intent?.data
|
||||
val type = data?.pathSegments?.getOrNull(0)
|
||||
if (type != "user") {
|
||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||
var isMAL = false
|
||||
var continueMedia = true
|
||||
if (id == 0) {
|
||||
continueMedia = false
|
||||
val data: Uri? = intent?.data
|
||||
isMAL = data?.host != "anilist.co"
|
||||
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
||||
} else loadMedia = id
|
||||
|
@ -26,5 +27,9 @@ class UrlMedia : Activity() {
|
|||
this,
|
||||
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)
|
||||
)
|
||||
} else {
|
||||
val username = data.pathSegments?.getOrNull(1)
|
||||
startMainActivity(this, bundleOf("username" to username))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ data class Character(
|
|||
|
||||
// Notes for site moderators
|
||||
@SerialName("modNotes") var modNotes: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CharacterConnection(
|
||||
|
@ -56,7 +56,7 @@ data class CharacterConnection(
|
|||
|
||||
// The pagination information
|
||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CharacterEdge(
|
||||
|
@ -82,7 +82,7 @@ data class CharacterEdge(
|
|||
|
||||
// The order the character should be displayed from the users favourites
|
||||
@SerialName("favouriteOrder") var favouriteOrder: Int?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CharacterName(
|
||||
|
@ -109,7 +109,7 @@ data class CharacterName(
|
|||
|
||||
// The currently authenticated users preferred name language. Default romaji for non-authenticated
|
||||
@SerialName("userPreferred") var userPreferred: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CharacterImage(
|
||||
|
@ -118,4 +118,4 @@ data class CharacterImage(
|
|||
|
||||
// The character's image of media at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
|
@ -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
|
||||
data class GenreCollection(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) {
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("GenreCollection")
|
||||
val genreCollection: List<String>?
|
||||
)
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MediaTagCollection(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) {
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("MediaTagCollection")
|
||||
val mediaTagCollection: List<MediaTag>?
|
||||
)
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) {
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("User")
|
||||
val user: ani.dantotsu.connections.anilist.api.User?
|
||||
)
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UserProfileResponse(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("followerPage")
|
||||
val followerPage: UserProfilePage?,
|
||||
@SerialName("followingPage")
|
||||
val followingPage: UserProfilePage?,
|
||||
@SerialName("user")
|
||||
val user: UserProfile?
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UserProfilePage(
|
||||
@SerialName("pageInfo")
|
||||
val pageInfo: PageInfo,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Following(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Page")
|
||||
val page: FollowingPage?
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Follower(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Page")
|
||||
val page: FollowerPage?
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class FollowerPage(
|
||||
@SerialName("followers")
|
||||
val followers: List<ani.dantotsu.connections.anilist.api.User>?
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FollowingPage(
|
||||
@SerialName("following")
|
||||
val following: List<ani.dantotsu.connections.anilist.api.User>?
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserProfile(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@SerialName("about")
|
||||
val about: String?,
|
||||
@SerialName("avatar")
|
||||
val avatar: UserAvatar?,
|
||||
@SerialName("bannerImage")
|
||||
val bannerImage: String?,
|
||||
@SerialName("isFollowing")
|
||||
var isFollowing: Boolean,
|
||||
@SerialName("isFollower")
|
||||
val isFollower: Boolean,
|
||||
@SerialName("isBlocked")
|
||||
val isBlocked: Boolean,
|
||||
@SerialName("favourites")
|
||||
val favourites: UserFavourites?,
|
||||
@SerialName("statistics")
|
||||
val statistics: NNUserStatisticTypes,
|
||||
@SerialName("siteUrl")
|
||||
val siteUrl: String,
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NNUserStatisticTypes(
|
||||
@SerialName("anime") var anime: NNUserStatistics,
|
||||
@SerialName("manga") var manga: NNUserStatistics
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NNUserStatistics(
|
||||
@SerialName("count") var count: Int,
|
||||
@SerialName("meanScore") var meanScore: Float,
|
||||
@SerialName("standardDeviation") var standardDeviation: Float,
|
||||
@SerialName("minutesWatched") var minutesWatched: Int,
|
||||
@SerialName("episodesWatched") var episodesWatched: Int,
|
||||
@SerialName("chaptersRead") var chaptersRead: Int,
|
||||
@SerialName("volumesRead") var volumesRead: Int,
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserFavourites(
|
||||
@SerialName("anime")
|
||||
val anime: UserMediaFavouritesCollection,
|
||||
@SerialName("manga")
|
||||
val manga: UserMediaFavouritesCollection,
|
||||
@SerialName("characters")
|
||||
val characters: UserCharacterFavouritesCollection,
|
||||
@SerialName("staff")
|
||||
val staff: UserStaffFavouritesCollection,
|
||||
@SerialName("studios")
|
||||
val studios: UserStudioFavouritesCollection,
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserMediaFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserMediaImageFavorite>,
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserMediaImageFavorite(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("coverImage")
|
||||
val coverImage: MediaCoverImage
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserCharacterFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserCharacterImageFavorite>,
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserCharacterImageFavorite(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: CharacterName,
|
||||
@SerialName("image")
|
||||
val image: CharacterImage,
|
||||
@SerialName("isFavourite")
|
||||
val isFavourite: Boolean
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStaffFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStudioFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserStudioFavorite>,
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStudioFavorite(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
): java.io.Serializable
|
||||
|
||||
//----------------------------------------
|
||||
// Statistics
|
||||
|
||||
@Serializable
|
||||
data class StatisticsResponse(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
): java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("User")
|
||||
val user: StatisticsUser?
|
||||
): java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class StatisticsUser(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@SerialName("mediaListOptions")
|
||||
val mediaListOptions: MediaListOptions,
|
||||
@SerialName("statistics")
|
||||
val statistics: StatisticsTypes
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsTypes(
|
||||
@SerialName("anime")
|
||||
val anime: Statistics,
|
||||
@SerialName("manga")
|
||||
val manga: Statistics
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Statistics(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("standardDeviation")
|
||||
val standardDeviation: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("episodesWatched")
|
||||
val episodesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("volumesRead")
|
||||
val volumesRead: Int,
|
||||
@SerialName("formats")
|
||||
val formats: List<StatisticsFormat>,
|
||||
@SerialName("statuses")
|
||||
val statuses: List<StatisticsStatus>,
|
||||
@SerialName("scores")
|
||||
val scores: List<StatisticsScore>,
|
||||
@SerialName("lengths")
|
||||
val lengths: List<StatisticsLength>,
|
||||
@SerialName("releaseYears")
|
||||
val releaseYears: List<StatisticsReleaseYear>,
|
||||
@SerialName("startYears")
|
||||
val startYears: List<StatisticsStartYear>,
|
||||
@SerialName("genres")
|
||||
val genres: List<StatisticsGenre>,
|
||||
@SerialName("tags")
|
||||
val tags: List<StatisticsTag>,
|
||||
@SerialName("countries")
|
||||
val countries: List<StatisticsCountry>,
|
||||
@SerialName("voiceActors")
|
||||
val voiceActors: List<StatisticsVoiceActor>,
|
||||
@SerialName("staff")
|
||||
val staff: List<StatisticsStaff>,
|
||||
@SerialName("studios")
|
||||
val studios: List<StatisticsStudio>
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsFormat(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("format")
|
||||
val format: String
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsStatus(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("status")
|
||||
val status: String
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsScore(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("score")
|
||||
val score: Int
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsLength(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("length")
|
||||
val length: String? //can be null for manga
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsReleaseYear(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("releaseYear")
|
||||
val releaseYear: Int
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsStartYear(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("startYear")
|
||||
val startYear: Int
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsGenre(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("genre")
|
||||
val genre: String
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsTag(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("tag")
|
||||
val tag: Tag
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Tag(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsCountry(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("country")
|
||||
val country: String
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsVoiceActor(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("voiceActor")
|
||||
val voiceActor: VoiceActor,
|
||||
@SerialName("characterIds")
|
||||
val characterIds: List<Int>
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class VoiceActor(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: StaffName
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StaffName(
|
||||
@SerialName("first")
|
||||
val first: String?,
|
||||
@SerialName("middle")
|
||||
val middle: String?,
|
||||
@SerialName("last")
|
||||
val last: String?,
|
||||
@SerialName("full")
|
||||
val full: String?,
|
||||
@SerialName("native")
|
||||
val native: String?,
|
||||
@SerialName("alternative")
|
||||
val alternative: List<String>?,
|
||||
@SerialName("userPreferred")
|
||||
val userPreferred: String?
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsStaff(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("staff")
|
||||
val staff: VoiceActor
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsStudio(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("studio")
|
||||
val studio: StatStudio
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatStudio(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@SerialName("isAnimationStudio")
|
||||
val isAnimationStudio: Boolean
|
||||
) : java.io.Serializable
|
||||
|
||||
}
|
||||
|
||||
//data class WhaData(
|
||||
|
@ -203,7 +712,7 @@ class Query {
|
|||
// // Activity reply query
|
||||
// val ActivityReply: ActivityReply?,
|
||||
|
||||
// // Comment query
|
||||
// // CommentNotificationWorker query
|
||||
// val ThreadComment: List<ThreadComment>?,
|
||||
|
||||
// // Notification query
|
||||
|
|
114
app/src/main/java/ani/dantotsu/connections/anilist/api/Feed.kt
Normal file
114
app/src/main/java/ani/dantotsu/connections/anilist/api/Feed.kt
Normal 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
|
|
@ -251,7 +251,7 @@ data class MediaCoverImage(
|
|||
|
||||
// Average #hex color of cover image
|
||||
@SerialName("color") var color: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MediaList(
|
||||
|
@ -490,7 +490,7 @@ data class MediaExternalLink(
|
|||
|
||||
// isDisabled: Boolean
|
||||
@SerialName("notes") var notes: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class ExternalLinkType {
|
||||
|
@ -512,7 +512,13 @@ data class MediaListCollection(
|
|||
// If there is another chunk
|
||||
@SerialName("hasNextChunk") var hasNextChunk: Boolean?,
|
||||
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FollowData(
|
||||
@SerialName("id") var id: Int,
|
||||
@SerialName("isFollowing") var isFollowing: Boolean,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MediaListGroup(
|
||||
|
@ -526,4 +532,4 @@ data class MediaListGroup(
|
|||
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
|
||||
|
||||
@SerialName("status") var status: MediaListStatus?,
|
||||
)
|
||||
) : java.io.Serializable
|
|
@ -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
|
|
@ -15,7 +15,7 @@ data class Staff(
|
|||
@SerialName("languageV2") var languageV2: String?,
|
||||
|
||||
// The staff images
|
||||
// @SerialName("image") var image: StaffImage?,
|
||||
@SerialName("image") var image: StaffImage?,
|
||||
|
||||
// A general description of the staff member
|
||||
@SerialName("description") var description: String?,
|
||||
|
@ -93,7 +93,14 @@ data class StaffConnection(
|
|||
// The pagination information
|
||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
)
|
||||
@Serializable
|
||||
data class StaffImage(
|
||||
// The character's image of media at its largest size
|
||||
@SerialName("large") var large: String?,
|
||||
|
||||
// The character's image of media at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
) : java.io.Serializable
|
||||
@Serializable
|
||||
data class StaffEdge(
|
||||
var role: String?,
|
||||
|
|
|
@ -46,7 +46,7 @@ data class User(
|
|||
@SerialName("statistics") var statistics: UserStatisticTypes?,
|
||||
|
||||
// The number of unread notifications the user has
|
||||
// @SerialName("unreadNotificationCount") var unreadNotificationCount: Int?,
|
||||
@SerialName("unreadNotificationCount") var unreadNotificationCount: Int?,
|
||||
|
||||
// The url for the user page on the AniList website
|
||||
// @SerialName("siteUrl") var siteUrl: String?,
|
||||
|
@ -111,7 +111,7 @@ data class UserAvatar(
|
|||
|
||||
// The avatar of user at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
)
|
||||
): java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStatisticTypes(
|
||||
|
@ -164,7 +164,7 @@ data class Favourites(
|
|||
@Serializable
|
||||
data class MediaListOptions(
|
||||
// The score format the user is using for media lists
|
||||
// @SerialName("scoreFormat") var scoreFormat: ScoreFormat?,
|
||||
@SerialName("scoreFormat") var scoreFormat: String?,
|
||||
|
||||
// The default order list rows should be displayed in
|
||||
@SerialName("rowOrder") var rowOrder: String?,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,17 +1,18 @@
|
|||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.util.Logger
|
||||
|
||||
class CrashlyticsStub : CrashlyticsInterface {
|
||||
override fun initialize(context: Context) {
|
||||
//no-op
|
||||
}
|
||||
override fun logException(e: Throwable) {
|
||||
//no-op
|
||||
Logger.log(e)
|
||||
}
|
||||
|
||||
override fun log(message: String) {
|
||||
//no-op
|
||||
Logger.log(message)
|
||||
}
|
||||
|
||||
override fun setUserId(id: String) {
|
||||
|
|
|
@ -70,19 +70,5 @@ object Discord {
|
|||
|
||||
const val application_Id = "1163925779692912771"
|
||||
const val small_Image: String =
|
||||
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png"
|
||||
/*fun defaultRPC(): RPC? {
|
||||
return token?.let {
|
||||
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/"))
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
"mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
|
||||
}
|
|
@ -15,7 +15,6 @@ import android.os.Environment
|
|||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
|
@ -26,6 +25,7 @@ import ani.dantotsu.connections.discord.serializers.User
|
|||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
|
@ -274,7 +274,7 @@ class DiscordService : Service() {
|
|||
return
|
||||
}
|
||||
}
|
||||
t.message?.let { Log.d("WebSocket", "onFailure() $it") }
|
||||
t.message?.let { Logger.log("onFailure() $it") }
|
||||
log("WebSocket: Error, onFailure() reason: ${t.message}")
|
||||
client = OkHttpClient()
|
||||
client.newWebSocket(
|
||||
|
@ -289,7 +289,7 @@ class DiscordService : Service() {
|
|||
|
||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||
super.onClosing(webSocket, code, reason)
|
||||
Log.d("WebSocket", "onClosing() $code $reason")
|
||||
Logger.log("onClosing() $code $reason")
|
||||
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
|
||||
heartbeatThread.interrupt()
|
||||
}
|
||||
|
@ -297,7 +297,7 @@ class DiscordService : Service() {
|
|||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
super.onClosed(webSocket, code, reason)
|
||||
Log.d("WebSocket", "onClosed() $code $reason")
|
||||
Logger.log("onClosed() $code $reason")
|
||||
if (code >= 4000) {
|
||||
log("WebSocket: Error, code: $code reason: $reason")
|
||||
client = OkHttpClient()
|
||||
|
@ -382,52 +382,7 @@ class DiscordService : Service() {
|
|||
}
|
||||
|
||||
fun log(string: String) {
|
||||
Log.d("WebSocket_Discord", string)
|
||||
//log += "${SimpleDateFormat("HH:mm:ss").format(Calendar.getInstance().time)} $string\n"
|
||||
}
|
||||
|
||||
fun saveLogToFile() {
|
||||
val fileName = "log_${System.currentTimeMillis()}.txt"
|
||||
|
||||
// ContentValues to store file metadata
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
|
||||
}
|
||||
}
|
||||
|
||||
// Inserting the file in the MediaStore
|
||||
val resolver = baseContext.contentResolver
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
} else {
|
||||
val directory =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(directory, fileName)
|
||||
|
||||
// Make sure the Downloads directory exists
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
// Use FileProvider to get the URI for the file
|
||||
val authority =
|
||||
"${baseContext.packageName}.provider" // Adjust with your app's package name
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
|
||||
// Writing to the file
|
||||
uri?.let {
|
||||
resolver.openOutputStream(it).use { outputStream ->
|
||||
OutputStreamWriter(outputStream).use { writer ->
|
||||
writer.write(log)
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
log("Error saving log file")
|
||||
}
|
||||
//Logger.log(string)
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
|
|
|
@ -2,6 +2,8 @@ package ani.dantotsu.connections.discord
|
|||
|
||||
import ani.dantotsu.connections.discord.serializers.Activity
|
||||
import ani.dantotsu.connections.discord.serializers.Presence
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
@ -81,7 +83,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
|||
),
|
||||
afk = true,
|
||||
since = data.startTimestamp,
|
||||
status = data.status
|
||||
status = PrefManager.getVal(PrefName.DiscordStatus)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import ani.dantotsu.download.DownloadedType
|
|||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
|
@ -54,6 +54,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
@ -249,7 +250,7 @@ class AnimeDownloaderService : Service() {
|
|||
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
|
||||
|
||||
if (!downloadStarted) {
|
||||
logger("Download failed to start")
|
||||
Logger.log("Download failed to start")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download failed to start")
|
||||
|
@ -263,11 +264,11 @@ class AnimeDownloaderService : Service() {
|
|||
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||
if (download != null) {
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
|
||||
logger("Download failed")
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download failed")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download failed")
|
||||
logger("Download failed: ${download.failureReason}")
|
||||
Logger.log("Download failed: ${download.failureReason}")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
|
@ -289,7 +290,7 @@ class AnimeDownloaderService : Service() {
|
|||
break
|
||||
}
|
||||
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")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download completed")
|
||||
|
@ -309,7 +310,7 @@ class AnimeDownloaderService : Service() {
|
|||
break
|
||||
}
|
||||
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")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download stopped")
|
||||
|
@ -328,7 +329,7 @@ class AnimeDownloaderService : Service() {
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
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}")
|
||||
e.printStackTrace()
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
|
@ -355,15 +356,13 @@ class AnimeDownloaderService : Service() {
|
|||
return false
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: AnimeDownloadTask) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
launchIO {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${DownloadsManager.animeLocation}/${task.title}"
|
||||
)
|
||||
val episodeDirectory = File(directory, task.episode)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
|
|
|
@ -33,7 +33,7 @@ import ani.dantotsu.currContext
|
|||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
|
@ -318,8 +318,8 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||
val mediaJson = media.readText()
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
null
|
||||
}
|
||||
|
@ -374,8 +374,8 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
|||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineAnimeModel(
|
||||
"unknown",
|
||||
|
|
|
@ -21,7 +21,7 @@ import ani.dantotsu.R
|
|||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -47,6 +48,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
@ -251,7 +253,7 @@ class MangaDownloaderService : Service() {
|
|||
snackString("${task.title} - ${task.chapter} Download finished")
|
||||
}
|
||||
} 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}")
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
broadcastDownloadFailed(task.chapter)
|
||||
|
@ -287,8 +289,9 @@ class MangaDownloaderService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
launchIO {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${task.title}"
|
||||
|
|
|
@ -30,7 +30,7 @@ import ani.dantotsu.currContext
|
|||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
|
@ -308,8 +308,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
val mediaJson = media.readText()
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
null
|
||||
}
|
||||
|
@ -358,8 +358,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
|||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
|
|
|
@ -20,7 +20,7 @@ import ani.dantotsu.R
|
|||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.novel.NovelReadFragment
|
||||
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.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -42,6 +43,7 @@ import kotlinx.coroutines.withContext
|
|||
import okhttp3.Request
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
|
@ -186,15 +188,15 @@ class NovelDownloaderService : Service() {
|
|||
val contentType = response.header("Content-Type")
|
||||
val contentDisposition = response.header("Content-Disposition")
|
||||
|
||||
logger("Content-Type: $contentType")
|
||||
logger("Content-Disposition: $contentDisposition")
|
||||
Logger.log("Content-Type: $contentType")
|
||||
Logger.log("Content-Disposition: $contentDisposition")
|
||||
|
||||
// Return true if the Content-Type or Content-Disposition indicates an EPUB file
|
||||
contentType == "application/epub+zip" ||
|
||||
(contentDisposition?.contains(".epub") == true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Error checking file type: ${e.message}")
|
||||
Logger.log("Error checking file type: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -225,12 +227,12 @@ class NovelDownloaderService : Service() {
|
|||
|
||||
if (!isEpubFile(task.downloadLink)) {
|
||||
if (isAlreadyDownloaded(task.originalLink)) {
|
||||
logger("Already downloaded")
|
||||
Logger.log("Already downloaded")
|
||||
broadcastDownloadFinished(task.originalLink)
|
||||
snackString("Already downloaded")
|
||||
return@withContext
|
||||
}
|
||||
logger("Download link is not an .epub file")
|
||||
Logger.log("Download link is not an .epub file")
|
||||
broadcastDownloadFailed(task.originalLink)
|
||||
snackString("Download link is not an .epub file")
|
||||
return@withContext
|
||||
|
@ -301,7 +303,7 @@ class NovelDownloaderService : Service() {
|
|||
withContext(Dispatchers.Main) {
|
||||
val progress =
|
||||
(downloadedBytes * 100 / totalBytes).toInt()
|
||||
logger("Download progress: $progress")
|
||||
Logger.log("Download progress: $progress")
|
||||
broadcastDownloadProgress(task.originalLink, progress)
|
||||
}
|
||||
lastBroadcastUpdate = downloadedBytes
|
||||
|
@ -316,7 +318,7 @@ class NovelDownloaderService : Service() {
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Exception while downloading .epub inside request: ${e.message}")
|
||||
Logger.log("Exception while downloading .epub inside request: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
@ -340,15 +342,16 @@ class NovelDownloaderService : Service() {
|
|||
snackString("${task.title} - ${task.chapter} Download finished")
|
||||
}
|
||||
} 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}")
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
broadcastDownloadFailed(task.originalLink)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
launchIO {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${task.title}"
|
||||
|
|
|
@ -43,6 +43,7 @@ import ani.dantotsu.parsers.SubtitleType
|
|||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
@ -157,15 +158,15 @@ object Helper {
|
|||
finalException: Exception?
|
||||
) {
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
Log.e("Downloader", "Download Completed")
|
||||
Logger.log("Download Completed")
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
Log.e("Downloader", "Download Failed")
|
||||
Logger.log("Download Failed")
|
||||
} else if (download.state == Download.STATE_STOPPED) {
|
||||
Log.e("Downloader", "Download Stopped")
|
||||
Logger.log("Download Stopped")
|
||||
} else if (download.state == Download.STATE_QUEUED) {
|
||||
Log.e("Downloader", "Download Queued")
|
||||
Logger.log("Download Queued")
|
||||
} else if (download.state == Download.STATE_DOWNLOADING) {
|
||||
Log.e("Downloader", "Download Downloading")
|
||||
Logger.log("Download Downloading")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -283,7 +283,6 @@ class AnimeFragment : Fragment() {
|
|||
binding.root.requestApplyInsets()
|
||||
binding.root.requestLayout()
|
||||
}
|
||||
|
||||
super.onResume()
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import ani.dantotsu.media.CalendarActivity
|
|||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
|
@ -94,6 +95,17 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
|||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
|
||||
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(
|
||||
binding.animePreviousSeason,
|
||||
|
|
|
@ -21,6 +21,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
|
@ -32,6 +33,7 @@ import ani.dantotsu.media.Media
|
|||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.user.ListActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
import ani.dantotsu.setSlideUp
|
||||
|
@ -77,8 +79,10 @@ class HomeFragment : Fragment() {
|
|||
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
|
||||
binding.homeUserAvatar.loadImage(Anilist.avatar)
|
||||
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.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
|
||||
binding.homeAnimeList.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
|
@ -118,6 +122,13 @@ class HomeFragment : Fragment() {
|
|||
"dialog"
|
||||
)
|
||||
}
|
||||
binding.homeUserAvatarContainer.setOnLongClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
|
||||
.putExtra("userId", Anilist.userid),null
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
|
@ -127,6 +138,8 @@ class HomeFragment : Fragment() {
|
|||
|
||||
var reached = false
|
||||
val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (!binding.homeScroll.canScrollVertically(1)) {
|
||||
reached = true
|
||||
|
@ -141,6 +154,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var height = statusBarHeight
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
|
||||
|
@ -305,6 +319,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
val array = arrayOf(
|
||||
"AnimeContinue",
|
||||
"AnimeFav",
|
||||
|
@ -357,9 +372,12 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ import ani.dantotsu.openLinkInBrowser
|
|||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
|
||||
class LoginFragment : Fragment() {
|
||||
|
@ -50,7 +51,7 @@ class LoginFragment : Fragment() {
|
|||
DocumentFile.fromSingleUri(requireActivity(), uri)?.name ?: "settings"
|
||||
//.sani is encrypted, .ani is not
|
||||
if (name.endsWith(".sani")) {
|
||||
passwordAlertDialog() { password ->
|
||||
passwordAlertDialog { password ->
|
||||
if (password != null) {
|
||||
val salt = jsonString.copyOfRange(0, 16)
|
||||
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
||||
|
@ -78,7 +79,7 @@ class LoginFragment : Fragment() {
|
|||
toast("Invalid file type")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Logger.log(e)
|
||||
toast("Error importing settings")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import ani.dantotsu.loadImage
|
|||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
|
@ -74,7 +75,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||
}
|
||||
|
||||
updateAvatar()
|
||||
|
||||
binding.mangaNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
binding.mangaSearchBar.hint = "MANGA"
|
||||
binding.mangaSearchBarText.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
|
@ -89,6 +91,14 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
|
||||
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.mangaSearchBarText.performClick()
|
||||
|
@ -145,8 +155,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
|||
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
binding.mangaTrendingViewPager.currentItem =
|
||||
binding.mangaTrendingViewPager.currentItem + 1
|
||||
binding.mangaTrendingViewPager.currentItem += 1
|
||||
}
|
||||
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
|
|
|
@ -3,7 +3,9 @@ package ani.dantotsu.media
|
|||
import java.io.Serializable
|
||||
|
||||
data class Author(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val id: Int,
|
||||
val name: String?,
|
||||
val image: String?,
|
||||
val role: String?,
|
||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
|
||||
) : Serializable
|
||||
|
|
60
app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt
Normal file
60
app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -80,14 +80,13 @@ class CalendarActivity : AppCompatActivity() {
|
|||
)
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.listTitle.setText(R.string.release_calendar)
|
||||
binding.listSort.visibility = View.GONE
|
||||
|
||||
binding.random.visibility = View.GONE
|
||||
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
|
||||
|
|
|
@ -9,7 +9,7 @@ data class Character(
|
|||
val image: String?,
|
||||
val banner: String?,
|
||||
val role: String,
|
||||
|
||||
var isFav: Boolean,
|
||||
var description: String? = null,
|
||||
var age: String? = null,
|
||||
var gender: String? = null,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ani.dantotsu.media
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -15,20 +16,25 @@ import androidx.recyclerview.widget.ConcatAdapter
|
|||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistMutations
|
||||
import ani.dantotsu.databinding.ActivityCharacterBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.abs
|
||||
|
||||
class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
||||
|
@ -48,7 +54,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||
initActivity(this)
|
||||
screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.status)
|
||||
ContextCompat.getColor(this, R.color.transparent)
|
||||
|
||||
val banner =
|
||||
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
|
||||
|
@ -75,7 +81,39 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||
character.image
|
||||
)
|
||||
}
|
||||
|
||||
val link = "https://anilist.co/character/${character.id}"
|
||||
binding.characterShare.setOnClickListener {
|
||||
val i = Intent(Intent.ACTION_SEND)
|
||||
i.type = "text/plain"
|
||||
i.putExtra(Intent.EXTRA_TEXT, link)
|
||||
startActivity(Intent.createChooser(i, character.name))
|
||||
}
|
||||
binding.characterShare.setOnLongClickListener {
|
||||
openLinkInBrowser(link)
|
||||
true
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
character.isFav = Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.characterFav.setImageResource(
|
||||
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.characterFav.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, character.id)) {
|
||||
character.isFav = !character.isFav
|
||||
binding.characterFav.setImageResource(
|
||||
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
|
||||
)
|
||||
} else {
|
||||
snackString("Failed to toggle favorite")
|
||||
}
|
||||
}
|
||||
}
|
||||
model.getCharacter().observe(this) {
|
||||
if (it != null && !loaded) {
|
||||
character = it
|
||||
|
@ -139,13 +177,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
|||
isCollapsed = true
|
||||
if (immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg)
|
||||
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
|
||||
}
|
||||
if (percentage <= percent && isCollapsed) {
|
||||
isCollapsed = false
|
||||
if (immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.status)
|
||||
binding.characterAppBar.setBackgroundResource(R.color.bg)
|
||||
ContextCompat.getColor(this, R.color.transparent)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
|||
binding.characterDesc.isTextSelectable
|
||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(SpoilerPlugin()).build()
|
||||
markWon.setMarkdown(binding.characterDesc, desc)
|
||||
markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -67,11 +67,12 @@ class GenreActivity : AppCompatActivity() {
|
|||
|
||||
private fun loadLocalGenres(): ArrayList<String>? {
|
||||
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList)
|
||||
.toMutableList() as ArrayList<String>?
|
||||
return if (genres.isNullOrEmpty()) {
|
||||
.toMutableList()
|
||||
return if (genres.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
genres
|
||||
//sort alphabetically
|
||||
genres.sort().let { genres as ArrayList<String> }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -58,6 +58,7 @@ data class Media(
|
|||
var endDate: FuzzyDate? = null,
|
||||
|
||||
var characters: ArrayList<Character>? = null,
|
||||
var staff: ArrayList<Author>? = null,
|
||||
var prequel: Media? = null,
|
||||
var sequel: Media? = null,
|
||||
var relations: ArrayList<Media>? = null,
|
||||
|
@ -108,6 +109,7 @@ data class Media(
|
|||
this.userScore = mediaList.score?.toInt() ?: 0
|
||||
this.userStatus = mediaList.status?.toString()
|
||||
this.userUpdatedAt = mediaList.updatedAt?.toLong()
|
||||
this.genres = mediaList.media?.genres?.toMutableList() as? ArrayList<String>? ?: arrayListOf()
|
||||
}
|
||||
|
||||
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {
|
||||
|
|
|
@ -43,6 +43,7 @@ class MediaAdaptor(
|
|||
private val activity: FragmentActivity,
|
||||
private val matchParent: Boolean = false,
|
||||
private val viewPager: ViewPager2? = null,
|
||||
private val fav: Boolean = false,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
@ -128,6 +129,7 @@ class MediaAdaptor(
|
|||
)
|
||||
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)
|
||||
if (media != null) {
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
b.itemCompactBanner.loadImage(media.banner ?: media.cover)
|
||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
|
@ -185,15 +187,7 @@ class MediaAdaptor(
|
|||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
val banner =
|
||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val context = b.itemCompactBanner.context
|
||||
if (!(context as Activity).isDestroyed)
|
||||
Glide.with(context as Context)
|
||||
.load(GlideUrl(media.banner ?: media.cover))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||
.into(banner)
|
||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
|
@ -242,15 +236,7 @@ class MediaAdaptor(
|
|||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
val banner =
|
||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val context = b.itemCompactBanner.context
|
||||
if (!(context as Activity).isDestroyed)
|
||||
Glide.with(context as Context)
|
||||
.load(GlideUrl(media.banner ?: media.cover))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||
.into(banner)
|
||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
|
|
|
@ -2,7 +2,9 @@ package ani.dantotsu.media
|
|||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.util.TypedValue
|
||||
|
@ -10,7 +12,9 @@ import android.view.GestureDetector
|
|||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
@ -18,6 +22,7 @@ import androidx.appcompat.content.res.AppCompatResources
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.bold
|
||||
import androidx.core.text.color
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
|
@ -25,21 +30,23 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import ani.dantotsu.CustomBottomNavBar
|
||||
import ani.dantotsu.GesturesListener
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.ZoomOutPageTransformer
|
||||
import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.databinding.ActivityMediaBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
import ani.dantotsu.media.comments.CommentsFragment
|
||||
import ani.dantotsu.media.manga.MangaReadFragment
|
||||
import ani.dantotsu.media.novel.NovelReadFragment
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.AndroidBug5497Workaround
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
|
@ -49,20 +56,21 @@ import ani.dantotsu.statusBarHeight
|
|||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
||||
|
||||
private lateinit var binding: ActivityMediaBinding
|
||||
lateinit var binding: ActivityMediaBinding
|
||||
private val scope = lifecycleScope
|
||||
private val model: MediaDetailsViewModel by viewModels()
|
||||
private lateinit var tabLayout: NavigationBarView
|
||||
lateinit var tabLayout: TripleNavAdapter
|
||||
var selected = 0
|
||||
var anime = true
|
||||
private var adult = false
|
||||
|
@ -71,6 +79,15 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
||||
val id = intent.getIntExtra("mediaId", -1)
|
||||
if (id != -1) {
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
media =
|
||||
Anilist.query.getMedia(id, false) ?: emptyMedia()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (media.name == "No media found") {
|
||||
snackString(media.name)
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
|
@ -84,20 +101,31 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
setContentView(binding.root)
|
||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||
|
||||
val isVertical = resources.configuration.orientation
|
||||
//Ui init
|
||||
|
||||
initActivity(this)
|
||||
|
||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||
val oldMargin = binding.mediaViewPager.marginBottom
|
||||
AndroidBug5497Workaround.assistActivity(this) {
|
||||
if (it) {
|
||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = 0
|
||||
}
|
||||
binding.mediaTabContainer.visibility = View.GONE
|
||||
} else {
|
||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = oldMargin
|
||||
}
|
||||
binding.mediaTabContainer.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.mediaCollapsing.minimumHeight = statusBarHeight
|
||||
|
||||
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
|
||||
binding.mediaTitle.isSelected = true
|
||||
|
||||
mMaxScrollSize = binding.mediaAppBar.totalScrollRange
|
||||
|
@ -119,7 +147,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
val banner =
|
||||
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||
val viewPager = binding.mediaViewPager
|
||||
tabLayout = binding.mediaTab as NavigationBarView
|
||||
//tabLayout = binding.mediaTab as AnimatedBottomBar
|
||||
viewPager.isUserInputEnabled = false
|
||||
viewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
|
||||
|
@ -135,7 +163,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
media.cover
|
||||
)
|
||||
}
|
||||
banner.loadImage(media.banner ?: media.cover, 400)
|
||||
|
||||
blurImage(banner, media.banner ?: media.cover)
|
||||
val gestureDetector = GestureDetector(this, object : GesturesListener() {
|
||||
override fun onDoubleClick(event: MotionEvent) {
|
||||
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
|
||||
|
@ -313,49 +342,54 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
progress()
|
||||
}
|
||||
}
|
||||
|
||||
tabLayout = TripleNavAdapter(
|
||||
binding.mediaTab1,
|
||||
binding.mediaTab2,
|
||||
binding.mediaTab3,
|
||||
media.anime != null,
|
||||
media.format ?: "",
|
||||
isVertical == 1
|
||||
)
|
||||
adult = media.isAdult
|
||||
tabLayout.menu.clear()
|
||||
if (media.anime != null) {
|
||||
viewPager.adapter =
|
||||
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME)
|
||||
tabLayout.inflateMenu(R.menu.anime_menu_detail)
|
||||
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME, media, intent.getIntExtra("commentId", -1))
|
||||
} else if (media.manga != null) {
|
||||
viewPager.adapter = ViewPagerAdapter(
|
||||
supportFragmentManager,
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
selected = media.selected!!.window
|
||||
binding.mediaTitle.translationX = -screenWidth
|
||||
tabLayout.visibility = View.VISIBLE
|
||||
|
||||
tabLayout.setOnItemSelectedListener { item ->
|
||||
selectFromID(item.itemId)
|
||||
tabLayout.selectionListener = { selected, newId ->
|
||||
binding.commentInputLayout.visibility = if (selected == 2) View.VISIBLE else View.GONE
|
||||
this.selected = selected
|
||||
selectFromID(newId)
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
val sel = model.loadSelected(media, isDownload)
|
||||
sel.window = selected
|
||||
model.saveSelected(media.id, sel)
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
tabLayout.selectedItemId = idFromSelect()
|
||||
tabLayout.selectTab(selected)
|
||||
selectFromID(tabLayout.selected)
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
|
||||
if (model.continueMedia == null && media.cameFromContinue) {
|
||||
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
|
||||
selected = 1
|
||||
}
|
||||
val frag = intent.getStringExtra("FRAGMENT_TO_LOAD")
|
||||
if (frag != null) {
|
||||
selected = 2
|
||||
}
|
||||
|
||||
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
|
||||
live.observe(this) {
|
||||
|
@ -368,7 +402,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun selectFromID(id: Int) {
|
||||
when (id) {
|
||||
R.id.info -> {
|
||||
|
@ -378,6 +411,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
R.id.watch, R.id.read -> {
|
||||
selected = 1
|
||||
}
|
||||
|
||||
R.id.comment -> {
|
||||
selected = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,17 +422,19 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
if (anime) when (selected) {
|
||||
0 -> return R.id.info
|
||||
1 -> return R.id.watch
|
||||
2 -> return R.id.comment
|
||||
}
|
||||
else when (selected) {
|
||||
0 -> return R.id.info
|
||||
1 -> return R.id.read
|
||||
2 -> return R.id.comment
|
||||
}
|
||||
return R.id.info
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
if (this::tabLayout.isInitialized) {
|
||||
tabLayout.selectedItemId = idFromSelect()
|
||||
tabLayout.selectTab(selected)
|
||||
}
|
||||
super.onResume()
|
||||
}
|
||||
|
@ -408,19 +447,30 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
private class ViewPagerAdapter(
|
||||
fragmentManager: FragmentManager,
|
||||
lifecycle: Lifecycle,
|
||||
private val media: SupportedMedia
|
||||
private val mediaType: SupportedMedia,
|
||||
private val media: Media,
|
||||
private val commentId: Int
|
||||
) :
|
||||
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
override fun getItemCount(): Int = 2
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> MediaInfoFragment()
|
||||
1 -> when (media) {
|
||||
1 -> when (mediaType) {
|
||||
SupportedMedia.ANIME -> AnimeWatchFragment()
|
||||
SupportedMedia.MANGA -> MangaReadFragment()
|
||||
SupportedMedia.NOVEL -> NovelReadFragment()
|
||||
}
|
||||
2 -> {
|
||||
val fragment = CommentsFragment()
|
||||
val bundle = Bundle()
|
||||
bundle.putInt("mediaId", media.id)
|
||||
bundle.putString("mediaName", media.mainName())
|
||||
if (commentId != -1) bundle.putInt("commentId", commentId)
|
||||
fragment.arguments = bundle
|
||||
fragment
|
||||
}
|
||||
|
||||
else -> MediaInfoFragment()
|
||||
}
|
||||
|
@ -484,6 +534,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
private val c1: Int,
|
||||
private val c2: Int,
|
||||
var clicked: Boolean,
|
||||
needsInitialClick: Boolean = false,
|
||||
callback: suspend (Boolean) -> (Unit)
|
||||
) {
|
||||
private var disabled = false
|
||||
|
@ -492,6 +543,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
|
||||
init {
|
||||
enabled(true)
|
||||
if (needsInitialClick) {
|
||||
scope.launch {
|
||||
clicked()
|
||||
}
|
||||
}
|
||||
image.setOnClickListener {
|
||||
if (pressable && !disabled) {
|
||||
pressable = false
|
||||
|
@ -547,4 +603,3 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
|||
var mediaSingleton: Media? = null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import androidx.lifecycle.ViewModel
|
|||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
|
@ -223,7 +223,7 @@ class MediaDetailsViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
fun setEpisode(ep: Episode?, who: String) {
|
||||
logger("set episode ${ep?.number} - $who", false)
|
||||
Logger.log("set episode ${ep?.number} - $who")
|
||||
episode.postValue(ep)
|
||||
MainScope().launch(Dispatchers.Main) {
|
||||
episode.value = null
|
||||
|
@ -270,7 +270,7 @@ class MediaDetailsViewModel : ViewModel() {
|
|||
mangaChapters
|
||||
|
||||
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 {
|
||||
mangaLoaded[i] =
|
||||
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
||||
|
|
|
@ -3,6 +3,7 @@ package ani.dantotsu.media
|
|||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.CountDownTimer
|
||||
|
@ -73,6 +74,8 @@ class MediaInfoFragment : Fragment() {
|
|||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
if (media != null && !loaded) {
|
||||
loaded = true
|
||||
|
||||
|
||||
binding.mediaInfoProgressBar.visibility = View.GONE
|
||||
binding.mediaInfoContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
|
||||
|
@ -408,23 +411,6 @@ class MediaInfoFragment : Fragment() {
|
|||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
bind.itemTitle.setText(R.string.characters)
|
||||
bind.itemRecycler.adapter =
|
||||
CharacterAdapter(media.characters!!)
|
||||
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (!media.relations.isNullOrEmpty() && !offline) {
|
||||
if (media.sequel != null || media.prequel != null) {
|
||||
val bind = ItemQuelsBinding.inflate(
|
||||
|
@ -487,7 +473,38 @@ class MediaInfoFragment : Fragment() {
|
|||
)
|
||||
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) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
|
|
|
@ -254,20 +254,28 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
binding.mediaListDelete.setOnClickListener {
|
||||
val id = media!!.userListId
|
||||
if (id != null) {
|
||||
var id = media!!.userListId
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
Anilist.mutation.deleteList(id)
|
||||
if (id != null) {
|
||||
Anilist.mutation.deleteList(id!!)
|
||||
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
||||
} else {
|
||||
val profile = Anilist.query.userMediaDetails(media!!)
|
||||
profile.userListId?.let { listId ->
|
||||
id = listId
|
||||
Anilist.mutation.deleteList(listId)
|
||||
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
Refresh.all()
|
||||
snackString(getString(R.string.deleted_from_list))
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
} else {
|
||||
snackString(getString(R.string.no_list_id))
|
||||
Refresh.all()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,40 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||
val scope = viewLifecycleOwner.lifecycleScope
|
||||
binding.mediaListDelete.setOnClickListener {
|
||||
var id = media.userListId
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (id != null) {
|
||||
try {
|
||||
Anilist.mutation.deleteList(id!!)
|
||||
MAL.query.deleteList(media.anime != null, media.idMAL)
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
snackString("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.mediaListLayout.visibility = View.VISIBLE
|
||||
|
|
|
@ -199,7 +199,9 @@ class SearchActivity : AppCompatActivity() {
|
|||
|
||||
var state: Parcelable? = null
|
||||
override fun onPause() {
|
||||
if (this::headerAdaptor.isInitialized) {
|
||||
headerAdaptor.addHistory()
|
||||
}
|
||||
super.onPause()
|
||||
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
|
@ -14,6 +15,7 @@ import android.view.inputmethod.EditorInfo
|
|||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
||||
|
@ -23,6 +25,7 @@ import ani.dantotsu.connections.anilist.Anilist
|
|||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.databinding.ItemSearchHeaderBinding
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.imagesearch.ImageSearchActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.google.android.material.checkbox.MaterialCheckBox.*
|
||||
|
@ -92,13 +95,16 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
|||
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
|
||||
activity.updateChips = { it.update() }
|
||||
}
|
||||
|
||||
binding.searchChipRecycler.layoutManager =
|
||||
LinearLayoutManager(binding.root.context, HORIZONTAL, false)
|
||||
|
||||
binding.searchFilter.setOnClickListener {
|
||||
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
|
||||
}
|
||||
|
||||
binding.searchByImage.setOnClickListener {
|
||||
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
|
||||
}
|
||||
fun searchTitle() {
|
||||
activity.result.apply {
|
||||
search =
|
||||
|
@ -208,13 +214,16 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
|||
binding.searchHistoryList.startAnimation(fadeInAnimation())
|
||||
binding.searchResultLayout.visibility = View.GONE
|
||||
binding.searchHistoryList.visibility = View.VISIBLE
|
||||
binding.searchByImage.visibility = View.VISIBLE
|
||||
} else {
|
||||
if (binding.searchResultLayout.visibility != View.VISIBLE) {
|
||||
binding.searchResultLayout.startAnimation(fadeInAnimation())
|
||||
binding.searchHistoryList.startAnimation(fadeOutAnimation())
|
||||
}
|
||||
|
||||
binding.searchResultLayout.visibility = View.VISIBLE
|
||||
binding.searchHistoryList.visibility = View.GONE
|
||||
binding.searchByImage.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
|
|
136
app/src/main/java/ani/dantotsu/media/TripleNavAdapter.kt
Normal file
136
app/src/main/java/ani/dantotsu/media/TripleNavAdapter.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import java.util.regex.Pattern
|
|||
class AnimeNameAdapter {
|
||||
companion object {
|
||||
const val episodeRegex =
|
||||
"(episode|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||
"(episode|episodio|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||
const val failedEpisodeNumberRegex =
|
||||
"(?<!part\\s)\\b(\\d+)\\b"
|
||||
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Intent
|
|||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import ani.dantotsu.settings.FAQActivity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageButton
|
||||
|
@ -28,10 +29,9 @@ import ani.dantotsu.parsers.DynamicAnimeParser
|
|||
import ani.dantotsu.parsers.WatchSources
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
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 eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -59,15 +59,21 @@ class AnimeWatchAdapter(
|
|||
val binding = holder.binding
|
||||
_binding = binding
|
||||
|
||||
binding.faqbutton.setOnClickListener {
|
||||
startActivity(
|
||||
fragment.requireContext(),
|
||||
Intent(fragment.requireContext(), FAQActivity::class.java),
|
||||
null
|
||||
)
|
||||
}
|
||||
//Youtube
|
||||
if (media.anime!!.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
|
||||
if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
|
||||
binding.animeSourceYT.visibility = View.VISIBLE
|
||||
binding.animeSourceYT.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
|
||||
fragment.requireContext().startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
|
||||
binding.animeSourceDubbedText.text =
|
||||
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
|
||||
|
@ -179,7 +185,8 @@ class AnimeWatchAdapter(
|
|||
R.drawable.ic_round_notifications_none_24,
|
||||
R.color.bg_opp,
|
||||
R.color.violet_400,
|
||||
fragment.subscribed
|
||||
fragment.subscribed,
|
||||
true
|
||||
) {
|
||||
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
|
||||
}
|
||||
|
@ -187,7 +194,7 @@ class AnimeWatchAdapter(
|
|||
subscribeButton(false)
|
||||
|
||||
binding.animeSourceSubscribe.setOnLongClickListener {
|
||||
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
|
||||
}
|
||||
|
||||
//Nested Button
|
||||
|
@ -421,13 +428,17 @@ class AnimeWatchAdapter(
|
|||
}
|
||||
|
||||
binding.animeSourceProgressBar.visibility = View.GONE
|
||||
if (media.anime.episodes!!.isNotEmpty())
|
||||
if (media.anime.episodes!!.isNotEmpty()) {
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
else
|
||||
binding.faqbutton.visibility = View.GONE}
|
||||
else {
|
||||
binding.animeSourceNotFound.visibility = View.VISIBLE
|
||||
binding.faqbutton.visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
binding.animeSourceContinue.visibility = View.GONE
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
binding.faqbutton.visibility = View.GONE
|
||||
clearChips()
|
||||
binding.animeSourceProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
|
|
|
@ -43,13 +43,9 @@ import ani.dantotsu.parsers.HAnimeSources
|
|||
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.subcriptions.Notifications
|
||||
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.navigationrail.NavigationRailView
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -333,16 +329,7 @@ class AnimeWatchFragment : Fragment() {
|
|||
var subscribed = false
|
||||
fun onNotificationPressed(subscribed: Boolean, source: String) {
|
||||
this.subscribed = subscribed
|
||||
saveSubscription(requireContext(), media, subscribed)
|
||||
if (!subscribed)
|
||||
Notifications.deleteChannel(requireContext(), getChannelId(true, media.id))
|
||||
else
|
||||
Notifications.createChannel(
|
||||
requireContext(),
|
||||
ANIME_GROUP,
|
||||
getChannelId(true, media.id),
|
||||
media.userPreferredName
|
||||
)
|
||||
saveSubscription(media, subscribed)
|
||||
snackString(
|
||||
if (subscribed) getString(R.string.subscribed_notification, source)
|
||||
else getString(R.string.unsubscribed_notification)
|
||||
|
@ -358,11 +345,9 @@ class AnimeWatchFragment : Fragment() {
|
|||
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
|
||||
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
|
||||
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||
try {
|
||||
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
||||
} catch (e: ClassCastException) {
|
||||
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
|
||||
}
|
||||
|
||||
activity.tabLayout.setVisibility(visibility)
|
||||
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
@ -561,6 +546,8 @@ class AnimeWatchFragment : Fragment() {
|
|||
super.onResume()
|
||||
binding.mediaInfoProgressBar.visibility = progress
|
||||
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
|
||||
|
||||
requireActivity().setNavigationTheme()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.media.AudioManager.*
|
|||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.CountDownTimer
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings.System
|
||||
|
@ -39,6 +40,7 @@ import androidx.annotation.RequiresApi
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -61,6 +63,7 @@ import androidx.media3.exoplayer.util.EventLogger
|
|||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.ui.*
|
||||
import androidx.media3.ui.CaptionStyleCompat.*
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.mediarouter.app.MediaRouteButton
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
|
@ -85,6 +88,7 @@ import ani.dantotsu.settings.PlayerSettingsActivity
|
|||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
|
@ -113,6 +117,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
private val resumePosition = "resumePosition"
|
||||
private val playerFullscreen = "playerFullscreen"
|
||||
private val playerOnPlay = "playerOnPlay"
|
||||
private var disappeared: Boolean = false
|
||||
private var functionstarted: Boolean = false
|
||||
|
||||
private lateinit var exoPlayer: ExoPlayer
|
||||
private var castPlayer: CastPlayer? = null
|
||||
|
@ -311,7 +317,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
2 -> ResourcesCompat.getFont(this, R.font.poppins)
|
||||
3 -> ResourcesCompat.getFont(this, R.font.poppins_thin)
|
||||
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)
|
||||
}
|
||||
playerView.subtitleView?.setStyle(
|
||||
|
@ -391,13 +398,20 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
object : OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
if (orientation in 45..135) {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) exoRotate.visibility =
|
||||
View.VISIBLE
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE) {
|
||||
exoRotate.visibility = View.VISIBLE
|
||||
}
|
||||
rotation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
} else if (orientation in 225..315) {
|
||||
if (rotation != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) exoRotate.visibility =
|
||||
View.VISIBLE
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -963,7 +977,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
episodeTitle.setSelection(currentEpisodeIndex)
|
||||
episodeTitle.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
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<*>) {}
|
||||
|
@ -975,6 +993,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
if (isInitialized) {
|
||||
nextEpisode { i ->
|
||||
updateAniProgress()
|
||||
disappeared = false
|
||||
functionstarted = false
|
||||
change(currentEpisodeIndex + i)
|
||||
}
|
||||
}
|
||||
|
@ -983,6 +1003,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
exoPrev = playerView.findViewById(R.id.exo_prev_ep)
|
||||
exoPrev.setOnClickListener {
|
||||
if (currentEpisodeIndex > 0) {
|
||||
disappeared = false
|
||||
change(currentEpisodeIndex - 1)
|
||||
} else
|
||||
snackString(getString(R.string.first_episode))
|
||||
|
@ -1243,10 +1264,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
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)
|
||||
list.add(media.id)
|
||||
PrefManager.setVal(PrefName.ContinuedAnime, list.toList())
|
||||
PrefManager.setCustomVal("continueAnimeList", list)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
extractor?.onVideoStopped(video)
|
||||
|
@ -1258,7 +1279,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
|
||||
subtitle = intent.getSerialized("subtitle")
|
||||
?: when (val subLang: String? =
|
||||
PrefManager.getCustomVal("subLang_${media.id}", null as String?)) {
|
||||
PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java)) {
|
||||
null -> {
|
||||
when (episode.selectedSubtitle) {
|
||||
null -> null
|
||||
|
@ -1370,8 +1391,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
|
||||
mediaItem = if (downloadedMediaItem == null) {
|
||||
val builder = MediaItem.Builder().setUri(video!!.file.url).setMimeType(mimeType)
|
||||
logger("url: ${video!!.file.url}")
|
||||
logger("mimeType: $mimeType")
|
||||
Logger.log("url: ${video!!.file.url}")
|
||||
Logger.log("mimeType: $mimeType")
|
||||
|
||||
if (sub != null) {
|
||||
val listofnotnullsubs = listOfNotNull(sub)
|
||||
|
@ -1447,10 +1468,26 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
|
||||
private fun buildExoplayer() {
|
||||
//Player
|
||||
val DEFAULT_MIN_BUFFER_MS = 600000
|
||||
val DEFAULT_MAX_BUFFER_MS = 600000
|
||||
val BUFFER_FOR_PLAYBACK_MS = 2500
|
||||
val BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000
|
||||
|
||||
val loadControl = DefaultLoadControl.Builder()
|
||||
.setBackBuffer(1000 * 60 * 2, true)
|
||||
.setBufferDurationsMs(
|
||||
DEFAULT_MIN_BUFFER_MS,
|
||||
DEFAULT_MAX_BUFFER_MS,
|
||||
BUFFER_FOR_PLAYBACK_MS,
|
||||
BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||
)
|
||||
.build()
|
||||
|
||||
hideSystemBars()
|
||||
exoPlayer = ExoPlayer.Builder(this)
|
||||
.setMediaSourceFactory(DefaultMediaSourceFactory(cacheFactory))
|
||||
.setTrackSelector(trackSelector)
|
||||
.setLoadControl(loadControl)
|
||||
.build().apply {
|
||||
playWhenReady = true
|
||||
this.playbackParameters = this@ExoplayerView.playbackParameters
|
||||
|
@ -1509,6 +1546,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
private fun releasePlayer() {
|
||||
isPlayerPlaying = exoPlayer.playWhenReady
|
||||
playbackPosition = exoPlayer.currentPosition
|
||||
disappeared = false
|
||||
functionstarted = false
|
||||
exoPlayer.release()
|
||||
VideoCache.release()
|
||||
mediaSession?.release()
|
||||
|
@ -1682,7 +1721,49 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
|
||||
val new = currentTimeStamp
|
||||
timeStampText.text = if (new != null) {
|
||||
fun disappearSkip() {
|
||||
functionstarted = true
|
||||
skipTimeButton.visibility = View.VISIBLE
|
||||
exoSkip.visibility = View.GONE
|
||||
skipTimeText.text = new.skipType.getType()
|
||||
skipTimeButton.setOnClickListener {
|
||||
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.ShowTimeStampButton)) {
|
||||
|
||||
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()
|
||||
|
@ -1690,18 +1771,19 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
|
||||
}
|
||||
}
|
||||
if (PrefManager.getVal(PrefName.AutoSkipOPED) && (new.skipType == "op" || new.skipType == "ed") && !skippedTimeStamps.contains(
|
||||
new
|
||||
)
|
||||
}
|
||||
if (PrefManager.getVal(PrefName.AutoSkipOPED) && (new.skipType == "op" || new.skipType == "ed")
|
||||
&& !skippedTimeStamps.contains(new)
|
||||
) {
|
||||
exoPlayer.seekTo((new.interval.endTime * 1000).toLong())
|
||||
skippedTimeStamps.add(new)
|
||||
}
|
||||
new.skipType.getType()
|
||||
} else {
|
||||
disappeared = false
|
||||
functionstarted = false
|
||||
skipTimeButton.visibility = View.GONE
|
||||
if (PrefManager.getVal<Int>(PrefName.SkipTime) > 0) exoSkip.visibility =
|
||||
View.VISIBLE
|
||||
exoSkip.isVisible = PrefManager.getVal<Int>(PrefName.SkipTime) > 0
|
||||
""
|
||||
}
|
||||
}
|
||||
|
@ -1772,20 +1854,26 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
|
||||
private fun updateAniProgress() {
|
||||
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
|
||||
) && Anilist.userid != null
|
||||
)
|
||||
val episode0 = currentEpisodeIndex == 0 && PrefManager.getVal(PrefName.ChapterZeroPlayer)
|
||||
if (!incognito && (episodeEnd || episode0) && Anilist.userid != null
|
||||
)
|
||||
if (PrefManager.getCustomVal(
|
||||
"${media.id}_save_progress",
|
||||
true
|
||||
) && (if (media.isAdult) PrefManager.getVal(PrefName.UpdateForHPlayer) else true)
|
||||
) {
|
||||
if (episode0) {
|
||||
updateProgress(media, "0")
|
||||
} else {
|
||||
media.anime!!.selectedEpisode?.apply {
|
||||
updateProgress(media, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun nextEpisode(toast: Boolean = true, runnable: ((Int) -> Unit)) {
|
||||
var isFiller = true
|
||||
|
@ -1822,6 +1910,8 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
|||
|
||||
if (isInitialized) {
|
||||
updateAniProgress()
|
||||
disappeared = false
|
||||
functionstarted = false
|
||||
releasePlayer()
|
||||
}
|
||||
|
||||
|
|
|
@ -181,11 +181,23 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
|||
model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex)
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.selectorProgressBar.visibility = View.GONE
|
||||
if (adapter.itemCount == 0) {
|
||||
snackString(getString(R.string.stream_selection_empty))
|
||||
tryWith {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
|
||||
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)) {
|
||||
adapter.performClick(0)
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
|||
binding.subtitleTitle.setText(R.string.none)
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
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") {
|
||||
binding.root.setCardBackgroundColor(TRANSPARENT)
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
|
|||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
val mediaID: Int = media.id
|
||||
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) {
|
||||
binding.root.setCardBackgroundColor(TRANSPARENT)
|
||||
}
|
||||
|
|
394
app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt
Normal file
394
app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt
Normal 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",
|
||||
)
|
||||
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import android.os.Build
|
|||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.LruCache
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.snackString
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
|
@ -32,8 +32,8 @@ data class ImageData(
|
|||
try {
|
||||
// Fetch the image
|
||||
val response = httpSource.getImage(page)
|
||||
logger("Response: ${response.code}")
|
||||
logger("Response: ${response.message}")
|
||||
Logger.log("Response: ${response.code}")
|
||||
Logger.log("Response: ${response.message}")
|
||||
|
||||
// Convert the Response to an InputStream
|
||||
val inputStream = response.body.byteStream()
|
||||
|
@ -47,7 +47,7 @@ data class ImageData(
|
|||
return@withContext bitmap
|
||||
} catch (e: Exception) {
|
||||
// Handle any exceptions
|
||||
logger("An error occurred: ${e.message}")
|
||||
Logger.log("An error occurred: ${e.message}")
|
||||
snackString("An error occurred: ${e.message}")
|
||||
return@withContext null
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ data class MangaChapter(
|
|||
var description: String? = null,
|
||||
var sChapter: SChapter,
|
||||
val scanlator: String? = null,
|
||||
val date: Long? = null,
|
||||
var progress: String? = ""
|
||||
) : Serializable {
|
||||
constructor(chapter: MangaChapter) : this(
|
||||
|
@ -21,7 +22,8 @@ data class MangaChapter(
|
|||
chapter.title,
|
||||
chapter.description,
|
||||
chapter.sChapter,
|
||||
chapter.scanlator
|
||||
chapter.scanlator,
|
||||
chapter.date
|
||||
)
|
||||
|
||||
private val images = mutableListOf<MangaImage>()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ani.dantotsu.media.manga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
|
@ -16,6 +17,9 @@ import ani.dantotsu.databinding.ItemChapterListBinding
|
|||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.setAnimation
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
@ -258,6 +262,7 @@ class MangaChapterAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is ChapterCompactViewHolder -> {
|
||||
|
@ -290,6 +295,23 @@ class MangaChapterAdapter(
|
|||
holder.bind(ep.number, ep.progress)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||
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()) {
|
||||
binding.itemChapterTitle.visibility = View.GONE
|
||||
} else binding.itemChapterTitle.visibility = View.VISIBLE
|
||||
|
@ -322,6 +344,33 @@ class MangaChapterAdapter(
|
|||
fun updateType(t: Int) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -12,6 +12,7 @@ import android.widget.ImageButton
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.NumberPicker
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.*
|
||||
|
@ -27,11 +28,11 @@ import ani.dantotsu.others.webview.CookieCatcher
|
|||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.MangaReadSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.settings.FAQActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
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 eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import kotlinx.coroutines.MainScope
|
||||
|
@ -63,6 +64,12 @@ class MangaReadAdapter(
|
|||
_binding = binding
|
||||
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
|
||||
binding.animeSourceSearch.setOnClickListener {
|
||||
SourceSearchDialogFragment().show(
|
||||
|
@ -142,7 +149,8 @@ class MangaReadAdapter(
|
|||
R.drawable.ic_round_notifications_none_24,
|
||||
R.color.bg_opp,
|
||||
R.color.violet_400,
|
||||
fragment.subscribed
|
||||
fragment.subscribed,
|
||||
true
|
||||
) {
|
||||
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
|
||||
}
|
||||
|
@ -150,7 +158,7 @@ class MangaReadAdapter(
|
|||
subscribeButton(false)
|
||||
|
||||
binding.animeSourceSubscribe.setOnLongClickListener {
|
||||
openSettings(fragment.requireContext(), getChannelId(true, media.id))
|
||||
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
|
||||
}
|
||||
|
||||
binding.animeNestedButton.setOnClickListener {
|
||||
|
@ -245,17 +253,41 @@ class MangaReadAdapter(
|
|||
if (options.count() > 1) View.VISIBLE else View.GONE
|
||||
dialogBinding.scanlatorNo.text = "${options.count()}"
|
||||
dialogBinding.animeScanlatorTop.setOnClickListener {
|
||||
val dialogView2 =
|
||||
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
|
||||
val checkboxContainer =
|
||||
dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
|
||||
val dialogView2 = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
|
||||
val checkboxContainer = dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
|
||||
val tickAllButton = dialogView2.findViewById<ImageButton>(R.id.toggleButton)
|
||||
|
||||
// Function to get the right image resource for the toggle button
|
||||
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
|
||||
options.forEach { option ->
|
||||
val checkBox = CheckBox(currContext()).apply {
|
||||
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) {
|
||||
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
|
@ -269,7 +301,6 @@ class MangaReadAdapter(
|
|||
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
.setView(dialogView2)
|
||||
.setPositiveButton("OK") { _, _ ->
|
||||
//add unchecked to hidden
|
||||
hiddenScanlators.clear()
|
||||
for (i in 0 until checkboxContainer.childCount) {
|
||||
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
|
||||
|
@ -283,6 +314,21 @@ class MangaReadAdapter(
|
|||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
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)
|
||||
|
@ -391,7 +437,7 @@ class MangaReadAdapter(
|
|||
if (media.manga?.chapters != null) {
|
||||
val chapters = media.manga.chapters!!.keys.toTypedArray()
|
||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||
val appEp = PrefManager.getCustomVal<String?>("${media.id}_current_chp", null)
|
||||
val appEp = PrefManager.getNullableCustomVal("${media.id}_current_chp", null, String::class.java)
|
||||
?.toIntOrNull() ?: 1
|
||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||
val filteredChapters = chapters.filter { chapterKey ->
|
||||
|
@ -435,13 +481,17 @@ class MangaReadAdapter(
|
|||
binding.animeSourceContinue.visibility = View.GONE
|
||||
}
|
||||
binding.animeSourceProgressBar.visibility = View.GONE
|
||||
if (media.manga.chapters!!.isNotEmpty())
|
||||
if (media.manga.chapters!!.isNotEmpty()) {
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
else
|
||||
binding.faqbutton.visibility = View.GONE
|
||||
} else {
|
||||
binding.animeSourceNotFound.visibility = View.VISIBLE
|
||||
binding.faqbutton.visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
binding.animeSourceContinue.visibility = View.GONE
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
binding.faqbutton.visibility = View.GONE
|
||||
clearChips()
|
||||
binding.animeSourceProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
|
|
|
@ -46,13 +46,9 @@ import ani.dantotsu.parsers.MangaSources
|
|||
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.subcriptions.Notifications
|
||||
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper
|
||||
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
|
||||
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.source.ConfigurableSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -347,16 +343,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
var subscribed = false
|
||||
fun onNotificationPressed(subscribed: Boolean, source: String) {
|
||||
this.subscribed = subscribed
|
||||
saveSubscription(requireContext(), media, subscribed)
|
||||
if (!subscribed)
|
||||
Notifications.deleteChannel(requireContext(), getChannelId(true, media.id))
|
||||
else
|
||||
Notifications.createChannel(
|
||||
requireContext(),
|
||||
MANGA_GROUP,
|
||||
getChannelId(true, media.id),
|
||||
media.userPreferredName
|
||||
)
|
||||
saveSubscription(media, subscribed)
|
||||
snackString(
|
||||
if (subscribed) getString(R.string.subscribed_notification, source)
|
||||
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<CardView>(R.id.mediaCover).visibility = visibility
|
||||
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
|
||||
try {
|
||||
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
|
||||
} catch (e: ClassCastException) {
|
||||
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
|
||||
}
|
||||
activity.tabLayout.setVisibility(visibility)
|
||||
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
|
||||
if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
@ -605,6 +588,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
|||
super.onResume()
|
||||
binding.mediaInfoProgressBar.visibility = progress
|
||||
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
|
||||
|
||||
requireActivity().setNavigationTheme()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
|
|
@ -88,6 +88,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
|
||||
private var isContVisible = false
|
||||
private var showProgressDialog = true
|
||||
private var hidescrollbar = false
|
||||
|
||||
private var maxChapterPage = 0L
|
||||
private var currentChapterPage = 0L
|
||||
|
@ -121,8 +122,11 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun hideBars() {
|
||||
if (!PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) hideSystemBars()
|
||||
private fun hideSystemBars() {
|
||||
if (PrefManager.getVal<Boolean>(PrefName.ShowSystemBars))
|
||||
showSystemBarsRetractView()
|
||||
else
|
||||
hideSystemBarsExtendView()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -147,12 +151,26 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
defaultSettings = loadReaderSettings("reader_settings") ?: defaultSettings
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
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()
|
||||
|
||||
hideBars()
|
||||
hideSystemBars()
|
||||
|
||||
var pageSliderTimer = Timer()
|
||||
fun pageSliderHide() {
|
||||
|
@ -285,17 +303,27 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
binding.mangaReaderNextChapter.performClick()
|
||||
}
|
||||
binding.mangaReaderNextChapter.setOnClickListener {
|
||||
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||
if (currentChapterIndex > 0) change(currentChapterIndex - 1)
|
||||
else snackString(getString(R.string.first_chapter))
|
||||
} else {
|
||||
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
|
||||
else snackString(getString(R.string.next_chapter_not_found))
|
||||
}
|
||||
}
|
||||
//Prev Chapter
|
||||
binding.mangaReaderPrevChap.setOnClickListener {
|
||||
binding.mangaReaderPreviousChapter.performClick()
|
||||
}
|
||||
binding.mangaReaderPreviousChapter.setOnClickListener {
|
||||
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||
if (chaptersArr.size > currentChapterIndex + 1) progress { change(currentChapterIndex + 1) }
|
||||
else snackString(getString(R.string.next_chapter_not_found))
|
||||
} else {
|
||||
if (currentChapterIndex > 0) change(currentChapterIndex - 1)
|
||||
else snackString(getString(R.string.first_chapter))
|
||||
}
|
||||
}
|
||||
|
||||
model.getMangaChapter().observe(this) { chap ->
|
||||
if (chap != null) {
|
||||
|
@ -305,10 +333,17 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
PrefManager.setCustomVal("${media.id}_current_chp", chap.number)
|
||||
currentChapterIndex = chaptersArr.indexOf(chap.number)
|
||||
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
|
||||
if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||
binding.mangaReaderNextChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
} else {
|
||||
binding.mangaReaderNextChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
}
|
||||
applySettings()
|
||||
val context = this
|
||||
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||
|
@ -377,7 +412,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
fun applySettings() {
|
||||
|
||||
saveReaderSettings("${media.id}_current_settings", defaultSettings)
|
||||
hideBars()
|
||||
hideSystemBars()
|
||||
|
||||
//true colors
|
||||
SubsamplingScaleImageView.setPreferredBitmapConfig(
|
||||
|
@ -422,6 +457,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
if ((defaultSettings.direction == TOP_TO_BOTTOM || defaultSettings.direction == BOTTOM_TO_TOP)) {
|
||||
binding.mangaReaderSwipy.vertical = true
|
||||
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)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||
|
@ -433,6 +472,10 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
binding.mangaReaderNextChapter.performClick()
|
||||
}
|
||||
} else {
|
||||
binding.mangaReaderNextChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||
|
@ -459,34 +502,36 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
} else {
|
||||
binding.mangaReaderSwipy.vertical = false
|
||||
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)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.mangaReaderSwipy.onLeftSwiped = {
|
||||
binding.mangaReaderNextChapter.performClick()
|
||||
}
|
||||
binding.mangaReaderSwipy.onRightSwiped = {
|
||||
binding.mangaReaderPreviousChapter.performClick()
|
||||
}
|
||||
} else {
|
||||
binding.mangaReaderNextChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
|
||||
binding.mangaReaderPrevChap.text =
|
||||
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
|
||||
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
|
||||
?: getString(R.string.no_chapter)
|
||||
}
|
||||
binding.mangaReaderSwipy.onLeftSwiped = {
|
||||
binding.mangaReaderPreviousChapter.performClick()
|
||||
}
|
||||
binding.mangaReaderSwipy.onRightSwiped = {
|
||||
binding.mangaReaderNextChapter.performClick()
|
||||
}
|
||||
}
|
||||
binding.mangaReaderSwipy.leftBeingSwiped = { value ->
|
||||
binding.LeftSwipeContainer.apply {
|
||||
alpha = value
|
||||
translationX = -width.dp * (1 - min(value, 1f))
|
||||
}
|
||||
}
|
||||
binding.mangaReaderSwipy.onRightSwiped = {
|
||||
binding.mangaReaderNextChapter.performClick()
|
||||
}
|
||||
binding.mangaReaderSwipy.rightBeingSwiped = { value ->
|
||||
binding.RightSwipeContainer.apply {
|
||||
alpha = value
|
||||
|
@ -710,7 +755,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
|
||||
if (screenWidth / 5 in x + 1..<y) {
|
||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT) {
|
||||
pressLocation = if (defaultSettings.direction == RIGHT_TO_LEFT || defaultSettings.direction == BOTTOM_TO_TOP) {
|
||||
pressPos.RIGHT
|
||||
} else {
|
||||
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
|
||||
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
|
||||
} else {
|
||||
pressPos.RIGHT
|
||||
|
@ -751,9 +796,41 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
if (!PrefManager.getVal<Boolean>(PrefName.ShowSystemBars)) {
|
||||
hideBars()
|
||||
hideSystemBars()
|
||||
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
|
||||
if (defaultSettings.horizontalScrollBar) {
|
||||
binding.mangaReaderSliderContainer.updateLayoutParams {
|
||||
|
@ -877,7 +954,7 @@ class MangaReaderActivity : AppCompatActivity() {
|
|||
dialog.dismiss()
|
||||
runnable.run()
|
||||
}
|
||||
.setOnCancelListener { hideBars() }
|
||||
.setOnCancelListener { hideSystemBars() }
|
||||
.create()
|
||||
.show()
|
||||
} else {
|
||||
|
|
|
@ -127,6 +127,12 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
|
|||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.readerHideScrollBar.isChecked = settings.hideScrollBar
|
||||
binding.readerHideScrollBar.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.hideScrollBar = isChecked
|
||||
activity.applySettings()
|
||||
}
|
||||
|
||||
binding.readerHidePageNumbers.isChecked = settings.hidePageNumbers
|
||||
binding.readerHidePageNumbers.setOnCheckedChangeListener { _, isChecked ->
|
||||
settings.hidePageNumbers = isChecked
|
||||
|
|
|
@ -31,6 +31,7 @@ import ani.dantotsu.media.MediaDetailsViewModel
|
|||
import ani.dantotsu.media.novel.novelreader.NovelReaderActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -59,7 +60,7 @@ class NovelReadFragment : Fragment(),
|
|||
var loaded = false
|
||||
|
||||
override fun downloadTrigger(novelDownloadPackage: NovelDownloadPackage) {
|
||||
Log.e("downloadTrigger", novelDownloadPackage.link)
|
||||
Logger.log("novel link: ${novelDownloadPackage.link}")
|
||||
val downloadTask = NovelDownloaderService.DownloadTask(
|
||||
title = media.mainName(),
|
||||
chapter = novelDownloadPackage.novelName,
|
||||
|
|
|
@ -5,12 +5,14 @@ import android.util.TypedValue
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ItemNovelResponseBinding
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
|
||||
|
@ -58,11 +60,11 @@ class NovelResponseAdapter(
|
|||
}
|
||||
if (binding.itemEpisodeFiller.text.contains("Downloading")) {
|
||||
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")) {
|
||||
binding.itemEpisodeFiller.setTextColor(
|
||||
fragment.requireContext().getColor(android.R.color.holo_green_light)
|
||||
ContextCompat.getColor(fragment.requireContext(), android.R.color.holo_green_light)
|
||||
)
|
||||
} else {
|
||||
binding.itemEpisodeFiller.setTextColor(color)
|
||||
|
@ -181,7 +183,7 @@ class NovelResponseAdapter(
|
|||
if (position != -1) {
|
||||
list[position].extra?.remove("0")
|
||||
list[position].extra?.set("0", "Downloading: $progress%")
|
||||
Log.d("NovelResponseAdapter", "updateDownloadProgress: $progress, position: $position")
|
||||
Logger.log( "updateDownloadProgress: $progress, position: $position")
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -291,7 +291,7 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
|||
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) }
|
||||
binding.progress.visibility = View.GONE
|
||||
|
|
|
@ -78,7 +78,6 @@ class ListActivity : AppCompatActivity() {
|
|||
)
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
setContentView(binding.root)
|
||||
|
@ -171,6 +170,21 @@ class ListActivity : AppCompatActivity() {
|
|||
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 {
|
||||
//get the current tab
|
||||
val currentTab =
|
||||
|
|
|
@ -13,10 +13,29 @@ class ListViewModel : ViewModel() {
|
|||
var grid = MutableLiveData(PrefManager.getVal<Boolean>(PrefName.ListGrid))
|
||||
|
||||
private val lists = MutableLiveData<MutableMap<String, ArrayList<Media>>>()
|
||||
private val unfilteredLists = MutableLiveData<MutableMap<String, ArrayList<Media>>>()
|
||||
fun getLists(): LiveData<MutableMap<String, ArrayList<Media>>> = lists
|
||||
suspend fun loadLists(anime: Boolean, userId: Int, sortOrder: String? = null) {
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ani.dantotsu.subcriptions
|
||||
package ani.dantotsu.notifications
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
|
@ -9,7 +9,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
|||
import ani.dantotsu.settings.saving.PrefName
|
||||
|
||||
|
||||
class NotificationClickReceiver : BroadcastReceiver() {
|
||||
class IncognitoNotificationClickReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
|
||||
PrefManager.setVal(PrefName.Incognito, false)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package ani.dantotsu.subcriptions
|
||||
package ani.dantotsu.notifications.subscription
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currContext
|
||||
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.PrefName
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
class SubscriptionHelper {
|
||||
companion object {
|
||||
private fun loadSelected(
|
||||
context: Context,
|
||||
mediaId: Int,
|
||||
isAdult: Boolean,
|
||||
isAnime: Boolean
|
||||
mediaId: Int
|
||||
): Selected {
|
||||
val data =
|
||||
PrefManager.getNullableCustomVal("${mediaId}-select", null, Selected::class.java)
|
||||
|
@ -37,26 +34,25 @@ class SubscriptionHelper {
|
|||
return data
|
||||
}
|
||||
|
||||
private fun saveSelected(context: Context, mediaId: Int, data: Selected) {
|
||||
private fun saveSelected( mediaId: Int, data: Selected) {
|
||||
PrefManager.setCustomVal("${mediaId}-select", data)
|
||||
}
|
||||
|
||||
fun getAnimeParser(context: Context, isAdult: Boolean, id: Int): AnimeParser {
|
||||
val sources = if (isAdult) HAnimeSources else AnimeSources
|
||||
val selected = loadSelected(context, id, isAdult, true)
|
||||
fun getAnimeParser(id: Int): AnimeParser {
|
||||
val sources = AnimeSources
|
||||
Logger.log("getAnimeParser size: ${sources.list.size}")
|
||||
val selected = loadSelected(id)
|
||||
val parser = sources[selected.sourceIndex]
|
||||
parser.selectDub = selected.preferDub
|
||||
return parser
|
||||
}
|
||||
|
||||
suspend fun getEpisode(
|
||||
context: Context,
|
||||
parser: AnimeParser,
|
||||
id: Int,
|
||||
isAdult: Boolean
|
||||
id: Int
|
||||
): Episode? {
|
||||
|
||||
val selected = loadSelected(context, id, isAdult, true)
|
||||
val selected = loadSelected(id)
|
||||
val ep = withTimeoutOrNull(10 * 1000) {
|
||||
tryWithSuspend {
|
||||
val show = parser.loadSavedShowResponse(id) ?: throw Exception(
|
||||
|
@ -76,23 +72,21 @@ class SubscriptionHelper {
|
|||
|
||||
return ep?.apply {
|
||||
selected.latest = number.toFloat()
|
||||
saveSelected(context, id, selected)
|
||||
saveSelected(id, selected)
|
||||
}
|
||||
}
|
||||
|
||||
fun getMangaParser(context: Context, isAdult: Boolean, id: Int): MangaParser {
|
||||
val sources = if (isAdult) HMangaSources else MangaSources
|
||||
val selected = loadSelected(context, id, isAdult, false)
|
||||
fun getMangaParser(id: Int): MangaParser {
|
||||
val sources = MangaSources
|
||||
val selected = loadSelected(id)
|
||||
return sources[selected.sourceIndex]
|
||||
}
|
||||
|
||||
suspend fun getChapter(
|
||||
context: Context,
|
||||
parser: MangaParser,
|
||||
id: Int,
|
||||
isAdult: Boolean
|
||||
id: Int
|
||||
): MangaChapter? {
|
||||
val selected = loadSelected(context, id, isAdult, true)
|
||||
val selected = loadSelected(id)
|
||||
val chp = withTimeoutOrNull(10 * 1000) {
|
||||
tryWithSuspend {
|
||||
val show = parser.loadSavedShowResponse(id) ?: throw Exception(
|
||||
|
@ -112,7 +106,7 @@ class SubscriptionHelper {
|
|||
|
||||
return chp?.apply {
|
||||
selected.latest = MangaNameAdapter.findChapterNumber(number) ?: 0f
|
||||
saveSelected(context, id, selected)
|
||||
saveSelected(id, selected)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,21 +118,21 @@ class SubscriptionHelper {
|
|||
val image: String?
|
||||
) : java.io.Serializable
|
||||
|
||||
private const val subscriptions = "subscriptions"
|
||||
private const val SUBSCRIPTIONS = "subscriptions"
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun getSubscriptions(): Map<Int, SubscribeMedia> =
|
||||
(PrefManager.getNullableCustomVal(
|
||||
subscriptions,
|
||||
SUBSCRIPTIONS,
|
||||
null,
|
||||
Map::class.java
|
||||
) as? Map<Int, SubscribeMedia>)
|
||||
?: mapOf<Int, SubscribeMedia>().also { PrefManager.setCustomVal(subscriptions, it) }
|
||||
?: mapOf<Int, SubscribeMedia>().also { PrefManager.setCustomVal(SUBSCRIPTIONS, it) }
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun saveSubscription(context: Context, media: Media, subscribed: Boolean) {
|
||||
fun saveSubscription(media: Media, subscribed: Boolean) {
|
||||
val data = PrefManager.getNullableCustomVal(
|
||||
subscriptions,
|
||||
SUBSCRIPTIONS,
|
||||
null,
|
||||
Map::class.java
|
||||
) as? MutableMap<Int, SubscribeMedia>
|
||||
|
@ -157,7 +151,7 @@ class SubscriptionHelper {
|
|||
} else {
|
||||
data.remove(media.id)
|
||||
}
|
||||
PrefManager.setCustomVal(subscriptions, data)
|
||||
PrefManager.setCustomVal(SUBSCRIPTIONS, data)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package ani.dantotsu.others
|
||||
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.util.Logger
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.pow
|
||||
|
||||
|
@ -64,7 +64,7 @@ class JsUnpacker(packedJS: String?) {
|
|||
return decoded.toString()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger(e)
|
||||
Logger.log(e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package ani.dantotsu.others
|
|||
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
|
@ -41,8 +41,7 @@ object Kitsu {
|
|||
}
|
||||
|
||||
suspend fun getKitsuEpisodesDetails(media: Media): Map<String, Episode>? {
|
||||
val print = false
|
||||
logger("Kitsu : title=${media.mainName()}", print)
|
||||
Logger.log("Kitsu : title=${media.mainName()}")
|
||||
val query =
|
||||
"""
|
||||
query {
|
||||
|
@ -70,7 +69,7 @@ query {
|
|||
|
||||
|
||||
val result = getKitsuData(query) ?: return null
|
||||
logger("Kitsu : result=$result", print)
|
||||
//Logger.log("Kitsu : result=$result")
|
||||
media.idKitsu = result.data?.lookupMapping?.id
|
||||
val a = (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep ->
|
||||
val num = ep?.number?.toString() ?: return@mapNotNull null
|
||||
|
@ -81,7 +80,7 @@ query {
|
|||
thumb = FileUrl[ep.thumbnail?.original?.url],
|
||||
)
|
||||
}.toMap()
|
||||
logger("Kitsu : a=$a", print)
|
||||
//Logger.log("Kitsu : a=$a")
|
||||
return a
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class SpoilerPlugin : AbstractMarkwonPlugin() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val RE = Pattern.compile("~!.+?!~")
|
||||
private val RE = Pattern.compile("\\|\\|.+?\\|\\|")
|
||||
private fun applySpoilerSpans(spannable: Spannable) {
|
||||
val text = spannable.toString()
|
||||
val matcher = RE.matcher(text)
|
||||
|
|
|
@ -11,7 +11,7 @@ class CloudFlare(override val location: FileUrl) : WebViewBottomDialog() {
|
|||
override var title = "Cloudflare Bypass"
|
||||
override val webViewClient = object : WebViewClient() {
|
||||
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) {
|
||||
val clearance = cookie.substringAfter("$cfTag=").substringBefore(";")
|
||||
privateCallback.invoke(mapOf(cfTag to clearance))
|
||||
|
|
|
@ -34,8 +34,8 @@ class CookieCatcher : AppCompatActivity() {
|
|||
|
||||
val webView = findViewById<WebView>(R.id.discordWebview)
|
||||
|
||||
val cookies: CookieManager = Injekt.get<NetworkHelper>().cookieJar.manager
|
||||
cookies.setAcceptThirdPartyCookies(webView, true)
|
||||
val cookies: CookieManager? = Injekt.get<NetworkHelper>().cookieJar.manager
|
||||
cookies?.setAcceptThirdPartyCookies(webView, true)
|
||||
|
||||
webView.apply {
|
||||
settings.javaScriptEnabled = true
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue