Merge pull request #552 from rebelonion/dev

Dev
This commit is contained in:
rebel onion 2024-12-30 23:58:26 -06:00 committed by GitHub
commit 88feb4d811
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
208 changed files with 8967 additions and 3972 deletions

View file

@ -1,17 +1,25 @@
name: Build APK and Notify Discord name: Build APK and Notify Discord
on: on:
push: push:
branches: branches-ignore:
- dev - main
- l10n_dev_crowdin
- custom-download-location
paths-ignore: paths-ignore:
- '**/README.md' - '**/README.md'
tags:
- "v*.*.*"
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
env: env:
CI: true CI: true
SKIP_BUILD: false
steps: steps:
- name: Checkout repo - name: Checkout repo
@ -19,14 +27,12 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Download last SHA artifact - name: Download last SHA artifact
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v6
with: with:
workflow: beta.yml workflow: beta.yml
name: last-sha name: last-sha
path: . path: .
continue-on-error: true continue-on-error: true
- name: Get Commits Since Last Run - name: Get Commits Since Last Run
@ -39,7 +45,9 @@ jobs:
fi fi
echo "Commits since $LAST_SHA:" echo "Commits since $LAST_SHA:"
# Accumulate commit logs in a shell variable # Accumulate commit logs in a shell variable
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %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 # URL-encode the newline characters for GitHub Actions
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}" COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}" COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
@ -49,6 +57,10 @@ jobs:
# Debugging: Print the variable to check its content # Debugging: Print the variable to check its content
echo "$COMMIT_LOGS" echo "$COMMIT_LOGS"
echo "$COMMIT_LOGS" > commit_log.txt 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} shell: /usr/bin/bash -e {0}
env: env:
CI: true CI: true
@ -65,53 +77,278 @@ jobs:
echo "Version $VERSION" echo "Version $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: List files in the directory
run: ls -l
- name: Setup JDK 17 - name: Setup JDK 17
if: ${{ env.SKIP_BUILD != 'true' }}
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: 17 java-version: 17
cache: gradle 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 - name: Upload a Build Artifact
if: ${{ env.SKIP_BUILD != 'true' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Dantotsu name: Dantotsu
retention-days: 5 retention-days: 5
compression-level: 9 compression-level: 9
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk" path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
- name: Upload APK to Discord and Telegram - name: Upload APK to Discord and Telegram
if: ${{ github.repository == 'rebelonion/Dantotsu' }} if: ${{ github.repository == 'rebelonion/Dantotsu' }}
shell: bash shell: bash
run: | 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](<https://anilist.co/user/5790266/>)"
additional_info["aayush262"]="\n Discord: <@918825160654598224>\n AniList: [aayush262](<https://anilist.co/user/5144645/>)"
additional_info["rebelonion"]="\n Discord: <@714249925248024617>\n AniList: [rebelonion](<https://anilist.co/user/6077251/>)\n PornHub: [rebelonion](<https://www.cornhub.com/model/rebelonion>)"
additional_info["Ankit Grai"]="\n Discord: <@1125628254330560623>\n AniList: [bheshnarayan](<https://anilist.co/user/6417303/>)"
# 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/') commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g; s/^/\n/')
# Truncate commit messages if they are too long if [ ${#developers} -gt $max_length ]; then
max_length=1900 # Adjust this value as needed developers="${developers:0:$max_length}... (truncated)"
fi
if [ ${#commit_messages} -gt $max_length ]; then if [ ${#commit_messages} -gt $max_length ]; then
commit_messages="${commit_messages:0:$max_length}... (truncated)" commit_messages="${commit_messages:0:$max_length}... (truncated)"
fi 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 href="\3">֍<\/a>/')
message=$(echo "$message" | sed -E 's/\[#([0-9]+)\]\((https:\/\/github\.com\/[^)]+)\)/<a href="\2">#\1<\/a>/g')
echo "$message"
done)
telegram_commit_messages="<blockquote>${telegram_commit_messages}</blockquote>"
# 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="<blockquote>${dev_info_tel}</blockquote>"
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: env:
COMMIT_LOG: ${{ env.COMMIT_LOG }} COMMIT_LOG: ${{ env.COMMIT_LOG }}
VERSION: ${{ env.VERSION }} VERSION: ${{ env.VERSION }}

8
.gitignore vendored
View file

@ -2,6 +2,9 @@
.gradle/ .gradle/
build/ build/
#kotlin
.kotlin/
# Local configuration file (sdk path, etc) # Local configuration file (sdk path, etc)
local.properties local.properties
@ -33,4 +36,7 @@ output.json
scripts/ scripts/
#crowdin #crowdin
crowdin.yml crowdin.yml
#vscode
.vscode

View file

@ -11,12 +11,12 @@ def gitCommitHash = providers.exec {
}.standardOutput.asText.get().trim() }.standardOutput.asText.get().trim()
android { android {
compileSdk 34 compileSdk 35
defaultConfig { defaultConfig {
applicationId "ani.dantotsu" applicationId "ani.dantotsu"
minSdk 21 minSdk 21
targetSdk 34 targetSdk 35
versionCode((System.currentTimeMillis() / 60000).toInteger()) versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "3.1.0" versionName "3.1.0"
versionCode 300100000 versionCode 300100000
@ -101,6 +101,8 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.11.0' implementation 'androidx.webkit:webkit:1.11.0'
implementation "com.anggrayudi:storage:1.5.5" implementation "com.anggrayudi:storage:1.5.5"
implementation "androidx.biometric:biometric:1.1.0"
// Glide // Glide
ext.glide_version = '4.16.0' ext.glide_version = '4.16.0'
@ -111,7 +113,7 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer // Exoplayer
ext.exo_version = '1.3.1' ext.exo_version = '1.5.0'
implementation "androidx.media3:media3-exoplayer:$exo_version" implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version" implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version" implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
@ -121,6 +123,8 @@ dependencies {
// Media3 Casting // Media3 Casting
implementation "androidx.media3:media3-cast:$exo_version" implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.7.0" implementation "androidx.mediarouter:mediarouter:1.7.0"
// Media3 extension
implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.3"
// UI // UI
implementation 'com.google.android.material:material:1.12.0' implementation 'com.google.android.material:material:1.12.0'
@ -131,7 +135,7 @@ dependencies {
implementation 'com.github.VipulOG:ebook-reader:0.1.6' implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'com.github.eltos:simpledialogfragments:v3.7' implementation 'com.github.eltos:simpledialogfragments:v3.7'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1' implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.3'
// Markwon // Markwon
ext.markwon_version = '4.6.2' ext.markwon_version = '4.6.2'
@ -157,7 +161,7 @@ dependencies {
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4' implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07' implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
implementation 'com.squareup.logcat:logcat:0.1' 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:logging-interceptor:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'

View file

@ -1,9 +1,40 @@
package ani.dantotsu.others package ani.dantotsu.others
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
object AppUpdater { object AppUpdater {
suspend fun check(activity: FragmentActivity, post: Boolean = false) { suspend fun check(activity: FragmentActivity, post: Boolean = false) {
//no-op // no-op
} }
}
@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<Asset>? = 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())
}
}
}

View file

@ -29,6 +29,7 @@ import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.time.delay
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
@ -69,7 +70,11 @@ object AppUpdater {
) )
addView( addView(
TextView(activity).apply { 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) markWon.setMarkdown(this, md)
} }
) )

View file

@ -19,6 +19,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" /> android:maxSdkVersion="29" />
@ -115,7 +116,8 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/epub+zip" /> <data android:host="*"/>
<data android:mimeType="application/epub+zip"/>
<data android:mimeType="application/x-mobipocket-ebook" /> <data android:mimeType="application/x-mobipocket-ebook" />
<data android:mimeType="application/vnd.amazon.ebook" /> <data android:mimeType="application/vnd.amazon.ebook" />
<data android:mimeType="application/fb2+zip" /> <data android:mimeType="application/fb2+zip" />
@ -131,10 +133,11 @@
</activity> </activity>
<activity android:name=".others.calc.CalcActivity" <activity android:name=".others.calc.CalcActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity android:name=".settings.FAQActivity" /> <activity android:name=".settings.AnilistSettingsActivity"/>
<activity android:name=".settings.ReaderSettingsActivity" />
<activity android:name=".settings.UserInterfaceSettingsActivity" /> <activity android:name=".settings.UserInterfaceSettingsActivity" />
<activity android:name=".settings.PlayerSettingsActivity" /> <activity android:name=".settings.PlayerSettingsActivity" />
<activity android:name=".settings.ReaderSettingsActivity" />
<activity android:name=".settings.FAQActivity" />
<activity <activity
android:name=".settings.SettingsActivity" android:name=".settings.SettingsActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
@ -155,7 +158,8 @@
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".settings.SettingsExtensionsActivity" android:name=".settings.SettingsExtensionsActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustPan"/>
<activity <activity
android:name=".settings.SettingsAddonActivity" android:name=".settings.SettingsAddonActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
@ -194,14 +198,15 @@
android:label="Inbox Activity" android:label="Inbox Activity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".profile.activity.NotificationActivity" android:name=".profile.notification.NotificationActivity"
android:label="Inbox Activity" android:label="Inbox Activity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".others.imagesearch.ImageSearchActivity" android:name=".others.imagesearch.ImageSearchActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".util.MarkdownCreatorActivity"/> android:name=".util.ActivityMarkdownCreator"
android:windowSoftInputMode="adjustResize|stateVisible" />
<activity android:name=".parsers.ParserTestActivity" /> <activity android:name=".parsers.ParserTestActivity" />
<activity <activity
android:name=".media.ReviewActivity" android:name=".media.ReviewActivity"
@ -370,25 +375,31 @@
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.Main" /> <action android:name="android.intent.action.Main" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" /> <data android:scheme="content" />
<data android:mimeType="*/*" /> <data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" /> <data android:pathPattern=".*\\.ani" />
<data android:pathPattern=".*\\.sani" /> <data android:pathPattern=".*\\.sani" />
<data android:host="*" /> <data android:host="*" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Support both schemes -->
<data android:scheme="tachiyomi"/>
<data android:host="add-repo"/>
<data android:scheme="aniyomi"/>
<data android:host="add-repo"/>
</intent-filter>
</activity> </activity>
<activity <activity
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity" android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"

View file

@ -105,6 +105,14 @@ class App : MultiDexApplication() {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
} }
if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 0) {
if (BuildConfig.FLAVOR.contains("fdroid")) {
PrefManager.setVal(PrefName.CommentsEnabled, 2)
} else {
PrefManager.setVal(PrefName.CommentsEnabled, 1)
}
}
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager = Injekt.get() animeExtensionManager = Injekt.get()
animeExtensionManager.findAvailableExtensions() animeExtensionManager.findAvailableExtensions()
@ -128,7 +136,9 @@ class App : MultiDexApplication() {
downloadAddonManager = Injekt.get() downloadAddonManager = Injekt.get()
torrentAddonManager.init() torrentAddonManager.init()
downloadAddonManager.init() downloadAddonManager.init()
CommentsAPI.fetchAuthToken(this@App) if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1) {
CommentsAPI.fetchAuthToken(this@App)
}
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager) val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
val scheduler = TaskScheduler.create(this@App, useAlarmManager) val scheduler = TaskScheduler.create(this@App, useAlarmManager)

View file

@ -68,7 +68,6 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@ -92,12 +91,12 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.BuildConfig.APPLICATION_ID import ani.dantotsu.BuildConfig.APPLICATION_ID
import ani.dantotsu.connections.anilist.Genre import ani.dantotsu.connections.anilist.Genre
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.bakaupdates.MangaUpdates
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
import ani.dantotsu.others.AlignTagHandler
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.SpoilerPlugin import ani.dantotsu.others.SpoilerPlugin
import ani.dantotsu.parsers.ShowResponse 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.PrefName
import ani.dantotsu.settings.saving.internal.PreferenceKeystore import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.util.CountUpTimer
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder 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.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions 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.Target
import com.bumptech.glide.request.target.ViewTarget
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -154,6 +152,7 @@ import java.io.FileOutputStream
import java.io.OutputStream import java.io.OutputStream
import java.lang.reflect.Field import java.lang.reflect.Field
import java.util.Calendar import java.util.Calendar
import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
@ -314,6 +313,7 @@ fun Activity.reloadActivity() {
Refresh.all() Refresh.all()
finish() finish()
startActivity(Intent(this, this::class.java)) startActivity(Intent(this, this::class.java))
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
initActivity(this) initActivity(this)
} }
@ -854,7 +854,7 @@ fun savePrefsToDownloads(
} }
) )
} }
@SuppressLint("StringFormatMatches")
fun savePrefs(serialized: String, path: String, title: String, context: Context): File? { fun savePrefs(serialized: String, path: String, title: String, context: Context): File? {
var file = File(path, "$title.ani") var file = File(path, "$title.ani")
var counter = 1 var counter = 1
@ -874,6 +874,7 @@ fun savePrefs(serialized: String, path: String, title: String, context: Context)
} }
} }
@SuppressLint("StringFormatMatches")
fun savePrefs( fun savePrefs(
serialized: String, serialized: String,
path: String, path: String,
@ -920,7 +921,7 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
intent.putExtra(Intent.EXTRA_STREAM, contentUri) intent.putExtra(Intent.EXTRA_STREAM, contentUri)
context.startActivity(Intent.createChooser(intent, "Share $title")) context.startActivity(Intent.createChooser(intent, "Share $title"))
} }
@SuppressLint("StringFormatMatches")
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? { fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
val imageFile = File(path, "$imageFileName.png") val imageFile = File(path, "$imageFileName.png")
return try { 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) { fun displayTimer(media: Media, view: ViewGroup) {
when { when {
media.anime != null -> countDown(media, view) media.anime != null -> countDown(media, view)
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view) else -> {}
else -> {} // No timer yet
} }
} }
@ -1447,6 +1411,8 @@ fun openOrCopyAnilistLink(link: String) {
} else { } else {
copyToClipboard(link, true) copyToClipboard(link, true)
} }
} else if (getYoutubeId(link).isNotEmpty()) {
openLinkInYouTube(link)
} else { } else {
copyToClipboard(link, true) copyToClipboard(link, true)
} }
@ -1483,6 +1449,7 @@ fun buildMarkwon(
TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a") TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a")
) )
} }
plugin.addHandler(AlignTagHandler())
}) })
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore { .usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
@ -1500,7 +1467,6 @@ fun buildMarkwon(
} }
return false return false
} }
override fun onLoadFailed( override fun onLoadFailed(
e: GlideException?, e: GlideException?,
model: Any?, model: Any?,
@ -1527,3 +1493,34 @@ fun buildMarkwon(
.build() .build()
return markwon 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
}

View file

@ -2,7 +2,6 @@ package ani.dantotsu
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
@ -51,7 +50,8 @@ import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.calc.CalcActivity import ani.dantotsu.others.calc.CalcActivity
import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity 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.ExtensionsActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool import ani.dantotsu.settings.saving.PrefManager.asLiveBool
@ -60,10 +60,11 @@ import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
import ani.dantotsu.settings.saving.internal.PreferenceKeystore import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.AudioHelper
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
@ -116,58 +117,8 @@ class MainActivity : AppCompatActivity() {
} }
} }
val action = intent.action if (Intent.ACTION_VIEW == intent.action) {
val type = intent.type handleViewIntent(intent)
if (Intent.ACTION_VIEW == action && type != null) {
val uri: Uri? = intent.data
try {
if (uri == null) {
throw Exception("Uri is null")
}
val jsonString =
contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name =
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try {
PreferenceKeystore.decryptWithPassword(
password,
encrypted,
salt
)
} catch (e: Exception) {
toast("Incorrect password")
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
toast("Error importing settings")
}
} }
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar) val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
@ -287,7 +238,7 @@ class MainActivity : AppCompatActivity() {
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0 .get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) { ) {
snackString(R.string.extension_updates_available) snackString(R.string.extension_updates_available)
?.setDuration(Snackbar.LENGTH_LONG) ?.setDuration(Snackbar.LENGTH_SHORT)
?.setAction(R.string.review) { ?.setAction(R.string.review) {
startActivity(Intent(this, ExtensionsActivity::class.java)) startActivity(Intent(this, ExtensionsActivity::class.java))
} }
@ -365,7 +316,6 @@ class MainActivity : AppCompatActivity() {
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) { } else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
Logger.log("MainActivity, onCreate: $activityId") Logger.log("MainActivity, onCreate: $activityId")
val notificationIntent = Intent(this, NotificationActivity::class.java).apply { val notificationIntent = Intent(this, NotificationActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId) putExtra("activityId", activityId)
} }
launched = true 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<TorrentAddonManager>() val torrentManager = Injekt.get<TorrentAddonManager>()
fun startTorrent() { fun startTorrent() {
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) { if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
@ -490,39 +443,101 @@ class MainActivity : AppCompatActivity() {
params.updateMargins(bottom = margin.toPx) 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<String> = 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) { private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') } val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout // Inflate the dialog layout
val dialogView = DialogUserAgentBinding.inflate(layoutInflater) val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
dialogView.userAgentTextBox.hint = "Password" userAgentTextBox.hint = "Password"
dialogView.subtitle.visibility = View.VISIBLE subtitle.visibility = View.VISIBLE
dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file) subtitle.text = getString(R.string.enter_password_to_decrypt_file)
}
val dialog = AlertDialog.Builder(this, R.style.MyPopup) customAlertDialog().apply {
.setTitle("Enter Password") setTitle("Enter Password")
.setView(dialogView.root) setCustomView(dialogView.root)
.setPositiveButton("OK", null) setPosButton(R.string.yes) {
.setNegativeButton("Cancel") { dialog, _ -> 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') password.fill('0')
dialog.dismiss()
callback(null) 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<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
dialog.dismiss()
callback(password)
} else {
toast("Password cannot be empty")
}
} }
} }

View file

@ -15,6 +15,8 @@ import ani.dantotsu.snackString
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import java.util.Calendar import java.util.Calendar
import java.util.Locale
import kotlin.math.abs
object Anilist { object Anilist {
val query: AnilistQueries = AnilistQueries() val query: AnilistQueries = AnilistQueries()
@ -22,7 +24,7 @@ object Anilist {
var token: String? = null var token: String? = null
var username: String? = null var username: String? = null
var adult: Boolean = false
var userid: Int? = null var userid: Int? = null
var avatar: String? = null var avatar: String? = null
var bg: String? = null var bg: String? = null
@ -36,6 +38,17 @@ object Anilist {
var rateLimitReset: Long = 0 var rateLimitReset: Long = 0
var initialized = false 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<String>? = null
var mangaCustomLists: List<String>? = null
val sortBy = listOf( val sortBy = listOf(
"SCORE_DESC", "SCORE_DESC",
@ -96,6 +109,86 @@ object Anilist {
"Original Creator", "Story & Art", "Story" "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 cal: Calendar = Calendar.getInstance()
private val currentYear = cal.get(Calendar.YEAR) private val currentYear = cal.get(Calendar.YEAR)
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) { private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
@ -106,6 +199,33 @@ object Anilist {
else -> 0 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<String, Int> { private fun getSeason(next: Boolean): Pair<String, Int> {
var newSeason = if (next) currentSeason + 1 else currentSeason - 1 var newSeason = if (next) currentSeason + 1 else currentSeason - 1
var newYear = currentYear var newYear = currentYear
@ -191,6 +311,7 @@ object Anilist {
) )
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1 val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
Logger.log("Remaining requests: $remaining") Logger.log("Remaining requests: $remaining")
println("Remaining requests: $remaining")
if (json.code == 429) { if (json.code == 429) {
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1 val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0 val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0

View file

@ -3,16 +3,99 @@ package ani.dantotsu.connections.anilist
import ani.dantotsu.connections.anilist.Anilist.executeQuery import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.anilist.api.ToggleLike
import ani.dantotsu.currContext import ani.dantotsu.currContext
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
class AnilistMutations { 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<JsonObject>(query, variables)
}
suspend fun toggleFav(anime: Boolean = true, id: Int) { suspend fun toggleFav(anime: Boolean = true, id: Int) {
val query = val query = """
"""mutation (${"$"}animeId: Int,${"$"}mangaId:Int) { ToggleFavourite(animeId:${"$"}animeId,mangaId:${"$"}mangaId){ anime { edges { id } } manga { edges { id } } } }""" 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"}""" val variables = if (anime) """{"animeId":"$id"}""" else """{"mangaId":"$id"}"""
executeQuery<JsonObject>(query, variables) executeQuery<JsonObject>(query, variables)
} }
@ -25,7 +108,17 @@ class AnilistMutations {
FavType.STAFF -> "staffId" FavType.STAFF -> "staffId"
FavType.STUDIO -> "studioId" 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<JsonObject>(query) val result = executeQuery<JsonObject>(query)
return result?.get("errors") == null && result != null return result?.get("errors") == null && result != null
} }
@ -34,6 +127,51 @@ class AnilistMutations {
ANIME, MANGA, CHARACTER, STAFF, STUDIO 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<JsonObject>(query, variables)
return result?.get("errors") == null
}
suspend fun updateCustomLists(animeCustomLists: List<String>?, mangaCustomLists: List<String>?): 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<JsonObject>(query, variables)
return result?.get("errors") == null
}
suspend fun editList( suspend fun editList(
mediaID: Int, mediaID: Int,
progress: Int? = null, progress: Int? = null,
@ -46,14 +184,45 @@ class AnilistMutations {
completedAt: FuzzyDate? = null, completedAt: FuzzyDate? = null,
customList: List<String>? = null customList: List<String>? = null
) { ) {
val query = """ 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 ""} ) { mutation (
SaveMediaListEntry( mediaId: ${"$"}mediaID, progress: ${"$"}progress, repeat: ${"$"}repeat, notes: ${"$"}notes, private: ${"$"}private, scoreRaw: ${"$"}scoreRaw, status:${"$"}status, startedAt: ${"$"}start, completedAt: ${"$"}completed , customLists: ${"$"}customLists ) { ${"$"}mediaID: Int,
score(format:POINT_10_DECIMAL) startedAt{year month day} completedAt{year month day} ${"$"}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 val variables = """{"mediaID":$mediaID
${if (private != null) ""","private":$private""" else ""} ${if (private != null) ""","private":$private""" else ""}
@ -69,43 +238,168 @@ class AnilistMutations {
} }
suspend fun deleteList(listId: Int) { 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"}""" val variables = """{"id":"$listId"}"""
executeQuery<JsonObject>(query, variables) executeQuery<JsonObject>(query, variables)
} }
suspend fun rateReview(reviewId: Int, rating: String): Query.RateReviewResponse? { 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.RateReviewResponse>(query) return executeQuery<Query.RateReviewResponse>(query)
} }
suspend fun postActivity(text:String): String { suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
return executeQuery<Query.ToggleFollow>(
"""
mutation {
ToggleFollow(userId: $id) {
id
isFollowing
isFollower
}
}
""".trimIndent())
}
suspend fun toggleLike(id: Int, type: String): ToggleLike? {
return executeQuery<ToggleLike>(
"""
mutation Like {
ToggleLikeV2(id: $id, type: $type) {
__typename
}
}
""".trimIndent())
}
suspend fun postActivity(text: String, edit: Int? = null): String {
val encodedText = text.stringSanitizer() 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<JsonObject>(query) val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors") val errors = result?.get("errors")
return errors?.toString() return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
?: (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<JsonObject>(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<JsonObject>(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 { suspend fun postReview(summary: String, body: String, mediaId: Int, score: Int): String {
val encodedSummary = summary.stringSanitizer() val encodedSummary = summary.stringSanitizer()
val encodedBody = body.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<JsonObject>(query) val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors") val errors = result?.get("errors")
return errors?.toString() return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
} }
suspend fun postReply(activityId: Int, text: String): String { suspend fun deleteActivityReply(activityId: Int): Boolean {
val encodedText = text.stringSanitizer() val query = """
val query = "mutation{SaveActivityReply(activityId:$activityId,text:$encodedText){id}}" mutation {
DeleteActivityReply(id: $activityId) {
deleted
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query) val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors") val errors = result?.get("errors")
return errors?.toString() return errors == null
?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success") }
suspend fun deleteActivity(activityId: Int): Boolean {
val query = """
mutation {
DeleteActivity(id: $activityId) {
deleted
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors == null
} }
private fun String.stringSanitizer(): String { private fun String.stringSanitizer(): String {

View file

@ -8,11 +8,12 @@ import ani.dantotsu.connections.anilist.Anilist.authorRoles
import ani.dantotsu.connections.anilist.Anilist.executeQuery import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FeedResponse import ani.dantotsu.connections.anilist.api.FeedResponse
import ani.dantotsu.connections.anilist.api.FuzzyDate 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.NotificationResponse
import ani.dantotsu.connections.anilist.api.Page import ani.dantotsu.connections.anilist.api.Page
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.anilist.api.ReplyResponse import ani.dantotsu.connections.anilist.api.ReplyResponse
import ani.dantotsu.connections.anilist.api.ToggleLike
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.isOnline import ani.dantotsu.isOnline
import ani.dantotsu.logError import ani.dantotsu.logError
@ -27,6 +28,7 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@ -41,8 +43,8 @@ class AnilistQueries {
suspend fun getUserData(): Boolean { suspend fun getUserData(): Boolean {
val response: Query.Viewer? val response: Query.Viewer?
measureTimeMillis { measureTimeMillis {
response = response = executeQuery(
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}unreadNotificationCount}}""") """{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") } }.also { println("time : $it") }
val user = response?.data?.user ?: return false val user = response?.data?.user ?: return false
@ -59,6 +61,27 @@ class AnilistQueries {
val unread = PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications) val unread = PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications)
Anilist.unreadNotificationCount += unread Anilist.unreadNotificationCount += unread
Anilist.initialized = true 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 return true
} }
@ -75,7 +98,7 @@ class AnilistQueries {
media.cameFromContinue = false media.cameFromContinue = false
val query = 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 { runBlocking {
val anilist = async { val anilist = async {
var response = executeQuery<Query.Media>(query, force = true) var response = executeQuery<Query.Media>(query, force = true)
@ -90,7 +113,7 @@ class AnilistQueries {
media.popularity = fetchedMedia.popularity media.popularity = fetchedMedia.popularity
media.startDate = fetchedMedia.startDate media.startDate = fetchedMedia.startDate
media.endDate = fetchedMedia.endDate media.endDate = fetchedMedia.endDate
media.streamingEpisodes = fetchedMedia.streamingEpisodes
if (fetchedMedia.genres != null) { if (fetchedMedia.genres != null) {
media.genres = arrayListOf() media.genres = arrayListOf()
fetchedMedia.genres?.forEach { i -> 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<Query.Review> media.review = fetchedMedia.reviews!!.nodes as ArrayList<Query.Review>
} }
if (user?.mediaList?.isNotEmpty() == true) { if (user?.mediaList?.isNotEmpty() == true) {
@ -376,9 +399,8 @@ class AnilistQueries {
} }
return media 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<Media> { private suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
var hasNextPage = true var hasNextPage = true
@ -404,10 +426,68 @@ class AnilistQueries {
return responseArray return responseArray
} }
suspend fun getUserStatus(): ArrayList<User>? {
val toShow: List<Boolean> =
PrefManager.getVal(PrefName.HomeLayout)
if (toShow.getOrNull(7) != true) return null
val query = """{Page1:${status(1)}Page2:${status(2)}}"""
val response = executeQuery<Query.HomePageMedia>(query)
val list = mutableListOf<User>()
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<User>()
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 { 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}}}}}}""" 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 { 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 } } } } """ 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 { private fun recommendationPlannedQuery(type: String): String {
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${if (type == "ANIME") ", sort: MEDIA_POPULARITY_DESC" else ""} ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }""" return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${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 } } } } }"""
} }
private fun continueMediaQuery(type: String, status: String): String {
suspend fun initHomePage(): Map<String, ArrayList<*>> { 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<String, ArrayList<Media>> {
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>()) val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
val hidePrivate = PrefManager.getVal<Boolean>(PrefName.HidePrivate)
val removedMedia = ArrayList<Media>() val removedMedia = ArrayList<Media>()
val toShow: List<Boolean> = val toShow: List<Boolean> =
PrefManager.getVal(PrefName.HomeLayout) // anime continue, anime fav, anime planned, manga continue, manga fav, manga planned, recommendations PrefManager.getVal(PrefName.HomeLayout) // list of booleans for what to show
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(',')
val queries = mutableListOf<String>()
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.HomePageMedia>(query, show = true) val response = executeQuery<Query.HomePageMedia>(query, show = true)
val returnMap = mutableMapOf<String, ArrayList<*>>() val returnMap = mutableMapOf<String, ArrayList<Media>>()
fun current(type: String) {
fun processMedia(
type: String,
currentMedia: List<MediaList>?,
repeatingMedia: List<MediaList>?
) {
val subMap = mutableMapOf<Int, Media>() val subMap = mutableMapOf<Int, Media>()
val returnArray = arrayListOf<Media>() val returnArray = arrayListOf<Media>()
val current =
if (type == "Anime") response?.data?.currentAnime else response?.data?.currentManga (currentMedia ?: emptyList()).forEach { entry ->
val repeating = val media = Media(entry)
if (type == "Anime") response?.data?.repeatingAnime else response?.data?.repeatingManga if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) {
current?.lists?.forEach { li -> media.cameFromContinue = true
li.entries?.reversed()?.forEach { subMap[media.id] = media
val m = Media(it) } else {
if (m.id !in removeList) { removedMedia.add(media)
m.cameFromContinue = true
subMap[m.id] = m
} else {
removedMedia.add(m)
}
} }
} }
repeating?.lists?.forEach { li -> (repeatingMedia ?: emptyList()).forEach { entry ->
li.entries?.reversed()?.forEach { val media = Media(entry)
val m = Media(it) if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) {
if (m.id !in removeList) { media.cameFromContinue = true
m.cameFromContinue = true subMap[media.id] = media
subMap[m.id] = m } else {
} else { removedMedia.add(media)
removedMedia.add(m)
}
} }
} }
if (type != "Anime") { @Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal(
"continue${type}List",
listOf<Int>(),
List::class.java
) as List<Int>
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) returnArray.addAll(subMap.values)
returnMap["current$type"] = returnArray
return
} }
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
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 returnMap["current$type"] = returnArray
} }
fun planned(type: String) { if (toShow.getOrNull(0) == true) processMedia(
val subMap = mutableMapOf<Int, Media>() "Anime",
val returnArray = arrayListOf<Media>() response?.data?.currentAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
val current = response?.data?.repeatingAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed()
if (type == "Anime") response?.data?.plannedAnime else response?.data?.plannedManga )
current?.lists?.forEach { li -> if (toShow.getOrNull(2) == true) processMedia(
li.entries?.reversed()?.forEach { "AnimePlanned",
val m = Media(it) response?.data?.plannedAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
if (m.id !in removeList) { null
m.cameFromContinue = true )
subMap[m.id] = m if (toShow.getOrNull(3) == true) processMedia(
} else { "Manga",
removedMedia.add(m) response?.data?.currentManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
} response?.data?.repeatingManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed()
} )
} if (toShow.getOrNull(5) == true) processMedia(
@Suppress("UNCHECKED_CAST") "MangaPlanned",
val list = PrefManager.getNullableCustomVal( response?.data?.plannedManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
"continueAnimeList", null
listOf<Int>(), )
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
}
for (i in subMap) {
if (i.value !in returnArray) returnArray.add(i.value)
}
} else returnArray.addAll(subMap.values)
returnMap["planned$type"] = returnArray
}
fun favorite(type: String) { fun processFavorites(type: String, favorites: List<MediaEdge>?) {
val favourites =
if (type == "Anime") response?.data?.favoriteAnime?.favourites else response?.data?.favoriteManga?.favourites
val apiMediaList = if (type == "Anime") favourites?.anime else favourites?.manga
val returnArray = arrayListOf<Media>() val returnArray = arrayListOf<Media>()
apiMediaList?.edges?.forEach { favorites?.forEach { edge ->
it.node?.let { i -> edge.node?.let {
val m = Media(i).apply { isFav = true } val media = Media(it).apply { isFav = true }
if (m.id !in removeList) { if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) {
returnArray.add(m) returnArray.add(media)
} else { } else {
removedMedia.add(m) removedMedia.add(media)
} }
} }
} }
returnMap["favorite$type"] = returnArray returnMap["favorite$type"] = returnArray
} }
if (toShow.getOrNull(0) == true) { if (toShow.getOrNull(1) == true) processFavorites(
current("Anime") "Anime",
} response?.data?.favoriteAnime?.favourites?.anime?.edges
if (toShow.getOrNull(1) == true) { )
favorite("Anime") if (toShow.getOrNull(4) == true) processFavorites(
} "Manga",
if (toShow.getOrNull(2) == true) { response?.data?.favoriteManga?.favourites?.manga?.edges
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(6) == true) { if (toShow.getOrNull(6) == true) {
val subMap = mutableMapOf<Int, Media>() val subMap = mutableMapOf<Int, Media>()
response?.data?.recommendationQuery?.apply { response?.data?.recommendationQuery?.recommendations?.forEach {
recommendations?.onEach { it.mediaRecommendation?.let { json ->
val json = it.mediaRecommendation val media = Media(json)
if (json != null) { media.relation = json.type?.toString()
val m = Media(json) subMap[media.id] = media
m.relation = json.type?.toString()
subMap[m.id] = m
}
} }
} }
response?.data?.recommendationPlannedQueryAnime?.apply { response?.data?.recommendationPlannedQueryAnime?.lists?.flatMap {
lists?.forEach { li -> it.entries ?: emptyList()
li.entries?.forEach { }?.forEach {
val m = Media(it) val media = Media(it)
if (m.status == "RELEASING" || m.status == "FINISHED") { if (media.status in listOf("RELEASING", "FINISHED")) {
m.relation = it.media?.type?.toString() media.relation = it.media?.type?.toString()
subMap[m.id] = m subMap[media.id] = media
}
}
} }
} }
response?.data?.recommendationPlannedQueryManga?.apply { response?.data?.recommendationPlannedQueryManga?.lists?.flatMap {
lists?.forEach { li -> it.entries ?: emptyList()
li.entries?.forEach { }?.forEach {
val m = Media(it) val media = Media(it)
if (m.status == "RELEASING" || m.status == "FINISHED") { if (media.status in listOf("RELEASING", "FINISHED")) {
m.relation = it.media?.type?.toString() media.relation = it.media?.type?.toString()
subMap[m.id] = m subMap[media.id] = media
}
}
} }
} }
val list = ArrayList(subMap.values.toList()) val list = ArrayList(subMap.values).apply { sortByDescending { it.meanScore } }
list.sortByDescending { it.meanScore }
returnMap["recommendations"] = list returnMap["recommendations"] = list
} }
if (toShow.getOrNull(7) == true) {
val list = mutableListOf<User>()
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<User>()
val groupedActivities = activities.groupBy { it.userId }
groupedActivities.forEach { (_, userActivities) -> returnMap["hidden"] = removedMedia.distinctBy { it.id }.toCollection(arrayListOf())
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<Media>
}
return returnMap return returnMap
} }
@ -1034,153 +1047,105 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
return null return null
} }
private val onListAnime = private fun mediaList(media1: Page?): ArrayList<Media> {
(if (PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace( val combinedList = arrayListOf<Media>()
"\"", media1?.media?.mapTo(combinedList) { Media(it) }
"" return combinedList
) }
private val isAdult =
(if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "") 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 { 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}}}}""" 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}}}}""")
private fun trendingMovies(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: ANIME, format: MOVIE, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun topRatedAnime(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun mostFavAnime(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
suspend fun loadAnimeList(): Map<String, ArrayList<Media>> {
val list = mutableMapOf<String, ArrayList<Media>>()
fun query(): String {
return """{
recentUpdates:${recentAnimeUpdates(1)}
recentUpdates2:${recentAnimeUpdates(2)}
trendingMovies:${trendingMovies(1)}
trendingMovies2:${trendingMovies(2)}
topRated:${topRatedAnime(1)}
topRated2:${topRatedAnime(2)}
mostFav:${mostFavAnime(1)}
mostFav2:${mostFavAnime(2)}
}""".trimIndent()
} }
executeQuery<Query.AnimeList>(query(), force = true)?.data?.apply { }
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly) private fun queryAnimeList(): String {
val idArr = mutableListOf<Int>() return buildString {
list["recentUpdates"] = recentUpdates?.airingSchedules?.mapNotNull { i -> append("""{recentUpdates:${recentAnimeUpdates(1)} recentUpdates2:${recentAnimeUpdates(2)} trendingMovies:${buildQueryString("POPULARITY_DESC", "ANIME", "MOVIE")} topRated:${buildQueryString("SCORE_DESC", "ANIME")} mostFav:${buildQueryString("FAVOURITES_DESC", "ANIME")}}""")
i.media?.let { }
if (!idArr.contains(it.id)) }
if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
idArr.add(it.id) private fun queryMangaList(): String {
Media(it) return buildString {
} else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)) { 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")}}""")
idArr.add(it.id) }
Media(it) }
} else if ((listOnly && it.mediaListEntry != null)) {
idArr.add(it.id) suspend fun loadAnimeList(): Map<String, ArrayList<Media>> = coroutineScope {
Media(it) val list = mutableMapOf<String, ArrayList<Media>>()
} else null
else null fun filterRecentUpdates(page: Page?): ArrayList<Media> {
val listOnly = getPreference(PrefName.RecentlyListOnly)
val adultOnly = getPreference(PrefName.AdultOnly)
val idArr = mutableSetOf<Int>()
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() }?.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<Query.AnimeList>(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 = suspend fun loadMangaList(): Map<String, ArrayList<Media>> = coroutineScope {
(if (PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace(
"\"",
""
)
private fun trendingManga(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA,countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun trendingManhwa(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, countryOfOrigin:KR, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun trendingNovel(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, format: NOVEL, countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun topRatedManga(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun mostFavManga(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
suspend fun loadMangaList(): Map<String, ArrayList<Media>> {
val list = mutableMapOf<String, ArrayList<Media>>() val list = mutableMapOf<String, ArrayList<Media>>()
fun query(): String {
return """{ val mangaList = async { executeQuery<Query.MangaList>(queryMangaList(), force = true) }
trendingManga:${trendingManga(1)}
trendingManga2:${trendingManga(2)} mangaList.await()?.data?.apply {
trendingManhwa:${trendingManhwa(1)} list["trendingManga"] = mediaList(trendingManga)
trendingManhwa2:${trendingManhwa(2)} list["trendingManhwa"] = mediaList(trendingManhwa)
trendingNovel:${trendingNovel(1)} list["trendingNovel"] = mediaList(trendingNovel)
trendingNovel2:${trendingNovel(2)} list["topRated"] = mediaList(topRated)
topRated:${topRatedManga(1)} list["mostFav"] = mediaList(mostFav)
topRated2:${topRatedManga(2)}
mostFav:${mostFavManga(1)}
mostFav2:${mostFavManga(2)}
}""".trimIndent()
} }
executeQuery<Query.MangaList>(query(), force = true)?.data?.apply { list
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
} }
suspend fun recentlyUpdated( suspend fun recentlyUpdated(
greater: Long = 0, greater: Long = 0,
lesser: Long = System.currentTimeMillis() / 1000 - 10000 lesser: Long = System.currentTimeMillis() / 1000 - 10000
@ -1509,25 +1474,17 @@ Page(page:$page,perPage:50) {
return author 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<Query.ReviewsResponse>( return executeQuery<Query.ReviewsResponse>(
"""{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}}}}}""", """{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 force = true
) )
} }
suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
return executeQuery<Query.ToggleFollow>(
"""mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}"""
)
}
suspend fun toggleLike(id: Int, type: String): ToggleLike? {
return executeQuery<ToggleLike>(
"""mutation Like{ToggleLikeV2(id:$id,type:$type){__typename}}"""
)
}
suspend fun getUserProfile(id: Int): Query.UserProfileResponse? { suspend fun getUserProfile(id: Int): Query.UserProfileResponse? {
return executeQuery<Query.UserProfileResponse>( return executeQuery<Query.UserProfileResponse>(
"""{followerPage:Page{followers(userId:$id){id}pageInfo{total}}followingPage:Page{following(userId:$id){id}pageInfo{total}}user:User(id:$id){id name about(asHtml:true)avatar{medium large}bannerImage isFollowing isFollower isBlocked favourites{anime{nodes{id coverImage{extraLarge large medium color}}}manga{nodes{id coverImage{extraLarge large medium color}}}characters{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}staff{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}studios{nodes{id name isFavourite}}}statistics{anime{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}manga{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}}siteUrl}}""", """{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( suspend fun getNotifications(
id: Int, id: Int,
page: Int = 1, page: Int = 1,
resetNotification: Boolean = true resetNotification: Boolean = true,
type: Boolean? = null
): NotificationResponse? { ): NotificationResponse? {
val typeIn = "type_in:[AIRING,MEDIA_MERGE,MEDIA_DELETION,MEDIA_DATA_CHANGE]"
val reset = if (resetNotification) "true" else "false" val reset = if (resetNotification) "true" else "false"
val res = executeQuery<NotificationResponse>( val res = executeQuery<NotificationResponse>(
"""{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){pageInfo{currentPage,hasNextPage}notifications(resetNotificationCount:$reset){__typename...on AiringNotification{id,type,animeId,episode,contexts,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}},}...on FollowingNotification{id,userId,type,context,createdAt,user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMessageNotification{id,userId,type,activityId,context,createdAt,message{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMentionNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplySubscribedNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentMentionNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentReplyNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentSubscribedNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentLikeNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadLikeNotification{id,userId,type,threadId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on RelatedMediaAdditionNotification{id,type,context,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDataChangeNotification{id,type,mediaId,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaMergeNotification{id,type,mediaId,deletedMediaTitles,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDeletionNotification{id,type,deletedMediaTitle,context,reason,createdAt,}}}}""", """{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 force = true
) )
if (res != null && resetNotification) { if (res != null && resetNotification) {
@ -1612,8 +1571,9 @@ Page(page:$page,perPage:50) {
else if (userId != null) "userId:$userId," else if (userId != null) "userId:$userId,"
else if (global) "isFollowing:false,hasRepliesOrTypeText:true," else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
else "isFollowing:true," else "isFollowing:true,"
val typeIn = if (filter == "isFollowing:true,") "type_in:[TEXT,ANIME_LIST,MANGA_LIST,MEDIA_LIST]," else ""
return executeQuery<FeedResponse>( return executeQuery<FeedResponse>(
"""{Page(page:$page,perPage:$ITEMS_PER_PAGE){activities(${filter}sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id title{english romaji native userPreferred}bannerImage coverImage{medium large}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 force = true
) )
} }
@ -1621,13 +1581,14 @@ Page(page:$page,perPage:50) {
suspend fun getReplies( suspend fun getReplies(
activityId: Int, activityId: Int,
page: Int = 1 page: Int = 1
) : ReplyResponse? { ): 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}}}}}""" 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) return executeQuery(query, force = true)
} }
private fun status(page: Int = 1): String { 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<Media> { suspend fun getUpcomingAnime(id: String): List<Media> {
@ -1674,4 +1635,4 @@ Page(page:$page,perPage:50) {
companion object { companion object {
const val ITEMS_PER_PAGE = 25 const val ITEMS_PER_PAGE = 25
} }
} }

View file

@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
suspend fun getUserId(context: Context, block: () -> Unit) { suspend fun getUserId(context: Context, block: () -> Unit) {
if (!Anilist.initialized) { if (!Anilist.initialized && PrefManager.getVal<String>(PrefName.AnilistToken) != "") {
if (Anilist.query.getUserData()) { if (Anilist.query.getUserData()) {
tryWithSuspend { tryWithSuspend {
if (MAL.token != null && !MAL.query.getUserData()) if (MAL.token != null && !MAL.query.getUserData())
@ -81,24 +81,26 @@ class AnilistHomeViewModel : ViewModel() {
MutableLiveData<ArrayList<User>>(null) MutableLiveData<ArrayList<User>>(null)
fun getUserStatus(): LiveData<ArrayList<User>> = userStatus fun getUserStatus(): LiveData<ArrayList<User>> = userStatus
suspend fun initUserStatus() {
val res = Anilist.query.getUserStatus()
res?.let { userStatus.postValue(it) }
}
private val hidden: MutableLiveData<ArrayList<Media>> = private val hidden: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null) MutableLiveData<ArrayList<Media>>(null)
fun getHidden(): LiveData<ArrayList<Media>> = hidden fun getHidden(): LiveData<ArrayList<Media>> = hidden
@Suppress("UNCHECKED_CAST")
suspend fun initHomePage() { suspend fun initHomePage() {
val res = Anilist.query.initHomePage() val res = Anilist.query.initHomePage()
res["currentAnime"]?.let { animeContinue.postValue(it as ArrayList<Media>?) } res["currentAnime"]?.let { animeContinue.postValue(it) }
res["favoriteAnime"]?.let { animeFav.postValue(it as ArrayList<Media>?) } res["favoriteAnime"]?.let { animeFav.postValue(it) }
res["plannedAnime"]?.let { animePlanned.postValue(it as ArrayList<Media>?) } res["currentAnimePlanned"]?.let { animePlanned.postValue(it) }
res["currentManga"]?.let { mangaContinue.postValue(it as ArrayList<Media>?) } res["currentManga"]?.let { mangaContinue.postValue(it) }
res["favoriteManga"]?.let { mangaFav.postValue(it as ArrayList<Media>?) } res["favoriteManga"]?.let { mangaFav.postValue(it) }
res["plannedManga"]?.let { mangaPlanned.postValue(it as ArrayList<Media>?) } res["currentMangaPlanned"]?.let { mangaPlanned.postValue(it) }
res["recommendations"]?.let { recommendation.postValue(it as ArrayList<Media>?) } res["recommendations"]?.let { recommendation.postValue(it) }
res["hidden"]?.let { hidden.postValue(it as ArrayList<Media>?) } res["hidden"]?.let { hidden.postValue(it) }
res["status"]?.let { userStatus.postValue(it as ArrayList<User>?) }
} }
suspend fun loadMain(context: FragmentActivity) { suspend fun loadMain(context: FragmentActivity) {

View file

@ -163,13 +163,9 @@ class Query {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?, @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("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("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("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 @Serializable
data class Data( data class Data(
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?, @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("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("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("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("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
) )
} }

View file

@ -143,7 +143,7 @@ data class Media(
@SerialName("externalLinks") var externalLinks: List<MediaExternalLink>?, @SerialName("externalLinks") var externalLinks: List<MediaExternalLink>?,
// Data and links to legal streaming episodes on external sites // Data and links to legal streaming episodes on external sites
// @SerialName("streamingEpisodes") var streamingEpisodes: List<MediaStreamingEpisode>?, @SerialName("streamingEpisodes") var streamingEpisodes: List<MediaStreamingEpisode>?,
// The ranking of the media in a particular time span and format compared to other media // The ranking of the media in a particular time span and format compared to other media
// @SerialName("rankings") var rankings: List<MediaRank>?, // @SerialName("rankings") var rankings: List<MediaRank>?,
@ -239,7 +239,20 @@ data class AiringSchedule(
// The associate media of the airing episode // The associate media of the airing episode
@SerialName("media") var media: Media?, @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 @Serializable
data class MediaCoverImage( 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. // The cover image url of the media at its largest size. If this size isn't available, large will be provided instead.

View file

@ -74,7 +74,7 @@ data class User(
@Serializable @Serializable
data class UserOptions( data class UserOptions(
// The language the user wants to see media titles in // 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 // Whether the user has enabled viewing of 18+ content
@SerialName("displayAdultContent") var displayAdultContent: Boolean?, @SerialName("displayAdultContent") var displayAdultContent: Boolean?,
@ -88,17 +88,17 @@ data class UserOptions(
// // Notification options // // Notification options
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?, // // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,
// //
// // The user's timezone offset (Auth user only) // The user's timezone offset (Auth user only)
// @SerialName("timezone") var timezone: String?, @SerialName("timezone") var timezone: String?,
// //
// // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always. // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always.
// @SerialName("activityMergeTime") var activityMergeTime: Int?, @SerialName("activityMergeTime") var activityMergeTime: Int?,
// //
// // The language the user wants to see staff and character names in // The language the user wants to see staff and character names in
// // @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?, @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?,
// //
// // Whether the user only allow messages from users they follow // Whether the user only allow messages from users they follow
// @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?, @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?,
// The list activity types the user has disabled from being created from list updates // The list activity types the user has disabled from being created from list updates
// @SerialName("disabledListActivity") var disabledListActivity: List<ListActivityOption>?, // @SerialName("disabledListActivity") var disabledListActivity: List<ListActivityOption>?,
@ -119,6 +119,40 @@ data class UserStatisticTypes(
@SerialName("manga") var manga: UserStatistics? @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 @Serializable
data class UserStatistics( data class UserStatistics(
// //
@ -164,7 +198,7 @@ data class Favourites(
@Serializable @Serializable
data class MediaListOptions( data class MediaListOptions(
// The score format the user is using for media lists // The score format the user is using for media lists
@SerialName("scoreFormat") var scoreFormat: String?, @SerialName("scoreFormat") var scoreFormat: ScoreFormat?,
// The default order list rows should be displayed in // The default order list rows should be displayed in
@SerialName("rowOrder") var rowOrder: String?, @SerialName("rowOrder") var rowOrder: String?,
@ -181,8 +215,8 @@ data class MediaListTypeOptions(
// The order each list should be displayed in // The order each list should be displayed in
@SerialName("sectionOrder") var sectionOrder: List<String>?, @SerialName("sectionOrder") var sectionOrder: List<String>?,
// If the completed sections of the list should be separated by format // // If the completed sections of the list should be separated by format
@SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?, // @SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?,
// The names of the user's custom lists // The names of the user's custom lists
@SerialName("customLists") var customLists: List<String>?, @SerialName("customLists") var customLists: List<String>?,

View file

@ -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<MangaUpdatesResponse>()
} 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<Results>? = null
) {
@Serializable
data class Results(
val record: Record,
val metadata: MetaData
) {
@Serializable
data class Record(
@SerialName("id")
val id: Int,
@SerialName("title")
val title: String,
@SerialName("volume")
val volume: String?,
@SerialName("chapter")
val chapter: String?,
@SerialName("release_date")
val releaseDate: String
)
@Serializable
data class MetaData(
val series: Series
) {
@Serializable
data class Series(
@SerialName("series_id")
val seriesId: Long?,
@SerialName("title")
val title: String?,
@SerialName("latest_chapter")
val latestChapter: Int?,
@SerialName("last_updated")
val lastUpdated: LastUpdated?
) {
@Serializable
data class LastUpdated(
@SerialName("timestamp")
val timestamp: Long,
@SerialName("as_rfc3339")
val asRfc3339: String,
@SerialName("as_string")
val asString: String
)
}
}
}
}
}

View file

@ -27,8 +27,11 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
object CommentsAPI { 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 isOnline: Boolean = true
private var commentsEnabled = PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1
private val ADDRESS: String get() = if (commentsEnabled) API_ADDRESS else LOCAL_HOST
var authToken: String? = null var authToken: String? = null
var userId: String? = null var userId: String? = null
var isBanned: Boolean = false var isBanned: Boolean = false
@ -369,10 +372,9 @@ object CommentsAPI {
} }
errorMessage("Failed to login after multiple attempts") errorMessage("Failed to login after multiple attempts")
} }
private fun errorMessage(reason: String) { private fun errorMessage(reason: String) {
Logger.log(reason) if (commentsEnabled) Logger.log(reason)
if (isOnline) snackString(reason) if (isOnline && commentsEnabled) snackString(reason)
} }
fun logout() { fun logout() {

View file

@ -70,7 +70,7 @@ object Discord {
const val application_Id = "1163925779692912771" const val application_Id = "1163925779692912771"
const val small_Image: String = 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 = 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"
} }

View file

@ -1,24 +1,19 @@
package ani.dantotsu.connections.discord 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.Activity
import ani.dantotsu.connections.discord.serializers.Presence import ani.dantotsu.connections.discord.serializers.Presence
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import ani.dantotsu.client as app import java.util.concurrent.TimeUnit.SECONDS
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
open class RPC(val token: String, val coroutineContext: CoroutineContext) { open class RPC(val token: String, val coroutineContext: CoroutineContext) {
private val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
}
enum class Type { enum class Type {
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
} }
@ -27,7 +22,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
companion object { companion object {
data class RPCData( data class RPCData(
val applicationId: String? = null, val applicationId: String,
val type: Type? = null, val type: Type? = null,
val activityName: String? = null, val activityName: String? = null,
val details: String? = null, val details: String? = null,
@ -39,23 +34,21 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
val stopTimestamp: Long? = null, val stopTimestamp: Long? = null,
val buttons: MutableList<Link> = mutableListOf() val buttons: MutableList<Link> = 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<KizzyApi>()
return json?.id
}
suspend fun createPresence(data: RPCData): String { suspend fun createPresence(data: RPCData): String {
val json = Json { val json = Json {
encodeDefaults = true encodeDefaults = true
allowStructuredMapKeys = true allowStructuredMapKeys = true
ignoreUnknownKeys = 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( return json.encodeToString(Presence.Response(
3, 3,
Presence( Presence(

View file

@ -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<List<ExternalAsset>>(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)
}
})
}
}
}

View file

@ -40,6 +40,7 @@ data class Activity(
@Serializable @Serializable
data class Timestamps( data class Timestamps(
val start: Long? = null, val start: Long? = null,
@SerialName("end")
val stop: Long? = null val stop: Long? = null
) )
} }

View file

@ -28,6 +28,7 @@ class Contributors {
"rebelonion" -> "Owner & Maintainer" "rebelonion" -> "Owner & Maintainer"
"sneazy-ibo" -> "Contributor & Comment Moderator" "sneazy-ibo" -> "Contributor & Comment Moderator"
"WaiWhat" -> "Icon Designer" "WaiWhat" -> "Icon Designer"
"itsmechinmoy" -> "Discord and Telegram Admin/Helper, Comment Moderator & Translator"
else -> "Contributor" else -> "Contributor"
} }
developers = developers.plus( developers = developers.plus(
@ -89,9 +90,15 @@ class Contributors {
"Comment Moderator and Arabic Translator", "Comment Moderator and Arabic Translator",
"https://anilist.co/user/6049773" "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( Developer(
"hastsu", "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", "Comment Moderator and Arabic Translator",
"https://anilist.co/user/6183359" "https://anilist.co/user/6183359"
), ),
@ -111,4 +118,4 @@ class Contributors {
@SerialName("html_url") @SerialName("html_url")
val htmlUrl: String val htmlUrl: String
) )
} }

View file

@ -125,7 +125,7 @@ class DownloadCompat {
Logger.log(e) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineAnimeModel( return OfflineAnimeModel(
"unknown", downloadedType.titleName,
"0", "0",
"??", "??",
"??", "??",
@ -188,7 +188,7 @@ class DownloadCompat {
Logger.log(e) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel( return OfflineMangaModel(
"unknown", downloadedType.titleName,
"0", "0",
"??", "??",
"??", "??",

View file

@ -13,7 +13,6 @@ import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import com.anggrayudi.storage.callback.FolderCallback import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.deleteRecursively import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.findFolder
import com.anggrayudi.storage.file.moveFolderTo import com.anggrayudi.storage.file.moveFolderTo
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
@ -279,6 +278,7 @@ class DownloadsManager(private val context: Context) {
* @param type the type of media * @param type the type of media
* @return the base directory * @return the base directory
*/ */
@Synchronized
private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? { private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir)) val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null if (baseDirectory == Uri.EMPTY) return null
@ -307,6 +307,7 @@ class DownloadsManager(private val context: Context) {
* @param chapter the chapter of the media * @param chapter the chapter of the media
* @return the subdirectory * @return the subdirectory
*/ */
@Synchronized
fun getSubDirectory( fun getSubDirectory(
context: Context, context: Context,
type: MediaType, type: MediaType,
@ -344,23 +345,34 @@ class DownloadsManager(private val context: Context) {
} }
} }
@Synchronized
private fun getBaseDirectory(context: Context): DocumentFile? { private fun getBaseDirectory(context: Context): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir)) val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null 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( private fun DocumentFile.findOrCreateFolder(
name: String, overwrite: Boolean name: String, overwrite: Boolean
): DocumentFile? { ): DocumentFile? {
return if (overwrite) { val validName = name.findValidName()
findFolder(name.findValidName())?.delete() synchronized(lock) {
createDirectory(name.findValidName()) return if (overwrite) {
} else { findFolder(validName)?.delete()
findFolder(name.findValidName()) ?: createDirectory(name.findValidName()) 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 private const val RATIO_THRESHOLD = 95
fun Media.compareName(name: String): Boolean { fun Media.compareName(name: String): Boolean {
val mainName = mainName().findValidName().lowercase() val mainName = mainName().findValidName().lowercase()
@ -379,7 +391,7 @@ class DownloadsManager(private val context: Context) {
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'" private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
fun String?.findValidName(): String { fun String?.findValidName(): String {
return this?.replace("/","_")?.filterNot { RESERVED_CHARS.contains(it) } ?: "" return this?.replace("/", "_")?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
} }
data class DownloadedType( data class DownloadedType(

View file

@ -181,7 +181,6 @@ class AnimeDownloaderService : Service() {
} }
private fun updateNotification() { private fun updateNotification() {
// Update the notification to reflect the current state of the queue
val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
val text = if (pendingDownloads > 0) { val text = if (pendingDownloads > 0) {
"Pending downloads: $pendingDownloads" "Pending downloads: $pendingDownloads"
@ -201,8 +200,8 @@ class AnimeDownloaderService : Service() {
@androidx.annotation.OptIn(UnstableApi::class) @androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) { suspend fun download(task: AnimeDownloadTask) {
try { withContext(Dispatchers.IO) {
withContext(Dispatchers.Main) { try {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
this@AnimeDownloaderService, this@AnimeDownloaderService,
@ -214,22 +213,34 @@ class AnimeDownloaderService : Service() {
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}") builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
if (notifi) { 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, this@AnimeDownloaderService,
MediaType.ANIME, MediaType.ANIME,
false, false,
task.title
) ?: throw Exception("Failed to create output directory")
val outputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
true,
task.title, task.title,
task.episode task.episode
) ?: throw Exception("Failed to create output directory") ) ?: throw Exception("Failed to create output directory")
val extension = ffExtension!!.getFileExtension() val extension = ffExtension!!.getFileExtension()
outputDir.findFile("${task.getTaskName().findValidName()}.${extension.first}")?.delete() outputDir.findFile("${task.getTaskName().findValidName()}.${extension.first}")
?.delete()
val outputFile = 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") ?: throw Exception("Failed to create output file")
var percent = 0 var percent = 0
@ -273,7 +284,7 @@ class AnimeDownloaderService : Service() {
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId = currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
ffTask ffTask
saveMediaInfo(task) saveMediaInfo(task, baseOutputDir)
// periodically check if the download is complete // periodically check if the download is complete
while (ffExtension.getState(ffTask) != "COMPLETED") { while (ffExtension.getState(ffTask) != "COMPLETED") {
@ -287,7 +298,11 @@ class AnimeDownloaderService : Service() {
) )
} Download failed" } 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") toast("${getTaskName(task.title, task.episode)} Download failed")
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}") Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
downloadsManager.removeDownload( downloadsManager.removeDownload(
@ -320,7 +335,9 @@ class AnimeDownloaderService : Service() {
percent.coerceAtMost(99) percent.coerceAtMost(99)
) )
if (notifi) { if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build()) withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
} }
kotlinx.coroutines.delay(2000) kotlinx.coroutines.delay(2000)
} }
@ -335,7 +352,11 @@ class AnimeDownloaderService : Service() {
) )
} Download failed" } 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") snackString("${getTaskName(task.title, task.episode)} Download failed")
downloadsManager.removeDownload( downloadsManager.removeDownload(
DownloadedType( DownloadedType(
@ -367,7 +388,11 @@ class AnimeDownloaderService : Service() {
) )
} Download completed" } 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") snackString("${getTaskName(task.title, task.episode)} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString( PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(), task.getTaskName(),
@ -385,23 +410,20 @@ class AnimeDownloaderService : Service() {
broadcastDownloadFinished(task.episode) broadcastDownloadFinished(task.episode)
} else throw Exception("Download failed") } 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<CrashlyticsInterface>().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<CrashlyticsInterface>().logException(e)
}
broadcastDownloadFailed(task.episode)
} }
} }
private fun saveMediaInfo(task: AnimeDownloadTask) { private fun saveMediaInfo(task: AnimeDownloadTask, directory: DocumentFile) {
CoroutineScope(Dispatchers.IO).launch { 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) directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
val file = directory.createFile("application/json", "media.json") val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created") ?: throw Exception("File not created")

View file

@ -30,6 +30,7 @@ import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity import ani.dantotsu.currActivity
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
@ -48,6 +49,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.anggrayudi.storage.file.openInputStream import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
@ -202,25 +204,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val type: MediaType = MediaType.ANIME val type: MediaType = MediaType.ANIME
// Alert dialog to confirm deletion // Alert dialog to confirm deletion
val builder = requireContext().customAlertDialog().apply {
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup) setTitle("Delete ${item.title}?")
builder.setTitle("Delete ${item.title}?") setMessage("Are you sure you want to delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?") setPosButton(R.string.yes) {
builder.setPositiveButton("Yes") { _, _ -> downloadManager.removeMedia(item.title, type)
downloadManager.removeMedia(item.title, type) val mediaIds = PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values ?: emptySet()
val mediaIds = if (mediaIds.isEmpty()) {
PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values snackString("No media found") // if this happens, terrible things have happened
?: emptySet() }
if (mediaIds.isEmpty()) { getDownloads()
snackString("No media found") // if this happens, terrible things have happened
} }
getDownloads() setNegButton(R.string.no) {
// Do nothing
}
show()
} }
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true true
} }
} }
@ -319,17 +318,20 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
) )
val gson = GsonBuilder() val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> { .registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl SChapterImpl()
}) })
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> { .registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
SAnimeImpl() // Provide an instance of SAnimeImpl SAnimeImpl()
}) })
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> { .registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
SEpisodeImpl() // Provide an instance of SEpisodeImpl SEpisodeImpl()
}) })
.create() .create()
val media = directory?.findFile("media.json") 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 = val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use { media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText() it?.readText()
@ -394,6 +396,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
bannerUri bannerUri
) )
} catch (e: Exception) { } catch (e: Exception) {
Logger.log(e)
return try { return try {
loadOfflineAnimeModelCompat(downloadedType) loadOfflineAnimeModelCompat(downloadedType)
} catch (e: Exception) { } catch (e: Exception) {
@ -401,7 +404,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
Logger.log(e) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
OfflineAnimeModel( OfflineAnimeModel(
"unknown", downloadedType.titleName,
"0", "0",
"??", "??",
"??", "??",

View file

@ -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.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.NumberConverter.Companion.ofLength
import com.anggrayudi.storage.file.deleteRecursively import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.forceDelete import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream import com.anggrayudi.storage.file.openOutputStream
@ -134,15 +135,15 @@ class MangaDownloaderService : Service() {
mutex.withLock { mutex.withLock {
downloadJobs[task.chapter] = job downloadJobs[task.chapter] = job
} }
job.join() // Wait for the job to complete before continuing to the next task job.join()
mutex.withLock { mutex.withLock {
downloadJobs.remove(task.chapter) downloadJobs.remove(task.chapter)
} }
updateNotification() // Update the notification after each task is completed updateNotification()
} }
if (MangaServiceDataSingleton.downloadQueue.isEmpty()) { if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) { 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) { suspend fun download(task: DownloadTask) {
try { try {
withContext(Dispatchers.Main) { withContext(Dispatchers.IO) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
this@MangaDownloaderService, this@MangaDownloaderService,
@ -194,18 +195,27 @@ class MangaDownloaderService : Service() {
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>() val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}") builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) { 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, this@MangaDownloaderService,
MediaType.MANGA, MediaType.MANGA,
false, false,
task.title, task.title,
task.chapter 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 var farthest = 0
for ((index, image) in task.imageData.withIndex()) { for ((index, image) in task.imageData.withIndex()) {
if (deferredMap.size >= task.simultaneousDownloads) { if (deferredMap.size >= task.simultaneousDownloads) {
@ -226,30 +236,36 @@ class MangaDownloaderService : Service() {
} }
if (bitmap != null) { if (bitmap != null) {
saveToDisk("$index.jpg", bitmap, task.title, task.chapter) saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap)
} }
farthest++ farthest++
builder.setProgress(task.imageData.size, farthest, false) builder.setProgress(task.imageData.size, farthest, false)
broadcastDownloadProgress( broadcastDownloadProgress(
task.chapter, task.chapter,
farthest * 100 / task.imageData.size farthest * 100 / task.imageData.size
) )
if (notifi) { if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build()) withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
} }
bitmap bitmap
} }
} }
// Wait for any remaining deferred to complete
deferredMap.values.awaitAll() deferredMap.values.awaitAll()
builder.setContentText("${task.title} - ${task.chapter} Download complete") withContext(Dispatchers.Main) {
.setProgress(0, 0, false) builder.setContentText("${task.title} - ${task.chapter} Download complete")
notificationManager.notify(NOTIFICATION_ID, builder.build()) .setProgress(0, 0, false)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
saveMediaInfo(task) saveMediaInfo(task, baseOutputDir)
downloadsManager.addDownload( downloadsManager.addDownload(
DownloadedType( DownloadedType(
task.title, 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 { 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) directory.findFile(fileName)?.forceDelete(this)
// Create a file reference within that directory for the image
val file = val file =
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created") 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 -> file.openOutputStream(this, false).use { outputStream ->
if (outputStream == null) throw Exception("Output stream is null") if (outputStream == null) throw Exception("Output stream is null")
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
@ -292,11 +307,8 @@ class MangaDownloaderService : Service() {
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) { private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
launchIO { launchIO {
val directory =
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService) directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
val file = directory.createFile("application/json", "media.json") val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created") ?: throw Exception("File not created")

View file

@ -46,6 +46,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.anggrayudi.storage.file.openInputStream import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
@ -171,7 +172,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val item = adapter.getItem(position) as OfflineMangaModel val item = adapter.getItem(position) as OfflineMangaModel
val media = val media =
downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) } 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 { media?.let {
lifecycleScope.launch { lifecycleScope.launch {
ContextCompat.startActivity( ContextCompat.startActivity(
@ -197,19 +202,15 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
MediaType.NOVEL MediaType.NOVEL
} }
// Alert dialog to confirm deletion // Alert dialog to confirm deletion
val builder = requireContext().customAlertDialog().apply {
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup) setTitle("Delete ${item.title}?")
builder.setTitle("Delete ${item.title}?") setMessage("Are you sure you want to delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?") setPosButton(R.string.yes) {
builder.setPositiveButton("Yes") { _, _ -> downloadManager.removeMedia(item.title, type)
downloadManager.removeMedia(item.title, type) getDownloads()
getDownloads() }
} setNegButton(R.string.no)
builder.setNegativeButton("No") { _, _ -> }.show()
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true true
} }
} }
@ -279,10 +280,12 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
downloads = listOf() downloads = listOf()
downloadsJob = Job() downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch { 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<OfflineMangaModel>() val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) { 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 download = tDownloads.firstOrNull() ?: continue
val offlineMangaModel = loadOfflineMangaModel(download) val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel newMangaDownloads += offlineMangaModel
@ -291,7 +294,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct() val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>() val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) { 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 download = tDownloads.firstOrNull() ?: continue
val offlineMangaModel = loadOfflineMangaModel(download) val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel newNovelDownloads += offlineMangaModel
@ -320,11 +324,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
) )
val gson = GsonBuilder() val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> { .registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl SChapterImpl()
}) })
.create() .create()
val media = directory?.findFile("media.json") 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 = val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use { media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText() it?.readText()
@ -340,7 +347,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel { private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = downloadedType.type.asText() val type = downloadedType.type.asText()
//load media.json and convert to media class with gson
try { try {
val directory = getSubDirectory( val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type, context ?: currContext()!!, downloadedType.type,
@ -378,6 +384,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
bannerUri bannerUri
) )
} catch (e: Exception) { } catch (e: Exception) {
Logger.log(e)
return try { return try {
loadOfflineMangaModelCompat(downloadedType) loadOfflineMangaModelCompat(downloadedType)
} catch (e: Exception) { } catch (e: Exception) {
@ -385,7 +392,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
Logger.log(e) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel( return OfflineMangaModel(
"unknown", downloadedType.titleName,
"0", "0",
"??", "??",
"??", "??",

View file

@ -239,6 +239,13 @@ class NovelDownloaderService : Service() {
return@withContext return@withContext
} }
val baseDirectory = getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title
) ?: throw Exception("Directory not found")
// Start the download // Start the download
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
@ -334,7 +341,7 @@ class NovelDownloaderService : Service() {
notificationManager.notify(NOTIFICATION_ID, builder.build()) notificationManager.notify(NOTIFICATION_ID, builder.build())
} }
saveMediaInfo(task) saveMediaInfo(task, baseDirectory)
downloadsManager.addDownload( downloadsManager.addDownload(
DownloadedType( DownloadedType(
task.title, task.title,
@ -354,15 +361,8 @@ class NovelDownloaderService : Service() {
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) { private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
launchIO { launchIO {
val directory =
getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title
) ?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService) directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
val file = directory.createFile("application/json", "media.json") val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created") ?: throw Exception("File not created")

View file

@ -3,7 +3,6 @@ package ani.dantotsu.download.video
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -29,10 +28,10 @@ import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.anime.AnimeServiceDataSingleton import ani.dantotsu.download.anime.AnimeServiceDataSingleton
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -72,19 +71,19 @@ object Helper {
episodeImage episodeImage
) )
val downloadsManger = Injekt.get<DownloadsManager>() val downloadsManager = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManger val downloadCheck = downloadsManager
.queryDownload(title, episode, MediaType.ANIME) .queryDownload(title, episode, MediaType.ANIME)
if (downloadCheck) { if (downloadCheck) {
AlertDialog.Builder(context, R.style.MyPopup) context.customAlertDialog().apply {
.setTitle("Download Exists") setTitle("Download Exists")
.setMessage("A download for this episode already exists. Do you want to overwrite it?") setMessage("A download for this episode already exists. Do you want to overwrite it?")
.setPositiveButton("Yes") { _, _ -> setPosButton(R.string.yes) {
PrefManager.getAnimeDownloadPreferences().edit() PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName()) .remove(animeDownloadTask.getTaskName())
.apply() .apply()
downloadsManger.removeDownload( downloadsManager.removeDownload(
DownloadedType( DownloadedType(
title, title,
episode, episode,
@ -99,8 +98,9 @@ object Helper {
} }
} }
} }
.setNegativeButton("No") { _, _ -> } setNegButton(R.string.no)
.show() show()
}
} else { } else {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask) AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) { if (!AnimeServiceDataSingleton.isServiceRunning) {

View file

@ -38,6 +38,7 @@ import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -289,15 +290,20 @@ class AnimeFragment : Fragment() {
} }
} }
} }
model.loaded = true }
model.loadTrending(1) model.loaded = true
model.loadAll() val loadTrending = async(Dispatchers.IO) { model.loadTrending(1) }
val loadAll = async(Dispatchers.IO) { model.loadAll() }
val loadPopular = async(Dispatchers.IO) {
model.loadPopular( model.loadPopular(
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal( "ANIME",
PrefName.PopularAnimeList sort = Anilist.sortBy[1],
) onList = PrefManager.getVal(PrefName.PopularAnimeList)
) )
} }
loadTrending.await()
loadAll.await()
loadPopular.await()
live.postValue(false) live.postValue(false)
_binding?.animeRefresh?.isRefreshing = false _binding?.animeRefresh?.isRefreshing = false
running = false running = false

View file

@ -111,8 +111,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
trendingBinding.searchBar.performClick() trendingBinding.searchBar.performClick()
} }
trendingBinding.notificationCount.visibility = trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE && PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
listOf( listOf(
@ -268,8 +268,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
LinearLayoutManager.HORIZONTAL, LinearLayoutManager.HORIZONTAL,
false false
) )
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
more.setOnClickListener { more.setOnClickListener {
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
ContextCompat.startActivity( ContextCompat.startActivity(
it.context, Intent(it.context, MediaListViewActivity::class.java) it.context, Intent(it.context, MediaListViewActivity::class.java)
.putExtra("title", string), .putExtra("title", string),
@ -294,8 +295,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
fun updateNotificationCount() { fun updateNotificationCount() {
if (this::binding.isInitialized) { if (this::binding.isInitialized) {
trendingBinding.notificationCount.visibility = trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE && PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
} }
} }

View file

@ -50,6 +50,7 @@ import ani.dantotsu.statusBarHeight
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.max import kotlin.math.max
@ -92,6 +93,7 @@ class HomeFragment : Fragment() {
) )
binding.homeUserDataProgressBar.visibility = View.GONE binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0 binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString() binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener { binding.homeAnimeList.setOnClickListener {
@ -456,51 +458,56 @@ class HomeFragment : Fragment() {
var running = false var running = false
val live = Refresh.activity.getOrPut(1) { MutableLiveData(true) } val live = Refresh.activity.getOrPut(1) { MutableLiveData(true) }
live.observe(viewLifecycleOwner) live.observe(viewLifecycleOwner) { shouldRefresh ->
{ if (!running && shouldRefresh) {
if (!running && it) {
running = true running = true
scope.launch { scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
//Get userData First // Get user data first
Anilist.userid = Anilist.userid = PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)?.toIntOrNull()
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull()
if (Anilist.userid == null) { if (Anilist.userid == null) {
getUserId(requireContext()) { withContext(Dispatchers.Main) {
load()
}
} else {
CoroutineScope(Dispatchers.IO).launch {
getUserId(requireContext()) { getUserId(requireContext()) {
load() load()
} }
} }
} else {
getUserId(requireContext()) {
load()
}
} }
model.loaded = true model.loaded = true
CoroutineScope(Dispatchers.IO).launch { model.setListImages()
model.setListImages() }
}
var empty = true var empty = true
val homeLayoutShow: List<Boolean> = val homeLayoutShow: List<Boolean> = PrefManager.getVal(PrefName.HomeLayout)
PrefManager.getVal(PrefName.HomeLayout)
model.initHomePage() withContext(Dispatchers.Main) {
(array.indices).forEach { i -> homeLayoutShow.indices.forEach { i ->
if (homeLayoutShow.elementAt(i)) { if (homeLayoutShow.elementAt(i)) {
empty = false empty = false
} else withContext(Dispatchers.Main) { } else {
containers[i].visibility = View.GONE 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) live.postValue(false)
_binding?.homeRefresh?.isRefreshing = false _binding?.homeRefresh?.isRefreshing = false
running = false running = false
} }
binding.homeHiddenItemsContainer.visibility = View.GONE
} }
} }
} }
@ -508,6 +515,7 @@ class HomeFragment : Fragment() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true) if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) { if (_binding != null) {
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0 binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString() binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
} }
super.onResume() super.onResume()

View file

@ -12,12 +12,14 @@ import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.DialogUserAgentBinding
import ani.dantotsu.databinding.FragmentLoginBinding import ani.dantotsu.databinding.FragmentLoginBinding
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.internal.PreferenceKeystore import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
class LoginFragment : Fragment() { class LoginFragment : Fragment() {
@ -94,38 +96,31 @@ class LoginFragment : Fragment() {
val password = CharArray(16).apply { fill('0') } val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout // Inflate the dialog layout
val dialogView = val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
LayoutInflater.from(requireActivity()).inflate(R.layout.dialog_user_agent, null) userAgentTextBox.hint = "Password"
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password" subtitle.visibility = View.VISIBLE
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle) subtitle.text = getString(R.string.enter_password_to_decrypt_file)
subtitleTextView?.visibility = View.VISIBLE }
subtitleTextView?.text = "Enter your password to decrypt the file"
val dialog = AlertDialog.Builder(requireActivity(), R.style.MyPopup) requireActivity().customAlertDialog().apply {
.setTitle("Enter Password") setTitle("Enter Password")
.setView(dialogView) setCustomView(dialogView.root)
.setPositiveButton("OK", null) setPosButton(R.string.ok){
.setNegativeButton("Cancel") { dialog, _ -> 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') password.fill('0')
dialog.dismiss()
callback(null) 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<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
dialog.dismiss()
callback(password)
} else {
toast("Password cannot be empty")
}
}
} }
private fun restartApp() { private fun restartApp() {

View file

@ -35,6 +35,7 @@ import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -274,15 +275,22 @@ class MangaFragment : Fragment() {
} }
} }
} }
model.loaded = true }
model.loadTrending() model.loaded = true
model.loadAll() val loadTrending = async(Dispatchers.IO) { model.loadTrending() }
val loadAll = async(Dispatchers.IO) { model.loadAll() }
val loadPopular = async(Dispatchers.IO) {
model.loadPopular( model.loadPopular(
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal( "MANGA",
PrefName.PopularMangaList sort = Anilist.sortBy[1],
) onList = PrefManager.getVal(PrefName.PopularAnimeList)
) )
} }
loadTrending.await()
loadAll.await()
loadPopular.await()
live.postValue(false) live.postValue(false)
_binding?.mangaRefresh?.isRefreshing = false _binding?.mangaRefresh?.isRefreshing = false
running = false running = false

View file

@ -80,6 +80,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
updateAvatar() updateAvatar()
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0 trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
trendingBinding.searchBar.hint = "MANGA" trendingBinding.searchBar.hint = "MANGA"
trendingBinding.searchBarText.setOnClickListener { trendingBinding.searchBarText.setOnClickListener {
@ -296,8 +297,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
fun updateNotificationCount() { fun updateNotificationCount() {
if (this::binding.isInitialized) { if (this::binding.isInitialized) {
trendingBinding.notificationCount.visibility = trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE && PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
} }
} }

View file

@ -16,6 +16,8 @@ import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.User import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
class StatusActivity : AppCompatActivity(), StoriesCallback { class StatusActivity : AppCompatActivity(), StoriesCallback {
private lateinit var activity: ArrayList<User> private lateinit var activity: ArrayList<User>
@ -44,10 +46,17 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
val key = "activities" val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf()) val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity ) if (activity.getOrNull(position) != null) {
val startIndex = if ( startFrom > 0) startFrom else 0 val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1) 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<Int>, activity: List<Activity>): Int { private fun findFirstNonMatch(watchedActivity: Set<Int>, activity: List<Activity>): Int {
@ -58,13 +67,16 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
} }
return -1 return -1
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
binding.stories.pause() binding.stories.pause()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.stories.resume() if (hasWindowFocus())
binding.stories.resume()
} }
override fun onWindowFocusChanged(hasFocus: Boolean) { override fun onWindowFocusChanged(hasFocus: Boolean) {
@ -83,7 +95,7 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity ) val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
val startIndex= if ( startFrom > 0) startFrom else 0 val startIndex= if ( startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutLeft) 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) binding.stories.startAnimation(slideInRight)
} else { } else {
finish() finish()
@ -92,13 +104,13 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
override fun onStoriesStart() { override fun onStoriesStart() {
position -= 1 position -= 1
if (position >= 0) { if (position >= 0 && activity[position].activity.isNotEmpty()) {
val key = "activities" val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf()) val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity ) val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
val startIndex = if ( startFrom > 0) startFrom else 0 val startIndex = if ( startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutRight) 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) binding.stories.startAnimation(slideInLeft)
} else { } else {
finish() finish()

View file

@ -30,6 +30,7 @@ import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User import ani.dantotsu.profile.User
import ani.dantotsu.profile.UsersDialogFragment import ani.dantotsu.profile.UsersDialogFragment
import ani.dantotsu.profile.activity.ActivityItemBuilder import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.profile.activity.RepliesBottomDialog
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
@ -48,7 +49,6 @@ import kotlin.math.abs
class Stories @JvmOverloads constructor( class Stories @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener { ) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener {
private lateinit var activity: FragmentActivity
private lateinit var binding: FragmentStatusBinding private lateinit var binding: FragmentStatusBinding
private lateinit var activityList: List<Activity> private lateinit var activityList: List<Activity>
private lateinit var storiesListener: StoriesCallback private lateinit var storiesListener: StoriesCallback
@ -74,16 +74,14 @@ class Stories @JvmOverloads constructor(
if (context is StoriesCallback) storiesListener = context as StoriesCallback if (context is StoriesCallback) storiesListener = context as StoriesCallback
binding.leftTouchPanel.setOnTouchListener(this) binding.touchPanel.setOnTouchListener(this)
binding.rightTouchPanel.setOnTouchListener(this)
} }
fun setStoriesList( fun setStoriesList(
activityList: List<Activity>, activity: FragmentActivity, startIndex: Int = 1 activityList: List<Activity>, startIndex: Int = 1
) { ) {
this.activityList = activityList this.activityList = activityList
this.activity = activity
this.storyIndex = startIndex this.storyIndex = startIndex
addLoadingViews(activityList) 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() { private fun rightPanelTouch() {
Logger.log("rightPanelTouch: $storyIndex") Logger.log("rightPanelTouch: $storyIndex")
@ -359,6 +315,7 @@ class Stories @JvmOverloads constructor(
timer.resume() timer.resume()
} }
@SuppressLint("ClickableViewAccessibility")
private fun loadStory(story: Activity) { private fun loadStory(story: Activity) {
val key = "activities" val key = "activities"
val set = PrefManager.getCustomVal<Set<Int>>(key, setOf()).plus((story.id)) val set = PrefManager.getCustomVal<Set<Int>>(key, setOf()).plus((story.id))
@ -374,6 +331,15 @@ class Stories @JvmOverloads constructor(
null 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) { fun visible(isList: Boolean) {
binding.textActivity.isVisible = !isList binding.textActivity.isVisible = !isList
binding.textActivityContainer.isVisible = !isList binding.textActivityContainer.isVisible = !isList
@ -397,15 +363,17 @@ class Stories @JvmOverloads constructor(
} }
} }
} ${story.progress ?: story.media?.title?.userPreferred} " + } ${story.progress ?: story.media?.title?.userPreferred} " +
if ( if (
story.status?.contains("completed") == false && story.status?.contains("completed") == false &&
!story.status.contains("plans") && !story.status.contains("plans") &&
!story.status.contains("repeating") !story.status.contains("repeating")&&
) { !story.status.contains("paused")&&
"of ${story.media?.title?.userPreferred}" !story.status.contains("dropped")
} else { ) {
"" "of ${story.media?.title?.userPreferred}"
} } else {
""
}
binding.infoText.text = text binding.infoText.text = text
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations) val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage( blurImage(
@ -421,7 +389,7 @@ class Stories @JvmOverloads constructor(
story.media?.id story.media?.id
), ),
ActivityOptionsCompat.makeSceneTransitionAnimation( ActivityOptionsCompat.makeSceneTransitionAnimation(
activity, (it.context as FragmentActivity),
binding.coverImage, binding.coverImage,
ViewCompat.getTransitionName(binding.coverImage)!! ViewCompat.getTransitionName(binding.coverImage)!!
).toBundle() ).toBundle()
@ -455,22 +423,21 @@ class Stories @JvmOverloads constructor(
} }
val likeColor = ContextCompat.getColor(context, R.color.yt_red) val likeColor = ContextCompat.getColor(context, R.color.yt_red)
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp) 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 { binding.activityRepliesContainer.setOnClickListener {
RepliesBottomDialog.newInstance(story.id) 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.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
binding.replyCount.text = story.replyCount.toString()
binding.activityLikeCount.text = story.likeCount.toString() binding.activityLikeCount.text = story.likeCount.toString()
binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp))
binding.activityLikeContainer.setOnClickListener { binding.activityLikeContainer.setOnClickListener {
like() like()
} }
binding.activityLikeContainer.setOnLongClickListener { binding.activityLikeContainer.setOnLongClickListener {
val context = activity
UsersDialogFragment().apply { UsersDialogFragment().apply {
userList(userList) userList(userList)
show(context.supportFragmentManager, "dialog") show((it.context as FragmentActivity).supportFragmentManager, "dialog")
} }
true true
} }
@ -484,7 +451,7 @@ class Stories @JvmOverloads constructor(
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp) val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch { scope.launch {
val res = Anilist.query.toggleLike(story.id, "ACTIVITY") val res = Anilist.mutation.toggleLike(story.id, "ACTIVITY")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (res != null) { if (res != null) {
if (story.isLiked == true) { 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()
}
}
}
}
} }

View file

@ -1,6 +1,5 @@
package ani.dantotsu.home.status package ani.dantotsu.home.status
import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
@ -15,6 +14,8 @@ import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User import ani.dantotsu.profile.User
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.util.ActivityMarkdownCreator
class UserStatusAdapter(private val user: ArrayList<User>) : class UserStatusAdapter(private val user: ArrayList<User>) :
RecyclerView.Adapter<UserStatusAdapter.UsersViewHolder>() { RecyclerView.Adapter<UserStatusAdapter.UsersViewHolder>() {
@ -23,6 +24,10 @@ class UserStatusAdapter(private val user: ArrayList<User>) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (user[bindingAdapterPosition].activity.isEmpty()) {
snackString("No activity")
return@setOnClickListener
}
StatusActivity.user = user StatusActivity.user = user
ContextCompat.startActivity( ContextCompat.startActivity(
itemView.context, itemView.context,
@ -34,14 +39,23 @@ class UserStatusAdapter(private val user: ArrayList<User>) :
) )
} }
itemView.setOnLongClickListener { itemView.setOnLongClickListener {
ContextCompat.startActivity( if (user[bindingAdapterPosition].id == Anilist.userid) {
itemView.context, ContextCompat.startActivity(
Intent(
itemView.context, itemView.context,
ProfileActivity::class.java Intent(itemView.context, ActivityMarkdownCreator::class.java)
).putExtra("userId", user[bindingAdapterPosition].id), .putExtra("type", "activity"),
null null
) )
}else{
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
ProfileActivity::class.java
).putExtra("userId", user[bindingAdapterPosition].id),
null
)
}
true true
} }
} }

View file

@ -30,6 +30,7 @@ class CalendarActivity : AppCompatActivity() {
private lateinit var binding: ActivityListBinding private lateinit var binding: ActivityListBinding
private val scope = lifecycleScope private val scope = lifecycleScope
private var selectedTabIdx = 1 private var selectedTabIdx = 1
private var showOnlyLibrary = false
private val model: OtherDetailsViewModel by viewModels() private val model: OtherDetailsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -38,8 +39,6 @@ class CalendarActivity : AppCompatActivity() {
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityListBinding.inflate(layoutInflater) binding = ActivityListBinding.inflate(layoutInflater)
val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface) val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface)
val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary) val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline) val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline)
@ -79,6 +78,17 @@ class CalendarActivity : AppCompatActivity() {
override fun onTabReselected(tab: TabLayout.Tab?) {} 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) { model.getCalendar().observe(this) {
if (it != null) { if (it != null) {
binding.listProgressBar.visibility = View.GONE binding.listProgressBar.visibility = View.GONE
@ -97,11 +107,10 @@ class CalendarActivity : AppCompatActivity() {
live.observe(this) { live.observe(this) {
if (it) { if (it) {
scope.launch { scope.launch {
withContext(Dispatchers.IO) { model.loadCalendar() } withContext(Dispatchers.IO) { model.loadCalendar(showOnlyLibrary) }
live.postValue(false) live.postValue(false)
} }
} }
} }
} }
} }

View file

@ -9,6 +9,7 @@ import androidx.core.content.ContextCompat
import androidx.core.util.Pair import androidx.core.util.Pair
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemCharacterBinding import ani.dantotsu.databinding.ItemCharacterBinding
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
@ -55,6 +56,7 @@ class CharacterAdapter(
).toBundle() ).toBundle()
) )
} }
itemView.setOnLongClickListener { copyToClipboard(characterList[bindingAdapterPosition].name ?: ""); true }
} }
} }
} }

View file

@ -1,14 +1,22 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.graphics.Bitmap import android.graphics.Bitmap
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.MediaEdge import ani.dantotsu.connections.anilist.api.MediaEdge
import ani.dantotsu.connections.anilist.api.MediaList 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.MediaType
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.media.anime.Anime import ani.dantotsu.media.anime.Anime
import ani.dantotsu.media.manga.Manga import ani.dantotsu.media.manga.Manga
import ani.dantotsu.profile.User 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 java.io.Serializable
import ani.dantotsu.connections.anilist.api.Media as ApiMedia import ani.dantotsu.connections.anilist.api.Media as ApiMedia
@ -76,7 +84,7 @@ data class Media(
var nameMAL: String? = null, var nameMAL: String? = null,
var shareLink: String? = null, var shareLink: String? = null,
var selected: Selected? = null, var selected: Selected? = null,
var streamingEpisodes: List<MediaStreamingEpisode>? = null,
var idKitsu: String? = null, var idKitsu: String? = null,
var cameFromContinue: Boolean = false var cameFromContinue: Boolean = false
@ -129,6 +137,37 @@ data class Media(
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji 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<Int>())
PrefManager.setCustomVal(
"removeList", removeList.minus(listId)
)
onSuccess()
} catch (e: Exception) {
onError(e)
}
} ?: onNotFound()
}
}
}
}
fun emptyMedia() = Media( fun emptyMedia() = Media(
id = 0, id = 0,
name = "No media found", name = "No media found",

View file

@ -293,7 +293,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaTotal.visibility = View.VISIBLE binding.mediaTotal.visibility = View.VISIBLE
binding.mediaAddToList.text = userStatus binding.mediaAddToList.text = userStatus
} else { } else {
binding.mediaAddToList.setText(R.string.add) binding.mediaAddToList.setText(R.string.add_list)
} }
total() total()
binding.mediaAddToList.setOnClickListener { 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.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
navBar.addTab(infoTab) navBar.addTab(infoTab)
navBar.addTab(watchTab) navBar.addTab(watchTab)
navBar.addTab(commentTab) if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1) {
navBar.addTab(commentTab)
}
if (model.continueMedia == null && media.cameFromContinue) { if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia) model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
selected = 1 selected = 1
@ -424,7 +426,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} }
override fun onResume() { override fun onResume() {
navBar.selectTabAt(selected) if (::navBar.isInitialized)
navBar.selectTabAt(selected)
super.onResume() super.onResume()
} }

View file

@ -13,6 +13,7 @@ import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.AniSkip import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.Anify
import ani.dantotsu.others.Jikan import ani.dantotsu.others.Jikan
import ani.dantotsu.others.Kitsu import ani.dantotsu.others.Kitsu
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
@ -99,6 +100,15 @@ class MediaDetailsViewModel : ViewModel() {
if (kitsuEpisodes.value == null) kitsuEpisodes.postValue(Kitsu.getKitsuEpisodesDetails(s)) if (kitsuEpisodes.value == null) kitsuEpisodes.postValue(Kitsu.getKitsuEpisodesDetails(s))
} }
} }
private val anifyEpisodes: MutableLiveData<Map<String, Episode>> =
MutableLiveData<Map<String, Episode>>(null)
fun getAnifyEpisodes(): LiveData<Map<String, Episode>> = anifyEpisodes
suspend fun loadAnifyEpisodes(s: Int) {
tryWithSuspend {
if (anifyEpisodes.value == null) anifyEpisodes.postValue(Anify.fetchAndParseMetadata(s))
}
}
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> = private val fillerEpisodes: MutableLiveData<Map<String, Episode>> =
MutableLiveData<Map<String, Episode>>(null) MutableLiveData<Map<String, Episode>>(null)

View file

@ -105,8 +105,8 @@ class MediaInfoFragment : Fragment() {
} }
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility = if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
View.VISIBLE View.VISIBLE
val infoNameRomanji = tripleTab + media.nameRomaji val infoNameRomaji = tripleTab + media.nameRomaji
binding.mediaInfoNameRomaji.text = infoNameRomanji binding.mediaInfoNameRomaji.text = infoNameRomaji
binding.mediaInfoNameRomaji.setOnLongClickListener { binding.mediaInfoNameRomaji.setOnLongClickListener {
copyToClipboard(media.nameRomaji) copyToClipboard(media.nameRomaji)
true true

View file

@ -271,29 +271,23 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
} }
binding.mediaListDelete.setOnClickListener { binding.mediaListDelete.setOnClickListener {
var id = media!!.userListId
scope.launch { scope.launch {
withContext(Dispatchers.IO) { media?.deleteFromList(scope, onSuccess = {
if (id != null) { Refresh.all()
Anilist.mutation.deleteList(id!!) snackString(getString(R.string.deleted_from_list))
MAL.query.deleteList(media?.anime != null, media?.idMAL) dismissAllowingStateLoss()
} else { }, onError = { e ->
val profile = Anilist.query.userMediaDetails(media!!) withContext(Dispatchers.Main) {
profile.userListId?.let { listId -> snackString(
id = listId getString(
Anilist.mutation.deleteList(listId) R.string.delete_fail_reason, e.message
MAL.query.deleteList(media?.anime != null, media?.idMAL) )
} )
} }
} }, onNotFound = {
PrefManager.setCustomVal("removeList", removeList.minus(media?.id)) snackString(getString(R.string.no_list_id))
} })
if (id != null) {
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} else {
snackString(getString(R.string.no_list_id))
} }
} }
} }

View file

@ -63,36 +63,24 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight } binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = viewLifecycleOwner.lifecycleScope val scope = viewLifecycleOwner.lifecycleScope
binding.mediaListDelete.setOnClickListener { binding.mediaListDelete.setOnClickListener {
var id = media.userListId
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) { scope.launch {
if (id != null) { media.deleteFromList(scope, onSuccess = {
try {
Anilist.mutation.deleteList(id!!)
MAL.query.deleteList(media.anime != null, media.idMAL)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
snackString(getString(R.string.delete_fail_reason, e.message))
}
return@withContext
}
} else {
val profile = Anilist.query.userMediaDetails(media)
profile.userListId?.let { listId ->
id = listId
Anilist.mutation.deleteList(listId)
MAL.query.deleteList(media.anime != null, media.idMAL)
}
}
}
withContext(Dispatchers.Main) {
if (id != null) {
Refresh.all() Refresh.all()
snackString(getString(R.string.deleted_from_list)) snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss() dismissAllowingStateLoss()
} else { }, onError = { e ->
withContext(Dispatchers.Main) {
snackString(
getString(
R.string.delete_fail_reason, e.message
)
)
}
}, onNotFound = {
snackString(getString(R.string.no_list_id)) snackString(getString(R.string.no_list_id))
} })
} }
} }
} }

View file

@ -26,25 +26,50 @@ class OtherDetailsViewModel : ViewModel() {
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m)) if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
} }
private var cachedAllCalendarData: Map<String, MutableList<Media>>? = null
private var cachedLibraryCalendarData: Map<String, MutableList<Media>>? = null
private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null) private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
suspend fun loadCalendar() { suspend fun loadCalendar(showOnlyLibrary: Boolean = false) {
val curr = System.currentTimeMillis() / 1000 if (cachedAllCalendarData == null || cachedLibraryCalendarData == null) {
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6)) val curr = System.currentTimeMillis() / 1000
val df = DateFormat.getDateInstance(DateFormat.FULL) val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
val map = mutableMapOf<String, MutableList<Media>>() val df = DateFormat.getDateInstance(DateFormat.FULL)
val idMap = mutableMapOf<String, MutableList<Int>>() val allMap = mutableMapOf<String, MutableList<Media>>()
res?.forEach { val libraryMap = mutableMapOf<String, MutableList<Media>>()
val v = it.relation?.split(",")?.map { i -> i.toLong() }!! val idMap = mutableMapOf<String, MutableList<Int>>()
val dateInfo = df.format(Date(v[1] * 1000))
val list = map.getOrPut(dateInfo) { mutableListOf() } val userId = Anilist.userid ?: 0
val idList = idMap.getOrPut(dateInfo) { mutableListOf() } val userLibrary = Anilist.query.getMediaLists(true, userId)
it.relation = "Episode ${v[0]}" val libraryMediaIds = userLibrary.flatMap { it.value }.map { it.id }
if (!idList.contains(it.id)) {
idList.add(it.id) res.forEach {
list.add(it) val v = it.relation?.split(",")?.map { i -> i.toLong() }!!
val dateInfo = df.format(Date(v[1] * 1000))
val list = allMap.getOrPut(dateInfo) { mutableListOf() }
val libraryList = if (libraryMediaIds.contains(it.id)) {
libraryMap.getOrPut(dateInfo) { mutableListOf() }
} else {
null
}
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
it.relation = "Episode ${v[0]}"
if (!idList.contains(it.id)) {
idList.add(it.id)
list.add(it)
libraryList?.add(it)
}
} }
cachedAllCalendarData = allMap
cachedLibraryCalendarData = libraryMap
} }
calendar.postValue(map)
val cacheToUse: Map<String, MutableList<Media>> = if (showOnlyLibrary) {
cachedLibraryCalendarData ?: emptyMap()
} else {
cachedAllCalendarData ?: emptyMap()
}
calendar.postValue(cacheToUse)
} }
} }

View file

@ -3,7 +3,6 @@ package ani.dantotsu.media
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.SpannableString
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -21,7 +20,7 @@ import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.MarkdownCreatorActivity import ani.dantotsu.util.ActivityMarkdownCreator
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -59,7 +58,7 @@ class ReviewActivity : AppCompatActivity() {
binding.followFilterButton.setOnClickListener { binding.followFilterButton.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
this, this,
Intent(this, MarkdownCreatorActivity::class.java) Intent(this, ActivityMarkdownCreator::class.java)
.putExtra("type", "review"), .putExtra("type", "review"),
null null
) )

View file

@ -183,6 +183,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchByImage.setOnClickListener { binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java)) activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
} }
binding.clearHistory.setOnClickListener {
it.startAnimation(fadeOutAnimation())
it.visibility = View.GONE
searchHistoryAdapter.clearHistory()
}
updateClearHistoryVisibility()
fun searchTitle() { fun searchTitle() {
activity.result.apply { activity.result.apply {
search = search =
@ -300,11 +306,17 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
} }
binding.searchResultLayout.visibility = View.VISIBLE binding.searchResultLayout.visibility = View.VISIBLE
binding.clearHistory.visibility = View.GONE
binding.searchHistoryList.visibility = View.GONE binding.searchHistoryList.visibility = View.GONE
binding.searchByImage.visibility = View.GONE binding.searchByImage.visibility = View.GONE
} }
} }
private fun updateClearHistoryVisibility() {
binding.clearHistory.visibility =
if (searchHistoryAdapter.itemCount > 0) View.VISIBLE else View.GONE
}
private fun fadeInAnimation(): Animation { private fun fadeInAnimation(): Animation {
return AlphaAnimation(0f, 1f).apply { return AlphaAnimation(0f, 1f).apply {
duration = 150 duration = 150
@ -375,4 +387,3 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
override fun getItemCount(): Int = chips.size override fun getItemCount(): Int = chips.size
} }
} }

View file

@ -49,6 +49,12 @@ class SearchHistoryAdapter(private val type: String, private val searchClicked:
PrefManager.setVal(historyType, searchHistory) PrefManager.setVal(historyType, searchHistory)
} }
fun clearHistory() {
searchHistory?.clear()
PrefManager.setVal(historyType, searchHistory)
submitList(searchHistory?.toList())
}
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int

View file

@ -26,4 +26,5 @@ data class Anime(
var slug: String? = null, var slug: String? = null,
var kitsuEpisodes: Map<String, Episode>? = null, var kitsuEpisodes: Map<String, Episode>? = null,
var fillerEpisodes: Map<String, Episode>? = null, var fillerEpisodes: Map<String, Episode>? = null,
var anifyEpisodes: Map<String, Episode>? = null,
) : Serializable ) : Serializable

View file

@ -8,8 +8,8 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getString
import androidx.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -18,8 +18,9 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.FileUrl import ani.dantotsu.FileUrl
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.currActivity import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.databinding.DialogLayoutBinding import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemMediaSourceBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.displayTimer import ani.dantotsu.displayTimer
import ani.dantotsu.isOnline import ani.dantotsu.isOnline
@ -33,12 +34,15 @@ import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.OfflineAnimeParser
import ani.dantotsu.parsers.WatchSources import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.settings.FAQActivity import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
@ -54,16 +58,13 @@ class AnimeWatchAdapter(
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() { ) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
private var autoSelect = true private var autoSelect = true
var subscribe: MediaDetailsActivity.PopImageButton? = null var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null private var _binding: ItemMediaSourceBinding? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false) val bind = ItemMediaSourceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(bind) return ViewHolder(bind)
} }
private var nestedDialog: AlertDialog? = null
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
_binding = binding _binding = binding
@ -75,7 +76,7 @@ class AnimeWatchAdapter(
null null
) )
} }
//Youtube // Youtube
if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) { if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
binding.animeSourceYT.visibility = View.VISIBLE binding.animeSourceYT.visibility = View.VISIBLE
binding.animeSourceYT.setOnClickListener { binding.animeSourceYT.setOnClickListener {
@ -89,7 +90,7 @@ class AnimeWatchAdapter(
R.string.subbed R.string.subbed
) )
//PreferDub // PreferDub
var changing = false var changing = false
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked -> binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
binding.animeSourceDubbedText.text = binding.animeSourceDubbedText.text =
@ -99,8 +100,8 @@ class AnimeWatchAdapter(
if (!changing) fragment.onDubClicked(isChecked) if (!changing) fragment.onDubClicked(isChecked)
} }
//Wrong Title // Wrong Title
binding.animeSourceSearch.setOnClickListener { binding.mediaSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show( SourceSearchDialogFragment().show(
fragment.requireActivity().supportFragmentManager, fragment.requireActivity().supportFragmentManager,
null null
@ -108,37 +109,37 @@ class AnimeWatchAdapter(
} }
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode) val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
binding.animeSourceNameContainer.isGone = offline binding.mediaSourceNameContainer.isGone = offline
binding.animeSourceSettings.isGone = offline binding.mediaSourceSettings.isGone = offline
binding.animeSourceSearch.isGone = offline binding.mediaSourceSearch.isGone = offline
binding.animeSourceTitle.isGone = offline binding.mediaSourceTitle.isGone = offline
//Source Selection // Source Selection
var source = var source =
media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it } media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
setLanguageList(media.selected!!.langIndex, source) setLanguageList(media.selected!!.langIndex, source)
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) { if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
binding.animeSource.setText(watchSources.names[source]) binding.mediaSource.setText(watchSources.names[source])
watchSources[source].apply { watchSources[source].apply {
this.selectDub = media.selected!!.preferDub this.selectDub = media.selected!!.preferDub
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately() binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
} }
} }
binding.animeSource.setAdapter( binding.mediaSource.setAdapter(
ArrayAdapter( ArrayAdapter(
fragment.requireContext(), fragment.requireContext(),
R.layout.item_dropdown, R.layout.item_dropdown,
watchSources.names watchSources.names
) )
) )
binding.animeSourceTitle.isSelected = true binding.mediaSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ -> binding.mediaSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply { fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
changing = true changing = true
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
@ -150,15 +151,15 @@ class AnimeWatchAdapter(
fragment.loadEpisodes(i, false) fragment.loadEpisodes(i, false)
} }
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ -> binding.mediaSourceLanguage.setOnItemClickListener { _, _, i, _ ->
// Check if 'extension' and 'selected' properties exist and are accessible // Check if 'extension' and 'selected' properties exist and are accessible
(watchSources[source] as? DynamicAnimeParser)?.let { ext -> (watchSources[source] as? DynamicAnimeParser)?.let { ext ->
ext.sourceLanguage = i ext.sourceLanguage = i
fragment.onLangChange(i) fragment.onLangChange(i)
fragment.onSourceChange(media.selected!!.sourceIndex).apply { fragment.onSourceChange(media.selected!!.sourceIndex).apply {
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } } { MainScope().launch { binding.mediaSourceTitle.text = it } }
changing = true changing = true
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
@ -170,19 +171,19 @@ class AnimeWatchAdapter(
} ?: run { } } ?: run { }
} }
//settings // Settings
binding.animeSourceSettings.setOnClickListener { binding.mediaSourceSettings.setOnClickListener {
(watchSources[source] as? DynamicAnimeParser)?.let { ext -> (watchSources[source] as? DynamicAnimeParser)?.let { ext ->
fragment.openSettings(ext.extension) fragment.openSettings(ext.extension)
} }
} }
//Icons // Icons
//subscribe // Subscribe
subscribe = MediaDetailsActivity.PopImageButton( subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope, fragment.lifecycleScope,
binding.animeSourceSubscribe, binding.mediaSourceSubscribe,
R.drawable.ic_round_notifications_active_24, R.drawable.ic_round_notifications_active_24,
R.drawable.ic_round_notifications_none_24, R.drawable.ic_round_notifications_none_24,
R.color.bg_opp, R.color.bg_opp,
@ -190,125 +191,164 @@ class AnimeWatchAdapter(
fragment.subscribed, fragment.subscribed,
true true
) { ) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString()) fragment.onNotificationPressed(it, binding.mediaSource.text.toString())
} }
subscribeButton(false) subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener { binding.mediaSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK) openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
} }
//Nested Button // Nested Button
binding.animeNestedButton.setOnClickListener { binding.mediaNestedButton.setOnClickListener {
val dialogView = val dialogBinding = DialogLayoutBinding.inflate(fragment.layoutInflater)
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null) dialogBinding.apply {
val dialogBinding = DialogLayoutBinding.bind(dialogView) var refresh = false
var refresh = false var run = false
var run = false var reversed = media.selected!!.recyclerReversed
var reversed = media.selected!!.recyclerReversed var style =
var style = media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f mediaSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down" sortText.text = if (reversed) "Down to Up" else "Up to Down"
dialogBinding.animeSourceTop.setOnClickListener { mediaSourceTop.setOnClickListener {
reversed = !reversed reversed = !reversed
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f mediaSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down" sortText.text = if (reversed) "Down to Up" else "Up to Down"
run = true run = true
}
//Grids
var selected = when (style) {
0 -> dialogBinding.animeSourceList
1 -> dialogBinding.animeSourceGrid
2 -> dialogBinding.animeSourceCompact
else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.setText(R.string.list)
1 -> dialogBinding.layoutText.setText(R.string.grid)
2 -> dialogBinding.layoutText.setText(R.string.compact)
else -> dialogBinding.animeSourceList
}
selected.alpha = 1f
fun selected(it: ImageButton) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
dialogBinding.layoutText.setText(R.string.list)
run = true
}
dialogBinding.animeSourceGrid.setOnClickListener {
selected(it as ImageButton)
style = 1
dialogBinding.layoutText.setText(R.string.grid)
run = true
}
dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 2
dialogBinding.layoutText.setText(R.string.compact)
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast(R.string.webview_not_installed)
} }
//start CookieCatcher activity // Grids
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) { var selected = when (style) {
val sourceAHH = watchSources[source] as? DynamicAnimeParser 0 -> mediaSourceList
val sourceHttp = 1 -> mediaSourceGrid
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource 2 -> mediaSourceCompact
val url = sourceHttp?.baseUrl else -> mediaSourceList
url?.let { }
refresh = true when (style) {
val headersMap = try { 0 -> layoutText.setText(R.string.list)
sourceHttp.headers.toMultimap() 1 -> layoutText.setText(R.string.grid)
.mapValues { it.value.getOrNull(0) ?: "" } 2 -> layoutText.setText(R.string.compact)
} catch (e: Exception) { else -> mediaSourceList
emptyMap() }
selected.alpha = 1f
fun selected(it: ImageButton) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
mediaSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
layoutText.setText(R.string.list)
run = true
}
mediaSourceGrid.setOnClickListener {
selected(it as ImageButton)
style = 1
layoutText.setText(R.string.grid)
run = true
}
mediaSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 2
layoutText.setText(R.string.compact)
run = true
}
mediaWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast(R.string.webview_not_installed)
}
// Start CookieCatcher activity
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
val sourceAHH = watchSources[source] as? DynamicAnimeParser
val sourceHttp =
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource
val url = sourceHttp?.baseUrl
url?.let {
refresh = true
val headersMap = try {
sourceHttp.headers.toMultimap()
.mapValues { it.value.getOrNull(0) ?: "" }
} catch (e: Exception) {
emptyMap()
}
val intent =
Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
.putExtra("headers", headersMap as HashMap<String, String>)
startActivity(fragment.requireContext(), intent, null)
} }
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
.putExtra("headers", headersMap as HashMap<String, String>)
startActivity(fragment.requireContext(), intent, null)
} }
} }
resetProgress.setOnClickListener {
fragment.requireContext().customAlertDialog().apply {
setTitle(" Delete Progress for all episodes of ${media.nameRomaji}")
setMessage("This will delete all the locally stored progress for all episodes")
setPosButton(R.string.ok){
val prefix = "${media.id}_"
val regex = Regex("^${prefix}\\d+$")
PrefManager.getAllCustomValsForMedia(prefix)
.keys
.filter { it.matches(regex) }
.onEach { key -> PrefManager.removeCustomVal(key) }
snackString("Deleted the progress of all Episodes for ${media.nameRomaji}")
}
setNegButton(R.string.no)
show()
}
}
resetProgressDef.text = getString(currContext()!!,R.string.clear_stored_episode)
// Hidden
mangaScanlatorContainer.visibility = View.GONE
animeDownloadContainer.visibility = View.GONE
fragment.requireContext().customAlertDialog().apply {
setTitle("Options")
setCustomView(dialogBinding.root)
setPosButton("OK") {
if (run) fragment.onIconPressed(style, reversed)
if (refresh) fragment.loadEpisodes(source, true)
}
setNegButton("Cancel") {
if (refresh) fragment.loadEpisodes(source, true)
}
show()
}
} }
//hidden
dialogBinding.animeScanlatorContainer.visibility = View.GONE
dialogBinding.animeDownloadContainer.visibility = View.GONE
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
.setTitle("Options")
.setView(dialogView)
.setPositiveButton("OK") { _, _ ->
if (run) fragment.onIconPressed(style, reversed)
if (refresh) fragment.loadEpisodes(source, true)
}
.setNegativeButton("Cancel") { _, _ ->
if (refresh) fragment.loadEpisodes(source, true)
}
.setOnCancelListener {
if (refresh) fragment.loadEpisodes(source, true)
}
.create()
nestedDialog?.show()
} }
//Episode Handling // Episode Handling
handleEpisodes() handleEpisodes()
//clear progress
binding.sourceTitle.setOnLongClickListener {
fragment.requireContext().customAlertDialog().apply {
setTitle(" Delete Progress for all episodes of ${media.nameRomaji}")
setMessage("This will delete all the locally stored progress for all episodes")
setPosButton(R.string.ok){
val prefix = "${media.id}_"
val regex = Regex("^${prefix}\\d+$")
PrefManager.getAllCustomValsForMedia(prefix)
.keys
.filter { it.matches(regex) }
.onEach { key -> PrefManager.removeCustomVal(key) }
snackString("Deleted the progress of all Episodes for ${media.nameRomaji}")
}
setNegButton(R.string.no)
show()
}
true
}
} }
fun subscribeButton(enabled: Boolean) { fun subscribeButton(enabled: Boolean) {
subscribe?.enabled(enabled) subscribe?.enabled(enabled)
} }
//Chips // Chips
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) { fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding val binding = _binding
if (binding != null) { if (binding != null) {
@ -319,13 +359,13 @@ class AnimeWatchAdapter(
val chip = val chip =
ItemChipBinding.inflate( ItemChipBinding.inflate(
LayoutInflater.from(fragment.context), LayoutInflater.from(fragment.context),
binding.animeSourceChipGroup, binding.mediaSourceChipGroup,
false false
).root ).root
chip.isCheckable = true chip.isCheckable = true
fun selected() { fun selected() {
chip.isChecked = true chip.isChecked = true
binding.animeWatchChipScroll.smoothScrollTo( binding.mediaWatchChipScroll.smoothScrollTo(
(chip.left - screenWidth / 2) + (chip.width / 2), (chip.left - screenWidth / 2) + (chip.width / 2),
0 0
) )
@ -344,14 +384,14 @@ class AnimeWatchAdapter(
selected() selected()
fragment.onChipClicked(position, limit * (position), last - 1) fragment.onChipClicked(position, limit * (position), last - 1)
} }
binding.animeSourceChipGroup.addView(chip) binding.mediaSourceChipGroup.addView(chip)
if (selected == position) { if (selected == position) {
selected() selected()
select = chip select = chip
} }
} }
if (select != null) if (select != null)
binding.animeWatchChipScroll.apply { binding.mediaWatchChipScroll.apply {
post { post {
scrollTo( scrollTo(
(select.left - screenWidth / 2) + (select.width / 2), (select.left - screenWidth / 2) + (select.width / 2),
@ -363,7 +403,7 @@ class AnimeWatchAdapter(
} }
fun clearChips() { fun clearChips() {
_binding?.animeSourceChipGroup?.removeAllViews() _binding?.mediaSourceChipGroup?.removeAllViews()
} }
fun handleEpisodes() { fun handleEpisodes() {
@ -379,15 +419,15 @@ class AnimeWatchAdapter(
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString() var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
if (episodes.contains(continueEp)) { if (episodes.contains(continueEp)) {
binding.animeSourceContinue.visibility = View.VISIBLE binding.sourceContinue.visibility = View.VISIBLE
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemMediaProgressCont,
binding.itemEpisodeProgress, binding.itemMediaProgress,
binding.itemEpisodeProgressEmpty, binding.itemMediaProgressEmpty,
media.id, media.id,
continueEp continueEp
) )
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > PrefManager.getVal<Float>( if ((binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams).weight > PrefManager.getVal<Float>(
PrefName.WatchPercentage PrefName.WatchPercentage
) )
) { ) {
@ -395,9 +435,9 @@ class AnimeWatchAdapter(
if (e != -1 && e + 1 < episodes.size) { if (e != -1 && e + 1 < episodes.size) {
continueEp = episodes[e + 1] continueEp = episodes[e + 1]
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemMediaProgressCont,
binding.itemEpisodeProgress, binding.itemMediaProgress,
binding.itemEpisodeProgressEmpty, binding.itemMediaProgressEmpty,
media.id, media.id,
continueEp continueEp
) )
@ -407,51 +447,63 @@ class AnimeWatchAdapter(
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) } val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
binding.itemEpisodeImage.loadImage( binding.itemMediaImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0 ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
) )
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text = binding.mediaSourceContinueText.text =
currActivity()!!.getString( currActivity()!!.getString(
R.string.continue_episode, ep.number, if (ep.filler) R.string.continue_episode, ep.number, if (ep.filler)
currActivity()!!.getString(R.string.filler_tag) currActivity()!!.getString(R.string.filler_tag)
else else
"", cleanedTitle "", cleanedTitle
) )
binding.animeSourceContinue.setOnClickListener { binding.sourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp) fragment.onEpisodeClick(continueEp)
} }
if (fragment.continueEp) { if (fragment.continueEp) {
if ( if (
(binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams) (binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams)
.weight < PrefManager.getVal<Float>(PrefName.WatchPercentage) .weight < PrefManager.getVal<Float>(PrefName.WatchPercentage)
) { ) {
binding.animeSourceContinue.performClick() binding.sourceContinue.performClick()
fragment.continueEp = false fragment.continueEp = false
} }
} }
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.sourceContinue.visibility = View.GONE
} }
binding.animeSourceProgressBar.visibility = View.GONE binding.sourceProgressBar.visibility = View.GONE
val sourceFound = media.anime.episodes!!.isNotEmpty() val sourceFound = media.anime.episodes!!.isNotEmpty()
binding.animeSourceNotFound.isGone = sourceFound val isDownloadedSource = watchSources[media.selected!!.sourceIndex] is OfflineAnimeParser
if (isDownloadedSource) {
binding.sourceNotFound.text = if (sourceFound) {
currActivity()!!.getString(R.string.source_not_found)
} else {
currActivity()!!.getString(R.string.download_not_found)
}
} else {
binding.sourceNotFound.text = currActivity()!!.getString(R.string.source_not_found)
}
binding.sourceNotFound.isGone = sourceFound
binding.faqbutton.isGone = sourceFound binding.faqbutton.isGone = sourceFound
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) { if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) {
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) { if (binding.mediaSource.adapter.count > media.selected!!.sourceIndex + 1) {
val nextIndex = media.selected!!.sourceIndex + 1 val nextIndex = media.selected!!.sourceIndex + 1
binding.animeSource.setText( binding.mediaSource.setText(
binding.animeSource.adapter binding.mediaSource.adapter
.getItem(nextIndex).toString(), false .getItem(nextIndex).toString(), false
) )
fragment.onSourceChange(nextIndex).apply { fragment.onSourceChange(nextIndex).apply {
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } } { MainScope().launch { binding.mediaSourceTitle.text = it } }
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately() binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
setLanguageList(0, nextIndex) setLanguageList(0, nextIndex)
@ -460,13 +512,13 @@ class AnimeWatchAdapter(
fragment.loadEpisodes(nextIndex, false) fragment.loadEpisodes(nextIndex, false)
} }
} }
binding.animeSource.setOnClickListener { autoSelect = false } binding.mediaSource.setOnClickListener { autoSelect = false }
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.sourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE binding.sourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE binding.faqbutton.visibility = View.GONE
clearChips() clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE binding.sourceProgressBar.visibility = View.VISIBLE
} }
} }
} }
@ -480,9 +532,9 @@ class AnimeWatchAdapter(
ext.sourceLanguage = lang ext.sourceLanguage = lang
} }
try { try {
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang) binding?.mediaSourceLanguage?.setText(parser.extension.sources[lang].lang)
} catch (e: IndexOutOfBoundsException) { } catch (e: IndexOutOfBoundsException) {
binding?.animeSourceLanguage?.setText( binding?.mediaSourceLanguage?.setText(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown" parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
) )
} }
@ -493,9 +545,9 @@ class AnimeWatchAdapter(
) )
val items = adapter.count val items = adapter.count
binding?.animeSourceLanguageContainer?.visibility = binding?.mediaSourceLanguageContainer?.visibility =
if (items > 1) View.VISIBLE else View.GONE if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguage?.setAdapter(adapter) binding?.mediaSourceLanguage?.setAdapter(adapter)
} }
} }
@ -503,7 +555,7 @@ class AnimeWatchAdapter(
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : inner class ViewHolder(val binding: ItemMediaSourceBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
init { init {
displayTimer(media, binding.animeSourceContainer) displayTimer(media, binding.animeSourceContainer)

View file

@ -31,7 +31,8 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.FileUrl import ani.dantotsu.FileUrl
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.addons.download.DownloadAddonManager import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.connections.anilist.api.MediaStreamingEpisode
import ani.dantotsu.databinding.FragmentMediaSourceBinding
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName import ani.dantotsu.download.DownloadsManager.Companion.compareName
@ -48,6 +49,7 @@ import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.notifications.subscription.SubscriptionHelper import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
import ani.dantotsu.others.Anify
import ani.dantotsu.others.LanguageMapper import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.AnimeParser import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
@ -61,6 +63,7 @@ import ani.dantotsu.toast
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import ani.dantotsu.util.customAlertDialog
import com.anggrayudi.storage.file.extension import com.anggrayudi.storage.file.extension
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
@ -78,7 +81,7 @@ import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
class AnimeWatchFragment : Fragment() { class AnimeWatchFragment : Fragment() {
private var _binding: FragmentAnimeWatchBinding? = null private var _binding: FragmentMediaSourceBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels() private val model: MediaDetailsViewModel by activityViewModels()
@ -105,7 +108,7 @@ class AnimeWatchFragment : Fragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
_binding = FragmentAnimeWatchBinding.inflate(inflater, container, false) _binding = FragmentMediaSourceBinding.inflate(inflater, container, false)
return _binding?.root return _binding?.root
} }
@ -126,7 +129,7 @@ class AnimeWatchFragment : Fragment() {
) )
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.mediaSourceRecycler.updatePadding(bottom = binding.mediaSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp screenWidth = resources.displayMetrics.widthPixels.dp
var maxGridSize = (screenWidth / 100f).roundToInt() var maxGridSize = (screenWidth / 100f).roundToInt()
@ -150,13 +153,13 @@ class AnimeWatchFragment : Fragment() {
} }
} }
binding.animeSourceRecycler.layoutManager = gridLayoutManager binding.mediaSourceRecycler.layoutManager = gridLayoutManager
binding.ScrollTop.setOnClickListener { binding.ScrollTop.setOnClickListener {
binding.animeSourceRecycler.scrollToPosition(10) binding.mediaSourceRecycler.scrollToPosition(10)
binding.animeSourceRecycler.smoothScrollToPosition(0) binding.mediaSourceRecycler.smoothScrollToPosition(0)
} }
binding.animeSourceRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { binding.mediaSourceRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
@ -170,7 +173,7 @@ class AnimeWatchFragment : Fragment() {
} }
}) })
model.scrolledToTop.observe(viewLifecycleOwner) { model.scrolledToTop.observe(viewLifecycleOwner) {
if (it) binding.animeSourceRecycler.scrollToPosition(0) if (it) binding.mediaSourceRecycler.scrollToPosition(0)
} }
continueEp = model.continueMedia ?: false continueEp = model.continueMedia ?: false
@ -203,7 +206,7 @@ class AnimeWatchFragment : Fragment() {
offlineMode = offlineMode offlineMode = offlineMode
) )
binding.animeSourceRecycler.adapter = binding.mediaSourceRecycler.adapter =
ConcatAdapter(headerAdapter, episodeAdapter) ConcatAdapter(headerAdapter, episodeAdapter)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@ -212,10 +215,11 @@ class AnimeWatchFragment : Fragment() {
if (offline) { if (offline) {
media.selected!!.sourceIndex = model.watchSources!!.list.lastIndex media.selected!!.sourceIndex = model.watchSources!!.list.lastIndex
} else { } else {
awaitAll( val kitsuEpisodes = async { model.loadKitsuEpisodes(media) }
async { model.loadKitsuEpisodes(media) }, val anifyEpisodes = async { model.loadAnifyEpisodes(media.id) }
async { model.loadFillerEpisodes(media) } val fillerEpisodes = async { model.loadFillerEpisodes(media) }
)
awaitAll(kitsuEpisodes, anifyEpisodes, fillerEpisodes)
} }
model.loadEpisodes(media, media.selected!!.sourceIndex) model.loadEpisodes(media, media.selected!!.sourceIndex)
} }
@ -230,6 +234,18 @@ class AnimeWatchFragment : Fragment() {
val episodes = loadedEpisodes[media.selected!!.sourceIndex] val episodes = loadedEpisodes[media.selected!!.sourceIndex]
if (episodes != null) { if (episodes != null) {
episodes.forEach { (i, episode) -> episodes.forEach { (i, episode) ->
if (media.anime?.anifyEpisodes != null) {
if (media.anime!!.anifyEpisodes!!.containsKey(i)) {
episode.desc = media.anime!!.anifyEpisodes!![i]?.desc ?: episode.desc
episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: ""
).isBlank()
) media.anime!!.anifyEpisodes!![i]?.title ?: episode.title else episode.title
?: media.anime!!.anifyEpisodes!![i]?.title ?: episode.title
episode.thumb = media.anime!!.anifyEpisodes!![i]?.thumb ?: episode.thumb
}
}
if (media.anime?.fillerEpisodes != null) { if (media.anime?.fillerEpisodes != null) {
if (media.anime!!.fillerEpisodes!!.containsKey(i)) { if (media.anime!!.fillerEpisodes!!.containsKey(i)) {
episode.title = episode.title =
@ -239,22 +255,19 @@ class AnimeWatchFragment : Fragment() {
} }
if (media.anime?.kitsuEpisodes != null) { if (media.anime?.kitsuEpisodes != null) {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) { if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc = episode.desc = media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely( episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: "" episode.title ?: ""
).isBlank() ).isBlank()
) media.anime!!.kitsuEpisodes!![i]?.title ) media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title else episode.title
?: episode.title else episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title
?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb ?: episode.thumb
episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb
?: FileUrl[media.cover]
} }
} }
} }
media.anime?.episodes = episodes media.anime?.episodes = episodes
//CHIP GROUP // CHIP GROUP
val total = episodes.size val total = episodes.size
val divisions = total.toDouble() / 10 val divisions = total.toDouble() / 10
start = 0 start = 0
@ -295,6 +308,10 @@ class AnimeWatchFragment : Fragment() {
if (i != null) if (i != null)
media.anime?.fillerEpisodes = i media.anime?.fillerEpisodes = i
} }
model.getAnifyEpisodes().observe(viewLifecycleOwner) { i ->
if (i != null)
media.anime?.anifyEpisodes = i
}
} }
fun onSourceChange(i: Int): AnimeParser { fun onSourceChange(i: Int): AnimeParser {
@ -380,20 +397,18 @@ class AnimeWatchFragment : Fragment() {
if (allSettings.size > 1) { if (allSettings.size > 1) {
val names = val names =
allSettings.map { LanguageMapper.getLanguageName(it.lang) }.toTypedArray() allSettings.map { LanguageMapper.getLanguageName(it.lang) }.toTypedArray()
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) requireContext()
.setTitle("Select a Source") .customAlertDialog()
.setSingleChoiceItems(names, -1) { dialog, which -> .apply {
setTitle("Select a Source")
singleChoiceItems(names) { which ->
selectedSetting = allSettings[which] selectedSetting = allSettings[which]
itemSelected = true itemSelected = true
dialog.dismiss()
// Move the fragment transaction here
requireActivity().runOnUiThread { requireActivity().runOnUiThread {
val fragment = val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { changeUIVisibility(true)
changeUIVisibility(true) loadEpisodes(media.selected!!.sourceIndex, true)
loadEpisodes(media.selected!!.sourceIndex, true) }
}
parentFragmentManager.beginTransaction() parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down) .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment) .replace(R.id.fragmentExtensionsContainer, fragment)
@ -401,13 +416,13 @@ class AnimeWatchFragment : Fragment() {
.commit() .commit()
} }
} }
.setOnDismissListener { onDismiss {
if (!itemSelected) { if (!itemSelected) {
changeUIVisibility(true) changeUIVisibility(true)
} }
} }
.show() show()
dialog.window?.setDimAmount(0.8f) }
} else { } else {
// If there's only one setting, proceed with the fragment transaction // If there's only one setting, proceed with the fragment transaction
requireActivity().runOnUiThread { requireActivity().runOnUiThread {
@ -416,11 +431,12 @@ class AnimeWatchFragment : Fragment() {
changeUIVisibility(true) changeUIVisibility(true)
loadEpisodes(media.selected!!.sourceIndex, true) loadEpisodes(media.selected!!.sourceIndex, true)
} }
parentFragmentManager.beginTransaction() parentFragmentManager.beginTransaction().apply {
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down) setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment) replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null) addToBackStack(null)
.commit() commit()
}
} }
} }
@ -619,7 +635,7 @@ class AnimeWatchFragment : Fragment() {
private fun reload() { private fun reload() {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
//Find latest episode for subscription // Find latest episode for subscription
selected.latest = selected.latest =
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest = selected.latest =
@ -663,14 +679,14 @@ class AnimeWatchFragment : Fragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.mediaInfoProgressBar.visibility = progress binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state) binding.mediaSourceRecycler.layoutManager?.onRestoreInstanceState(state)
requireActivity().setNavigationTheme() requireActivity().setNavigationTheme()
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() state = binding.mediaSourceRecycler.layoutManager?.onSaveInstanceState()
} }
companion object { companion object {

View file

@ -23,6 +23,7 @@ import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType import ani.dantotsu.media.MediaType
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.customAlertDialog
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -106,8 +107,8 @@ class EpisodeAdapter(
val thumb = val thumb =
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0) Glide.with(binding.itemMediaImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage) .into(binding.itemMediaImage)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
@ -140,9 +141,9 @@ class EpisodeAdapter(
} }
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemMediaProgressCont,
binding.itemEpisodeProgress, binding.itemMediaProgress,
binding.itemEpisodeProgressEmpty, binding.itemMediaProgressEmpty,
media.id, media.id,
ep.number ep.number
) )
@ -154,8 +155,8 @@ class EpisodeAdapter(
val thumb = val thumb =
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0) Glide.with(binding.itemMediaImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage) .into(binding.itemMediaImage)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title binding.itemEpisodeTitle.text = title
@ -183,9 +184,9 @@ class EpisodeAdapter(
binding.itemEpisodeViewed.visibility = View.GONE binding.itemEpisodeViewed.visibility = View.GONE
} }
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemMediaProgressCont,
binding.itemEpisodeProgress, binding.itemMediaProgress,
binding.itemEpisodeProgressEmpty, binding.itemMediaProgressEmpty,
media.id, media.id,
ep.number ep.number
) )
@ -208,9 +209,9 @@ class EpisodeAdapter(
} }
} }
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemMediaProgressCont,
binding.itemEpisodeProgress, binding.itemMediaProgress,
binding.itemEpisodeProgressEmpty, binding.itemMediaProgressEmpty,
media.id, media.id,
ep.number ep.number
) )
@ -318,16 +319,14 @@ class EpisodeAdapter(
fragment.onAnimeEpisodeStopDownloadClick(episodeNumber) fragment.onAnimeEpisodeStopDownloadClick(episodeNumber)
return@setOnClickListener return@setOnClickListener
} else if (downloadedEpisodes.contains(episodeNumber)) { } else if (downloadedEpisodes.contains(episodeNumber)) {
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup) binding.root.context.customAlertDialog().apply {
builder.setTitle("Delete Episode") setTitle("Delete Episode")
builder.setMessage("Are you sure you want to delete Episode ${episodeNumber}?") setMessage("Are you sure you want to delete Episode $episodeNumber?")
builder.setPositiveButton("Yes") { _, _ -> setPosButton(R.string.yes) {
fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber) fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
} }
builder.setNegativeButton("No") { _, _ -> setNegButton(R.string.no)
} }.show()
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
return@setOnClickListener return@setOnClickListener
} else { } else {
fragment.onAnimeEpisodeDownloadClick(episodeNumber) fragment.onAnimeEpisodeDownloadClick(episodeNumber)

View file

@ -12,6 +12,7 @@ import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.hardware.SensorManager import android.hardware.SensorManager
@ -71,9 +72,12 @@ import androidx.media3.common.MimeTypes
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.PlaybackParameters import androidx.media3.common.PlaybackParameters
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.text.Cue
import androidx.media3.common.text.CueGroup
import androidx.media3.common.TrackGroup import androidx.media3.common.TrackGroup
import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.Tracks import androidx.media3.common.Tracks
import androidx.media3.common.util.Util
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultDataSource
@ -81,6 +85,7 @@ import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.hls.HlsMediaSource import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
@ -117,6 +122,7 @@ import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.video.Helper import ani.dantotsu.download.video.Helper
import ani.dantotsu.dp import ani.dantotsu.dp
import ani.dantotsu.getCurrentBrightnessValue import ani.dantotsu.getCurrentBrightnessValue
import ani.dantotsu.getLanguageCode
import ani.dantotsu.hideSystemBars import ani.dantotsu.hideSystemBars
import ani.dantotsu.hideSystemBarsExtendView import ani.dantotsu.hideSystemBarsExtendView
import ani.dantotsu.isOnline import ani.dantotsu.isOnline
@ -135,6 +141,7 @@ import ani.dantotsu.others.getSerialized
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.others.Xubtitle
import ani.dantotsu.parsers.SubtitleType import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.parsers.Video import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoExtractor
@ -164,6 +171,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import io.github.anilbeesetti.nextlib.media3ext.ffdecoder.NextRenderersFactory
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
import java.util.Timer import java.util.Timer
@ -223,6 +231,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
private lateinit var animeTitle: TextView private lateinit var animeTitle: TextView
private lateinit var videoInfo: TextView private lateinit var videoInfo: TextView
private lateinit var episodeTitle: Spinner private lateinit var episodeTitle: Spinner
private lateinit var customSubtitleView: Xubtitle
private var orientationListener: OrientationEventListener? = null private var orientationListener: OrientationEventListener? = null
@ -318,36 +327,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
} }
private fun setupSubFormatting(playerView: PlayerView) { private fun setupSubFormatting(playerView: PlayerView) {
val primaryColor = when (PrefManager.getVal<Int>(PrefName.PrimaryColor)) { val primaryColor = PrefManager.getVal<Int>(PrefName.PrimaryColor)
0 -> Color.BLACK
1 -> Color.DKGRAY val secondaryColor = PrefManager.getVal<Int>(PrefName.SecondaryColor)
2 -> Color.GRAY
3 -> Color.LTGRAY
4 -> Color.WHITE
5 -> Color.RED
6 -> Color.YELLOW
7 -> Color.GREEN
8 -> Color.CYAN
9 -> Color.BLUE
10 -> Color.MAGENTA
11 -> Color.TRANSPARENT
else -> Color.WHITE
}
val secondaryColor = when (PrefManager.getVal<Int>(PrefName.SecondaryColor)) {
0 -> Color.BLACK
1 -> Color.DKGRAY
2 -> Color.GRAY
3 -> Color.LTGRAY
4 -> Color.WHITE
5 -> Color.RED
6 -> Color.YELLOW
7 -> Color.GREEN
8 -> Color.CYAN
9 -> Color.BLUE
10 -> Color.MAGENTA
11 -> Color.TRANSPARENT
else -> Color.BLACK
}
val outline = when (PrefManager.getVal<Int>(PrefName.Outline)) { val outline = when (PrefManager.getVal<Int>(PrefName.Outline)) {
0 -> EDGE_TYPE_OUTLINE // Normal 0 -> EDGE_TYPE_OUTLINE // Normal
1 -> EDGE_TYPE_DEPRESSED // Shine 1 -> EDGE_TYPE_DEPRESSED // Shine
@ -355,36 +338,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
3 -> EDGE_TYPE_NONE // No outline 3 -> EDGE_TYPE_NONE // No outline
else -> EDGE_TYPE_OUTLINE // Normal else -> EDGE_TYPE_OUTLINE // Normal
} }
val subBackground = when (PrefManager.getVal<Int>(PrefName.SubBackground)) {
0 -> Color.TRANSPARENT val subBackground = PrefManager.getVal<Int>(PrefName.SubBackground)
1 -> Color.BLACK
2 -> Color.DKGRAY val subWindow = PrefManager.getVal<Int>(PrefName.SubWindow)
3 -> Color.GRAY
4 -> Color.LTGRAY
5 -> Color.WHITE
6 -> Color.RED
7 -> Color.YELLOW
8 -> Color.GREEN
9 -> Color.CYAN
10 -> Color.BLUE
11 -> Color.MAGENTA
else -> Color.TRANSPARENT
}
val subWindow = when (PrefManager.getVal<Int>(PrefName.SubWindow)) {
0 -> Color.TRANSPARENT
1 -> Color.BLACK
2 -> Color.DKGRAY
3 -> Color.GRAY
4 -> Color.LTGRAY
5 -> Color.WHITE
6 -> Color.RED
7 -> Color.YELLOW
8 -> Color.GREEN
9 -> Color.CYAN
10 -> Color.BLUE
11 -> Color.MAGENTA
else -> Color.TRANSPARENT
}
val font = when (PrefManager.getVal<Int>(PrefName.Font)) { val font = when (PrefManager.getVal<Int>(PrefName.Font)) {
0 -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold) 0 -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
1 -> ResourcesCompat.getFont(this, R.font.poppins_bold) 1 -> ResourcesCompat.getFont(this, R.font.poppins_bold)
@ -422,6 +380,53 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
} }
} }
private fun applySubtitleStyles(textView: Xubtitle) {
val primaryColor = PrefManager.getVal<Int>(PrefName.PrimaryColor)
val subBackground = PrefManager.getVal<Int>(PrefName.SubBackground)
val secondaryColor = PrefManager.getVal<Int>(PrefName.SecondaryColor)
val subStroke = PrefManager.getVal<Float>(PrefName.SubStroke)
val fontSize = PrefManager.getVal<Int>(PrefName.FontSize).toFloat()
val font = when (PrefManager.getVal<Int>(PrefName.Font)) {
0 -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
1 -> ResourcesCompat.getFont(this, R.font.poppins_bold)
2 -> ResourcesCompat.getFont(this, R.font.poppins)
3 -> ResourcesCompat.getFont(this, R.font.poppins_thin)
4 -> ResourcesCompat.getFont(this, R.font.century_gothic_regular)
5 -> ResourcesCompat.getFont(this, R.font.levenim_mt_bold)
6 -> ResourcesCompat.getFont(this, R.font.blocky)
else -> ResourcesCompat.getFont(this, R.font.poppins_semi_bold)
}
textView.setBackgroundColor(subBackground)
textView.setTextColor(primaryColor)
textView.typeface = font
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize)
textView.apply {
when (PrefManager.getVal<Int>(PrefName.Outline)) {
0 -> applyOutline(secondaryColor, subStroke)
1 -> applyShineEffect(secondaryColor)
2 -> applyDropShadow(secondaryColor, subStroke)
3 -> {}
else -> applyOutline(secondaryColor, subStroke)
}
}
textView.alpha =
when (PrefManager.getVal<Boolean>(PrefName.Subtitles)) {
true -> PrefManager.getVal(PrefName.SubAlpha)
false -> 0f
}
val textElevation = PrefManager.getVal<Float>(PrefName.SubBottomMargin) / 50 * resources.displayMetrics.heightPixels
textView.translationY = -textElevation
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -467,6 +472,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
skipTimeButton = playerView.findViewById(R.id.exo_skip_timestamp) skipTimeButton = playerView.findViewById(R.id.exo_skip_timestamp)
skipTimeText = skipTimeButton.findViewById(R.id.exo_skip_timestamp_text) skipTimeText = skipTimeButton.findViewById(R.id.exo_skip_timestamp_text)
timeStampText = playerView.findViewById(R.id.exo_time_stamp_text) timeStampText = playerView.findViewById(R.id.exo_time_stamp_text)
customSubtitleView = playerView.findViewById(R.id.customSubtitleView)
animeTitle = playerView.findViewById(R.id.exo_anime_title) animeTitle = playerView.findViewById(R.id.exo_anime_title)
episodeTitle = playerView.findViewById(R.id.exo_ep_sel) episodeTitle = playerView.findViewById(R.id.exo_ep_sel)
@ -520,7 +526,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
it.visibility = View.GONE it.visibility = View.GONE
} }
} }
setupSubFormatting(playerView)
if (savedInstanceState != null) { if (savedInstanceState != null) {
currentWindow = savedInstanceState.getInt(resumeWindow) currentWindow = savedInstanceState.getInt(resumeWindow)
@ -1114,60 +1119,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
) )
initPlayer() initPlayer()
preloading = false preloading = false
val context = this
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if ((isOnline(context) && !offline) && Discord.token != null && !incognito) {
lifecycleScope.launch {
val discordMode = PrefManager.getCustomVal("discord_mode", "dantotsu")
val buttons = when (discordMode) {
"nothing" -> mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
)
"dantotsu" -> mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link("Watch on Dantotsu", getString(R.string.dantotsu))
)
"anilist" -> {
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
val anilistLink = "https://anilist.co/user/$userId/"
mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link("View My AniList", anilistLink)
)
}
else -> mutableListOf()
}
val presence = RPC.createPresence(
RPC.Companion.RPCData(
applicationId = Discord.application_Id,
type = RPC.Type.WATCHING,
activityName = media.userPreferredName,
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
R.string.episode_num,
ep.number
),
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
largeImage = media.cover?.let {
RPC.Link(
media.userPreferredName,
it
)
},
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
buttons = buttons
)
)
val intent = Intent(context, DiscordService::class.java).apply {
putExtra("presence", presence)
}
DiscordServiceRunningSingleton.running = true
startService(intent)
}
}
updateProgress() updateProgress()
} }
} }
@ -1358,6 +1309,73 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
} }
private fun discordRPC(){
val context = this
val ep = episode
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
val rpcenabled: Boolean = PrefManager.getVal(PrefName.rpcEnabled)
if ((isOnline(context) && !offline) && Discord.token != null && !incognito && rpcenabled) {
lifecycleScope.launch {
val discordMode = PrefManager.getCustomVal("discord_mode", "dantotsu")
val buttons = when (discordMode) {
"nothing" -> mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
)
"dantotsu" -> mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link("Watch on Dantotsu", getString(R.string.dantotsu))
)
"anilist" -> {
val userId = PrefManager.getVal<String>(PrefName.AnilistUserId)
val anilistLink = "https://anilist.co/user/$userId/"
mutableListOf(
RPC.Link(getString(R.string.view_anime), media.shareLink ?: ""),
RPC.Link("View My AniList", anilistLink)
)
}
else -> mutableListOf()
}
val startTimestamp = Calendar.getInstance()
val durationInSeconds = if (exoPlayer.duration != C.TIME_UNSET) (exoPlayer.duration / 1000).toInt() else 1440
val endTimestamp = Calendar.getInstance().apply {
timeInMillis = startTimestamp.timeInMillis
add(Calendar.SECOND, durationInSeconds)
}
val presence = RPC.createPresence(
RPC.Companion.RPCData(
applicationId = Discord.application_Id,
type = RPC.Type.WATCHING,
activityName = media.userPreferredName,
details = ep.title?.takeIf { it.isNotEmpty() } ?: getString(
R.string.episode_num,
ep.number
),
startTimestamp = startTimestamp.timeInMillis,
stopTimestamp = endTimestamp.timeInMillis,
state = "Episode : ${ep.number}/${media.anime?.totalEpisodes ?: "??"}",
largeImage = media.cover?.let {
RPC.Link(
media.userPreferredName,
it
)
},
smallImage = RPC.Link("Dantotsu", Discord.small_Image),
buttons = buttons
)
)
val intent = Intent(context, DiscordService::class.java).apply {
putExtra("presence", presence)
}
DiscordServiceRunningSingleton.running = true
startService(intent)
}
}
}
private fun initPlayer() { private fun initPlayer() {
checkNotch() checkNotch()
@ -1383,13 +1401,57 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
val ext = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return val ext = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return
extractor = ext extractor = ext
video = ext.videos.getOrNull(episode.selectedVideo) ?: return video = ext.videos.getOrNull(episode.selectedVideo) ?: return
val subLanguages = arrayOf(
"Albanian",
"Arabic",
"Bosnian",
"Bulgarian",
"Chinese",
"Croatian",
"Czech",
"Danish",
"Dutch",
"English",
"Estonian",
"Finnish",
"French",
"Georgian",
"German",
"Greek",
"Hebrew",
"Hindi",
"Indonesian",
"Irish",
"Italian",
"Japanese",
"Korean",
"Lithuanian",
"Luxembourgish",
"Macedonian",
"Mongolian",
"Norwegian",
"Polish",
"Portuguese",
"Punjabi",
"Romanian",
"Russian",
"Serbian",
"Slovak",
"Slovenian",
"Spanish",
"Turkish",
"Ukrainian",
"Urdu",
"Vietnamese",
)
val lang = subLanguages[PrefManager.getVal(PrefName.SubLanguage)]
subtitle = intent.getSerialized("subtitle") subtitle = intent.getSerialized("subtitle")
?: when (val subLang: String? = ?: when (val subLang: String? =
PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java)) { PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java)) {
null -> { null -> {
when (episode.selectedSubtitle) { when (episode.selectedSubtitle) {
null -> null null -> null
-1 -> ext.subtitles.find { it.language.trim() == "English" || it.language == "en-US" } -1 -> ext.subtitles.find { it.language.contains( lang, ignoreCase = true ) || it.language.contains( getLanguageCode(lang), ignoreCase = true ) }
else -> ext.subtitles.getOrNull(episode.selectedSubtitle!!) else -> ext.subtitles.getOrNull(episode.selectedSubtitle!!)
} }
} }
@ -1642,7 +1704,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
.build() .build()
hideSystemBars() hideSystemBars()
exoPlayer = ExoPlayer.Builder(this)
val useExtensionDecoder = PrefManager.getVal<Boolean>(PrefName.UseAdditionalCodec)
val decoder = if (useExtensionDecoder) {
DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
} else {
DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF
}
val renderersFactory = NextRenderersFactory(this)
.setEnableDecoderFallback(true)
.setExtensionRendererMode(decoder)
exoPlayer = ExoPlayer.Builder(this, renderersFactory)
.setMediaSourceFactory(DefaultMediaSourceFactory(cacheFactory)) .setMediaSourceFactory(DefaultMediaSourceFactory(cacheFactory))
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.setLoadControl(loadControl) .setLoadControl(loadControl)
@ -1661,6 +1734,54 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
} }
playerView.player = exoPlayer playerView.player = exoPlayer
exoPlayer.addListener(object : Player.Listener {
var activeSubtitles = ArrayDeque<String>(3)
var lastSubtitle: String? = null
var lastPosition: Long = 0
override fun onCues(cueGroup: CueGroup) {
if (PrefManager.getVal<Boolean>(PrefName.TextviewSubtitles)) {
exoSubtitleView.visibility = View.GONE
customSubtitleView.visibility = View.VISIBLE
val newCues = cueGroup.cues.map { it.text.toString() ?: "" }
if (newCues.isEmpty()) {
customSubtitleView.text = ""
activeSubtitles.clear()
lastSubtitle = null
lastPosition = 0
return
}
val currentPosition = exoPlayer.currentPosition
if ((lastSubtitle?.length ?: 0) < 20 || (lastPosition != 0L && currentPosition - lastPosition > 1500)) {
activeSubtitles.clear()
}
for (newCue in newCues) {
if (newCue !in activeSubtitles) {
if (activeSubtitles.size >= 2) {
activeSubtitles.removeLast()
}
activeSubtitles.addFirst(newCue)
lastSubtitle = newCue
lastPosition = currentPosition
}
}
customSubtitleView.text = activeSubtitles.joinToString("\n")
} else {
customSubtitleView.text = ""
customSubtitleView.visibility = View.GONE
exoSubtitleView.visibility = View.VISIBLE
}
}
})
applySubtitleStyles(customSubtitleView)
setupSubFormatting(playerView)
try { try {
val rightNow = Calendar.getInstance() val rightNow = Calendar.getInstance()
mediaSession = MediaSession.Builder(this, exoPlayer) mediaSession = MediaSession.Builder(this, exoPlayer)
@ -1950,7 +2071,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
TrackSelectionOverride(trackGroup.mediaTrackGroup, index) TrackSelectionOverride(trackGroup.mediaTrackGroup, index)
) )
.build() .build()
if (type == TRACK_TYPE_TEXT) setupSubFormatting(playerView) if (type == TRACK_TYPE_TEXT) {
setupSubFormatting(playerView)
applySubtitleStyles(customSubtitleView)
}
playerView.subtitleView?.alpha = when (isDisabled) { playerView.subtitleView?.alpha = when (isDisabled) {
false -> PrefManager.getVal(PrefName.SubAlpha) false -> PrefManager.getVal(PrefName.SubAlpha)
true -> 0f true -> 0f
@ -2042,6 +2166,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
exoPlayer.play() exoPlayer.play()
if (episodeLength == 0f) { if (episodeLength == 0f) {
episodeLength = exoPlayer.duration.toFloat() episodeLength = exoPlayer.duration.toFloat()
discordRPC()
} }
} }
isBuffering = playbackState == Player.STATE_BUFFERING isBuffering = playbackState == Player.STATE_BUFFERING

View file

@ -444,15 +444,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
if (subtitles.isNotEmpty()) { if (subtitles.isNotEmpty()) {
val subtitleNames = subtitles.map { it.language } val subtitleNames = subtitles.map { it.language }
var subtitleToDownload: Subtitle? = null var subtitleToDownload: Subtitle? = null
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) requireActivity().customAlertDialog().apply {
.setTitle(R.string.download_subtitle) setTitle(R.string.download_subtitle)
.setSingleChoiceItems( singleChoiceItems(subtitleNames.toTypedArray()) {which ->
subtitleNames.toTypedArray(),
-1
) { _, which ->
subtitleToDownload = subtitles[which] subtitleToDownload = subtitles[which]
} }
.setPositiveButton(R.string.download) { dialog, _ -> setPosButton(R.string.download) {
scope.launch { scope.launch {
if (subtitleToDownload != null) { if (subtitleToDownload != null) {
SubtitleDownloader.downloadSubtitle( SubtitleDownloader.downloadSubtitle(
@ -466,13 +463,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
) )
} }
} }
dialog.dismiss()
} }
.setNegativeButton(R.string.cancel) { dialog, _ -> setNegButton(R.string.cancel) {}
dialog.dismiss() }.show()
}
.show()
alertDialog.window?.setDimAmount(0.8f)
} else { } else {
snackString(R.string.no_subtitles_available) snackString(R.string.no_subtitles_available)
} }
@ -576,65 +569,63 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
if (audioTracks.isNotEmpty()) { if (audioTracks.isNotEmpty()) {
val audioNamesArray = audioTracks.toTypedArray() val audioNamesArray = audioTracks.toTypedArray()
val checkedItems = BooleanArray(audioNamesArray.size) { false } val checkedItems = BooleanArray(audioNamesArray.size) { false }
val alertDialog = AlertDialog.Builder(currContext, R.style.MyPopup)
.setTitle(R.string.download_audio_tracks) currContext.customAlertDialog().apply{ // ToTest
.setMultiChoiceItems(audioNamesArray, checkedItems) { _, which, isChecked -> setTitle(R.string.download_audio_tracks)
val audioPair = Pair(extractor.audioTracks[which].url, extractor.audioTracks[which].lang) multiChoiceItems(audioNamesArray, checkedItems) {
if (isChecked) { it.forEachIndexed { index, isChecked ->
selectedAudioTracks.add(audioPair) val audioPair = Pair(extractor.audioTracks[index].url, extractor.audioTracks[index].lang)
} else { if (isChecked) {
selectedAudioTracks.remove(audioPair) selectedAudioTracks.add(audioPair)
} else {
selectedAudioTracks.remove(audioPair)
}
} }
} }
.setPositiveButton(R.string.download) { _, _ -> setPosButton(R.string.download) {
dialog?.dismiss()
go() go()
} }
.setNegativeButton(R.string.skip) { dialog, _ -> setNegButton(R.string.skip) {
selectedAudioTracks = mutableListOf() selectedAudioTracks = mutableListOf()
go() go()
dialog.dismiss()
} }
.setNeutralButton(R.string.cancel) { dialog, _ -> setNeutralButton(R.string.cancel) {
selectedAudioTracks = mutableListOf() selectedAudioTracks = mutableListOf()
dialog.dismiss()
} }
.show() show()
alertDialog.window?.setDimAmount(0.8f) }
} else { } else {
go() go()
} }
} }
if (subtitles.isNotEmpty()) { if (subtitles.isNotEmpty()) { // ToTest
val subtitleNamesArray = subtitleNames.toTypedArray() val subtitleNamesArray = subtitleNames.toTypedArray()
val checkedItems = BooleanArray(subtitleNamesArray.size) { false } val checkedItems = BooleanArray(subtitleNamesArray.size) { false }
val alertDialog = AlertDialog.Builder(currContext, R.style.MyPopup) currContext.customAlertDialog().apply {
.setTitle(R.string.download_subtitle) setTitle(R.string.download_subtitle)
.setMultiChoiceItems(subtitleNamesArray, checkedItems) { _, which, isChecked -> multiChoiceItems(subtitleNamesArray, checkedItems) {
val subtitlePair = Pair(subtitles[which].file.url, subtitles[which].language) it.forEachIndexed { index, isChecked ->
if (isChecked) { val subtitlePair = Pair(subtitles[index].file.url, subtitles[index].language)
selectedSubtitles.add(subtitlePair) if (isChecked) {
} else { selectedSubtitles.add(subtitlePair)
selectedSubtitles.remove(subtitlePair) } else {
selectedSubtitles.remove(subtitlePair)
}
} }
} }
.setPositiveButton(R.string.download) { _, _ -> setPosButton(R.string.download) {
dialog?.dismiss()
checkAudioTracks() checkAudioTracks()
} }
.setNegativeButton(R.string.skip) { dialog, _ -> setNegButton(R.string.skip) {
selectedSubtitles = mutableListOf() selectedSubtitles = mutableListOf()
checkAudioTracks() checkAudioTracks()
dialog.dismiss()
} }
.setNeutralButton(R.string.cancel) { dialog, _ -> setNeutralButton(R.string.cancel) {
selectedSubtitles = mutableListOf() selectedSubtitles = mutableListOf()
dialog.dismiss()
} }
.show() show()
alertDialog.window?.setDimAmount(0.8f) }
} else { } else {
checkAudioTracks() checkAudioTracks()
} }

View file

@ -21,6 +21,7 @@ import ani.dantotsu.setAnimation
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.ColorEditor.Companion.adjustColorForContrast import ani.dantotsu.util.ColorEditor.Companion.adjustColorForContrast
import ani.dantotsu.util.ColorEditor.Companion.getContrastRatio import ani.dantotsu.util.ColorEditor.Companion.getContrastRatio
import ani.dantotsu.util.customAlertDialog
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
@ -385,19 +386,14 @@ class CommentItem(
* @param callback the callback to call when the user clicks yes * @param callback the callback to call when the user clicks yes
*/ */
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) { private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
val alertDialog = commentsFragment.activity.customAlertDialog().apply {
android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup) setTitle(title)
.setTitle(title) setMessage(message)
.setMessage(message) setPosButton("Yes") {
.setPositiveButton("Yes") { dialog, _ -> callback()
callback() }
dialog.dismiss() setNegButton("No") {}
} }.show()
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
} }
private val usernameColors: Array<String> = arrayOf( private val usernameColors: Array<String> = arrayOf(

View file

@ -25,6 +25,7 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.comments.Comment import ani.dantotsu.connections.comments.Comment
import ani.dantotsu.connections.comments.CommentResponse import ani.dantotsu.connections.comments.CommentResponse
import ani.dantotsu.connections.comments.CommentsAPI import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.databinding.DialogEdittextBinding
import ani.dantotsu.databinding.FragmentCommentsBinding import ani.dantotsu.databinding.FragmentCommentsBinding
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
@ -34,6 +35,7 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section import com.xwray.groupie.Section
import io.noties.markwon.editor.MarkwonEditor import io.noties.markwon.editor.MarkwonEditor
@ -160,35 +162,48 @@ class CommentsFragment : Fragment() {
popup.inflate(R.menu.comments_sort_menu) popup.inflate(R.menu.comments_sort_menu)
popup.show() popup.show()
} }
binding.openRules.setOnClickListener {
activity.customAlertDialog().apply {
setTitle("Commenting Rules")
.setMessage(
"🚨 BREAK ANY RULE = YOU'RE GONE\n\n" +
"1. NO RACISM, DISCRIMINATION, OR HATE SPEECH\n" +
"2. NO SPAMMING OR SELF-PROMOTION\n" +
"3. ABSOLUTELY NO NSFW CONTENT\n" +
"4. ENGLISH ONLY NO EXCEPTIONS\n" +
"5. NO IMPERSONATION, HARASSMENT, OR ABUSE\n" +
"6. NO ILLEGAL CONTENT OR EXTREME DISRESPECT TOWARDS ANY FANDOM\n" +
"7. DO NOT REQUEST OR SHARE REPOSITORIES/EXTENSIONS\n" +
"8. SPOILERS ALLOWED ONLY WITH SPOILER TAGS AND A WARNING\n" +
"9. NO SEXUALIZING OR INAPPROPRIATE COMMENTS ABOUT MINOR CHARACTERS\n" +
"10. IF IT'S WRONG, DON'T POST IT!\n\n"
)
setNegButton("I Understand") {}
show()
}
}
binding.commentFilter.setOnClickListener { binding.commentFilter.setOnClickListener {
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup) activity.customAlertDialog().apply {
.setTitle("Enter a chapter/episode number tag") val customView = DialogEdittextBinding.inflate(layoutInflater)
.setView(R.layout.dialog_edittext) setTitle("Enter a chapter/episode number tag")
.setPositiveButton("OK") { dialog, _ -> setCustomView(customView.root)
val editText = setPosButton("OK") {
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText) val text = customView.dialogEditText.text.toString()
val text = editText?.text.toString()
filterTag = text.toIntOrNull() filterTag = text.toIntOrNull()
lifecycleScope.launch { lifecycleScope.launch {
loadAndDisplayComments() loadAndDisplayComments()
} }
dialog.dismiss()
} }
.setNeutralButton("Clear") { dialog, _ -> setNeutralButton("Clear") {
filterTag = null filterTag = null
lifecycleScope.launch { lifecycleScope.launch {
loadAndDisplayComments() loadAndDisplayComments()
} }
dialog.dismiss()
} }
.setNegativeButton("Cancel") { dialog, _ -> setNegButton("Cancel") { filterTag = null }
filterTag = null show()
dialog.dismiss() }
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
} }
var isFetching = false var isFetching = false
@ -303,13 +318,12 @@ class CommentsFragment : Fragment() {
activity.binding.commentLabel.setOnClickListener { activity.binding.commentLabel.setOnClickListener {
//alert dialog to enter a number, with a cancel and ok button //alert dialog to enter a number, with a cancel and ok button
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup) activity.customAlertDialog().apply {
.setTitle("Enter a chapter/episode number tag") val customView = DialogEdittextBinding.inflate(layoutInflater)
.setView(R.layout.dialog_edittext) setTitle("Enter a chapter/episode number tag")
.setPositiveButton("OK") { dialog, _ -> setCustomView(customView.root)
val editText = setPosButton("OK") {
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText) val text = customView.dialogEditText.text.toString()
val text = editText?.text.toString()
tag = text.toIntOrNull() tag = text.toIntOrNull()
if (tag == null) { if (tag == null) {
activity.binding.commentLabel.background = ResourcesCompat.getDrawable( activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
@ -324,28 +338,25 @@ class CommentsFragment : Fragment() {
null null
) )
} }
dialog.dismiss()
} }
.setNeutralButton("Clear") { dialog, _ -> setNeutralButton("Clear") {
tag = null tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable( activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources, resources,
R.drawable.ic_label_off_24, R.drawable.ic_label_off_24,
null null
) )
dialog.dismiss()
} }
.setNegativeButton("Cancel") { dialog, _ -> setNegButton("Cancel") {
tag = null tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable( activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources, resources,
R.drawable.ic_label_off_24, R.drawable.ic_label_off_24,
null null
) )
dialog.dismiss()
} }
val dialog = alertDialog.show() show()
dialog?.window?.setDimAmount(0.8f) }
} }
} }
@ -363,11 +374,6 @@ class CommentsFragment : Fragment() {
} }
} }
@SuppressLint("NotifyDataSetChanged")
override fun onStart() {
super.onStart()
}
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -579,31 +585,28 @@ class CommentsFragment : Fragment() {
* Called when the user tries to comment for the first time * Called when the user tries to comment for the first time
*/ */
private fun showCommentRulesDialog() { private fun showCommentRulesDialog() {
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup) activity.customAlertDialog().apply {
.setTitle("Commenting Rules") setTitle("Commenting Rules")
.setMessage( .setMessage(
"I WILL BAN YOU WITHOUT HESITATION\n" + "🚨 BREAK ANY RULE = YOU'RE GONE\n\n" +
"1. No racism\n" + "1. NO RACISM, DISCRIMINATION, OR HATE SPEECH\n" +
"2. No hate speech\n" + "2. NO SPAMMING OR SELF-PROMOTION\n" +
"3. No spam\n" + "3. ABSOLUTELY NO NSFW CONTENT\n" +
"4. No NSFW content\n" + "4. ENGLISH ONLY NO EXCEPTIONS\n" +
"6. ENGLISH ONLY\n" + "5. NO IMPERSONATION, HARASSMENT, OR ABUSE\n" +
"7. No self promotion\n" + "6. NO ILLEGAL CONTENT OR EXTREME DISRESPECT TOWARDS ANY FANDOM\n" +
"8. No impersonation\n" + "7. DO NOT REQUEST OR SHARE REPOSITORIES/EXTENSIONS\n" +
"9. No harassment\n" + "8. SPOILERS ALLOWED ONLY WITH SPOILER TAGS AND A WARNING\n" +
"10. No illegal content\n" + "9. NO SEXUALIZING OR INAPPROPRIATE COMMENTS ABOUT MINOR CHARACTERS\n" +
"11. Anything you know you shouldn't comment\n" "10. IF IT'S WRONG, DON'T POST IT!\n\n"
) )
.setPositiveButton("I Understand") { dialog, _ -> setPosButton("I Understand") {
dialog.dismiss()
PrefManager.setVal(PrefName.FirstComment, false) PrefManager.setVal(PrefName.FirstComment, false)
processComment() processComment()
} }
.setNegativeButton("Cancel") { dialog, _ -> setNegButton(R.string.cancel)
dialog.dismiss() show()
} }
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
} }
private fun processComment() { private fun processComment() {
@ -709,4 +712,4 @@ class CommentsFragment : Fragment() {
} }
} }
} }
} }

View file

@ -1,7 +1,7 @@
package ani.dantotsu.media.manga package ani.dantotsu.media.manga
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -11,6 +11,7 @@ import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.NumberPicker import android.widget.NumberPicker
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getString
import androidx.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -19,8 +20,9 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.currActivity import ani.dantotsu.currActivity
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.databinding.CustomDialogLayoutBinding
import ani.dantotsu.databinding.DialogLayoutBinding import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemMediaSourceBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.isOnline import ani.dantotsu.isOnline
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
@ -35,11 +37,14 @@ import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.MangaReadSources import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.parsers.OfflineMangaParser
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.settings.FAQActivity import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@ -55,86 +60,108 @@ class MangaReadAdapter(
) : RecyclerView.Adapter<MangaReadAdapter.ViewHolder>() { ) : RecyclerView.Adapter<MangaReadAdapter.ViewHolder>() {
var subscribe: MediaDetailsActivity.PopImageButton? = null var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null private var _binding: ItemMediaSourceBinding? = null
val hiddenScanlators = mutableListOf<String>() val hiddenScanlators = mutableListOf<String>()
var scanlatorSelectionListener: ScanlatorSelectionListener? = null var scanlatorSelectionListener: ScanlatorSelectionListener? = null
var options = listOf<String>() var options = listOf<String>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { private fun clearCustomValsForMedia(mediaId: String, suffix: String) {
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false) val customVals = PrefManager.getAllCustomValsForMedia("$mediaId$suffix")
return ViewHolder(bind) customVals.forEach { (key) ->
PrefManager.removeCustomVal(key)
Log.d("PrefManager", "Removed key: $key")
}
} }
private var nestedDialog: AlertDialog? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemMediaSourceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(bind)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
_binding = binding _binding = binding
binding.sourceTitle.setText(R.string.chaps) binding.sourceTitle.setText(R.string.chaps)
//Fuck u launch // Fuck u launch
binding.faqbutton.setOnClickListener { binding.faqbutton.setOnClickListener {
val intent = Intent(fragment.requireContext(), FAQActivity::class.java) val intent = Intent(fragment.requireContext(), FAQActivity::class.java)
startActivity(fragment.requireContext(), intent, null) startActivity(fragment.requireContext(), intent, null)
} }
//Wrong Title // Wrong Title
binding.animeSourceSearch.setOnClickListener { binding.mediaSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show( SourceSearchDialogFragment().show(
fragment.requireActivity().supportFragmentManager, fragment.requireActivity().supportFragmentManager,
null null
) )
} }
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode) val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
//for removing saved progress
binding.sourceTitle.setOnLongClickListener{
fragment.requireContext().customAlertDialog().apply {
setTitle(" Delete Progress for all chapters of ${media.nameRomaji}")
setMessage("This will delete all the locally stored progress for chapters")
setPosButton(R.string.ok){
clearCustomValsForMedia("${media.id}", "_Chapter")
clearCustomValsForMedia("${media.id}", "_Vol")
snackString("Deleted the progress of Chapters for ${media.nameRomaji}")
}
setNegButton(R.string.no)
show()
}
true
}
binding.animeSourceNameContainer.isGone = offline binding.mediaSourceNameContainer.isGone = offline
binding.animeSourceSettings.isGone = offline binding.mediaSourceSettings.isGone = offline
binding.animeSourceSearch.isGone = offline binding.mediaSourceSearch.isGone = offline
binding.animeSourceTitle.isGone = offline binding.mediaSourceTitle.isGone = offline
//Source Selection // Source Selection
var source = var source =
media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it } media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
setLanguageList(media.selected!!.langIndex, source) setLanguageList(media.selected!!.langIndex, source)
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) { if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
binding.animeSource.setText(mangaReadSources.names[source]) binding.mediaSource.setText(mangaReadSources.names[source])
mangaReadSources[source].apply { mangaReadSources[source].apply {
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
} }
} }
media.selected?.scanlators?.let { media.selected?.scanlators?.let {
hiddenScanlators.addAll(it) hiddenScanlators.addAll(it)
} }
binding.animeSource.setAdapter( binding.mediaSource.setAdapter(
ArrayAdapter( ArrayAdapter(
fragment.requireContext(), fragment.requireContext(),
R.layout.item_dropdown, R.layout.item_dropdown,
mangaReadSources.names mangaReadSources.names
) )
) )
binding.animeSourceTitle.isSelected = true binding.mediaSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ -> binding.mediaSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply { fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
source = i source = i
setLanguageList(0, i) setLanguageList(0, i)
} }
subscribeButton(false) subscribeButton(false)
//invalidate if it's the last source // Invalidate if it's the last source
val invalidate = i == mangaReadSources.names.size - 1 val invalidate = i == mangaReadSources.names.size - 1
fragment.loadChapters(i, invalidate) fragment.loadChapters(i, invalidate)
} }
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ -> binding.mediaSourceLanguage.setOnItemClickListener { _, _, i, _ ->
// Check if 'extension' and 'selected' properties exist and are accessible // Check if 'extension' and 'selected' properties exist and are accessible
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext -> (mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
ext.sourceLanguage = i ext.sourceLanguage = i
fragment.onLangChange(i, ext.saveName) fragment.onLangChange(i, ext.saveName)
fragment.onSourceChange(media.selected!!.sourceIndex).apply { fragment.onSourceChange(media.selected!!.sourceIndex).apply {
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } } { MainScope().launch { binding.mediaSourceTitle.text = it } }
setLanguageList(i, source) setLanguageList(i, source)
} }
subscribeButton(false) subscribeButton(false)
@ -143,17 +170,17 @@ class MangaReadAdapter(
} }
} }
//settings // Settings
binding.animeSourceSettings.setOnClickListener { binding.mediaSourceSettings.setOnClickListener {
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext -> (mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
fragment.openSettings(ext.extension) fragment.openSettings(ext.extension)
} }
} }
//Grids // Grids
subscribe = MediaDetailsActivity.PopImageButton( subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope, fragment.lifecycleScope,
binding.animeSourceSubscribe, binding.mediaSourceSubscribe,
R.drawable.ic_round_notifications_active_24, R.drawable.ic_round_notifications_active_24,
R.drawable.ic_round_notifications_none_24, R.drawable.ic_round_notifications_none_24,
R.color.bg_opp, R.color.bg_opp,
@ -161,206 +188,207 @@ class MangaReadAdapter(
fragment.subscribed, fragment.subscribed,
true true
) { ) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString()) fragment.onNotificationPressed(it, binding.mediaSource.text.toString())
} }
subscribeButton(false) subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener { binding.mediaSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK) openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
} }
binding.animeNestedButton.setOnClickListener { binding.mediaNestedButton.setOnClickListener {
val dialogBinding = DialogLayoutBinding.inflate(fragment.layoutInflater)
val dialogView =
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
val dialogBinding = DialogLayoutBinding.bind(dialogView)
var refresh = false var refresh = false
var run = false var run = false
var reversed = media.selected!!.recyclerReversed var reversed = media.selected!!.recyclerReversed
var style = var style =
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.MangaDefaultView) media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.MangaDefaultView)
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f dialogBinding.apply {
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down" mediaSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.animeSourceTop.setOnClickListener { sortText.text = if (reversed) "Down to Up" else "Up to Down"
reversed = !reversed mediaSourceTop.setOnClickListener {
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f reversed = !reversed
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down" mediaSourceTop.rotation = if (reversed) -90f else 90f
run = true sortText.text = if (reversed) "Down to Up" else "Up to Down"
} run = true
}
//Grids // Grids
dialogBinding.animeSourceGrid.visibility = View.GONE mediaSourceGrid.visibility = View.GONE
var selected = when (style) { var selected = when (style) {
0 -> dialogBinding.animeSourceList 0 -> mediaSourceList
1 -> dialogBinding.animeSourceCompact 1 -> mediaSourceCompact
else -> dialogBinding.animeSourceList else -> mediaSourceList
} }
when (style) { when (style) {
0 -> dialogBinding.layoutText.setText(R.string.list) 0 -> layoutText.setText(R.string.list)
1 -> dialogBinding.layoutText.setText(R.string.compact) 1 -> layoutText.setText(R.string.compact)
else -> dialogBinding.animeSourceList else -> mediaSourceList
} }
selected.alpha = 1f
fun selected(it: ImageButton) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f selected.alpha = 1f
} fun selected(it: ImageButton) {
dialogBinding.animeSourceList.setOnClickListener { selected.alpha = 0.33f
selected(it as ImageButton) selected = it
style = 0 selected.alpha = 1f
dialogBinding.layoutText.setText(R.string.list)
run = true
}
dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 1
dialogBinding.layoutText.setText(R.string.compact)
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast(R.string.webview_not_installed)
} }
//start CookieCatcher activity mediaSourceList.setOnClickListener {
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) { selected(it as ImageButton)
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser style = 0
val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource layoutText.setText(R.string.list)
val url = sourceHttp?.baseUrl run = true
url?.let { }
refresh = true mediaSourceCompact.setOnClickListener {
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java) selected(it as ImageButton)
.putExtra("url", url) style = 1
startActivity(fragment.requireContext(), intent, null) layoutText.setText(R.string.compact)
run = true
}
mediaWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast(R.string.webview_not_installed)
} }
} // Start CookieCatcher activity
} if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
//Multi download val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
dialogBinding.downloadNo.text = "0" val url = sourceHttp?.baseUrl
dialogBinding.animeDownloadTop.setOnClickListener { url?.let {
//Alert dialog asking for the number of chapters to download refresh = true
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup) val intent =
alertDialog.setTitle("Multi Chapter Downloader") Intent(fragment.requireContext(), CookieCatcher::class.java)
alertDialog.setMessage("Enter the number of chapters to download") .putExtra("url", url)
val input = NumberPicker(currContext()) startActivity(fragment.requireContext(), intent, null)
input.minValue = 1
input.maxValue = 20
input.value = 1
alertDialog.setView(input)
alertDialog.setPositiveButton("OK") { _, _ ->
dialogBinding.downloadNo.text = "${input.value}"
}
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
val dialog = alertDialog.show()
dialog.window?.setDimAmount(0.8f)
}
//Scanlator
dialogBinding.animeScanlatorContainer.isVisible = options.count() > 1
dialogBinding.scanlatorNo.text = "${options.count()}"
dialogBinding.animeScanlatorTop.setOnClickListener {
val dialogView2 =
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
val checkboxContainer =
dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
val tickAllButton = dialogView2.findViewById<ImageButton>(R.id.toggleButton)
// Function to get the right image resource for the toggle button
fun getToggleImageResource(container: ViewGroup): Int {
var allChecked = true
var allUnchecked = true
for (i in 0 until container.childCount) {
val checkBox = container.getChildAt(i) as CheckBox
if (!checkBox.isChecked) {
allChecked = false
} else {
allUnchecked = false
} }
} }
return when {
allChecked -> R.drawable.untick_all_boxes
allUnchecked -> R.drawable.tick_all_boxes
else -> R.drawable.invert_all_boxes
}
} }
// Dynamically add checkboxes // Multi download
options.forEach { option -> downloadNo.text = "0"
val checkBox = CheckBox(currContext()).apply { mediaDownloadTop.setOnClickListener {
text = option // Alert dialog asking for the number of chapters to download
setOnCheckedChangeListener { _, _ -> fragment.requireContext().customAlertDialog().apply {
// Update image resource when you change a checkbox setTitle("Multi Chapter Downloader")
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer)) setMessage("Enter the number of chapters to download")
val input = NumberPicker(currContext())
input.minValue = 1
input.maxValue = 20
input.value = 1
setCustomView(input)
setPosButton(R.string.ok) {
downloadNo.text = "${input.value}"
} }
setNegButton(R.string.cancel)
show()
} }
// Set checked if its already selected
if (media.selected!!.scanlators != null) {
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
scanlatorSelectionListener?.onScanlatorsSelected()
} else {
checkBox.isChecked = true
}
checkboxContainer.addView(checkBox)
} }
resetProgress.setOnClickListener {
fragment.requireContext().customAlertDialog().apply {
setTitle(" Delete Progress for all chapters of ${media.nameRomaji}")
setMessage("This will delete all the locally stored progress for chapters")
setPosButton(R.string.ok){
// Usage
clearCustomValsForMedia("${media.id}", "_Chapter")
clearCustomValsForMedia("${media.id}", "_Vol")
// Create AlertDialog snackString("Deleted the progress of Chapters for ${media.nameRomaji}")
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup) }
.setView(dialogView2) setNegButton(R.string.no)
.setPositiveButton("OK") { _, _ -> show()
hiddenScanlators.clear() }
for (i in 0 until checkboxContainer.childCount) { }
val checkBox = checkboxContainer.getChildAt(i) as CheckBox resetProgressDef.text = getString(currContext()!!,R.string.clear_stored_chapter)
// Scanlator
mangaScanlatorContainer.isVisible = options.count() > 1
scanlatorNo.text = "${options.count()}"
mangaScanlatorTop.setOnClickListener {
CustomDialogLayoutBinding.inflate(fragment.layoutInflater)
val dialogView = CustomDialogLayoutBinding.inflate(fragment.layoutInflater)
val checkboxContainer = dialogView.checkboxContainer
val tickAllButton = dialogView.toggleButton
fun getToggleImageResource(container: ViewGroup): Int {
var allChecked = true
var allUnchecked = true
for (i in 0 until container.childCount) {
val checkBox = container.getChildAt(i) as CheckBox
if (!checkBox.isChecked) { if (!checkBox.isChecked) {
hiddenScanlators.add(checkBox.text.toString()) allChecked = false
} else {
allUnchecked = false
} }
} }
fragment.onScanlatorChange(hiddenScanlators) return when {
scanlatorSelectionListener?.onScanlatorsSelected() allChecked -> R.drawable.untick_all_boxes
} allUnchecked -> R.drawable.tick_all_boxes
.setNegativeButton("Cancel", null) else -> R.drawable.invert_all_boxes
.show() }
dialog.window?.setDimAmount(0.8f)
// Standard image resource
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
// Listens to ticked checkboxes and changes image resource accordingly
tickAllButton.setOnClickListener {
// Toggle checkboxes
for (i in 0 until checkboxContainer.childCount) {
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
checkBox.isChecked = !checkBox.isChecked
} }
// Update image resource options.forEach { option ->
val checkBox = CheckBox(currContext()).apply {
text = option
setOnCheckedChangeListener { _, _ ->
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
}
}
if (media.selected!!.scanlators != null) {
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
scanlatorSelectionListener?.onScanlatorsSelected()
} else {
checkBox.isChecked = true
}
checkboxContainer.addView(checkBox)
}
fragment.requireContext().customAlertDialog().apply {
setCustomView(dialogView.root)
setPosButton("OK") {
hiddenScanlators.clear()
for (i in 0 until checkboxContainer.childCount) {
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
if (!checkBox.isChecked) {
hiddenScanlators.add(checkBox.text.toString())
}
}
fragment.onScanlatorChange(hiddenScanlators)
scanlatorSelectionListener?.onScanlatorsSelected()
}
setNegButton("Cancel")
}.show()
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer)) tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
tickAllButton.setOnClickListener {
for (i in 0 until checkboxContainer.childCount) {
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
checkBox.isChecked = !checkBox.isChecked
}
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
}
}
fragment.requireContext().customAlertDialog().apply {
setTitle("Options")
setCustomView(root)
setPosButton("OK") {
if (run) fragment.onIconPressed(style, reversed)
if (downloadNo.text != "0") {
fragment.multiDownload(downloadNo.text.toString().toInt())
}
if (refresh) fragment.loadChapters(source, true)
}
setNegButton("Cancel") {
if (refresh) fragment.loadChapters(source, true)
}
show()
} }
} }
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
.setTitle("Options")
.setView(dialogView)
.setPositiveButton("OK") { _, _ ->
if (run) fragment.onIconPressed(style, reversed)
if (dialogBinding.downloadNo.text != "0") {
fragment.multiDownload(dialogBinding.downloadNo.text.toString().toInt())
}
if (refresh) fragment.loadChapters(source, true)
}
.setNegativeButton("Cancel") { _, _ ->
if (refresh) fragment.loadChapters(source, true)
}
.setOnCancelListener {
if (refresh) fragment.loadChapters(source, true)
}
.create()
nestedDialog?.show()
} }
//Chapter Handling // Chapter Handling
handleChapters() handleChapters()
} }
@ -368,7 +396,7 @@ class MangaReadAdapter(
subscribe?.enabled(enabled) subscribe?.enabled(enabled)
} }
//Chips // Chips
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) { fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding val binding = _binding
if (binding != null) { if (binding != null) {
@ -379,13 +407,13 @@ class MangaReadAdapter(
val chip = val chip =
ItemChipBinding.inflate( ItemChipBinding.inflate(
LayoutInflater.from(fragment.context), LayoutInflater.from(fragment.context),
binding.animeSourceChipGroup, binding.mediaSourceChipGroup,
false false
).root ).root
chip.isCheckable = true chip.isCheckable = true
fun selected() { fun selected() {
chip.isChecked = true chip.isChecked = true
binding.animeWatchChipScroll.smoothScrollTo( binding.mediaWatchChipScroll.smoothScrollTo(
(chip.left - screenWidth / 2) + (chip.width / 2), (chip.left - screenWidth / 2) + (chip.width / 2),
0 0
) )
@ -403,7 +431,7 @@ class MangaReadAdapter(
} else { } else {
names[last - 1] names[last - 1]
} }
//chip.text = "${names[limit * (position)]} - ${names[last - 1]}" // chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
val chipText = "$startChapterString - $endChapterString" val chipText = "$startChapterString - $endChapterString"
chip.text = chipText chip.text = chipText
chip.setTextColor( chip.setTextColor(
@ -417,14 +445,14 @@ class MangaReadAdapter(
selected() selected()
fragment.onChipClicked(position, limit * (position), last - 1) fragment.onChipClicked(position, limit * (position), last - 1)
} }
binding.animeSourceChipGroup.addView(chip) binding.mediaSourceChipGroup.addView(chip)
if (selected == position) { if (selected == position) {
selected() selected()
select = chip select = chip
} }
} }
if (select != null) if (select != null)
binding.animeWatchChipScroll.apply { binding.mediaWatchChipScroll.apply {
post { post {
scrollTo( scrollTo(
(select.left - screenWidth / 2) + (select.width / 2), (select.left - screenWidth / 2) + (select.width / 2),
@ -436,7 +464,7 @@ class MangaReadAdapter(
} }
fun clearChips() { fun clearChips() {
_binding?.animeSourceChipGroup?.removeAllViews() _binding?.mediaSourceChipGroup?.removeAllViews()
} }
fun handleChapters() { fun handleChapters() {
@ -462,70 +490,86 @@ class MangaReadAdapter(
} }
if (formattedChapters.contains(continueEp)) { if (formattedChapters.contains(continueEp)) {
continueEp = chapters[formattedChapters.indexOf(continueEp)] continueEp = chapters[formattedChapters.indexOf(continueEp)]
binding.animeSourceContinue.visibility = View.VISIBLE binding.sourceContinue.visibility = View.VISIBLE
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemMediaProgressCont,
binding.itemEpisodeProgress, binding.itemMediaProgress,
binding.itemEpisodeProgressEmpty, binding.itemMediaProgressEmpty,
media.id, media.id,
continueEp continueEp
) )
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > 0.8f) { if ((binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams).weight > 0.8f) {
val e = chapters.indexOf(continueEp) val e = chapters.indexOf(continueEp)
if (e != -1 && e + 1 < chapters.size) { if (e != -1 && e + 1 < chapters.size) {
continueEp = chapters[e + 1] continueEp = chapters[e + 1]
} }
} }
val ep = media.manga.chapters!![continueEp]!! val ep = media.manga.chapters!![continueEp]!!
binding.itemEpisodeImage.loadImage(media.banner ?: media.cover) binding.itemMediaImage.loadImage(media.banner ?: media.cover)
binding.animeSourceContinueText.text = binding.mediaSourceContinueText.text =
currActivity()!!.getString( currActivity()!!.getString(
R.string.continue_chapter, R.string.continue_chapter,
ep.number, ep.number,
if (!ep.title.isNullOrEmpty()) ep.title else "" if (!ep.title.isNullOrEmpty()) ep.title else ""
) )
binding.animeSourceContinue.setOnClickListener { binding.sourceContinue.setOnClickListener {
fragment.onMangaChapterClick(continueEp) fragment.onMangaChapterClick(continueEp)
} }
if (fragment.continueEp) { if (fragment.continueEp) {
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < 0.8f) { if ((binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams).weight < 0.8f) {
binding.animeSourceContinue.performClick() binding.sourceContinue.performClick()
fragment.continueEp = false fragment.continueEp = false
} }
} }
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.sourceContinue.visibility = View.GONE
} }
binding.animeSourceProgressBar.visibility = View.GONE
val sourceFound = media.manga.chapters!!.isNotEmpty() binding.sourceProgressBar.visibility = View.GONE
binding.animeSourceNotFound.isGone = sourceFound
val sourceFound = filteredChapters.isNotEmpty()
val isDownloadedSource = mangaReadSources[media.selected!!.sourceIndex] is OfflineMangaParser
if (isDownloadedSource) {
binding.sourceNotFound.text = if (sourceFound) {
currActivity()!!.getString(R.string.source_not_found)
} else {
currActivity()!!.getString(R.string.download_not_found)
}
} else {
binding.sourceNotFound.text = currActivity()!!.getString(R.string.source_not_found)
}
binding.sourceNotFound.isGone = sourceFound
binding.faqbutton.isGone = sourceFound binding.faqbutton.isGone = sourceFound
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources)) { if (!sourceFound && PrefManager.getVal(PrefName.SearchSources)) {
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) { if (binding.mediaSource.adapter.count > media.selected!!.sourceIndex + 1) {
val nextIndex = media.selected!!.sourceIndex + 1 val nextIndex = media.selected!!.sourceIndex + 1
binding.animeSource.setText( binding.mediaSource.setText(
binding.animeSource.adapter binding.mediaSource.adapter
.getItem(nextIndex).toString(), false .getItem(nextIndex).toString(), false
) )
fragment.onSourceChange(nextIndex).apply { fragment.onSourceChange(nextIndex).apply {
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } } { MainScope().launch { binding.mediaSourceTitle.text = it } }
setLanguageList(0, nextIndex) setLanguageList(0, nextIndex)
} }
subscribeButton(false) subscribeButton(false)
// invalidate if it's the last source // Invalidate if it's the last source
val invalidate = nextIndex == mangaReadSources.names.size - 1 val invalidate = nextIndex == mangaReadSources.names.size - 1
fragment.loadChapters(nextIndex, invalidate) fragment.loadChapters(nextIndex, invalidate)
} }
} }
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.sourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE binding.sourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE binding.faqbutton.visibility = View.GONE
clearChips() clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE binding.sourceProgressBar.visibility = View.VISIBLE
} }
} }
} }
@ -539,9 +583,9 @@ class MangaReadAdapter(
ext.sourceLanguage = lang ext.sourceLanguage = lang
} }
try { try {
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang) binding?.mediaSourceLanguage?.setText(parser.extension.sources[lang].lang)
} catch (e: IndexOutOfBoundsException) { } catch (e: IndexOutOfBoundsException) {
binding?.animeSourceLanguage?.setText( binding?.mediaSourceLanguage?.setText(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown" parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
) )
} }
@ -551,9 +595,9 @@ class MangaReadAdapter(
parser.extension.sources.map { LanguageMapper.getLanguageName(it.lang) } parser.extension.sources.map { LanguageMapper.getLanguageName(it.lang) }
) )
val items = adapter.count val items = adapter.count
binding?.animeSourceLanguageContainer?.isVisible = items > 1 binding?.mediaSourceLanguageContainer?.isVisible = items > 1
binding?.animeSourceLanguage?.setAdapter(adapter) binding?.mediaSourceLanguage?.setAdapter(adapter)
} }
} }
@ -561,7 +605,7 @@ class MangaReadAdapter(
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : inner class ViewHolder(val binding: ItemMediaSourceBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
} }

View file

@ -2,7 +2,6 @@ package ani.dantotsu.media.manga
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -31,7 +30,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentMediaSourceBinding
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName import ani.dantotsu.download.DownloadsManager.Companion.compareName
@ -60,6 +59,7 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
@ -74,7 +74,7 @@ import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
private var _binding: FragmentAnimeWatchBinding? = null private var _binding: FragmentMediaSourceBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels() private val model: MediaDetailsViewModel by activityViewModels()
@ -101,7 +101,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
_binding = FragmentAnimeWatchBinding.inflate(inflater, container, false) _binding = FragmentMediaSourceBinding.inflate(inflater, container, false)
return _binding?.root return _binding?.root
} }
@ -121,7 +121,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
ContextCompat.RECEIVER_EXPORTED ContextCompat.RECEIVER_EXPORTED
) )
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.mediaSourceRecycler.updatePadding(bottom = binding.mediaSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp screenWidth = resources.displayMetrics.widthPixels.dp
var maxGridSize = (screenWidth / 100f).roundToInt() var maxGridSize = (screenWidth / 100f).roundToInt()
@ -144,13 +144,13 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
} }
} }
binding.animeSourceRecycler.layoutManager = gridLayoutManager binding.mediaSourceRecycler.layoutManager = gridLayoutManager
binding.ScrollTop.setOnClickListener { binding.ScrollTop.setOnClickListener {
binding.animeSourceRecycler.scrollToPosition(10) binding.mediaSourceRecycler.scrollToPosition(10)
binding.animeSourceRecycler.smoothScrollToPosition(0) binding.mediaSourceRecycler.smoothScrollToPosition(0)
} }
binding.animeSourceRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { binding.mediaSourceRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
@ -164,7 +164,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
} }
}) })
model.scrolledToTop.observe(viewLifecycleOwner) { model.scrolledToTop.observe(viewLifecycleOwner) {
if (it) binding.animeSourceRecycler.scrollToPosition(0) if (it) binding.mediaSourceRecycler.scrollToPosition(0)
} }
continueEp = model.continueMedia ?: false continueEp = model.continueMedia ?: false
@ -199,7 +199,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
} }
} }
binding.animeSourceRecycler.adapter = binding.mediaSourceRecycler.adapter =
ConcatAdapter(headerAdapter, chapterAdapter) ConcatAdapter(headerAdapter, chapterAdapter)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@ -214,8 +214,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
reload() reload()
} }
} else { } else {
binding.animeNotSupported.visibility = View.VISIBLE binding.mediaNotSupported.visibility = View.VISIBLE
binding.animeNotSupported.text = binding.mediaNotSupported.text =
getString(R.string.not_supported, media.format ?: "") getString(R.string.not_supported, media.format ?: "")
} }
} }
@ -231,10 +231,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
} }
fun multiDownload(n: Int) { fun multiDownload(n: Int) {
//get last viewed chapter // Get last viewed chapter
val selected = media.userProgress val selected = media.userProgress
val chapters = media.manga?.chapters?.values?.toList() val chapters = media.manga?.chapters?.values?.toList()
//filter by selected language // Filter by selected language
val progressChapterIndex = (chapters?.indexOfFirst { val progressChapterIndex = (chapters?.indexOfFirst {
MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
} ?: 0) + 1 } ?: 0) + 1
@ -244,7 +244,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
// Calculate the end index // Calculate the end index
val endIndex = minOf(progressChapterIndex + n, chapters.size) val endIndex = minOf(progressChapterIndex + n, chapters.size)
//make sure there are enough chapters // Make sure there are enough chapters
val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex) val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex)
@ -386,32 +386,30 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
if (allSettings.size > 1) { if (allSettings.size > 1) {
val names = val names =
allSettings.map { LanguageMapper.getLanguageName(it.lang) }.toTypedArray() allSettings.map { LanguageMapper.getLanguageName(it.lang) }.toTypedArray()
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) requireContext().customAlertDialog().apply {
.setTitle("Select a Source") setTitle("Select a Source")
.setSingleChoiceItems(names, -1) { dialog, which -> singleChoiceItems(names) { which ->
selectedSetting = allSettings[which] selectedSetting = allSettings[which]
itemSelected = true itemSelected = true
dialog.dismiss()
// Move the fragment transaction here val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
val fragment = changeUIVisibility(true)
MangaSourcePreferencesFragment().getInstance(selectedSetting.id) { loadChapters(media.selected!!.sourceIndex, true)
changeUIVisibility(true) }
loadChapters(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction() parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down) .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment) .replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null) .addToBackStack(null)
.commit() .commit()
} }
.setOnDismissListener { onDismiss{
if (!itemSelected) { if (!itemSelected) {
changeUIVisibility(true) changeUIVisibility(true)
} }
} }
.show() show()
dialog.window?.setDimAmount(0.8f)
}
} else { } else {
// If there's only one setting, proceed with the fragment transaction // If there's only one setting, proceed with the fragment transaction
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) { val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
@ -584,7 +582,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
private fun reload() { private fun reload() {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
//Find latest chapter for subscription // Find latest chapter for subscription
selected.latest = selected.latest =
media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest = selected.latest =
@ -618,14 +616,14 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.mediaInfoProgressBar.visibility = progress binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state) binding.mediaSourceRecycler.layoutManager?.onRestoreInstanceState(state)
requireActivity().setNavigationTheme() requireActivity().setNavigationTheme()
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() state = binding.mediaSourceRecycler.layoutManager?.onSaveInstanceState()
} }
companion object { companion object {

View file

@ -58,6 +58,8 @@ import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaNameAdapter import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaSingleton import ani.dantotsu.media.MediaSingleton
import ani.dantotsu.media.anime.ExoplayerView
import ani.dantotsu.media.anime.ExoplayerView.Companion
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
@ -83,6 +85,7 @@ import ani.dantotsu.showSystemBarsRetractView
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.tryWith import ani.dantotsu.tryWith
import ani.dantotsu.util.customAlertDialog
import com.alexvasilkov.gestures.views.GestureFrameLayout import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
@ -184,6 +187,8 @@ class MangaReaderActivity : AppCompatActivity() {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
defaultSettings = loadReaderSettings("reader_settings") ?: defaultSettings defaultSettings = loadReaderSettings("reader_settings") ?: defaultSettings
onBackPressedDispatcher.addCallback(this) { onBackPressedDispatcher.addCallback(this) {
@ -258,7 +263,16 @@ class MangaReaderActivity : AppCompatActivity() {
} }
else model.getMedia().value ?: return else model.getMedia().value ?: return
model.setMedia(media) model.setMedia(media)
@Suppress("UNCHECKED_CAST")
val list = (PrefManager.getNullableCustomVal(
"continueMangaList",
listOf<Int>(),
List::class.java
) as List<Int>).toMutableList()
if (list.contains(media.id)) list.remove(media.id)
list.add(media.id)
PrefManager.setCustomVal("continueMangaList", list)
if (PrefManager.getVal(PrefName.AutoDetectWebtoon) && media.countryOfOrigin != "JP") applyWebtoon( if (PrefManager.getVal(PrefName.AutoDetectWebtoon) && media.countryOfOrigin != "JP") applyWebtoon(
defaultSettings defaultSettings
) )
@ -400,7 +414,8 @@ class MangaReaderActivity : AppCompatActivity() {
val context = this val context = this
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode) val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito) val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if ((isOnline(context) && !offline) && Discord.token != null && !incognito) { val rpcenabled: Boolean = PrefManager.getVal(PrefName.rpcEnabled)
if ((isOnline(context) && !offline) && Discord.token != null && !incognito && rpcenabled) {
lifecycleScope.launch { lifecycleScope.launch {
val discordMode = PrefManager.getCustomVal("discord_mode", "dantotsu") val discordMode = PrefManager.getCustomVal("discord_mode", "dantotsu")
val buttons = when (discordMode) { val buttons = when (discordMode) {
@ -1013,28 +1028,27 @@ class MangaReaderActivity : AppCompatActivity() {
PrefManager.setCustomVal("${media.id}_progressDialog", !isChecked) PrefManager.setCustomVal("${media.id}_progressDialog", !isChecked)
showProgressDialog = !isChecked showProgressDialog = !isChecked
} }
AlertDialog.Builder(this, R.style.MyPopup) customAlertDialog().apply {
.setTitle(getString(R.string.title_update_progress)) setTitle(R.string.title_update_progress)
.setView(dialogView) setCustomView(dialogView)
.setCancelable(false) setCancelable(false)
.setPositiveButton(getString(R.string.yes)) { dialog, _ -> setPosButton(R.string.yes) {
PrefManager.setCustomVal("${media.id}_save_progress", true) PrefManager.setCustomVal("${media.id}_save_progress", true)
updateProgress( updateProgress(
media, media,
MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!) MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
.toString() .toString()
) )
dialog.dismiss()
runnable.run() runnable.run()
} }
.setNegativeButton(getString(R.string.no)) { dialog, _ -> setNegButton(R.string.no) {
PrefManager.setCustomVal("${media.id}_save_progress", false) PrefManager.setCustomVal("${media.id}_save_progress", false)
dialog.dismiss()
runnable.run() runnable.run()
} }
.setOnCancelListener { hideSystemBars() } setOnCancelListener { hideSystemBars() }
.create() show()
.show()
}
} else { } else {
if (!incognito && PrefManager.getCustomVal( if (!incognito && PrefManager.getCustomVal(
"${media.id}_save_progress", "${media.id}_save_progress",

View file

@ -8,6 +8,7 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewConfiguration import android.view.ViewConfiguration
import android.widget.FrameLayout import android.widget.FrameLayout
import kotlin.math.abs
class Swipy @JvmOverloads constructor( class Swipy @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
@ -16,7 +17,6 @@ class Swipy @JvmOverloads constructor(
var dragDivider: Int = 5 var dragDivider: Int = 5
var vertical = true var vertical = true
//public, in case a different sub child needs to be considered
var child: View? = getChildAt(0) var child: View? = getChildAt(0)
var topBeingSwiped: ((Float) -> Unit) = {} var topBeingSwiped: ((Float) -> Unit) = {}
@ -29,49 +29,47 @@ class Swipy @JvmOverloads constructor(
var rightBeingSwiped: ((Float) -> Unit) = {} var rightBeingSwiped: ((Float) -> Unit) = {}
companion object { companion object {
private const val DRAG_RATE = .5f private const val DRAG_RATE = 0.5f
private const val INVALID_POINTER = -1 private const val INVALID_POINTER = -1
} }
private var touchSlop = ViewConfiguration.get(context).scaledTouchSlop private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
private var activePointerId = INVALID_POINTER private var activePointerId = INVALID_POINTER
private var isBeingDragged = false private var isBeingDragged = false
private var initialDown = 0f private var initialDown = 0f
private var initialMotion = 0f private var initialMotion = 0f
enum class VerticalPosition { private enum class VerticalPosition { Top, None, Bottom }
Top, private enum class HorizontalPosition { Left, None, Right }
None,
Bottom
}
enum class HorizontalPosition {
Left,
None,
Right
}
private var horizontalPos = HorizontalPosition.None private var horizontalPos = HorizontalPosition.None
private var verticalPos = VerticalPosition.None private var verticalPos = VerticalPosition.None
private fun setChildPosition() { private fun setChildPosition() {
child?.apply { child?.let {
if (vertical) { if (vertical) {
verticalPos = VerticalPosition.None verticalPos = when {
if (!canScrollVertically(1)) { !it.canScrollVertically(1) && !it.canScrollVertically(-1) -> {
verticalPos = VerticalPosition.Bottom if (initialDown > (Resources.getSystem().displayMetrics.heightPixels / 2))
} VerticalPosition.Bottom
if (!canScrollVertically(-1)) { else
verticalPos = VerticalPosition.Top VerticalPosition.Top
}
!it.canScrollVertically(1) -> VerticalPosition.Bottom
!it.canScrollVertically(-1) -> VerticalPosition.Top
else -> VerticalPosition.None
} }
} else { } else {
horizontalPos = HorizontalPosition.None horizontalPos = when {
if (!canScrollHorizontally(1)) { !it.canScrollHorizontally(1) && !it.canScrollHorizontally(-1) -> {
horizontalPos = HorizontalPosition.Right if (initialDown > (Resources.getSystem().displayMetrics.widthPixels / 2))
} HorizontalPosition.Right
if (!canScrollHorizontally(-1)) { else
horizontalPos = HorizontalPosition.Left HorizontalPosition.Left
}
!it.canScrollHorizontally(1) -> HorizontalPosition.Right
!it.canScrollHorizontally(-1) -> HorizontalPosition.Left
else -> HorizontalPosition.None
} }
} }
} }
@ -85,44 +83,26 @@ class Swipy @JvmOverloads constructor(
private fun onSecondaryPointerUp(ev: MotionEvent) { private fun onSecondaryPointerUp(ev: MotionEvent) {
val pointerIndex = ev.actionIndex val pointerIndex = ev.actionIndex
val pointerId = ev.getPointerId(pointerIndex) if (ev.getPointerId(pointerIndex) == activePointerId) {
if (pointerId == activePointerId) { activePointerId = ev.getPointerId(if (pointerIndex == 0) 1 else 0)
val newPointerIndex = if (pointerIndex == 0) 1 else 0
activePointerId = ev.getPointerId(newPointerIndex)
} }
} }
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
val action = ev.actionMasked if (!isEnabled || canChildScroll()) return false
val pointerIndex: Int
if (!isEnabled || canChildScroll()) {
return false
}
when (action) { when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
activePointerId = ev.getPointerId(0) activePointerId = ev.getPointerId(0)
initialDown = if (vertical) ev.getY(0) else ev.getX(0)
isBeingDragged = false isBeingDragged = false
pointerIndex = ev.findPointerIndex(activePointerId)
if (pointerIndex < 0) {
return false
}
initialDown = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
if (activePointerId == INVALID_POINTER) { val pointerIndex = ev.findPointerIndex(activePointerId)
//("Got ACTION_MOVE event but don't have an active pointer id.") if (pointerIndex >= 0) {
return false startDragging(if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex))
} }
pointerIndex = ev.findPointerIndex(activePointerId)
if (pointerIndex < 0) {
return false
}
val pos = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
startDragging(pos)
} }
MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev) MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
isBeingDragged = false isBeingDragged = false
@ -134,127 +114,97 @@ class Swipy @JvmOverloads constructor(
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(ev: MotionEvent): Boolean { override fun onTouchEvent(ev: MotionEvent): Boolean {
val action = ev.actionMasked if (!isEnabled || canChildScroll()) return false
val pointerIndex: Int val pointerIndex: Int
if (!isEnabled || canChildScroll()) { when (ev.actionMasked) {
return false
}
when (action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
activePointerId = ev.getPointerId(0) activePointerId = ev.getPointerId(0)
isBeingDragged = false isBeingDragged = false
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
pointerIndex = ev.findPointerIndex(activePointerId) pointerIndex = ev.findPointerIndex(activePointerId)
if (pointerIndex < 0) { if (pointerIndex >= 0) {
//("Got ACTION_MOVE event but have an invalid active pointer id.") val pos = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
return false startDragging(pos)
} if (isBeingDragged) handleDrag(pos)
val pos = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
startDragging(pos)
if (isBeingDragged) {
val overscroll = (
if (vertical)
if (verticalPos == VerticalPosition.Top) pos - initialMotion else initialMotion - pos
else
if (horizontalPos == HorizontalPosition.Left) pos - initialMotion else initialMotion - pos
) * DRAG_RATE
if (overscroll > 0) {
parent.requestDisallowInterceptTouchEvent(true)
if (vertical) {
val totalDragDistance =
Resources.getSystem().displayMetrics.heightPixels / dragDivider
if (verticalPos == VerticalPosition.Top)
topBeingSwiped.invoke(overscroll * 2 / totalDragDistance)
else
bottomBeingSwiped.invoke(overscroll * 2 / totalDragDistance)
} else {
val totalDragDistance =
Resources.getSystem().displayMetrics.widthPixels / dragDivider
if (horizontalPos == HorizontalPosition.Left)
leftBeingSwiped.invoke(overscroll / totalDragDistance)
else
rightBeingSwiped.invoke(overscroll / totalDragDistance)
}
} else {
return false
}
} }
} }
MotionEvent.ACTION_POINTER_DOWN -> { MotionEvent.ACTION_POINTER_DOWN -> {
pointerIndex = ev.actionIndex pointerIndex = ev.actionIndex
if (pointerIndex < 0) { if (pointerIndex >= 0) activePointerId = ev.getPointerId(pointerIndex)
//("Got ACTION_POINTER_DOWN event but have an invalid action index.")
return false
}
activePointerId = ev.getPointerId(pointerIndex)
} }
MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev) MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
if (vertical) { resetSwipes()
topBeingSwiped.invoke(0f)
bottomBeingSwiped.invoke(0f)
} else {
rightBeingSwiped.invoke(0f)
leftBeingSwiped.invoke(0f)
}
pointerIndex = ev.findPointerIndex(activePointerId) pointerIndex = ev.findPointerIndex(activePointerId)
if (pointerIndex < 0) { if (pointerIndex >= 0) finishSpinner(if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex))
//("Got ACTION_UP event but don't have an active pointer id.")
return false
}
if (isBeingDragged) {
val pos = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
val overscroll = (
if (vertical)
if (verticalPos == VerticalPosition.Top) pos - initialMotion else initialMotion - pos
else
if (horizontalPos == HorizontalPosition.Left) pos - initialMotion else initialMotion - pos
) * DRAG_RATE
isBeingDragged = false
finishSpinner(overscroll)
}
activePointerId = INVALID_POINTER activePointerId = INVALID_POINTER
return false return false
} }
MotionEvent.ACTION_CANCEL -> return false MotionEvent.ACTION_CANCEL -> return false
} }
return true return true
} }
private fun startDragging(pos: Float) { private fun startDragging(pos: Float) {
val posDiff = val posDiff = if ((vertical && verticalPos == VerticalPosition.Top) || (!vertical && horizontalPos == HorizontalPosition.Left))
if ((vertical && verticalPos == VerticalPosition.Top) || (!vertical && horizontalPos == HorizontalPosition.Left)) pos - initialDown
pos - initialDown else
else initialDown - pos
initialDown - pos
if (posDiff > touchSlop && !isBeingDragged) { if (posDiff > touchSlop && !isBeingDragged) {
initialMotion = initialDown + touchSlop initialMotion = initialDown + touchSlop
isBeingDragged = true isBeingDragged = true
} }
} }
private fun finishSpinner(overscrollDistance: Float) { private fun handleDrag(pos: Float) {
val overscroll = abs((pos - initialMotion) * DRAG_RATE)
parent.requestDisallowInterceptTouchEvent(true)
if (vertical) { if (vertical) {
val totalDragDistance = Resources.getSystem().displayMetrics.heightPixels / dragDivider val totalDragDistance = Resources.getSystem().displayMetrics.heightPixels / dragDivider
if (overscrollDistance * 2 > totalDragDistance) if (verticalPos == VerticalPosition.Top)
topBeingSwiped.invoke(overscroll * 2 / totalDragDistance)
else
bottomBeingSwiped.invoke(overscroll * 2 / totalDragDistance)
} else {
val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider
if (horizontalPos == HorizontalPosition.Left)
leftBeingSwiped.invoke(overscroll / totalDragDistance)
else
rightBeingSwiped.invoke(overscroll / totalDragDistance)
}
}
private fun resetSwipes() {
if (vertical) {
topBeingSwiped.invoke(0f)
bottomBeingSwiped.invoke(0f)
} else {
rightBeingSwiped.invoke(0f)
leftBeingSwiped.invoke(0f)
}
}
private fun finishSpinner(overscrollDistance: Float) {
if (vertical) {
val totalDragDistance = Resources.getSystem().displayMetrics.heightPixels / dragDivider
val swipeDistance = abs(overscrollDistance - initialMotion)
if (swipeDistance > totalDragDistance) {
if (verticalPos == VerticalPosition.Top) if (verticalPos == VerticalPosition.Top)
onTopSwiped.invoke() onTopSwiped.invoke()
else else
onBottomSwiped.invoke() onBottomSwiped.invoke()
}
} else { } else {
val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider
if (overscrollDistance > totalDragDistance) val swipeDistance = abs(overscrollDistance - initialMotion)
if (swipeDistance > totalDragDistance) {
if (horizontalPos == HorizontalPosition.Left) if (horizontalPos == HorizontalPosition.Left)
onLeftSwiped.invoke() onLeftSwiped.invoke()
else else
onRightSwiped.invoke() onRightSwiped.invoke()
}
} }
} }
} }

View file

@ -50,16 +50,16 @@ class NovelReadAdapter(
val source = val source =
media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it } media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it }
if (novelReadSources.names.isNotEmpty() && source in 0 until novelReadSources.names.size) { if (novelReadSources.names.isNotEmpty() && source in 0 until novelReadSources.names.size) {
binding.animeSource.setText(novelReadSources.names[source], false) binding.mediaSource.setText(novelReadSources.names[source], false)
} }
binding.animeSource.setAdapter( binding.mediaSource.setAdapter(
ArrayAdapter( ArrayAdapter(
fragment.requireContext(), fragment.requireContext(),
R.layout.item_dropdown, R.layout.item_dropdown,
novelReadSources.names novelReadSources.names
) )
) )
binding.animeSource.setOnItemClickListener { _, _, i, _ -> binding.mediaSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i) fragment.onSourceChange(i)
search() search()
} }

View file

@ -20,7 +20,7 @@ import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentMediaSourceBinding
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.novel.NovelDownloaderService import ani.dantotsu.download.novel.NovelDownloaderService
@ -47,7 +47,7 @@ class NovelReadFragment : Fragment(),
DownloadTriggerCallback, DownloadTriggerCallback,
DownloadedCheckCallback { DownloadedCheckCallback {
private var _binding: FragmentAnimeWatchBinding? = null private var _binding: FragmentMediaSourceBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels() private val model: MediaDetailsViewModel by activityViewModels()
@ -214,11 +214,11 @@ class NovelReadFragment : Fragment(),
ContextCompat.RECEIVER_EXPORTED ContextCompat.RECEIVER_EXPORTED
) )
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.mediaSourceRecycler.updatePadding(bottom = binding.mediaSourceRecycler.paddingBottom + navBarHeight)
binding.animeSourceRecycler.layoutManager = LinearLayoutManager(requireContext()) binding.mediaSourceRecycler.layoutManager = LinearLayoutManager(requireContext())
model.scrolledToTop.observe(viewLifecycleOwner) { model.scrolledToTop.observe(viewLifecycleOwner) {
if (it) binding.animeSourceRecycler.scrollToPosition(0) if (it) binding.mediaSourceRecycler.scrollToPosition(0)
} }
continueEp = model.continueMedia ?: false continueEp = model.continueMedia ?: false
@ -237,7 +237,7 @@ class NovelReadFragment : Fragment(),
this, this,
this this
) // probably a better way to do this but it works ) // probably a better way to do this but it works
binding.animeSourceRecycler.adapter = binding.mediaSourceRecycler.adapter =
ConcatAdapter(headerAdapter, novelResponseAdapter) ConcatAdapter(headerAdapter, novelResponseAdapter)
loaded = true loaded = true
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
@ -290,7 +290,7 @@ class NovelReadFragment : Fragment(),
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
_binding = FragmentAnimeWatchBinding.inflate(inflater, container, false) _binding = FragmentMediaSourceBinding.inflate(inflater, container, false)
return _binding?.root return _binding?.root
} }
@ -304,12 +304,12 @@ class NovelReadFragment : Fragment(),
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.mediaInfoProgressBar.visibility = progress binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state) binding.mediaSourceRecycler.layoutManager?.onRestoreInstanceState(state)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() state = binding.mediaSourceRecycler.layoutManager?.onSaveInstanceState()
} }
companion object { companion object {

View file

@ -13,6 +13,7 @@ import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
@ -38,7 +39,7 @@ class NovelResponseAdapter(
val binding = holder.binding val binding = holder.binding
val novel = list[position] val novel = list[position]
setAnimation(fragment.requireContext(), holder.binding.root) setAnimation(fragment.requireContext(), holder.binding.root)
binding.itemEpisodeImage.loadImage(novel.coverUrl, 400, 0) binding.itemMediaImage.loadImage(novel.coverUrl, 400, 0)
val color =fragment.requireContext().getThemeColor(com.google.android.material.R.attr.colorOnBackground) val color =fragment.requireContext().getThemeColor(com.google.android.material.R.attr.colorOnBackground)
binding.itemEpisodeTitle.text = novel.name binding.itemEpisodeTitle.text = novel.name
@ -93,27 +94,22 @@ class NovelResponseAdapter(
} }
binding.root.setOnLongClickListener { binding.root.setOnLongClickListener {
val builder = androidx.appcompat.app.AlertDialog.Builder( it.context.customAlertDialog().apply {
fragment.requireContext(), setTitle("Delete ${novel.name}?")
R.style.MyPopup setMessage("Are you sure you want to delete ${novel.name}?")
) setPosButton(R.string.yes) {
builder.setTitle("Delete ${novel.name}?") downloadedCheckCallback.deleteDownload(novel)
builder.setMessage("Are you sure you want to delete ${novel.name}?") deleteDownload(novel.link)
builder.setPositiveButton("Yes") { _, _ -> snackString("Deleted ${novel.name}")
downloadedCheckCallback.deleteDownload(novel) if (binding.itemEpisodeFiller.text.toString()
deleteDownload(novel.link) .contains("Download", ignoreCase = true)
snackString("Deleted ${novel.name}") ) {
if (binding.itemEpisodeFiller.text.toString() binding.itemEpisodeFiller.text = ""
.contains("Download", ignoreCase = true) }
) {
binding.itemEpisodeFiller.text = ""
} }
setNegButton(R.string.no)
show()
} }
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true true
} }
} }

View file

@ -2,6 +2,7 @@ package ani.dantotsu.media.novel.novelreader
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
@ -45,12 +46,17 @@ import ani.dantotsu.tryWith
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import com.vipulog.ebookreader.Book import com.vipulog.ebookreader.Book
import com.vipulog.ebookreader.EbookReaderEventListener import com.vipulog.ebookreader.EbookReaderEventListener
import com.vipulog.ebookreader.EbookReaderView
import com.vipulog.ebookreader.ReaderError import com.vipulog.ebookreader.ReaderError
import com.vipulog.ebookreader.ReaderFlow import com.vipulog.ebookreader.ReaderFlow
import com.vipulog.ebookreader.ReaderTheme import com.vipulog.ebookreader.ReaderTheme
import com.vipulog.ebookreader.RelocationInfo import com.vipulog.ebookreader.RelocationInfo
import com.vipulog.ebookreader.TocItem import com.vipulog.ebookreader.TocItem
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -190,6 +196,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
private fun setupViews() { private fun setupViews() {
binding.bookReader.useSafeScope(this)
scope.launch { binding.bookReader.openBook(intent.data!!) } scope.launch { binding.bookReader.openBook(intent.data!!) }
binding.bookReader.setEbookReaderListener(this) binding.bookReader.setEbookReaderListener(this)
@ -540,4 +548,42 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
hideSystemBars() hideSystemBars()
} }
} }
} }
/**
* TEMPORARY HOTFIX
*
* This is a hacky workaround to handle crashes in the deprecated ebookreader library.
*
* Current implementation:
* - Uses reflection to access the private `scope` field in `EbookReaderView`.
* - Replaces the existing `CoroutineScope` with a new one that includes a
* `CoroutineExceptionHandler`.
* - Ensures that uncaught exceptions in coroutines are handled gracefully by showing a snackbar
* with error details.
*
* TODO:
* - This is NOT a long-term solution
* - The underlying library is archived and unmaintained
* - Schedule migration to an actively maintained library
* - Consider alternatives like https://github.com/readium/kotlin-toolkit
*/
fun EbookReaderView.useSafeScope(activity: Activity) {
runCatching {
val scopeField = javaClass.getDeclaredField("scope").apply { isAccessible = true }
val currentScope = scopeField.get(this) as CoroutineScope
val safeScope = CoroutineScope(
SupervisorJob() +
currentScope.coroutineContext.minusKey(Job) +
scopeExceptionHandler(activity)
)
scopeField.set(this, safeScope)
}.onFailure { e ->
snackString(e.localizedMessage, activity, e.stackTraceToString())
}
}
private fun scopeExceptionHandler(activity: Activity) = CoroutineExceptionHandler { _, e ->
snackString(e.localizedMessage, activity, e.stackTraceToString())
}

View file

@ -47,6 +47,7 @@ class ListActivity : AppCompatActivity() {
window.statusBarColor = primaryColor window.statusBarColor = primaryColor
window.navigationBarColor = primaryColor window.navigationBarColor = primaryColor
binding.listed.visibility = View.GONE
binding.listTabLayout.setBackgroundColor(primaryColor) binding.listTabLayout.setBackgroundColor(primaryColor)
binding.listAppBar.setBackgroundColor(primaryColor) binding.listAppBar.setBackgroundColor(primaryColor)
binding.listTitle.setTextColor(primaryTextColor) binding.listTitle.setTextColor(primaryTextColor)

View file

@ -20,21 +20,18 @@ class AlarmManagerScheduler(private val context: Context) : TaskScheduler {
return return
} }
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = when (taskType) {
TaskType.COMMENT_NOTIFICATION -> Intent(
context,
CommentNotificationReceiver::class.java
)
TaskType.ANILIST_NOTIFICATION -> Intent( val intent = when {
context, taskType == TaskType.COMMENT_NOTIFICATION && PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1 ->
AnilistNotificationReceiver::class.java Intent(context, CommentNotificationReceiver::class.java)
)
TaskType.SUBSCRIPTION_NOTIFICATION -> Intent( taskType == TaskType.ANILIST_NOTIFICATION ->
context, Intent(context, AnilistNotificationReceiver::class.java)
SubscriptionNotificationReceiver::class.java
) taskType == TaskType.SUBSCRIPTION_NOTIFICATION ->
Intent(context, SubscriptionNotificationReceiver::class.java)
else -> return
} }
val pendingIntent = PendingIntent.getBroadcast( val pendingIntent = PendingIntent.getBroadcast(
@ -64,21 +61,18 @@ class AlarmManagerScheduler(private val context: Context) : TaskScheduler {
override fun cancelTask(taskType: TaskType) { override fun cancelTask(taskType: TaskType) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = when (taskType) {
TaskType.COMMENT_NOTIFICATION -> Intent(
context,
CommentNotificationReceiver::class.java
)
TaskType.ANILIST_NOTIFICATION -> Intent( val intent = when {
context, taskType == TaskType.COMMENT_NOTIFICATION && PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1 ->
AnilistNotificationReceiver::class.java Intent(context, CommentNotificationReceiver::class.java)
)
TaskType.SUBSCRIPTION_NOTIFICATION -> Intent( taskType == TaskType.ANILIST_NOTIFICATION ->
context, Intent(context, AnilistNotificationReceiver::class.java)
SubscriptionNotificationReceiver::class.java
) taskType == TaskType.SUBSCRIPTION_NOTIFICATION ->
Intent(context, SubscriptionNotificationReceiver::class.java)
else -> return
} }
val pendingIntent = PendingIntent.getBroadcast( val pendingIntent = PendingIntent.getBroadcast(

View file

@ -0,0 +1,32 @@
package ani.dantotsu.others
import android.text.Layout
import android.text.style.AlignmentSpan
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.RenderProps
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.tag.SimpleTagHandler
class AlignTagHandler : SimpleTagHandler() {
override fun getSpans(
configuration: MarkwonConfiguration,
renderProps: RenderProps,
tag: HtmlTag
): Any {
val alignment: Layout.Alignment = if (tag.attributes().containsKey("center")) {
Layout.Alignment.ALIGN_CENTER
} else if (tag.attributes().containsKey("end")) {
Layout.Alignment.ALIGN_OPPOSITE
} else {
Layout.Alignment.ALIGN_NORMAL
}
return AlignmentSpan.Standard(alignment)
}
override fun supportedTags(): Collection<String> {
return setOf("align")
}
}

View file

@ -0,0 +1,46 @@
package ani.dantotsu.others
import ani.dantotsu.FileUrl
import ani.dantotsu.Mapper
import ani.dantotsu.client
import ani.dantotsu.media.anime.Episode
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
object Anify {
suspend fun fetchAndParseMetadata(id :Int): Map<String, Episode> {
val response = client.get("https://anify.eltik.cc/content-metadata/$id")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<AnifyElement>(it)
}
return response.firstOrNull()?.data?.associate {
it.number.toString() to Episode(
number = it.number.toString(),
title = it.title,
desc = it.description,
thumb = FileUrl[it.img],
)
} ?: emptyMap()
}
@Serializable
data class AnifyElement (
@SerialName("providerId")
val providerID: String? = null,
val data: List<Datum>? = null
)
@Serializable
data class Datum (
val id: String? = null,
val description: String? = null,
val hasDub: Boolean? = null,
val img: String? = null,
val isFiller: Boolean? = null,
val number: Long? = null,
val title: String? = null,
val updatedAt: Long? = null,
val rating: Double? = null
)
}

View file

@ -4,6 +4,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
@ -24,7 +25,10 @@ class CrashActivity : AppCompatActivity() {
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
initActivity(this) initActivity(this)
binding = ActivityCrashBinding.inflate(layoutInflater) binding = ActivityCrashBinding.inflate(layoutInflater)
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
setContentView(binding.root) setContentView(binding.root)
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight topMargin = statusBarHeight

View file

@ -18,14 +18,6 @@ object Kitsu {
val headers = mapOf( val headers = mapOf(
"Content-Type" to "application/json", "Content-Type" to "application/json",
"Accept" to "application/json", "Accept" to "application/json",
"Accept-Encoding" to "gzip, deflate",
"Accept-Language" to "en-US,en;q=0.5",
"Host" to "kitsu.io",
"Connection" to "keep-alive",
"Origin" to "https://kitsu.io",
"Sec-Fetch-Dest" to "empty",
"Sec-Fetch-Mode" to "cors",
"Sec-Fetch-Site" to "cross-site",
) )
val response = tryWithSuspend { val response = tryWithSuspend {
val res = client.post( val res = client.post(
@ -152,4 +144,4 @@ query {
} }
} }

View file

@ -0,0 +1,144 @@
package ani.dantotsu.others
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Shader
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
class Xubtitle
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : AppCompatTextView(context, attrs, defStyleAttr) {
private var outlineThickness: Float = 0f
private var effectColor: Int = currentTextColor
private var currentEffect: Effect = Effect.NONE
private val shadowPaint = Paint().apply { isAntiAlias = true }
private val outlinePaint = Paint().apply { isAntiAlias = true }
private var shineShader: Shader? = null
enum class Effect {
NONE,
OUTLINE,
SHINE,
DROP_SHADOW,
}
override fun onDraw(canvas: Canvas) {
val text = text.toString()
val textPaint =
TextPaint(paint).apply {
color = currentTextColor
}
val staticLayout =
StaticLayout.Builder
.obtain(text, 0, text.length, textPaint, width)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.setLineSpacing(0f, 1f)
.build()
when (currentEffect) {
Effect.OUTLINE -> {
textPaint.style = Paint.Style.STROKE
textPaint.strokeWidth = outlineThickness
textPaint.color = effectColor
staticLayout.draw(canvas)
textPaint.style = Paint.Style.FILL
textPaint.color = currentTextColor
staticLayout.draw(canvas)
}
Effect.DROP_SHADOW -> {
setLayerType(LAYER_TYPE_SOFTWARE, null)
textPaint.setShadowLayer(outlineThickness, 4f, 4f, effectColor)
staticLayout.draw(canvas)
textPaint.clearShadowLayer()
}
Effect.SHINE -> {
val shadowShader =
LinearGradient(
0f,
0f,
width.toFloat(),
height.toFloat(),
intArrayOf(Color.WHITE, effectColor, Color.BLACK),
null,
Shader.TileMode.CLAMP,
)
val shadowPaint =
Paint().apply {
isAntiAlias = true
style = Paint.Style.FILL
textSize = textPaint.textSize
typeface = textPaint.typeface
shader = shadowShader
}
canvas.drawText(
text,
x + 4f, // Shadow offset
y + 4f,
shadowPaint,
)
val shader =
LinearGradient(
0f,
0f,
width.toFloat(),
height.toFloat(),
intArrayOf(effectColor, Color.WHITE, Color.WHITE),
null,
Shader.TileMode.CLAMP,
)
textPaint.shader = shader
staticLayout.draw(canvas)
textPaint.shader = null
}
Effect.NONE -> {
staticLayout.draw(canvas)
}
}
}
fun applyOutline(
color: Int,
outlineThickness: Float,
) {
this.effectColor = color
this.outlineThickness = outlineThickness
currentEffect = Effect.OUTLINE
}
// Too hard for me to figure it out
fun applyShineEffect(color: Int) {
this.effectColor = color
currentEffect = Effect.SHINE
}
fun applyDropShadow(
color: Int,
outlineThickness: Float,
) {
this.effectColor = color
this.outlineThickness = outlineThickness
currentEffect = Effect.DROP_SHADOW
}
}

View file

@ -0,0 +1,57 @@
package ani.dantotsu.others.calc
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.util.Logger
object BiometricPromptUtils {
private const val TAG = "BiometricPromptUtils"
/**
* Create a BiometricPrompt instance
* @param activity: AppCompatActivity
* @param processSuccess: success callback
*/
fun createBiometricPrompt(
activity: AppCompatActivity,
processSuccess: (BiometricPrompt.AuthenticationResult) -> Unit
): BiometricPrompt {
val executor = ContextCompat.getMainExecutor(activity)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errCode: Int, errString: CharSequence) {
super.onAuthenticationError(errCode, errString)
Logger.log("$TAG errCode is $errCode and errString is: $errString")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Logger.log("$TAG User biometric rejected.")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d(TAG, "Authentication was successful")
processSuccess(result)
}
}
return BiometricPrompt(activity, executor, callback)
}
/**
* Create a BiometricPrompt.PromptInfo instance
* @param activity: AppCompatActivity
* @return BiometricPrompt.PromptInfo: instance
*/
fun createPromptInfo(activity: AppCompatActivity): BiometricPrompt.PromptInfo =
BiometricPrompt.PromptInfo.Builder().apply {
setTitle(activity.getString(R.string.bio_prompt_info_title))
setDescription(activity.getString(R.string.bio_prompt_info_desc))
setConfirmationRequired(false)
setNegativeButtonText(activity.getString(R.string.cancel))
}.build()
}

View file

@ -1,10 +1,14 @@
package ani.dantotsu.others.calc package ani.dantotsu.others.calc
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.view.MotionEvent
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -16,6 +20,8 @@ import ani.dantotsu.databinding.ActivityCalcBinding
import ani.dantotsu.getThemeColor import ani.dantotsu.getThemeColor
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.NumberConverter.Companion.toBinary import ani.dantotsu.util.NumberConverter.Companion.toBinary
@ -24,7 +30,13 @@ import ani.dantotsu.util.NumberConverter.Companion.toHex
class CalcActivity : AppCompatActivity() { class CalcActivity : AppCompatActivity() {
private lateinit var binding: ActivityCalcBinding private lateinit var binding: ActivityCalcBinding
private lateinit var code: String private lateinit var code: String
private val handler = Handler(Looper.getMainLooper())
private val runnable = Runnable {
success()
}
private val stack = CalcStack() private val stack = CalcStack()
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
@ -73,6 +85,29 @@ class CalcActivity : AppCompatActivity() {
binding.displayHex.text = "" binding.displayHex.text = ""
binding.display.text = "0" binding.display.text = "0"
} }
if (PrefManager.getVal(PrefName.OverridePassword, false)) {
buttonClear.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
handler.postDelayed(runnable, 10000)
true
}
MotionEvent.ACTION_UP -> {
v.performClick()
handler.removeCallbacks(runnable)
true
}
MotionEvent.ACTION_CANCEL -> {
handler.removeCallbacks(runnable)
true
}
else -> false
}
}
}
buttonBackspace.setOnClickListener { buttonBackspace.setOnClickListener {
stack.remove() stack.remove()
updateDisplay() updateDisplay()
@ -81,6 +116,20 @@ class CalcActivity : AppCompatActivity() {
} }
} }
override fun onResume() {
super.onResume()
if (hasPermission) {
success()
}
if (PrefManager.getVal(PrefName.BiometricToken, "").isNotEmpty()) {
val bioMetricPrompt = BiometricPromptUtils.createBiometricPrompt(this) {
success()
}
val promptInfo = BiometricPromptUtils.createPromptInfo(this)
bioMetricPrompt.authenticate(promptInfo)
}
}
private fun success() { private fun success() {
hasPermission = true hasPermission = true
ContextCompat.startActivity( ContextCompat.startActivity(

View file

@ -39,7 +39,7 @@ object AnimeSources : WatchSources() {
} }
fun performReorderAnimeSources() { fun performReorderAnimeSources() {
//remove the downloaded source from the list to avoid duplicates // Remove the downloaded source from the list to avoid duplicates
list = list.filter { it.name != "Downloaded" } list = list.filter { it.name != "Downloaded" }
list = sortPinnedAnimeSources(list, pinnedAnimeSources) + Lazier( list = sortPinnedAnimeSources(list, pinnedAnimeSources) + Lazier(
{ OfflineAnimeParser() }, { OfflineAnimeParser() },

View file

@ -348,9 +348,6 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
val res = source.getChapterList(sManga) val res = source.getChapterList(sManga)
val reversedRes = res.reversed() val reversedRes = res.reversed()
val chapterList = reversedRes.map { sChapterToMangaChapter(it) } val chapterList = reversedRes.map { sChapterToMangaChapter(it) }
Logger.log("chapterList size: ${chapterList.size}")
Logger.log("chapterList: ${chapterList[1].title}")
Logger.log("chapterList: ${chapterList[1].description}")
chapterList chapterList
} catch (e: Exception) { } catch (e: Exception) {
Logger.log("loadChapters Exception: $e") Logger.log("loadChapters Exception: $e")

View file

@ -55,13 +55,11 @@ class OfflineAnimeParser : AnimeParser() {
episodes.add(episode) episodes.add(episode)
} }
} }
//episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) }
episodes.addAll(loadEpisodesCompat(animeLink, extra, sAnime))
//filter those with the same name
return episodes.distinctBy { it.number }
.sortedBy { MediaNameAdapter.findEpisodeNumber(it.number) }
} }
return emptyList() episodes.addAll(loadEpisodesCompat(animeLink, extra, sAnime))
//filter those with the same name
return episodes.distinctBy { it.number }
.sortedBy { MediaNameAdapter.findEpisodeNumber(it.number) }
} }
override suspend fun loadVideoServers( override suspend fun loadVideoServers(

View file

@ -43,11 +43,10 @@ class OfflineMangaParser : MangaParser() {
chapters.add(chapter) chapters.add(chapter)
} }
} }
chapters.addAll(loadChaptersCompat(mangaLink, extra, sManga))
return chapters.distinctBy { it.number }
.sortedBy { MediaNameAdapter.findChapterNumber(it.number) }
} }
return emptyList() chapters.addAll(loadChaptersCompat(mangaLink, extra, sManga))
return chapters.distinctBy { it.number }
.sortedBy { MediaNameAdapter.findChapterNumber(it.number) }
} }
override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> { override suspend fun loadImages(chapterLink: String, sChapter: SChapter): List<MangaImage> {
@ -66,17 +65,16 @@ class OfflineMangaParser : MangaParser() {
for (image in images) { for (image in images) {
Logger.log("imageNumber: ${image.url.url}") Logger.log("imageNumber: ${image.url.url}")
} }
return if (images.isNotEmpty()) {
images.sortBy { image ->
val matchResult = imageNumberRegex.find(image.url.url)
matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE
}
images
} else {
loadImagesCompat(chapterLink, sChapter)
}
} }
return emptyList() return if (images.isNotEmpty()) {
images.sortBy { image ->
val matchResult = imageNumberRegex.find(image.url.url)
matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE
}
images
} else {
loadImagesCompat(chapterLink, sChapter)
}
} }
override suspend fun search(query: String): List<ShowResponse> { override suspend fun search(query: String): List<ShowResponse> {

View file

@ -159,7 +159,7 @@ class NovelExtensionManager(private val context: Context) {
* *
* @param pkgName The package name of the application to uninstall. * @param pkgName The package name of the application to uninstall.
*/ */
fun uninstallExtension(pkgName: String, context: Context) { fun uninstallExtension(pkgName: String) {
installer.uninstallApk(pkgName) installer.uninstallApk(pkgName)
} }

View file

@ -22,6 +22,7 @@ import ani.dantotsu.R
import ani.dantotsu.blurImage import ani.dantotsu.blurImage
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ActivityProfileBinding import ani.dantotsu.databinding.ActivityProfileBinding
import ani.dantotsu.databinding.ItemProfileAppBarBinding import ani.dantotsu.databinding.ItemProfileAppBarBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
@ -30,15 +31,14 @@ import ani.dantotsu.media.user.ListActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.openImage import ani.dantotsu.openImage
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.profile.activity.ActivityFragment
import ani.dantotsu.profile.activity.FeedFragment import ani.dantotsu.profile.activity.ActivityFragment.Companion.ActivityType
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.util.MarkdownCreatorActivity
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -136,7 +136,7 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
followButton.setOnClickListener { followButton.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val res = Anilist.query.toggleFollow(user.id) val res = Anilist.mutation.toggleFollow(user.id)
if (res?.data?.toggleFollow != null) { if (res?.data?.toggleFollow != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
snackString(R.string.success) snackString(R.string.success)
@ -153,16 +153,18 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
popup.setOnMenuItemClickListener { item -> popup.setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.action_view_on_anilist -> { R.id.action_view_on_anilist -> {
openLinkInBrowser("https://anilist.co/user/${user.name}") openLinkInBrowser(getString(R.string.anilist_link, user.name))
true true
} }
R.id.action_create_new_activity -> { R.id.action_share_profile -> {
ContextCompat.startActivity( val shareIntent = Intent(Intent.ACTION_SEND)
context, shareIntent.type = "text/plain"
Intent(context, MarkdownCreatorActivity::class.java) shareIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.anilist_link, user.name))
.putExtra("type", "activity"), startActivity(Intent.createChooser(shareIntent, "Share Profile"))
null true
) }
R.id.action_copy_user_id -> {
copyToClipboard(user.id.toString(), true)
true true
} }
else -> false else -> false
@ -177,7 +179,11 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
user.avatar?.medium ?: "" user.avatar?.medium ?: ""
) )
profileUserName.text = user.name profileUserName.text = user.name
val bannerAnimations: ImageView= if (PrefManager.getVal(PrefName.BannerAnimations)) profileBannerImage else profileBannerImageNoKen profileUserName.setOnClickListener {
copyToClipboard(profileUserName.text.toString(), true)
}
val bannerAnimations: ImageView =
if (PrefManager.getVal(PrefName.BannerAnimations)) profileBannerImage else profileBannerImageNoKen
blurImage( blurImage(
bannerAnimations, bannerAnimations,
@ -199,7 +205,8 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
profileAppBar.addOnOffsetChangedListener(context) profileAppBar.addOnOffsetChangedListener(context)
profileFollowerCount.text = (respond.data.followerPage?.pageInfo?.total ?: 0).toString() profileFollowerCount.text =
(respond.data.followerPage?.pageInfo?.total ?: 0).toString()
profileFollowerCountContainer.setOnClickListener { profileFollowerCountContainer.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
context, context,
@ -209,7 +216,8 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
null null
) )
} }
profileFollowingCount.text = (respond.data.followingPage?.pageInfo?.total ?: 0).toString() profileFollowingCount.text =
(respond.data.followingPage?.pageInfo?.total ?: 0).toString()
profileFollowingCountContainer.setOnClickListener { profileFollowingCountContainer.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
context, context,
@ -320,7 +328,7 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
override fun getItemCount(): Int = 3 override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment = when (position) { override fun createFragment(position: Int): Fragment = when (position) {
0 -> ProfileFragment.newInstance(user) 0 -> ProfileFragment.newInstance(user)
1 -> FeedFragment.newInstance(user.id, false, -1) 1 -> ActivityFragment.newInstance(ActivityType.OTHER_USER, user.id)
2 -> StatsFragment.newInstance(user) 2 -> StatsFragment.newInstance(user)
else -> ProfileFragment.newInstance(user) else -> ProfileFragment.newInstance(user)
} }

View file

@ -252,14 +252,14 @@ class StatsFragment :
stat?.statistics?.anime?.scores?.map { stat?.statistics?.anime?.scores?.map {
convertScore( convertScore(
it.score, it.score,
stat.mediaListOptions.scoreFormat stat.mediaListOptions.scoreFormat.toString()
) )
} ?: emptyList() } ?: emptyList()
} else { } else {
stat?.statistics?.manga?.scores?.map { stat?.statistics?.manga?.scores?.map {
convertScore( convertScore(
it.score, it.score,
stat.mediaListOptions.scoreFormat stat.mediaListOptions.scoreFormat.toString()
) )
} ?: emptyList() } ?: emptyList()
} }

View file

@ -0,0 +1,173 @@
package ani.dantotsu.profile.activity
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Activity
import ani.dantotsu.databinding.FragmentFeedBinding
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.util.ActivityMarkdownCreator
import com.xwray.groupie.GroupieAdapter
import eu.kanade.tachiyomi.util.system.getSerializableCompat
import kotlinx.coroutines.launch
class ActivityFragment : Fragment() {
private lateinit var type: ActivityType
private var userId: Int? = null
private var activityId: Int? = null
private lateinit var binding: FragmentFeedBinding
private var adapter: GroupieAdapter = GroupieAdapter()
private var page: Int = 1
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentFeedBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.let {
type = it.getSerializableCompat<ActivityType>("type") as ActivityType
userId = it.getInt("userId")
activityId = it.getInt("activityId")
}
binding.titleBar.visibility =
if (type == ActivityType.OTHER_USER) View.VISIBLE else View.GONE
binding.titleText.text =
if (userId == Anilist.userid) getString(R.string.create_new_activity) else getString(R.string.write_a_message)
binding.titleImage.setOnClickListener { handleTitleImageClick() }
binding.listRecyclerView.adapter = adapter
binding.listRecyclerView.layoutManager = LinearLayoutManager(context)
binding.listProgressBar.isVisible = true
binding.feedRefresh.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.emptyTextView.text = getString(R.string.nothing_here)
lifecycleScope.launch {
getList()
if (adapter.itemCount == 0) {
binding.emptyTextView.isVisible = true
}
binding.listProgressBar.isVisible = false
}
binding.feedSwipeRefresh.setOnRefreshListener {
lifecycleScope.launch {
adapter.clear()
page = 1
getList()
binding.feedSwipeRefresh.isRefreshing = false
}
}
binding.listRecyclerView.addOnScrollListener(object :
RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (shouldLoadMore()) {
lifecycleScope.launch {
binding.feedRefresh.isVisible = true
getList()
binding.feedRefresh.isVisible = false
}
}
}
})
}
private fun handleTitleImageClick() {
val intent = Intent(context, ActivityMarkdownCreator::class.java).apply {
putExtra("type", if (userId == Anilist.userid) "activity" else "message")
putExtra("userId", userId)
}
ContextCompat.startActivity(requireContext(), intent, null)
}
private suspend fun getList() {
val list = when (type) {
ActivityType.GLOBAL -> getActivities(global = true)
ActivityType.USER -> getActivities(filter = true)
ActivityType.OTHER_USER -> getActivities(userId = userId)
ActivityType.ONE -> getActivities(activityId = activityId)
}
adapter.addAll(list.map { ActivityItem(it, adapter, ::onActivityClick) })
}
private suspend fun getActivities(
global: Boolean = false,
userId: Int? = null,
activityId: Int? = null,
filter: Boolean = false
): List<Activity> {
val res = Anilist.query.getFeed(userId, global, page, activityId)?.data?.page?.activities
page += 1
return res
?.filter { if (Anilist.adult) true else it.media?.isAdult != true }
?.filterNot { it.recipient?.id != null && it.recipient.id != Anilist.userid && filter }
?: emptyList()
}
private fun shouldLoadMore(): Boolean {
val layoutManager =
(binding.listRecyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
val adapter = binding.listRecyclerView.adapter
return !binding.listRecyclerView.canScrollVertically(1) &&
!binding.feedRefresh.isVisible && adapter?.itemCount != 0 &&
layoutManager == (adapter!!.itemCount - 1)
}
private fun onActivityClick(id: Int, type: String) {
val intent = when (type) {
"USER" -> Intent(requireContext(), ProfileActivity::class.java).putExtra("userId", id)
"MEDIA" -> Intent(
requireContext(),
MediaDetailsActivity::class.java
).putExtra("mediaId", id)
else -> return
}
ContextCompat.startActivity(requireContext(), intent, null)
}
override fun onResume() {
super.onResume()
if (this::binding.isInitialized) {
binding.root.requestLayout()
}
}
companion object {
enum class ActivityType { GLOBAL, USER, OTHER_USER, ONE }
fun newInstance(
type: ActivityType,
userId: Int? = null,
activityId: Int? = null
): ActivityFragment {
return ActivityFragment().apply {
arguments = Bundle().apply {
putSerializable("type", type)
userId?.let { putInt("userId", it) }
activityId?.let { putInt("activityId", it) }
}
}
}
}
}

View file

@ -5,7 +5,6 @@ import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.blurImage import ani.dantotsu.blurImage
import ani.dantotsu.buildMarkwon import ani.dantotsu.buildMarkwon
@ -18,7 +17,7 @@ import ani.dantotsu.profile.UsersDialogFragment
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.AniMarkdown.Companion.getBasicAniHTML import ani.dantotsu.util.AniMarkdown.Companion.getBasicAniHTML
import ani.dantotsu.util.MarkdownCreatorActivity import ani.dantotsu.util.ActivityMarkdownCreator
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -29,23 +28,16 @@ import kotlinx.coroutines.withContext
class ActivityItem( class ActivityItem(
private val activity: Activity, private val activity: Activity,
private val parentAdapter: GroupieAdapter,
val clickCallback: (Int, type: String) -> Unit, val clickCallback: (Int, type: String) -> Unit,
private val fragActivity: FragmentActivity
) : BindableItem<ItemActivityBinding>() { ) : BindableItem<ItemActivityBinding>() {
private lateinit var binding: ItemActivityBinding private lateinit var binding: ItemActivityBinding
private lateinit var repliesAdapter: GroupieAdapter
override fun bind(viewBinding: ItemActivityBinding, position: Int) { override fun bind(viewBinding: ItemActivityBinding, position: Int) {
binding = viewBinding binding = viewBinding
val context = binding.root.context
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
setAnimation(binding.root.context, binding.root) setAnimation(binding.root.context, binding.root)
repliesAdapter = GroupieAdapter()
binding.activityReplies.adapter = repliesAdapter
binding.activityReplies.layoutManager = LinearLayoutManager(
binding.root.context,
LinearLayoutManager.VERTICAL,
false
)
binding.activityUserName.text = activity.user?.name ?: activity.messenger?.name binding.activityUserName.text = activity.user?.name ?: activity.messenger?.name
binding.activityUserAvatar.loadImage( binding.activityUserAvatar.loadImage(
activity.user?.avatar?.medium ?: activity.messenger?.avatar?.medium activity.user?.avatar?.medium ?: activity.messenger?.avatar?.medium
@ -54,66 +46,29 @@ class ActivityItem(
val likeColor = ContextCompat.getColor(binding.root.context, R.color.yt_red) val likeColor = ContextCompat.getColor(binding.root.context, R.color.yt_red)
val notLikeColor = ContextCompat.getColor(binding.root.context, R.color.bg_opp) val notLikeColor = ContextCompat.getColor(binding.root.context, R.color.bg_opp)
binding.activityLike.setColorFilter(if (activity.isLiked == true) likeColor else notLikeColor) binding.activityLike.setColorFilter(if (activity.isLiked == true) likeColor else notLikeColor)
binding.commentTotalReplies.isVisible = activity.replyCount > 0
binding.dot.isVisible = activity.replyCount > 0
binding.commentTotalReplies.setOnClickListener {
when (binding.activityReplies.visibility) {
View.GONE -> {
val replyItems = activity.replies?.map {
ActivityReplyItem(it,fragActivity) { id, type ->
clickCallback(
id,
type
)
}
} ?: emptyList()
repliesAdapter.addAll(replyItems)
binding.activityReplies.visibility = View.VISIBLE
binding.commentTotalReplies.setText(R.string.hide_replies)
}
else -> {
repliesAdapter.clear()
binding.activityReplies.visibility = View.GONE
binding.commentTotalReplies.setText(R.string.view_replies)
}
}
}
if (activity.isLocked != true) {
binding.commentReply.setOnClickListener {
val context = binding.root.context
ContextCompat.startActivity(
context,
Intent(context, MarkdownCreatorActivity::class.java)
.putExtra("type", "replyActivity")
.putExtra("parentId", activity.id),
null
)
}
} else {
binding.commentReply.visibility = View.GONE
binding.dot.visibility = View.GONE
}
val userList = arrayListOf<User>() val userList = arrayListOf<User>()
activity.likes?.forEach { i -> activity.likes?.forEach { i ->
userList.add(User(i.id, i.name.toString(), i.avatar?.medium, i.bannerImage)) userList.add(User(i.id, i.name.toString(), i.avatar?.medium, i.bannerImage))
} }
binding.activityRepliesContainer.setOnClickListener {
RepliesBottomDialog.newInstance(activity.id)
.show((context as FragmentActivity).supportFragmentManager, "replies")
}
binding.replyCount.text = activity.replyCount.toString()
binding.activityReplies.setColorFilter(ContextCompat.getColor(binding.root.context, R.color.bg_opp))
binding.activityLikeContainer.setOnLongClickListener { binding.activityLikeContainer.setOnLongClickListener {
UsersDialogFragment().apply { UsersDialogFragment().apply {
userList(userList) userList(userList)
show(fragActivity.supportFragmentManager, "dialog") show((context as FragmentActivity).supportFragmentManager, "dialog")
} }
true true
} }
binding.activityLikeCount.text = (activity.likeCount ?: 0).toString() binding.activityLikeCount.text = (activity.likeCount ?: 0).toString()
binding.activityLikeContainer.setOnClickListener { binding.activityLikeContainer.setOnClickListener {
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch { scope.launch {
val res = Anilist.query.toggleLike(activity.id, "ACTIVITY") val res = Anilist.mutation.toggleLike(activity.id, "ACTIVITY")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (res != null) { if (res != null) {
if (activity.isLiked == true) { if (activity.isLiked == true) {
activity.likeCount = activity.likeCount?.minus(1) activity.likeCount = activity.likeCount?.minus(1)
} else { } else {
@ -129,13 +84,27 @@ class ActivityItem(
} }
} }
} }
val context = binding.root.context binding.activityDelete.isVisible = activity.userId == Anilist.userid || activity.messenger?.id == Anilist.userid
binding.activityDelete.setOnClickListener {
scope.launch {
val res = Anilist.mutation.deleteActivity(activity.id)
withContext(Dispatchers.Main) {
if (res) {
snackString("Deleted activity")
parentAdapter.remove(this@ActivityItem)
} else {
snackString("Failed to delete activity")
}
}
}
}
when (activity.typename) { when (activity.typename) {
"ListActivity" -> { "ListActivity" -> {
val cover = activity.media?.coverImage?.large val cover = activity.media?.coverImage?.large
val banner = activity.media?.bannerImage val banner = activity.media?.bannerImage
binding.activityContent.visibility = View.GONE binding.activityContent.visibility = View.GONE
binding.activityBannerContainer.visibility = View.VISIBLE binding.activityBannerContainer.visibility = View.VISIBLE
binding.activityPrivate.visibility = View.GONE
binding.activityMediaName.text = activity.media?.title?.userPreferred binding.activityMediaName.text = activity.media?.title?.userPreferred
val activityText = "${activity.user!!.name} ${activity.status} ${ val activityText = "${activity.user!!.name} ${activity.status} ${
activity.progress activity.progress
@ -156,11 +125,13 @@ class ActivityItem(
binding.activityMediaName.setOnClickListener { binding.activityMediaName.setOnClickListener {
clickCallback(activity.media?.id ?: -1, "MEDIA") clickCallback(activity.media?.id ?: -1, "MEDIA")
} }
binding.activityEdit.isVisible = false
} }
"TextActivity" -> { "TextActivity" -> {
binding.activityBannerContainer.visibility = View.GONE binding.activityBannerContainer.visibility = View.GONE
binding.activityContent.visibility = View.VISIBLE binding.activityContent.visibility = View.VISIBLE
binding.activityPrivate.visibility = View.GONE
if (!(context as android.app.Activity).isDestroyed) { if (!(context as android.app.Activity).isDestroyed) {
val markwon = buildMarkwon(context, false) val markwon = buildMarkwon(context, false)
markwon.setMarkdown( markwon.setMarkdown(
@ -174,11 +145,23 @@ class ActivityItem(
binding.activityUserName.setOnClickListener { binding.activityUserName.setOnClickListener {
clickCallback(activity.userId ?: -1, "USER") clickCallback(activity.userId ?: -1, "USER")
} }
binding.activityEdit.isVisible = activity.userId == Anilist.userid
binding.activityEdit.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, ActivityMarkdownCreator::class.java)
.putExtra("type", "activity")
.putExtra("other", activity.text)
.putExtra("edit", activity.id),
null
)
}
} }
"MessageActivity" -> { "MessageActivity" -> {
binding.activityBannerContainer.visibility = View.GONE binding.activityBannerContainer.visibility = View.GONE
binding.activityContent.visibility = View.VISIBLE binding.activityContent.visibility = View.VISIBLE
binding.activityPrivate.visibility = if (activity.isPrivate == true) View.VISIBLE else View.GONE
if (!(context as android.app.Activity).isDestroyed) { if (!(context as android.app.Activity).isDestroyed) {
val markwon = buildMarkwon(context, false) val markwon = buildMarkwon(context, false)
markwon.setMarkdown( markwon.setMarkdown(
@ -192,6 +175,19 @@ class ActivityItem(
binding.activityUserName.setOnClickListener { binding.activityUserName.setOnClickListener {
clickCallback(activity.messengerId ?: -1, "USER") clickCallback(activity.messengerId ?: -1, "USER")
} }
binding.activityEdit.isVisible = false
binding.activityEdit.isVisible = activity.messenger?.id == Anilist.userid
binding.activityEdit.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, ActivityMarkdownCreator::class.java)
.putExtra("type", "message")
.putExtra("other", activity.message)
.putExtra("edit", activity.id)
.putExtra("userId", activity.recipientId),
null
)
}
} }
} }
} }

View file

@ -1,7 +1,9 @@
package ani.dantotsu.profile.activity package ani.dantotsu.profile.activity
import android.content.Intent
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.buildMarkwon import ani.dantotsu.buildMarkwon
@ -13,6 +15,8 @@ import ani.dantotsu.profile.User
import ani.dantotsu.profile.UsersDialogFragment import ani.dantotsu.profile.UsersDialogFragment
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.AniMarkdown.Companion.getBasicAniHTML import ani.dantotsu.util.AniMarkdown.Companion.getBasicAniHTML
import ani.dantotsu.util.ActivityMarkdownCreator
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -22,23 +26,27 @@ import kotlinx.coroutines.withContext
class ActivityReplyItem( class ActivityReplyItem(
private val reply: ActivityReply, private val reply: ActivityReply,
private val parentId : Int,
private val fragActivity: FragmentActivity, private val fragActivity: FragmentActivity,
private val parentAdapter: GroupieAdapter,
private val clickCallback: (Int, type: String) -> Unit, private val clickCallback: (Int, type: String) -> Unit,
) : BindableItem<ItemActivityReplyBinding>() { ) : BindableItem<ItemActivityReplyBinding>() {
private lateinit var binding: ItemActivityReplyBinding private lateinit var binding: ItemActivityReplyBinding
override fun bind(viewBinding: ItemActivityReplyBinding, position: Int) { override fun bind(viewBinding: ItemActivityReplyBinding, position: Int) {
binding = viewBinding binding = viewBinding
val context = binding.root.context
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
binding.activityUserAvatar.loadImage(reply.user.avatar?.medium) binding.activityUserAvatar.loadImage(reply.user.avatar?.medium)
binding.activityUserName.text = reply.user.name binding.activityUserName.text = reply.user.name
binding.activityTime.text = ActivityItemBuilder.getDateTime(reply.createdAt) binding.activityTime.text = ActivityItemBuilder.getDateTime(reply.createdAt)
binding.activityLikeCount.text = reply.likeCount.toString() binding.activityLikeCount.text = reply.likeCount.toString()
val likeColor = ContextCompat.getColor(binding.root.context, R.color.yt_red) val likeColor = ContextCompat.getColor(context, R.color.yt_red)
val notLikeColor = ContextCompat.getColor(binding.root.context, R.color.bg_opp) val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
binding.activityLike.setColorFilter(if (reply.isLiked) likeColor else notLikeColor) binding.activityLike.setColorFilter(if (reply.isLiked) likeColor else notLikeColor)
val markwon = buildMarkwon(binding.root.context) val markwon = buildMarkwon(context)
markwon.setMarkdown(binding.activityContent, getBasicAniHTML(reply.text)) markwon.setMarkdown(binding.activityContent, getBasicAniHTML(reply.text))
val userList = arrayListOf<User>() val userList = arrayListOf<User>()
reply.likes?.forEach { i -> reply.likes?.forEach { i ->
userList.add(User(i.id, i.name.toString(), i.avatar?.medium, i.bannerImage)) userList.add(User(i.id, i.name.toString(), i.avatar?.medium, i.bannerImage))
@ -51,9 +59,8 @@ class ActivityReplyItem(
true true
} }
binding.activityLikeContainer.setOnClickListener { binding.activityLikeContainer.setOnClickListener {
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch { scope.launch {
val res = Anilist.query.toggleLike(reply.id, "ACTIVITY_REPLY") val res = Anilist.mutation.toggleLike(reply.id, "ACTIVITY_REPLY")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (res != null) { if (res != null) {
if (reply.isLiked) { if (reply.isLiked) {
@ -71,6 +78,42 @@ class ActivityReplyItem(
} }
} }
} }
binding.activityReply.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, ActivityMarkdownCreator::class.java)
.putExtra("type", "replyActivity")
.putExtra("parentId", parentId)
.putExtra("other", "@${reply.user.name} "),
null
)
}
binding.activityEdit.isVisible = reply.userId == Anilist.userid
binding.activityEdit.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, ActivityMarkdownCreator::class.java)
.putExtra("type", "replyActivity")
.putExtra("parentId", parentId)
.putExtra("other", reply.text)
.putExtra("edit", reply.id),
null
)
}
binding.activityDelete.isVisible = reply.userId == Anilist.userid
binding.activityDelete.setOnClickListener {
scope.launch {
val res = Anilist.mutation.deleteActivityReply(reply.id)
withContext(Dispatchers.Main) {
if (res) {
snackString("Deleted")
parentAdapter.remove(this@ActivityReplyItem)
} else {
snackString("Failed to delete")
}
}
}
}
binding.activityAvatarContainer.setOnClickListener { binding.activityAvatarContainer.setOnClickListener {
clickCallback(reply.userId, "USER") clickCallback(reply.userId, "USER")

View file

@ -2,6 +2,7 @@ package ani.dantotsu.profile.activity
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
@ -10,16 +11,20 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.databinding.ActivityFeedBinding import ani.dantotsu.databinding.ActivityFeedBinding
import ani.dantotsu.databinding.ActivityNotificationBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.profile.activity.ActivityFragment.Companion.ActivityType
import ani.dantotsu.profile.notification.NotificationActivity
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
class FeedActivity : AppCompatActivity() { class FeedActivity : AppCompatActivity() {
private lateinit var binding: ActivityFeedBinding private lateinit var binding: ActivityNotificationBinding
private var selected: Int = 0 private var selected: Int = 0
lateinit var navBar: AnimatedBottomBar lateinit var navBar: AnimatedBottomBar
@ -27,28 +32,29 @@ class FeedActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
initActivity(this) initActivity(this)
binding = ActivityFeedBinding.inflate(layoutInflater) binding = ActivityNotificationBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
navBar = binding.feedNavBar binding.notificationTitle.text = getString(R.string.activities)
val navBarMargin = if (resources.configuration.orientation == binding.notificationToolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
Configuration.ORIENTATION_LANDSCAPE topMargin = statusBarHeight
) 0 else navBarHeight
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin = navBarMargin }
val personalTab = navBar.createTab(R.drawable.ic_round_person_24, "Following")
val globalTab = navBar.createTab(R.drawable.ic_globe_24, "Global")
navBar.addTab(personalTab)
navBar.addTab(globalTab)
binding.listTitle.text = getString(R.string.activities)
binding.feedViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarMargin
topMargin += statusBarHeight
} }
binding.listToolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } navBar = binding.notificationNavBar
val activityId = intent.getIntExtra("activityId", -1) binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
binding.feedViewPager.adapter = bottomMargin = navBarHeight
ViewPagerAdapter(supportFragmentManager, lifecycle, activityId) }
binding.feedViewPager.setCurrentItem(selected, false) val tabs = listOf(
binding.feedViewPager.isUserInputEnabled = false Pair(R.drawable.ic_round_person_24, "Following"),
Pair(R.drawable.ic_globe_24, "Global"),
)
tabs.forEach { (icon, title) -> navBar.addTab(navBar.createTab(icon, title)) }
binding.notificationBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() }
val getOne = intent.getIntExtra("activityId", -1)
if (getOne != -1) { navBar.visibility = View.GONE }
binding.notificationViewPager.isUserInputEnabled = false
binding.notificationViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, getOne)
binding.notificationViewPager.setOffscreenPageLimit(4)
binding.notificationViewPager.setCurrentItem(selected, false)
navBar.selectTabAt(selected) navBar.selectTabAt(selected)
navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener { navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected( override fun onTabSelected(
@ -58,24 +64,9 @@ class FeedActivity : AppCompatActivity() {
newTab: AnimatedBottomBar.Tab newTab: AnimatedBottomBar.Tab
) { ) {
selected = newIndex selected = newIndex
binding.feedViewPager.setCurrentItem(selected, true) binding.notificationViewPager.setCurrentItem(selected, false)
} }
}) })
binding.listBack.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val margin =
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 0 else navBarHeight
val params: ViewGroup.MarginLayoutParams =
binding.feedViewPager.layoutParams as ViewGroup.MarginLayoutParams
val paramsNav: ViewGroup.MarginLayoutParams =
navBar.layoutParams as ViewGroup.MarginLayoutParams
params.updateMargins(bottom = margin)
paramsNav.updateMargins(bottom = margin)
} }
override fun onResume() { override fun onResume() {
@ -88,12 +79,12 @@ class FeedActivity : AppCompatActivity() {
lifecycle: Lifecycle, lifecycle: Lifecycle,
private val activityId: Int private val activityId: Int
) : FragmentStateAdapter(fragmentManager, lifecycle) { ) : FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int = 2 override fun getItemCount(): Int = if (activityId != -1) 1 else 2
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
return when (position) { return when (position) {
0 -> FeedFragment.newInstance(null, false, activityId) 0 -> ActivityFragment.newInstance(if (activityId != -1) ActivityType.ONE else ActivityType.USER, activityId = activityId)
else -> FeedFragment.newInstance(null, true, -1) else -> ActivityFragment.newInstance(ActivityType.GLOBAL)
} }
} }
} }

View file

@ -1,188 +0,0 @@
package ani.dantotsu.profile.activity
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistQueries
import ani.dantotsu.connections.anilist.api.Activity
import ani.dantotsu.databinding.FragmentFeedBinding
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.setBaseline
import ani.dantotsu.util.Logger
import com.xwray.groupie.GroupieAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class FeedFragment : Fragment() {
private lateinit var binding: FragmentFeedBinding
private var adapter: GroupieAdapter = GroupieAdapter()
private var activityList: List<Activity> = emptyList()
private lateinit var activity: androidx.activity.ComponentActivity
private var page: Int = 1
private var loadedFirstTime = false
private var userId: Int? = null
private var global: Boolean = false
private var activityId: Int = -1
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentFeedBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity = requireActivity()
userId = arguments?.getInt("userId", -1)
activityId = arguments?.getInt("activityId", -1) ?: -1
if (userId == -1) userId = null
global = arguments?.getBoolean("global", false) ?: false
val navBar = if (userId != null) {
(activity as ProfileActivity).navBar
} else {
(activity as FeedActivity).navBar
}
binding.listRecyclerView.setBaseline(navBar)
binding.listRecyclerView.adapter = adapter
binding.listRecyclerView.layoutManager =
LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
binding.listProgressBar.visibility = ViewGroup.VISIBLE
}
@SuppressLint("ClickableViewAccessibility")
override fun onResume() {
super.onResume()
if (this::binding.isInitialized) {
binding.root.requestLayout()
val navBar = if (userId != null) {
(activity as ProfileActivity).navBar
} else {
(activity as FeedActivity).navBar
}
binding.listRecyclerView.setBaseline(navBar)
if (!loadedFirstTime) {
activity.lifecycleScope.launch(Dispatchers.IO) {
val nulledId = if (activityId == -1) null else activityId
val res = Anilist.query.getFeed(userId, global, activityId = nulledId)
withContext(Dispatchers.Main) {
res?.data?.page?.activities?.let { activities ->
activityList = activities
val filtered =
activityList
.filter { if (Anilist.adult) true else it.media?.isAdult == false }
.filterNot { //filter out messages that are not directed to the user
it.recipient?.id != null && it.recipient.id != Anilist.userid
}
adapter.update(filtered.map {
ActivityItem(
it,
::onActivityClick,
requireActivity()
)
})
}
binding.listProgressBar.visibility = ViewGroup.GONE
val scrollView = binding.listRecyclerView
binding.listRecyclerView.setOnTouchListener { _, event ->
if (event?.action == MotionEvent.ACTION_UP) {
if (activityList.size % AnilistQueries.ITEMS_PER_PAGE != 0 && !global) {
//snackString("No more activities") fix spam?
Logger.log("No more activities")
} else if (!scrollView.canScrollVertically(1) && !binding.feedRefresh.isVisible
&& binding.listRecyclerView.adapter!!.itemCount != 0 &&
(binding.listRecyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() == (binding.listRecyclerView.adapter!!.itemCount - 1)
) {
page++
binding.feedRefresh.visibility = ViewGroup.VISIBLE
loadPage {
binding.feedRefresh.visibility = ViewGroup.GONE
}
}
}
false
}
binding.feedSwipeRefresh.setOnRefreshListener {
page = 1
adapter.clear()
activityList = emptyList()
loadPage()
}
}
}
loadedFirstTime = true
}
}
}
private fun loadPage(onFinish: () -> Unit = {}) {
activity.lifecycleScope.launch(Dispatchers.IO) {
val newRes = Anilist.query.getFeed(userId, global, page)
withContext(Dispatchers.Main) {
newRes?.data?.page?.activities?.let { activities ->
activityList += activities
val filtered = activities.filterNot {
it.recipient?.id != null && it.recipient.id != Anilist.userid
}
adapter.addAll(filtered.map {
ActivityItem(
it,
::onActivityClick,
requireActivity()
)
})
}
binding.feedSwipeRefresh.isRefreshing = false
onFinish()
}
}
}
private fun onActivityClick(id: Int, type: String) {
when (type) {
"USER" -> {
ContextCompat.startActivity(
activity, Intent(activity, ProfileActivity::class.java)
.putExtra("userId", id), null
)
}
"MEDIA" -> {
ContextCompat.startActivity(
activity, Intent(activity, MediaDetailsActivity::class.java)
.putExtra("mediaId", id), null
)
}
}
}
companion object {
fun newInstance(userId: Int?, global: Boolean, activityId: Int): FeedFragment {
val fragment = FeedFragment()
val args = Bundle()
args.putInt("userId", userId ?: -1)
args.putBoolean("global", global)
args.putInt("activityId", activityId)
fragment.arguments = args
return fragment
}
}
}

View file

@ -1,309 +0,0 @@
package ani.dantotsu.profile.activity
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Notification
import ani.dantotsu.connections.anilist.api.NotificationType
import ani.dantotsu.connections.anilist.api.NotificationType.Companion.fromFormattedString
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ActivityFollowBinding
import ani.dantotsu.initActivity
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.notifications.comment.CommentStore
import ani.dantotsu.notifications.subscription.SubscriptionStore
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.Logger
import com.xwray.groupie.GroupieAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class NotificationActivity : AppCompatActivity() {
private lateinit var binding: ActivityFollowBinding
private lateinit var commentStore: List<CommentStore>
private lateinit var subscriptionStore: List<SubscriptionStore>
private var adapter: GroupieAdapter = GroupieAdapter()
private var notificationList: List<Notification> = emptyList()
val filters = ArrayList<String>()
private var currentPage: Int = 1
private var hasNextPage: Boolean = true
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
binding = ActivityFollowBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.listTitle.text = getString(R.string.notifications)
binding.listToolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
binding.listFrameLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.listRecyclerView.adapter = adapter
binding.listRecyclerView.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
binding.followerGrid.visibility = ViewGroup.GONE
binding.followerList.visibility = ViewGroup.GONE
binding.listBack.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
binding.listProgressBar.visibility = ViewGroup.VISIBLE
commentStore = PrefManager.getNullableVal<List<CommentStore>>(
PrefName.CommentNotificationStore,
null
) ?: listOf()
subscriptionStore = PrefManager.getNullableVal<List<SubscriptionStore>>(
PrefName.SubscriptionNotificationStore,
null
) ?: listOf()
binding.followFilterButton.setOnClickListener {
val dialogView = LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
val checkboxContainer = dialogView.findViewById<LinearLayout>(R.id.checkboxContainer)
val tickAllButton = dialogView.findViewById<ImageButton>(R.id.toggleButton)
val title = dialogView.findViewById<TextView>(R.id.scantitle)
title.visibility = ViewGroup.GONE
fun getToggleImageResource(container: ViewGroup): Int {
var allChecked = true
var allUnchecked = true
for (i in 0 until container.childCount) {
val checkBox = container.getChildAt(i) as CheckBox
if (!checkBox.isChecked) {
allChecked = false
} else {
allUnchecked = false
}
}
return when {
allChecked -> R.drawable.untick_all_boxes
allUnchecked -> R.drawable.tick_all_boxes
else -> R.drawable.invert_all_boxes
}
}
NotificationType.entries.forEach { notificationType ->
val checkBox = CheckBox(currContext())
checkBox.text = notificationType.toFormattedString()
checkBox.isChecked = !filters.contains(notificationType.value.fromFormattedString())
checkBox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
filters.remove(notificationType.value.fromFormattedString())
} else {
filters.add(notificationType.value.fromFormattedString())
}
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
}
checkboxContainer.addView(checkBox)
}
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
tickAllButton.setOnClickListener {
for (i in 0 until checkboxContainer.childCount) {
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
checkBox.isChecked = !checkBox.isChecked
}
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
}
val alertD = AlertDialog.Builder(this, R.style.MyPopup)
alertD.setTitle("Filter")
alertD.setView(dialogView)
alertD.setPositiveButton("OK") { _, _ ->
currentPage = 1
hasNextPage = true
adapter.clear()
adapter.addAll(notificationList.filter { notification ->
!filters.contains(notification.notificationType)
}.map {
NotificationItem(
it,
::onNotificationClick
)
})
loadPage(-1) {
binding.followRefresh.visibility = ViewGroup.GONE
}
}
alertD.setNegativeButton("Cancel") { _, _ -> }
val dialog = alertD.show()
dialog.window?.setDimAmount(0.8f)
}
val activityId = intent.getIntExtra("activityId", -1)
lifecycleScope.launch {
loadPage(activityId) {
binding.listProgressBar.visibility = ViewGroup.GONE
}
withContext(Dispatchers.Main) {
binding.listProgressBar.visibility = ViewGroup.GONE
binding.listRecyclerView.setOnTouchListener { _, event ->
if (event?.action == MotionEvent.ACTION_UP) {
if (hasNextPage && !binding.listRecyclerView.canScrollVertically(1) && !binding.followRefresh.isVisible
&& binding.listRecyclerView.adapter!!.itemCount != 0 &&
(binding.listRecyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() == (binding.listRecyclerView.adapter!!.itemCount - 1)
) {
binding.followRefresh.visibility = ViewGroup.VISIBLE
loadPage(-1) {
binding.followRefresh.visibility = ViewGroup.GONE
}
}
}
false
}
binding.followSwipeRefresh.setOnRefreshListener {
currentPage = 1
hasNextPage = true
adapter.clear()
notificationList = emptyList()
loadPage(-1) {
binding.followSwipeRefresh.isRefreshing = false
}
}
}
}
}
private fun loadPage(activityId: Int, onFinish: () -> Unit = {}) {
lifecycleScope.launch(Dispatchers.IO) {
val resetNotification = activityId == -1
val res = Anilist.query.getNotifications(
Anilist.userid ?: PrefManager.getVal<String>(PrefName.AnilistUserId).toIntOrNull()
?: 0, currentPage, resetNotification = resetNotification
)
withContext(Dispatchers.Main) {
val newNotifications: MutableList<Notification> = mutableListOf()
res?.data?.page?.notifications?.let { notifications ->
Logger.log("Notifications: $notifications")
newNotifications += if (activityId != -1) {
notifications.filter { it.id == activityId }
} else {
notifications
}.toMutableList()
}
if (activityId == -1) {
val furthestTime = newNotifications.minOfOrNull { it.createdAt } ?: 0
commentStore.forEach {
if ((it.time > furthestTime * 1000L || !hasNextPage) && notificationList.none { notification ->
notification.commentId == it.commentId && notification.createdAt == (it.time / 1000L).toInt()
}) {
val notification = Notification(
it.type.toString(),
System.currentTimeMillis().toInt(),
commentId = it.commentId,
notificationType = it.type.toString(),
mediaId = it.mediaId,
context = it.title + "\n" + it.content,
createdAt = (it.time / 1000L).toInt(),
)
newNotifications += notification
}
}
subscriptionStore.forEach {
if ((it.time > furthestTime * 1000L || !hasNextPage) && notificationList.none { notification ->
notification.mediaId == it.mediaId && notification.createdAt == (it.time / 1000L).toInt()
}) {
val notification = Notification(
it.type,
System.currentTimeMillis().toInt(),
commentId = it.mediaId,
mediaId = it.mediaId,
notificationType = it.type,
context = it.title + ": " + it.content,
createdAt = (it.time / 1000L).toInt(),
image = it.image,
banner = it.banner ?: it.image
)
newNotifications += notification
}
}
newNotifications.sortByDescending { it.createdAt }
}
notificationList += newNotifications
adapter.addAll(newNotifications.filter { notification ->
!filters.contains(notification.notificationType)
}.map {
NotificationItem(
it,
::onNotificationClick
)
})
currentPage = res?.data?.page?.pageInfo?.currentPage?.plus(1) ?: 1
hasNextPage = res?.data?.page?.pageInfo?.hasNextPage ?: false
binding.followSwipeRefresh.isRefreshing = false
onFinish()
}
}
}
private fun onNotificationClick(id: Int, optional: Int?, type: NotificationClickType) {
when (type) {
NotificationClickType.USER -> {
ContextCompat.startActivity(
this, Intent(this, ProfileActivity::class.java)
.putExtra("userId", id), null
)
}
NotificationClickType.MEDIA -> {
ContextCompat.startActivity(
this, Intent(this, MediaDetailsActivity::class.java)
.putExtra("mediaId", id), null
)
}
NotificationClickType.ACTIVITY -> {
ContextCompat.startActivity(
this, Intent(this, FeedActivity::class.java)
.putExtra("activityId", id), null
)
}
NotificationClickType.COMMENT -> {
ContextCompat.startActivity(
this, Intent(this, MediaDetailsActivity::class.java)
.putExtra("FRAGMENT_TO_LOAD", "COMMENTS")
.putExtra("mediaId", id)
.putExtra("commentId", optional ?: -1),
null
)
}
NotificationClickType.UNDEFINED -> {
// Do nothing
}
}
}
companion object {
enum class NotificationClickType {
USER, MEDIA, ACTIVITY, COMMENT, UNDEFINED
}
}
}

View file

@ -1,4 +1,4 @@
package ani.dantotsu.home.status package ani.dantotsu.profile.activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -14,9 +14,8 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.ActivityReply import ani.dantotsu.connections.anilist.api.ActivityReply
import ani.dantotsu.databinding.BottomSheetRecyclerBinding import ani.dantotsu.databinding.BottomSheetRecyclerBinding
import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.ActivityReplyItem
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.MarkdownCreatorActivity import ani.dantotsu.util.ActivityMarkdownCreator
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -49,7 +48,7 @@ class RepliesBottomDialog : BottomSheetDialogFragment() {
binding.replyButton.setOnClickListener { binding.replyButton.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
context, context,
Intent(context, MarkdownCreatorActivity::class.java) Intent(context, ActivityMarkdownCreator::class.java)
.putExtra("type", "replyActivity") .putExtra("type", "replyActivity")
.putExtra("parentId", activityId), .putExtra("parentId", activityId),
null null
@ -58,29 +57,30 @@ class RepliesBottomDialog : BottomSheetDialogFragment() {
activityId = requireArguments().getInt("activityId") activityId = requireArguments().getInt("activityId")
loading(true) loading(true)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val response = Anilist.query.getReplies(activityId) loadData()
withContext(Dispatchers.Main) { }
loading(false) }
if (response != null) {
replies.clear() private suspend fun loadData() {
replies.addAll(response.data.page.activityReplies) val response = Anilist.query.getReplies(activityId)
adapter.update( withContext(Dispatchers.Main) {
replies.map { loading(false)
ActivityReplyItem( if (response != null) {
it, replies.clear()
requireActivity(), replies.addAll(response.data.page.activityReplies)
clickCallback = { int, _ -> adapter.update(
onClick(int) replies.map {
} ActivityReplyItem(
) it, activityId, requireActivity(), adapter,
) { i, _ ->
onClick(i)
} }
) }
} else { )
snackString("Failed to load replies") } else {
} snackString("Failed to load replies")
} }
} }
} }
private fun onClick(int: Int) { private fun onClick(int: Int) {
@ -101,6 +101,14 @@ class RepliesBottomDialog : BottomSheetDialogFragment() {
super.onDestroyView() super.onDestroyView()
} }
override fun onResume() {
super.onResume()
loading(true)
lifecycleScope.launch(Dispatchers.IO) {
loadData()
}
}
companion object { companion object {
fun newInstance(activityId: Int): RepliesBottomDialog { fun newInstance(activityId: Int): RepliesBottomDialog {
return RepliesBottomDialog().apply { return RepliesBottomDialog().apply {

View file

@ -0,0 +1,100 @@
package ani.dantotsu.profile.notification
import android.os.Bundle
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.databinding.ActivityNotificationBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.*
import ani.dantotsu.profile.notification.NotificationFragment.Companion.newInstance
import nl.joery.animatedbottombar.AnimatedBottomBar
class NotificationActivity : AppCompatActivity() {
lateinit var binding: ActivityNotificationBinding
private var selected: Int = 0
lateinit var navBar: AnimatedBottomBar
private val CommentsEnabled = PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
binding = ActivityNotificationBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.notificationTitle.text = getString(R.string.notifications)
binding.notificationToolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
navBar = binding.notificationNavBar
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
val tabs = mutableListOf(
Pair(R.drawable.ic_round_person_24, "User"),
Pair(R.drawable.ic_round_movie_filter_24, "Media"),
Pair(R.drawable.ic_round_notifications_active_24, "Subs")
)
if (CommentsEnabled) {
tabs.add(Pair(R.drawable.ic_round_comment_24, "Comments"))
}
tabs.forEach { (icon, title) -> navBar.addTab(navBar.createTab(icon, title)) }
binding.notificationBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() }
val getOne = intent.getIntExtra("activityId", -1)
if (getOne != -1) navBar.isVisible = false
binding.notificationViewPager.isUserInputEnabled = false
binding.notificationViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, getOne, CommentsEnabled)
binding.notificationViewPager.setCurrentItem(selected, false)
navBar.selectTabAt(selected)
navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
selected = newIndex
binding.notificationViewPager.setCurrentItem(selected, false)
}
})
}
override fun onResume() {
super.onResume()
if (this::navBar.isInitialized) {
navBar.selectTabAt(selected)
}
}
private class ViewPagerAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle,
val id: Int = -1,
val commentsEnabled: Boolean
) : FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int = if (id != -1) 1 else if (commentsEnabled) 4 else 3
override fun createFragment(position: Int): Fragment = when (position) {
0 -> newInstance(if (id != -1) ONE else USER, id)
1 -> newInstance(MEDIA)
2 -> newInstance(SUBSCRIPTION)
3 -> newInstance(COMMENT)
else -> newInstance(MEDIA)
}
}
}

View file

@ -0,0 +1,233 @@
package ani.dantotsu.profile.notification
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Notification
import ani.dantotsu.databinding.FragmentNotificationsBinding
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.comment.CommentStore
import ani.dantotsu.notifications.subscription.SubscriptionStore
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity
import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.COMMENT
import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.MEDIA
import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.ONE
import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.SUBSCRIPTION
import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.USER
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.xwray.groupie.GroupieAdapter
import eu.kanade.tachiyomi.util.system.getSerializableCompat
import kotlinx.coroutines.launch
class NotificationFragment : Fragment() {
private lateinit var type: NotificationType
private var getID: Int = -1
private lateinit var binding: FragmentNotificationsBinding
private var adapter: GroupieAdapter = GroupieAdapter()
private var currentPage = 1
private var hasNextPage = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentNotificationsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.let {
getID = it.getInt("id")
type = it.getSerializableCompat<NotificationType>("type") as NotificationType
}
binding.notificationRecyclerView.adapter = adapter
binding.notificationRecyclerView.layoutManager = LinearLayoutManager(context)
binding.notificationProgressBar.isVisible = true
binding.emptyTextView.text = getString(R.string.nothing_here)
lifecycleScope.launch {
getList()
binding.notificationProgressBar.isVisible = false
}
binding.notificationSwipeRefresh.setOnRefreshListener {
lifecycleScope.launch {
adapter.clear()
currentPage = 1
getList()
binding.notificationSwipeRefresh.isRefreshing = false
}
}
binding.notificationRecyclerView.addOnScrollListener(object :
RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (shouldLoadMore()) {
lifecycleScope.launch {
binding.notificationRefresh.isVisible = true
getList()
binding.notificationRefresh.isVisible = false
}
}
}
})
}
private suspend fun getList() {
val list = when (type) {
ONE -> getNotificationsFiltered(false) { it.id == getID }
MEDIA -> getNotificationsFiltered(type = true) { it.media != null }
USER -> getNotificationsFiltered { it.media == null }
SUBSCRIPTION -> getSubscriptions()
COMMENT -> getComments()
}
adapter.addAll(list.map { NotificationItem(it, type, adapter, ::onClick) })
if (adapter.itemCount == 0) {
binding.emptyTextView.isVisible = true
}
}
private suspend fun getNotificationsFiltered(
reset: Boolean = true,
type: Boolean? = null,
filter: (Notification) -> Boolean
): List<Notification> {
val userId =
Anilist.userid ?: PrefManager.getVal<String>(PrefName.AnilistUserId).toIntOrNull() ?: 0
val res = Anilist.query.getNotifications(userId, currentPage, reset, type)?.data?.page
currentPage = res?.pageInfo?.currentPage?.plus(1) ?: 1
hasNextPage = res?.pageInfo?.hasNextPage ?: false
return res?.notifications?.filter(filter) ?: listOf()
}
private fun getSubscriptions(): List<Notification> {
val list = PrefManager.getNullableVal<List<SubscriptionStore>>(
PrefName.SubscriptionNotificationStore,
null
) ?: listOf()
return list
.sortedByDescending { (it.time / 1000L).toInt() }
.filter { it.image != null } // to remove old data
.map {
Notification(
it.type,
System.currentTimeMillis().toInt(),
commentId = it.mediaId,
mediaId = it.mediaId,
notificationType = it.type,
context = it.title + ": " + it.content,
createdAt = (it.time / 1000L).toInt(),
image = it.image,
banner = it.banner ?: it.image
)
}
}
private fun getComments(): List<Notification> {
val list = PrefManager.getNullableVal<List<CommentStore>>(
PrefName.CommentNotificationStore,
null
) ?: listOf()
return list
.sortedByDescending { (it.time / 1000L).toInt() }
.map {
Notification(
it.type.toString(),
System.currentTimeMillis().toInt(),
commentId = it.commentId,
notificationType = it.type.toString(),
mediaId = it.mediaId,
context = it.title + "\n" + it.content,
createdAt = (it.time / 1000L).toInt(),
)
}
}
private fun shouldLoadMore(): Boolean {
val layoutManager =
(binding.notificationRecyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
val adapter = binding.notificationRecyclerView.adapter
return hasNextPage && !binding.notificationRefresh.isVisible && adapter?.itemCount != 0 &&
layoutManager == (adapter!!.itemCount - 1) &&
!binding.notificationRecyclerView.canScrollVertically(1)
}
fun onClick(id: Int, optional: Int?, type: NotificationClickType) {
val intent = when (type) {
NotificationClickType.USER -> Intent(
requireContext(),
ProfileActivity::class.java
).apply {
putExtra("userId", id)
}
NotificationClickType.MEDIA -> Intent(
requireContext(),
MediaDetailsActivity::class.java
).apply {
putExtra("mediaId", id)
}
NotificationClickType.ACTIVITY -> Intent(
requireContext(),
FeedActivity::class.java
).apply {
putExtra("activityId", id)
}
NotificationClickType.COMMENT -> Intent(
requireContext(),
MediaDetailsActivity::class.java
).apply {
putExtra("FRAGMENT_TO_LOAD", "COMMENTS")
putExtra("mediaId", id)
putExtra("commentId", optional ?: -1)
}
NotificationClickType.UNDEFINED -> null
}
intent?.let {
ContextCompat.startActivity(requireContext(), it, null)
}
}
override fun onResume() {
super.onResume()
if (this::binding.isInitialized) {
binding.root.requestLayout()
}
}
companion object {
enum class NotificationClickType { USER, MEDIA, ACTIVITY, COMMENT, UNDEFINED }
enum class NotificationType { MEDIA, USER, SUBSCRIPTION, COMMENT, ONE }
fun newInstance(type: NotificationType, id: Int = -1): NotificationFragment {
return NotificationFragment().apply {
arguments = Bundle().apply {
putSerializable("type", type)
putInt("id", id)
}
}
}
}
}

View file

@ -1,4 +1,4 @@
package ani.dantotsu.profile.activity package ani.dantotsu.profile.notification
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -8,15 +8,27 @@ import ani.dantotsu.connections.anilist.api.Notification
import ani.dantotsu.connections.anilist.api.NotificationType import ani.dantotsu.connections.anilist.api.NotificationType
import ani.dantotsu.databinding.ItemNotificationBinding import ani.dantotsu.databinding.ItemNotificationBinding
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.profile.activity.NotificationActivity.Companion.NotificationClickType import ani.dantotsu.notifications.comment.CommentStore
import ani.dantotsu.notifications.subscription.SubscriptionStore
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationClickType
import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.COMMENT
import ani.dantotsu.profile.notification.NotificationFragment.Companion.NotificationType.SUBSCRIPTION
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.toPx import ani.dantotsu.toPx
import ani.dantotsu.util.customAlertDialog
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
class NotificationItem( class NotificationItem(
private val notification: Notification, private val notification: Notification,
val clickCallback: (Int, Int?, NotificationClickType) -> Unit val type: NotificationFragment.Companion.NotificationType,
) : BindableItem<ItemNotificationBinding>() { val parentAdapter: GroupieAdapter,
val clickCallback: (Int, Int?, NotificationClickType) -> Unit,
) : BindableItem<ItemNotificationBinding>() {
private lateinit var binding: ItemNotificationBinding private lateinit var binding: ItemNotificationBinding
override fun bind(viewBinding: ItemNotificationBinding, position: Int) { override fun bind(viewBinding: ItemNotificationBinding, position: Int) {
binding = viewBinding binding = viewBinding
@ -24,6 +36,48 @@ class NotificationItem(
setBinding() setBinding()
} }
fun dialog() {
when (type) {
COMMENT, SUBSCRIPTION -> {
binding.root.context.customAlertDialog().apply {
setTitle(R.string.delete)
setMessage(ActivityItemBuilder.getContent(notification))
setPosButton(R.string.yes) {
when (type) {
COMMENT -> {
val list = PrefManager.getNullableVal<List<CommentStore>>(
PrefName.CommentNotificationStore,
null
) ?: listOf()
val newList = list.filter { it.commentId != notification.commentId }
PrefManager.setVal(PrefName.CommentNotificationStore, newList)
parentAdapter.remove(this@NotificationItem)
}
SUBSCRIPTION -> {
val list = PrefManager.getNullableVal<List<SubscriptionStore>>(
PrefName.SubscriptionNotificationStore,
null
) ?: listOf()
val newList = list.filter { (it.time / 1000L).toInt() != notification.createdAt}
PrefManager.setVal(PrefName.SubscriptionNotificationStore, newList)
parentAdapter.remove(this@NotificationItem)
}
else -> {}
}
}
setNegButton(R.string.no)
show()
}
}
else -> {}
}
}
override fun getLayout(): Int { override fun getLayout(): Int {
return R.layout.item_notification return R.layout.item_notification
} }
@ -32,7 +86,11 @@ class NotificationItem(
return ItemNotificationBinding.bind(view) return ItemNotificationBinding.bind(view)
} }
private fun image(user: Boolean = false, commentNotification: Boolean = false, newRelease: Boolean = false) { private fun image(
user: Boolean = false,
commentNotification: Boolean = false,
newRelease: Boolean = false
) {
val cover = if (user) notification.user?.bannerImage val cover = if (user) notification.user?.bannerImage
?: notification.user?.avatar?.medium else notification.media?.bannerImage ?: notification.user?.avatar?.medium else notification.media?.bannerImage
@ -347,6 +405,14 @@ class NotificationItem(
} }
} }
} }
binding.notificationCoverUser.setOnLongClickListener {
dialog()
true
}
binding.notificationBannerImage.setOnLongClickListener {
dialog()
true
}
} }
} }

View file

@ -0,0 +1,159 @@
package ani.dantotsu.settings
import android.content.Context
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetAddRepositoryBinding
import ani.dantotsu.databinding.ItemRepoBinding
import ani.dantotsu.media.MediaType
import ani.dantotsu.util.customAlertDialog
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.viewbinding.BindableItem
class RepoItem(
val url: String,
val onRemove: (String) -> Unit
) :BindableItem<ItemRepoBinding>() {
override fun getLayout() = R.layout.item_repo
override fun bind(viewBinding: ItemRepoBinding, position: Int) {
viewBinding.repoNameTextView.text = url
viewBinding.repoDeleteImageView.setOnClickListener {
onRemove(url)
}
}
override fun initializeViewBinding(view: View): ItemRepoBinding {
return ItemRepoBinding.bind(view)
}
}
class AddRepositoryBottomSheet : BottomSheetDialogFragment() {
private var _binding: BottomSheetAddRepositoryBinding? = null
private val binding get() = _binding!!
private var mediaType: MediaType = MediaType.ANIME
private var onRepositoryAdded: ((String, MediaType) -> Unit)? = null
private var repositories: MutableList<String> = mutableListOf()
private var onRepositoryRemoved: ((String) -> Unit)? = null
private var adapter: GroupieAdapter = GroupieAdapter()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetAddRepositoryBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.repositoriesRecyclerView.adapter = adapter
binding.repositoriesRecyclerView.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.VERTICAL,
false
)
adapter.addAll(repositories.map { RepoItem(it, ::onRepositoryRemoved) })
binding.repositoryInput.hint = when(mediaType) {
MediaType.ANIME -> getString(R.string.anime_add_repository)
MediaType.MANGA -> getString(R.string.manga_add_repository)
else -> ""
}
binding.addButton.setOnClickListener {
val input = binding.repositoryInput.text.toString()
val error = isValidUrl(input)
if (error == null) {
context?.let { context ->
addRepoWarning(context) {
onRepositoryAdded?.invoke(input, mediaType)
dismiss()
}
}
} else {
binding.repositoryInput.error = error
}
}
binding.cancelButton.setOnClickListener {
dismiss()
}
binding.repositoryInput.setOnEditorActionListener { textView, action, keyEvent ->
if (action == EditorInfo.IME_ACTION_DONE ||
(keyEvent?.action == KeyEvent.ACTION_UP && keyEvent.keyCode == KeyEvent.KEYCODE_ENTER)) {
val url = textView.text.toString()
if (url.isNotBlank()) {
val error = isValidUrl(url)
if (error == null) {
context?.let { context ->
addRepoWarning(context) {
onRepositoryAdded?.invoke(url, mediaType)
dismiss()
}
}
return@setOnEditorActionListener true
} else {
binding.repositoryInput.error = error
}
}
}
false
}
}
private fun onRepositoryRemoved(url: String) {
onRepositoryRemoved?.invoke(url)
repositories.remove(url)
adapter.update(repositories.map { RepoItem(it, ::onRepositoryRemoved) })
}
private fun isValidUrl(url: String): String? {
if (!url.startsWith("https://") && !url.startsWith("http://"))
return "URL must start with http:// or https://"
if (!url.removeSuffix("/").endsWith("index.min.json"))
return "URL must end with index.min.json"
return null
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
fun addRepoWarning(context: Context, onRepositoryAdded: () -> Unit) {
context.customAlertDialog()
.setTitle(R.string.warning)
.setMessage(R.string.add_repository_warning)
.setPosButton(R.string.ok) {
onRepositoryAdded.invoke()
}
.setNegButton(R.string.cancel) { }
.show()
}
fun newInstance(
mediaType: MediaType,
repositories: List<String>,
onRepositoryAdded: (String, MediaType) -> Unit,
onRepositoryRemoved: (String) -> Unit
): AddRepositoryBottomSheet {
return AddRepositoryBottomSheet().apply {
this.mediaType = mediaType
this.repositories.addAll(repositories)
this.onRepositoryAdded = onRepositoryAdded
this.onRepositoryRemoved = onRepositoryRemoved
}
}
}
}

View file

@ -0,0 +1,329 @@
package ani.dantotsu.settings
import android.os.Bundle
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.Anilist.activityMergeTimeMap
import ani.dantotsu.connections.anilist.Anilist.rowOrderMap
import ani.dantotsu.connections.anilist.Anilist.scoreFormats
import ani.dantotsu.connections.anilist.Anilist.staffNameLang
import ani.dantotsu.connections.anilist.Anilist.titleLang
import ani.dantotsu.connections.anilist.AnilistMutations
import ani.dantotsu.connections.anilist.api.ScoreFormat
import ani.dantotsu.connections.anilist.api.UserStaffNameLanguage
import ani.dantotsu.connections.anilist.api.UserTitleLanguage
import ani.dantotsu.databinding.ActivitySettingsAnilistBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.restartApp
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.launch
class AnilistSettingsActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsAnilistBinding
private lateinit var anilistMutations: AnilistMutations
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
val context = this
binding = ActivitySettingsAnilistBinding.inflate(layoutInflater)
setContentView(binding.root)
anilistMutations = AnilistMutations()
binding.apply {
settingsAnilistLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
binding.anilistSettingsBack.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
val currentTitleLang = Anilist.titleLanguage
val titleFormat = UserTitleLanguage.entries.firstOrNull { it.name == currentTitleLang } ?: UserTitleLanguage.ENGLISH
settingsAnilistTitleLanguage.setText(titleLang[titleFormat.ordinal])
settingsAnilistTitleLanguage.setAdapter(
ArrayAdapter(context, R.layout.item_dropdown, titleLang)
)
settingsAnilistTitleLanguage.setOnItemClickListener { _, _, i, _ ->
val selectedLanguage = when (i) {
0 -> "ENGLISH"
1 -> "ROMAJI"
2 -> "NATIVE"
else -> "ENGLISH"
}
lifecycleScope.launch {
anilistMutations.updateSettings(titleLanguage = selectedLanguage)
Anilist.titleLanguage = selectedLanguage
restartApp()
}
settingsAnilistTitleLanguage.clearFocus()
}
val currentStaffNameLang = Anilist.staffNameLanguage
val staffNameFormat = UserStaffNameLanguage.entries.firstOrNull { it.name == currentStaffNameLang } ?: UserStaffNameLanguage.ROMAJI_WESTERN
settingsAnilistStaffLanguage.setText(staffNameLang[staffNameFormat.ordinal])
settingsAnilistStaffLanguage.setAdapter(
ArrayAdapter(context, R.layout.item_dropdown, staffNameLang)
)
settingsAnilistStaffLanguage.setOnItemClickListener { _, _, i, _ ->
val selectedLanguage = when (i) {
0 -> "ROMAJI_WESTERN"
1 -> "ROMAJI"
2 -> "NATIVE"
else -> "ROMAJI_WESTERN"
}
lifecycleScope.launch {
anilistMutations.updateSettings(staffNameLanguage = selectedLanguage)
Anilist.staffNameLanguage = selectedLanguage
restartApp()
}
settingsAnilistStaffLanguage.clearFocus()
}
val currentMergeTimeDisplay = activityMergeTimeMap.entries.firstOrNull { it.value == Anilist.activityMergeTime }?.key
?: "${Anilist.activityMergeTime} mins"
settingsAnilistActivityMergeTime.setText(currentMergeTimeDisplay)
settingsAnilistActivityMergeTime.setAdapter(
ArrayAdapter(context, R.layout.item_dropdown, activityMergeTimeMap.keys.toList())
)
settingsAnilistActivityMergeTime.setOnItemClickListener { _, _, i, _ ->
val selectedDisplayTime = activityMergeTimeMap.keys.toList()[i]
val selectedApiTime = activityMergeTimeMap[selectedDisplayTime] ?: 0
lifecycleScope.launch {
anilistMutations.updateSettings(activityMergeTime = selectedApiTime)
Anilist.activityMergeTime = selectedApiTime
restartApp()
}
settingsAnilistActivityMergeTime.clearFocus()
}
val currentScoreFormat = Anilist.scoreFormat
val scoreFormat = ScoreFormat.entries.firstOrNull{ it.name == currentScoreFormat } ?: ScoreFormat.POINT_100
settingsAnilistScoreFormat.setText(scoreFormats[scoreFormat.ordinal])
settingsAnilistScoreFormat.setAdapter(
ArrayAdapter(context, R.layout.item_dropdown, scoreFormats)
)
settingsAnilistScoreFormat.setOnItemClickListener { _, _, i, _ ->
val selectedFormat = when (i) {
0 -> "POINT_100"
1 -> "POINT_10_DECIMAL"
2 -> "POINT_10"
3 -> "POINT_5"
4 -> "POINT_3"
else -> "POINT_100"
}
lifecycleScope.launch {
anilistMutations.updateSettings(scoreFormat = selectedFormat)
Anilist.scoreFormat = selectedFormat
restartApp()
}
settingsAnilistScoreFormat.clearFocus()
}
val currentRowOrder = rowOrderMap.entries.firstOrNull { it.value == Anilist.rowOrder }?.key ?: "Score"
settingsAnilistRowOrder.setText(currentRowOrder)
settingsAnilistRowOrder.setAdapter(
ArrayAdapter(context, R.layout.item_dropdown, rowOrderMap.keys.toList())
)
settingsAnilistRowOrder.setOnItemClickListener { _, _, i, _ ->
val selectedDisplayOrder = rowOrderMap.keys.toList()[i]
val selectedApiOrder = rowOrderMap[selectedDisplayOrder] ?: "score"
lifecycleScope.launch {
anilistMutations.updateSettings(rowOrder = selectedApiOrder)
Anilist.rowOrder = selectedApiOrder
restartApp()
}
settingsAnilistRowOrder.clearFocus()
}
val containers = listOf(binding.animeCustomListsContainer, binding.mangaCustomListsContainer)
val customLists = listOf(Anilist.animeCustomLists, Anilist.mangaCustomLists)
val buttons = listOf(binding.addAnimeListButton, binding.addMangaListButton)
containers.forEachIndexed { index, container ->
customLists[index]?.forEach { listName ->
addCustomListItem(listName, container, index == 0)
}
}
buttons.forEachIndexed { index, button ->
button.setOnClickListener {
addCustomListItem("", containers[index], index == 0)
}
}
binding.SettingsAnilistCustomListSave.setOnClickListener {
saveCustomLists()
}
val currentTimezone = Anilist.timezone?.let { Anilist.getDisplayTimezone(it, context) } ?: context.getString(R.string.selected_no_time_zone)
settingsAnilistTimezone.setText(currentTimezone)
settingsAnilistTimezone.setAdapter(
ArrayAdapter(context, R.layout.item_dropdown, Anilist.timeZone)
)
settingsAnilistTimezone.setOnItemClickListener { _, _, i, _ ->
val selectedTimezone = Anilist.timeZone[i]
val apiTimezone = Anilist.getApiTimezone(selectedTimezone)
lifecycleScope.launch {
anilistMutations.updateSettings(timezone = apiTimezone)
Anilist.timezone = apiTimezone
restartApp()
}
settingsAnilistTimezone.clearFocus()
}
val displayAdultContent = Anilist.adult
val airingNotifications = Anilist.airingNotifications
binding.settingsRecyclerView1.adapter = SettingsAdapter(
arrayListOf(
Settings(
type = 2,
name = getString(R.string.airing_notifications),
desc = getString(R.string.airing_notifications_desc),
icon = R.drawable.ic_round_notifications_active_24,
isChecked = airingNotifications,
switch = { isChecked, _ ->
lifecycleScope.launch {
anilistMutations.updateSettings(airingNotifications = isChecked)
Anilist.airingNotifications = isChecked
restartApp()
}
}
),
Settings(
type = 2,
name = getString(R.string.display_adult_content),
desc = getString(R.string.display_adult_content_desc),
icon = R.drawable.ic_round_nsfw_24,
isChecked = displayAdultContent,
switch = { isChecked, _ ->
lifecycleScope.launch {
anilistMutations.updateSettings(displayAdultContent = isChecked)
Anilist.adult = isChecked
restartApp()
}
}
),
)
)
binding.settingsRecyclerView1.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}
binding.settingsRecyclerView2.adapter = SettingsAdapter(
arrayListOf(
Settings(
type = 2,
name = getString(R.string.restrict_messages),
desc = getString(R.string.restrict_messages_desc),
icon = R.drawable.ic_round_lock_open_24,
isChecked = Anilist.restrictMessagesToFollowing,
switch = { isChecked, _ ->
lifecycleScope.launch {
anilistMutations.updateSettings(restrictMessagesToFollowing = isChecked)
Anilist.restrictMessagesToFollowing = isChecked
restartApp()
}
}
),
)
)
binding.settingsRecyclerView2.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}
private fun addCustomListItem(listName: String, container: LinearLayout, isAnime: Boolean) {
val customListItemView = layoutInflater.inflate(R.layout.item_custom_list, container, false)
val textInputLayout = customListItemView.findViewById<TextInputLayout>(R.id.customListItem)
val editText = textInputLayout.editText as? TextInputEditText
editText?.setText(listName)
textInputLayout.setEndIconOnClickListener {
val name = editText?.text.toString()
if (name.isNotEmpty()) {
val listExists = if (isAnime) {
Anilist.animeCustomLists?.contains(name) ?: false
} else {
Anilist.mangaCustomLists?.contains(name) ?: false
}
if (listExists) {
customAlertDialog().apply {
setTitle(getString(R.string.delete_custom_list))
setMessage(getString(R.string.delete_custom_list_confirm, name))
setPosButton(getString(R.string.delete)) {
deleteCustomList(name, isAnime)
container.removeView(customListItemView)
}
setNegButton(getString(R.string.cancel))
}.show()
} else {
container.removeView(customListItemView)
}
} else {
container.removeView(customListItemView)
}
}
container.addView(customListItemView)
}
private fun deleteCustomList(name: String, isAnime: Boolean) {
lifecycleScope.launch {
val type = if (isAnime) "ANIME" else "MANGA"
val success = anilistMutations.deleteCustomList(name, type)
if (success) {
if (isAnime) {
Anilist.animeCustomLists = Anilist.animeCustomLists?.filter { it != name }
} else {
Anilist.mangaCustomLists = Anilist.mangaCustomLists?.filter { it != name }
}
toast("Custom list deleted")
} else {
toast("Failed to delete custom list")
}
}
}
private fun saveCustomLists() {
val animeCustomLists = binding.animeCustomListsContainer.children
.mapNotNull { (it.findViewById<TextInputLayout>(R.id.customListItem).editText as? TextInputEditText)?.text?.toString() }
.filter { it.isNotEmpty() }
.toList()
val mangaCustomLists = binding.mangaCustomListsContainer.children
.mapNotNull { (it.findViewById<TextInputLayout>(R.id.customListItem).editText as? TextInputEditText)?.text?.toString() }
.filter { it.isNotEmpty() }
.toList()
lifecycleScope.launch {
val success = anilistMutations.updateCustomLists(animeCustomLists, mangaCustomLists)
if (success) {
Anilist.animeCustomLists = animeCustomLists
Anilist.mangaCustomLists = mangaCustomLists
toast("Custom lists saved")
} else {
toast("Failed to save custom lists")
}
}
}
}

View file

@ -21,7 +21,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.copyToClipboard import ani.dantotsu.copyToClipboard
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ActivityExtensionsBinding import ani.dantotsu.databinding.ActivityExtensionsBinding
import ani.dantotsu.databinding.DialogRepositoriesBinding import ani.dantotsu.databinding.DialogRepositoriesBinding
import ani.dantotsu.databinding.ItemRepositoryBinding import ani.dantotsu.databinding.ItemRepositoryBinding
@ -35,6 +34,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
@ -43,6 +43,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Locale
class ExtensionsActivity : AppCompatActivity() { class ExtensionsActivity : AppCompatActivity() {
lateinit var binding: ActivityExtensionsBinding lateinit var binding: ActivityExtensionsBinding
@ -173,26 +174,24 @@ class ExtensionsActivity : AppCompatActivity() {
initActivity(this) initActivity(this)
binding.languageselect.setOnClickListener { binding.languageselect.setOnClickListener {
val languageOptions = val languageOptions =
LanguageMapper.Companion.Language.entries.map { it.name }.toTypedArray() LanguageMapper.Companion.Language.entries.map { entry ->
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup) entry.name.lowercase().replace("_", " ")
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() }
}.toTypedArray()
val listOrder: String = PrefManager.getVal(PrefName.LangSort) val listOrder: String = PrefManager.getVal(PrefName.LangSort)
val index = LanguageMapper.Companion.Language.entries.toTypedArray() val index = LanguageMapper.Companion.Language.entries.toTypedArray()
.indexOfFirst { it.code == listOrder } .indexOfFirst { it.code == listOrder }
builder.setTitle("Language") customAlertDialog().apply {
builder.setSingleChoiceItems(languageOptions, index) { dialog, i -> setTitle("Language")
PrefManager.setVal( singleChoiceItems(languageOptions, index) { selected ->
PrefName.LangSort, PrefManager.setVal(PrefName.LangSort, LanguageMapper.Companion.Language.entries[selected].code)
LanguageMapper.Companion.Language.entries[i].code val currentFragment = supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}")
) if (currentFragment is SearchQueryHandler) {
val currentFragment = currentFragment.notifyDataChanged()
supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}") }
if (currentFragment is SearchQueryHandler) {
currentFragment.notifyDataChanged()
} }
dialog.dismiss() show()
} }
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
} }
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight topMargin = statusBarHeight
@ -243,10 +242,10 @@ class ExtensionsActivity : AppCompatActivity() {
) )
view.repositoryItem.text = item.removePrefix("https://raw.githubusercontent.com") view.repositoryItem.text = item.removePrefix("https://raw.githubusercontent.com")
view.repositoryItem.setOnClickListener { view.repositoryItem.setOnClickListener {
AlertDialog.Builder(this@ExtensionsActivity, R.style.MyPopup) customAlertDialog().apply {
.setTitle(R.string.rem_repository) setTitle(R.string.rem_repository)
.setMessage(item) setMessage(item)
.setPositiveButton(getString(R.string.ok)) { dialog, _ -> setPosButton(R.string.ok) {
val repos = PrefManager.getVal<Set<String>>(prefName).minus(item) val repos = PrefManager.getVal<Set<String>>(prefName).minus(item)
PrefManager.setVal(prefName, repos) PrefManager.setVal(prefName, repos)
repoInventory.removeView(view.root) repoInventory.removeView(view.root)
@ -263,13 +262,10 @@ class ExtensionsActivity : AppCompatActivity() {
else -> {} else -> {}
} }
} }
dialog.dismiss()
} }
.setNegativeButton(getString(R.string.cancel)) { dialog, _ -> setNegButton(R.string.cancel)
dialog.dismiss() show()
} }
.create()
.show()
} }
view.repositoryItem.setOnLongClickListener { view.repositoryItem.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
@ -320,20 +316,18 @@ class ExtensionsActivity : AppCompatActivity() {
dialogView.repoInventory.apply { dialogView.repoInventory.apply {
getSavedRepositories(this, type) getSavedRepositories(this, type)
} }
val alertDialog = AlertDialog.Builder(this@ExtensionsActivity, R.style.MyPopup)
.setTitle(R.string.edit_repositories)
.setView(dialogView.root)
.setPositiveButton(getString(R.string.add)) { _, _ ->
if (!dialogView.repositoryTextBox.text.isNullOrBlank())
processUserInput(dialogView.repositoryTextBox.text.toString(), type)
}
.setNegativeButton(getString(R.string.close)) { dialog, _ ->
dialog.dismiss()
}
.create()
processEditorAction(dialogView.repositoryTextBox, type) processEditorAction(dialogView.repositoryTextBox, type)
alertDialog.show() customAlertDialog().apply {
alertDialog.window?.setDimAmount(0.8f) setTitle(R.string.edit_repositories)
setCustomView(dialogView.root)
setPosButton(R.string.add_list) {
if (!dialogView.repositoryTextBox.text.isNullOrBlank()) {
processUserInput(dialogView.repositoryTextBox.text.toString(), type)
}
}
setNegButton(R.string.close)
show()
}
} }
} }
} }

View file

@ -50,6 +50,11 @@ class FAQActivity : AppCompatActivity() {
currContext()?.getString(R.string.question_5) ?: "", currContext()?.getString(R.string.question_5) ?: "",
currContext()?.getString(R.string.answer_5) ?: "" currContext()?.getString(R.string.answer_5) ?: ""
), ),
Triple(
R.drawable.ic_anilist,
currContext()?.getString(R.string.question_18) ?: "",
currContext()?.getString(R.string.answer_18) ?: ""
),
Triple( Triple(
R.drawable.ic_anilist, R.drawable.ic_anilist,
currContext()?.getString(R.string.question_6) ?: "", currContext()?.getString(R.string.question_6) ?: "",
@ -60,6 +65,11 @@ class FAQActivity : AppCompatActivity() {
currContext()?.getString(R.string.question_7) ?: "", currContext()?.getString(R.string.question_7) ?: "",
currContext()?.getString(R.string.answer_7) ?: "" currContext()?.getString(R.string.answer_7) ?: ""
), ),
Triple(
R.drawable.ic_round_magnet_24,
currContext()?.getString(R.string.question_19) ?: "",
currContext()?.getString(R.string.answer_19) ?: ""
),
Triple( Triple(
R.drawable.ic_round_lock_open_24, R.drawable.ic_round_lock_open_24,
currContext()?.getString(R.string.question_9) ?: "", currContext()?.getString(R.string.question_9) ?: "",

View file

@ -1,6 +1,5 @@
package ani.dantotsu.settings package ani.dantotsu.settings
import android.app.AlertDialog
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
@ -25,13 +24,15 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.FragmentExtensionsBinding import ani.dantotsu.databinding.FragmentExtensionsBinding
import ani.dantotsu.others.LanguageMapper import ani.dantotsu.others.LanguageMapper.Companion.getLanguageName
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
@ -42,13 +43,10 @@ import kotlinx.coroutines.launch
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Collections
import java.util.Locale import java.util.Locale
class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler { class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
private var _binding: FragmentExtensionsBinding? = null private var _binding: FragmentExtensionsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var extensionsRecyclerView: RecyclerView private lateinit var extensionsRecyclerView: RecyclerView
@ -72,16 +70,15 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
if (allSettings.isNotEmpty()) { if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0] var selectedSetting = allSettings[0]
if (allSettings.size > 1) { if (allSettings.size > 1) {
val names = allSettings.map { LanguageMapper.getLanguageName(it.lang) } val names = allSettings.map { getLanguageName(it.lang) }
.toTypedArray() .toTypedArray()
var selectedIndex = 0 var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) requireContext().customAlertDialog().apply {
.setTitle("Select a Source") setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { dialog, which -> singleChoiceItems(names, selectedIndex) { which ->
itemSelected = true itemSelected = true
selectedIndex = which selectedIndex = which
selectedSetting = allSettings[selectedIndex] selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
val fragment = val fragment =
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
@ -93,13 +90,13 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
.addToBackStack(null) .addToBackStack(null)
.commit() .commit()
} }
.setOnDismissListener { onDismiss {
if (!itemSelected) { if (!itemSelected) {
changeUIVisibility(true) changeUIVisibility(true)
} }
} }
.show() show()
dialog.window?.setDimAmount(0.8f) }
} else { } else {
// If there's only one setting, proceed with the fragment transaction // If there's only one setting, proceed with the fragment transaction
val fragment = val fragment =
@ -121,15 +118,20 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
.show() .show()
} }
}, },
{ pkg, forceDelete -> { pkg ->
if (isAdded) { // Check if the fragment is currently added to its activity if (isAdded) {
val context = requireContext() // Store context in a variable animeExtensionManager.uninstallExtension(pkg.pkgName)
snackString("Extension uninstalled")
}
}, { pkg ->
if (isAdded) {
val context = requireContext()
val notificationManager = val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (pkg.hasUpdate && !forceDelete) { if (pkg.hasUpdate) {
animeExtensionManager.updateExtension(pkg) animeExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
{ installStep -> { installStep ->
val builder = NotificationCompat.Builder( val builder = NotificationCompat.Builder(
@ -144,7 +146,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
}, },
{ error -> { error ->
Injekt.get<CrashlyticsInterface>().logException(error) Injekt.get<CrashlyticsInterface>().logException(error)
Logger.log(error) // Log the error Logger.log(error)
val builder = NotificationCompat.Builder( val builder = NotificationCompat.Builder(
context, context,
Notifications.CHANNEL_DOWNLOADER_ERROR Notifications.CHANNEL_DOWNLOADER_ERROR
@ -170,14 +172,13 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
} }
) )
} else { } else {
animeExtensionManager.uninstallExtension(pkg.pkgName) snackString("No update available")
snackString("Extension uninstalled")
} }
} }
}, skipIcons }, skipIcons
) )
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -197,17 +198,10 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder target: RecyclerView.ViewHolder
): Boolean { ): Boolean {
val newList = extensionsAdapter.currentList.toMutableList()
val fromPosition = viewHolder.absoluteAdapterPosition val fromPosition = viewHolder.absoluteAdapterPosition
val toPosition = target.absoluteAdapterPosition val toPosition = target.absoluteAdapterPosition
if (fromPosition < toPosition) { //probably need to switch to a recyclerview adapter val newList = extensionsAdapter.currentList.toMutableList().apply {
for (i in fromPosition until toPosition) { add(toPosition, removeAt(fromPosition))
Collections.swap(newList, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(newList, i, i - 1)
}
} }
extensionsAdapter.submitList(newList) extensionsAdapter.submitList(newList)
return true return true
@ -269,7 +263,8 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
private class AnimeExtensionsAdapter( private class AnimeExtensionsAdapter(
private val onSettingsClicked: (AnimeExtension.Installed) -> Unit, private val onSettingsClicked: (AnimeExtension.Installed) -> Unit,
private val onUninstallClicked: (AnimeExtension.Installed, Boolean) -> Unit, private val onUninstallClicked: (AnimeExtension.Installed) -> Unit,
private val onUpdateClicked: (AnimeExtension.Installed) -> Unit,
val skipIcons: Boolean val skipIcons: Boolean
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>( ) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED DIFF_CALLBACK_INSTALLED
@ -295,7 +290,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val extension = getItem(position) val extension = getItem(position)
val nsfw = if (extension.isNsfw) "(18+)" else "" val nsfw = if (extension.isNsfw) "(18+)" else ""
val lang = LanguageMapper.getLanguageName(extension.lang) val lang = getLanguageName(extension.lang)
holder.extensionNameTextView.text = extension.name holder.extensionNameTextView.text = extension.name
val versionText = "$lang ${extension.versionName} $nsfw" val versionText = "$lang ${extension.versionName} $nsfw"
holder.extensionVersionTextView.text = versionText holder.extensionVersionTextView.text = versionText
@ -303,20 +298,19 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
holder.extensionIconImageView.setImageDrawable(extension.icon) holder.extensionIconImageView.setImageDrawable(extension.icon)
} }
if (extension.hasUpdate) { if (extension.hasUpdate) {
holder.closeTextView.setImageResource(R.drawable.ic_round_sync_24) holder.updateView.isVisible = true
} else { } else {
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24) holder.updateView.isVisible = false
} }
holder.closeTextView.setOnClickListener { holder.deleteView.setOnClickListener {
onUninstallClicked(extension, false) onUninstallClicked(extension)
}
holder.updateView.setOnClickListener {
onUpdateClicked(extension)
} }
holder.settingsImageView.setOnClickListener { holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension) onSettingsClicked(extension)
} }
holder.closeTextView.setOnLongClickListener {
onUninstallClicked(extension, true)
true
}
} }
fun filter(query: String, currentList: List<AnimeExtension.Installed>) { fun filter(query: String, currentList: List<AnimeExtension.Installed>) {
@ -336,7 +330,8 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
view.findViewById(R.id.extensionVersionTextView) view.findViewById(R.id.extensionVersionTextView)
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView) val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView) val deleteView: ImageView = view.findViewById(R.id.deleteTextView)
val updateView: ImageView = view.findViewById(R.id.updateTextView)
} }
companion object { companion object {

View file

@ -1,8 +1,6 @@
package ani.dantotsu.settings package ani.dantotsu.settings
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
@ -28,12 +26,14 @@ import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.FragmentExtensionsBinding import ani.dantotsu.databinding.FragmentExtensionsBinding
import ani.dantotsu.others.LanguageMapper import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.LanguageMapper.Companion.getLanguageName
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
@ -44,7 +44,6 @@ import kotlinx.coroutines.launch
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Collections
import java.util.Locale import java.util.Locale
class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler { class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
@ -74,13 +73,12 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
val names = allSettings.map { LanguageMapper.getLanguageName(it.lang) } val names = allSettings.map { LanguageMapper.getLanguageName(it.lang) }
.toTypedArray() .toTypedArray()
var selectedIndex = 0 var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) requireContext().customAlertDialog().apply {
.setTitle("Select a Source") setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { dialog, which -> singleChoiceItems(names, selectedIndex) { which ->
itemSelected = true itemSelected = true
selectedIndex = which selectedIndex = which
selectedSetting = allSettings[selectedIndex] selectedSetting = allSettings[selectedIndex]
dialog.dismiss()
// Move the fragment transaction here // Move the fragment transaction here
val fragment = val fragment =
@ -93,13 +91,13 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
.addToBackStack(null) .addToBackStack(null)
.commit() .commit()
} }
.setOnDismissListener { onDismiss {
if (!itemSelected) { if (!itemSelected) {
changeUIVisibility(true) changeUIVisibility(true)
} }
} }
.show() show()
dialog.window?.setDimAmount(0.8f) }
} else { } else {
// If there's only one setting, proceed with the fragment transaction // If there's only one setting, proceed with the fragment transaction
val fragment = val fragment =
@ -120,15 +118,20 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
.show() .show()
} }
}, },
{ pkg: MangaExtension.Installed, forceDelete: Boolean -> { pkg: MangaExtension.Installed ->
if (isAdded) { // Check if the fragment is currently added to its activity if (isAdded) {
val context = requireContext() // Store context in a variable mangaExtensionManager.uninstallExtension(pkg.pkgName)
snackString("Extension uninstalled")
}
}, { pkg ->
if (isAdded) {
val context = requireContext()
val notificationManager = val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Initialize NotificationManager once context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (pkg.hasUpdate && !forceDelete) { if (pkg.hasUpdate) {
mangaExtensionManager.updateExtension(pkg) mangaExtensionManager.updateExtension(pkg)
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
{ installStep -> { installStep ->
val builder = NotificationCompat.Builder( val builder = NotificationCompat.Builder(
@ -143,7 +146,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
}, },
{ error -> { error ->
Injekt.get<CrashlyticsInterface>().logException(error) Injekt.get<CrashlyticsInterface>().logException(error)
Logger.log(error) // Log the error Logger.log(error)
val builder = NotificationCompat.Builder( val builder = NotificationCompat.Builder(
context, context,
Notifications.CHANNEL_DOWNLOADER_ERROR Notifications.CHANNEL_DOWNLOADER_ERROR
@ -160,7 +163,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
context, context,
Notifications.CHANNEL_DOWNLOADER_PROGRESS Notifications.CHANNEL_DOWNLOADER_PROGRESS
) )
.setSmallIcon(R.drawable.ic_check) .setSmallIcon(R.drawable.ic_circle_check)
.setContentTitle("Update complete") .setContentTitle("Update complete")
.setContentText("The extension has been successfully updated.") .setContentText("The extension has been successfully updated.")
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
@ -169,9 +172,9 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
} }
) )
} else { } else {
mangaExtensionManager.uninstallExtension(pkg.pkgName) snackString("No update available")
snackString("Extension uninstalled")
} }
} }
}, skipIcons }, skipIcons
) )
@ -195,17 +198,10 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder target: RecyclerView.ViewHolder
): Boolean { ): Boolean {
val newList = extensionsAdapter.currentList.toMutableList()
val fromPosition = viewHolder.absoluteAdapterPosition val fromPosition = viewHolder.absoluteAdapterPosition
val toPosition = target.absoluteAdapterPosition val toPosition = target.absoluteAdapterPosition
if (fromPosition < toPosition) { //probably need to switch to a recyclerview adapter val newList = extensionsAdapter.currentList.toMutableList().apply {
for (i in fromPosition until toPosition) { add(toPosition, removeAt(fromPosition))
Collections.swap(newList, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(newList, i, i - 1)
}
} }
extensionsAdapter.submitList(newList) extensionsAdapter.submitList(newList)
return true return true
@ -266,7 +262,8 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
private class MangaExtensionsAdapter( private class MangaExtensionsAdapter(
private val onSettingsClicked: (MangaExtension.Installed) -> Unit, private val onSettingsClicked: (MangaExtension.Installed) -> Unit,
private val onUninstallClicked: (MangaExtension.Installed, Boolean) -> Unit, private val onUninstallClicked: (MangaExtension.Installed) -> Unit,
private val onUpdateClicked: (MangaExtension.Installed) -> Unit,
val skipIcons: Boolean val skipIcons: Boolean
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>( ) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
DIFF_CALLBACK_INSTALLED DIFF_CALLBACK_INSTALLED
@ -276,24 +273,23 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
submitList(newExtensions) submitList(newExtensions)
} }
fun updatePref() {
val map = currentList.map { it.name }
PrefManager.setVal(PrefName.MangaSourcesOrder, map)
MangaSources.pinnedMangaSources = map
MangaSources.performReorderMangaSources()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_extension, parent, false) .inflate(R.layout.item_extension, parent, false)
return ViewHolder(view) return ViewHolder(view)
} }
fun updatePref() {
val map = currentList.map { it.name }.toList()
PrefManager.setVal(PrefName.MangaSourcesOrder, map)
MangaSources.pinnedMangaSources = map
MangaSources.performReorderMangaSources()
}
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val extension = getItem(position) // Use getItem() from ListAdapter val extension = getItem(position)
val nsfw = if (extension.isNsfw) "(18+)" else "" val nsfw = if (extension.isNsfw) "(18+)" else ""
val lang = LanguageMapper.getLanguageName(extension.lang) val lang = getLanguageName(extension.lang)
holder.extensionNameTextView.text = extension.name holder.extensionNameTextView.text = extension.name
val versionText = "$lang ${extension.versionName} $nsfw" val versionText = "$lang ${extension.versionName} $nsfw"
holder.extensionVersionTextView.text = versionText holder.extensionVersionTextView.text = versionText
@ -301,12 +297,15 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
holder.extensionIconImageView.setImageDrawable(extension.icon) holder.extensionIconImageView.setImageDrawable(extension.icon)
} }
if (extension.hasUpdate) { if (extension.hasUpdate) {
holder.closeTextView.setImageResource(R.drawable.ic_round_sync_24) holder.updateView.isVisible = true
} else { } else {
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24) holder.updateView.isVisible = false
} }
holder.closeTextView.setOnClickListener { holder.deleteView.setOnClickListener {
onUninstallClicked(extension, false) onUninstallClicked(extension)
}
holder.updateView.setOnClickListener {
onUpdateClicked(extension)
} }
holder.settingsImageView.setOnClickListener { holder.settingsImageView.setOnClickListener {
onSettingsClicked(extension) onSettingsClicked(extension)
@ -330,7 +329,8 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
view.findViewById(R.id.extensionVersionTextView) view.findViewById(R.id.extensionVersionTextView)
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView) val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView) val extensionIconImageView: ImageView = view.findViewById(R.id.extensionIconImageView)
val closeTextView: ImageView = view.findViewById(R.id.closeTextView) val deleteView: ImageView = view.findViewById(R.id.deleteTextView)
val updateView: ImageView = view.findViewById(R.id.updateTextView)
} }
companion object { companion object {

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