diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml
index 629f508c..46e64586 100644
--- a/.github/workflows/beta.yml
+++ b/.github/workflows/beta.yml
@@ -1,17 +1,25 @@
name: Build APK and Notify Discord
+
on:
push:
- branches:
- - dev
+ branches-ignore:
+ - main
+ - l10n_dev_crowdin
+ - custom-download-location
paths-ignore:
- '**/README.md'
+ tags:
+ - "v*.*.*"
jobs:
build:
runs-on: ubuntu-latest
+ permissions:
+ contents: write
env:
CI: true
+ SKIP_BUILD: false
steps:
- name: Checkout repo
@@ -19,14 +27,12 @@ jobs:
with:
fetch-depth: 0
-
- name: Download last SHA artifact
uses: dawidd6/action-download-artifact@v6
with:
workflow: beta.yml
name: last-sha
path: .
-
continue-on-error: true
- name: Get Commits Since Last Run
@@ -39,7 +45,9 @@ jobs:
fi
echo "Commits since $LAST_SHA:"
# Accumulate commit logs in a shell variable
- COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an")
+ COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an [֍](https://github.com/${{ github.repository }}/commit/%H)" --max-count=10)
+ # Replace commit messages with pull request links
+ COMMIT_LOGS=$(echo "$COMMIT_LOGS" | sed -E 's/#([0-9]+)/[#\1](https:\/\/github.com\/rebelonion\/Dantotsu\/pull\/\1)/g')
# URL-encode the newline characters for GitHub Actions
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
@@ -49,6 +57,10 @@ jobs:
# Debugging: Print the variable to check its content
echo "$COMMIT_LOGS"
echo "$COMMIT_LOGS" > commit_log.txt
+ # Extract branch name from github.ref
+ BRANCH=${{ github.ref }}
+ BRANCH=${BRANCH#refs/heads/}
+ echo "BRANCH=${BRANCH}" >> $GITHUB_ENV
shell: /usr/bin/bash -e {0}
env:
CI: true
@@ -65,53 +77,278 @@ jobs:
echo "Version $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV
+ - name: List files in the directory
+ run: ls -l
+
- name: Setup JDK 17
+ if: ${{ env.SKIP_BUILD != 'true' }}
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
cache: gradle
-
- - name: Decode Keystore File
- run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
-
- - name: List files in the directory
- run: ls -l
-
- - name: Make gradlew executable
- run: chmod +x ./gradlew
-
- - name: Build with Gradle
- 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: Decode Keystore File
+ if: ${{ github.repository == 'rebelonion/Dantotsu' }}
+ run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
+
+ - name: Make gradlew executable
+ if: ${{ env.SKIP_BUILD != 'true' }}
+ run: chmod +x ./gradlew
+
+ - name: Build with Gradle
+ if: ${{ env.SKIP_BUILD != 'true' }}
+ run: |
+ if [ "${{ github.repository }}" == "rebelonion/Dantotsu" ]; then
+ ./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 }};
+ else
+ ./gradlew assembleGoogleAlpha;
+ fi
+
- name: Upload a Build Artifact
+ if: ${{ env.SKIP_BUILD != 'true' }}
uses: actions/upload-artifact@v4
with:
name: Dantotsu
retention-days: 5
compression-level: 9
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
-
+
- name: Upload APK to Discord and Telegram
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
shell: bash
run: |
- #Discord
+ # Prepare Discord embed
+ fetch_user_details() {
+ local login=$1
+ user_details=$(curl -s "https://api.github.com/users/$login")
+ name=$(echo "$user_details" | jq -r '.name // .login')
+ login=$(echo "$user_details" | jq -r '.login')
+ avatar_url=$(echo "$user_details" | jq -r '.avatar_url')
+ echo "$name|$login|$avatar_url"
+ }
+
+ # Additional information for the goats
+ declare -A additional_info
+ additional_info["ibo"]="\n Discord: <@951737931159187457>\n AniList: [takarealist112]()"
+ additional_info["aayush262"]="\n Discord: <@918825160654598224>\n AniList: [aayush262]()"
+ additional_info["rebelonion"]="\n Discord: <@714249925248024617>\n AniList: [rebelonion]()\n PornHub: [rebelonion]()"
+ additional_info["Ankit Grai"]="\n Discord: <@1125628254330560623>\n AniList: [bheshnarayan]()"
+
+ # Decimal color codes for contributors
+ declare -A contributor_colors
+ default_color="16721401"
+ contributor_colors["grayankit"]="#350297"
+ contributor_colors["ibo"]="#ff9b46"
+ contributor_colors["aayush262"]="#5d689d"
+ contributor_colors["Sadwhy"]="#ff7e95"
+ contributor_colors["rebelonion"]="#d4e5ed"
+
+ hex_to_decimal() { printf '%d' "0x${1#"#"}"; }
+
+ # Count recent commits and create an associative array Okay
+ declare -A recent_commit_counts
+ echo "Debug: Processing COMMIT_LOG:"
+ echo "$COMMIT_LOG"
+ while read -r count name; do
+ recent_commit_counts["$name"]=$count
+ echo "Debug: Commit count for $name: $count"
+ done < <(echo "$COMMIT_LOG" | sed 's/%0A/\n/g' | grep -oP '(?<=~)[^[]*' | sort | uniq -c | sort -rn)
+
+ echo "Debug: Fetching contributors from GitHub"
+ # Fetch contributors from GitHub
+ contributors=$(curl -s "https://api.github.com/repos/${{ github.repository }}/contributors")
+ echo "Debug: Contributors response:"
+ echo "$contributors"
+
+ # Create a sorted list of contributors based on recent commit counts
+ sorted_contributors=$(for login in $(echo "$contributors" | jq -r '.[].login'); do
+ user_info=$(fetch_user_details "$login")
+ name=$(echo "$user_info" | cut -d'|' -f1)
+ count=${recent_commit_counts["$name"]:-0}
+ echo "$count|$login"
+ done | sort -rn | cut -d'|' -f2)
+
+ # Initialize needed variables
+ developers=""
+ committers_count=0
+ max_commits=0
+ top_contributor=""
+ top_contributor_count=0
+ top_contributor_avatar=""
+ embed_color=$default_color
+
+ # Process contributors in the new order
+ while read -r login; do
+ user_info=$(fetch_user_details "$login")
+ name=$(echo "$user_info" | cut -d'|' -f1)
+ login=$(echo "$user_info" | cut -d'|' -f2)
+ avatar_url=$(echo "$user_info" | cut -d'|' -f3)
+
+ # Only process if they have recent commits
+ commit_count=${recent_commit_counts["$name"]:-0}
+ if [ $commit_count -gt 0 ]; then
+ # Update top contributor information
+ if [ $commit_count -gt $max_commits ]; then
+ max_commits=$commit_count
+ top_contributors=("$login")
+ top_contributor_count=1
+ top_contributor_avatar="$avatar_url"
+ embed_color=$(hex_to_decimal "${contributor_colors[$name]:-$default_color}")
+ elif [ $commit_count -eq $max_commits ]; then
+ top_contributors+=("$login")
+ top_contributor_count=$((top_contributor_count + 1))
+ embed_color=$default_color
+ fi
+ echo "Debug top contributors:"
+ echo "$top_contributors"
+
+ # Get commit count for this contributor on the dev branch
+ branch_commit_count=$(git log --author="$login" --author="$name" --oneline | awk '!seen[$0]++' | wc -l)
+
+ # Debug: Print recent_commit_counts
+ echo "Debug: recent_commit_counts contents:"
+ for key in "${!recent_commit_counts[@]}"; do
+ echo "$key: ${recent_commit_counts[$key]}"
+ done
+
+ extra_info="${additional_info[$name]}"
+ if [ -n "$extra_info" ]; then
+ extra_info=$(echo "$extra_info" | sed 's/\\n/\n- /g')
+ fi
+
+ # Construct the developer entry
+ developer_entry="◗ **${name}** ${extra_info}
+ - Github: [${login}](https://github.com/${login})
+ - Commits: ${branch_commit_count}"
+
+ # Add the entry to developers, with a newline if it's not the first entry
+ if [ -n "$developers" ]; then
+ developers="${developers}
+ ${developer_entry}"
+ else
+ developers="${developer_entry}"
+ fi
+ committers_count=$((committers_count + 1))
+ fi
+ done <<< "$sorted_contributors"
+
+ # Set the thumbnail URL and color based on top contributor(s)
+ if [ $top_contributor_count -eq 1 ]; then
+ thumbnail_url="$top_contributor_avatar"
+ else
+ thumbnail_url="https://i.imgur.com/5o3Y9Jb.gif"
+ embed_color=$default_color
+ fi
+
+ # Truncate field values
+ max_length=1000
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 [ ${#developers} -gt $max_length ]; then
+ developers="${developers:0:$max_length}... (truncated)"
+ fi
if [ ${#commit_messages} -gt $max_length ]; then
commit_messages="${commit_messages:0:$max_length}... (truncated)"
fi
- contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
- curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
-
- #Telegram
- curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
- -F "document=@app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk" \
- -F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
- https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
+ # Construct Discord payload
+ discord_data=$(jq -nc \
+ --arg field_value "$commit_messages" \
+ --arg author_value "$developers" \
+ --arg footer_text "Version $VERSION" \
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
+ --arg thumbnail_url "$thumbnail_url" \
+ --arg embed_color "$embed_color" \
+ '{
+ "content": "<@&1225347048321191996>",
+ "embeds": [
+ {
+ "title": "New Alpha-Build dropped",
+ "color": $embed_color,
+ "fields": [
+ {
+ "name": "Commits:",
+ "value": $field_value,
+ "inline": true
+ },
+ {
+ "name": "Developers:",
+ "value": $author_value,
+ "inline": false
+ }
+ ],
+ "footer": {
+ "text": $footer_text
+ },
+ "timestamp": $timestamp,
+ "thumbnail": {
+ "url": $thumbnail_url
+ }
+ }
+ ],
+ "attachments": []
+ }')
+ echo "Debug: Final Discord payload:"
+ echo "$discord_data"
+
+ # Send Discord message
+ curl -H "Content-Type: application/json" \
+ -d "$discord_data" \
+ ${{ secrets.DISCORD_WEBHOOK }}
+ echo "You have only send an embed to discord due to SKIP_BUILD being set to true"
+
+ # Upload APK to Discord
+ if [ "$SKIP_BUILD" != "true" ]; then
+ curl -F "payload_json=${contentbody}" \
+ -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
+ ${{ secrets.DISCORD_WEBHOOK }}
+ else
+ echo "Skipping APK upload to Discord due to SKIP_BUILD being set to true"
+ fi
+
+ # Format commit messages for Telegram
+ telegram_commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g' | while read -r line; do
+ message=$(echo "$line" | sed -E 's/● (.*) ~(.*) \[֍\]\((.*)\)/● \1 ~\2 ֍<\/a>/')
+ message=$(echo "$message" | sed -E 's/\[#([0-9]+)\]\((https:\/\/github\.com\/[^)]+)\)/#\1<\/a>/g')
+ echo "$message"
+ done)
+ telegram_commit_messages="${telegram_commit_messages}
"
+
+ # Configuring dev info
+ echo "$developers" > dev_info.txt
+ echo "$developers"
+ # making the file executable
+ chmod +x workflowscripts/tel_parser.sed
+ ./workflowscripts/tel_parser.sed dev_info.txt >> output.txt
+ dev_info_tel=$(< output.txt)
+
+ telegram_dev_info="${dev_info_tel}
"
+ echo "$telegram_dev_info"
+
+ # Upload APK to Telegram
+ if [ "$SKIP_BUILD" != "true" ]; then
+ APK_PATH="app/build/outputs/apk/google/alpha/app-google-alpha.apk"
+ response=$(curl -sS -f -X POST \
+ "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument" \
+ -F "chat_id=-1002117798698" \
+ -F "message_thread_id=7044" \
+ -F "document=@$APK_PATH" \
+ -F "caption=New Alpha-Build dropped 🔥
+
+ Commits:
+ ${telegram_commit_messages}
+ Dev:
+ ${telegram_dev_info}
+ version: ${VERSION}" \
+ -F "parse_mode=HTML")
+ else
+ echo "skipping because skip build set to true"
+ fi
+
env:
COMMIT_LOG: ${{ env.COMMIT_LOG }}
VERSION: ${{ env.VERSION }}
diff --git a/.gitignore b/.gitignore
index 2ccc322f..0fc4c86f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,9 @@
.gradle/
build/
+#kotlin
+.kotlin/
+
# Local configuration file (sdk path, etc)
local.properties
@@ -33,4 +36,7 @@ output.json
scripts/
#crowdin
-crowdin.yml
\ No newline at end of file
+crowdin.yml
+
+#vscode
+.vscode
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 811cab90..5d422b29 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -11,12 +11,12 @@ def gitCommitHash = providers.exec {
}.standardOutput.asText.get().trim()
android {
- compileSdk 34
+ compileSdk 35
defaultConfig {
applicationId "ani.dantotsu"
minSdk 21
- targetSdk 34
+ targetSdk 35
versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "3.1.0"
versionCode 300100000
@@ -101,6 +101,8 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.11.0'
implementation "com.anggrayudi:storage:1.5.5"
+ implementation "androidx.biometric:biometric:1.1.0"
+
// Glide
ext.glide_version = '4.16.0'
@@ -111,7 +113,7 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer
- ext.exo_version = '1.3.1'
+ ext.exo_version = '1.5.0'
implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
@@ -121,6 +123,8 @@ dependencies {
// Media3 Casting
implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.7.0"
+// Media3 extension
+ implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.3"
// UI
implementation 'com.google.android.material:material:1.12.0'
@@ -131,7 +135,7 @@ dependencies {
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'com.github.eltos:simpledialogfragments:v3.7'
- implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1'
+ implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.3'
// Markwon
ext.markwon_version = '4.6.2'
@@ -157,7 +161,7 @@ dependencies {
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
implementation 'com.squareup.logcat:logcat:0.1'
- implementation 'com.github.inorichi.injekt:injekt-core:65b0440'
+ implementation 'uy.kohesive.injekt:injekt-core:1.16.+'
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'
diff --git a/app/src/fdroid/java/ani/dantotsu/others/AppUpdater.kt b/app/src/fdroid/java/ani/dantotsu/others/AppUpdater.kt
index bb9893f6..60d06857 100644
--- a/app/src/fdroid/java/ani/dantotsu/others/AppUpdater.kt
+++ b/app/src/fdroid/java/ani/dantotsu/others/AppUpdater.kt
@@ -1,9 +1,40 @@
package ani.dantotsu.others
import androidx.fragment.app.FragmentActivity
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import java.text.SimpleDateFormat
+import java.util.Locale
object AppUpdater {
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
- //no-op
+ // no-op
}
-}
\ No newline at end of file
+
+ @Serializable
+ data class GithubResponse(
+ @SerialName("html_url")
+ val htmlUrl: String,
+ @SerialName("tag_name")
+ val tagName: String,
+ val prerelease: Boolean,
+ @SerialName("created_at")
+ val createdAt: String,
+ val body: String? = null,
+ val assets: List? = null
+ ) {
+ @Serializable
+ data class Asset(
+ @SerialName("browser_download_url")
+ val browserDownloadURL: String
+ )
+
+ fun timeStamp(): Long {
+ return dateFormat.parse(createdAt)!!.time
+ }
+
+ companion object {
+ private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
+ }
+ }
+}
diff --git a/app/src/google/java/ani/dantotsu/others/AppUpdater.kt b/app/src/google/java/ani/dantotsu/others/AppUpdater.kt
index 67b7ce43..f6f8e8ca 100644
--- a/app/src/google/java/ani/dantotsu/others/AppUpdater.kt
+++ b/app/src/google/java/ani/dantotsu/others/AppUpdater.kt
@@ -29,6 +29,7 @@ import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.time.delay
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
@@ -69,7 +70,11 @@ object AppUpdater {
)
addView(
TextView(activity).apply {
- val markWon = buildMarkwon(activity, false)
+ val markWon = try { //slower phones can destroy the activity before this is done
+ buildMarkwon(activity, false)
+ } catch (e: IllegalArgumentException) {
+ return@runOnUiThread
+ }
markWon.setMarkdown(this, md)
}
)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 43c9c29f..644d218a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -19,6 +19,7 @@
+
@@ -115,7 +116,8 @@
-
+
+
@@ -131,10 +133,11 @@
-
-
+
+
+
@@ -155,7 +158,8 @@
android:parentActivityName=".MainActivity" />
+ android:parentActivityName=".MainActivity"
+ android:windowSoftInputMode="adjustPan"/>
@@ -194,14 +198,15 @@
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
+ android:name=".util.ActivityMarkdownCreator"
+ android:windowSoftInputMode="adjustResize|stateVisible" />
-
-
-
-
+
+
+
+
+
+
+
+
+
+
(PrefName.CommentsEnabled) == 0) {
+ if (BuildConfig.FLAVOR.contains("fdroid")) {
+ PrefManager.setVal(PrefName.CommentsEnabled, 2)
+ } else {
+ PrefManager.setVal(PrefName.CommentsEnabled, 1)
+ }
+ }
+
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager = Injekt.get()
animeExtensionManager.findAvailableExtensions()
@@ -128,7 +136,9 @@ class App : MultiDexApplication() {
downloadAddonManager = Injekt.get()
torrentAddonManager.init()
downloadAddonManager.init()
- CommentsAPI.fetchAuthToken(this@App)
+ if (PrefManager.getVal(PrefName.CommentsEnabled) == 1) {
+ CommentsAPI.fetchAuthToken(this@App)
+ }
val useAlarmManager = PrefManager.getVal(PrefName.UseAlarmManager)
val scheduler = TaskScheduler.create(this@App, useAlarmManager)
diff --git a/app/src/main/java/ani/dantotsu/Functions.kt b/app/src/main/java/ani/dantotsu/Functions.kt
index 04b83f52..e4e18ed9 100644
--- a/app/src/main/java/ani/dantotsu/Functions.kt
+++ b/app/src/main/java/ani/dantotsu/Functions.kt
@@ -68,7 +68,6 @@ import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.AttrRes
-import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@@ -92,12 +91,12 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.BuildConfig.APPLICATION_ID
import ani.dantotsu.connections.anilist.Genre
import ani.dantotsu.connections.anilist.api.FuzzyDate
-import ani.dantotsu.connections.bakaupdates.MangaUpdates
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
+import ani.dantotsu.others.AlignTagHandler
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.SpoilerPlugin
import ani.dantotsu.parsers.ShowResponse
@@ -106,7 +105,6 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
-import ani.dantotsu.util.CountUpTimer
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
@@ -119,8 +117,8 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withC
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.CustomTarget
import com.bumptech.glide.request.target.Target
-import com.bumptech.glide.request.target.ViewTarget
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -154,6 +152,7 @@ import java.io.FileOutputStream
import java.io.OutputStream
import java.lang.reflect.Field
import java.util.Calendar
+import java.util.Locale
import java.util.TimeZone
import java.util.Timer
import java.util.TimerTask
@@ -314,6 +313,7 @@ fun Activity.reloadActivity() {
Refresh.all()
finish()
startActivity(Intent(this, this::class.java))
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
initActivity(this)
}
@@ -854,7 +854,7 @@ fun savePrefsToDownloads(
}
)
}
-
+@SuppressLint("StringFormatMatches")
fun savePrefs(serialized: String, path: String, title: String, context: Context): File? {
var file = File(path, "$title.ani")
var counter = 1
@@ -874,6 +874,7 @@ fun savePrefs(serialized: String, path: String, title: String, context: Context)
}
}
+@SuppressLint("StringFormatMatches")
fun savePrefs(
serialized: String,
path: String,
@@ -920,7 +921,7 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
intent.putExtra(Intent.EXTRA_STREAM, contentUri)
context.startActivity(Intent.createChooser(intent, "Share $title"))
}
-
+@SuppressLint("StringFormatMatches")
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
val imageFile = File(path, "$imageFileName.png")
return try {
@@ -1010,47 +1011,10 @@ fun countDown(media: Media, view: ViewGroup) {
}
}
-fun sinceWhen(media: Media, view: ViewGroup) {
- if (media.status != "RELEASING" && media.status != "HIATUS") return
- CoroutineScope(Dispatchers.IO).launch {
- MangaUpdates().search(media.mangaName(), media.startDate)?.let {
- val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
- val timeSince = (System.currentTimeMillis() -
- (it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
-
- withContext(Dispatchers.Main) {
- val v =
- ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
- view.addView(v.root, 0)
- v.mediaCountdownText.text =
- currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
-
- object : CountUpTimer(86400000) {
- override fun onTick(second: Int) {
- val a = second + timeSince
- v.mediaCountdown.text = currActivity()?.getString(
- R.string.time_format,
- a / 86400,
- a % 86400 / 3600,
- a % 86400 % 3600 / 60,
- a % 86400 % 3600 % 60
- )
- }
-
- override fun onFinish() {
- // The legend will never die.
- }
- }.start()
- }
- }
- }
-}
-
fun displayTimer(media: Media, view: ViewGroup) {
when {
media.anime != null -> countDown(media, view)
- media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
- else -> {} // No timer yet
+ else -> {}
}
}
@@ -1447,6 +1411,8 @@ fun openOrCopyAnilistLink(link: String) {
} else {
copyToClipboard(link, true)
}
+ } else if (getYoutubeId(link).isNotEmpty()) {
+ openLinkInYouTube(link)
} else {
copyToClipboard(link, true)
}
@@ -1483,6 +1449,7 @@ fun buildMarkwon(
TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a")
)
}
+ plugin.addHandler(AlignTagHandler())
})
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
@@ -1500,7 +1467,6 @@ fun buildMarkwon(
}
return false
}
-
override fun onLoadFailed(
e: GlideException?,
model: Any?,
@@ -1527,3 +1493,34 @@ fun buildMarkwon(
.build()
return markwon
}
+
+
+
+fun getYoutubeId(url: String): String {
+ val regex = """(?:youtube\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|(?:youtu\.be|youtube\.com)/)([^"&?/\s]{11})|youtube\.com/""".toRegex()
+ val matchResult = regex.find(url)
+ return matchResult?.groupValues?.getOrNull(1) ?: ""
+}
+
+fun getLanguageCode(language: String): CharSequence {
+ val locales = Locale.getAvailableLocales()
+ for (locale in locales) {
+ if (locale.displayLanguage.equals(language, ignoreCase = true)) {
+ val lang: CharSequence = locale.language
+ return lang
+
+ }
+ }
+ val out: CharSequence = "null"
+ return out
+}
+
+fun getLanguageName(language: String): String? {
+ val locales = Locale.getAvailableLocales()
+ for (locale in locales) {
+ if (locale.language.equals(language, ignoreCase = true)) {
+ return locale.displayLanguage
+ }
+ }
+ return null
+}
diff --git a/app/src/main/java/ani/dantotsu/MainActivity.kt b/app/src/main/java/ani/dantotsu/MainActivity.kt
index f9740533..2faddd5d 100644
--- a/app/src/main/java/ani/dantotsu/MainActivity.kt
+++ b/app/src/main/java/ani/dantotsu/MainActivity.kt
@@ -2,7 +2,6 @@ package ani.dantotsu
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
-import android.app.AlertDialog
import android.content.Intent
import android.content.res.Configuration
import android.graphics.drawable.Animatable
@@ -51,7 +50,8 @@ import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.calc.CalcActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity
-import ani.dantotsu.profile.activity.NotificationActivity
+import ani.dantotsu.profile.notification.NotificationActivity
+import ani.dantotsu.settings.AddRepositoryBottomSheet
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
@@ -60,10 +60,11 @@ import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.themes.ThemeManager
+import ani.dantotsu.util.AudioHelper
import ani.dantotsu.util.Logger
+import ani.dantotsu.util.customAlertDialog
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
@@ -116,58 +117,8 @@ class MainActivity : AppCompatActivity() {
}
}
- 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")
- }
+ if (Intent.ACTION_VIEW == intent.action) {
+ handleViewIntent(intent)
}
val bottomNavBar = findViewById(R.id.navbar)
@@ -287,7 +238,7 @@ class MainActivity : AppCompatActivity() {
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
snackString(R.string.extension_updates_available)
- ?.setDuration(Snackbar.LENGTH_LONG)
+ ?.setDuration(Snackbar.LENGTH_SHORT)
?.setAction(R.string.review) {
startActivity(Intent(this, ExtensionsActivity::class.java))
}
@@ -365,7 +316,6 @@ class MainActivity : AppCompatActivity() {
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
Logger.log("MainActivity, onCreate: $activityId")
val notificationIntent = Intent(this, NotificationActivity::class.java).apply {
- putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId)
}
launched = true
@@ -455,7 +405,10 @@ class MainActivity : AppCompatActivity() {
}
}
}
-
+ if (PrefManager.getVal(PrefName.OC)) {
+ AudioHelper.run(this, R.raw.audio)
+ PrefManager.setVal(PrefName.OC, false)
+ }
val torrentManager = Injekt.get()
fun startTorrent() {
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
@@ -490,39 +443,101 @@ class MainActivity : AppCompatActivity() {
params.updateMargins(bottom = margin.toPx)
}
+ private fun handleViewIntent(intent: Intent) {
+ val uri: Uri? = intent.data
+ try {
+ if (uri == null) {
+ throw Exception("Uri is null")
+ }
+ if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi") && uri.host == "add-repo") {
+ val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import")
+ val prefName = if (uri.scheme == "tachiyomi") {
+ PrefName.MangaExtensionRepos
+ } else {
+ PrefName.AnimeExtensionRepos
+ }
+ val savedRepos: Set = PrefManager.getVal(prefName)
+ val newRepos = savedRepos.toMutableSet()
+ AddRepositoryBottomSheet.addRepoWarning(this) {
+ newRepos.add(url)
+ PrefManager.setVal(prefName, newRepos)
+ toast("${if (uri.scheme == "tachiyomi") "Manga" else "Anime"} Extension Repo added")
+ }
+ return
+ }
+ if (intent.type == null) return
+ 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")
+ }
+ }
+
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
- val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
- dialogView.userAgentTextBox.hint = "Password"
- dialogView.subtitle.visibility = View.VISIBLE
- dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file)
-
- val dialog = AlertDialog.Builder(this, R.style.MyPopup)
- .setTitle("Enter Password")
- .setView(dialogView.root)
- .setPositiveButton("OK", null)
- .setNegativeButton("Cancel") { dialog, _ ->
+ val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
+ userAgentTextBox.hint = "Password"
+ subtitle.visibility = View.VISIBLE
+ subtitle.text = getString(R.string.enter_password_to_decrypt_file)
+ }
+ customAlertDialog().apply {
+ setTitle("Enter Password")
+ setCustomView(dialogView.root)
+ setPosButton(R.string.yes) {
+ val editText = dialogView.userAgentTextBox
+ if (editText.text?.isNotBlank() == true) {
+ editText.text?.toString()?.trim()?.toCharArray(password)
+ callback(password)
+ } else {
+ toast("Password cannot be empty")
+ }
+ }
+ setNegButton(R.string.cancel) {
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(R.id.userAgentTextBox)
- if (editText?.text?.isNotBlank() == true) {
- editText.text?.toString()?.trim()?.toCharArray(password)
- dialog.dismiss()
- callback(password)
- } else {
- toast("Password cannot be empty")
- }
+ show()
}
}
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt
index 51f3bc9d..6acb67a4 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt
@@ -15,6 +15,8 @@ import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import java.util.Calendar
+import java.util.Locale
+import kotlin.math.abs
object Anilist {
val query: AnilistQueries = AnilistQueries()
@@ -22,7 +24,7 @@ object Anilist {
var token: String? = null
var username: String? = null
- var adult: Boolean = false
+
var userid: Int? = null
var avatar: String? = null
var bg: String? = null
@@ -36,6 +38,17 @@ object Anilist {
var rateLimitReset: Long = 0
var initialized = false
+ var adult: Boolean = false
+ var titleLanguage: String? = null
+ var staffNameLanguage: String? = null
+ var airingNotifications: Boolean = false
+ var restrictMessagesToFollowing: Boolean = false
+ var scoreFormat: String? = null
+ var rowOrder: String? = null
+ var activityMergeTime: Int? = null
+ var timezone: String? = null
+ var animeCustomLists: List? = null
+ var mangaCustomLists: List? = null
val sortBy = listOf(
"SCORE_DESC",
@@ -96,6 +109,86 @@ object Anilist {
"Original Creator", "Story & Art", "Story"
)
+ val timeZone = listOf(
+ "(GMT-11:00) Pago Pago",
+ "(GMT-10:00) Hawaii Time",
+ "(GMT-09:00) Alaska Time",
+ "(GMT-08:00) Pacific Time",
+ "(GMT-07:00) Mountain Time",
+ "(GMT-06:00) Central Time",
+ "(GMT-05:00) Eastern Time",
+ "(GMT-04:00) Atlantic Time - Halifax",
+ "(GMT-03:00) Sao Paulo",
+ "(GMT-02:00) Mid-Atlantic",
+ "(GMT-01:00) Azores",
+ "(GMT+00:00) London",
+ "(GMT+01:00) Berlin",
+ "(GMT+02:00) Helsinki",
+ "(GMT+03:00) Istanbul",
+ "(GMT+04:00) Dubai",
+ "(GMT+04:30) Kabul",
+ "(GMT+05:00) Maldives",
+ "(GMT+05:30) India Standard Time",
+ "(GMT+05:45) Kathmandu",
+ "(GMT+06:00) Dhaka",
+ "(GMT+06:30) Cocos",
+ "(GMT+07:00) Bangkok",
+ "(GMT+08:00) Hong Kong",
+ "(GMT+08:30) Pyongyang",
+ "(GMT+09:00) Tokyo",
+ "(GMT+09:30) Central Time - Darwin",
+ "(GMT+10:00) Eastern Time - Brisbane",
+ "(GMT+10:30) Central Time - Adelaide",
+ "(GMT+11:00) Eastern Time - Melbourne, Sydney",
+ "(GMT+12:00) Nauru",
+ "(GMT+13:00) Auckland",
+ "(GMT+14:00) Kiritimati",
+ )
+
+ val titleLang = listOf(
+ "English (Attack on Titan)",
+ "Romaji (Shingeki no Kyojin)",
+ "Native (進撃の巨人)"
+ )
+
+ val staffNameLang = listOf(
+ "Romaji, Western Order (Killua Zoldyck)",
+ "Romaji (Zoldyck Killua)",
+ "Native (キルア=ゾルディック)"
+ )
+
+ val scoreFormats = listOf(
+ "100 Point (55/100)",
+ "10 Point Decimal (5.5/10)",
+ "10 Point (5/10)",
+ "5 Star (3/5)",
+ "3 Point Smiley :)"
+ )
+
+ val rowOrderMap = mapOf(
+ "Score" to "score",
+ "Title" to "title",
+ "Last Updated" to "updatedAt",
+ "Last Added" to "id"
+ )
+
+ val activityMergeTimeMap = mapOf(
+ "Never" to 0,
+ "30 mins" to 30,
+ "69 mins" to 69,
+ "1 hour" to 60,
+ "2 hours" to 120,
+ "3 hours" to 180,
+ "6 hours" to 360,
+ "12 hours" to 720,
+ "1 day" to 1440,
+ "2 days" to 2880,
+ "3 days" to 4320,
+ "1 week" to 10080,
+ "2 weeks" to 20160,
+ "Always" to 29160
+ )
+
private val cal: Calendar = Calendar.getInstance()
private val currentYear = cal.get(Calendar.YEAR)
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
@@ -106,6 +199,33 @@ object Anilist {
else -> 0
}
+ fun getDisplayTimezone(apiTimezone: String, context: Context): String {
+ val noTimezone = context.getString(R.string.selected_no_time_zone)
+ val parts = apiTimezone.split(":")
+ if (parts.size != 2) return noTimezone
+
+ val hours = parts[0].toIntOrNull() ?: 0
+ val minutes = parts[1].toIntOrNull() ?: 0
+ val sign = if (hours >= 0) "+" else "-"
+ val formattedHours = String.format(Locale.US, "%02d", abs(hours))
+ val formattedMinutes = String.format(Locale.US, "%02d", minutes)
+
+ val searchString = "(GMT$sign$formattedHours:$formattedMinutes)"
+ return timeZone.find { it.contains(searchString) } ?: noTimezone
+ }
+
+ fun getApiTimezone(displayTimezone: String): String {
+ val regex = """\(GMT([+-])(\d{2}):(\d{2})\)""".toRegex()
+ val matchResult = regex.find(displayTimezone)
+ return if (matchResult != null) {
+ val (sign, hours, minutes) = matchResult.destructured
+ val formattedSign = if (sign == "+") "" else "-"
+ "$formattedSign$hours:$minutes"
+ } else {
+ "00:00"
+ }
+ }
+
private fun getSeason(next: Boolean): Pair {
var newSeason = if (next) currentSeason + 1 else currentSeason - 1
var newYear = currentYear
@@ -191,6 +311,7 @@ object Anilist {
)
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
Logger.log("Remaining requests: $remaining")
+ println("Remaining requests: $remaining")
if (json.code == 429) {
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt
index a8bd8904..de0f6e2f 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistMutations.kt
@@ -3,16 +3,99 @@ package ani.dantotsu.connections.anilist
import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.Query
+import ani.dantotsu.connections.anilist.api.ToggleLike
import ani.dantotsu.currContext
import com.google.gson.Gson
-import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
class AnilistMutations {
+ suspend fun updateSettings(
+ timezone: String? = null,
+ titleLanguage: String? = null,
+ staffNameLanguage: String? = null,
+ activityMergeTime: Int? = null,
+ airingNotifications: Boolean? = null,
+ displayAdultContent: Boolean? = null,
+ restrictMessagesToFollowing: Boolean? = null,
+ scoreFormat: String? = null,
+ rowOrder: String? = null,
+ ) {
+ val query = """
+ mutation (
+ ${"$"}timezone: String,
+ ${"$"}titleLanguage: UserTitleLanguage,
+ ${"$"}staffNameLanguage: UserStaffNameLanguage,
+ ${"$"}activityMergeTime: Int,
+ ${"$"}airingNotifications: Boolean,
+ ${"$"}displayAdultContent: Boolean,
+ ${"$"}restrictMessagesToFollowing: Boolean,
+ ${"$"}scoreFormat: ScoreFormat,
+ ${"$"}rowOrder: String
+ ) {
+ UpdateUser(
+ timezone: ${"$"}timezone,
+ titleLanguage: ${"$"}titleLanguage,
+ staffNameLanguage: ${"$"}staffNameLanguage,
+ activityMergeTime: ${"$"}activityMergeTime,
+ airingNotifications: ${"$"}airingNotifications,
+ displayAdultContent: ${"$"}displayAdultContent,
+ restrictMessagesToFollowing: ${"$"}restrictMessagesToFollowing,
+ scoreFormat: ${"$"}scoreFormat,
+ rowOrder: ${"$"}rowOrder,
+ ) {
+ id
+ options {
+ timezone
+ titleLanguage
+ staffNameLanguage
+ activityMergeTime
+ airingNotifications
+ displayAdultContent
+ restrictMessagesToFollowing
+ }
+ mediaListOptions {
+ scoreFormat
+ rowOrder
+ }
+ }
+ }
+ """.trimIndent()
+
+ val variables = """
+ {
+ ${timezone?.let { """"timezone":"$it"""" } ?: ""}
+ ${titleLanguage?.let { """"titleLanguage":"$it"""" } ?: ""}
+ ${staffNameLanguage?.let { """"staffNameLanguage":"$it"""" } ?: ""}
+ ${activityMergeTime?.let { """"activityMergeTime":$it""" } ?: ""}
+ ${airingNotifications?.let { """"airingNotifications":$it""" } ?: ""}
+ ${displayAdultContent?.let { """"displayAdultContent":$it""" } ?: ""}
+ ${restrictMessagesToFollowing?.let { """"restrictMessagesToFollowing":$it""" } ?: ""}
+ ${scoreFormat?.let { """"scoreFormat":"$it"""" } ?: ""}
+ ${rowOrder?.let { """"rowOrder":"$it"""" } ?: ""}
+ }
+ """.trimIndent().replace("\n", "").replace(""" """, "").replace(",}", "}")
+
+ executeQuery(query, variables)
+ }
+
suspend fun toggleFav(anime: Boolean = true, id: Int) {
- val query =
- """mutation (${"$"}animeId: Int,${"$"}mangaId:Int) { ToggleFavourite(animeId:${"$"}animeId,mangaId:${"$"}mangaId){ anime { edges { id } } manga { edges { id } } } }"""
+ val query = """
+ mutation (${"$"}animeId: Int, ${"$"}mangaId: Int) {
+ ToggleFavourite(animeId: ${"$"}animeId, mangaId: ${"$"}mangaId) {
+ anime {
+ edges {
+ id
+ }
+ }
+ manga {
+ edges {
+ id
+ }
+ }
+ }
+ }
+ """.trimIndent()
val variables = if (anime) """{"animeId":"$id"}""" else """{"mangaId":"$id"}"""
executeQuery(query, variables)
}
@@ -25,7 +108,17 @@ class AnilistMutations {
FavType.STAFF -> "staffId"
FavType.STUDIO -> "studioId"
}
- val query = """mutation{ToggleFavourite($filter:$id){anime{pageInfo{total}}}}"""
+ val query = """
+ mutation {
+ ToggleFavourite($filter: $id) {
+ anime {
+ pageInfo {
+ total
+ }
+ }
+ }
+ }
+ """.trimIndent()
val result = executeQuery(query)
return result?.get("errors") == null && result != null
}
@@ -34,6 +127,51 @@ class AnilistMutations {
ANIME, MANGA, CHARACTER, STAFF, STUDIO
}
+ suspend fun deleteCustomList(name: String, type: String): Boolean {
+ val query = """
+ mutation (${"$"}name: String, ${"$"}type: MediaType) {
+ DeleteCustomList(customList: ${"$"}name, type: ${"$"}type) {
+ deleted
+ }
+ }
+ """.trimIndent()
+ val variables = """
+ {
+ "name": "$name",
+ "type": "$type"
+ }
+ """.trimIndent()
+ val result = executeQuery(query, variables)
+ return result?.get("errors") == null
+ }
+
+ suspend fun updateCustomLists(animeCustomLists: List?, mangaCustomLists: List?): Boolean {
+ val query = """
+ mutation (${"$"}animeListOptions: MediaListOptionsInput, ${"$"}mangaListOptions: MediaListOptionsInput) {
+ UpdateUser(animeListOptions: ${"$"}animeListOptions, mangaListOptions: ${"$"}mangaListOptions) {
+ mediaListOptions {
+ animeList {
+ customLists
+ }
+ mangaList {
+ customLists
+ }
+ }
+ }
+ }
+ """.trimIndent()
+ val variables = """
+ {
+ ${animeCustomLists?.let { """"animeListOptions": {"customLists": ${Gson().toJson(it)}}""" } ?: ""}
+ ${if (animeCustomLists != null && mangaCustomLists != null) "," else ""}
+ ${mangaCustomLists?.let { """"mangaListOptions": {"customLists": ${Gson().toJson(it)}}""" } ?: ""}
+ }
+ """.trimIndent().replace("\n", "").replace(""" """, "").replace(",}", "}")
+
+ val result = executeQuery(query, variables)
+ return result?.get("errors") == null
+ }
+
suspend fun editList(
mediaID: Int,
progress: Int? = null,
@@ -46,14 +184,45 @@ class AnilistMutations {
completedAt: FuzzyDate? = null,
customList: List? = null
) {
-
val query = """
- mutation ( ${"$"}mediaID: Int, ${"$"}progress: Int,${"$"}private:Boolean,${"$"}repeat: Int, ${"$"}notes: String, ${"$"}customLists: [String], ${"$"}scoreRaw:Int, ${"$"}status:MediaListStatus, ${"$"}start:FuzzyDateInput${if (startedAt != null) "=" + startedAt.toVariableString() else ""}, ${"$"}completed:FuzzyDateInput${if (completedAt != null) "=" + completedAt.toVariableString() else ""} ) {
- SaveMediaListEntry( mediaId: ${"$"}mediaID, progress: ${"$"}progress, repeat: ${"$"}repeat, notes: ${"$"}notes, private: ${"$"}private, scoreRaw: ${"$"}scoreRaw, status:${"$"}status, startedAt: ${"$"}start, completedAt: ${"$"}completed , customLists: ${"$"}customLists ) {
- score(format:POINT_10_DECIMAL) startedAt{year month day} completedAt{year month day}
+ mutation (
+ ${"$"}mediaID: Int,
+ ${"$"}progress: Int,
+ ${"$"}private: Boolean,
+ ${"$"}repeat: Int,
+ ${"$"}notes: String,
+ ${"$"}customLists: [String],
+ ${"$"}scoreRaw: Int,
+ ${"$"}status: MediaListStatus,
+ ${"$"}start: FuzzyDateInput${if (startedAt != null) "=" + startedAt.toVariableString() else ""},
+ ${"$"}completed: FuzzyDateInput${if (completedAt != null) "=" + completedAt.toVariableString() else ""}
+ ) {
+ SaveMediaListEntry(
+ mediaId: ${"$"}mediaID,
+ progress: ${"$"}progress,
+ repeat: ${"$"}repeat,
+ notes: ${"$"}notes,
+ private: ${"$"}private,
+ scoreRaw: ${"$"}scoreRaw,
+ status: ${"$"}status,
+ startedAt: ${"$"}start,
+ completedAt: ${"$"}completed,
+ customLists: ${"$"}customLists
+ ) {
+ score(format: POINT_10_DECIMAL)
+ startedAt {
+ year
+ month
+ day
+ }
+ completedAt {
+ year
+ month
+ day
+ }
}
}
- """.replace("\n", "").replace(""" """, "")
+ """.trimIndent()
val variables = """{"mediaID":$mediaID
${if (private != null) ""","private":$private""" else ""}
@@ -69,43 +238,168 @@ class AnilistMutations {
}
suspend fun deleteList(listId: Int) {
- val query = "mutation(${"$"}id:Int){DeleteMediaListEntry(id:${"$"}id){deleted}}"
+ val query = """
+ mutation(${"$"}id: Int) {
+ DeleteMediaListEntry(id: ${"$"}id) {
+ deleted
+ }
+ }
+ """.trimIndent()
val variables = """{"id":"$listId"}"""
executeQuery(query, variables)
}
-
suspend fun rateReview(reviewId: Int, rating: String): Query.RateReviewResponse? {
- val query = "mutation{RateReview(reviewId:$reviewId,rating:$rating){id mediaId mediaType summary body(asHtml:true)rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}"
+ val query = """
+ mutation {
+ RateReview(reviewId: $reviewId, rating: $rating) {
+ id
+ mediaId
+ mediaType
+ summary
+ body(asHtml: true)
+ rating
+ ratingAmount
+ userRating
+ score
+ private
+ siteUrl
+ createdAt
+ updatedAt
+ user {
+ id
+ name
+ bannerImage
+ avatar {
+ medium
+ large
+ }
+ }
+ }
+ }
+ """.trimIndent()
return executeQuery(query)
}
- suspend fun postActivity(text:String): String {
+ suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
+ return executeQuery(
+ """
+ mutation {
+ ToggleFollow(userId: $id) {
+ id
+ isFollowing
+ isFollower
+ }
+ }
+ """.trimIndent())
+ }
+
+ suspend fun toggleLike(id: Int, type: String): ToggleLike? {
+ return executeQuery(
+ """
+ mutation Like {
+ ToggleLikeV2(id: $id, type: $type) {
+ __typename
+ }
+ }
+ """.trimIndent())
+ }
+
+ suspend fun postActivity(text: String, edit: Int? = null): String {
val encodedText = text.stringSanitizer()
- val query = "mutation{SaveTextActivity(text:$encodedText){siteUrl}}"
+ val query = """
+ mutation {
+ SaveTextActivity(${if (edit != null) "id: $edit," else ""} text: $encodedText) {
+ siteUrl
+ }
+ }
+ """.trimIndent()
val result = executeQuery(query)
val errors = result?.get("errors")
- return errors?.toString()
- ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
+ return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
+ }
+
+ suspend fun postMessage(userId: Int, text: String, edit: Int? = null, isPrivate: Boolean = false): String {
+ val encodedText = text.replace("", "").stringSanitizer()
+ val query = """
+ mutation {
+ SaveMessageActivity(
+ ${if (edit != null) "id: $edit," else ""}
+ recipientId: $userId,
+ message: $encodedText,
+ private: $isPrivate
+ ) {
+ id
+ }
+ }
+ """.trimIndent()
+ val result = executeQuery(query)
+ val errors = result?.get("errors")
+ return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
+ }
+
+ suspend fun postReply(activityId: Int, text: String, edit: Int? = null): String {
+ val encodedText = text.stringSanitizer()
+ val query = """
+ mutation {
+ SaveActivityReply(
+ ${if (edit != null) "id: $edit," else ""}
+ activityId: $activityId,
+ text: $encodedText
+ ) {
+ id
+ }
+ }
+ """.trimIndent()
+ val result = executeQuery(query)
+ val errors = result?.get("errors")
+ return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
}
suspend fun postReview(summary: String, body: String, mediaId: Int, score: Int): String {
val encodedSummary = summary.stringSanitizer()
val encodedBody = body.stringSanitizer()
- val query = "mutation{SaveReview(mediaId:$mediaId,summary:$encodedSummary,body:$encodedBody,score:$score){siteUrl}}"
+ val query = """
+ mutation {
+ SaveReview(
+ mediaId: $mediaId,
+ summary: $encodedSummary,
+ body: $encodedBody,
+ score: $score
+ ) {
+ siteUrl
+ }
+ }
+ """.trimIndent()
val result = executeQuery(query)
val errors = result?.get("errors")
- return errors?.toString()
- ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
+ return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
}
- suspend fun postReply(activityId: Int, text: String): String {
- val encodedText = text.stringSanitizer()
- val query = "mutation{SaveActivityReply(activityId:$activityId,text:$encodedText){id}}"
+ suspend fun deleteActivityReply(activityId: Int): Boolean {
+ val query = """
+ mutation {
+ DeleteActivityReply(id: $activityId) {
+ deleted
+ }
+ }
+ """.trimIndent()
val result = executeQuery(query)
val errors = result?.get("errors")
- return errors?.toString()
- ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
+ return errors == null
+ }
+
+ suspend fun deleteActivity(activityId: Int): Boolean {
+ val query = """
+ mutation {
+ DeleteActivity(id: $activityId) {
+ deleted
+ }
+ }
+ """.trimIndent()
+ val result = executeQuery(query)
+ val errors = result?.get("errors")
+ return errors == null
}
private fun String.stringSanitizer(): String {
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt
index 4315ad13..4c1ececd 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt
@@ -8,11 +8,12 @@ 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.MediaEdge
+import ani.dantotsu.connections.anilist.api.MediaList
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.ReplyResponse
-import ani.dantotsu.connections.anilist.api.ToggleLike
import ani.dantotsu.currContext
import ani.dantotsu.isOnline
import ani.dantotsu.logError
@@ -27,6 +28,7 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@@ -41,8 +43,8 @@ class AnilistQueries {
suspend fun getUserData(): Boolean {
val response: Query.Viewer?
measureTimeMillis {
- response =
- executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}unreadNotificationCount}}""")
+ response = executeQuery(
+ """{Viewer{name options{timezone titleLanguage staffNameLanguage activityMergeTime airingNotifications displayAdultContent restrictMessagesToFollowing} avatar{medium} bannerImage id mediaListOptions{scoreFormat rowOrder animeList{customLists} mangaList{customLists}} statistics{anime{episodesWatched} manga{chaptersRead}} unreadNotificationCount}}""")
}.also { println("time : $it") }
val user = response?.data?.user ?: return false
@@ -59,6 +61,27 @@ class AnilistQueries {
val unread = PrefManager.getVal(PrefName.UnreadCommentNotifications)
Anilist.unreadNotificationCount += unread
Anilist.initialized = true
+
+ user.options?.let {
+ Anilist.titleLanguage = it.titleLanguage.toString()
+ Anilist.staffNameLanguage = it.staffNameLanguage.toString()
+ Anilist.airingNotifications = it.airingNotifications ?: false
+ Anilist.restrictMessagesToFollowing = it.restrictMessagesToFollowing ?: false
+ Anilist.timezone = it.timezone
+ Anilist.activityMergeTime = it.activityMergeTime
+ }
+ user.mediaListOptions?.let {
+ Anilist.scoreFormat = it.scoreFormat.toString()
+ Anilist.rowOrder = it.rowOrder
+
+ it.animeList?.let { animeList ->
+ Anilist.animeCustomLists = animeList.customLists
+ }
+
+ it.mangaList?.let { mangaList ->
+ Anilist.mangaCustomLists = mangaList.customLists
+ }
+ }
return true
}
@@ -75,7 +98,7 @@ class AnilistQueries {
media.cameFromContinue = false
val query =
- """{Media(id:${media.id}){id favourites popularity episodes chapters mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}reviews(perPage:3, sort:SCORE_DESC){nodes{id mediaId mediaType summary body(asHtml:true) rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
+ """{Media(id:${media.id}){id favourites popularity episodes chapters streamingEpisodes {title thumbnail url site} mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}reviews(perPage:3, sort:SCORE_DESC){nodes{id mediaId mediaType summary body(asHtml:true) rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
runBlocking {
val anilist = async {
var response = executeQuery(query, force = true)
@@ -90,7 +113,7 @@ class AnilistQueries {
media.popularity = fetchedMedia.popularity
media.startDate = fetchedMedia.startDate
media.endDate = fetchedMedia.endDate
-
+ media.streamingEpisodes = fetchedMedia.streamingEpisodes
if (fetchedMedia.genres != null) {
media.genres = arrayListOf()
fetchedMedia.genres?.forEach { i ->
@@ -211,7 +234,7 @@ class AnilistQueries {
}
}
}
- if (fetchedMedia.reviews?.nodes != null){
+ if (fetchedMedia.reviews?.nodes != null) {
media.review = fetchedMedia.reviews!!.nodes as ArrayList
}
if (user?.mediaList?.isNotEmpty() == true) {
@@ -376,9 +399,8 @@ class AnilistQueries {
}
return media
}
- private fun continueMediaQuery(type: String, status: String): String {
- 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 } } } } } """
- }
+
+
private suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList {
var hasNextPage = true
@@ -404,10 +426,68 @@ class AnilistQueries {
return responseArray
}
+
+
+ suspend fun getUserStatus(): ArrayList? {
+ val toShow: List =
+ PrefManager.getVal(PrefName.HomeLayout)
+ if (toShow.getOrNull(7) != true) return null
+ val query = """{Page1:${status(1)}Page2:${status(2)}}"""
+ val response = executeQuery(query)
+ val list = mutableListOf()
+ val threeDaysAgo = Calendar.getInstance().apply {
+ add(Calendar.DAY_OF_MONTH, -3)
+ }.timeInMillis
+ if (response?.data?.page1 != null && response.data.page2 != null) {
+ val activities = listOf(
+ response.data.page1.activities,
+ response.data.page2.activities
+ ).asSequence().flatten()
+ .filter { it.typename != "MessageActivity" }
+ .filter { if (Anilist.adult) true else it.media?.isAdult != true }
+ .filter { it.createdAt * 1000L > threeDaysAgo }.toList()
+ .sortedByDescending { it.createdAt }
+ val anilistActivities = mutableListOf()
+ val groupedActivities = activities.groupBy { it.userId }
+
+ groupedActivities.forEach { (_, userActivities) ->
+ val user = userActivities.firstOrNull()?.user
+ if (user != null) {
+ val userToAdd = User(
+ user.id,
+ user.name ?: "",
+ user.avatar?.medium,
+ user.bannerImage,
+ activity = userActivities.sortedBy { it.createdAt }.toList()
+ )
+ if (user.id == Anilist.userid) {
+ anilistActivities.add(0, userToAdd)
+
+ } else {
+ list.add(userToAdd)
+ }
+ }
+ }
+
+ if (anilistActivities.isEmpty() && Anilist.token != null) {
+ anilistActivities.add(
+ 0,
+ User(
+ Anilist.userid!!,
+ Anilist.username!!,
+ Anilist.avatar,
+ Anilist.bg,
+ activity = listOf()
+ )
+ )
+ }
+ list.addAll(0, anilistActivities)
+ return list.toCollection(ArrayList())
+ } else return null
+ }
private fun favMediaQuery(anime: Boolean, page: Int, id: Int? = Anilist.userid): String {
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
}
-
private fun recommendationQuery(): String {
return """ Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """
}
@@ -415,250 +495,183 @@ class AnilistQueries {
private fun recommendationPlannedQuery(type: String): String {
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${if (type == "ANIME") ", sort: MEDIA_POPULARITY_DESC" else ""} ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
}
-
- suspend fun initHomePage(): Map> {
+ private fun continueMediaQuery(type: String, status: String): String {
+ 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 initHomePage(): Map> {
val removeList = PrefManager.getCustomVal("removeList", setOf())
+ val hidePrivate = PrefManager.getVal(PrefName.HidePrivate)
val removedMedia = ArrayList()
val toShow: List =
- PrefManager.getVal(PrefName.HomeLayout) // anime continue, anime fav, anime planned, manga continue, manga fav, manga planned, recommendations
- var query = """{"""
- if (toShow.getOrNull(0) == true) query += """currentAnime: ${
- continueMediaQuery(
- "ANIME",
- "CURRENT"
- )
- }, repeatingAnime: ${continueMediaQuery("ANIME", "REPEATING")}"""
- if (toShow.getOrNull(1) == true) query += """favoriteAnime: ${favMediaQuery(true, 1)}"""
- if (toShow.getOrNull(2) == true) query += """plannedAnime: ${
- continueMediaQuery(
- "ANIME",
- "PLANNING"
- )
- }"""
- if (toShow.getOrNull(3) == true) query += """currentManga: ${
- continueMediaQuery(
- "MANGA",
- "CURRENT"
- )
- }, repeatingManga: ${continueMediaQuery("MANGA", "REPEATING")}"""
- if (toShow.getOrNull(4) == true) query += """favoriteManga: ${favMediaQuery(false, 1)}"""
- if (toShow.getOrNull(5) == true) query += """plannedManga: ${
- continueMediaQuery(
- "MANGA",
- "PLANNING"
- )
- }"""
- if (toShow.getOrNull(6) == true) query += """recommendationQuery: ${recommendationQuery()}, recommendationPlannedQueryAnime: ${
- recommendationPlannedQuery(
- "ANIME"
- )
- }, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
- if (toShow.getOrNull(7) == true) query += "Page1:${status(1)}Page2:${status(2)}"
- query += """}""".trimEnd(',')
+ PrefManager.getVal(PrefName.HomeLayout) // list of booleans for what to show
+ val queries = mutableListOf()
+ if (toShow.getOrNull(0) == true) {
+ queries.add("""currentAnime: ${continueMediaQuery("ANIME", "CURRENT")}""")
+ queries.add("""repeatingAnime: ${continueMediaQuery("ANIME", "REPEATING")}""")
+ }
+ if (toShow.getOrNull(1) == true) queries.add("""favoriteAnime: ${favMediaQuery(true, 1)}""")
+ if (toShow.getOrNull(2) == true) queries.add(
+ """plannedAnime: ${
+ continueMediaQuery(
+ "ANIME",
+ "PLANNING"
+ )
+ }"""
+ )
+ if (toShow.getOrNull(3) == true) {
+ queries.add("""currentManga: ${continueMediaQuery("MANGA", "CURRENT")}""")
+ queries.add("""repeatingManga: ${continueMediaQuery("MANGA", "REPEATING")}""")
+ }
+ if (toShow.getOrNull(4) == true) queries.add(
+ """favoriteManga: ${
+ favMediaQuery(
+ false,
+ 1
+ )
+ }"""
+ )
+ if (toShow.getOrNull(5) == true) queries.add(
+ """plannedManga: ${
+ continueMediaQuery(
+ "MANGA",
+ "PLANNING"
+ )
+ }"""
+ )
+ if (toShow.getOrNull(6) == true) {
+ queries.add("""recommendationQuery: ${recommendationQuery()}""")
+ queries.add("""recommendationPlannedQueryAnime: ${recommendationPlannedQuery("ANIME")}""")
+ queries.add("""recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}""")
+ }
+
+ val query = "{${queries.joinToString(",")}}"
val response = executeQuery(query, show = true)
- val returnMap = mutableMapOf>()
- fun current(type: String) {
+ val returnMap = mutableMapOf>()
+
+ fun processMedia(
+ type: String,
+ currentMedia: List?,
+ repeatingMedia: List?
+ ) {
val subMap = mutableMapOf()
val returnArray = arrayListOf()
- val current =
- if (type == "Anime") response?.data?.currentAnime else response?.data?.currentManga
- val repeating =
- if (type == "Anime") response?.data?.repeatingAnime else response?.data?.repeatingManga
- current?.lists?.forEach { li ->
- li.entries?.reversed()?.forEach {
- val m = Media(it)
- if (m.id !in removeList) {
- m.cameFromContinue = true
- subMap[m.id] = m
- } else {
- removedMedia.add(m)
- }
+
+ (currentMedia ?: emptyList()).forEach { entry ->
+ val media = Media(entry)
+ if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) {
+ media.cameFromContinue = true
+ subMap[media.id] = media
+ } else {
+ removedMedia.add(media)
}
}
- repeating?.lists?.forEach { li ->
- li.entries?.reversed()?.forEach {
- val m = Media(it)
- if (m.id !in removeList) {
- m.cameFromContinue = true
- subMap[m.id] = m
- } else {
- removedMedia.add(m)
- }
+ (repeatingMedia ?: emptyList()).forEach { entry ->
+ val media = Media(entry)
+ if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) {
+ media.cameFromContinue = true
+ subMap[media.id] = media
+ } else {
+ removedMedia.add(media)
}
}
- if (type != "Anime") {
+ @Suppress("UNCHECKED_CAST")
+ val list = PrefManager.getNullableCustomVal(
+ "continue${type}List",
+ listOf(),
+ List::class.java
+ ) as List
+ if (list.isNotEmpty()) {
+ list.reversed().forEach { id ->
+ subMap[id]?.let { returnArray.add(it) }
+ }
+ subMap.values.forEach {
+ if (!returnArray.contains(it)) returnArray.add(it)
+ }
+ } else {
returnArray.addAll(subMap.values)
- returnMap["current$type"] = returnArray
- return
}
- @Suppress("UNCHECKED_CAST")
- val list = PrefManager.getNullableCustomVal(
- "continueAnimeList",
- listOf(),
- List::class.java
- ) as List
- if (list.isNotEmpty()) {
- list.reversed().forEach {
- if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
- }
- for (i in subMap) {
- if (i.value !in returnArray) returnArray.add(i.value)
- }
- } else returnArray.addAll(subMap.values)
returnMap["current$type"] = returnArray
-
}
- fun planned(type: String) {
- val subMap = mutableMapOf()
- val returnArray = arrayListOf()
- val current =
- if (type == "Anime") response?.data?.plannedAnime else response?.data?.plannedManga
- current?.lists?.forEach { li ->
- li.entries?.reversed()?.forEach {
- val m = Media(it)
- if (m.id !in removeList) {
- m.cameFromContinue = true
- subMap[m.id] = m
- } else {
- removedMedia.add(m)
- }
- }
- }
- @Suppress("UNCHECKED_CAST")
- val list = PrefManager.getNullableCustomVal(
- "continueAnimeList",
- listOf(),
- List::class.java
- ) as List
- if (list.isNotEmpty()) {
- list.reversed().forEach {
- if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
- }
- for (i in subMap) {
- if (i.value !in returnArray) returnArray.add(i.value)
- }
- } else returnArray.addAll(subMap.values)
- returnMap["planned$type"] = returnArray
- }
+ if (toShow.getOrNull(0) == true) processMedia(
+ "Anime",
+ response?.data?.currentAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
+ response?.data?.repeatingAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed()
+ )
+ if (toShow.getOrNull(2) == true) processMedia(
+ "AnimePlanned",
+ response?.data?.plannedAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
+ null
+ )
+ if (toShow.getOrNull(3) == true) processMedia(
+ "Manga",
+ response?.data?.currentManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
+ response?.data?.repeatingManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed()
+ )
+ if (toShow.getOrNull(5) == true) processMedia(
+ "MangaPlanned",
+ response?.data?.plannedManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
+ null
+ )
- fun favorite(type: String) {
- val favourites =
- if (type == "Anime") response?.data?.favoriteAnime?.favourites else response?.data?.favoriteManga?.favourites
- val apiMediaList = if (type == "Anime") favourites?.anime else favourites?.manga
+ fun processFavorites(type: String, favorites: List?) {
val returnArray = arrayListOf()
- apiMediaList?.edges?.forEach {
- it.node?.let { i ->
- val m = Media(i).apply { isFav = true }
- if (m.id !in removeList) {
- returnArray.add(m)
+ favorites?.forEach { edge ->
+ edge.node?.let {
+ val media = Media(it).apply { isFav = true }
+ if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) {
+ returnArray.add(media)
} else {
- removedMedia.add(m)
+ removedMedia.add(media)
}
}
}
returnMap["favorite$type"] = returnArray
}
- if (toShow.getOrNull(0) == true) {
- current("Anime")
- }
- if (toShow.getOrNull(1) == true) {
- favorite("Anime")
- }
- if (toShow.getOrNull(2) == true) {
- planned("Anime")
- }
- if (toShow.getOrNull(3) == true) {
- current("Manga")
- }
- if (toShow.getOrNull(4) == true) {
- favorite("Manga")
- }
- if (toShow.getOrNull(5) == true) {
- planned("Manga")
- }
+ if (toShow.getOrNull(1) == true) processFavorites(
+ "Anime",
+ response?.data?.favoriteAnime?.favourites?.anime?.edges
+ )
+ if (toShow.getOrNull(4) == true) processFavorites(
+ "Manga",
+ response?.data?.favoriteManga?.favourites?.manga?.edges
+ )
+
if (toShow.getOrNull(6) == true) {
val subMap = mutableMapOf()
- response?.data?.recommendationQuery?.apply {
- recommendations?.onEach {
- val json = it.mediaRecommendation
- if (json != null) {
- val m = Media(json)
- m.relation = json.type?.toString()
- subMap[m.id] = m
- }
+ response?.data?.recommendationQuery?.recommendations?.forEach {
+ it.mediaRecommendation?.let { json ->
+ val media = Media(json)
+ media.relation = json.type?.toString()
+ subMap[media.id] = media
}
}
- response?.data?.recommendationPlannedQueryAnime?.apply {
- lists?.forEach { li ->
- li.entries?.forEach {
- val m = Media(it)
- if (m.status == "RELEASING" || m.status == "FINISHED") {
- m.relation = it.media?.type?.toString()
- subMap[m.id] = m
- }
- }
+ response?.data?.recommendationPlannedQueryAnime?.lists?.flatMap {
+ it.entries ?: emptyList()
+ }?.forEach {
+ val media = Media(it)
+ if (media.status in listOf("RELEASING", "FINISHED")) {
+ media.relation = it.media?.type?.toString()
+ subMap[media.id] = media
}
}
- response?.data?.recommendationPlannedQueryManga?.apply {
- lists?.forEach { li ->
- li.entries?.forEach {
- val m = Media(it)
- if (m.status == "RELEASING" || m.status == "FINISHED") {
- m.relation = it.media?.type?.toString()
- subMap[m.id] = m
- }
- }
+ response?.data?.recommendationPlannedQueryManga?.lists?.flatMap {
+ it.entries ?: emptyList()
+ }?.forEach {
+ val media = Media(it)
+ if (media.status in listOf("RELEASING", "FINISHED")) {
+ media.relation = it.media?.type?.toString()
+ subMap[media.id] = media
}
}
- val list = ArrayList(subMap.values.toList())
- list.sortByDescending { it.meanScore }
+ val list = ArrayList(subMap.values).apply { sortByDescending { it.meanScore } }
returnMap["recommendations"] = list
}
- if (toShow.getOrNull(7) == true) {
- val list = mutableListOf()
- val threeDaysAgo = Calendar.getInstance().apply {
- add(Calendar.DAY_OF_MONTH, -3)
- }.timeInMillis
- if (response?.data?.page1 != null && response.data.page2 != null) {
- val activities = listOf(
- response.data.page1.activities,
- response.data.page2.activities
- ).asSequence().flatten()
- .filter { it.typename != "MessageActivity" }
- .filter { if (Anilist.adult) true else it.media?.isAdult == false }
- .filter { it.createdAt * 1000L > threeDaysAgo }.toList()
- .sortedByDescending { it.createdAt }
- val anilistActivities = mutableListOf()
- val groupedActivities = activities.groupBy { it.userId }
- groupedActivities.forEach { (_, userActivities) ->
- val user = userActivities.firstOrNull()?.user
- if (user != null) {
- val userToAdd = User(
- user.id,
- user.name ?: "",
- user.avatar?.medium,
- user.bannerImage,
- activity = userActivities.sortedBy { it.createdAt }.toList()
- )
- if (user.id == Anilist.userid) {
- anilistActivities.add(0, userToAdd)
- } else {
- list.add(userToAdd)
- }
- }
- }
-
-
- list.addAll(0, anilistActivities)
- returnMap["status"] = ArrayList(list)
- }
- returnMap["hidden"] = removedMedia.distinctBy { it.id } as ArrayList
- }
+ returnMap["hidden"] = removedMedia.distinctBy { it.id }.toCollection(arrayListOf())
return returnMap
}
@@ -1034,153 +1047,105 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
return null
}
- private val onListAnime =
- (if (PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace(
- "\"",
- ""
- )
- private val isAdult =
- (if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "")
+ private fun mediaList(media1: Page?): ArrayList {
+ val combinedList = arrayListOf()
+ media1?.media?.mapTo(combinedList) { Media(it) }
+ return combinedList
+ }
+
+ private fun getPreference(pref: PrefName): Boolean = PrefManager.getVal(pref)
+
+ private fun buildQueryString(
+ sort: String,
+ type: String,
+ format: String? = null,
+ country: String? = null
+ ): String {
+ val includeList = when {
+ type == "ANIME" && !getPreference(PrefName.IncludeAnimeList) -> "onList:false"
+ type == "MANGA" && !getPreference(PrefName.IncludeMangaList) -> "onList:false"
+ else -> ""
+ }
+ val isAdult = if (getPreference(PrefName.AdultOnly)) "isAdult:true" else ""
+ val formatFilter = format?.let { "format:$it, " } ?: ""
+ val countryFilter = country?.let { "countryOfOrigin:$it, " } ?: ""
+
+ return buildString {
+ append("""Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:$sort, type:$type, $formatFilter $countryFilter $includeList $isAdult){id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}""")
+ }
+ }
private fun recentAnimeUpdates(page: Int): String {
- return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${System.currentTimeMillis() / 1000 - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}"""
- }
-
- private fun trendingMovies(page: Int): String {
- return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: ANIME, format: MOVIE, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
- }
-
- private fun topRatedAnime(page: Int): String {
- return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
- }
-
- private fun mostFavAnime(page: Int): String {
- return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
- }
-
- suspend fun loadAnimeList(): Map> {
- val list = mutableMapOf>()
- fun query(): String {
- return """{
- recentUpdates:${recentAnimeUpdates(1)}
- recentUpdates2:${recentAnimeUpdates(2)}
- trendingMovies:${trendingMovies(1)}
- trendingMovies2:${trendingMovies(2)}
- topRated:${topRatedAnime(1)}
- topRated2:${topRatedAnime(2)}
- mostFav:${mostFavAnime(1)}
- mostFav2:${mostFavAnime(2)}
- }""".trimIndent()
+ val currentTime = System.currentTimeMillis() / 1000
+ return buildString {
+ append("""Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${currentTime - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}""")
}
- executeQuery(query(), force = true)?.data?.apply {
- val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
- val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly)
- val idArr = mutableListOf()
- list["recentUpdates"] = recentUpdates?.airingSchedules?.mapNotNull { i ->
- i.media?.let {
- if (!idArr.contains(it.id))
- if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
- idArr.add(it.id)
- Media(it)
- } else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)) {
- idArr.add(it.id)
- Media(it)
- } else if ((listOnly && it.mediaListEntry != null)) {
- idArr.add(it.id)
- Media(it)
- } else null
- else null
+ }
+
+ private fun queryAnimeList(): String {
+ return buildString {
+ append("""{recentUpdates:${recentAnimeUpdates(1)} recentUpdates2:${recentAnimeUpdates(2)} trendingMovies:${buildQueryString("POPULARITY_DESC", "ANIME", "MOVIE")} topRated:${buildQueryString("SCORE_DESC", "ANIME")} mostFav:${buildQueryString("FAVOURITES_DESC", "ANIME")}}""")
+ }
+ }
+
+ private fun queryMangaList(): String {
+ return buildString {
+ append("""{trendingManga:${buildQueryString("POPULARITY_DESC", "MANGA", country = "JP")} trendingManhwa:${buildQueryString("POPULARITY_DESC", "MANGA", country = "KR")} trendingNovel:${buildQueryString("POPULARITY_DESC", "MANGA", format = "NOVEL", country = "JP")} topRated:${buildQueryString("SCORE_DESC", "MANGA")} mostFav:${buildQueryString("FAVOURITES_DESC", "MANGA")}}""")
+ }
+ }
+
+ suspend fun loadAnimeList(): Map> = coroutineScope {
+ val list = mutableMapOf>()
+
+ fun filterRecentUpdates(page: Page?): ArrayList {
+ val listOnly = getPreference(PrefName.RecentlyListOnly)
+ val adultOnly = getPreference(PrefName.AdultOnly)
+ val idArr = mutableSetOf()
+ return page?.airingSchedules?.mapNotNull { i ->
+ i.media?.takeIf { !idArr.contains(it.id) }?.let {
+ val shouldAdd = when {
+ !listOnly && it.countryOfOrigin == "JP" && adultOnly && it.isAdult == true -> true
+ !listOnly && !adultOnly && it.countryOfOrigin == "JP" && it.isAdult == false -> true
+ listOnly && it.mediaListEntry != null -> true
+ else -> false
+ }
+ if (shouldAdd) {
+ idArr.add(it.id)
+ Media(it)
+ } else null
}
}?.toCollection(ArrayList()) ?: arrayListOf()
-
- list["trendingMovies"] = trendingMovies?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
- list["topRated"] = topRated?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
- list["mostFav"] = mostFav?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
-
- list["recentUpdates"]?.addAll(recentUpdates2?.airingSchedules?.mapNotNull { i ->
- i.media?.let {
- if (!idArr.contains(it.id))
- if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
- idArr.add(it.id)
- Media(it)
- } else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)) {
- idArr.add(it.id)
- Media(it)
- } else if ((listOnly && it.mediaListEntry != null)) {
- idArr.add(it.id)
- Media(it)
- } else null
- else null
- }
- }?.toCollection(ArrayList()) ?: arrayListOf())
- list["trendingMovies"]?.addAll(trendingMovies2?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf())
- list["topRated"]?.addAll(topRated2?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf())
- list["mostFav"]?.addAll(mostFav2?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf())
}
- return list
+
+ val animeList = async { executeQuery(queryAnimeList(), force = true) }
+
+ animeList.await()?.data?.apply {
+ list["recentUpdates"] = filterRecentUpdates(recentUpdates)
+ list["trendingMovies"] = mediaList(trendingMovies)
+ list["topRated"] = mediaList(topRated)
+ list["mostFav"] = mediaList(mostFav)
+ }
+
+ list
}
- private val onListManga =
- (if (PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace(
- "\"",
- ""
- )
-
- private fun trendingManga(page: Int): String {
- return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA,countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
- }
-
- private fun trendingManhwa(page: Int): String {
- return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, countryOfOrigin:KR, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
- }
-
- private fun trendingNovel(page: Int): String {
- return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, format: NOVEL, countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
- }
-
- private fun topRatedManga(page: Int): String {
- return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
- }
-
- private fun mostFavManga(page: Int): String {
- return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
- }
-
- suspend fun loadMangaList(): Map> {
+ suspend fun loadMangaList(): Map> = coroutineScope {
val list = mutableMapOf>()
- fun query(): String {
- return """{
- trendingManga:${trendingManga(1)}
- trendingManga2:${trendingManga(2)}
- trendingManhwa:${trendingManhwa(1)}
- trendingManhwa2:${trendingManhwa(2)}
- trendingNovel:${trendingNovel(1)}
- trendingNovel2:${trendingNovel(2)}
- topRated:${topRatedManga(1)}
- topRated2:${topRatedManga(2)}
- mostFav:${mostFavManga(1)}
- mostFav2:${mostFavManga(2)}
- }""".trimIndent()
+
+ val mangaList = async { executeQuery(queryMangaList(), force = true) }
+
+ mangaList.await()?.data?.apply {
+ list["trendingManga"] = mediaList(trendingManga)
+ list["trendingManhwa"] = mediaList(trendingManhwa)
+ list["trendingNovel"] = mediaList(trendingNovel)
+ list["topRated"] = mediaList(topRated)
+ list["mostFav"] = mediaList(mostFav)
}
- executeQuery(query(), force = true)?.data?.apply {
- list["trendingManga"] = trendingManga?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
- list["trendingManhwa"] = trendingManhwa?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
- list["trendingNovel"] = trendingNovel?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
- list["topRated"] = topRated?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
- list["mostFav"] = mostFav?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
-
- list["trendingManga"]?.addAll(trendingManga2?.media?.map { Media(it) }?.toList() ?: arrayListOf())
- list["trendingManhwa"]?.addAll(trendingManhwa2?.media?.map { Media(it) }?.toList() ?: arrayListOf())
- list["trendingNovel"]?.addAll(trendingNovel2?.media?.map { Media(it) }?.toList() ?: arrayListOf())
- list["topRated"]?.addAll(topRated2?.media?.map { Media(it) }?.toList() ?: arrayListOf())
- list["mostFav"]?.addAll(mostFav2?.media?.map { Media(it) }?.toList() ?: arrayListOf())
- }
-
-
- return list
+ list
}
+
suspend fun recentlyUpdated(
greater: Long = 0,
lesser: Long = System.currentTimeMillis() / 1000 - 10000
@@ -1509,25 +1474,17 @@ Page(page:$page,perPage:50) {
return author
}
- suspend fun getReviews(mediaId: Int, page: Int = 1, sort: String = "SCORE_DESC"): Query.ReviewsResponse? {
+ suspend fun getReviews(
+ mediaId: Int,
+ page: Int = 1,
+ sort: String = "SCORE_DESC"
+ ): Query.ReviewsResponse? {
return executeQuery(
"""{Page(page:$page,perPage:10){pageInfo{currentPage,hasNextPage,total}reviews(mediaId:$mediaId,sort:$sort){id,mediaId,mediaType,summary,body(asHtml:true)rating,ratingAmount,userRating,score,private,siteUrl,createdAt,updatedAt,user{id,name,bannerImage avatar{medium,large}}}}}""",
force = true
)
}
- suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
- return executeQuery(
- """mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}"""
- )
- }
-
- suspend fun toggleLike(id: Int, type: String): ToggleLike? {
- return executeQuery(
- """mutation Like{ToggleLikeV2(id:$id,type:$type){__typename}}"""
- )
- }
-
suspend fun getUserProfile(id: Int): Query.UserProfileResponse? {
return executeQuery(
"""{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}}""",
@@ -1586,11 +1543,13 @@ Page(page:$page,perPage:50) {
suspend fun getNotifications(
id: Int,
page: Int = 1,
- resetNotification: Boolean = true
+ resetNotification: Boolean = true,
+ type: Boolean? = null
): NotificationResponse? {
+ val typeIn = "type_in:[AIRING,MEDIA_MERGE,MEDIA_DELETION,MEDIA_DATA_CHANGE]"
val reset = if (resetNotification) "true" else "false"
val res = executeQuery(
- """{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,}}}}""",
+ """{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){pageInfo{currentPage,hasNextPage}notifications(resetNotificationCount:$reset , ${if (type == true) typeIn else ""}){__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) {
@@ -1612,8 +1571,9 @@ Page(page:$page,perPage:50) {
else if (userId != null) "userId:$userId,"
else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
else "isFollowing:true,"
+ val typeIn = if (filter == "isFollowing:true,") "type_in:[TEXT,ANIME_LIST,MANGA_LIST,MEDIA_LIST]," else ""
return executeQuery(
- """{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}isAdult}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}}}}}}""",
+ """{Page(page:$page,perPage:$ITEMS_PER_PAGE){activities(${filter}${typeIn}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}isAdult}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
)
}
@@ -1621,13 +1581,14 @@ Page(page:$page,perPage:50) {
suspend fun getReplies(
activityId: Int,
page: Int = 1
- ) : ReplyResponse? {
- val query = """{Page(page:$page,perPage:50){activityReplies(activityId:$activityId){id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}}}"""
+ ): ReplyResponse? {
+ val query =
+ """{Page(page:$page,perPage:50){activityReplies(activityId:$activityId){id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}}}"""
return executeQuery(query, force = true)
}
private fun status(page: Int = 1): String {
- return """Page(page:$page,perPage:50){activities(isFollowing: true,sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed replyCount likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed replyCount likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id isAdult title{english romaji native userPreferred}bannerImage coverImage{extraLarge medium large}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id type createdAt}}}"""
+ return """Page(page:$page,perPage:50){activities(isFollowing: true,type_in:[TEXT,ANIME_LIST,MANGA_LIST,MEDIA_LIST],sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed replyCount likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed replyCount likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id isAdult title{english romaji native userPreferred}bannerImage coverImage{extraLarge medium large}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id type createdAt}}}"""
}
suspend fun getUpcomingAnime(id: String): List {
@@ -1674,4 +1635,4 @@ Page(page:$page,perPage:50) {
companion object {
const val ITEMS_PER_PAGE = 25
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt
index 6cda8e7a..ee978214 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt
@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
suspend fun getUserId(context: Context, block: () -> Unit) {
- if (!Anilist.initialized) {
+ if (!Anilist.initialized && PrefManager.getVal(PrefName.AnilistToken) != "") {
if (Anilist.query.getUserData()) {
tryWithSuspend {
if (MAL.token != null && !MAL.query.getUserData())
@@ -81,24 +81,26 @@ class AnilistHomeViewModel : ViewModel() {
MutableLiveData>(null)
fun getUserStatus(): LiveData> = userStatus
+ suspend fun initUserStatus() {
+ val res = Anilist.query.getUserStatus()
+ res?.let { userStatus.postValue(it) }
+ }
private val hidden: MutableLiveData> =
MutableLiveData>(null)
fun getHidden(): LiveData> = hidden
- @Suppress("UNCHECKED_CAST")
suspend fun initHomePage() {
val res = Anilist.query.initHomePage()
- res["currentAnime"]?.let { animeContinue.postValue(it as ArrayList?) }
- res["favoriteAnime"]?.let { animeFav.postValue(it as ArrayList?) }
- res["plannedAnime"]?.let { animePlanned.postValue(it as ArrayList?) }
- res["currentManga"]?.let { mangaContinue.postValue(it as ArrayList?) }
- res["favoriteManga"]?.let { mangaFav.postValue(it as ArrayList?) }
- res["plannedManga"]?.let { mangaPlanned.postValue(it as ArrayList?) }
- res["recommendations"]?.let { recommendation.postValue(it as ArrayList?) }
- res["hidden"]?.let { hidden.postValue(it as ArrayList?) }
- res["status"]?.let { userStatus.postValue(it as ArrayList?) }
+ res["currentAnime"]?.let { animeContinue.postValue(it) }
+ res["favoriteAnime"]?.let { animeFav.postValue(it) }
+ res["currentAnimePlanned"]?.let { animePlanned.postValue(it) }
+ res["currentManga"]?.let { mangaContinue.postValue(it) }
+ res["favoriteManga"]?.let { mangaFav.postValue(it) }
+ res["currentMangaPlanned"]?.let { mangaPlanned.postValue(it) }
+ res["recommendations"]?.let { recommendation.postValue(it) }
+ res["hidden"]?.let { hidden.postValue(it) }
}
suspend fun loadMain(context: FragmentActivity) {
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt
index e6635b9a..e909a3bc 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt
@@ -163,13 +163,9 @@ class Query {
@Serializable
data class Data(
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?,
- @SerialName("recentUpdates2") val recentUpdates2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?,
- @SerialName("trendingMovies2") val trendingMovies2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
- @SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
- @SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
)
}
@@ -181,15 +177,10 @@ class Query {
@Serializable
data class Data(
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?,
- @SerialName("trendingManga2") val trendingManga2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
- @SerialName("trendingManhwa2") val trendingManhwa2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
- @SerialName("trendingNovel2") val trendingNovel2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
- @SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
- @SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
)
}
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt
index 23c79045..a54e3ad8 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt
@@ -143,7 +143,7 @@ data class Media(
@SerialName("externalLinks") var externalLinks: List?,
// Data and links to legal streaming episodes on external sites
- // @SerialName("streamingEpisodes") var streamingEpisodes: List?,
+ @SerialName("streamingEpisodes") var streamingEpisodes: List?,
// The ranking of the media in a particular time span and format compared to other media
// @SerialName("rankings") var rankings: List?,
@@ -239,7 +239,20 @@ data class AiringSchedule(
// The associate media of the airing episode
@SerialName("media") var media: Media?,
)
+@Serializable
+data class MediaStreamingEpisode(
+ // The title of the episode
+ @SerialName("title") var title: String?,
+ // The thumbnail image of the episode
+ @SerialName("thumbnail") var thumbnail: String?,
+
+ // The url of the episode
+ @SerialName("url") var url: String?,
+
+ // The site location of the streaming episode
+ @SerialName("site") var site: String?,
+)
@Serializable
data class MediaCoverImage(
// The cover image url of the media at its largest size. If this size isn't available, large will be provided instead.
diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt
index 9117c144..ed20cb26 100644
--- a/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt
+++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt
@@ -74,7 +74,7 @@ data class User(
@Serializable
data class UserOptions(
// The language the user wants to see media titles in
- // @SerialName("titleLanguage") var titleLanguage: UserTitleLanguage?,
+ @SerialName("titleLanguage") var titleLanguage: UserTitleLanguage?,
// Whether the user has enabled viewing of 18+ content
@SerialName("displayAdultContent") var displayAdultContent: Boolean?,
@@ -88,17 +88,17 @@ data class UserOptions(
// // Notification options
// // @SerialName("notificationOptions") var notificationOptions: List?,
//
- // // The user's timezone offset (Auth user only)
- // @SerialName("timezone") var timezone: String?,
+ // The user's timezone offset (Auth user only)
+ @SerialName("timezone") var timezone: String?,
//
- // // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always.
- // @SerialName("activityMergeTime") var activityMergeTime: Int?,
+ // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always.
+ @SerialName("activityMergeTime") var activityMergeTime: Int?,
//
- // // The language the user wants to see staff and character names in
- // // @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?,
+ // The language the user wants to see staff and character names in
+ @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?,
//
- // // Whether the user only allow messages from users they follow
- // @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?,
+ // Whether the user only allow messages from users they follow
+ @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?,
// The list activity types the user has disabled from being created from list updates
// @SerialName("disabledListActivity") var disabledListActivity: List?,
@@ -119,6 +119,40 @@ data class UserStatisticTypes(
@SerialName("manga") var manga: UserStatistics?
)
+@Serializable
+enum class UserTitleLanguage {
+ @SerialName("ENGLISH")
+ ENGLISH,
+ @SerialName("ROMAJI")
+ ROMAJI,
+ @SerialName("NATIVE")
+ NATIVE
+}
+
+@Serializable
+enum class UserStaffNameLanguage {
+ @SerialName("ROMAJI_WESTERN")
+ ROMAJI_WESTERN,
+ @SerialName("ROMAJI")
+ ROMAJI,
+ @SerialName("NATIVE")
+ NATIVE
+}
+
+@Serializable
+enum class ScoreFormat {
+ @SerialName("POINT_100")
+ POINT_100,
+ @SerialName("POINT_10_DECIMAL")
+ POINT_10_DECIMAL,
+ @SerialName("POINT_10")
+ POINT_10,
+ @SerialName("POINT_5")
+ POINT_5,
+ @SerialName("POINT_3")
+ POINT_3,
+}
+
@Serializable
data class UserStatistics(
//
@@ -164,7 +198,7 @@ data class Favourites(
@Serializable
data class MediaListOptions(
// The score format the user is using for media lists
- @SerialName("scoreFormat") var scoreFormat: String?,
+ @SerialName("scoreFormat") var scoreFormat: ScoreFormat?,
// The default order list rows should be displayed in
@SerialName("rowOrder") var rowOrder: String?,
@@ -181,8 +215,8 @@ data class MediaListTypeOptions(
// The order each list should be displayed in
@SerialName("sectionOrder") var sectionOrder: List?,
- // If the completed sections of the list should be separated by format
- @SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?,
+ // // If the completed sections of the list should be separated by format
+ // @SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?,
// The names of the user's custom lists
@SerialName("customLists") var customLists: List?,
diff --git a/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt b/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt
deleted file mode 100644
index 53a88815..00000000
--- a/app/src/main/java/ani/dantotsu/connections/bakaupdates/MangaUpdates.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-package ani.dantotsu.connections.bakaupdates
-
-import android.content.Context
-import ani.dantotsu.R
-import ani.dantotsu.client
-import ani.dantotsu.connections.anilist.api.FuzzyDate
-import ani.dantotsu.tryWithSuspend
-import ani.dantotsu.util.Logger
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import okio.ByteString.Companion.encode
-import org.json.JSONException
-import org.json.JSONObject
-import java.nio.charset.Charset
-
-
-class MangaUpdates {
-
- private val Int?.dateFormat get() = String.format("%02d", this)
-
- private val apiUrl = "https://api.mangaupdates.com/v1/releases/search"
-
- suspend fun search(title: String, startDate: FuzzyDate?): MangaUpdatesResponse.Results? {
- return tryWithSuspend {
- val query = JSONObject().apply {
- try {
- put("search", title.encode(Charset.forName("UTF-8")))
- startDate?.let {
- put(
- "start_date",
- "${it.year}-${it.month.dateFormat}-${it.day.dateFormat}"
- )
- }
- put("include_metadata", true)
- } catch (e: JSONException) {
- e.printStackTrace()
- }
- }
- val res = try {
- client.post(apiUrl, json = query).parsed()
- } catch (e: Exception) {
- Logger.log(e.toString())
- return@tryWithSuspend null
- }
- coroutineScope {
- res.results?.map {
- async(Dispatchers.IO) {
- Logger.log(it.toString())
- }
- }
- }?.awaitAll()
- res.results?.first {
- it.metadata.series.lastUpdated?.timestamp != null
- && (it.metadata.series.latestChapter != null
- || (it.record.volume.isNullOrBlank() && it.record.chapter != null))
- }
- }
- }
-
- companion object {
- fun getLatestChapter(context: Context, results: MangaUpdatesResponse.Results): String {
- return results.metadata.series.latestChapter?.let {
- context.getString(R.string.chapter_number, it)
- } ?: results.record.chapter!!.substringAfterLast("-").trim().let { chapter ->
- chapter.takeIf {
- it.toIntOrNull() == null
- } ?: context.getString(R.string.chapter_number, chapter.toInt())
- }
- }
- }
-
- @Serializable
- data class MangaUpdatesResponse(
- @SerialName("total_hits")
- val totalHits: Int?,
- @SerialName("page")
- val page: Int?,
- @SerialName("per_page")
- val perPage: Int?,
- val results: List? = null
- ) {
- @Serializable
- data class Results(
- val record: Record,
- val metadata: MetaData
- ) {
- @Serializable
- data class Record(
- @SerialName("id")
- val id: Int,
- @SerialName("title")
- val title: String,
- @SerialName("volume")
- val volume: String?,
- @SerialName("chapter")
- val chapter: String?,
- @SerialName("release_date")
- val releaseDate: String
- )
-
- @Serializable
- data class MetaData(
- val series: Series
- ) {
- @Serializable
- data class Series(
- @SerialName("series_id")
- val seriesId: Long?,
- @SerialName("title")
- val title: String?,
- @SerialName("latest_chapter")
- val latestChapter: Int?,
- @SerialName("last_updated")
- val lastUpdated: LastUpdated?
- ) {
- @Serializable
- data class LastUpdated(
- @SerialName("timestamp")
- val timestamp: Long,
- @SerialName("as_rfc3339")
- val asRfc3339: String,
- @SerialName("as_string")
- val asString: String
- )
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt b/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt
index 57d08531..8f6fb98d 100644
--- a/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt
+++ b/app/src/main/java/ani/dantotsu/connections/comments/CommentsAPI.kt
@@ -27,8 +27,11 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object CommentsAPI {
- private const val ADDRESS: String = "https://api.dantotsu.app"
+ private const val API_ADDRESS: String = "https://api.dantotsu.app"
+ private const val LOCAL_HOST: String = "https://127.0.0.1"
private var isOnline: Boolean = true
+ private var commentsEnabled = PrefManager.getVal(PrefName.CommentsEnabled) == 1
+ private val ADDRESS: String get() = if (commentsEnabled) API_ADDRESS else LOCAL_HOST
var authToken: String? = null
var userId: String? = null
var isBanned: Boolean = false
@@ -369,10 +372,9 @@ object CommentsAPI {
}
errorMessage("Failed to login after multiple attempts")
}
-
private fun errorMessage(reason: String) {
- Logger.log(reason)
- if (isOnline) snackString(reason)
+ if (commentsEnabled) Logger.log(reason)
+ if (isOnline && commentsEnabled) snackString(reason)
}
fun logout() {
diff --git a/app/src/main/java/ani/dantotsu/connections/discord/Discord.kt b/app/src/main/java/ani/dantotsu/connections/discord/Discord.kt
index 9318e5e6..f6e791f4 100644
--- a/app/src/main/java/ani/dantotsu/connections/discord/Discord.kt
+++ b/app/src/main/java/ani/dantotsu/connections/discord/Discord.kt
@@ -70,7 +70,7 @@ object Discord {
const val application_Id = "1163925779692912771"
const val small_Image: String =
- "mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
+ "mp:external/9NqpMxXs4ZNQtMG42L7hqINW92GqqDxgxS9Oh0Sp880/%3Fsize%3D48%26quality%3Dlossless%26name%3DDantotsu/https/cdn.discordapp.com/emojis/1167344924874784828.gif"
const val small_Image_AniList: String =
- "mp:external/rHOIjjChluqQtGyL_UHk6Z4oAqiVYlo_B7HSGPLSoUg/%3Fsize%3D128/https/cdn.discordapp.com/icons/210521487378087947/a_f54f910e2add364a3da3bb2f2fce0c72.webp"
+ "https://anilist.co/img/icons/android-chrome-512x512.png"
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt b/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt
index f46dd63d..468e75cf 100644
--- a/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt
+++ b/app/src/main/java/ani/dantotsu/connections/discord/RPC.kt
@@ -1,24 +1,19 @@
package ani.dantotsu.connections.discord
+import ani.dantotsu.connections.discord.Discord.token
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
+import okhttp3.OkHttpClient
import kotlin.coroutines.CoroutineContext
-import ani.dantotsu.client as app
+import java.util.concurrent.TimeUnit.SECONDS
@Suppress("MemberVisibilityCanBePrivate")
open class RPC(val token: String, val coroutineContext: CoroutineContext) {
- private val json = Json {
- encodeDefaults = true
- allowStructuredMapKeys = true
- ignoreUnknownKeys = true
- }
-
enum class Type {
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
}
@@ -27,7 +22,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
companion object {
data class RPCData(
- val applicationId: String? = null,
+ val applicationId: String,
val type: Type? = null,
val activityName: String? = null,
val details: String? = null,
@@ -39,23 +34,21 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
val stopTimestamp: Long? = null,
val buttons: MutableList = mutableListOf()
)
-
- @Serializable
- data class KizzyApi(val id: String)
-
- val api = "https://kizzy-api.vercel.app/image?url="
- private suspend fun String.discordUrl(): String? {
- if (startsWith("mp:")) return this
- val json = app.get("$api$this").parsedSafe()
- return json?.id
- }
-
suspend fun createPresence(data: RPCData): String {
val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
}
+ val client = OkHttpClient.Builder()
+ .connectTimeout(10, SECONDS)
+ .readTimeout(10, SECONDS)
+ .writeTimeout(10, SECONDS)
+ .build()
+
+ val assetApi = RPCExternalAsset(data.applicationId, token!!, client, json)
+ suspend fun String.discordUrl() = assetApi.getDiscordUri(this)
+
return json.encodeToString(Presence.Response(
3,
Presence(
diff --git a/app/src/main/java/ani/dantotsu/connections/discord/RpcExternalAsset.kt b/app/src/main/java/ani/dantotsu/connections/discord/RpcExternalAsset.kt
new file mode 100644
index 00000000..c16f4b82
--- /dev/null
+++ b/app/src/main/java/ani/dantotsu/connections/discord/RpcExternalAsset.kt
@@ -0,0 +1,59 @@
+package ani.dantotsu.connections.discord
+
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import okio.IOException
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+class RPCExternalAsset(
+ applicationId: String,
+ private val token: String,
+ private val client: OkHttpClient,
+ private val json: Json
+) {
+
+ @Serializable
+ data class ExternalAsset(
+ val url: String? = null,
+ @SerialName("external_asset_path")
+ val externalAssetPath: String? = null
+ )
+
+ private val api = "https://discord.com/api/v9/applications/$applicationId/external-assets"
+ suspend fun getDiscordUri(imageUrl: String): String? {
+ if (imageUrl.startsWith("mp:")) return imageUrl
+ val request = Request.Builder().url(api).header("Authorization", token)
+ .post("{\"urls\":[\"$imageUrl\"]}".toRequestBody("application/json".toMediaType()))
+ .build()
+ return runCatching {
+ val res = client.newCall(request).await()
+ json.decodeFromString>(res.body.string())
+ .firstOrNull()?.externalAssetPath?.let { "mp:$it" }
+ }.getOrNull()
+ }
+
+ private suspend inline fun Call.await(): Response {
+ return suspendCoroutine {
+ enqueue(object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ it.resumeWithException(e)
+ }
+
+ override fun onResponse(call: Call, response: Response) {
+ it.resume(response)
+ }
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/connections/discord/serializers/Activity.kt b/app/src/main/java/ani/dantotsu/connections/discord/serializers/Activity.kt
index 2e6366ac..4416fe5c 100644
--- a/app/src/main/java/ani/dantotsu/connections/discord/serializers/Activity.kt
+++ b/app/src/main/java/ani/dantotsu/connections/discord/serializers/Activity.kt
@@ -40,6 +40,7 @@ data class Activity(
@Serializable
data class Timestamps(
val start: Long? = null,
+ @SerialName("end")
val stop: Long? = null
)
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/connections/github/Contributors.kt b/app/src/main/java/ani/dantotsu/connections/github/Contributors.kt
index 337e4c49..64ef15bb 100644
--- a/app/src/main/java/ani/dantotsu/connections/github/Contributors.kt
+++ b/app/src/main/java/ani/dantotsu/connections/github/Contributors.kt
@@ -28,6 +28,7 @@ class Contributors {
"rebelonion" -> "Owner & Maintainer"
"sneazy-ibo" -> "Contributor & Comment Moderator"
"WaiWhat" -> "Icon Designer"
+ "itsmechinmoy" -> "Discord and Telegram Admin/Helper, Comment Moderator & Translator"
else -> "Contributor"
}
developers = developers.plus(
@@ -89,9 +90,15 @@ class Contributors {
"Comment Moderator and Arabic Translator",
"https://anilist.co/user/6049773"
),
+ Developer(
+ "Dawnusedyeet",
+ "https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6237399-RHFvRHriXjwS.png",
+ "Contributor",
+ "https://anilist.co/user/Dawnusedyeet/"
+ ),
Developer(
"hastsu",
- "https://cdn.discordapp.com/avatars/602422545077108749/20b4a6efa4314550e4ed51cdbe4fef3d.webp?size=160",
+ "https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6183359-9os7zUhYdF64.jpg",
"Comment Moderator and Arabic Translator",
"https://anilist.co/user/6183359"
),
@@ -111,4 +118,4 @@ class Contributors {
@SerialName("html_url")
val htmlUrl: String
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt b/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt
index 892c6521..bd4e5c84 100644
--- a/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt
+++ b/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt
@@ -125,7 +125,7 @@ class DownloadCompat {
Logger.log(e)
Injekt.get().logException(e)
return OfflineAnimeModel(
- "unknown",
+ downloadedType.titleName,
"0",
"??",
"??",
@@ -188,7 +188,7 @@ class DownloadCompat {
Logger.log(e)
Injekt.get().logException(e)
return OfflineMangaModel(
- "unknown",
+ downloadedType.titleName,
"0",
"??",
"??",
diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
index 3b44e66d..771684db 100644
--- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
+++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt
@@ -13,7 +13,6 @@ import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.deleteRecursively
-import com.anggrayudi.storage.file.findFolder
import com.anggrayudi.storage.file.moveFolderTo
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
@@ -279,6 +278,7 @@ class DownloadsManager(private val context: Context) {
* @param type the type of media
* @return the base directory
*/
+ @Synchronized
private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null
@@ -307,6 +307,7 @@ class DownloadsManager(private val context: Context) {
* @param chapter the chapter of the media
* @return the subdirectory
*/
+ @Synchronized
fun getSubDirectory(
context: Context,
type: MediaType,
@@ -344,23 +345,34 @@ class DownloadsManager(private val context: Context) {
}
}
+ @Synchronized
private fun getBaseDirectory(context: Context): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null
- return DocumentFile.fromTreeUri(context, baseDirectory)
+ val base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
+ return base.findOrCreateFolder(BASE_LOCATION, false)
}
+ private val lock = Any()
+
private fun DocumentFile.findOrCreateFolder(
name: String, overwrite: Boolean
): DocumentFile? {
- return if (overwrite) {
- findFolder(name.findValidName())?.delete()
- createDirectory(name.findValidName())
- } else {
- findFolder(name.findValidName()) ?: createDirectory(name.findValidName())
+ val validName = name.findValidName()
+ synchronized(lock) {
+ return if (overwrite) {
+ findFolder(validName)?.delete()
+ createDirectory(validName)
+ } else {
+ val folder = findFolder(validName)
+ folder ?: createDirectory(validName)
+ }
}
}
+ private fun DocumentFile.findFolder(name: String): DocumentFile? =
+ listFiles().find { it.name == name && it.isDirectory }
+
private const val RATIO_THRESHOLD = 95
fun Media.compareName(name: String): Boolean {
val mainName = mainName().findValidName().lowercase()
@@ -379,7 +391,7 @@ class DownloadsManager(private val context: Context) {
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
fun String?.findValidName(): String {
- return this?.replace("/","_")?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
+ return this?.replace("/", "_")?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
}
data class DownloadedType(
diff --git a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt
index ebeb17b4..96b4e39e 100644
--- a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt
@@ -181,7 +181,6 @@ class AnimeDownloaderService : Service() {
}
private fun updateNotification() {
- // Update the notification to reflect the current state of the queue
val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
val text = if (pendingDownloads > 0) {
"Pending downloads: $pendingDownloads"
@@ -201,8 +200,8 @@ class AnimeDownloaderService : Service() {
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) {
- try {
- withContext(Dispatchers.Main) {
+ withContext(Dispatchers.IO) {
+ try {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@AnimeDownloaderService,
@@ -214,22 +213,34 @@ class AnimeDownloaderService : Service() {
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
if (notifi) {
- notificationManager.notify(NOTIFICATION_ID, builder.build())
+ withContext(Dispatchers.Main) {
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ }
}
- val outputDir = getSubDirectory(
+ val baseOutputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
+ task.title
+ ) ?: throw Exception("Failed to create output directory")
+ val outputDir = getSubDirectory(
+ this@AnimeDownloaderService,
+ MediaType.ANIME,
+ true,
task.title,
task.episode
) ?: throw Exception("Failed to create output directory")
val extension = ffExtension!!.getFileExtension()
- outputDir.findFile("${task.getTaskName().findValidName()}.${extension.first}")?.delete()
+ outputDir.findFile("${task.getTaskName().findValidName()}.${extension.first}")
+ ?.delete()
val outputFile =
- outputDir.createFile(extension.second, "${task.getTaskName()}.${extension.first}")
+ outputDir.createFile(
+ extension.second,
+ "${task.getTaskName()}.${extension.first}"
+ )
?: throw Exception("Failed to create output file")
var percent = 0
@@ -273,7 +284,7 @@ class AnimeDownloaderService : Service() {
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
ffTask
- saveMediaInfo(task)
+ saveMediaInfo(task, baseOutputDir)
// periodically check if the download is complete
while (ffExtension.getState(ffTask) != "COMPLETED") {
@@ -287,7 +298,11 @@ class AnimeDownloaderService : Service() {
)
} Download failed"
)
- notificationManager.notify(NOTIFICATION_ID, builder.build())
+ if (notifi) {
+ withContext(Dispatchers.Main) {
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ }
+ }
toast("${getTaskName(task.title, task.episode)} Download failed")
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
downloadsManager.removeDownload(
@@ -320,7 +335,9 @@ class AnimeDownloaderService : Service() {
percent.coerceAtMost(99)
)
if (notifi) {
- notificationManager.notify(NOTIFICATION_ID, builder.build())
+ withContext(Dispatchers.Main) {
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ }
}
kotlinx.coroutines.delay(2000)
}
@@ -335,7 +352,11 @@ class AnimeDownloaderService : Service() {
)
} Download failed"
)
- notificationManager.notify(NOTIFICATION_ID, builder.build())
+ if (notifi) {
+ withContext(Dispatchers.Main) {
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ }
+ }
snackString("${getTaskName(task.title, task.episode)} Download failed")
downloadsManager.removeDownload(
DownloadedType(
@@ -367,7 +388,11 @@ class AnimeDownloaderService : Service() {
)
} Download completed"
)
- notificationManager.notify(NOTIFICATION_ID, builder.build())
+ if (notifi) {
+ withContext(Dispatchers.Main) {
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ }
+ }
snackString("${getTaskName(task.title, task.episode)} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
@@ -385,23 +410,20 @@ class AnimeDownloaderService : Service() {
broadcastDownloadFinished(task.episode)
} else throw Exception("Download failed")
+ } catch (e: Exception) {
+ if (e.message?.contains("Coroutine was cancelled") == false) { //wut
+ Logger.log("Exception while downloading file: ${e.message}")
+ snackString("Exception while downloading file: ${e.message}")
+ e.printStackTrace()
+ Injekt.get().logException(e)
+ }
+ broadcastDownloadFailed(task.episode)
}
- } catch (e: Exception) {
- if (e.message?.contains("Coroutine was cancelled") == false) { //wut
- Logger.log("Exception while downloading file: ${e.message}")
- snackString("Exception while downloading file: ${e.message}")
- e.printStackTrace()
- Injekt.get().logException(e)
- }
- broadcastDownloadFailed(task.episode)
}
}
- private fun saveMediaInfo(task: AnimeDownloadTask) {
+ private fun saveMediaInfo(task: AnimeDownloadTask, directory: DocumentFile) {
CoroutineScope(Dispatchers.IO).launch {
- val directory =
- getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
- ?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
diff --git a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt
index cdf7e993..4f3876fe 100644
--- a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt
+++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt
@@ -30,6 +30,7 @@ import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
+import ani.dantotsu.download.DownloadCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat
import ani.dantotsu.download.DownloadedType
@@ -48,6 +49,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
+import ani.dantotsu.util.customAlertDialog
import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
@@ -202,25 +204,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val type: MediaType = MediaType.ANIME
// Alert dialog to confirm deletion
- val builder =
- androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
- builder.setTitle("Delete ${item.title}?")
- builder.setMessage("Are you sure you want to delete ${item.title}?")
- builder.setPositiveButton("Yes") { _, _ ->
- downloadManager.removeMedia(item.title, type)
- val mediaIds =
- PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values
- ?: emptySet()
- if (mediaIds.isEmpty()) {
- snackString("No media found") // if this happens, terrible things have happened
+ requireContext().customAlertDialog().apply {
+ setTitle("Delete ${item.title}?")
+ setMessage("Are you sure you want to delete ${item.title}?")
+ setPosButton(R.string.yes) {
+ downloadManager.removeMedia(item.title, type)
+ val mediaIds = PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values ?: emptySet()
+ if (mediaIds.isEmpty()) {
+ snackString("No media found") // if this happens, terrible things have happened
+ }
+ getDownloads()
}
- getDownloads()
+ setNegButton(R.string.no) {
+ // Do nothing
+ }
+ show()
}
- builder.setNegativeButton("No") { _, _ ->
- // Do nothing
- }
- val dialog = builder.show()
- dialog.window?.setDimAmount(0.8f)
true
}
}
@@ -319,17 +318,20 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator {
- SChapterImpl() // Provide an instance of SChapterImpl
+ SChapterImpl()
})
.registerTypeAdapter(SAnime::class.java, InstanceCreator {
- SAnimeImpl() // Provide an instance of SAnimeImpl
+ SAnimeImpl()
})
.registerTypeAdapter(SEpisode::class.java, InstanceCreator {
- SEpisodeImpl() // Provide an instance of SEpisodeImpl
+ SEpisodeImpl()
})
.create()
val media = directory?.findFile("media.json")
- ?: return loadMediaCompat(downloadedType)
+ if (media == null) {
+ Logger.log("No media.json found at ${directory?.uri?.path}")
+ return loadMediaCompat(downloadedType)
+ }
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
@@ -394,6 +396,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
bannerUri
)
} catch (e: Exception) {
+ Logger.log(e)
return try {
loadOfflineAnimeModelCompat(downloadedType)
} catch (e: Exception) {
@@ -401,7 +404,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
Logger.log(e)
Injekt.get().logException(e)
OfflineAnimeModel(
- "unknown",
+ downloadedType.titleName,
"0",
"??",
"??",
diff --git a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
index 08ddb3a7..ed3cb02c 100644
--- a/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/manga/MangaDownloaderService.kt
@@ -32,6 +32,7 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STAR
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
+import ani.dantotsu.util.NumberConverter.Companion.ofLength
import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
@@ -134,15 +135,15 @@ class MangaDownloaderService : Service() {
mutex.withLock {
downloadJobs[task.chapter] = job
}
- job.join() // Wait for the job to complete before continuing to the next task
+ job.join()
mutex.withLock {
downloadJobs.remove(task.chapter)
}
- updateNotification() // Update the notification after each task is completed
+ updateNotification()
}
if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) {
- stopSelf() // Stop the service when the queue is empty
+ stopSelf()
}
}
}
@@ -181,7 +182,7 @@ class MangaDownloaderService : Service() {
suspend fun download(task: DownloadTask) {
try {
- withContext(Dispatchers.Main) {
+ withContext(Dispatchers.IO) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@MangaDownloaderService,
@@ -194,18 +195,27 @@ class MangaDownloaderService : Service() {
val deferredMap = mutableMapOf>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) {
- notificationManager.notify(NOTIFICATION_ID, builder.build())
+ withContext(Dispatchers.Main) {
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ }
}
- getSubDirectory(
+ val baseOutputDir = getSubDirectory(
+ this@MangaDownloaderService,
+ MediaType.MANGA,
+ false,
+ task.title
+ ) ?: throw Exception("Base output directory not found")
+ val outputDir = getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title,
task.chapter
- )?.deleteRecursively(this@MangaDownloaderService)
+ ) ?: throw Exception("Output directory not found")
+
+ outputDir.deleteRecursively(this@MangaDownloaderService, true)
- // Loop through each ImageData object from the task
var farthest = 0
for ((index, image) in task.imageData.withIndex()) {
if (deferredMap.size >= task.simultaneousDownloads) {
@@ -226,30 +236,36 @@ class MangaDownloaderService : Service() {
}
if (bitmap != null) {
- saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
+ saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap)
}
farthest++
+
builder.setProgress(task.imageData.size, farthest, false)
+
broadcastDownloadProgress(
task.chapter,
farthest * 100 / task.imageData.size
)
if (notifi) {
- notificationManager.notify(NOTIFICATION_ID, builder.build())
+ withContext(Dispatchers.Main) {
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ }
}
-
bitmap
}
}
- // Wait for any remaining deferred to complete
deferredMap.values.awaitAll()
- builder.setContentText("${task.title} - ${task.chapter} Download complete")
- .setProgress(0, 0, false)
- notificationManager.notify(NOTIFICATION_ID, builder.build())
+ withContext(Dispatchers.Main) {
+ builder.setContentText("${task.title} - ${task.chapter} Download complete")
+ .setProgress(0, 0, false)
+ if (notifi) {
+ notificationManager.notify(NOTIFICATION_ID, builder.build())
+ }
+ }
- saveMediaInfo(task)
+ saveMediaInfo(task, baseOutputDir)
downloadsManager.addDownload(
DownloadedType(
task.title,
@@ -269,17 +285,16 @@ class MangaDownloaderService : Service() {
}
- private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
+ private fun saveToDisk(
+ fileName: String,
+ directory: DocumentFile,
+ bitmap: Bitmap
+ ) {
try {
- // Define the directory within the private external storage space
- val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
- ?: throw Exception("Directory not found")
directory.findFile(fileName)?.forceDelete(this)
- // Create a file reference within that directory for the image
val file =
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
- // Use a FileOutputStream to write the bitmap to the file
file.openOutputStream(this, false).use { outputStream ->
if (outputStream == null) throw Exception("Output stream is null")
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
@@ -292,11 +307,8 @@ class MangaDownloaderService : Service() {
}
@OptIn(DelicateCoroutinesApi::class)
- private fun saveMediaInfo(task: DownloadTask) {
+ private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
launchIO {
- val directory =
- getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
- ?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt
index 36e90e60..dcb462e8 100644
--- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt
+++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt
@@ -46,6 +46,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
+import ani.dantotsu.util.customAlertDialog
import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
@@ -171,7 +172,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val item = adapter.getItem(position) as OfflineMangaModel
val media =
downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
- ?: downloadManager.novelDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
+ ?: downloadManager.novelDownloadedTypes.firstOrNull {
+ it.titleName.compareName(
+ item.title
+ )
+ }
media?.let {
lifecycleScope.launch {
ContextCompat.startActivity(
@@ -197,19 +202,15 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
MediaType.NOVEL
}
// Alert dialog to confirm deletion
- val builder =
- androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
- builder.setTitle("Delete ${item.title}?")
- builder.setMessage("Are you sure you want to delete ${item.title}?")
- builder.setPositiveButton("Yes") { _, _ ->
- downloadManager.removeMedia(item.title, type)
- getDownloads()
- }
- builder.setNegativeButton("No") { _, _ ->
- // Do nothing
- }
- val dialog = builder.show()
- dialog.window?.setDimAmount(0.8f)
+ requireContext().customAlertDialog().apply {
+ setTitle("Delete ${item.title}?")
+ setMessage("Are you sure you want to delete ${item.title}?")
+ setPosButton(R.string.yes) {
+ downloadManager.removeMedia(item.title, type)
+ getDownloads()
+ }
+ setNegButton(R.string.no)
+ }.show()
true
}
}
@@ -279,10 +280,12 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
downloads = listOf()
downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
- val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
+ val mangaTitles =
+ downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
val newMangaDownloads = mutableListOf()
for (title in mangaTitles) {
- val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName.findValidName() == title }
+ val tDownloads =
+ downloadManager.mangaDownloadedTypes.filter { it.titleName.findValidName() == title }
val download = tDownloads.firstOrNull() ?: continue
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
@@ -291,7 +294,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct()
val newNovelDownloads = mutableListOf()
for (title in novelTitles) {
- val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName.findValidName() == title }
+ val tDownloads =
+ downloadManager.novelDownloadedTypes.filter { it.titleName.findValidName() == title }
val download = tDownloads.firstOrNull() ?: continue
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
@@ -320,11 +324,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator {
- SChapterImpl() // Provide an instance of SChapterImpl
+ SChapterImpl()
})
.create()
val media = directory?.findFile("media.json")
- ?: return DownloadCompat.loadMediaCompat(downloadedType)
+ if (media == null) {
+ Logger.log("No media.json found at ${directory?.uri?.path}")
+ return DownloadCompat.loadMediaCompat(downloadedType)
+ }
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
@@ -340,7 +347,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = downloadedType.type.asText()
- //load media.json and convert to media class with gson
try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
@@ -378,6 +384,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
bannerUri
)
} catch (e: Exception) {
+ Logger.log(e)
return try {
loadOfflineMangaModelCompat(downloadedType)
} catch (e: Exception) {
@@ -385,7 +392,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
Logger.log(e)
Injekt.get().logException(e)
return OfflineMangaModel(
- "unknown",
+ downloadedType.titleName,
"0",
"??",
"??",
diff --git a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt
index bc1d8e31..38f7e231 100644
--- a/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt
+++ b/app/src/main/java/ani/dantotsu/download/novel/NovelDownloaderService.kt
@@ -239,6 +239,13 @@ class NovelDownloaderService : Service() {
return@withContext
}
+ val baseDirectory = getSubDirectory(
+ this@NovelDownloaderService,
+ MediaType.NOVEL,
+ false,
+ task.title
+ ) ?: throw Exception("Directory not found")
+
// Start the download
withContext(Dispatchers.IO) {
try {
@@ -334,7 +341,7 @@ class NovelDownloaderService : Service() {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
- saveMediaInfo(task)
+ saveMediaInfo(task, baseDirectory)
downloadsManager.addDownload(
DownloadedType(
task.title,
@@ -354,15 +361,8 @@ class NovelDownloaderService : Service() {
}
@OptIn(DelicateCoroutinesApi::class)
- private fun saveMediaInfo(task: DownloadTask) {
+ private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
launchIO {
- val directory =
- getSubDirectory(
- this@NovelDownloaderService,
- MediaType.NOVEL,
- false,
- task.title
- ) ?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
diff --git a/app/src/main/java/ani/dantotsu/download/video/Helper.kt b/app/src/main/java/ani/dantotsu/download/video/Helper.kt
index b207d634..ab8dd73d 100644
--- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt
+++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt
@@ -3,7 +3,6 @@ package ani.dantotsu.download.video
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
-import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@@ -29,10 +28,10 @@ import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
-import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.Logger
+import ani.dantotsu.util.customAlertDialog
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -72,19 +71,19 @@ object Helper {
episodeImage
)
- val downloadsManger = Injekt.get()
- val downloadCheck = downloadsManger
+ val downloadsManager = Injekt.get()
+ val downloadCheck = downloadsManager
.queryDownload(title, episode, MediaType.ANIME)
if (downloadCheck) {
- AlertDialog.Builder(context, R.style.MyPopup)
- .setTitle("Download Exists")
- .setMessage("A download for this episode already exists. Do you want to overwrite it?")
- .setPositiveButton("Yes") { _, _ ->
+ context.customAlertDialog().apply {
+ setTitle("Download Exists")
+ setMessage("A download for this episode already exists. Do you want to overwrite it?")
+ setPosButton(R.string.yes) {
PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName())
.apply()
- downloadsManger.removeDownload(
+ downloadsManager.removeDownload(
DownloadedType(
title,
episode,
@@ -99,8 +98,9 @@ object Helper {
}
}
}
- .setNegativeButton("No") { _, _ -> }
- .show()
+ setNegButton(R.string.no)
+ show()
+ }
} else {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
diff --git a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt
index f9a9f596..87713885 100644
--- a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt
+++ b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt
@@ -38,6 +38,7 @@ import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -289,15 +290,20 @@ class AnimeFragment : Fragment() {
}
}
}
- model.loaded = true
- model.loadTrending(1)
- model.loadAll()
+ }
+ model.loaded = true
+ val loadTrending = async(Dispatchers.IO) { model.loadTrending(1) }
+ val loadAll = async(Dispatchers.IO) { model.loadAll() }
+ val loadPopular = async(Dispatchers.IO) {
model.loadPopular(
- "ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
- PrefName.PopularAnimeList
- )
+ "ANIME",
+ sort = Anilist.sortBy[1],
+ onList = PrefManager.getVal(PrefName.PopularAnimeList)
)
}
+ loadTrending.await()
+ loadAll.await()
+ loadPopular.await()
live.postValue(false)
_binding?.animeRefresh?.isRefreshing = false
running = false
diff --git a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt
index df0e0939..0f31fca2 100644
--- a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt
@@ -111,8 +111,8 @@ class AnimePageAdapter : RecyclerView.Adapter 0) View.VISIBLE else View.GONE
+ trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
+ && PrefManager.getVal(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
listOf(
@@ -268,8 +268,9 @@ class AnimePageAdapter : RecyclerView.Adapter 0) View.VISIBLE else View.GONE
+ trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
+ && PrefManager.getVal(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
}
}
diff --git a/app/src/main/java/ani/dantotsu/home/HomeFragment.kt b/app/src/main/java/ani/dantotsu/home/HomeFragment.kt
index fc729b9d..dfd39e6a 100644
--- a/app/src/main/java/ani/dantotsu/home/HomeFragment.kt
+++ b/app/src/main/java/ani/dantotsu/home/HomeFragment.kt
@@ -50,6 +50,7 @@ import ani.dantotsu.statusBarHeight
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.max
@@ -92,6 +93,7 @@ class HomeFragment : Fragment() {
)
binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
+ && PrefManager.getVal(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener {
@@ -456,51 +458,56 @@ class HomeFragment : Fragment() {
var running = false
val live = Refresh.activity.getOrPut(1) { MutableLiveData(true) }
- live.observe(viewLifecycleOwner)
- {
- if (!running && it) {
+ live.observe(viewLifecycleOwner) { shouldRefresh ->
+ if (!running && shouldRefresh) {
running = true
scope.launch {
withContext(Dispatchers.IO) {
- //Get userData First
- Anilist.userid =
- PrefManager.getNullableVal(PrefName.AnilistUserId, null)
- ?.toIntOrNull()
+ // Get user data first
+ Anilist.userid = PrefManager.getNullableVal(PrefName.AnilistUserId, null)?.toIntOrNull()
if (Anilist.userid == null) {
- getUserId(requireContext()) {
- load()
- }
- } else {
- CoroutineScope(Dispatchers.IO).launch {
+ withContext(Dispatchers.Main) {
getUserId(requireContext()) {
load()
}
}
+ } else {
+ getUserId(requireContext()) {
+ load()
+ }
}
model.loaded = true
- CoroutineScope(Dispatchers.IO).launch {
- model.setListImages()
- }
- var empty = true
- val homeLayoutShow: List =
- PrefManager.getVal(PrefName.HomeLayout)
- model.initHomePage()
- (array.indices).forEach { i ->
+ model.setListImages()
+ }
+
+ var empty = true
+ val homeLayoutShow: List = PrefManager.getVal(PrefName.HomeLayout)
+
+ withContext(Dispatchers.Main) {
+ homeLayoutShow.indices.forEach { i ->
if (homeLayoutShow.elementAt(i)) {
empty = false
- } else withContext(Dispatchers.Main) {
+ } else {
containers[i].visibility = View.GONE
}
}
- model.empty.postValue(empty)
}
+
+ val initHomePage = async(Dispatchers.IO) { model.initHomePage() }
+ val initUserStatus = async(Dispatchers.IO) { model.initUserStatus() }
+ initHomePage.await()
+ initUserStatus.await()
+
+ withContext(Dispatchers.Main) {
+ model.empty.postValue(empty)
+ binding.homeHiddenItemsContainer.visibility = View.GONE
+ }
+
live.postValue(false)
_binding?.homeRefresh?.isRefreshing = false
running = false
}
- binding.homeHiddenItemsContainer.visibility = View.GONE
}
-
}
}
@@ -508,6 +515,7 @@ class HomeFragment : Fragment() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) {
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
+ && PrefManager.getVal(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
super.onResume()
diff --git a/app/src/main/java/ani/dantotsu/home/LoginFragment.kt b/app/src/main/java/ani/dantotsu/home/LoginFragment.kt
index 5f89464e..13fde1e5 100644
--- a/app/src/main/java/ani/dantotsu/home/LoginFragment.kt
+++ b/app/src/main/java/ani/dantotsu/home/LoginFragment.kt
@@ -12,12 +12,14 @@ import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
+import ani.dantotsu.databinding.DialogUserAgentBinding
import ani.dantotsu.databinding.FragmentLoginBinding
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 ani.dantotsu.util.customAlertDialog
import com.google.android.material.textfield.TextInputEditText
class LoginFragment : Fragment() {
@@ -94,38 +96,31 @@ class LoginFragment : Fragment() {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
- val dialogView =
- LayoutInflater.from(requireActivity()).inflate(R.layout.dialog_user_agent, null)
- dialogView.findViewById(R.id.userAgentTextBox)?.hint = "Password"
- val subtitleTextView = dialogView.findViewById(R.id.subtitle)
- subtitleTextView?.visibility = View.VISIBLE
- subtitleTextView?.text = "Enter your password to decrypt the file"
+ val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
+ userAgentTextBox.hint = "Password"
+ subtitle.visibility = View.VISIBLE
+ subtitle.text = getString(R.string.enter_password_to_decrypt_file)
+ }
- val dialog = AlertDialog.Builder(requireActivity(), R.style.MyPopup)
- .setTitle("Enter Password")
- .setView(dialogView)
- .setPositiveButton("OK", null)
- .setNegativeButton("Cancel") { dialog, _ ->
+ requireActivity().customAlertDialog().apply {
+ setTitle("Enter Password")
+ setCustomView(dialogView.root)
+ setPosButton(R.string.ok){
+ val editText = dialogView.userAgentTextBox
+ if (editText.text?.isNotBlank() == true) {
+ editText.text?.toString()?.trim()?.toCharArray(password)
+ callback(password)
+ } else {
+ toast("Password cannot be empty")
+ }
+ }
+ setNegButton(R.string.cancel) {
password.fill('0')
- dialog.dismiss()
callback(null)
}
- .create()
+ }.show()
- dialog.window?.setDimAmount(0.8f)
- dialog.show()
- // Override the positive button here
- dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
- val editText = dialog.findViewById(R.id.userAgentTextBox)
- if (editText?.text?.isNotBlank() == true) {
- editText.text?.toString()?.trim()?.toCharArray(password)
- dialog.dismiss()
- callback(password)
- } else {
- toast("Password cannot be empty")
- }
- }
}
private fun restartApp() {
diff --git a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt
index c878c584..0ed19510 100644
--- a/app/src/main/java/ani/dantotsu/home/MangaFragment.kt
+++ b/app/src/main/java/ani/dantotsu/home/MangaFragment.kt
@@ -35,6 +35,7 @@ import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -274,15 +275,22 @@ class MangaFragment : Fragment() {
}
}
}
- model.loaded = true
- model.loadTrending()
- model.loadAll()
+ }
+ model.loaded = true
+ val loadTrending = async(Dispatchers.IO) { model.loadTrending() }
+ val loadAll = async(Dispatchers.IO) { model.loadAll() }
+ val loadPopular = async(Dispatchers.IO) {
model.loadPopular(
- "MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
- PrefName.PopularMangaList
- )
+ "MANGA",
+ sort = Anilist.sortBy[1],
+ onList = PrefManager.getVal(PrefName.PopularAnimeList)
)
}
+
+ loadTrending.await()
+ loadAll.await()
+ loadPopular.await()
+
live.postValue(false)
_binding?.mangaRefresh?.isRefreshing = false
running = false
diff --git a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt
index 2e3e6a8c..2577e3b2 100644
--- a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt
@@ -80,6 +80,7 @@ class MangaPageAdapter : RecyclerView.Adapter 0
+ && PrefManager.getVal(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
trendingBinding.searchBar.hint = "MANGA"
trendingBinding.searchBarText.setOnClickListener {
@@ -296,8 +297,8 @@ class MangaPageAdapter : RecyclerView.Adapter 0) View.VISIBLE else View.GONE
+ trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
+ && PrefManager.getVal(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
}
}
diff --git a/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt b/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt
index 43b2cb55..ced8b2d8 100644
--- a/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt
+++ b/app/src/main/java/ani/dantotsu/home/status/StatusActivity.kt
@@ -16,6 +16,8 @@ import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.statusBarHeight
+import ani.dantotsu.toast
+import ani.dantotsu.util.Logger
class StatusActivity : AppCompatActivity(), StoriesCallback {
private lateinit var activity: ArrayList
@@ -44,10 +46,17 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
val key = "activities"
val watchedActivity = PrefManager.getCustomVal>(key, setOf())
- val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
- val startIndex = if ( startFrom > 0) startFrom else 0
- binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
-
+ if (activity.getOrNull(position) != null) {
+ val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
+ val startIndex = if ( startFrom > 0) startFrom else 0
+ binding.stories.setStoriesList(
+ activityList = activity[position].activity,
+ startIndex = startIndex + 1
+ )
+ } else {
+ Logger.log("index out of bounds for position $position of size ${activity.size}")
+ finish()
+ }
}
private fun findFirstNonMatch(watchedActivity: Set, activity: List): Int {
@@ -58,13 +67,16 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
}
return -1
}
+
override fun onPause() {
super.onPause()
binding.stories.pause()
}
+
override fun onResume() {
super.onResume()
- binding.stories.resume()
+ if (hasWindowFocus())
+ binding.stories.resume()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
@@ -83,7 +95,7 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
val startIndex= if ( startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutLeft)
- binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
+ binding.stories.setStoriesList(activity[position].activity, startIndex + 1)
binding.stories.startAnimation(slideInRight)
} else {
finish()
@@ -92,13 +104,13 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
override fun onStoriesStart() {
position -= 1
- if (position >= 0) {
+ if (position >= 0 && activity[position].activity.isNotEmpty()) {
val key = "activities"
val watchedActivity = PrefManager.getCustomVal>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
val startIndex = if ( startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutRight)
- binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
+ binding.stories.setStoriesList(activity[position].activity,startIndex + 1)
binding.stories.startAnimation(slideInLeft)
} else {
finish()
diff --git a/app/src/main/java/ani/dantotsu/home/status/Stories.kt b/app/src/main/java/ani/dantotsu/home/status/Stories.kt
index 7f2c60ec..09158aae 100644
--- a/app/src/main/java/ani/dantotsu/home/status/Stories.kt
+++ b/app/src/main/java/ani/dantotsu/home/status/Stories.kt
@@ -30,6 +30,7 @@ import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.profile.UsersDialogFragment
import ani.dantotsu.profile.activity.ActivityItemBuilder
+import ani.dantotsu.profile.activity.RepliesBottomDialog
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
@@ -48,7 +49,6 @@ import kotlin.math.abs
class Stories @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener {
- private lateinit var activity: FragmentActivity
private lateinit var binding: FragmentStatusBinding
private lateinit var activityList: List
private lateinit var storiesListener: StoriesCallback
@@ -74,16 +74,14 @@ class Stories @JvmOverloads constructor(
if (context is StoriesCallback) storiesListener = context as StoriesCallback
- binding.leftTouchPanel.setOnTouchListener(this)
- binding.rightTouchPanel.setOnTouchListener(this)
+ binding.touchPanel.setOnTouchListener(this)
}
fun setStoriesList(
- activityList: List, activity: FragmentActivity, startIndex: Int = 1
+ activityList: List, startIndex: Int = 1
) {
this.activityList = activityList
- this.activity = activity
this.storyIndex = startIndex
addLoadingViews(activityList)
}
@@ -264,49 +262,7 @@ class Stories @JvmOverloads constructor(
}
- private var startClickTime = 0L
- private var startX = 0f
- private var startY = 0f
- private var isLongPress = false
- private val swipeThreshold = 100
- override fun onTouch(view: View?, event: MotionEvent?): Boolean {
- val maxClickDuration = 200
- when (event?.action) {
- MotionEvent.ACTION_DOWN -> {
- startX = event.x
- startY = event.y
- startClickTime = Calendar.getInstance().timeInMillis
- pause()
- isLongPress = false
- }
- MotionEvent.ACTION_MOVE -> {
- val deltaX = event.x - startX
- val deltaY = event.y - startY
- if (!isLongPress && (abs(deltaX) > swipeThreshold || abs(deltaY) > swipeThreshold)) {
- isLongPress = true
- }
- }
-
- MotionEvent.ACTION_UP -> {
- val clickDuration = Calendar.getInstance().timeInMillis - startClickTime
- if (clickDuration < maxClickDuration && !isLongPress) {
- when (view?.id) {
- R.id.leftTouchPanel -> leftPanelTouch()
- R.id.rightTouchPanel -> rightPanelTouch()
- }
- } else {
- resume()
- }
- val deltaX = event.x - startX
- if (abs(deltaX) > swipeThreshold) {
- if (deltaX > 0) onStoriesPrevious()
- else onStoriesCompleted()
- }
- }
- }
- return true
- }
private fun rightPanelTouch() {
Logger.log("rightPanelTouch: $storyIndex")
@@ -359,6 +315,7 @@ class Stories @JvmOverloads constructor(
timer.resume()
}
+ @SuppressLint("ClickableViewAccessibility")
private fun loadStory(story: Activity) {
val key = "activities"
val set = PrefManager.getCustomVal>(key, setOf()).plus((story.id))
@@ -374,6 +331,15 @@ class Stories @JvmOverloads constructor(
null
)
}
+
+ binding.textActivity.setOnTouchListener { v, event ->
+ onTouchView(v, event, true)
+ v.onTouchEvent(event)
+ }
+ binding.textActivityContainer.setOnTouchListener { v, event ->
+ onTouchView(v, event, true)
+ v.onTouchEvent(event)
+ }
fun visible(isList: Boolean) {
binding.textActivity.isVisible = !isList
binding.textActivityContainer.isVisible = !isList
@@ -397,15 +363,17 @@ class Stories @JvmOverloads constructor(
}
}
} ${story.progress ?: story.media?.title?.userPreferred} " +
- if (
- story.status?.contains("completed") == false &&
- !story.status.contains("plans") &&
- !story.status.contains("repeating")
- ) {
- "of ${story.media?.title?.userPreferred}"
- } else {
- ""
- }
+ if (
+ story.status?.contains("completed") == false &&
+ !story.status.contains("plans") &&
+ !story.status.contains("repeating")&&
+ !story.status.contains("paused")&&
+ !story.status.contains("dropped")
+ ) {
+ "of ${story.media?.title?.userPreferred}"
+ } else {
+ ""
+ }
binding.infoText.text = text
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage(
@@ -421,7 +389,7 @@ class Stories @JvmOverloads constructor(
story.media?.id
),
ActivityOptionsCompat.makeSceneTransitionAnimation(
- activity,
+ (it.context as FragmentActivity),
binding.coverImage,
ViewCompat.getTransitionName(binding.coverImage)!!
).toBundle()
@@ -455,22 +423,21 @@ class Stories @JvmOverloads constructor(
}
val likeColor = ContextCompat.getColor(context, R.color.yt_red)
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
+ binding.replyCount.text = story.replyCount.toString()
+ binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp))
binding.activityRepliesContainer.setOnClickListener {
RepliesBottomDialog.newInstance(story.id)
- .show(activity.supportFragmentManager, "replies")
+ .show((it.context as FragmentActivity).supportFragmentManager, "replies")
}
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
- binding.replyCount.text = story.replyCount.toString()
binding.activityLikeCount.text = story.likeCount.toString()
- binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp))
binding.activityLikeContainer.setOnClickListener {
like()
}
binding.activityLikeContainer.setOnLongClickListener {
- val context = activity
UsersDialogFragment().apply {
userList(userList)
- show(context.supportFragmentManager, "dialog")
+ show((it.context as FragmentActivity).supportFragmentManager, "dialog")
}
true
}
@@ -484,7 +451,7 @@ class Stories @JvmOverloads constructor(
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch {
- val res = Anilist.query.toggleLike(story.id, "ACTIVITY")
+ val res = Anilist.mutation.toggleLike(story.id, "ACTIVITY")
withContext(Dispatchers.Main) {
if (res != null) {
if (story.isLiked == true) {
@@ -502,4 +469,69 @@ class Stories @JvmOverloads constructor(
}
}
}
+ private var startClickTime = 0L
+ private var startX = 0f
+ private var startY = 0f
+ private var isLongPress = false
+ private val swipeThreshold = 100
+ override fun onTouch(view: View, event: MotionEvent): Boolean {
+ onTouchView(view, event)
+ return true
+ }
+ private fun onTouchView(view: View, event: MotionEvent, isText: Boolean = false){
+ val maxClickDuration = 200
+ val screenWidth = view.width
+ val leftHalf = screenWidth / 2
+ val leftQuarter = screenWidth * 0.15
+ val rightQuarter = screenWidth * 0.85
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ startX = event.x
+ startY = event.y
+ startClickTime = Calendar.getInstance().timeInMillis
+ pause()
+ isLongPress = false
+ }
+ MotionEvent.ACTION_MOVE -> {
+ val deltaX = event.x - startX
+ val deltaY = event.y - startY
+ if (!isLongPress && (abs(deltaX) > swipeThreshold || abs(deltaY) > swipeThreshold)) {
+ isLongPress = true
+ }
+ }
+ MotionEvent.ACTION_UP -> {
+ val clickDuration = Calendar.getInstance().timeInMillis - startClickTime
+ if (isText) {
+ if (clickDuration < maxClickDuration && !isLongPress) {
+ if (event.x < leftQuarter) {
+ leftPanelTouch()
+ } else if (event.x > rightQuarter) {
+ rightPanelTouch()
+ } else {
+ resume()
+ }
+ } else {
+ resume()
+ }
+ } else {
+ if (clickDuration < maxClickDuration && !isLongPress) {
+ if (event.x < leftHalf) {
+ leftPanelTouch()
+ } else {
+ rightPanelTouch()
+ }
+ } else {
+ resume()
+ }
+ }
+ val deltaX = event.x - startX
+ val deltaY = event.y - startY
+ if (abs(deltaX) > swipeThreshold && !(abs(deltaY) > 10)) {
+ if (deltaX > 0) onStoriesPrevious()
+ else onStoriesCompleted()
+ }
+
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/home/status/UserStatusAdapter.kt b/app/src/main/java/ani/dantotsu/home/status/UserStatusAdapter.kt
index 5daebcd9..c8db4c99 100644
--- a/app/src/main/java/ani/dantotsu/home/status/UserStatusAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/home/status/UserStatusAdapter.kt
@@ -1,6 +1,5 @@
package ani.dantotsu.home.status
-import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
@@ -15,6 +14,8 @@ import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager
+import ani.dantotsu.snackString
+import ani.dantotsu.util.ActivityMarkdownCreator
class UserStatusAdapter(private val user: ArrayList) :
RecyclerView.Adapter() {
@@ -23,6 +24,10 @@ class UserStatusAdapter(private val user: ArrayList) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
+ if (user[bindingAdapterPosition].activity.isEmpty()) {
+ snackString("No activity")
+ return@setOnClickListener
+ }
StatusActivity.user = user
ContextCompat.startActivity(
itemView.context,
@@ -34,14 +39,23 @@ class UserStatusAdapter(private val user: ArrayList) :
)
}
itemView.setOnLongClickListener {
- ContextCompat.startActivity(
- itemView.context,
- Intent(
+ if (user[bindingAdapterPosition].id == Anilist.userid) {
+ ContextCompat.startActivity(
itemView.context,
- ProfileActivity::class.java
- ).putExtra("userId", user[bindingAdapterPosition].id),
- null
- )
+ Intent(itemView.context, ActivityMarkdownCreator::class.java)
+ .putExtra("type", "activity"),
+ null
+ )
+ }else{
+ ContextCompat.startActivity(
+ itemView.context,
+ Intent(
+ itemView.context,
+ ProfileActivity::class.java
+ ).putExtra("userId", user[bindingAdapterPosition].id),
+ null
+ )
+ }
true
}
}
diff --git a/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt b/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt
index 66cec7e4..230f3a4e 100644
--- a/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/CalendarActivity.kt
@@ -30,6 +30,7 @@ class CalendarActivity : AppCompatActivity() {
private lateinit var binding: ActivityListBinding
private val scope = lifecycleScope
private var selectedTabIdx = 1
+ private var showOnlyLibrary = false
private val model: OtherDetailsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -38,8 +39,6 @@ class CalendarActivity : AppCompatActivity() {
ThemeManager(this).applyTheme()
binding = ActivityListBinding.inflate(layoutInflater)
-
-
val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface)
val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline)
@@ -79,6 +78,17 @@ class CalendarActivity : AppCompatActivity() {
override fun onTabReselected(tab: TabLayout.Tab?) {}
})
+ binding.listed.setOnClickListener {
+ showOnlyLibrary = !showOnlyLibrary
+ binding.listed.setImageResource(
+ if (showOnlyLibrary) R.drawable.ic_round_collections_bookmark_24
+ else R.drawable.ic_round_library_books_24
+ )
+ scope.launch {
+ model.loadCalendar(showOnlyLibrary)
+ }
+ }
+
model.getCalendar().observe(this) {
if (it != null) {
binding.listProgressBar.visibility = View.GONE
@@ -97,11 +107,10 @@ class CalendarActivity : AppCompatActivity() {
live.observe(this) {
if (it) {
scope.launch {
- withContext(Dispatchers.IO) { model.loadCalendar() }
+ withContext(Dispatchers.IO) { model.loadCalendar(showOnlyLibrary) }
live.postValue(false)
}
}
}
-
}
}
diff --git a/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt b/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt
index d97e138d..2186791d 100644
--- a/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt
+++ b/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt
@@ -9,6 +9,7 @@ import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
+import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemCharacterBinding
import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation
@@ -55,6 +56,7 @@ class CharacterAdapter(
).toBundle()
)
}
+ itemView.setOnLongClickListener { copyToClipboard(characterList[bindingAdapterPosition].name ?: ""); true }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ani/dantotsu/media/Media.kt b/app/src/main/java/ani/dantotsu/media/Media.kt
index a935dd4a..a3453c99 100644
--- a/app/src/main/java/ani/dantotsu/media/Media.kt
+++ b/app/src/main/java/ani/dantotsu/media/Media.kt
@@ -1,14 +1,22 @@
package ani.dantotsu.media
import android.graphics.Bitmap
+import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.MediaEdge
import ani.dantotsu.connections.anilist.api.MediaList
+import ani.dantotsu.connections.anilist.api.MediaStreamingEpisode
import ani.dantotsu.connections.anilist.api.MediaType
import ani.dantotsu.connections.anilist.api.Query
+import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.media.anime.Anime
import ani.dantotsu.media.manga.Manga
import ani.dantotsu.profile.User
+import ani.dantotsu.settings.saving.PrefManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.io.Serializable
import ani.dantotsu.connections.anilist.api.Media as ApiMedia
@@ -76,7 +84,7 @@ data class Media(
var nameMAL: String? = null,
var shareLink: String? = null,
var selected: Selected? = null,
-
+ var streamingEpisodes: List? = null,
var idKitsu: String? = null,
var cameFromContinue: Boolean = false
@@ -129,6 +137,37 @@ data class Media(
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
}
+fun Media?.deleteFromList(
+ scope: CoroutineScope,
+ onSuccess: suspend () -> Unit,
+ onError: suspend (e: Exception) -> Unit,
+ onNotFound: suspend () -> Unit
+) {
+ val id = this?.userListId
+ scope.launch {
+ withContext(Dispatchers.IO) {
+ this@deleteFromList?.let { media ->
+ val _id = id ?: Anilist.query.userMediaDetails(media).userListId;
+ _id?.let { listId ->
+ try {
+ Anilist.mutation.deleteList(listId)
+ MAL.query.deleteList(media.anime != null, media.idMAL)
+
+ val removeList = PrefManager.getCustomVal("removeList", setOf())
+ PrefManager.setCustomVal(
+ "removeList", removeList.minus(listId)
+ )
+
+ onSuccess()
+ } catch (e: Exception) {
+ onError(e)
+ }
+ } ?: onNotFound()
+ }
+ }
+ }
+}
+
fun emptyMedia() = Media(
id = 0,
name = "No media found",
diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
index 44b04457..151327f9 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsActivity.kt
@@ -293,7 +293,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaTotal.visibility = View.VISIBLE
binding.mediaAddToList.text = userStatus
} else {
- binding.mediaAddToList.setText(R.string.add)
+ binding.mediaAddToList.setText(R.string.add_list)
}
total()
binding.mediaAddToList.setOnClickListener {
@@ -372,7 +372,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
navBar.addTab(infoTab)
navBar.addTab(watchTab)
- navBar.addTab(commentTab)
+ if (PrefManager.getVal(PrefName.CommentsEnabled) == 1) {
+ navBar.addTab(commentTab)
+ }
if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
selected = 1
@@ -424,7 +426,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
override fun onResume() {
- navBar.selectTabAt(selected)
+ if (::navBar.isInitialized)
+ navBar.selectTabAt(selected)
super.onResume()
}
diff --git a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt
index cfd9c736..4a7ed075 100644
--- a/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt
+++ b/app/src/main/java/ani/dantotsu/media/MediaDetailsViewModel.kt
@@ -13,6 +13,7 @@ import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.AniSkip
+import ani.dantotsu.others.Anify
import ani.dantotsu.others.Jikan
import ani.dantotsu.others.Kitsu
import ani.dantotsu.parsers.AnimeSources
@@ -99,6 +100,15 @@ class MediaDetailsViewModel : ViewModel() {
if (kitsuEpisodes.value == null) kitsuEpisodes.postValue(Kitsu.getKitsuEpisodesDetails(s))
}
}
+ private val anifyEpisodes: MutableLiveData