mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-10 22:46:15 +00:00
295
.github/workflows/beta.yml
vendored
295
.github/workflows/beta.yml
vendored
@@ -1,17 +1,25 @@
|
||||
name: Build APK and Notify Discord
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
branches-ignore:
|
||||
- main
|
||||
- l10n_dev_crowdin
|
||||
- custom-download-location
|
||||
paths-ignore:
|
||||
- '**/README.md'
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
CI: true
|
||||
SKIP_BUILD: false
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -19,14 +27,12 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
- name: Download last SHA artifact
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
workflow: beta.yml
|
||||
name: last-sha
|
||||
path: .
|
||||
|
||||
continue-on-error: true
|
||||
|
||||
- name: Get Commits Since Last Run
|
||||
@@ -39,7 +45,9 @@ jobs:
|
||||
fi
|
||||
echo "Commits since $LAST_SHA:"
|
||||
# Accumulate commit logs in a shell variable
|
||||
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an")
|
||||
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an [֍](https://github.com/${{ github.repository }}/commit/%H)" --max-count=10)
|
||||
# Replace commit messages with pull request links
|
||||
COMMIT_LOGS=$(echo "$COMMIT_LOGS" | sed -E 's/#([0-9]+)/[#\1](https:\/\/github.com\/rebelonion\/Dantotsu\/pull\/\1)/g')
|
||||
# URL-encode the newline characters for GitHub Actions
|
||||
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
|
||||
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
|
||||
@@ -49,6 +57,10 @@ jobs:
|
||||
# Debugging: Print the variable to check its content
|
||||
echo "$COMMIT_LOGS"
|
||||
echo "$COMMIT_LOGS" > commit_log.txt
|
||||
# Extract branch name from github.ref
|
||||
BRANCH=${{ github.ref }}
|
||||
BRANCH=${BRANCH#refs/heads/}
|
||||
echo "BRANCH=${BRANCH}" >> $GITHUB_ENV
|
||||
shell: /usr/bin/bash -e {0}
|
||||
env:
|
||||
CI: true
|
||||
@@ -65,53 +77,278 @@ jobs:
|
||||
echo "Version $VERSION"
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: List files in the directory
|
||||
run: ls -l
|
||||
|
||||
- name: Setup JDK 17
|
||||
if: ${{ env.SKIP_BUILD != 'true' }}
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- name: Decode Keystore File
|
||||
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
|
||||
|
||||
- name: List files in the directory
|
||||
run: ls -l
|
||||
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Decode Keystore File
|
||||
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
|
||||
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
|
||||
|
||||
- name: Make gradlew executable
|
||||
if: ${{ env.SKIP_BUILD != 'true' }}
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Build with Gradle
|
||||
if: ${{ env.SKIP_BUILD != 'true' }}
|
||||
run: |
|
||||
if [ "${{ github.repository }}" == "rebelonion/Dantotsu" ]; then
|
||||
./gradlew assembleGoogleAlpha \
|
||||
-Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore \
|
||||
-Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} \
|
||||
-Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} \
|
||||
-Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }};
|
||||
else
|
||||
./gradlew assembleGoogleAlpha;
|
||||
fi
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
if: ${{ env.SKIP_BUILD != 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Dantotsu
|
||||
retention-days: 5
|
||||
compression-level: 9
|
||||
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
|
||||
|
||||
|
||||
- name: Upload APK to Discord and Telegram
|
||||
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
|
||||
shell: bash
|
||||
run: |
|
||||
#Discord
|
||||
# Prepare Discord embed
|
||||
fetch_user_details() {
|
||||
local login=$1
|
||||
user_details=$(curl -s "https://api.github.com/users/$login")
|
||||
name=$(echo "$user_details" | jq -r '.name // .login')
|
||||
login=$(echo "$user_details" | jq -r '.login')
|
||||
avatar_url=$(echo "$user_details" | jq -r '.avatar_url')
|
||||
echo "$name|$login|$avatar_url"
|
||||
}
|
||||
|
||||
# Additional information for the goats
|
||||
declare -A additional_info
|
||||
additional_info["ibo"]="\n Discord: <@951737931159187457>\n AniList: [takarealist112](<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/')
|
||||
# Truncate commit messages if they are too long
|
||||
max_length=1900 # Adjust this value as needed
|
||||
if [ ${#developers} -gt $max_length ]; then
|
||||
developers="${developers:0:$max_length}... (truncated)"
|
||||
fi
|
||||
if [ ${#commit_messages} -gt $max_length ]; then
|
||||
commit_messages="${commit_messages:0:$max_length}... (truncated)"
|
||||
fi
|
||||
contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
||||
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
#Telegram
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk" \
|
||||
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
|
||||
# Construct Discord payload
|
||||
discord_data=$(jq -nc \
|
||||
--arg field_value "$commit_messages" \
|
||||
--arg author_value "$developers" \
|
||||
--arg footer_text "Version $VERSION" \
|
||||
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
|
||||
--arg thumbnail_url "$thumbnail_url" \
|
||||
--arg embed_color "$embed_color" \
|
||||
'{
|
||||
"content": "<@&1225347048321191996>",
|
||||
"embeds": [
|
||||
{
|
||||
"title": "New Alpha-Build dropped",
|
||||
"color": $embed_color,
|
||||
"fields": [
|
||||
{
|
||||
"name": "Commits:",
|
||||
"value": $field_value,
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
"name": "Developers:",
|
||||
"value": $author_value,
|
||||
"inline": false
|
||||
}
|
||||
],
|
||||
"footer": {
|
||||
"text": $footer_text
|
||||
},
|
||||
"timestamp": $timestamp,
|
||||
"thumbnail": {
|
||||
"url": $thumbnail_url
|
||||
}
|
||||
}
|
||||
],
|
||||
"attachments": []
|
||||
}')
|
||||
echo "Debug: Final Discord payload:"
|
||||
echo "$discord_data"
|
||||
|
||||
# Send Discord message
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d "$discord_data" \
|
||||
${{ secrets.DISCORD_WEBHOOK }}
|
||||
echo "You have only send an embed to discord due to SKIP_BUILD being set to true"
|
||||
|
||||
# Upload APK to Discord
|
||||
if [ "$SKIP_BUILD" != "true" ]; then
|
||||
curl -F "payload_json=${contentbody}" \
|
||||
-F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
|
||||
${{ secrets.DISCORD_WEBHOOK }}
|
||||
else
|
||||
echo "Skipping APK upload to Discord due to SKIP_BUILD being set to true"
|
||||
fi
|
||||
|
||||
# Format commit messages for Telegram
|
||||
telegram_commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g' | while read -r line; do
|
||||
message=$(echo "$line" | sed -E 's/● (.*) ~(.*) \[֍\]\((.*)\)/● \1 ~\2 <a 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:
|
||||
COMMIT_LOG: ${{ env.COMMIT_LOG }}
|
||||
VERSION: ${{ env.VERSION }}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -2,6 +2,9 @@
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
#kotlin
|
||||
.kotlin/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
@@ -33,4 +36,7 @@ output.json
|
||||
scripts/
|
||||
|
||||
#crowdin
|
||||
crowdin.yml
|
||||
crowdin.yml
|
||||
|
||||
#vscode
|
||||
.vscode
|
||||
@@ -11,12 +11,12 @@ def gitCommitHash = providers.exec {
|
||||
}.standardOutput.asText.get().trim()
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
compileSdk 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId "ani.dantotsu"
|
||||
minSdk 21
|
||||
targetSdk 34
|
||||
targetSdk 35
|
||||
versionCode((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "3.1.0"
|
||||
versionCode 300100000
|
||||
@@ -101,6 +101,8 @@ dependencies {
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.11.0'
|
||||
implementation "com.anggrayudi:storage:1.5.5"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
|
||||
|
||||
// Glide
|
||||
ext.glide_version = '4.16.0'
|
||||
@@ -111,7 +113,7 @@ dependencies {
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.3.1'
|
||||
ext.exo_version = '1.5.0'
|
||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||
implementation "androidx.media3:media3-ui:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||
@@ -121,6 +123,8 @@ dependencies {
|
||||
// Media3 Casting
|
||||
implementation "androidx.media3:media3-cast:$exo_version"
|
||||
implementation "androidx.mediarouter:mediarouter:1.7.0"
|
||||
// Media3 extension
|
||||
implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.3"
|
||||
|
||||
// UI
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
@@ -131,7 +135,7 @@ dependencies {
|
||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1'
|
||||
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.3'
|
||||
|
||||
// Markwon
|
||||
ext.markwon_version = '4.6.2'
|
||||
@@ -157,7 +161,7 @@ dependencies {
|
||||
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
|
||||
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
|
||||
implementation 'com.squareup.logcat:logcat:0.1'
|
||||
implementation 'com.github.inorichi.injekt:injekt-core:65b0440'
|
||||
implementation 'uy.kohesive.injekt:injekt-core:1.16.+'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
||||
|
||||
@@ -1,9 +1,40 @@
|
||||
package ani.dantotsu.others
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
object AppUpdater {
|
||||
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
|
||||
//no-op
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
@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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.time.delay
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
@@ -69,7 +70,11 @@ object AppUpdater {
|
||||
)
|
||||
addView(
|
||||
TextView(activity).apply {
|
||||
val markWon = buildMarkwon(activity, false)
|
||||
val markWon = try { //slower phones can destroy the activity before this is done
|
||||
buildMarkwon(activity, false)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return@runOnUiThread
|
||||
}
|
||||
markWon.setMarkdown(this, md)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
@@ -115,7 +116,8 @@
|
||||
|
||||
<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/vnd.amazon.ebook" />
|
||||
<data android:mimeType="application/fb2+zip" />
|
||||
@@ -131,10 +133,11 @@
|
||||
</activity>
|
||||
<activity android:name=".others.calc.CalcActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity android:name=".settings.FAQActivity" />
|
||||
<activity android:name=".settings.ReaderSettingsActivity" />
|
||||
<activity android:name=".settings.AnilistSettingsActivity"/>
|
||||
<activity android:name=".settings.UserInterfaceSettingsActivity" />
|
||||
<activity android:name=".settings.PlayerSettingsActivity" />
|
||||
<activity android:name=".settings.ReaderSettingsActivity" />
|
||||
<activity android:name=".settings.FAQActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
@@ -155,7 +158,8 @@
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsExtensionsActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:windowSoftInputMode="adjustPan"/>
|
||||
<activity
|
||||
android:name=".settings.SettingsAddonActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
@@ -194,14 +198,15 @@
|
||||
android:label="Inbox Activity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".profile.activity.NotificationActivity"
|
||||
android:name=".profile.notification.NotificationActivity"
|
||||
android:label="Inbox Activity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".others.imagesearch.ImageSearchActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".util.MarkdownCreatorActivity"/>
|
||||
android:name=".util.ActivityMarkdownCreator"
|
||||
android:windowSoftInputMode="adjustResize|stateVisible" />
|
||||
<activity android:name=".parsers.ParserTestActivity" />
|
||||
<activity
|
||||
android:name=".media.ReviewActivity"
|
||||
@@ -370,25 +375,31 @@
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.Main" />
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:pathPattern=".*\\.ani" />
|
||||
<data android:pathPattern=".*\\.sani" />
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
<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
|
||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
|
||||
|
||||
@@ -105,6 +105,14 @@ class App : MultiDexApplication() {
|
||||
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 {
|
||||
animeExtensionManager = Injekt.get()
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
@@ -128,7 +136,9 @@ class App : MultiDexApplication() {
|
||||
downloadAddonManager = Injekt.get()
|
||||
torrentAddonManager.init()
|
||||
downloadAddonManager.init()
|
||||
CommentsAPI.fetchAuthToken(this@App)
|
||||
if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1) {
|
||||
CommentsAPI.fetchAuthToken(this@App)
|
||||
}
|
||||
|
||||
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
|
||||
val scheduler = TaskScheduler.create(this@App, useAlarmManager)
|
||||
|
||||
@@ -68,7 +68,6 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@@ -92,12 +91,12 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.BuildConfig.APPLICATION_ID
|
||||
import ani.dantotsu.connections.anilist.Genre
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.bakaupdates.MangaUpdates
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.ItemCountDownBinding
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
|
||||
import ani.dantotsu.others.AlignTagHandler
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.SpoilerPlugin
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
@@ -106,7 +105,6 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
|
||||
import ani.dantotsu.util.CountUpTimer
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
@@ -119,8 +117,8 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withC
|
||||
import com.bumptech.glide.load.resource.gif.GifDrawable
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.bumptech.glide.request.target.ViewTarget
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
@@ -154,6 +152,7 @@ import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
@@ -314,6 +313,7 @@ fun Activity.reloadActivity() {
|
||||
Refresh.all()
|
||||
finish()
|
||||
startActivity(Intent(this, this::class.java))
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
initActivity(this)
|
||||
}
|
||||
|
||||
@@ -854,7 +854,7 @@ fun savePrefsToDownloads(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
fun savePrefs(serialized: String, path: String, title: String, context: Context): File? {
|
||||
var file = File(path, "$title.ani")
|
||||
var counter = 1
|
||||
@@ -874,6 +874,7 @@ fun savePrefs(serialized: String, path: String, title: String, context: Context)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
fun savePrefs(
|
||||
serialized: String,
|
||||
path: String,
|
||||
@@ -920,7 +921,7 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||
intent.putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||
context.startActivity(Intent.createChooser(intent, "Share $title"))
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
|
||||
val imageFile = File(path, "$imageFileName.png")
|
||||
return try {
|
||||
@@ -1010,47 +1011,10 @@ fun countDown(media: Media, view: ViewGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
fun sinceWhen(media: Media, view: ViewGroup) {
|
||||
if (media.status != "RELEASING" && media.status != "HIATUS") return
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
MangaUpdates().search(media.mangaName(), media.startDate)?.let {
|
||||
val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
|
||||
val timeSince = (System.currentTimeMillis() -
|
||||
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
val v =
|
||||
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
|
||||
view.addView(v.root, 0)
|
||||
v.mediaCountdownText.text =
|
||||
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
|
||||
|
||||
object : CountUpTimer(86400000) {
|
||||
override fun onTick(second: Int) {
|
||||
val a = second + timeSince
|
||||
v.mediaCountdown.text = currActivity()?.getString(
|
||||
R.string.time_format,
|
||||
a / 86400,
|
||||
a % 86400 / 3600,
|
||||
a % 86400 % 3600 / 60,
|
||||
a % 86400 % 3600 % 60
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
// The legend will never die.
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun displayTimer(media: Media, view: ViewGroup) {
|
||||
when {
|
||||
media.anime != null -> countDown(media, view)
|
||||
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
|
||||
else -> {} // No timer yet
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1447,6 +1411,8 @@ fun openOrCopyAnilistLink(link: String) {
|
||||
} else {
|
||||
copyToClipboard(link, true)
|
||||
}
|
||||
} else if (getYoutubeId(link).isNotEmpty()) {
|
||||
openLinkInYouTube(link)
|
||||
} else {
|
||||
copyToClipboard(link, true)
|
||||
}
|
||||
@@ -1483,6 +1449,7 @@ fun buildMarkwon(
|
||||
TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a")
|
||||
)
|
||||
}
|
||||
plugin.addHandler(AlignTagHandler())
|
||||
})
|
||||
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
|
||||
|
||||
@@ -1500,7 +1467,6 @@ fun buildMarkwon(
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
@@ -1527,3 +1493,34 @@ fun buildMarkwon(
|
||||
.build()
|
||||
return markwon
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun getYoutubeId(url: String): String {
|
||||
val regex = """(?:youtube\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|(?:youtu\.be|youtube\.com)/)([^"&?/\s]{11})|youtube\.com/""".toRegex()
|
||||
val matchResult = regex.find(url)
|
||||
return matchResult?.groupValues?.getOrNull(1) ?: ""
|
||||
}
|
||||
|
||||
fun getLanguageCode(language: String): CharSequence {
|
||||
val locales = Locale.getAvailableLocales()
|
||||
for (locale in locales) {
|
||||
if (locale.displayLanguage.equals(language, ignoreCase = true)) {
|
||||
val lang: CharSequence = locale.language
|
||||
return lang
|
||||
|
||||
}
|
||||
}
|
||||
val out: CharSequence = "null"
|
||||
return out
|
||||
}
|
||||
|
||||
fun getLanguageName(language: String): String? {
|
||||
val locales = Locale.getAvailableLocales()
|
||||
for (locale in locales) {
|
||||
if (locale.language.equals(language, ignoreCase = true)) {
|
||||
return locale.displayLanguage
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package ani.dantotsu
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.Animatable
|
||||
@@ -51,7 +50,8 @@ import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.others.calc.CalcActivity
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.activity.FeedActivity
|
||||
import ani.dantotsu.profile.activity.NotificationActivity
|
||||
import ani.dantotsu.profile.notification.NotificationActivity
|
||||
import ani.dantotsu.settings.AddRepositoryBottomSheet
|
||||
import ani.dantotsu.settings.ExtensionsActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
|
||||
@@ -60,10 +60,11 @@ import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.util.AudioHelper
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
@@ -116,58 +117,8 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
val action = intent.action
|
||||
val type = intent.type
|
||||
if (Intent.ACTION_VIEW == action && type != null) {
|
||||
val uri: Uri? = intent.data
|
||||
try {
|
||||
if (uri == null) {
|
||||
throw Exception("Uri is null")
|
||||
}
|
||||
val jsonString =
|
||||
contentResolver.openInputStream(uri)?.readBytes()
|
||||
?: throw Exception("Error reading file")
|
||||
val name =
|
||||
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
|
||||
//.sani is encrypted, .ani is not
|
||||
if (name.endsWith(".sani")) {
|
||||
passwordAlertDialog { password ->
|
||||
if (password != null) {
|
||||
val salt = jsonString.copyOfRange(0, 16)
|
||||
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
||||
val decryptedJson = try {
|
||||
PreferenceKeystore.decryptWithPassword(
|
||||
password,
|
||||
encrypted,
|
||||
salt
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
toast("Incorrect password")
|
||||
return@passwordAlertDialog
|
||||
}
|
||||
if (PreferencePackager.unpack(decryptedJson)) {
|
||||
val intent = Intent(this, this.javaClass)
|
||||
this.finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
} else if (name.endsWith(".ani")) {
|
||||
val decryptedJson = jsonString.toString(Charsets.UTF_8)
|
||||
if (PreferencePackager.unpack(decryptedJson)) {
|
||||
val intent = Intent(this, this.javaClass)
|
||||
this.finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
toast("Invalid file type")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast("Error importing settings")
|
||||
}
|
||||
if (Intent.ACTION_VIEW == intent.action) {
|
||||
handleViewIntent(intent)
|
||||
}
|
||||
|
||||
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
@@ -287,7 +238,7 @@ class MainActivity : AppCompatActivity() {
|
||||
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
|
||||
) {
|
||||
snackString(R.string.extension_updates_available)
|
||||
?.setDuration(Snackbar.LENGTH_LONG)
|
||||
?.setDuration(Snackbar.LENGTH_SHORT)
|
||||
?.setAction(R.string.review) {
|
||||
startActivity(Intent(this, ExtensionsActivity::class.java))
|
||||
}
|
||||
@@ -365,7 +316,6 @@ class MainActivity : AppCompatActivity() {
|
||||
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
|
||||
Logger.log("MainActivity, onCreate: $activityId")
|
||||
val notificationIntent = Intent(this, NotificationActivity::class.java).apply {
|
||||
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
|
||||
putExtra("activityId", activityId)
|
||||
}
|
||||
launched = true
|
||||
@@ -455,7 +405,10 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (PrefManager.getVal(PrefName.OC)) {
|
||||
AudioHelper.run(this, R.raw.audio)
|
||||
PrefManager.setVal(PrefName.OC, false)
|
||||
}
|
||||
val torrentManager = Injekt.get<TorrentAddonManager>()
|
||||
fun startTorrent() {
|
||||
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
|
||||
@@ -490,39 +443,101 @@ class MainActivity : AppCompatActivity() {
|
||||
params.updateMargins(bottom = margin.toPx)
|
||||
}
|
||||
|
||||
private fun handleViewIntent(intent: Intent) {
|
||||
val uri: Uri? = intent.data
|
||||
try {
|
||||
if (uri == null) {
|
||||
throw Exception("Uri is null")
|
||||
}
|
||||
if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi") && uri.host == "add-repo") {
|
||||
val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import")
|
||||
val prefName = if (uri.scheme == "tachiyomi") {
|
||||
PrefName.MangaExtensionRepos
|
||||
} else {
|
||||
PrefName.AnimeExtensionRepos
|
||||
}
|
||||
val savedRepos: Set<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) {
|
||||
val password = CharArray(16).apply { fill('0') }
|
||||
|
||||
// Inflate the dialog layout
|
||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
|
||||
dialogView.userAgentTextBox.hint = "Password"
|
||||
dialogView.subtitle.visibility = View.VISIBLE
|
||||
dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file)
|
||||
|
||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Enter Password")
|
||||
.setView(dialogView.root)
|
||||
.setPositiveButton("OK", null)
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
|
||||
userAgentTextBox.hint = "Password"
|
||||
subtitle.visibility = View.VISIBLE
|
||||
subtitle.text = getString(R.string.enter_password_to_decrypt_file)
|
||||
}
|
||||
customAlertDialog().apply {
|
||||
setTitle("Enter Password")
|
||||
setCustomView(dialogView.root)
|
||||
setPosButton(R.string.yes) {
|
||||
val editText = dialogView.userAgentTextBox
|
||||
if (editText.text?.isNotBlank() == true) {
|
||||
editText.text?.toString()?.trim()?.toCharArray(password)
|
||||
callback(password)
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
setNegButton(R.string.cancel) {
|
||||
password.fill('0')
|
||||
dialog.dismiss()
|
||||
callback(null)
|
||||
}
|
||||
.create()
|
||||
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
|
||||
// Override the positive button here
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
val editText = dialog.findViewById<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")
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
|
||||
object Anilist {
|
||||
val query: AnilistQueries = AnilistQueries()
|
||||
@@ -22,7 +24,7 @@ object Anilist {
|
||||
|
||||
var token: String? = null
|
||||
var username: String? = null
|
||||
var adult: Boolean = false
|
||||
|
||||
var userid: Int? = null
|
||||
var avatar: String? = null
|
||||
var bg: String? = null
|
||||
@@ -36,6 +38,17 @@ object Anilist {
|
||||
var rateLimitReset: Long = 0
|
||||
|
||||
var initialized = false
|
||||
var adult: Boolean = false
|
||||
var titleLanguage: String? = null
|
||||
var staffNameLanguage: String? = null
|
||||
var airingNotifications: Boolean = false
|
||||
var restrictMessagesToFollowing: Boolean = false
|
||||
var scoreFormat: String? = null
|
||||
var rowOrder: String? = null
|
||||
var activityMergeTime: Int? = null
|
||||
var timezone: String? = null
|
||||
var animeCustomLists: List<String>? = null
|
||||
var mangaCustomLists: List<String>? = null
|
||||
|
||||
val sortBy = listOf(
|
||||
"SCORE_DESC",
|
||||
@@ -96,6 +109,86 @@ object Anilist {
|
||||
"Original Creator", "Story & Art", "Story"
|
||||
)
|
||||
|
||||
val timeZone = listOf(
|
||||
"(GMT-11:00) Pago Pago",
|
||||
"(GMT-10:00) Hawaii Time",
|
||||
"(GMT-09:00) Alaska Time",
|
||||
"(GMT-08:00) Pacific Time",
|
||||
"(GMT-07:00) Mountain Time",
|
||||
"(GMT-06:00) Central Time",
|
||||
"(GMT-05:00) Eastern Time",
|
||||
"(GMT-04:00) Atlantic Time - Halifax",
|
||||
"(GMT-03:00) Sao Paulo",
|
||||
"(GMT-02:00) Mid-Atlantic",
|
||||
"(GMT-01:00) Azores",
|
||||
"(GMT+00:00) London",
|
||||
"(GMT+01:00) Berlin",
|
||||
"(GMT+02:00) Helsinki",
|
||||
"(GMT+03:00) Istanbul",
|
||||
"(GMT+04:00) Dubai",
|
||||
"(GMT+04:30) Kabul",
|
||||
"(GMT+05:00) Maldives",
|
||||
"(GMT+05:30) India Standard Time",
|
||||
"(GMT+05:45) Kathmandu",
|
||||
"(GMT+06:00) Dhaka",
|
||||
"(GMT+06:30) Cocos",
|
||||
"(GMT+07:00) Bangkok",
|
||||
"(GMT+08:00) Hong Kong",
|
||||
"(GMT+08:30) Pyongyang",
|
||||
"(GMT+09:00) Tokyo",
|
||||
"(GMT+09:30) Central Time - Darwin",
|
||||
"(GMT+10:00) Eastern Time - Brisbane",
|
||||
"(GMT+10:30) Central Time - Adelaide",
|
||||
"(GMT+11:00) Eastern Time - Melbourne, Sydney",
|
||||
"(GMT+12:00) Nauru",
|
||||
"(GMT+13:00) Auckland",
|
||||
"(GMT+14:00) Kiritimati",
|
||||
)
|
||||
|
||||
val titleLang = listOf(
|
||||
"English (Attack on Titan)",
|
||||
"Romaji (Shingeki no Kyojin)",
|
||||
"Native (進撃の巨人)"
|
||||
)
|
||||
|
||||
val staffNameLang = listOf(
|
||||
"Romaji, Western Order (Killua Zoldyck)",
|
||||
"Romaji (Zoldyck Killua)",
|
||||
"Native (キルア=ゾルディック)"
|
||||
)
|
||||
|
||||
val scoreFormats = listOf(
|
||||
"100 Point (55/100)",
|
||||
"10 Point Decimal (5.5/10)",
|
||||
"10 Point (5/10)",
|
||||
"5 Star (3/5)",
|
||||
"3 Point Smiley :)"
|
||||
)
|
||||
|
||||
val rowOrderMap = mapOf(
|
||||
"Score" to "score",
|
||||
"Title" to "title",
|
||||
"Last Updated" to "updatedAt",
|
||||
"Last Added" to "id"
|
||||
)
|
||||
|
||||
val activityMergeTimeMap = mapOf(
|
||||
"Never" to 0,
|
||||
"30 mins" to 30,
|
||||
"69 mins" to 69,
|
||||
"1 hour" to 60,
|
||||
"2 hours" to 120,
|
||||
"3 hours" to 180,
|
||||
"6 hours" to 360,
|
||||
"12 hours" to 720,
|
||||
"1 day" to 1440,
|
||||
"2 days" to 2880,
|
||||
"3 days" to 4320,
|
||||
"1 week" to 10080,
|
||||
"2 weeks" to 20160,
|
||||
"Always" to 29160
|
||||
)
|
||||
|
||||
private val cal: Calendar = Calendar.getInstance()
|
||||
private val currentYear = cal.get(Calendar.YEAR)
|
||||
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
|
||||
@@ -106,6 +199,33 @@ object Anilist {
|
||||
else -> 0
|
||||
}
|
||||
|
||||
fun getDisplayTimezone(apiTimezone: String, context: Context): String {
|
||||
val noTimezone = context.getString(R.string.selected_no_time_zone)
|
||||
val parts = apiTimezone.split(":")
|
||||
if (parts.size != 2) return noTimezone
|
||||
|
||||
val hours = parts[0].toIntOrNull() ?: 0
|
||||
val minutes = parts[1].toIntOrNull() ?: 0
|
||||
val sign = if (hours >= 0) "+" else "-"
|
||||
val formattedHours = String.format(Locale.US, "%02d", abs(hours))
|
||||
val formattedMinutes = String.format(Locale.US, "%02d", minutes)
|
||||
|
||||
val searchString = "(GMT$sign$formattedHours:$formattedMinutes)"
|
||||
return timeZone.find { it.contains(searchString) } ?: noTimezone
|
||||
}
|
||||
|
||||
fun getApiTimezone(displayTimezone: String): String {
|
||||
val regex = """\(GMT([+-])(\d{2}):(\d{2})\)""".toRegex()
|
||||
val matchResult = regex.find(displayTimezone)
|
||||
return if (matchResult != null) {
|
||||
val (sign, hours, minutes) = matchResult.destructured
|
||||
val formattedSign = if (sign == "+") "" else "-"
|
||||
"$formattedSign$hours:$minutes"
|
||||
} else {
|
||||
"00:00"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSeason(next: Boolean): Pair<String, Int> {
|
||||
var newSeason = if (next) currentSeason + 1 else currentSeason - 1
|
||||
var newYear = currentYear
|
||||
@@ -191,6 +311,7 @@ object Anilist {
|
||||
)
|
||||
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
|
||||
Logger.log("Remaining requests: $remaining")
|
||||
println("Remaining requests: $remaining")
|
||||
if (json.code == 429) {
|
||||
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
|
||||
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
|
||||
|
||||
@@ -3,16 +3,99 @@ package ani.dantotsu.connections.anilist
|
||||
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.connections.anilist.api.ToggleLike
|
||||
import ani.dantotsu.currContext
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
class AnilistMutations {
|
||||
|
||||
suspend fun updateSettings(
|
||||
timezone: String? = null,
|
||||
titleLanguage: String? = null,
|
||||
staffNameLanguage: String? = null,
|
||||
activityMergeTime: Int? = null,
|
||||
airingNotifications: Boolean? = null,
|
||||
displayAdultContent: Boolean? = null,
|
||||
restrictMessagesToFollowing: Boolean? = null,
|
||||
scoreFormat: String? = null,
|
||||
rowOrder: String? = null,
|
||||
) {
|
||||
val query = """
|
||||
mutation (
|
||||
${"$"}timezone: String,
|
||||
${"$"}titleLanguage: UserTitleLanguage,
|
||||
${"$"}staffNameLanguage: UserStaffNameLanguage,
|
||||
${"$"}activityMergeTime: Int,
|
||||
${"$"}airingNotifications: Boolean,
|
||||
${"$"}displayAdultContent: Boolean,
|
||||
${"$"}restrictMessagesToFollowing: Boolean,
|
||||
${"$"}scoreFormat: ScoreFormat,
|
||||
${"$"}rowOrder: String
|
||||
) {
|
||||
UpdateUser(
|
||||
timezone: ${"$"}timezone,
|
||||
titleLanguage: ${"$"}titleLanguage,
|
||||
staffNameLanguage: ${"$"}staffNameLanguage,
|
||||
activityMergeTime: ${"$"}activityMergeTime,
|
||||
airingNotifications: ${"$"}airingNotifications,
|
||||
displayAdultContent: ${"$"}displayAdultContent,
|
||||
restrictMessagesToFollowing: ${"$"}restrictMessagesToFollowing,
|
||||
scoreFormat: ${"$"}scoreFormat,
|
||||
rowOrder: ${"$"}rowOrder,
|
||||
) {
|
||||
id
|
||||
options {
|
||||
timezone
|
||||
titleLanguage
|
||||
staffNameLanguage
|
||||
activityMergeTime
|
||||
airingNotifications
|
||||
displayAdultContent
|
||||
restrictMessagesToFollowing
|
||||
}
|
||||
mediaListOptions {
|
||||
scoreFormat
|
||||
rowOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val variables = """
|
||||
{
|
||||
${timezone?.let { """"timezone":"$it"""" } ?: ""}
|
||||
${titleLanguage?.let { """"titleLanguage":"$it"""" } ?: ""}
|
||||
${staffNameLanguage?.let { """"staffNameLanguage":"$it"""" } ?: ""}
|
||||
${activityMergeTime?.let { """"activityMergeTime":$it""" } ?: ""}
|
||||
${airingNotifications?.let { """"airingNotifications":$it""" } ?: ""}
|
||||
${displayAdultContent?.let { """"displayAdultContent":$it""" } ?: ""}
|
||||
${restrictMessagesToFollowing?.let { """"restrictMessagesToFollowing":$it""" } ?: ""}
|
||||
${scoreFormat?.let { """"scoreFormat":"$it"""" } ?: ""}
|
||||
${rowOrder?.let { """"rowOrder":"$it"""" } ?: ""}
|
||||
}
|
||||
""".trimIndent().replace("\n", "").replace(""" """, "").replace(",}", "}")
|
||||
|
||||
executeQuery<JsonObject>(query, variables)
|
||||
}
|
||||
|
||||
suspend fun toggleFav(anime: Boolean = true, id: Int) {
|
||||
val query =
|
||||
"""mutation (${"$"}animeId: Int,${"$"}mangaId:Int) { ToggleFavourite(animeId:${"$"}animeId,mangaId:${"$"}mangaId){ anime { edges { id } } manga { edges { id } } } }"""
|
||||
val query = """
|
||||
mutation (${"$"}animeId: Int, ${"$"}mangaId: Int) {
|
||||
ToggleFavourite(animeId: ${"$"}animeId, mangaId: ${"$"}mangaId) {
|
||||
anime {
|
||||
edges {
|
||||
id
|
||||
}
|
||||
}
|
||||
manga {
|
||||
edges {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val variables = if (anime) """{"animeId":"$id"}""" else """{"mangaId":"$id"}"""
|
||||
executeQuery<JsonObject>(query, variables)
|
||||
}
|
||||
@@ -25,7 +108,17 @@ class AnilistMutations {
|
||||
FavType.STAFF -> "staffId"
|
||||
FavType.STUDIO -> "studioId"
|
||||
}
|
||||
val query = """mutation{ToggleFavourite($filter:$id){anime{pageInfo{total}}}}"""
|
||||
val query = """
|
||||
mutation {
|
||||
ToggleFavourite($filter: $id) {
|
||||
anime {
|
||||
pageInfo {
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = executeQuery<JsonObject>(query)
|
||||
return result?.get("errors") == null && result != null
|
||||
}
|
||||
@@ -34,6 +127,51 @@ class AnilistMutations {
|
||||
ANIME, MANGA, CHARACTER, STAFF, STUDIO
|
||||
}
|
||||
|
||||
suspend fun deleteCustomList(name: String, type: String): Boolean {
|
||||
val query = """
|
||||
mutation (${"$"}name: String, ${"$"}type: MediaType) {
|
||||
DeleteCustomList(customList: ${"$"}name, type: ${"$"}type) {
|
||||
deleted
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val variables = """
|
||||
{
|
||||
"name": "$name",
|
||||
"type": "$type"
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = executeQuery<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(
|
||||
mediaID: Int,
|
||||
progress: Int? = null,
|
||||
@@ -46,14 +184,45 @@ class AnilistMutations {
|
||||
completedAt: FuzzyDate? = null,
|
||||
customList: List<String>? = null
|
||||
) {
|
||||
|
||||
val query = """
|
||||
mutation ( ${"$"}mediaID: Int, ${"$"}progress: Int,${"$"}private:Boolean,${"$"}repeat: Int, ${"$"}notes: String, ${"$"}customLists: [String], ${"$"}scoreRaw:Int, ${"$"}status:MediaListStatus, ${"$"}start:FuzzyDateInput${if (startedAt != null) "=" + startedAt.toVariableString() else ""}, ${"$"}completed:FuzzyDateInput${if (completedAt != null) "=" + completedAt.toVariableString() else ""} ) {
|
||||
SaveMediaListEntry( mediaId: ${"$"}mediaID, progress: ${"$"}progress, repeat: ${"$"}repeat, notes: ${"$"}notes, private: ${"$"}private, scoreRaw: ${"$"}scoreRaw, status:${"$"}status, startedAt: ${"$"}start, completedAt: ${"$"}completed , customLists: ${"$"}customLists ) {
|
||||
score(format:POINT_10_DECIMAL) startedAt{year month day} completedAt{year month day}
|
||||
mutation (
|
||||
${"$"}mediaID: Int,
|
||||
${"$"}progress: Int,
|
||||
${"$"}private: Boolean,
|
||||
${"$"}repeat: Int,
|
||||
${"$"}notes: String,
|
||||
${"$"}customLists: [String],
|
||||
${"$"}scoreRaw: Int,
|
||||
${"$"}status: MediaListStatus,
|
||||
${"$"}start: FuzzyDateInput${if (startedAt != null) "=" + startedAt.toVariableString() else ""},
|
||||
${"$"}completed: FuzzyDateInput${if (completedAt != null) "=" + completedAt.toVariableString() else ""}
|
||||
) {
|
||||
SaveMediaListEntry(
|
||||
mediaId: ${"$"}mediaID,
|
||||
progress: ${"$"}progress,
|
||||
repeat: ${"$"}repeat,
|
||||
notes: ${"$"}notes,
|
||||
private: ${"$"}private,
|
||||
scoreRaw: ${"$"}scoreRaw,
|
||||
status: ${"$"}status,
|
||||
startedAt: ${"$"}start,
|
||||
completedAt: ${"$"}completed,
|
||||
customLists: ${"$"}customLists
|
||||
) {
|
||||
score(format: POINT_10_DECIMAL)
|
||||
startedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
completedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
}
|
||||
}
|
||||
""".replace("\n", "").replace(""" """, "")
|
||||
""".trimIndent()
|
||||
|
||||
val variables = """{"mediaID":$mediaID
|
||||
${if (private != null) ""","private":$private""" else ""}
|
||||
@@ -69,43 +238,168 @@ class AnilistMutations {
|
||||
}
|
||||
|
||||
suspend fun deleteList(listId: Int) {
|
||||
val query = "mutation(${"$"}id:Int){DeleteMediaListEntry(id:${"$"}id){deleted}}"
|
||||
val query = """
|
||||
mutation(${"$"}id: Int) {
|
||||
DeleteMediaListEntry(id: ${"$"}id) {
|
||||
deleted
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val variables = """{"id":"$listId"}"""
|
||||
executeQuery<JsonObject>(query, variables)
|
||||
}
|
||||
|
||||
|
||||
suspend fun rateReview(reviewId: Int, rating: String): Query.RateReviewResponse? {
|
||||
val query = "mutation{RateReview(reviewId:$reviewId,rating:$rating){id mediaId mediaType summary body(asHtml:true)rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}"
|
||||
val query = """
|
||||
mutation {
|
||||
RateReview(reviewId: $reviewId, rating: $rating) {
|
||||
id
|
||||
mediaId
|
||||
mediaType
|
||||
summary
|
||||
body(asHtml: true)
|
||||
rating
|
||||
ratingAmount
|
||||
userRating
|
||||
score
|
||||
private
|
||||
siteUrl
|
||||
createdAt
|
||||
updatedAt
|
||||
user {
|
||||
id
|
||||
name
|
||||
bannerImage
|
||||
avatar {
|
||||
medium
|
||||
large
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
return executeQuery<Query.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 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 errors = result?.get("errors")
|
||||
return errors?.toString()
|
||||
?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
|
||||
return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
|
||||
}
|
||||
|
||||
suspend fun postMessage(userId: Int, text: String, edit: Int? = null, isPrivate: Boolean = false): String {
|
||||
val encodedText = text.replace("", "").stringSanitizer()
|
||||
val query = """
|
||||
mutation {
|
||||
SaveMessageActivity(
|
||||
${if (edit != null) "id: $edit," else ""}
|
||||
recipientId: $userId,
|
||||
message: $encodedText,
|
||||
private: $isPrivate
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = executeQuery<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 {
|
||||
val encodedSummary = summary.stringSanitizer()
|
||||
val encodedBody = body.stringSanitizer()
|
||||
val query = "mutation{SaveReview(mediaId:$mediaId,summary:$encodedSummary,body:$encodedBody,score:$score){siteUrl}}"
|
||||
val query = """
|
||||
mutation {
|
||||
SaveReview(
|
||||
mediaId: $mediaId,
|
||||
summary: $encodedSummary,
|
||||
body: $encodedBody,
|
||||
score: $score
|
||||
) {
|
||||
siteUrl
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = executeQuery<JsonObject>(query)
|
||||
val errors = result?.get("errors")
|
||||
return errors?.toString()
|
||||
?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
|
||||
return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
|
||||
}
|
||||
|
||||
suspend fun postReply(activityId: Int, text: String): String {
|
||||
val encodedText = text.stringSanitizer()
|
||||
val query = "mutation{SaveActivityReply(activityId:$activityId,text:$encodedText){id}}"
|
||||
suspend fun deleteActivityReply(activityId: Int): Boolean {
|
||||
val query = """
|
||||
mutation {
|
||||
DeleteActivityReply(id: $activityId) {
|
||||
deleted
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = executeQuery<JsonObject>(query)
|
||||
val errors = result?.get("errors")
|
||||
return errors?.toString()
|
||||
?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success")
|
||||
return errors == null
|
||||
}
|
||||
|
||||
suspend fun deleteActivity(activityId: Int): Boolean {
|
||||
val query = """
|
||||
mutation {
|
||||
DeleteActivity(id: $activityId) {
|
||||
deleted
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = executeQuery<JsonObject>(query)
|
||||
val errors = result?.get("errors")
|
||||
return errors == null
|
||||
}
|
||||
|
||||
private fun String.stringSanitizer(): String {
|
||||
|
||||
@@ -8,11 +8,12 @@ import ani.dantotsu.connections.anilist.Anilist.authorRoles
|
||||
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
||||
import ani.dantotsu.connections.anilist.api.FeedResponse
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.anilist.api.MediaEdge
|
||||
import ani.dantotsu.connections.anilist.api.MediaList
|
||||
import ani.dantotsu.connections.anilist.api.NotificationResponse
|
||||
import ani.dantotsu.connections.anilist.api.Page
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.connections.anilist.api.ReplyResponse
|
||||
import ani.dantotsu.connections.anilist.api.ToggleLike
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.logError
|
||||
@@ -27,6 +28,7 @@ import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
@@ -41,8 +43,8 @@ class AnilistQueries {
|
||||
suspend fun getUserData(): Boolean {
|
||||
val response: Query.Viewer?
|
||||
measureTimeMillis {
|
||||
response =
|
||||
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}unreadNotificationCount}}""")
|
||||
response = executeQuery(
|
||||
"""{Viewer{name options{timezone titleLanguage staffNameLanguage activityMergeTime airingNotifications displayAdultContent restrictMessagesToFollowing} avatar{medium} bannerImage id mediaListOptions{scoreFormat rowOrder animeList{customLists} mangaList{customLists}} statistics{anime{episodesWatched} manga{chaptersRead}} unreadNotificationCount}}""")
|
||||
}.also { println("time : $it") }
|
||||
val user = response?.data?.user ?: return false
|
||||
|
||||
@@ -59,6 +61,27 @@ class AnilistQueries {
|
||||
val unread = PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications)
|
||||
Anilist.unreadNotificationCount += unread
|
||||
Anilist.initialized = true
|
||||
|
||||
user.options?.let {
|
||||
Anilist.titleLanguage = it.titleLanguage.toString()
|
||||
Anilist.staffNameLanguage = it.staffNameLanguage.toString()
|
||||
Anilist.airingNotifications = it.airingNotifications ?: false
|
||||
Anilist.restrictMessagesToFollowing = it.restrictMessagesToFollowing ?: false
|
||||
Anilist.timezone = it.timezone
|
||||
Anilist.activityMergeTime = it.activityMergeTime
|
||||
}
|
||||
user.mediaListOptions?.let {
|
||||
Anilist.scoreFormat = it.scoreFormat.toString()
|
||||
Anilist.rowOrder = it.rowOrder
|
||||
|
||||
it.animeList?.let { animeList ->
|
||||
Anilist.animeCustomLists = animeList.customLists
|
||||
}
|
||||
|
||||
it.mangaList?.let { mangaList ->
|
||||
Anilist.mangaCustomLists = mangaList.customLists
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -75,7 +98,7 @@ class AnilistQueries {
|
||||
media.cameFromContinue = false
|
||||
|
||||
val query =
|
||||
"""{Media(id:${media.id}){id favourites popularity episodes chapters mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}reviews(perPage:3, sort:SCORE_DESC){nodes{id mediaId mediaType summary body(asHtml:true) rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
|
||||
"""{Media(id:${media.id}){id favourites popularity episodes chapters streamingEpisodes {title thumbnail url site} mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}reviews(perPage:3, sort:SCORE_DESC){nodes{id mediaId mediaType summary body(asHtml:true) rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
|
||||
runBlocking {
|
||||
val anilist = async {
|
||||
var response = executeQuery<Query.Media>(query, force = true)
|
||||
@@ -90,7 +113,7 @@ class AnilistQueries {
|
||||
media.popularity = fetchedMedia.popularity
|
||||
media.startDate = fetchedMedia.startDate
|
||||
media.endDate = fetchedMedia.endDate
|
||||
|
||||
media.streamingEpisodes = fetchedMedia.streamingEpisodes
|
||||
if (fetchedMedia.genres != null) {
|
||||
media.genres = arrayListOf()
|
||||
fetchedMedia.genres?.forEach { i ->
|
||||
@@ -211,7 +234,7 @@ class AnilistQueries {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fetchedMedia.reviews?.nodes != null){
|
||||
if (fetchedMedia.reviews?.nodes != null) {
|
||||
media.review = fetchedMedia.reviews!!.nodes as ArrayList<Query.Review>
|
||||
}
|
||||
if (user?.mediaList?.isNotEmpty() == true) {
|
||||
@@ -376,9 +399,8 @@ class AnilistQueries {
|
||||
}
|
||||
return media
|
||||
}
|
||||
private fun continueMediaQuery(type: String, status: String): String {
|
||||
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """
|
||||
}
|
||||
|
||||
|
||||
|
||||
private suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
|
||||
var hasNextPage = true
|
||||
@@ -404,10 +426,68 @@ class AnilistQueries {
|
||||
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 {
|
||||
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
|
||||
}
|
||||
|
||||
private fun recommendationQuery(): String {
|
||||
return """ Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """
|
||||
}
|
||||
@@ -415,250 +495,183 @@ class AnilistQueries {
|
||||
private fun recommendationPlannedQuery(type: String): String {
|
||||
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${if (type == "ANIME") ", sort: MEDIA_POPULARITY_DESC" else ""} ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
|
||||
}
|
||||
|
||||
suspend fun initHomePage(): Map<String, ArrayList<*>> {
|
||||
private fun continueMediaQuery(type: String, status: String): String {
|
||||
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """
|
||||
}
|
||||
suspend fun initHomePage(): Map<String, ArrayList<Media>> {
|
||||
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
|
||||
val hidePrivate = PrefManager.getVal<Boolean>(PrefName.HidePrivate)
|
||||
val removedMedia = ArrayList<Media>()
|
||||
val toShow: List<Boolean> =
|
||||
PrefManager.getVal(PrefName.HomeLayout) // anime continue, anime fav, anime planned, manga continue, manga fav, manga planned, recommendations
|
||||
var query = """{"""
|
||||
if (toShow.getOrNull(0) == true) query += """currentAnime: ${
|
||||
continueMediaQuery(
|
||||
"ANIME",
|
||||
"CURRENT"
|
||||
)
|
||||
}, repeatingAnime: ${continueMediaQuery("ANIME", "REPEATING")}"""
|
||||
if (toShow.getOrNull(1) == true) query += """favoriteAnime: ${favMediaQuery(true, 1)}"""
|
||||
if (toShow.getOrNull(2) == true) query += """plannedAnime: ${
|
||||
continueMediaQuery(
|
||||
"ANIME",
|
||||
"PLANNING"
|
||||
)
|
||||
}"""
|
||||
if (toShow.getOrNull(3) == true) query += """currentManga: ${
|
||||
continueMediaQuery(
|
||||
"MANGA",
|
||||
"CURRENT"
|
||||
)
|
||||
}, repeatingManga: ${continueMediaQuery("MANGA", "REPEATING")}"""
|
||||
if (toShow.getOrNull(4) == true) query += """favoriteManga: ${favMediaQuery(false, 1)}"""
|
||||
if (toShow.getOrNull(5) == true) query += """plannedManga: ${
|
||||
continueMediaQuery(
|
||||
"MANGA",
|
||||
"PLANNING"
|
||||
)
|
||||
}"""
|
||||
if (toShow.getOrNull(6) == true) query += """recommendationQuery: ${recommendationQuery()}, recommendationPlannedQueryAnime: ${
|
||||
recommendationPlannedQuery(
|
||||
"ANIME"
|
||||
)
|
||||
}, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
|
||||
if (toShow.getOrNull(7) == true) query += "Page1:${status(1)}Page2:${status(2)}"
|
||||
query += """}""".trimEnd(',')
|
||||
PrefManager.getVal(PrefName.HomeLayout) // list of booleans for what to show
|
||||
|
||||
val queries = mutableListOf<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 returnMap = mutableMapOf<String, ArrayList<*>>()
|
||||
fun current(type: String) {
|
||||
val returnMap = mutableMapOf<String, ArrayList<Media>>()
|
||||
|
||||
fun processMedia(
|
||||
type: String,
|
||||
currentMedia: List<MediaList>?,
|
||||
repeatingMedia: List<MediaList>?
|
||||
) {
|
||||
val subMap = mutableMapOf<Int, Media>()
|
||||
val returnArray = arrayListOf<Media>()
|
||||
val current =
|
||||
if (type == "Anime") response?.data?.currentAnime else response?.data?.currentManga
|
||||
val repeating =
|
||||
if (type == "Anime") response?.data?.repeatingAnime else response?.data?.repeatingManga
|
||||
current?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
if (m.id !in removeList) {
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
} else {
|
||||
removedMedia.add(m)
|
||||
}
|
||||
|
||||
(currentMedia ?: emptyList()).forEach { entry ->
|
||||
val media = Media(entry)
|
||||
if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) {
|
||||
media.cameFromContinue = true
|
||||
subMap[media.id] = media
|
||||
} else {
|
||||
removedMedia.add(media)
|
||||
}
|
||||
}
|
||||
|
||||
repeating?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
if (m.id !in removeList) {
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
} else {
|
||||
removedMedia.add(m)
|
||||
}
|
||||
(repeatingMedia ?: emptyList()).forEach { entry ->
|
||||
val media = Media(entry)
|
||||
if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) {
|
||||
media.cameFromContinue = true
|
||||
subMap[media.id] = media
|
||||
} else {
|
||||
removedMedia.add(media)
|
||||
}
|
||||
}
|
||||
if (type != "Anime") {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = PrefManager.getNullableCustomVal(
|
||||
"continue${type}List",
|
||||
listOf<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)
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
fun planned(type: String) {
|
||||
val subMap = mutableMapOf<Int, Media>()
|
||||
val returnArray = arrayListOf<Media>()
|
||||
val current =
|
||||
if (type == "Anime") response?.data?.plannedAnime else response?.data?.plannedManga
|
||||
current?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
if (m.id !in removeList) {
|
||||
m.cameFromContinue = true
|
||||
subMap[m.id] = m
|
||||
} else {
|
||||
removedMedia.add(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val list = PrefManager.getNullableCustomVal(
|
||||
"continueAnimeList",
|
||||
listOf<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
|
||||
}
|
||||
if (toShow.getOrNull(0) == true) processMedia(
|
||||
"Anime",
|
||||
response?.data?.currentAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
|
||||
response?.data?.repeatingAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed()
|
||||
)
|
||||
if (toShow.getOrNull(2) == true) processMedia(
|
||||
"AnimePlanned",
|
||||
response?.data?.plannedAnime?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
|
||||
null
|
||||
)
|
||||
if (toShow.getOrNull(3) == true) processMedia(
|
||||
"Manga",
|
||||
response?.data?.currentManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
|
||||
response?.data?.repeatingManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed()
|
||||
)
|
||||
if (toShow.getOrNull(5) == true) processMedia(
|
||||
"MangaPlanned",
|
||||
response?.data?.plannedManga?.lists?.flatMap { it.entries ?: emptyList() }?.reversed(),
|
||||
null
|
||||
)
|
||||
|
||||
fun favorite(type: String) {
|
||||
val favourites =
|
||||
if (type == "Anime") response?.data?.favoriteAnime?.favourites else response?.data?.favoriteManga?.favourites
|
||||
val apiMediaList = if (type == "Anime") favourites?.anime else favourites?.manga
|
||||
fun processFavorites(type: String, favorites: List<MediaEdge>?) {
|
||||
val returnArray = arrayListOf<Media>()
|
||||
apiMediaList?.edges?.forEach {
|
||||
it.node?.let { i ->
|
||||
val m = Media(i).apply { isFav = true }
|
||||
if (m.id !in removeList) {
|
||||
returnArray.add(m)
|
||||
favorites?.forEach { edge ->
|
||||
edge.node?.let {
|
||||
val media = Media(it).apply { isFav = true }
|
||||
if (media.id !in removeList && (!hidePrivate || !media.isListPrivate)) {
|
||||
returnArray.add(media)
|
||||
} else {
|
||||
removedMedia.add(m)
|
||||
removedMedia.add(media)
|
||||
}
|
||||
}
|
||||
}
|
||||
returnMap["favorite$type"] = returnArray
|
||||
}
|
||||
|
||||
if (toShow.getOrNull(0) == true) {
|
||||
current("Anime")
|
||||
}
|
||||
if (toShow.getOrNull(1) == true) {
|
||||
favorite("Anime")
|
||||
}
|
||||
if (toShow.getOrNull(2) == true) {
|
||||
planned("Anime")
|
||||
}
|
||||
if (toShow.getOrNull(3) == true) {
|
||||
current("Manga")
|
||||
}
|
||||
if (toShow.getOrNull(4) == true) {
|
||||
favorite("Manga")
|
||||
}
|
||||
if (toShow.getOrNull(5) == true) {
|
||||
planned("Manga")
|
||||
}
|
||||
if (toShow.getOrNull(1) == true) processFavorites(
|
||||
"Anime",
|
||||
response?.data?.favoriteAnime?.favourites?.anime?.edges
|
||||
)
|
||||
if (toShow.getOrNull(4) == true) processFavorites(
|
||||
"Manga",
|
||||
response?.data?.favoriteManga?.favourites?.manga?.edges
|
||||
)
|
||||
|
||||
if (toShow.getOrNull(6) == true) {
|
||||
val subMap = mutableMapOf<Int, Media>()
|
||||
response?.data?.recommendationQuery?.apply {
|
||||
recommendations?.onEach {
|
||||
val json = it.mediaRecommendation
|
||||
if (json != null) {
|
||||
val m = Media(json)
|
||||
m.relation = json.type?.toString()
|
||||
subMap[m.id] = m
|
||||
}
|
||||
response?.data?.recommendationQuery?.recommendations?.forEach {
|
||||
it.mediaRecommendation?.let { json ->
|
||||
val media = Media(json)
|
||||
media.relation = json.type?.toString()
|
||||
subMap[media.id] = media
|
||||
}
|
||||
}
|
||||
response?.data?.recommendationPlannedQueryAnime?.apply {
|
||||
lists?.forEach { li ->
|
||||
li.entries?.forEach {
|
||||
val m = Media(it)
|
||||
if (m.status == "RELEASING" || m.status == "FINISHED") {
|
||||
m.relation = it.media?.type?.toString()
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
response?.data?.recommendationPlannedQueryAnime?.lists?.flatMap {
|
||||
it.entries ?: emptyList()
|
||||
}?.forEach {
|
||||
val media = Media(it)
|
||||
if (media.status in listOf("RELEASING", "FINISHED")) {
|
||||
media.relation = it.media?.type?.toString()
|
||||
subMap[media.id] = media
|
||||
}
|
||||
}
|
||||
response?.data?.recommendationPlannedQueryManga?.apply {
|
||||
lists?.forEach { li ->
|
||||
li.entries?.forEach {
|
||||
val m = Media(it)
|
||||
if (m.status == "RELEASING" || m.status == "FINISHED") {
|
||||
m.relation = it.media?.type?.toString()
|
||||
subMap[m.id] = m
|
||||
}
|
||||
}
|
||||
response?.data?.recommendationPlannedQueryManga?.lists?.flatMap {
|
||||
it.entries ?: emptyList()
|
||||
}?.forEach {
|
||||
val media = Media(it)
|
||||
if (media.status in listOf("RELEASING", "FINISHED")) {
|
||||
media.relation = it.media?.type?.toString()
|
||||
subMap[media.id] = media
|
||||
}
|
||||
}
|
||||
val list = ArrayList(subMap.values.toList())
|
||||
list.sortByDescending { it.meanScore }
|
||||
val list = ArrayList(subMap.values).apply { sortByDescending { it.meanScore } }
|
||||
returnMap["recommendations"] = list
|
||||
}
|
||||
if (toShow.getOrNull(7) == true) {
|
||||
val list = mutableListOf<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) ->
|
||||
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>
|
||||
}
|
||||
returnMap["hidden"] = removedMedia.distinctBy { it.id }.toCollection(arrayListOf())
|
||||
return returnMap
|
||||
}
|
||||
|
||||
@@ -1034,153 +1047,105 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
|
||||
return null
|
||||
}
|
||||
|
||||
private val onListAnime =
|
||||
(if (PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace(
|
||||
"\"",
|
||||
""
|
||||
)
|
||||
private val isAdult =
|
||||
(if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "")
|
||||
private fun mediaList(media1: Page?): ArrayList<Media> {
|
||||
val combinedList = arrayListOf<Media>()
|
||||
media1?.media?.mapTo(combinedList) { Media(it) }
|
||||
return combinedList
|
||||
}
|
||||
|
||||
private fun getPreference(pref: PrefName): Boolean = PrefManager.getVal(pref)
|
||||
|
||||
private fun buildQueryString(
|
||||
sort: String,
|
||||
type: String,
|
||||
format: String? = null,
|
||||
country: String? = null
|
||||
): String {
|
||||
val includeList = when {
|
||||
type == "ANIME" && !getPreference(PrefName.IncludeAnimeList) -> "onList:false"
|
||||
type == "MANGA" && !getPreference(PrefName.IncludeMangaList) -> "onList:false"
|
||||
else -> ""
|
||||
}
|
||||
val isAdult = if (getPreference(PrefName.AdultOnly)) "isAdult:true" else ""
|
||||
val formatFilter = format?.let { "format:$it, " } ?: ""
|
||||
val countryFilter = country?.let { "countryOfOrigin:$it, " } ?: ""
|
||||
|
||||
return buildString {
|
||||
append("""Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:$sort, type:$type, $formatFilter $countryFilter $includeList $isAdult){id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}""")
|
||||
}
|
||||
}
|
||||
|
||||
private fun recentAnimeUpdates(page: Int): String {
|
||||
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${System.currentTimeMillis() / 1000 - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}"""
|
||||
}
|
||||
|
||||
private fun trendingMovies(page: Int): String {
|
||||
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: ANIME, format: MOVIE, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
|
||||
private fun topRatedAnime(page: Int): String {
|
||||
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
|
||||
private fun mostFavAnime(page: Int): String {
|
||||
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
|
||||
}
|
||||
|
||||
suspend fun loadAnimeList(): Map<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()
|
||||
val currentTime = System.currentTimeMillis() / 1000
|
||||
return buildString {
|
||||
append("""Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${currentTime - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}""")
|
||||
}
|
||||
executeQuery<Query.AnimeList>(query(), force = true)?.data?.apply {
|
||||
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
|
||||
val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly)
|
||||
val idArr = mutableListOf<Int>()
|
||||
list["recentUpdates"] = recentUpdates?.airingSchedules?.mapNotNull { i ->
|
||||
i.media?.let {
|
||||
if (!idArr.contains(it.id))
|
||||
if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
|
||||
idArr.add(it.id)
|
||||
Media(it)
|
||||
} else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)) {
|
||||
idArr.add(it.id)
|
||||
Media(it)
|
||||
} else if ((listOnly && it.mediaListEntry != null)) {
|
||||
idArr.add(it.id)
|
||||
Media(it)
|
||||
} else null
|
||||
else null
|
||||
}
|
||||
|
||||
private fun queryAnimeList(): String {
|
||||
return buildString {
|
||||
append("""{recentUpdates:${recentAnimeUpdates(1)} recentUpdates2:${recentAnimeUpdates(2)} trendingMovies:${buildQueryString("POPULARITY_DESC", "ANIME", "MOVIE")} topRated:${buildQueryString("SCORE_DESC", "ANIME")} mostFav:${buildQueryString("FAVOURITES_DESC", "ANIME")}}""")
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryMangaList(): String {
|
||||
return buildString {
|
||||
append("""{trendingManga:${buildQueryString("POPULARITY_DESC", "MANGA", country = "JP")} trendingManhwa:${buildQueryString("POPULARITY_DESC", "MANGA", country = "KR")} trendingNovel:${buildQueryString("POPULARITY_DESC", "MANGA", format = "NOVEL", country = "JP")} topRated:${buildQueryString("SCORE_DESC", "MANGA")} mostFav:${buildQueryString("FAVOURITES_DESC", "MANGA")}}""")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadAnimeList(): Map<String, ArrayList<Media>> = coroutineScope {
|
||||
val list = mutableMapOf<String, ArrayList<Media>>()
|
||||
|
||||
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()
|
||||
|
||||
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 =
|
||||
(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>> {
|
||||
suspend fun loadMangaList(): Map<String, ArrayList<Media>> = coroutineScope {
|
||||
val list = mutableMapOf<String, ArrayList<Media>>()
|
||||
fun query(): String {
|
||||
return """{
|
||||
trendingManga:${trendingManga(1)}
|
||||
trendingManga2:${trendingManga(2)}
|
||||
trendingManhwa:${trendingManhwa(1)}
|
||||
trendingManhwa2:${trendingManhwa(2)}
|
||||
trendingNovel:${trendingNovel(1)}
|
||||
trendingNovel2:${trendingNovel(2)}
|
||||
topRated:${topRatedManga(1)}
|
||||
topRated2:${topRatedManga(2)}
|
||||
mostFav:${mostFavManga(1)}
|
||||
mostFav2:${mostFavManga(2)}
|
||||
}""".trimIndent()
|
||||
|
||||
val mangaList = async { executeQuery<Query.MangaList>(queryMangaList(), force = true) }
|
||||
|
||||
mangaList.await()?.data?.apply {
|
||||
list["trendingManga"] = mediaList(trendingManga)
|
||||
list["trendingManhwa"] = mediaList(trendingManhwa)
|
||||
list["trendingNovel"] = mediaList(trendingNovel)
|
||||
list["topRated"] = mediaList(topRated)
|
||||
list["mostFav"] = mediaList(mostFav)
|
||||
}
|
||||
|
||||
executeQuery<Query.MangaList>(query(), force = true)?.data?.apply {
|
||||
list["trendingManga"] = trendingManga?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
|
||||
list["trendingManhwa"] = trendingManhwa?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
|
||||
list["trendingNovel"] = trendingNovel?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
|
||||
list["topRated"] = topRated?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
|
||||
list["mostFav"] = mostFav?.media?.map { Media(it) }?.toCollection(ArrayList()) ?: arrayListOf()
|
||||
|
||||
list["trendingManga"]?.addAll(trendingManga2?.media?.map { Media(it) }?.toList() ?: arrayListOf())
|
||||
list["trendingManhwa"]?.addAll(trendingManhwa2?.media?.map { Media(it) }?.toList() ?: arrayListOf())
|
||||
list["trendingNovel"]?.addAll(trendingNovel2?.media?.map { Media(it) }?.toList() ?: arrayListOf())
|
||||
list["topRated"]?.addAll(topRated2?.media?.map { Media(it) }?.toList() ?: arrayListOf())
|
||||
list["mostFav"]?.addAll(mostFav2?.media?.map { Media(it) }?.toList() ?: arrayListOf())
|
||||
}
|
||||
|
||||
|
||||
return list
|
||||
list
|
||||
}
|
||||
|
||||
|
||||
suspend fun recentlyUpdated(
|
||||
greater: Long = 0,
|
||||
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
||||
@@ -1509,25 +1474,17 @@ Page(page:$page,perPage:50) {
|
||||
return author
|
||||
}
|
||||
|
||||
suspend fun getReviews(mediaId: Int, page: Int = 1, sort: String = "SCORE_DESC"): Query.ReviewsResponse? {
|
||||
suspend fun getReviews(
|
||||
mediaId: Int,
|
||||
page: Int = 1,
|
||||
sort: String = "SCORE_DESC"
|
||||
): Query.ReviewsResponse? {
|
||||
return executeQuery<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}}}}}""",
|
||||
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? {
|
||||
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}}""",
|
||||
@@ -1586,11 +1543,13 @@ Page(page:$page,perPage:50) {
|
||||
suspend fun getNotifications(
|
||||
id: Int,
|
||||
page: Int = 1,
|
||||
resetNotification: Boolean = true
|
||||
resetNotification: Boolean = true,
|
||||
type: Boolean? = null
|
||||
): NotificationResponse? {
|
||||
val typeIn = "type_in:[AIRING,MEDIA_MERGE,MEDIA_DELETION,MEDIA_DATA_CHANGE]"
|
||||
val reset = if (resetNotification) "true" else "false"
|
||||
val res = executeQuery<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
|
||||
)
|
||||
if (res != null && resetNotification) {
|
||||
@@ -1612,8 +1571,9 @@ Page(page:$page,perPage:50) {
|
||||
else if (userId != null) "userId:$userId,"
|
||||
else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
|
||||
else "isFollowing:true,"
|
||||
val typeIn = if (filter == "isFollowing:true,") "type_in:[TEXT,ANIME_LIST,MANGA_LIST,MEDIA_LIST]," else ""
|
||||
return executeQuery<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
|
||||
)
|
||||
}
|
||||
@@ -1621,13 +1581,14 @@ Page(page:$page,perPage:50) {
|
||||
suspend fun getReplies(
|
||||
activityId: Int,
|
||||
page: Int = 1
|
||||
) : ReplyResponse? {
|
||||
val query = """{Page(page:$page,perPage:50){activityReplies(activityId:$activityId){id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}}}"""
|
||||
): ReplyResponse? {
|
||||
val query =
|
||||
"""{Page(page:$page,perPage:50){activityReplies(activityId:$activityId){id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}}}"""
|
||||
return executeQuery(query, force = true)
|
||||
}
|
||||
|
||||
private fun status(page: Int = 1): String {
|
||||
return """Page(page:$page,perPage:50){activities(isFollowing: true,sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed replyCount likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed replyCount likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id isAdult title{english romaji native userPreferred}bannerImage coverImage{extraLarge medium large}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id type createdAt}}}"""
|
||||
return """Page(page:$page,perPage:50){activities(isFollowing: true,type_in:[TEXT,ANIME_LIST,MANGA_LIST,MEDIA_LIST],sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed replyCount likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed replyCount likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id isAdult title{english romaji native userPreferred}bannerImage coverImage{extraLarge medium large}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id type createdAt}}}"""
|
||||
}
|
||||
|
||||
suspend fun getUpcomingAnime(id: String): List<Media> {
|
||||
@@ -1674,4 +1635,4 @@ Page(page:$page,perPage:50) {
|
||||
companion object {
|
||||
const val ITEMS_PER_PAGE = 25
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||
if (!Anilist.initialized) {
|
||||
if (!Anilist.initialized && PrefManager.getVal<String>(PrefName.AnilistToken) != "") {
|
||||
if (Anilist.query.getUserData()) {
|
||||
tryWithSuspend {
|
||||
if (MAL.token != null && !MAL.query.getUserData())
|
||||
@@ -81,24 +81,26 @@ class AnilistHomeViewModel : ViewModel() {
|
||||
MutableLiveData<ArrayList<User>>(null)
|
||||
|
||||
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>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getHidden(): LiveData<ArrayList<Media>> = hidden
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
suspend fun initHomePage() {
|
||||
val res = Anilist.query.initHomePage()
|
||||
res["currentAnime"]?.let { animeContinue.postValue(it as ArrayList<Media>?) }
|
||||
res["favoriteAnime"]?.let { animeFav.postValue(it as ArrayList<Media>?) }
|
||||
res["plannedAnime"]?.let { animePlanned.postValue(it as ArrayList<Media>?) }
|
||||
res["currentManga"]?.let { mangaContinue.postValue(it as ArrayList<Media>?) }
|
||||
res["favoriteManga"]?.let { mangaFav.postValue(it as ArrayList<Media>?) }
|
||||
res["plannedManga"]?.let { mangaPlanned.postValue(it as ArrayList<Media>?) }
|
||||
res["recommendations"]?.let { recommendation.postValue(it as ArrayList<Media>?) }
|
||||
res["hidden"]?.let { hidden.postValue(it as ArrayList<Media>?) }
|
||||
res["status"]?.let { userStatus.postValue(it as ArrayList<User>?) }
|
||||
res["currentAnime"]?.let { animeContinue.postValue(it) }
|
||||
res["favoriteAnime"]?.let { animeFav.postValue(it) }
|
||||
res["currentAnimePlanned"]?.let { animePlanned.postValue(it) }
|
||||
res["currentManga"]?.let { mangaContinue.postValue(it) }
|
||||
res["favoriteManga"]?.let { mangaFav.postValue(it) }
|
||||
res["currentMangaPlanned"]?.let { mangaPlanned.postValue(it) }
|
||||
res["recommendations"]?.let { recommendation.postValue(it) }
|
||||
res["hidden"]?.let { hidden.postValue(it) }
|
||||
}
|
||||
|
||||
suspend fun loadMain(context: FragmentActivity) {
|
||||
|
||||
@@ -163,13 +163,9 @@ class Query {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("recentUpdates2") val recentUpdates2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingMovies2") val trendingMovies2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -181,15 +177,10 @@ class Query {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingManga2") val trendingManga2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingManhwa2") val trendingManhwa2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingNovel2") val trendingNovel2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ data class Media(
|
||||
@SerialName("externalLinks") var externalLinks: List<MediaExternalLink>?,
|
||||
|
||||
// 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
|
||||
// @SerialName("rankings") var rankings: List<MediaRank>?,
|
||||
@@ -239,7 +239,20 @@ data class AiringSchedule(
|
||||
// The associate media of the airing episode
|
||||
@SerialName("media") var media: Media?,
|
||||
)
|
||||
@Serializable
|
||||
data class MediaStreamingEpisode(
|
||||
// The title of the episode
|
||||
@SerialName("title") var title: String?,
|
||||
|
||||
// The thumbnail image of the episode
|
||||
@SerialName("thumbnail") var thumbnail: String?,
|
||||
|
||||
// The url of the episode
|
||||
@SerialName("url") var url: String?,
|
||||
|
||||
// The site location of the streaming episode
|
||||
@SerialName("site") var site: String?,
|
||||
)
|
||||
@Serializable
|
||||
data class MediaCoverImage(
|
||||
// The cover image url of the media at its largest size. If this size isn't available, large will be provided instead.
|
||||
|
||||
@@ -74,7 +74,7 @@ data class User(
|
||||
@Serializable
|
||||
data class UserOptions(
|
||||
// The language the user wants to see media titles in
|
||||
// @SerialName("titleLanguage") var titleLanguage: UserTitleLanguage?,
|
||||
@SerialName("titleLanguage") var titleLanguage: UserTitleLanguage?,
|
||||
|
||||
// Whether the user has enabled viewing of 18+ content
|
||||
@SerialName("displayAdultContent") var displayAdultContent: Boolean?,
|
||||
@@ -88,17 +88,17 @@ data class UserOptions(
|
||||
// // Notification options
|
||||
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,
|
||||
//
|
||||
// // The user's timezone offset (Auth user only)
|
||||
// @SerialName("timezone") var timezone: String?,
|
||||
// The user's timezone offset (Auth user only)
|
||||
@SerialName("timezone") var timezone: String?,
|
||||
//
|
||||
// // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always.
|
||||
// @SerialName("activityMergeTime") var activityMergeTime: Int?,
|
||||
// Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always.
|
||||
@SerialName("activityMergeTime") var activityMergeTime: Int?,
|
||||
//
|
||||
// // The language the user wants to see staff and character names in
|
||||
// // @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?,
|
||||
// The language the user wants to see staff and character names in
|
||||
@SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?,
|
||||
//
|
||||
// // Whether the user only allow messages from users they follow
|
||||
// @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?,
|
||||
// Whether the user only allow messages from users they follow
|
||||
@SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?,
|
||||
|
||||
// The list activity types the user has disabled from being created from list updates
|
||||
// @SerialName("disabledListActivity") var disabledListActivity: List<ListActivityOption>?,
|
||||
@@ -119,6 +119,40 @@ data class UserStatisticTypes(
|
||||
@SerialName("manga") var manga: UserStatistics?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class UserTitleLanguage {
|
||||
@SerialName("ENGLISH")
|
||||
ENGLISH,
|
||||
@SerialName("ROMAJI")
|
||||
ROMAJI,
|
||||
@SerialName("NATIVE")
|
||||
NATIVE
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class UserStaffNameLanguage {
|
||||
@SerialName("ROMAJI_WESTERN")
|
||||
ROMAJI_WESTERN,
|
||||
@SerialName("ROMAJI")
|
||||
ROMAJI,
|
||||
@SerialName("NATIVE")
|
||||
NATIVE
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class ScoreFormat {
|
||||
@SerialName("POINT_100")
|
||||
POINT_100,
|
||||
@SerialName("POINT_10_DECIMAL")
|
||||
POINT_10_DECIMAL,
|
||||
@SerialName("POINT_10")
|
||||
POINT_10,
|
||||
@SerialName("POINT_5")
|
||||
POINT_5,
|
||||
@SerialName("POINT_3")
|
||||
POINT_3,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UserStatistics(
|
||||
//
|
||||
@@ -164,7 +198,7 @@ data class Favourites(
|
||||
@Serializable
|
||||
data class MediaListOptions(
|
||||
// The score format the user is using for media lists
|
||||
@SerialName("scoreFormat") var scoreFormat: String?,
|
||||
@SerialName("scoreFormat") var scoreFormat: ScoreFormat?,
|
||||
|
||||
// The default order list rows should be displayed in
|
||||
@SerialName("rowOrder") var rowOrder: String?,
|
||||
@@ -181,8 +215,8 @@ data class MediaListTypeOptions(
|
||||
// The order each list should be displayed in
|
||||
@SerialName("sectionOrder") var sectionOrder: List<String>?,
|
||||
|
||||
// If the completed sections of the list should be separated by format
|
||||
@SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?,
|
||||
// // If the completed sections of the list should be separated by format
|
||||
// @SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?,
|
||||
|
||||
// The names of the user's custom lists
|
||||
@SerialName("customLists") var customLists: List<String>?,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,11 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
object CommentsAPI {
|
||||
private const val ADDRESS: String = "https://api.dantotsu.app"
|
||||
private const val API_ADDRESS: String = "https://api.dantotsu.app"
|
||||
private const val LOCAL_HOST: String = "https://127.0.0.1"
|
||||
private var isOnline: Boolean = true
|
||||
private var commentsEnabled = PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1
|
||||
private val ADDRESS: String get() = if (commentsEnabled) API_ADDRESS else LOCAL_HOST
|
||||
var authToken: String? = null
|
||||
var userId: String? = null
|
||||
var isBanned: Boolean = false
|
||||
@@ -369,10 +372,9 @@ object CommentsAPI {
|
||||
}
|
||||
errorMessage("Failed to login after multiple attempts")
|
||||
}
|
||||
|
||||
private fun errorMessage(reason: String) {
|
||||
Logger.log(reason)
|
||||
if (isOnline) snackString(reason)
|
||||
if (commentsEnabled) Logger.log(reason)
|
||||
if (isOnline && commentsEnabled) snackString(reason)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
|
||||
@@ -70,7 +70,7 @@ object Discord {
|
||||
|
||||
const val application_Id = "1163925779692912771"
|
||||
const val small_Image: String =
|
||||
"mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
|
||||
"mp:external/9NqpMxXs4ZNQtMG42L7hqINW92GqqDxgxS9Oh0Sp880/%3Fsize%3D48%26quality%3Dlossless%26name%3DDantotsu/https/cdn.discordapp.com/emojis/1167344924874784828.gif"
|
||||
const val small_Image_AniList: String =
|
||||
"mp:external/rHOIjjChluqQtGyL_UHk6Z4oAqiVYlo_B7HSGPLSoUg/%3Fsize%3D128/https/cdn.discordapp.com/icons/210521487378087947/a_f54f910e2add364a3da3bb2f2fce0c72.webp"
|
||||
"https://anilist.co/img/icons/android-chrome-512x512.png"
|
||||
}
|
||||
@@ -1,24 +1,19 @@
|
||||
package ani.dantotsu.connections.discord
|
||||
|
||||
import ani.dantotsu.connections.discord.Discord.token
|
||||
import ani.dantotsu.connections.discord.serializers.Activity
|
||||
import ani.dantotsu.connections.discord.serializers.Presence
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import ani.dantotsu.client as app
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
||||
|
||||
private val json = Json {
|
||||
encodeDefaults = true
|
||||
allowStructuredMapKeys = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
|
||||
}
|
||||
@@ -27,7 +22,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
||||
|
||||
companion object {
|
||||
data class RPCData(
|
||||
val applicationId: String? = null,
|
||||
val applicationId: String,
|
||||
val type: Type? = null,
|
||||
val activityName: String? = null,
|
||||
val details: String? = null,
|
||||
@@ -39,23 +34,21 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
||||
val stopTimestamp: Long? = null,
|
||||
val buttons: MutableList<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 {
|
||||
val json = Json {
|
||||
encodeDefaults = true
|
||||
allowStructuredMapKeys = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
val client = OkHttpClient.Builder()
|
||||
.connectTimeout(10, SECONDS)
|
||||
.readTimeout(10, SECONDS)
|
||||
.writeTimeout(10, SECONDS)
|
||||
.build()
|
||||
|
||||
val assetApi = RPCExternalAsset(data.applicationId, token!!, client, json)
|
||||
suspend fun String.discordUrl() = assetApi.getDiscordUri(this)
|
||||
|
||||
return json.encodeToString(Presence.Response(
|
||||
3,
|
||||
Presence(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ data class Activity(
|
||||
@Serializable
|
||||
data class Timestamps(
|
||||
val start: Long? = null,
|
||||
@SerialName("end")
|
||||
val stop: Long? = null
|
||||
)
|
||||
}
|
||||
@@ -28,6 +28,7 @@ class Contributors {
|
||||
"rebelonion" -> "Owner & Maintainer"
|
||||
"sneazy-ibo" -> "Contributor & Comment Moderator"
|
||||
"WaiWhat" -> "Icon Designer"
|
||||
"itsmechinmoy" -> "Discord and Telegram Admin/Helper, Comment Moderator & Translator"
|
||||
else -> "Contributor"
|
||||
}
|
||||
developers = developers.plus(
|
||||
@@ -89,9 +90,15 @@ class Contributors {
|
||||
"Comment Moderator and Arabic Translator",
|
||||
"https://anilist.co/user/6049773"
|
||||
),
|
||||
Developer(
|
||||
"Dawnusedyeet",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6237399-RHFvRHriXjwS.png",
|
||||
"Contributor",
|
||||
"https://anilist.co/user/Dawnusedyeet/"
|
||||
),
|
||||
Developer(
|
||||
"hastsu",
|
||||
"https://cdn.discordapp.com/avatars/602422545077108749/20b4a6efa4314550e4ed51cdbe4fef3d.webp?size=160",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6183359-9os7zUhYdF64.jpg",
|
||||
"Comment Moderator and Arabic Translator",
|
||||
"https://anilist.co/user/6183359"
|
||||
),
|
||||
@@ -111,4 +118,4 @@ class Contributors {
|
||||
@SerialName("html_url")
|
||||
val htmlUrl: String
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ class DownloadCompat {
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineAnimeModel(
|
||||
"unknown",
|
||||
downloadedType.titleName,
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
@@ -188,7 +188,7 @@ class DownloadCompat {
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
downloadedType.titleName,
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
|
||||
@@ -13,7 +13,6 @@ import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.callback.FolderCallback
|
||||
import com.anggrayudi.storage.file.deleteRecursively
|
||||
import com.anggrayudi.storage.file.findFolder
|
||||
import com.anggrayudi.storage.file.moveFolderTo
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
@@ -279,6 +278,7 @@ class DownloadsManager(private val context: Context) {
|
||||
* @param type the type of media
|
||||
* @return the base directory
|
||||
*/
|
||||
@Synchronized
|
||||
private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
|
||||
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
|
||||
if (baseDirectory == Uri.EMPTY) return null
|
||||
@@ -307,6 +307,7 @@ class DownloadsManager(private val context: Context) {
|
||||
* @param chapter the chapter of the media
|
||||
* @return the subdirectory
|
||||
*/
|
||||
@Synchronized
|
||||
fun getSubDirectory(
|
||||
context: Context,
|
||||
type: MediaType,
|
||||
@@ -344,23 +345,34 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun getBaseDirectory(context: Context): DocumentFile? {
|
||||
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
|
||||
if (baseDirectory == Uri.EMPTY) return null
|
||||
return DocumentFile.fromTreeUri(context, baseDirectory)
|
||||
val base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
|
||||
return base.findOrCreateFolder(BASE_LOCATION, false)
|
||||
}
|
||||
|
||||
private val lock = Any()
|
||||
|
||||
private fun DocumentFile.findOrCreateFolder(
|
||||
name: String, overwrite: Boolean
|
||||
): DocumentFile? {
|
||||
return if (overwrite) {
|
||||
findFolder(name.findValidName())?.delete()
|
||||
createDirectory(name.findValidName())
|
||||
} else {
|
||||
findFolder(name.findValidName()) ?: createDirectory(name.findValidName())
|
||||
val validName = name.findValidName()
|
||||
synchronized(lock) {
|
||||
return if (overwrite) {
|
||||
findFolder(validName)?.delete()
|
||||
createDirectory(validName)
|
||||
} else {
|
||||
val folder = findFolder(validName)
|
||||
folder ?: createDirectory(validName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DocumentFile.findFolder(name: String): DocumentFile? =
|
||||
listFiles().find { it.name == name && it.isDirectory }
|
||||
|
||||
private const val RATIO_THRESHOLD = 95
|
||||
fun Media.compareName(name: String): Boolean {
|
||||
val mainName = mainName().findValidName().lowercase()
|
||||
@@ -379,7 +391,7 @@ class DownloadsManager(private val context: Context) {
|
||||
|
||||
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
|
||||
fun String?.findValidName(): String {
|
||||
return this?.replace("/","_")?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
|
||||
return this?.replace("/", "_")?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
|
||||
}
|
||||
|
||||
data class DownloadedType(
|
||||
|
||||
@@ -181,7 +181,6 @@ class AnimeDownloaderService : Service() {
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
// Update the notification to reflect the current state of the queue
|
||||
val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
|
||||
val text = if (pendingDownloads > 0) {
|
||||
"Pending downloads: $pendingDownloads"
|
||||
@@ -201,8 +200,8 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
suspend fun download(task: AnimeDownloadTask) {
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
this@AnimeDownloaderService,
|
||||
@@ -214,22 +213,34 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
withContext(Dispatchers.Main) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
val outputDir = getSubDirectory(
|
||||
val baseOutputDir = getSubDirectory(
|
||||
this@AnimeDownloaderService,
|
||||
MediaType.ANIME,
|
||||
false,
|
||||
task.title
|
||||
) ?: throw Exception("Failed to create output directory")
|
||||
val outputDir = getSubDirectory(
|
||||
this@AnimeDownloaderService,
|
||||
MediaType.ANIME,
|
||||
true,
|
||||
task.title,
|
||||
task.episode
|
||||
) ?: throw Exception("Failed to create output directory")
|
||||
|
||||
val extension = ffExtension!!.getFileExtension()
|
||||
outputDir.findFile("${task.getTaskName().findValidName()}.${extension.first}")?.delete()
|
||||
outputDir.findFile("${task.getTaskName().findValidName()}.${extension.first}")
|
||||
?.delete()
|
||||
|
||||
val outputFile =
|
||||
outputDir.createFile(extension.second, "${task.getTaskName()}.${extension.first}")
|
||||
outputDir.createFile(
|
||||
extension.second,
|
||||
"${task.getTaskName()}.${extension.first}"
|
||||
)
|
||||
?: throw Exception("Failed to create output file")
|
||||
|
||||
var percent = 0
|
||||
@@ -273,7 +284,7 @@ class AnimeDownloaderService : Service() {
|
||||
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
|
||||
ffTask
|
||||
|
||||
saveMediaInfo(task)
|
||||
saveMediaInfo(task, baseOutputDir)
|
||||
|
||||
// periodically check if the download is complete
|
||||
while (ffExtension.getState(ffTask) != "COMPLETED") {
|
||||
@@ -287,7 +298,11 @@ class AnimeDownloaderService : Service() {
|
||||
)
|
||||
} Download failed"
|
||||
)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
if (notifi) {
|
||||
withContext(Dispatchers.Main) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
toast("${getTaskName(task.title, task.episode)} Download failed")
|
||||
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
|
||||
downloadsManager.removeDownload(
|
||||
@@ -320,7 +335,9 @@ class AnimeDownloaderService : Service() {
|
||||
percent.coerceAtMost(99)
|
||||
)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
withContext(Dispatchers.Main) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
kotlinx.coroutines.delay(2000)
|
||||
}
|
||||
@@ -335,7 +352,11 @@ class AnimeDownloaderService : Service() {
|
||||
)
|
||||
} Download failed"
|
||||
)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
if (notifi) {
|
||||
withContext(Dispatchers.Main) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
snackString("${getTaskName(task.title, task.episode)} Download failed")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
@@ -367,7 +388,11 @@ class AnimeDownloaderService : Service() {
|
||||
)
|
||||
} Download completed"
|
||||
)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
if (notifi) {
|
||||
withContext(Dispatchers.Main) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
snackString("${getTaskName(task.title, task.episode)} Download completed")
|
||||
PrefManager.getAnimeDownloadPreferences().edit().putString(
|
||||
task.getTaskName(),
|
||||
@@ -385,23 +410,20 @@ class AnimeDownloaderService : Service() {
|
||||
broadcastDownloadFinished(task.episode)
|
||||
} else throw Exception("Download failed")
|
||||
|
||||
} catch (e: Exception) {
|
||||
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
|
||||
Logger.log("Exception while downloading file: ${e.message}")
|
||||
snackString("Exception while downloading file: ${e.message}")
|
||||
e.printStackTrace()
|
||||
Injekt.get<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 {
|
||||
val directory =
|
||||
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
|
||||
@@ -30,6 +30,7 @@ import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadCompat
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat
|
||||
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
@@ -48,6 +49,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.anggrayudi.storage.file.openInputStream
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
@@ -202,25 +204,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
val type: MediaType = MediaType.ANIME
|
||||
|
||||
// Alert dialog to confirm deletion
|
||||
val builder =
|
||||
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
builder.setTitle("Delete ${item.title}?")
|
||||
builder.setMessage("Are you sure you want to delete ${item.title}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
val mediaIds =
|
||||
PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values
|
||||
?: emptySet()
|
||||
if (mediaIds.isEmpty()) {
|
||||
snackString("No media found") // if this happens, terrible things have happened
|
||||
requireContext().customAlertDialog().apply {
|
||||
setTitle("Delete ${item.title}?")
|
||||
setMessage("Are you sure you want to delete ${item.title}?")
|
||||
setPosButton(R.string.yes) {
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
val mediaIds = PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values ?: emptySet()
|
||||
if (mediaIds.isEmpty()) {
|
||||
snackString("No media found") // if this happens, terrible things have happened
|
||||
}
|
||||
getDownloads()
|
||||
}
|
||||
getDownloads()
|
||||
setNegButton(R.string.no) {
|
||||
// Do nothing
|
||||
}
|
||||
show()
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
val dialog = builder.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -319,17 +318,20 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
SChapterImpl()
|
||||
})
|
||||
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
|
||||
SAnimeImpl() // Provide an instance of SAnimeImpl
|
||||
SAnimeImpl()
|
||||
})
|
||||
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
|
||||
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||
SEpisodeImpl()
|
||||
})
|
||||
.create()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return loadMediaCompat(downloadedType)
|
||||
if (media == null) {
|
||||
Logger.log("No media.json found at ${directory?.uri?.path}")
|
||||
return loadMediaCompat(downloadedType)
|
||||
}
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
@@ -394,6 +396,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
return try {
|
||||
loadOfflineAnimeModelCompat(downloadedType)
|
||||
} catch (e: Exception) {
|
||||
@@ -401,7 +404,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
OfflineAnimeModel(
|
||||
"unknown",
|
||||
downloadedType.titleName,
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
|
||||
@@ -32,6 +32,7 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STAR
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.NumberConverter.Companion.ofLength
|
||||
import com.anggrayudi.storage.file.deleteRecursively
|
||||
import com.anggrayudi.storage.file.forceDelete
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
@@ -134,15 +135,15 @@ class MangaDownloaderService : Service() {
|
||||
mutex.withLock {
|
||||
downloadJobs[task.chapter] = job
|
||||
}
|
||||
job.join() // Wait for the job to complete before continuing to the next task
|
||||
job.join()
|
||||
mutex.withLock {
|
||||
downloadJobs.remove(task.chapter)
|
||||
}
|
||||
updateNotification() // Update the notification after each task is completed
|
||||
updateNotification()
|
||||
}
|
||||
if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
stopSelf() // Stop the service when the queue is empty
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +182,7 @@ class MangaDownloaderService : Service() {
|
||||
|
||||
suspend fun download(task: DownloadTask) {
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
this@MangaDownloaderService,
|
||||
@@ -194,18 +195,27 @@ class MangaDownloaderService : Service() {
|
||||
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
withContext(Dispatchers.Main) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
getSubDirectory(
|
||||
val baseOutputDir = getSubDirectory(
|
||||
this@MangaDownloaderService,
|
||||
MediaType.MANGA,
|
||||
false,
|
||||
task.title
|
||||
) ?: throw Exception("Base output directory not found")
|
||||
val outputDir = getSubDirectory(
|
||||
this@MangaDownloaderService,
|
||||
MediaType.MANGA,
|
||||
false,
|
||||
task.title,
|
||||
task.chapter
|
||||
)?.deleteRecursively(this@MangaDownloaderService)
|
||||
) ?: throw Exception("Output directory not found")
|
||||
|
||||
outputDir.deleteRecursively(this@MangaDownloaderService, true)
|
||||
|
||||
// Loop through each ImageData object from the task
|
||||
var farthest = 0
|
||||
for ((index, image) in task.imageData.withIndex()) {
|
||||
if (deferredMap.size >= task.simultaneousDownloads) {
|
||||
@@ -226,30 +236,36 @@ class MangaDownloaderService : Service() {
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
|
||||
saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap)
|
||||
}
|
||||
farthest++
|
||||
|
||||
builder.setProgress(task.imageData.size, farthest, false)
|
||||
|
||||
broadcastDownloadProgress(
|
||||
task.chapter,
|
||||
farthest * 100 / task.imageData.size
|
||||
)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
withContext(Dispatchers.Main) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
bitmap
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for any remaining deferred to complete
|
||||
deferredMap.values.awaitAll()
|
||||
|
||||
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
||||
.setProgress(0, 0, false)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
withContext(Dispatchers.Main) {
|
||||
builder.setContentText("${task.title} - ${task.chapter} Download complete")
|
||||
.setProgress(0, 0, false)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
saveMediaInfo(task)
|
||||
saveMediaInfo(task, baseOutputDir)
|
||||
downloadsManager.addDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
@@ -269,17 +285,16 @@ class MangaDownloaderService : Service() {
|
||||
}
|
||||
|
||||
|
||||
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
|
||||
private fun saveToDisk(
|
||||
fileName: String,
|
||||
directory: DocumentFile,
|
||||
bitmap: Bitmap
|
||||
) {
|
||||
try {
|
||||
// Define the directory within the private external storage space
|
||||
val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile(fileName)?.forceDelete(this)
|
||||
// Create a file reference within that directory for the image
|
||||
val file =
|
||||
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
|
||||
|
||||
// Use a FileOutputStream to write the bitmap to the file
|
||||
file.openOutputStream(this, false).use { outputStream ->
|
||||
if (outputStream == null) throw Exception("Output stream is null")
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
@@ -292,11 +307,8 @@ class MangaDownloaderService : Service() {
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
|
||||
launchIO {
|
||||
val directory =
|
||||
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
|
||||
@@ -46,6 +46,7 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.anggrayudi.storage.file.openInputStream
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
@@ -171,7 +172,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val media =
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull {
|
||||
it.titleName.compareName(
|
||||
item.title
|
||||
)
|
||||
}
|
||||
media?.let {
|
||||
lifecycleScope.launch {
|
||||
ContextCompat.startActivity(
|
||||
@@ -197,19 +202,15 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
MediaType.NOVEL
|
||||
}
|
||||
// Alert dialog to confirm deletion
|
||||
val builder =
|
||||
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
builder.setTitle("Delete ${item.title}?")
|
||||
builder.setMessage("Are you sure you want to delete ${item.title}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
getDownloads()
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
}
|
||||
val dialog = builder.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
requireContext().customAlertDialog().apply {
|
||||
setTitle("Delete ${item.title}?")
|
||||
setMessage("Are you sure you want to delete ${item.title}?")
|
||||
setPosButton(R.string.yes) {
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
getDownloads()
|
||||
}
|
||||
setNegButton(R.string.no)
|
||||
}.show()
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -279,10 +280,12 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
downloads = listOf()
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||
val mangaTitles =
|
||||
downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName.findValidName() == title }
|
||||
val tDownloads =
|
||||
downloadManager.mangaDownloadedTypes.filter { it.titleName.findValidName() == title }
|
||||
val download = tDownloads.firstOrNull() ?: continue
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
@@ -291,7 +294,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName.findValidName() == title }
|
||||
val tDownloads =
|
||||
downloadManager.novelDownloadedTypes.filter { it.titleName.findValidName() == title }
|
||||
val download = tDownloads.firstOrNull() ?: continue
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
@@ -320,11 +324,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
SChapterImpl()
|
||||
})
|
||||
.create()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return DownloadCompat.loadMediaCompat(downloadedType)
|
||||
if (media == null) {
|
||||
Logger.log("No media.json found at ${directory?.uri?.path}")
|
||||
return DownloadCompat.loadMediaCompat(downloadedType)
|
||||
}
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
@@ -340,7 +347,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = downloadedType.type.asText()
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val directory = getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
@@ -378,6 +384,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
return try {
|
||||
loadOfflineMangaModelCompat(downloadedType)
|
||||
} catch (e: Exception) {
|
||||
@@ -385,7 +392,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
downloadedType.titleName,
|
||||
"0",
|
||||
"??",
|
||||
"??",
|
||||
|
||||
@@ -239,6 +239,13 @@ class NovelDownloaderService : Service() {
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val baseDirectory = getSubDirectory(
|
||||
this@NovelDownloaderService,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
task.title
|
||||
) ?: throw Exception("Directory not found")
|
||||
|
||||
// Start the download
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
@@ -334,7 +341,7 @@ class NovelDownloaderService : Service() {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
saveMediaInfo(task)
|
||||
saveMediaInfo(task, baseDirectory)
|
||||
downloadsManager.addDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
@@ -354,15 +361,8 @@ class NovelDownloaderService : Service() {
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
|
||||
launchIO {
|
||||
val directory =
|
||||
getSubDirectory(
|
||||
this@NovelDownloaderService,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
task.title
|
||||
) ?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
|
||||
@@ -3,7 +3,6 @@ package ani.dantotsu.download.video
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@@ -29,10 +28,10 @@ import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -72,19 +71,19 @@ object Helper {
|
||||
episodeImage
|
||||
)
|
||||
|
||||
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||
val downloadCheck = downloadsManger
|
||||
val downloadsManager = Injekt.get<DownloadsManager>()
|
||||
val downloadCheck = downloadsManager
|
||||
.queryDownload(title, episode, MediaType.ANIME)
|
||||
|
||||
if (downloadCheck) {
|
||||
AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle("Download Exists")
|
||||
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||
.setPositiveButton("Yes") { _, _ ->
|
||||
context.customAlertDialog().apply {
|
||||
setTitle("Download Exists")
|
||||
setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||
setPosButton(R.string.yes) {
|
||||
PrefManager.getAnimeDownloadPreferences().edit()
|
||||
.remove(animeDownloadTask.getTaskName())
|
||||
.apply()
|
||||
downloadsManger.removeDownload(
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
title,
|
||||
episode,
|
||||
@@ -99,8 +98,9 @@ object Helper {
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton("No") { _, _ -> }
|
||||
.show()
|
||||
setNegButton(R.string.no)
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
|
||||
@@ -38,6 +38,7 @@ import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -289,15 +290,20 @@ class AnimeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
model.loaded = true
|
||||
model.loadTrending(1)
|
||||
model.loadAll()
|
||||
}
|
||||
model.loaded = true
|
||||
val loadTrending = async(Dispatchers.IO) { model.loadTrending(1) }
|
||||
val loadAll = async(Dispatchers.IO) { model.loadAll() }
|
||||
val loadPopular = async(Dispatchers.IO) {
|
||||
model.loadPopular(
|
||||
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularAnimeList
|
||||
)
|
||||
"ANIME",
|
||||
sort = Anilist.sortBy[1],
|
||||
onList = PrefManager.getVal(PrefName.PopularAnimeList)
|
||||
)
|
||||
}
|
||||
loadTrending.await()
|
||||
loadAll.await()
|
||||
loadPopular.await()
|
||||
live.postValue(false)
|
||||
_binding?.animeRefresh?.isRefreshing = false
|
||||
running = false
|
||||
|
||||
@@ -111,8 +111,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
trendingBinding.searchBar.performClick()
|
||||
}
|
||||
|
||||
trendingBinding.notificationCount.visibility =
|
||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
|
||||
listOf(
|
||||
@@ -268,8 +268,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
|
||||
|
||||
more.setOnClickListener {
|
||||
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
|
||||
ContextCompat.startActivity(
|
||||
it.context, Intent(it.context, MediaListViewActivity::class.java)
|
||||
.putExtra("title", string),
|
||||
@@ -294,8 +295,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
|
||||
fun updateNotificationCount() {
|
||||
if (this::binding.isInitialized) {
|
||||
trendingBinding.notificationCount.visibility =
|
||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.max
|
||||
@@ -92,6 +93,7 @@ class HomeFragment : Fragment() {
|
||||
)
|
||||
binding.homeUserDataProgressBar.visibility = View.GONE
|
||||
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
|
||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
|
||||
binding.homeAnimeList.setOnClickListener {
|
||||
@@ -456,51 +458,56 @@ class HomeFragment : Fragment() {
|
||||
|
||||
var running = false
|
||||
val live = Refresh.activity.getOrPut(1) { MutableLiveData(true) }
|
||||
live.observe(viewLifecycleOwner)
|
||||
{
|
||||
if (!running && it) {
|
||||
live.observe(viewLifecycleOwner) { shouldRefresh ->
|
||||
if (!running && shouldRefresh) {
|
||||
running = true
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
//Get userData First
|
||||
Anilist.userid =
|
||||
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
|
||||
?.toIntOrNull()
|
||||
// Get user data first
|
||||
Anilist.userid = PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)?.toIntOrNull()
|
||||
if (Anilist.userid == null) {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
}
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
}
|
||||
}
|
||||
model.loaded = true
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
model.setListImages()
|
||||
}
|
||||
var empty = true
|
||||
val homeLayoutShow: List<Boolean> =
|
||||
PrefManager.getVal(PrefName.HomeLayout)
|
||||
model.initHomePage()
|
||||
(array.indices).forEach { i ->
|
||||
model.setListImages()
|
||||
}
|
||||
|
||||
var empty = true
|
||||
val homeLayoutShow: List<Boolean> = PrefManager.getVal(PrefName.HomeLayout)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
homeLayoutShow.indices.forEach { i ->
|
||||
if (homeLayoutShow.elementAt(i)) {
|
||||
empty = false
|
||||
} else withContext(Dispatchers.Main) {
|
||||
} else {
|
||||
containers[i].visibility = View.GONE
|
||||
}
|
||||
}
|
||||
model.empty.postValue(empty)
|
||||
}
|
||||
|
||||
val initHomePage = async(Dispatchers.IO) { model.initHomePage() }
|
||||
val initUserStatus = async(Dispatchers.IO) { model.initUserStatus() }
|
||||
initHomePage.await()
|
||||
initUserStatus.await()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
model.empty.postValue(empty)
|
||||
binding.homeHiddenItemsContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
live.postValue(false)
|
||||
_binding?.homeRefresh?.isRefreshing = false
|
||||
running = false
|
||||
}
|
||||
binding.homeHiddenItemsContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +515,7 @@ class HomeFragment : Fragment() {
|
||||
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
|
||||
if (_binding != null) {
|
||||
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
|
||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
}
|
||||
super.onResume()
|
||||
|
||||
@@ -12,12 +12,14 @@ import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.DialogUserAgentBinding
|
||||
import ani.dantotsu.databinding.FragmentLoginBinding
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
|
||||
class LoginFragment : Fragment() {
|
||||
@@ -94,38 +96,31 @@ class LoginFragment : Fragment() {
|
||||
val password = CharArray(16).apply { fill('0') }
|
||||
|
||||
// Inflate the dialog layout
|
||||
val dialogView =
|
||||
LayoutInflater.from(requireActivity()).inflate(R.layout.dialog_user_agent, null)
|
||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
|
||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
||||
subtitleTextView?.visibility = View.VISIBLE
|
||||
subtitleTextView?.text = "Enter your password to decrypt the file"
|
||||
val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
|
||||
userAgentTextBox.hint = "Password"
|
||||
subtitle.visibility = View.VISIBLE
|
||||
subtitle.text = getString(R.string.enter_password_to_decrypt_file)
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(requireActivity(), R.style.MyPopup)
|
||||
.setTitle("Enter Password")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK", null)
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
requireActivity().customAlertDialog().apply {
|
||||
setTitle("Enter Password")
|
||||
setCustomView(dialogView.root)
|
||||
setPosButton(R.string.ok){
|
||||
val editText = dialogView.userAgentTextBox
|
||||
if (editText.text?.isNotBlank() == true) {
|
||||
editText.text?.toString()?.trim()?.toCharArray(password)
|
||||
callback(password)
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
setNegButton(R.string.cancel) {
|
||||
password.fill('0')
|
||||
dialog.dismiss()
|
||||
callback(null)
|
||||
}
|
||||
.create()
|
||||
}.show()
|
||||
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
|
||||
// Override the positive button here
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
val editText = dialog.findViewById<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() {
|
||||
|
||||
@@ -35,6 +35,7 @@ import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -274,15 +275,22 @@ class MangaFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
model.loaded = true
|
||||
model.loadTrending()
|
||||
model.loadAll()
|
||||
}
|
||||
model.loaded = true
|
||||
val loadTrending = async(Dispatchers.IO) { model.loadTrending() }
|
||||
val loadAll = async(Dispatchers.IO) { model.loadAll() }
|
||||
val loadPopular = async(Dispatchers.IO) {
|
||||
model.loadPopular(
|
||||
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularMangaList
|
||||
)
|
||||
"MANGA",
|
||||
sort = Anilist.sortBy[1],
|
||||
onList = PrefManager.getVal(PrefName.PopularAnimeList)
|
||||
)
|
||||
}
|
||||
|
||||
loadTrending.await()
|
||||
loadAll.await()
|
||||
loadPopular.await()
|
||||
|
||||
live.postValue(false)
|
||||
_binding?.mangaRefresh?.isRefreshing = false
|
||||
running = false
|
||||
|
||||
@@ -80,6 +80,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
|
||||
updateAvatar()
|
||||
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
trendingBinding.searchBar.hint = "MANGA"
|
||||
trendingBinding.searchBarText.setOnClickListener {
|
||||
@@ -296,8 +297,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
|
||||
fun updateNotificationCount() {
|
||||
if (this::binding.isInitialized) {
|
||||
trendingBinding.notificationCount.visibility =
|
||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
|
||||
class StatusActivity : AppCompatActivity(), StoriesCallback {
|
||||
private lateinit var activity: ArrayList<User>
|
||||
@@ -44,10 +46,17 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
|
||||
|
||||
val key = "activities"
|
||||
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
|
||||
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
|
||||
val startIndex = if ( startFrom > 0) startFrom else 0
|
||||
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
|
||||
|
||||
if (activity.getOrNull(position) != null) {
|
||||
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
|
||||
val startIndex = if ( startFrom > 0) startFrom else 0
|
||||
binding.stories.setStoriesList(
|
||||
activityList = activity[position].activity,
|
||||
startIndex = startIndex + 1
|
||||
)
|
||||
} else {
|
||||
Logger.log("index out of bounds for position $position of size ${activity.size}")
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
private fun findFirstNonMatch(watchedActivity: Set<Int>, activity: List<Activity>): Int {
|
||||
@@ -58,13 +67,16 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
binding.stories.pause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.stories.resume()
|
||||
if (hasWindowFocus())
|
||||
binding.stories.resume()
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
@@ -83,7 +95,7 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
|
||||
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
|
||||
val startIndex= if ( startFrom > 0) startFrom else 0
|
||||
binding.stories.startAnimation(slideOutLeft)
|
||||
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
|
||||
binding.stories.setStoriesList(activity[position].activity, startIndex + 1)
|
||||
binding.stories.startAnimation(slideInRight)
|
||||
} else {
|
||||
finish()
|
||||
@@ -92,13 +104,13 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
|
||||
|
||||
override fun onStoriesStart() {
|
||||
position -= 1
|
||||
if (position >= 0) {
|
||||
if (position >= 0 && activity[position].activity.isNotEmpty()) {
|
||||
val key = "activities"
|
||||
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
|
||||
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
|
||||
val startIndex = if ( startFrom > 0) startFrom else 0
|
||||
binding.stories.startAnimation(slideOutRight)
|
||||
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
|
||||
binding.stories.setStoriesList(activity[position].activity,startIndex + 1)
|
||||
binding.stories.startAnimation(slideInLeft)
|
||||
} else {
|
||||
finish()
|
||||
|
||||
@@ -30,6 +30,7 @@ import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.profile.UsersDialogFragment
|
||||
import ani.dantotsu.profile.activity.ActivityItemBuilder
|
||||
import ani.dantotsu.profile.activity.RepliesBottomDialog
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
@@ -48,7 +49,6 @@ import kotlin.math.abs
|
||||
class Stories @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener {
|
||||
private lateinit var activity: FragmentActivity
|
||||
private lateinit var binding: FragmentStatusBinding
|
||||
private lateinit var activityList: List<Activity>
|
||||
private lateinit var storiesListener: StoriesCallback
|
||||
@@ -74,16 +74,14 @@ class Stories @JvmOverloads constructor(
|
||||
|
||||
if (context is StoriesCallback) storiesListener = context as StoriesCallback
|
||||
|
||||
binding.leftTouchPanel.setOnTouchListener(this)
|
||||
binding.rightTouchPanel.setOnTouchListener(this)
|
||||
binding.touchPanel.setOnTouchListener(this)
|
||||
}
|
||||
|
||||
|
||||
fun setStoriesList(
|
||||
activityList: List<Activity>, activity: FragmentActivity, startIndex: Int = 1
|
||||
activityList: List<Activity>, startIndex: Int = 1
|
||||
) {
|
||||
this.activityList = activityList
|
||||
this.activity = activity
|
||||
this.storyIndex = startIndex
|
||||
addLoadingViews(activityList)
|
||||
}
|
||||
@@ -264,49 +262,7 @@ class Stories @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
|
||||
private var startClickTime = 0L
|
||||
private var startX = 0f
|
||||
private var startY = 0f
|
||||
private var isLongPress = false
|
||||
private val swipeThreshold = 100
|
||||
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
|
||||
val maxClickDuration = 200
|
||||
when (event?.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
startX = event.x
|
||||
startY = event.y
|
||||
startClickTime = Calendar.getInstance().timeInMillis
|
||||
pause()
|
||||
isLongPress = false
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val deltaX = event.x - startX
|
||||
val deltaY = event.y - startY
|
||||
if (!isLongPress && (abs(deltaX) > swipeThreshold || abs(deltaY) > swipeThreshold)) {
|
||||
isLongPress = true
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
val clickDuration = Calendar.getInstance().timeInMillis - startClickTime
|
||||
if (clickDuration < maxClickDuration && !isLongPress) {
|
||||
when (view?.id) {
|
||||
R.id.leftTouchPanel -> leftPanelTouch()
|
||||
R.id.rightTouchPanel -> rightPanelTouch()
|
||||
}
|
||||
} else {
|
||||
resume()
|
||||
}
|
||||
val deltaX = event.x - startX
|
||||
if (abs(deltaX) > swipeThreshold) {
|
||||
if (deltaX > 0) onStoriesPrevious()
|
||||
else onStoriesCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun rightPanelTouch() {
|
||||
Logger.log("rightPanelTouch: $storyIndex")
|
||||
@@ -359,6 +315,7 @@ class Stories @JvmOverloads constructor(
|
||||
timer.resume()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun loadStory(story: Activity) {
|
||||
val key = "activities"
|
||||
val set = PrefManager.getCustomVal<Set<Int>>(key, setOf()).plus((story.id))
|
||||
@@ -374,6 +331,15 @@ class Stories @JvmOverloads constructor(
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
binding.textActivity.setOnTouchListener { v, event ->
|
||||
onTouchView(v, event, true)
|
||||
v.onTouchEvent(event)
|
||||
}
|
||||
binding.textActivityContainer.setOnTouchListener { v, event ->
|
||||
onTouchView(v, event, true)
|
||||
v.onTouchEvent(event)
|
||||
}
|
||||
fun visible(isList: Boolean) {
|
||||
binding.textActivity.isVisible = !isList
|
||||
binding.textActivityContainer.isVisible = !isList
|
||||
@@ -397,15 +363,17 @@ class Stories @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
} ${story.progress ?: story.media?.title?.userPreferred} " +
|
||||
if (
|
||||
story.status?.contains("completed") == false &&
|
||||
!story.status.contains("plans") &&
|
||||
!story.status.contains("repeating")
|
||||
) {
|
||||
"of ${story.media?.title?.userPreferred}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
if (
|
||||
story.status?.contains("completed") == false &&
|
||||
!story.status.contains("plans") &&
|
||||
!story.status.contains("repeating")&&
|
||||
!story.status.contains("paused")&&
|
||||
!story.status.contains("dropped")
|
||||
) {
|
||||
"of ${story.media?.title?.userPreferred}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
binding.infoText.text = text
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
blurImage(
|
||||
@@ -421,7 +389,7 @@ class Stories @JvmOverloads constructor(
|
||||
story.media?.id
|
||||
),
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
activity,
|
||||
(it.context as FragmentActivity),
|
||||
binding.coverImage,
|
||||
ViewCompat.getTransitionName(binding.coverImage)!!
|
||||
).toBundle()
|
||||
@@ -455,22 +423,21 @@ class Stories @JvmOverloads constructor(
|
||||
}
|
||||
val likeColor = ContextCompat.getColor(context, R.color.yt_red)
|
||||
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
|
||||
binding.replyCount.text = story.replyCount.toString()
|
||||
binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp))
|
||||
binding.activityRepliesContainer.setOnClickListener {
|
||||
RepliesBottomDialog.newInstance(story.id)
|
||||
.show(activity.supportFragmentManager, "replies")
|
||||
.show((it.context as FragmentActivity).supportFragmentManager, "replies")
|
||||
}
|
||||
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
|
||||
binding.replyCount.text = story.replyCount.toString()
|
||||
binding.activityLikeCount.text = story.likeCount.toString()
|
||||
binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp))
|
||||
binding.activityLikeContainer.setOnClickListener {
|
||||
like()
|
||||
}
|
||||
binding.activityLikeContainer.setOnLongClickListener {
|
||||
val context = activity
|
||||
UsersDialogFragment().apply {
|
||||
userList(userList)
|
||||
show(context.supportFragmentManager, "dialog")
|
||||
show((it.context as FragmentActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -484,7 +451,7 @@ class Stories @JvmOverloads constructor(
|
||||
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
|
||||
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
scope.launch {
|
||||
val res = Anilist.query.toggleLike(story.id, "ACTIVITY")
|
||||
val res = Anilist.mutation.toggleLike(story.id, "ACTIVITY")
|
||||
withContext(Dispatchers.Main) {
|
||||
if (res != null) {
|
||||
if (story.isLiked == true) {
|
||||
@@ -502,4 +469,69 @@ class Stories @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
private var startClickTime = 0L
|
||||
private var startX = 0f
|
||||
private var startY = 0f
|
||||
private var isLongPress = false
|
||||
private val swipeThreshold = 100
|
||||
override fun onTouch(view: View, event: MotionEvent): Boolean {
|
||||
onTouchView(view, event)
|
||||
return true
|
||||
}
|
||||
private fun onTouchView(view: View, event: MotionEvent, isText: Boolean = false){
|
||||
val maxClickDuration = 200
|
||||
val screenWidth = view.width
|
||||
val leftHalf = screenWidth / 2
|
||||
val leftQuarter = screenWidth * 0.15
|
||||
val rightQuarter = screenWidth * 0.85
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
startX = event.x
|
||||
startY = event.y
|
||||
startClickTime = Calendar.getInstance().timeInMillis
|
||||
pause()
|
||||
isLongPress = false
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val deltaX = event.x - startX
|
||||
val deltaY = event.y - startY
|
||||
if (!isLongPress && (abs(deltaX) > swipeThreshold || abs(deltaY) > swipeThreshold)) {
|
||||
isLongPress = true
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
val clickDuration = Calendar.getInstance().timeInMillis - startClickTime
|
||||
if (isText) {
|
||||
if (clickDuration < maxClickDuration && !isLongPress) {
|
||||
if (event.x < leftQuarter) {
|
||||
leftPanelTouch()
|
||||
} else if (event.x > rightQuarter) {
|
||||
rightPanelTouch()
|
||||
} else {
|
||||
resume()
|
||||
}
|
||||
} else {
|
||||
resume()
|
||||
}
|
||||
} else {
|
||||
if (clickDuration < maxClickDuration && !isLongPress) {
|
||||
if (event.x < leftHalf) {
|
||||
leftPanelTouch()
|
||||
} else {
|
||||
rightPanelTouch()
|
||||
}
|
||||
} else {
|
||||
resume()
|
||||
}
|
||||
}
|
||||
val deltaX = event.x - startX
|
||||
val deltaY = event.y - startY
|
||||
if (abs(deltaX) > swipeThreshold && !(abs(deltaY) > 10)) {
|
||||
if (deltaX > 0) onStoriesPrevious()
|
||||
else onStoriesCompleted()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.home.status
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
@@ -15,6 +14,8 @@ import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.ActivityMarkdownCreator
|
||||
|
||||
class UserStatusAdapter(private val user: ArrayList<User>) :
|
||||
RecyclerView.Adapter<UserStatusAdapter.UsersViewHolder>() {
|
||||
@@ -23,6 +24,10 @@ class UserStatusAdapter(private val user: ArrayList<User>) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (user[bindingAdapterPosition].activity.isEmpty()) {
|
||||
snackString("No activity")
|
||||
return@setOnClickListener
|
||||
}
|
||||
StatusActivity.user = user
|
||||
ContextCompat.startActivity(
|
||||
itemView.context,
|
||||
@@ -34,14 +39,23 @@ class UserStatusAdapter(private val user: ArrayList<User>) :
|
||||
)
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
ContextCompat.startActivity(
|
||||
itemView.context,
|
||||
Intent(
|
||||
if (user[bindingAdapterPosition].id == Anilist.userid) {
|
||||
ContextCompat.startActivity(
|
||||
itemView.context,
|
||||
ProfileActivity::class.java
|
||||
).putExtra("userId", user[bindingAdapterPosition].id),
|
||||
null
|
||||
)
|
||||
Intent(itemView.context, ActivityMarkdownCreator::class.java)
|
||||
.putExtra("type", "activity"),
|
||||
null
|
||||
)
|
||||
}else{
|
||||
ContextCompat.startActivity(
|
||||
itemView.context,
|
||||
Intent(
|
||||
itemView.context,
|
||||
ProfileActivity::class.java
|
||||
).putExtra("userId", user[bindingAdapterPosition].id),
|
||||
null
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class CalendarActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityListBinding
|
||||
private val scope = lifecycleScope
|
||||
private var selectedTabIdx = 1
|
||||
private var showOnlyLibrary = false
|
||||
private val model: OtherDetailsViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -38,8 +39,6 @@ class CalendarActivity : AppCompatActivity() {
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityListBinding.inflate(layoutInflater)
|
||||
|
||||
|
||||
|
||||
val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface)
|
||||
val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
|
||||
val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline)
|
||||
@@ -79,6 +78,17 @@ class CalendarActivity : AppCompatActivity() {
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {}
|
||||
})
|
||||
|
||||
binding.listed.setOnClickListener {
|
||||
showOnlyLibrary = !showOnlyLibrary
|
||||
binding.listed.setImageResource(
|
||||
if (showOnlyLibrary) R.drawable.ic_round_collections_bookmark_24
|
||||
else R.drawable.ic_round_library_books_24
|
||||
)
|
||||
scope.launch {
|
||||
model.loadCalendar(showOnlyLibrary)
|
||||
}
|
||||
}
|
||||
|
||||
model.getCalendar().observe(this) {
|
||||
if (it != null) {
|
||||
binding.listProgressBar.visibility = View.GONE
|
||||
@@ -97,11 +107,10 @@ class CalendarActivity : AppCompatActivity() {
|
||||
live.observe(this) {
|
||||
if (it) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) { model.loadCalendar() }
|
||||
withContext(Dispatchers.IO) { model.loadCalendar(showOnlyLibrary) }
|
||||
live.postValue(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.databinding.ItemCharacterBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.setAnimation
|
||||
@@ -55,6 +56,7 @@ class CharacterAdapter(
|
||||
).toBundle()
|
||||
)
|
||||
}
|
||||
itemView.setOnLongClickListener { copyToClipboard(characterList[bindingAdapterPosition].name ?: ""); true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.anilist.api.MediaEdge
|
||||
import ani.dantotsu.connections.anilist.api.MediaList
|
||||
import ani.dantotsu.connections.anilist.api.MediaStreamingEpisode
|
||||
import ani.dantotsu.connections.anilist.api.MediaType
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.media.anime.Anime
|
||||
import ani.dantotsu.media.manga.Manga
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.Serializable
|
||||
import ani.dantotsu.connections.anilist.api.Media as ApiMedia
|
||||
|
||||
@@ -76,7 +84,7 @@ data class Media(
|
||||
var nameMAL: String? = null,
|
||||
var shareLink: String? = null,
|
||||
var selected: Selected? = null,
|
||||
|
||||
var streamingEpisodes: List<MediaStreamingEpisode>? = null,
|
||||
var idKitsu: String? = null,
|
||||
|
||||
var cameFromContinue: Boolean = false
|
||||
@@ -129,6 +137,37 @@ data class Media(
|
||||
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
|
||||
}
|
||||
|
||||
fun Media?.deleteFromList(
|
||||
scope: CoroutineScope,
|
||||
onSuccess: suspend () -> Unit,
|
||||
onError: suspend (e: Exception) -> Unit,
|
||||
onNotFound: suspend () -> Unit
|
||||
) {
|
||||
val id = this?.userListId
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
this@deleteFromList?.let { media ->
|
||||
val _id = id ?: Anilist.query.userMediaDetails(media).userListId;
|
||||
_id?.let { listId ->
|
||||
try {
|
||||
Anilist.mutation.deleteList(listId)
|
||||
MAL.query.deleteList(media.anime != null, media.idMAL)
|
||||
|
||||
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
|
||||
PrefManager.setCustomVal(
|
||||
"removeList", removeList.minus(listId)
|
||||
)
|
||||
|
||||
onSuccess()
|
||||
} catch (e: Exception) {
|
||||
onError(e)
|
||||
}
|
||||
} ?: onNotFound()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun emptyMedia() = Media(
|
||||
id = 0,
|
||||
name = "No media found",
|
||||
|
||||
@@ -293,7 +293,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
binding.mediaTotal.visibility = View.VISIBLE
|
||||
binding.mediaAddToList.text = userStatus
|
||||
} else {
|
||||
binding.mediaAddToList.setText(R.string.add)
|
||||
binding.mediaAddToList.setText(R.string.add_list)
|
||||
}
|
||||
total()
|
||||
binding.mediaAddToList.setOnClickListener {
|
||||
@@ -372,7 +372,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
|
||||
navBar.addTab(infoTab)
|
||||
navBar.addTab(watchTab)
|
||||
navBar.addTab(commentTab)
|
||||
if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1) {
|
||||
navBar.addTab(commentTab)
|
||||
}
|
||||
if (model.continueMedia == null && media.cameFromContinue) {
|
||||
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
|
||||
selected = 1
|
||||
@@ -424,7 +426,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
navBar.selectTabAt(selected)
|
||||
if (::navBar.isInitialized)
|
||||
navBar.selectTabAt(selected)
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
import ani.dantotsu.others.AniSkip
|
||||
import ani.dantotsu.others.Anify
|
||||
import ani.dantotsu.others.Jikan
|
||||
import ani.dantotsu.others.Kitsu
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
@@ -99,6 +100,15 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
if (kitsuEpisodes.value == null) kitsuEpisodes.postValue(Kitsu.getKitsuEpisodesDetails(s))
|
||||
}
|
||||
}
|
||||
private val anifyEpisodes: MutableLiveData<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>> =
|
||||
MutableLiveData<Map<String, Episode>>(null)
|
||||
|
||||
@@ -105,8 +105,8 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
|
||||
View.VISIBLE
|
||||
val infoNameRomanji = tripleTab + media.nameRomaji
|
||||
binding.mediaInfoNameRomaji.text = infoNameRomanji
|
||||
val infoNameRomaji = tripleTab + media.nameRomaji
|
||||
binding.mediaInfoNameRomaji.text = infoNameRomaji
|
||||
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
||||
copyToClipboard(media.nameRomaji)
|
||||
true
|
||||
|
||||
@@ -271,29 +271,23 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
binding.mediaListDelete.setOnClickListener {
|
||||
var id = media!!.userListId
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (id != null) {
|
||||
Anilist.mutation.deleteList(id!!)
|
||||
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
||||
} else {
|
||||
val profile = Anilist.query.userMediaDetails(media!!)
|
||||
profile.userListId?.let { listId ->
|
||||
id = listId
|
||||
Anilist.mutation.deleteList(listId)
|
||||
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
||||
}
|
||||
media?.deleteFromList(scope, onSuccess = {
|
||||
Refresh.all()
|
||||
snackString(getString(R.string.deleted_from_list))
|
||||
dismissAllowingStateLoss()
|
||||
}, onError = { e ->
|
||||
withContext(Dispatchers.Main) {
|
||||
snackString(
|
||||
getString(
|
||||
R.string.delete_fail_reason, e.message
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
PrefManager.setCustomVal("removeList", removeList.minus(media?.id))
|
||||
}
|
||||
if (id != null) {
|
||||
Refresh.all()
|
||||
snackString(getString(R.string.deleted_from_list))
|
||||
dismissAllowingStateLoss()
|
||||
} else {
|
||||
snackString(getString(R.string.no_list_id))
|
||||
}, onNotFound = {
|
||||
snackString(getString(R.string.no_list_id))
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,36 +63,24 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||
val scope = viewLifecycleOwner.lifecycleScope
|
||||
binding.mediaListDelete.setOnClickListener {
|
||||
var id = media.userListId
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (id != null) {
|
||||
try {
|
||||
Anilist.mutation.deleteList(id!!)
|
||||
MAL.query.deleteList(media.anime != null, media.idMAL)
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
snackString(getString(R.string.delete_fail_reason, e.message))
|
||||
}
|
||||
return@withContext
|
||||
}
|
||||
} else {
|
||||
val profile = Anilist.query.userMediaDetails(media)
|
||||
profile.userListId?.let { listId ->
|
||||
id = listId
|
||||
Anilist.mutation.deleteList(listId)
|
||||
MAL.query.deleteList(media.anime != null, media.idMAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (id != null) {
|
||||
scope.launch {
|
||||
media.deleteFromList(scope, onSuccess = {
|
||||
Refresh.all()
|
||||
snackString(getString(R.string.deleted_from_list))
|
||||
dismissAllowingStateLoss()
|
||||
} else {
|
||||
}, onError = { e ->
|
||||
withContext(Dispatchers.Main) {
|
||||
snackString(
|
||||
getString(
|
||||
R.string.delete_fail_reason, e.message
|
||||
)
|
||||
)
|
||||
}
|
||||
}, onNotFound = {
|
||||
snackString(getString(R.string.no_list_id))
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,25 +26,50 @@ class OtherDetailsViewModel : ViewModel() {
|
||||
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)
|
||||
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
|
||||
suspend fun loadCalendar() {
|
||||
val curr = System.currentTimeMillis() / 1000
|
||||
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
|
||||
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
||||
val map = mutableMapOf<String, MutableList<Media>>()
|
||||
val idMap = mutableMapOf<String, MutableList<Int>>()
|
||||
res?.forEach {
|
||||
val v = it.relation?.split(",")?.map { i -> i.toLong() }!!
|
||||
val dateInfo = df.format(Date(v[1] * 1000))
|
||||
val list = map.getOrPut(dateInfo) { mutableListOf() }
|
||||
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
|
||||
it.relation = "Episode ${v[0]}"
|
||||
if (!idList.contains(it.id)) {
|
||||
idList.add(it.id)
|
||||
list.add(it)
|
||||
suspend fun loadCalendar(showOnlyLibrary: Boolean = false) {
|
||||
if (cachedAllCalendarData == null || cachedLibraryCalendarData == null) {
|
||||
val curr = System.currentTimeMillis() / 1000
|
||||
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
|
||||
val df = DateFormat.getDateInstance(DateFormat.FULL)
|
||||
val allMap = mutableMapOf<String, MutableList<Media>>()
|
||||
val libraryMap = mutableMapOf<String, MutableList<Media>>()
|
||||
val idMap = mutableMapOf<String, MutableList<Int>>()
|
||||
|
||||
val userId = Anilist.userid ?: 0
|
||||
val userLibrary = Anilist.query.getMediaLists(true, userId)
|
||||
val libraryMediaIds = userLibrary.flatMap { it.value }.map { it.id }
|
||||
|
||||
res.forEach {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package ani.dantotsu.media
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -21,7 +20,7 @@ import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.util.MarkdownCreatorActivity
|
||||
import ani.dantotsu.util.ActivityMarkdownCreator
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -59,7 +58,7 @@ class ReviewActivity : AppCompatActivity() {
|
||||
binding.followFilterButton.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
this,
|
||||
Intent(this, MarkdownCreatorActivity::class.java)
|
||||
Intent(this, ActivityMarkdownCreator::class.java)
|
||||
.putExtra("type", "review"),
|
||||
null
|
||||
)
|
||||
|
||||
@@ -183,6 +183,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
binding.searchByImage.setOnClickListener {
|
||||
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
|
||||
}
|
||||
binding.clearHistory.setOnClickListener {
|
||||
it.startAnimation(fadeOutAnimation())
|
||||
it.visibility = View.GONE
|
||||
searchHistoryAdapter.clearHistory()
|
||||
}
|
||||
updateClearHistoryVisibility()
|
||||
fun searchTitle() {
|
||||
activity.result.apply {
|
||||
search =
|
||||
@@ -300,11 +306,17 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
}
|
||||
|
||||
binding.searchResultLayout.visibility = View.VISIBLE
|
||||
binding.clearHistory.visibility = View.GONE
|
||||
binding.searchHistoryList.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 {
|
||||
return AlphaAnimation(0f, 1f).apply {
|
||||
duration = 150
|
||||
@@ -375,4 +387,3 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
override fun getItemCount(): Int = chips.size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,12 @@ class SearchHistoryAdapter(private val type: String, private val searchClicked:
|
||||
PrefManager.setVal(historyType, searchHistory)
|
||||
}
|
||||
|
||||
fun clearHistory() {
|
||||
searchHistory?.clear()
|
||||
PrefManager.setVal(historyType, searchHistory)
|
||||
submitList(searchHistory?.toList())
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
|
||||
@@ -26,4 +26,5 @@ data class Anime(
|
||||
var slug: String? = null,
|
||||
var kitsuEpisodes: Map<String, Episode>? = null,
|
||||
var fillerEpisodes: Map<String, Episode>? = null,
|
||||
var anifyEpisodes: Map<String, Episode>? = null,
|
||||
) : Serializable
|
||||
@@ -8,8 +8,8 @@ import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getString
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
@@ -18,8 +18,9 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemMediaSourceBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.displayTimer
|
||||
import ani.dantotsu.isOnline
|
||||
@@ -33,12 +34,15 @@ import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.others.webview.CookieCatcher
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.DynamicAnimeParser
|
||||
import ani.dantotsu.parsers.OfflineAnimeParser
|
||||
import ani.dantotsu.parsers.WatchSources
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.FAQActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
|
||||
@@ -54,16 +58,13 @@ class AnimeWatchAdapter(
|
||||
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
|
||||
private var autoSelect = true
|
||||
var subscribe: MediaDetailsActivity.PopImageButton? = null
|
||||
private var _binding: ItemAnimeWatchBinding? = null
|
||||
private var _binding: ItemMediaSourceBinding? = null
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private var nestedDialog: AlertDialog? = null
|
||||
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
_binding = binding
|
||||
@@ -75,7 +76,7 @@ class AnimeWatchAdapter(
|
||||
null
|
||||
)
|
||||
}
|
||||
//Youtube
|
||||
// Youtube
|
||||
if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
|
||||
binding.animeSourceYT.visibility = View.VISIBLE
|
||||
binding.animeSourceYT.setOnClickListener {
|
||||
@@ -89,7 +90,7 @@ class AnimeWatchAdapter(
|
||||
R.string.subbed
|
||||
)
|
||||
|
||||
//PreferDub
|
||||
// PreferDub
|
||||
var changing = false
|
||||
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
|
||||
binding.animeSourceDubbedText.text =
|
||||
@@ -99,8 +100,8 @@ class AnimeWatchAdapter(
|
||||
if (!changing) fragment.onDubClicked(isChecked)
|
||||
}
|
||||
|
||||
//Wrong Title
|
||||
binding.animeSourceSearch.setOnClickListener {
|
||||
// Wrong Title
|
||||
binding.mediaSourceSearch.setOnClickListener {
|
||||
SourceSearchDialogFragment().show(
|
||||
fragment.requireActivity().supportFragmentManager,
|
||||
null
|
||||
@@ -108,37 +109,37 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
|
||||
|
||||
binding.animeSourceNameContainer.isGone = offline
|
||||
binding.animeSourceSettings.isGone = offline
|
||||
binding.animeSourceSearch.isGone = offline
|
||||
binding.animeSourceTitle.isGone = offline
|
||||
binding.mediaSourceNameContainer.isGone = offline
|
||||
binding.mediaSourceSettings.isGone = offline
|
||||
binding.mediaSourceSearch.isGone = offline
|
||||
binding.mediaSourceTitle.isGone = offline
|
||||
|
||||
//Source Selection
|
||||
// Source Selection
|
||||
var source =
|
||||
media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
|
||||
setLanguageList(media.selected!!.langIndex, source)
|
||||
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 {
|
||||
this.selectDub = media.selected!!.preferDub
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
binding.mediaSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
|
||||
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
||||
}
|
||||
}
|
||||
|
||||
binding.animeSource.setAdapter(
|
||||
binding.mediaSource.setAdapter(
|
||||
ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
watchSources.names
|
||||
)
|
||||
)
|
||||
binding.animeSourceTitle.isSelected = true
|
||||
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
||||
binding.mediaSourceTitle.isSelected = true
|
||||
binding.mediaSource.setOnItemClickListener { _, _, i, _ ->
|
||||
fragment.onSourceChange(i).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
binding.mediaSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
|
||||
changing = true
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
changing = false
|
||||
@@ -150,15 +151,15 @@ class AnimeWatchAdapter(
|
||||
fragment.loadEpisodes(i, false)
|
||||
}
|
||||
|
||||
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
|
||||
binding.mediaSourceLanguage.setOnItemClickListener { _, _, i, _ ->
|
||||
// Check if 'extension' and 'selected' properties exist and are accessible
|
||||
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||
ext.sourceLanguage = i
|
||||
fragment.onLangChange(i)
|
||||
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
binding.mediaSourceTitle.text = showUserText
|
||||
showUserTextListener =
|
||||
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
{ MainScope().launch { binding.mediaSourceTitle.text = it } }
|
||||
changing = true
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
changing = false
|
||||
@@ -170,19 +171,19 @@ class AnimeWatchAdapter(
|
||||
} ?: run { }
|
||||
}
|
||||
|
||||
//settings
|
||||
binding.animeSourceSettings.setOnClickListener {
|
||||
// Settings
|
||||
binding.mediaSourceSettings.setOnClickListener {
|
||||
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
|
||||
fragment.openSettings(ext.extension)
|
||||
}
|
||||
}
|
||||
|
||||
//Icons
|
||||
// Icons
|
||||
|
||||
//subscribe
|
||||
// Subscribe
|
||||
subscribe = MediaDetailsActivity.PopImageButton(
|
||||
fragment.lifecycleScope,
|
||||
binding.animeSourceSubscribe,
|
||||
binding.mediaSourceSubscribe,
|
||||
R.drawable.ic_round_notifications_active_24,
|
||||
R.drawable.ic_round_notifications_none_24,
|
||||
R.color.bg_opp,
|
||||
@@ -190,125 +191,164 @@ class AnimeWatchAdapter(
|
||||
fragment.subscribed,
|
||||
true
|
||||
) {
|
||||
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
|
||||
fragment.onNotificationPressed(it, binding.mediaSource.text.toString())
|
||||
}
|
||||
|
||||
subscribeButton(false)
|
||||
|
||||
binding.animeSourceSubscribe.setOnLongClickListener {
|
||||
binding.mediaSourceSubscribe.setOnLongClickListener {
|
||||
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
|
||||
}
|
||||
|
||||
//Nested Button
|
||||
binding.animeNestedButton.setOnClickListener {
|
||||
val dialogView =
|
||||
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
|
||||
val dialogBinding = DialogLayoutBinding.bind(dialogView)
|
||||
var refresh = false
|
||||
var run = false
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style =
|
||||
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
dialogBinding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
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)
|
||||
// Nested Button
|
||||
binding.mediaNestedButton.setOnClickListener {
|
||||
val dialogBinding = DialogLayoutBinding.inflate(fragment.layoutInflater)
|
||||
dialogBinding.apply {
|
||||
var refresh = false
|
||||
var run = false
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style =
|
||||
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
|
||||
|
||||
mediaSourceTop.rotation = if (reversed) -90f else 90f
|
||||
sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
mediaSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
mediaSourceTop.rotation = if (reversed) -90f else 90f
|
||||
sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
run = true
|
||||
}
|
||||
//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()
|
||||
// Grids
|
||||
var selected = when (style) {
|
||||
0 -> mediaSourceList
|
||||
1 -> mediaSourceGrid
|
||||
2 -> mediaSourceCompact
|
||||
else -> mediaSourceList
|
||||
}
|
||||
when (style) {
|
||||
0 -> layoutText.setText(R.string.list)
|
||||
1 -> layoutText.setText(R.string.grid)
|
||||
2 -> layoutText.setText(R.string.compact)
|
||||
else -> mediaSourceList
|
||||
}
|
||||
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()
|
||||
|
||||
//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) {
|
||||
subscribe?.enabled(enabled)
|
||||
}
|
||||
|
||||
//Chips
|
||||
// Chips
|
||||
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
|
||||
val binding = _binding
|
||||
if (binding != null) {
|
||||
@@ -319,13 +359,13 @@ class AnimeWatchAdapter(
|
||||
val chip =
|
||||
ItemChipBinding.inflate(
|
||||
LayoutInflater.from(fragment.context),
|
||||
binding.animeSourceChipGroup,
|
||||
binding.mediaSourceChipGroup,
|
||||
false
|
||||
).root
|
||||
chip.isCheckable = true
|
||||
fun selected() {
|
||||
chip.isChecked = true
|
||||
binding.animeWatchChipScroll.smoothScrollTo(
|
||||
binding.mediaWatchChipScroll.smoothScrollTo(
|
||||
(chip.left - screenWidth / 2) + (chip.width / 2),
|
||||
0
|
||||
)
|
||||
@@ -344,14 +384,14 @@ class AnimeWatchAdapter(
|
||||
selected()
|
||||
fragment.onChipClicked(position, limit * (position), last - 1)
|
||||
}
|
||||
binding.animeSourceChipGroup.addView(chip)
|
||||
binding.mediaSourceChipGroup.addView(chip)
|
||||
if (selected == position) {
|
||||
selected()
|
||||
select = chip
|
||||
}
|
||||
}
|
||||
if (select != null)
|
||||
binding.animeWatchChipScroll.apply {
|
||||
binding.mediaWatchChipScroll.apply {
|
||||
post {
|
||||
scrollTo(
|
||||
(select.left - screenWidth / 2) + (select.width / 2),
|
||||
@@ -363,7 +403,7 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
|
||||
fun clearChips() {
|
||||
_binding?.animeSourceChipGroup?.removeAllViews()
|
||||
_binding?.mediaSourceChipGroup?.removeAllViews()
|
||||
}
|
||||
|
||||
fun handleEpisodes() {
|
||||
@@ -379,15 +419,15 @@ class AnimeWatchAdapter(
|
||||
|
||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||
if (episodes.contains(continueEp)) {
|
||||
binding.animeSourceContinue.visibility = View.VISIBLE
|
||||
binding.sourceContinue.visibility = View.VISIBLE
|
||||
handleProgress(
|
||||
binding.itemEpisodeProgressCont,
|
||||
binding.itemEpisodeProgress,
|
||||
binding.itemEpisodeProgressEmpty,
|
||||
binding.itemMediaProgressCont,
|
||||
binding.itemMediaProgress,
|
||||
binding.itemMediaProgressEmpty,
|
||||
media.id,
|
||||
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
|
||||
)
|
||||
) {
|
||||
@@ -395,9 +435,9 @@ class AnimeWatchAdapter(
|
||||
if (e != -1 && e + 1 < episodes.size) {
|
||||
continueEp = episodes[e + 1]
|
||||
handleProgress(
|
||||
binding.itemEpisodeProgressCont,
|
||||
binding.itemEpisodeProgress,
|
||||
binding.itemEpisodeProgressEmpty,
|
||||
binding.itemMediaProgressCont,
|
||||
binding.itemMediaProgress,
|
||||
binding.itemMediaProgressEmpty,
|
||||
media.id,
|
||||
continueEp
|
||||
)
|
||||
@@ -407,51 +447,63 @@ class AnimeWatchAdapter(
|
||||
|
||||
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
|
||||
|
||||
binding.itemEpisodeImage.loadImage(
|
||||
binding.itemMediaImage.loadImage(
|
||||
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
|
||||
)
|
||||
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
|
||||
|
||||
binding.animeSourceContinueText.text =
|
||||
binding.mediaSourceContinueText.text =
|
||||
currActivity()!!.getString(
|
||||
R.string.continue_episode, ep.number, if (ep.filler)
|
||||
currActivity()!!.getString(R.string.filler_tag)
|
||||
else
|
||||
"", cleanedTitle
|
||||
)
|
||||
binding.animeSourceContinue.setOnClickListener {
|
||||
binding.sourceContinue.setOnClickListener {
|
||||
fragment.onEpisodeClick(continueEp)
|
||||
}
|
||||
if (fragment.continueEp) {
|
||||
if (
|
||||
(binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams)
|
||||
(binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams)
|
||||
.weight < PrefManager.getVal<Float>(PrefName.WatchPercentage)
|
||||
) {
|
||||
binding.animeSourceContinue.performClick()
|
||||
binding.sourceContinue.performClick()
|
||||
fragment.continueEp = false
|
||||
}
|
||||
}
|
||||
} 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()
|
||||
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
|
||||
|
||||
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
|
||||
binding.animeSource.setText(
|
||||
binding.animeSource.adapter
|
||||
binding.mediaSource.setText(
|
||||
binding.mediaSource.adapter
|
||||
.getItem(nextIndex).toString(), false
|
||||
)
|
||||
fragment.onSourceChange(nextIndex).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
binding.mediaSourceTitle.text = showUserText
|
||||
showUserTextListener =
|
||||
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
{ MainScope().launch { binding.mediaSourceTitle.text = it } }
|
||||
binding.animeSourceDubbed.isChecked = selectDub
|
||||
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
|
||||
setLanguageList(0, nextIndex)
|
||||
@@ -460,13 +512,13 @@ class AnimeWatchAdapter(
|
||||
fragment.loadEpisodes(nextIndex, false)
|
||||
}
|
||||
}
|
||||
binding.animeSource.setOnClickListener { autoSelect = false }
|
||||
binding.mediaSource.setOnClickListener { autoSelect = false }
|
||||
} else {
|
||||
binding.animeSourceContinue.visibility = View.GONE
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
binding.sourceContinue.visibility = View.GONE
|
||||
binding.sourceNotFound.visibility = View.GONE
|
||||
binding.faqbutton.visibility = View.GONE
|
||||
clearChips()
|
||||
binding.animeSourceProgressBar.visibility = View.VISIBLE
|
||||
binding.sourceProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -480,9 +532,9 @@ class AnimeWatchAdapter(
|
||||
ext.sourceLanguage = lang
|
||||
}
|
||||
try {
|
||||
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||
binding?.mediaSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
binding?.animeSourceLanguage?.setText(
|
||||
binding?.mediaSourceLanguage?.setText(
|
||||
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
|
||||
)
|
||||
}
|
||||
@@ -493,9 +545,9 @@ class AnimeWatchAdapter(
|
||||
)
|
||||
val items = adapter.count
|
||||
|
||||
binding?.animeSourceLanguageContainer?.visibility =
|
||||
binding?.mediaSourceLanguageContainer?.visibility =
|
||||
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
|
||||
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||
inner class ViewHolder(val binding: ItemMediaSourceBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
displayTimer(media, binding.animeSourceContainer)
|
||||
|
||||
@@ -31,7 +31,8 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
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.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
@@ -48,6 +49,7 @@ import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper
|
||||
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
|
||||
import ani.dantotsu.others.Anify
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.parsers.AnimeParser
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
@@ -61,6 +63,7 @@ import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.anggrayudi.storage.file.extension
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
@@ -78,7 +81,7 @@ import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class AnimeWatchFragment : Fragment() {
|
||||
private var _binding: FragmentAnimeWatchBinding? = null
|
||||
private var _binding: FragmentMediaSourceBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val model: MediaDetailsViewModel by activityViewModels()
|
||||
|
||||
@@ -105,7 +108,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentAnimeWatchBinding.inflate(inflater, container, false)
|
||||
_binding = FragmentMediaSourceBinding.inflate(inflater, container, false)
|
||||
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
|
||||
|
||||
var maxGridSize = (screenWidth / 100f).roundToInt()
|
||||
@@ -150,13 +153,13 @@ class AnimeWatchFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
binding.animeSourceRecycler.layoutManager = gridLayoutManager
|
||||
binding.mediaSourceRecycler.layoutManager = gridLayoutManager
|
||||
|
||||
binding.ScrollTop.setOnClickListener {
|
||||
binding.animeSourceRecycler.scrollToPosition(10)
|
||||
binding.animeSourceRecycler.smoothScrollToPosition(0)
|
||||
binding.mediaSourceRecycler.scrollToPosition(10)
|
||||
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) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
@@ -170,7 +173,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
}
|
||||
})
|
||||
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||
if (it) binding.animeSourceRecycler.scrollToPosition(0)
|
||||
if (it) binding.mediaSourceRecycler.scrollToPosition(0)
|
||||
}
|
||||
|
||||
continueEp = model.continueMedia ?: false
|
||||
@@ -203,7 +206,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
offlineMode = offlineMode
|
||||
)
|
||||
|
||||
binding.animeSourceRecycler.adapter =
|
||||
binding.mediaSourceRecycler.adapter =
|
||||
ConcatAdapter(headerAdapter, episodeAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
@@ -212,10 +215,11 @@ class AnimeWatchFragment : Fragment() {
|
||||
if (offline) {
|
||||
media.selected!!.sourceIndex = model.watchSources!!.list.lastIndex
|
||||
} else {
|
||||
awaitAll(
|
||||
async { model.loadKitsuEpisodes(media) },
|
||||
async { model.loadFillerEpisodes(media) }
|
||||
)
|
||||
val kitsuEpisodes = async { model.loadKitsuEpisodes(media) }
|
||||
val anifyEpisodes = async { model.loadAnifyEpisodes(media.id) }
|
||||
val fillerEpisodes = async { model.loadFillerEpisodes(media) }
|
||||
|
||||
awaitAll(kitsuEpisodes, anifyEpisodes, fillerEpisodes)
|
||||
}
|
||||
model.loadEpisodes(media, media.selected!!.sourceIndex)
|
||||
}
|
||||
@@ -230,6 +234,18 @@ class AnimeWatchFragment : Fragment() {
|
||||
val episodes = loadedEpisodes[media.selected!!.sourceIndex]
|
||||
if (episodes != null) {
|
||||
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!!.containsKey(i)) {
|
||||
episode.title =
|
||||
@@ -239,22 +255,19 @@ class AnimeWatchFragment : Fragment() {
|
||||
}
|
||||
if (media.anime?.kitsuEpisodes != null) {
|
||||
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
|
||||
episode.desc =
|
||||
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
|
||||
episode.desc = media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
|
||||
episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
|
||||
episode.title ?: ""
|
||||
).isBlank()
|
||||
) media.anime!!.kitsuEpisodes!![i]?.title
|
||||
?: episode.title else episode.title
|
||||
?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title
|
||||
episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb
|
||||
?: FileUrl[media.cover]
|
||||
) media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title else episode.title
|
||||
?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title
|
||||
episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb ?: episode.thumb
|
||||
}
|
||||
}
|
||||
}
|
||||
media.anime?.episodes = episodes
|
||||
|
||||
//CHIP GROUP
|
||||
// CHIP GROUP
|
||||
val total = episodes.size
|
||||
val divisions = total.toDouble() / 10
|
||||
start = 0
|
||||
@@ -295,6 +308,10 @@ class AnimeWatchFragment : Fragment() {
|
||||
if (i != null)
|
||||
media.anime?.fillerEpisodes = i
|
||||
}
|
||||
model.getAnifyEpisodes().observe(viewLifecycleOwner) { i ->
|
||||
if (i != null)
|
||||
media.anime?.anifyEpisodes = i
|
||||
}
|
||||
}
|
||||
|
||||
fun onSourceChange(i: Int): AnimeParser {
|
||||
@@ -380,20 +397,18 @@ class AnimeWatchFragment : Fragment() {
|
||||
if (allSettings.size > 1) {
|
||||
val names =
|
||||
allSettings.map { LanguageMapper.getLanguageName(it.lang) }.toTypedArray()
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, -1) { dialog, which ->
|
||||
requireContext()
|
||||
.customAlertDialog()
|
||||
.apply {
|
||||
setTitle("Select a Source")
|
||||
singleChoiceItems(names) { which ->
|
||||
selectedSetting = allSettings[which]
|
||||
itemSelected = true
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
requireActivity().runOnUiThread {
|
||||
val fragment =
|
||||
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
changeUIVisibility(true)
|
||||
loadEpisodes(media.selected!!.sourceIndex, true)
|
||||
}
|
||||
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
changeUIVisibility(true)
|
||||
loadEpisodes(media.selected!!.sourceIndex, true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
@@ -401,13 +416,13 @@ class AnimeWatchFragment : Fragment() {
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
.setOnDismissListener {
|
||||
onDismiss {
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
requireActivity().runOnUiThread {
|
||||
@@ -416,11 +431,12 @@ class AnimeWatchFragment : Fragment() {
|
||||
changeUIVisibility(true)
|
||||
loadEpisodes(media.selected!!.sourceIndex, true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
parentFragmentManager.beginTransaction().apply {
|
||||
setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
addToBackStack(null)
|
||||
commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,7 +635,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
private fun reload() {
|
||||
val selected = model.loadSelected(media)
|
||||
|
||||
//Find latest episode for subscription
|
||||
// Find latest episode for subscription
|
||||
selected.latest =
|
||||
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
||||
selected.latest =
|
||||
@@ -663,14 +679,14 @@ class AnimeWatchFragment : Fragment() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.mediaInfoProgressBar.visibility = progress
|
||||
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
|
||||
binding.mediaSourceRecycler.layoutManager?.onRestoreInstanceState(state)
|
||||
|
||||
requireActivity().setNavigationTheme()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||
state = binding.mediaSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -23,6 +23,7 @@ import ani.dantotsu.media.MediaNameAdapter
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -106,8 +107,8 @@ class EpisodeAdapter(
|
||||
|
||||
val thumb =
|
||||
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)
|
||||
.into(binding.itemEpisodeImage)
|
||||
Glide.with(binding.itemMediaImage).load(thumb ?: media.cover).override(400, 0)
|
||||
.into(binding.itemMediaImage)
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
|
||||
|
||||
@@ -140,9 +141,9 @@ class EpisodeAdapter(
|
||||
}
|
||||
|
||||
handleProgress(
|
||||
binding.itemEpisodeProgressCont,
|
||||
binding.itemEpisodeProgress,
|
||||
binding.itemEpisodeProgressEmpty,
|
||||
binding.itemMediaProgressCont,
|
||||
binding.itemMediaProgress,
|
||||
binding.itemMediaProgressEmpty,
|
||||
media.id,
|
||||
ep.number
|
||||
)
|
||||
@@ -154,8 +155,8 @@ class EpisodeAdapter(
|
||||
|
||||
val thumb =
|
||||
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)
|
||||
.into(binding.itemEpisodeImage)
|
||||
Glide.with(binding.itemMediaImage).load(thumb ?: media.cover).override(400, 0)
|
||||
.into(binding.itemMediaImage)
|
||||
|
||||
binding.itemEpisodeNumber.text = ep.number
|
||||
binding.itemEpisodeTitle.text = title
|
||||
@@ -183,9 +184,9 @@ class EpisodeAdapter(
|
||||
binding.itemEpisodeViewed.visibility = View.GONE
|
||||
}
|
||||
handleProgress(
|
||||
binding.itemEpisodeProgressCont,
|
||||
binding.itemEpisodeProgress,
|
||||
binding.itemEpisodeProgressEmpty,
|
||||
binding.itemMediaProgressCont,
|
||||
binding.itemMediaProgress,
|
||||
binding.itemMediaProgressEmpty,
|
||||
media.id,
|
||||
ep.number
|
||||
)
|
||||
@@ -208,9 +209,9 @@ class EpisodeAdapter(
|
||||
}
|
||||
}
|
||||
handleProgress(
|
||||
binding.itemEpisodeProgressCont,
|
||||
binding.itemEpisodeProgress,
|
||||
binding.itemEpisodeProgressEmpty,
|
||||
binding.itemMediaProgressCont,
|
||||
binding.itemMediaProgress,
|
||||
binding.itemMediaProgressEmpty,
|
||||
media.id,
|
||||
ep.number
|
||||
)
|
||||
@@ -318,16 +319,14 @@ class EpisodeAdapter(
|
||||
fragment.onAnimeEpisodeStopDownloadClick(episodeNumber)
|
||||
return@setOnClickListener
|
||||
} else if (downloadedEpisodes.contains(episodeNumber)) {
|
||||
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
builder.setTitle("Delete Episode")
|
||||
builder.setMessage("Are you sure you want to delete Episode ${episodeNumber}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
}
|
||||
val dialog = builder.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
binding.root.context.customAlertDialog().apply {
|
||||
setTitle("Delete Episode")
|
||||
setMessage("Are you sure you want to delete Episode $episodeNumber?")
|
||||
setPosButton(R.string.yes) {
|
||||
fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
|
||||
}
|
||||
setNegButton(R.string.no)
|
||||
}.show()
|
||||
return@setOnClickListener
|
||||
} else {
|
||||
fragment.onAnimeEpisodeDownloadClick(episodeNumber)
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.hardware.SensorManager
|
||||
@@ -71,9 +72,12 @@ import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
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.TrackSelectionOverride
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
@@ -81,6 +85,7 @@ import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.datasource.cache.CacheDataSource
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
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.dp
|
||||
import ani.dantotsu.getCurrentBrightnessValue
|
||||
import ani.dantotsu.getLanguageCode
|
||||
import ani.dantotsu.hideSystemBars
|
||||
import ani.dantotsu.hideSystemBarsExtendView
|
||||
import ani.dantotsu.isOnline
|
||||
@@ -135,6 +141,7 @@ import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.HAnimeSources
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.others.Xubtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
@@ -164,6 +171,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import io.github.anilbeesetti.nextlib.media3ext.ffdecoder.NextRenderersFactory
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.Timer
|
||||
@@ -223,6 +231,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
private lateinit var animeTitle: TextView
|
||||
private lateinit var videoInfo: TextView
|
||||
private lateinit var episodeTitle: Spinner
|
||||
private lateinit var customSubtitleView: Xubtitle
|
||||
|
||||
private var orientationListener: OrientationEventListener? = null
|
||||
|
||||
@@ -318,36 +327,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
|
||||
private fun setupSubFormatting(playerView: PlayerView) {
|
||||
val primaryColor = when (PrefManager.getVal<Int>(PrefName.PrimaryColor)) {
|
||||
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.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 primaryColor = PrefManager.getVal<Int>(PrefName.PrimaryColor)
|
||||
|
||||
val secondaryColor = PrefManager.getVal<Int>(PrefName.SecondaryColor)
|
||||
|
||||
val outline = when (PrefManager.getVal<Int>(PrefName.Outline)) {
|
||||
0 -> EDGE_TYPE_OUTLINE // Normal
|
||||
1 -> EDGE_TYPE_DEPRESSED // Shine
|
||||
@@ -355,36 +338,11 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
3 -> EDGE_TYPE_NONE // No outline
|
||||
else -> EDGE_TYPE_OUTLINE // Normal
|
||||
}
|
||||
val subBackground = when (PrefManager.getVal<Int>(PrefName.SubBackground)) {
|
||||
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 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 subBackground = PrefManager.getVal<Int>(PrefName.SubBackground)
|
||||
|
||||
val subWindow = PrefManager.getVal<Int>(PrefName.SubWindow)
|
||||
|
||||
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)
|
||||
@@ -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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -467,6 +472,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
skipTimeButton = playerView.findViewById(R.id.exo_skip_timestamp)
|
||||
skipTimeText = skipTimeButton.findViewById(R.id.exo_skip_timestamp_text)
|
||||
timeStampText = playerView.findViewById(R.id.exo_time_stamp_text)
|
||||
customSubtitleView = playerView.findViewById(R.id.customSubtitleView)
|
||||
|
||||
animeTitle = playerView.findViewById(R.id.exo_anime_title)
|
||||
episodeTitle = playerView.findViewById(R.id.exo_ep_sel)
|
||||
@@ -520,7 +526,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
setupSubFormatting(playerView)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
currentWindow = savedInstanceState.getInt(resumeWindow)
|
||||
@@ -1114,60 +1119,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
)
|
||||
initPlayer()
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
checkNotch()
|
||||
|
||||
@@ -1383,13 +1401,57 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
val ext = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return
|
||||
extractor = ext
|
||||
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")
|
||||
?: when (val subLang: String? =
|
||||
PrefManager.getNullableCustomVal("subLang_${media.id}", null, String::class.java)) {
|
||||
null -> {
|
||||
when (episode.selectedSubtitle) {
|
||||
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!!)
|
||||
}
|
||||
}
|
||||
@@ -1642,7 +1704,18 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
.build()
|
||||
|
||||
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))
|
||||
.setTrackSelector(trackSelector)
|
||||
.setLoadControl(loadControl)
|
||||
@@ -1661,6 +1734,54 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
}
|
||||
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 {
|
||||
val rightNow = Calendar.getInstance()
|
||||
mediaSession = MediaSession.Builder(this, exoPlayer)
|
||||
@@ -1950,7 +2071,10 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
TrackSelectionOverride(trackGroup.mediaTrackGroup, index)
|
||||
)
|
||||
.build()
|
||||
if (type == TRACK_TYPE_TEXT) setupSubFormatting(playerView)
|
||||
if (type == TRACK_TYPE_TEXT) {
|
||||
setupSubFormatting(playerView)
|
||||
applySubtitleStyles(customSubtitleView)
|
||||
}
|
||||
playerView.subtitleView?.alpha = when (isDisabled) {
|
||||
false -> PrefManager.getVal(PrefName.SubAlpha)
|
||||
true -> 0f
|
||||
@@ -2042,6 +2166,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL
|
||||
exoPlayer.play()
|
||||
if (episodeLength == 0f) {
|
||||
episodeLength = exoPlayer.duration.toFloat()
|
||||
discordRPC()
|
||||
}
|
||||
}
|
||||
isBuffering = playbackState == Player.STATE_BUFFERING
|
||||
|
||||
@@ -444,15 +444,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
if (subtitles.isNotEmpty()) {
|
||||
val subtitleNames = subtitles.map { it.language }
|
||||
var subtitleToDownload: Subtitle? = null
|
||||
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle(R.string.download_subtitle)
|
||||
.setSingleChoiceItems(
|
||||
subtitleNames.toTypedArray(),
|
||||
-1
|
||||
) { _, which ->
|
||||
requireActivity().customAlertDialog().apply {
|
||||
setTitle(R.string.download_subtitle)
|
||||
singleChoiceItems(subtitleNames.toTypedArray()) {which ->
|
||||
subtitleToDownload = subtitles[which]
|
||||
}
|
||||
.setPositiveButton(R.string.download) { dialog, _ ->
|
||||
setPosButton(R.string.download) {
|
||||
scope.launch {
|
||||
if (subtitleToDownload != null) {
|
||||
SubtitleDownloader.downloadSubtitle(
|
||||
@@ -466,13 +463,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
)
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
setNegButton(R.string.cancel) {}
|
||||
}.show()
|
||||
} else {
|
||||
snackString(R.string.no_subtitles_available)
|
||||
}
|
||||
@@ -576,65 +569,63 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
if (audioTracks.isNotEmpty()) {
|
||||
val audioNamesArray = audioTracks.toTypedArray()
|
||||
val checkedItems = BooleanArray(audioNamesArray.size) { false }
|
||||
val alertDialog = AlertDialog.Builder(currContext, R.style.MyPopup)
|
||||
.setTitle(R.string.download_audio_tracks)
|
||||
.setMultiChoiceItems(audioNamesArray, checkedItems) { _, which, isChecked ->
|
||||
val audioPair = Pair(extractor.audioTracks[which].url, extractor.audioTracks[which].lang)
|
||||
if (isChecked) {
|
||||
selectedAudioTracks.add(audioPair)
|
||||
} else {
|
||||
selectedAudioTracks.remove(audioPair)
|
||||
|
||||
currContext.customAlertDialog().apply{ // ToTest
|
||||
setTitle(R.string.download_audio_tracks)
|
||||
multiChoiceItems(audioNamesArray, checkedItems) {
|
||||
it.forEachIndexed { index, isChecked ->
|
||||
val audioPair = Pair(extractor.audioTracks[index].url, extractor.audioTracks[index].lang)
|
||||
if (isChecked) {
|
||||
selectedAudioTracks.add(audioPair)
|
||||
} else {
|
||||
selectedAudioTracks.remove(audioPair)
|
||||
}
|
||||
}
|
||||
}
|
||||
.setPositiveButton(R.string.download) { _, _ ->
|
||||
dialog?.dismiss()
|
||||
setPosButton(R.string.download) {
|
||||
go()
|
||||
}
|
||||
.setNegativeButton(R.string.skip) { dialog, _ ->
|
||||
setNegButton(R.string.skip) {
|
||||
selectedAudioTracks = mutableListOf()
|
||||
go()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNeutralButton(R.string.cancel) { dialog, _ ->
|
||||
setNeutralButton(R.string.cancel) {
|
||||
selectedAudioTracks = mutableListOf()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
go()
|
||||
}
|
||||
}
|
||||
if (subtitles.isNotEmpty()) {
|
||||
if (subtitles.isNotEmpty()) { // ToTest
|
||||
val subtitleNamesArray = subtitleNames.toTypedArray()
|
||||
val checkedItems = BooleanArray(subtitleNamesArray.size) { false }
|
||||
|
||||
val alertDialog = AlertDialog.Builder(currContext, R.style.MyPopup)
|
||||
.setTitle(R.string.download_subtitle)
|
||||
.setMultiChoiceItems(subtitleNamesArray, checkedItems) { _, which, isChecked ->
|
||||
val subtitlePair = Pair(subtitles[which].file.url, subtitles[which].language)
|
||||
if (isChecked) {
|
||||
selectedSubtitles.add(subtitlePair)
|
||||
} else {
|
||||
selectedSubtitles.remove(subtitlePair)
|
||||
currContext.customAlertDialog().apply {
|
||||
setTitle(R.string.download_subtitle)
|
||||
multiChoiceItems(subtitleNamesArray, checkedItems) {
|
||||
it.forEachIndexed { index, isChecked ->
|
||||
val subtitlePair = Pair(subtitles[index].file.url, subtitles[index].language)
|
||||
if (isChecked) {
|
||||
selectedSubtitles.add(subtitlePair)
|
||||
} else {
|
||||
selectedSubtitles.remove(subtitlePair)
|
||||
}
|
||||
}
|
||||
}
|
||||
.setPositiveButton(R.string.download) { _, _ ->
|
||||
dialog?.dismiss()
|
||||
setPosButton(R.string.download) {
|
||||
checkAudioTracks()
|
||||
}
|
||||
.setNegativeButton(R.string.skip) { dialog, _ ->
|
||||
setNegButton(R.string.skip) {
|
||||
selectedSubtitles = mutableListOf()
|
||||
checkAudioTracks()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNeutralButton(R.string.cancel) { dialog, _ ->
|
||||
setNeutralButton(R.string.cancel) {
|
||||
selectedSubtitles = mutableListOf()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
checkAudioTracks()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.ColorEditor.Companion.adjustColorForContrast
|
||||
import ani.dantotsu.util.ColorEditor.Companion.getContrastRatio
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
@@ -385,19 +386,14 @@ class CommentItem(
|
||||
* @param callback the callback to call when the user clicks yes
|
||||
*/
|
||||
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
|
||||
val alertDialog =
|
||||
android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton("Yes") { dialog, _ ->
|
||||
callback()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("No") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = alertDialog.show()
|
||||
dialog?.window?.setDimAmount(0.8f)
|
||||
commentsFragment.activity.customAlertDialog().apply {
|
||||
setTitle(title)
|
||||
setMessage(message)
|
||||
setPosButton("Yes") {
|
||||
callback()
|
||||
}
|
||||
setNegButton("No") {}
|
||||
}.show()
|
||||
}
|
||||
|
||||
private val usernameColors: Array<String> = arrayOf(
|
||||
|
||||
@@ -25,6 +25,7 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.comments.Comment
|
||||
import ani.dantotsu.connections.comments.CommentResponse
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
import ani.dantotsu.databinding.DialogEdittextBinding
|
||||
import ani.dantotsu.databinding.FragmentCommentsBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
@@ -34,6 +35,7 @@ import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.Section
|
||||
import io.noties.markwon.editor.MarkwonEditor
|
||||
@@ -160,35 +162,48 @@ class CommentsFragment : Fragment() {
|
||||
popup.inflate(R.menu.comments_sort_menu)
|
||||
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 {
|
||||
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
|
||||
.setTitle("Enter a chapter/episode number tag")
|
||||
.setView(R.layout.dialog_edittext)
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
val editText =
|
||||
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText)
|
||||
val text = editText?.text.toString()
|
||||
activity.customAlertDialog().apply {
|
||||
val customView = DialogEdittextBinding.inflate(layoutInflater)
|
||||
setTitle("Enter a chapter/episode number tag")
|
||||
setCustomView(customView.root)
|
||||
setPosButton("OK") {
|
||||
val text = customView.dialogEditText.text.toString()
|
||||
filterTag = text.toIntOrNull()
|
||||
lifecycleScope.launch {
|
||||
loadAndDisplayComments()
|
||||
}
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNeutralButton("Clear") { dialog, _ ->
|
||||
setNeutralButton("Clear") {
|
||||
filterTag = null
|
||||
lifecycleScope.launch {
|
||||
loadAndDisplayComments()
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
filterTag = null
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = alertDialog.show()
|
||||
dialog?.window?.setDimAmount(0.8f)
|
||||
setNegButton("Cancel") { filterTag = null }
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
var isFetching = false
|
||||
@@ -303,13 +318,12 @@ class CommentsFragment : Fragment() {
|
||||
|
||||
activity.binding.commentLabel.setOnClickListener {
|
||||
//alert dialog to enter a number, with a cancel and ok button
|
||||
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
|
||||
.setTitle("Enter a chapter/episode number tag")
|
||||
.setView(R.layout.dialog_edittext)
|
||||
.setPositiveButton("OK") { dialog, _ ->
|
||||
val editText =
|
||||
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText)
|
||||
val text = editText?.text.toString()
|
||||
activity.customAlertDialog().apply {
|
||||
val customView = DialogEdittextBinding.inflate(layoutInflater)
|
||||
setTitle("Enter a chapter/episode number tag")
|
||||
setCustomView(customView.root)
|
||||
setPosButton("OK") {
|
||||
val text = customView.dialogEditText.text.toString()
|
||||
tag = text.toIntOrNull()
|
||||
if (tag == null) {
|
||||
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
|
||||
@@ -324,28 +338,25 @@ class CommentsFragment : Fragment() {
|
||||
null
|
||||
)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNeutralButton("Clear") { dialog, _ ->
|
||||
setNeutralButton("Clear") {
|
||||
tag = null
|
||||
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
|
||||
resources,
|
||||
R.drawable.ic_label_off_24,
|
||||
null
|
||||
)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
setNegButton("Cancel") {
|
||||
tag = null
|
||||
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
|
||||
resources,
|
||||
R.drawable.ic_label_off_24,
|
||||
null
|
||||
)
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = alertDialog.show()
|
||||
dialog?.window?.setDimAmount(0.8f)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,11 +374,6 @@ class CommentsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
@@ -579,31 +585,28 @@ class CommentsFragment : Fragment() {
|
||||
* Called when the user tries to comment for the first time
|
||||
*/
|
||||
private fun showCommentRulesDialog() {
|
||||
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
|
||||
.setTitle("Commenting Rules")
|
||||
activity.customAlertDialog().apply {
|
||||
setTitle("Commenting Rules")
|
||||
.setMessage(
|
||||
"I WILL BAN YOU WITHOUT HESITATION\n" +
|
||||
"1. No racism\n" +
|
||||
"2. No hate speech\n" +
|
||||
"3. No spam\n" +
|
||||
"4. No NSFW content\n" +
|
||||
"6. ENGLISH ONLY\n" +
|
||||
"7. No self promotion\n" +
|
||||
"8. No impersonation\n" +
|
||||
"9. No harassment\n" +
|
||||
"10. No illegal content\n" +
|
||||
"11. Anything you know you shouldn't comment\n"
|
||||
"🚨 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"
|
||||
)
|
||||
.setPositiveButton("I Understand") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
setPosButton("I Understand") {
|
||||
PrefManager.setVal(PrefName.FirstComment, false)
|
||||
processComment()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
val dialog = alertDialog.show()
|
||||
dialog?.window?.setDimAmount(0.8f)
|
||||
setNegButton(R.string.cancel)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun processComment() {
|
||||
@@ -709,4 +712,4 @@ class CommentsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package ani.dantotsu.media.manga
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -11,6 +11,7 @@ import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.NumberPicker
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getString
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
@@ -19,8 +20,9 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.CustomDialogLayoutBinding
|
||||
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemMediaSourceBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.loadImage
|
||||
@@ -35,11 +37,14 @@ import ani.dantotsu.others.webview.CookieCatcher
|
||||
import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.MangaReadSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.parsers.OfflineMangaParser
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.FAQActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
@@ -55,86 +60,108 @@ class MangaReadAdapter(
|
||||
) : RecyclerView.Adapter<MangaReadAdapter.ViewHolder>() {
|
||||
|
||||
var subscribe: MediaDetailsActivity.PopImageButton? = null
|
||||
private var _binding: ItemAnimeWatchBinding? = null
|
||||
private var _binding: ItemMediaSourceBinding? = null
|
||||
val hiddenScanlators = mutableListOf<String>()
|
||||
var scanlatorSelectionListener: ScanlatorSelectionListener? = null
|
||||
var options = listOf<String>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(bind)
|
||||
private fun clearCustomValsForMedia(mediaId: String, suffix: String) {
|
||||
val customVals = PrefManager.getAllCustomValsForMedia("$mediaId$suffix")
|
||||
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) {
|
||||
val binding = holder.binding
|
||||
_binding = binding
|
||||
binding.sourceTitle.setText(R.string.chaps)
|
||||
|
||||
//Fuck u launch
|
||||
// Fuck u launch
|
||||
binding.faqbutton.setOnClickListener {
|
||||
val intent = Intent(fragment.requireContext(), FAQActivity::class.java)
|
||||
startActivity(fragment.requireContext(), intent, null)
|
||||
}
|
||||
|
||||
//Wrong Title
|
||||
binding.animeSourceSearch.setOnClickListener {
|
||||
// Wrong Title
|
||||
binding.mediaSourceSearch.setOnClickListener {
|
||||
SourceSearchDialogFragment().show(
|
||||
fragment.requireActivity().supportFragmentManager,
|
||||
null
|
||||
)
|
||||
}
|
||||
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.animeSourceSettings.isGone = offline
|
||||
binding.animeSourceSearch.isGone = offline
|
||||
binding.animeSourceTitle.isGone = offline
|
||||
//Source Selection
|
||||
binding.mediaSourceNameContainer.isGone = offline
|
||||
binding.mediaSourceSettings.isGone = offline
|
||||
binding.mediaSourceSearch.isGone = offline
|
||||
binding.mediaSourceTitle.isGone = offline
|
||||
// Source Selection
|
||||
var source =
|
||||
media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
|
||||
setLanguageList(media.selected!!.langIndex, source)
|
||||
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 {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
binding.mediaSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
|
||||
}
|
||||
}
|
||||
media.selected?.scanlators?.let {
|
||||
hiddenScanlators.addAll(it)
|
||||
}
|
||||
binding.animeSource.setAdapter(
|
||||
binding.mediaSource.setAdapter(
|
||||
ArrayAdapter(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
mangaReadSources.names
|
||||
)
|
||||
)
|
||||
binding.animeSourceTitle.isSelected = true
|
||||
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
||||
binding.mediaSourceTitle.isSelected = true
|
||||
binding.mediaSource.setOnItemClickListener { _, _, i, _ ->
|
||||
fragment.onSourceChange(i).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
binding.mediaSourceTitle.text = showUserText
|
||||
showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
|
||||
source = i
|
||||
setLanguageList(0, i)
|
||||
}
|
||||
subscribeButton(false)
|
||||
//invalidate if it's the last source
|
||||
// Invalidate if it's the last source
|
||||
val invalidate = i == mangaReadSources.names.size - 1
|
||||
fragment.loadChapters(i, invalidate)
|
||||
}
|
||||
|
||||
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
|
||||
binding.mediaSourceLanguage.setOnItemClickListener { _, _, i, _ ->
|
||||
// Check if 'extension' and 'selected' properties exist and are accessible
|
||||
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||
ext.sourceLanguage = i
|
||||
fragment.onLangChange(i, ext.saveName)
|
||||
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
binding.mediaSourceTitle.text = showUserText
|
||||
showUserTextListener =
|
||||
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
{ MainScope().launch { binding.mediaSourceTitle.text = it } }
|
||||
setLanguageList(i, source)
|
||||
}
|
||||
subscribeButton(false)
|
||||
@@ -143,17 +170,17 @@ class MangaReadAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
//settings
|
||||
binding.animeSourceSettings.setOnClickListener {
|
||||
// Settings
|
||||
binding.mediaSourceSettings.setOnClickListener {
|
||||
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
|
||||
fragment.openSettings(ext.extension)
|
||||
}
|
||||
}
|
||||
|
||||
//Grids
|
||||
// Grids
|
||||
subscribe = MediaDetailsActivity.PopImageButton(
|
||||
fragment.lifecycleScope,
|
||||
binding.animeSourceSubscribe,
|
||||
binding.mediaSourceSubscribe,
|
||||
R.drawable.ic_round_notifications_active_24,
|
||||
R.drawable.ic_round_notifications_none_24,
|
||||
R.color.bg_opp,
|
||||
@@ -161,206 +188,207 @@ class MangaReadAdapter(
|
||||
fragment.subscribed,
|
||||
true
|
||||
) {
|
||||
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
|
||||
fragment.onNotificationPressed(it, binding.mediaSource.text.toString())
|
||||
}
|
||||
|
||||
subscribeButton(false)
|
||||
|
||||
binding.animeSourceSubscribe.setOnLongClickListener {
|
||||
binding.mediaSourceSubscribe.setOnLongClickListener {
|
||||
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
|
||||
}
|
||||
|
||||
binding.animeNestedButton.setOnClickListener {
|
||||
|
||||
val dialogView =
|
||||
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
|
||||
val dialogBinding = DialogLayoutBinding.bind(dialogView)
|
||||
binding.mediaNestedButton.setOnClickListener {
|
||||
val dialogBinding = DialogLayoutBinding.inflate(fragment.layoutInflater)
|
||||
var refresh = false
|
||||
var run = false
|
||||
var reversed = media.selected!!.recyclerReversed
|
||||
var style =
|
||||
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.MangaDefaultView)
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
dialogBinding.animeSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
|
||||
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
run = true
|
||||
}
|
||||
dialogBinding.apply {
|
||||
mediaSourceTop.rotation = if (reversed) -90f else 90f
|
||||
sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
mediaSourceTop.setOnClickListener {
|
||||
reversed = !reversed
|
||||
mediaSourceTop.rotation = if (reversed) -90f else 90f
|
||||
sortText.text = if (reversed) "Down to Up" else "Up to Down"
|
||||
run = true
|
||||
}
|
||||
|
||||
//Grids
|
||||
dialogBinding.animeSourceGrid.visibility = View.GONE
|
||||
var selected = when (style) {
|
||||
0 -> dialogBinding.animeSourceList
|
||||
1 -> dialogBinding.animeSourceCompact
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
when (style) {
|
||||
0 -> dialogBinding.layoutText.setText(R.string.list)
|
||||
1 -> dialogBinding.layoutText.setText(R.string.compact)
|
||||
else -> dialogBinding.animeSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
fun selected(it: ImageButton) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
// Grids
|
||||
mediaSourceGrid.visibility = View.GONE
|
||||
var selected = when (style) {
|
||||
0 -> mediaSourceList
|
||||
1 -> mediaSourceCompact
|
||||
else -> mediaSourceList
|
||||
}
|
||||
when (style) {
|
||||
0 -> layoutText.setText(R.string.list)
|
||||
1 -> layoutText.setText(R.string.compact)
|
||||
else -> mediaSourceList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
}
|
||||
dialogBinding.animeSourceList.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 0
|
||||
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)
|
||||
fun selected(it: ImageButton) {
|
||||
selected.alpha = 0.33f
|
||||
selected = it
|
||||
selected.alpha = 1f
|
||||
}
|
||||
//start CookieCatcher activity
|
||||
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
|
||||
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
|
||||
val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
|
||||
val url = sourceHttp?.baseUrl
|
||||
url?.let {
|
||||
refresh = true
|
||||
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
|
||||
.putExtra("url", url)
|
||||
startActivity(fragment.requireContext(), intent, null)
|
||||
mediaSourceList.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 0
|
||||
layoutText.setText(R.string.list)
|
||||
run = true
|
||||
}
|
||||
mediaSourceCompact.setOnClickListener {
|
||||
selected(it as ImageButton)
|
||||
style = 1
|
||||
layoutText.setText(R.string.compact)
|
||||
run = true
|
||||
}
|
||||
mediaWebviewContainer.setOnClickListener {
|
||||
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
|
||||
toast(R.string.webview_not_installed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Multi download
|
||||
dialogBinding.downloadNo.text = "0"
|
||||
dialogBinding.animeDownloadTop.setOnClickListener {
|
||||
//Alert dialog asking for the number of chapters to download
|
||||
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
alertDialog.setTitle("Multi Chapter Downloader")
|
||||
alertDialog.setMessage("Enter the number of chapters to download")
|
||||
val input = NumberPicker(currContext())
|
||||
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
|
||||
// Start CookieCatcher activity
|
||||
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
|
||||
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
|
||||
val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
|
||||
val url = sourceHttp?.baseUrl
|
||||
url?.let {
|
||||
refresh = true
|
||||
val intent =
|
||||
Intent(fragment.requireContext(), CookieCatcher::class.java)
|
||||
.putExtra("url", url)
|
||||
startActivity(fragment.requireContext(), intent, null)
|
||||
}
|
||||
}
|
||||
return when {
|
||||
allChecked -> R.drawable.untick_all_boxes
|
||||
allUnchecked -> R.drawable.tick_all_boxes
|
||||
else -> R.drawable.invert_all_boxes
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamically add checkboxes
|
||||
options.forEach { option ->
|
||||
val checkBox = CheckBox(currContext()).apply {
|
||||
text = option
|
||||
setOnCheckedChangeListener { _, _ ->
|
||||
// Update image resource when you change a checkbox
|
||||
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
|
||||
// Multi download
|
||||
downloadNo.text = "0"
|
||||
mediaDownloadTop.setOnClickListener {
|
||||
// Alert dialog asking for the number of chapters to download
|
||||
fragment.requireContext().customAlertDialog().apply {
|
||||
setTitle("Multi Chapter Downloader")
|
||||
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
|
||||
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
.setView(dialogView2)
|
||||
.setPositiveButton("OK") { _, _ ->
|
||||
hiddenScanlators.clear()
|
||||
for (i in 0 until checkboxContainer.childCount) {
|
||||
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
|
||||
snackString("Deleted the progress of Chapters for ${media.nameRomaji}")
|
||||
}
|
||||
setNegButton(R.string.no)
|
||||
show()
|
||||
}
|
||||
}
|
||||
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) {
|
||||
hiddenScanlators.add(checkBox.text.toString())
|
||||
allChecked = false
|
||||
} else {
|
||||
allUnchecked = false
|
||||
}
|
||||
}
|
||||
fragment.onScanlatorChange(hiddenScanlators)
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
|
||||
// Standard image resource
|
||||
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
|
||||
|
||||
// Listens to ticked checkboxes and changes image resource accordingly
|
||||
tickAllButton.setOnClickListener {
|
||||
// Toggle checkboxes
|
||||
for (i in 0 until checkboxContainer.childCount) {
|
||||
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
|
||||
checkBox.isChecked = !checkBox.isChecked
|
||||
return when {
|
||||
allChecked -> R.drawable.untick_all_boxes
|
||||
allUnchecked -> R.drawable.tick_all_boxes
|
||||
else -> R.drawable.invert_all_boxes
|
||||
}
|
||||
}
|
||||
|
||||
// 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.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()
|
||||
}
|
||||
|
||||
@@ -368,7 +396,7 @@ class MangaReadAdapter(
|
||||
subscribe?.enabled(enabled)
|
||||
}
|
||||
|
||||
//Chips
|
||||
// Chips
|
||||
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
|
||||
val binding = _binding
|
||||
if (binding != null) {
|
||||
@@ -379,13 +407,13 @@ class MangaReadAdapter(
|
||||
val chip =
|
||||
ItemChipBinding.inflate(
|
||||
LayoutInflater.from(fragment.context),
|
||||
binding.animeSourceChipGroup,
|
||||
binding.mediaSourceChipGroup,
|
||||
false
|
||||
).root
|
||||
chip.isCheckable = true
|
||||
fun selected() {
|
||||
chip.isChecked = true
|
||||
binding.animeWatchChipScroll.smoothScrollTo(
|
||||
binding.mediaWatchChipScroll.smoothScrollTo(
|
||||
(chip.left - screenWidth / 2) + (chip.width / 2),
|
||||
0
|
||||
)
|
||||
@@ -403,7 +431,7 @@ class MangaReadAdapter(
|
||||
} else {
|
||||
names[last - 1]
|
||||
}
|
||||
//chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||
// chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
|
||||
val chipText = "$startChapterString - $endChapterString"
|
||||
chip.text = chipText
|
||||
chip.setTextColor(
|
||||
@@ -417,14 +445,14 @@ class MangaReadAdapter(
|
||||
selected()
|
||||
fragment.onChipClicked(position, limit * (position), last - 1)
|
||||
}
|
||||
binding.animeSourceChipGroup.addView(chip)
|
||||
binding.mediaSourceChipGroup.addView(chip)
|
||||
if (selected == position) {
|
||||
selected()
|
||||
select = chip
|
||||
}
|
||||
}
|
||||
if (select != null)
|
||||
binding.animeWatchChipScroll.apply {
|
||||
binding.mediaWatchChipScroll.apply {
|
||||
post {
|
||||
scrollTo(
|
||||
(select.left - screenWidth / 2) + (select.width / 2),
|
||||
@@ -436,7 +464,7 @@ class MangaReadAdapter(
|
||||
}
|
||||
|
||||
fun clearChips() {
|
||||
_binding?.animeSourceChipGroup?.removeAllViews()
|
||||
_binding?.mediaSourceChipGroup?.removeAllViews()
|
||||
}
|
||||
|
||||
fun handleChapters() {
|
||||
@@ -462,70 +490,86 @@ class MangaReadAdapter(
|
||||
}
|
||||
if (formattedChapters.contains(continueEp)) {
|
||||
continueEp = chapters[formattedChapters.indexOf(continueEp)]
|
||||
binding.animeSourceContinue.visibility = View.VISIBLE
|
||||
binding.sourceContinue.visibility = View.VISIBLE
|
||||
handleProgress(
|
||||
binding.itemEpisodeProgressCont,
|
||||
binding.itemEpisodeProgress,
|
||||
binding.itemEpisodeProgressEmpty,
|
||||
binding.itemMediaProgressCont,
|
||||
binding.itemMediaProgress,
|
||||
binding.itemMediaProgressEmpty,
|
||||
media.id,
|
||||
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)
|
||||
if (e != -1 && e + 1 < chapters.size) {
|
||||
continueEp = chapters[e + 1]
|
||||
}
|
||||
}
|
||||
val ep = media.manga.chapters!![continueEp]!!
|
||||
binding.itemEpisodeImage.loadImage(media.banner ?: media.cover)
|
||||
binding.animeSourceContinueText.text =
|
||||
binding.itemMediaImage.loadImage(media.banner ?: media.cover)
|
||||
binding.mediaSourceContinueText.text =
|
||||
currActivity()!!.getString(
|
||||
R.string.continue_chapter,
|
||||
ep.number,
|
||||
if (!ep.title.isNullOrEmpty()) ep.title else ""
|
||||
)
|
||||
binding.animeSourceContinue.setOnClickListener {
|
||||
binding.sourceContinue.setOnClickListener {
|
||||
fragment.onMangaChapterClick(continueEp)
|
||||
}
|
||||
if (fragment.continueEp) {
|
||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < 0.8f) {
|
||||
binding.animeSourceContinue.performClick()
|
||||
if ((binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams).weight < 0.8f) {
|
||||
binding.sourceContinue.performClick()
|
||||
fragment.continueEp = false
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
binding.animeSourceContinue.visibility = View.GONE
|
||||
binding.sourceContinue.visibility = View.GONE
|
||||
}
|
||||
binding.animeSourceProgressBar.visibility = View.GONE
|
||||
val sourceFound = media.manga.chapters!!.isNotEmpty()
|
||||
binding.animeSourceNotFound.isGone = sourceFound
|
||||
|
||||
binding.sourceProgressBar.visibility = View.GONE
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
binding.animeSource.setText(
|
||||
binding.animeSource.adapter
|
||||
binding.mediaSource.setText(
|
||||
binding.mediaSource.adapter
|
||||
.getItem(nextIndex).toString(), false
|
||||
)
|
||||
fragment.onSourceChange(nextIndex).apply {
|
||||
binding.animeSourceTitle.text = showUserText
|
||||
binding.mediaSourceTitle.text = showUserText
|
||||
showUserTextListener =
|
||||
{ MainScope().launch { binding.animeSourceTitle.text = it } }
|
||||
{ MainScope().launch { binding.mediaSourceTitle.text = it } }
|
||||
setLanguageList(0, nextIndex)
|
||||
}
|
||||
subscribeButton(false)
|
||||
// invalidate if it's the last source
|
||||
// Invalidate if it's the last source
|
||||
val invalidate = nextIndex == mangaReadSources.names.size - 1
|
||||
fragment.loadChapters(nextIndex, invalidate)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.animeSourceContinue.visibility = View.GONE
|
||||
binding.animeSourceNotFound.visibility = View.GONE
|
||||
binding.sourceContinue.visibility = View.GONE
|
||||
binding.sourceNotFound.visibility = View.GONE
|
||||
binding.faqbutton.visibility = View.GONE
|
||||
clearChips()
|
||||
binding.animeSourceProgressBar.visibility = View.VISIBLE
|
||||
binding.sourceProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,9 +583,9 @@ class MangaReadAdapter(
|
||||
ext.sourceLanguage = lang
|
||||
}
|
||||
try {
|
||||
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||
binding?.mediaSourceLanguage?.setText(parser.extension.sources[lang].lang)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
binding?.animeSourceLanguage?.setText(
|
||||
binding?.mediaSourceLanguage?.setText(
|
||||
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
|
||||
)
|
||||
}
|
||||
@@ -551,9 +595,9 @@ class MangaReadAdapter(
|
||||
parser.extension.sources.map { LanguageMapper.getLanguageName(it.lang) }
|
||||
)
|
||||
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
|
||||
|
||||
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
|
||||
inner class ViewHolder(val binding: ItemMediaSourceBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package ani.dantotsu.media.manga
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -31,7 +30,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.FragmentMediaSourceBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
@@ -60,6 +59,7 @@ import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
|
||||
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
@@ -74,7 +74,7 @@ import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
private var _binding: FragmentAnimeWatchBinding? = null
|
||||
private var _binding: FragmentMediaSourceBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val model: MediaDetailsViewModel by activityViewModels()
|
||||
|
||||
@@ -101,7 +101,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentAnimeWatchBinding.inflate(inflater, container, false)
|
||||
_binding = FragmentMediaSourceBinding.inflate(inflater, container, false)
|
||||
return _binding?.root
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
|
||||
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
|
||||
binding.mediaSourceRecycler.updatePadding(bottom = binding.mediaSourceRecycler.paddingBottom + navBarHeight)
|
||||
screenWidth = resources.displayMetrics.widthPixels.dp
|
||||
|
||||
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.animeSourceRecycler.scrollToPosition(10)
|
||||
binding.animeSourceRecycler.smoothScrollToPosition(0)
|
||||
binding.mediaSourceRecycler.scrollToPosition(10)
|
||||
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) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
@@ -164,7 +164,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
}
|
||||
})
|
||||
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||
if (it) binding.animeSourceRecycler.scrollToPosition(0)
|
||||
if (it) binding.mediaSourceRecycler.scrollToPosition(0)
|
||||
}
|
||||
|
||||
continueEp = model.continueMedia ?: false
|
||||
@@ -199,7 +199,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
}
|
||||
}
|
||||
|
||||
binding.animeSourceRecycler.adapter =
|
||||
binding.mediaSourceRecycler.adapter =
|
||||
ConcatAdapter(headerAdapter, chapterAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
@@ -214,8 +214,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
reload()
|
||||
}
|
||||
} else {
|
||||
binding.animeNotSupported.visibility = View.VISIBLE
|
||||
binding.animeNotSupported.text =
|
||||
binding.mediaNotSupported.visibility = View.VISIBLE
|
||||
binding.mediaNotSupported.text =
|
||||
getString(R.string.not_supported, media.format ?: "")
|
||||
}
|
||||
}
|
||||
@@ -231,10 +231,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
}
|
||||
|
||||
fun multiDownload(n: Int) {
|
||||
//get last viewed chapter
|
||||
// Get last viewed chapter
|
||||
val selected = media.userProgress
|
||||
val chapters = media.manga?.chapters?.values?.toList()
|
||||
//filter by selected language
|
||||
// Filter by selected language
|
||||
val progressChapterIndex = (chapters?.indexOfFirst {
|
||||
MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
|
||||
} ?: 0) + 1
|
||||
@@ -244,7 +244,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
// Calculate the end index
|
||||
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)
|
||||
|
||||
|
||||
@@ -386,32 +386,30 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
if (allSettings.size > 1) {
|
||||
val names =
|
||||
allSettings.map { LanguageMapper.getLanguageName(it.lang) }.toTypedArray()
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, -1) { dialog, which ->
|
||||
requireContext().customAlertDialog().apply {
|
||||
setTitle("Select a Source")
|
||||
singleChoiceItems(names) { which ->
|
||||
selectedSetting = allSettings[which]
|
||||
itemSelected = true
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
val fragment =
|
||||
MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
changeUIVisibility(true)
|
||||
loadChapters(media.selected!!.sourceIndex, true)
|
||||
}
|
||||
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
changeUIVisibility(true)
|
||||
loadChapters(media.selected!!.sourceIndex, true)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.setOnDismissListener {
|
||||
onDismiss{
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
show()
|
||||
|
||||
}
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
@@ -584,7 +582,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
private fun reload() {
|
||||
val selected = model.loadSelected(media)
|
||||
|
||||
//Find latest chapter for subscription
|
||||
// Find latest chapter for subscription
|
||||
selected.latest =
|
||||
media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
|
||||
selected.latest =
|
||||
@@ -618,14 +616,14 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.mediaInfoProgressBar.visibility = progress
|
||||
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
|
||||
binding.mediaSourceRecycler.layoutManager?.onRestoreInstanceState(state)
|
||||
|
||||
requireActivity().setNavigationTheme()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||
state = binding.mediaSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -58,6 +58,8 @@ import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsViewModel
|
||||
import ani.dantotsu.media.MediaNameAdapter
|
||||
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.MangaChapter
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
@@ -83,6 +85,7 @@ import ani.dantotsu.showSystemBarsRetractView
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.tryWith
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.alexvasilkov.gestures.views.GestureFrameLayout
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
@@ -184,6 +187,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
|
||||
|
||||
defaultSettings = loadReaderSettings("reader_settings") ?: defaultSettings
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
@@ -258,7 +263,16 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
else model.getMedia().value ?: return
|
||||
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(
|
||||
defaultSettings
|
||||
)
|
||||
@@ -400,7 +414,8 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
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) {
|
||||
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) {
|
||||
@@ -1013,28 +1028,27 @@ class MangaReaderActivity : AppCompatActivity() {
|
||||
PrefManager.setCustomVal("${media.id}_progressDialog", !isChecked)
|
||||
showProgressDialog = !isChecked
|
||||
}
|
||||
AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle(getString(R.string.title_update_progress))
|
||||
.setView(dialogView)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
|
||||
customAlertDialog().apply {
|
||||
setTitle(R.string.title_update_progress)
|
||||
setCustomView(dialogView)
|
||||
setCancelable(false)
|
||||
setPosButton(R.string.yes) {
|
||||
PrefManager.setCustomVal("${media.id}_save_progress", true)
|
||||
updateProgress(
|
||||
media,
|
||||
MediaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
|
||||
.toString()
|
||||
)
|
||||
dialog.dismiss()
|
||||
runnable.run()
|
||||
}
|
||||
.setNegativeButton(getString(R.string.no)) { dialog, _ ->
|
||||
setNegButton(R.string.no) {
|
||||
PrefManager.setCustomVal("${media.id}_save_progress", false)
|
||||
dialog.dismiss()
|
||||
runnable.run()
|
||||
}
|
||||
.setOnCancelListener { hideSystemBars() }
|
||||
.create()
|
||||
.show()
|
||||
setOnCancelListener { hideSystemBars() }
|
||||
show()
|
||||
|
||||
}
|
||||
} else {
|
||||
if (!incognito && PrefManager.getCustomVal(
|
||||
"${media.id}_save_progress",
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.widget.FrameLayout
|
||||
import kotlin.math.abs
|
||||
|
||||
class Swipy @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
@@ -16,7 +17,6 @@ class Swipy @JvmOverloads constructor(
|
||||
var dragDivider: Int = 5
|
||||
var vertical = true
|
||||
|
||||
//public, in case a different sub child needs to be considered
|
||||
var child: View? = getChildAt(0)
|
||||
|
||||
var topBeingSwiped: ((Float) -> Unit) = {}
|
||||
@@ -29,49 +29,47 @@ class Swipy @JvmOverloads constructor(
|
||||
var rightBeingSwiped: ((Float) -> Unit) = {}
|
||||
|
||||
companion object {
|
||||
private const val DRAG_RATE = .5f
|
||||
private const val DRAG_RATE = 0.5f
|
||||
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 isBeingDragged = false
|
||||
private var initialDown = 0f
|
||||
private var initialMotion = 0f
|
||||
|
||||
enum class VerticalPosition {
|
||||
Top,
|
||||
None,
|
||||
Bottom
|
||||
}
|
||||
|
||||
enum class HorizontalPosition {
|
||||
Left,
|
||||
None,
|
||||
Right
|
||||
}
|
||||
private enum class VerticalPosition { Top, None, Bottom }
|
||||
private enum class HorizontalPosition { Left, None, Right }
|
||||
|
||||
private var horizontalPos = HorizontalPosition.None
|
||||
private var verticalPos = VerticalPosition.None
|
||||
|
||||
private fun setChildPosition() {
|
||||
child?.apply {
|
||||
child?.let {
|
||||
if (vertical) {
|
||||
verticalPos = VerticalPosition.None
|
||||
if (!canScrollVertically(1)) {
|
||||
verticalPos = VerticalPosition.Bottom
|
||||
}
|
||||
if (!canScrollVertically(-1)) {
|
||||
verticalPos = VerticalPosition.Top
|
||||
verticalPos = when {
|
||||
!it.canScrollVertically(1) && !it.canScrollVertically(-1) -> {
|
||||
if (initialDown > (Resources.getSystem().displayMetrics.heightPixels / 2))
|
||||
VerticalPosition.Bottom
|
||||
else
|
||||
VerticalPosition.Top
|
||||
}
|
||||
!it.canScrollVertically(1) -> VerticalPosition.Bottom
|
||||
!it.canScrollVertically(-1) -> VerticalPosition.Top
|
||||
else -> VerticalPosition.None
|
||||
}
|
||||
} else {
|
||||
horizontalPos = HorizontalPosition.None
|
||||
if (!canScrollHorizontally(1)) {
|
||||
horizontalPos = HorizontalPosition.Right
|
||||
}
|
||||
if (!canScrollHorizontally(-1)) {
|
||||
horizontalPos = HorizontalPosition.Left
|
||||
horizontalPos = when {
|
||||
!it.canScrollHorizontally(1) && !it.canScrollHorizontally(-1) -> {
|
||||
if (initialDown > (Resources.getSystem().displayMetrics.widthPixels / 2))
|
||||
HorizontalPosition.Right
|
||||
else
|
||||
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) {
|
||||
val pointerIndex = ev.actionIndex
|
||||
val pointerId = ev.getPointerId(pointerIndex)
|
||||
if (pointerId == activePointerId) {
|
||||
val newPointerIndex = if (pointerIndex == 0) 1 else 0
|
||||
activePointerId = ev.getPointerId(newPointerIndex)
|
||||
if (ev.getPointerId(pointerIndex) == activePointerId) {
|
||||
activePointerId = ev.getPointerId(if (pointerIndex == 0) 1 else 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
||||
val action = ev.actionMasked
|
||||
val pointerIndex: Int
|
||||
if (!isEnabled || canChildScroll()) {
|
||||
return false
|
||||
}
|
||||
if (!isEnabled || canChildScroll()) return false
|
||||
|
||||
when (action) {
|
||||
when (ev.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
activePointerId = ev.getPointerId(0)
|
||||
initialDown = if (vertical) ev.getY(0) else ev.getX(0)
|
||||
isBeingDragged = false
|
||||
pointerIndex = ev.findPointerIndex(activePointerId)
|
||||
if (pointerIndex < 0) {
|
||||
return false
|
||||
}
|
||||
initialDown = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (activePointerId == INVALID_POINTER) {
|
||||
//("Got ACTION_MOVE event but don't have an active pointer id.")
|
||||
return false
|
||||
val pointerIndex = ev.findPointerIndex(activePointerId)
|
||||
if (pointerIndex >= 0) {
|
||||
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_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
isBeingDragged = false
|
||||
@@ -134,127 +114,97 @@ class Swipy @JvmOverloads constructor(
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(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 -> {
|
||||
activePointerId = ev.getPointerId(0)
|
||||
isBeingDragged = false
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
pointerIndex = ev.findPointerIndex(activePointerId)
|
||||
if (pointerIndex < 0) {
|
||||
//("Got ACTION_MOVE event but have an invalid active pointer id.")
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
if (pointerIndex >= 0) {
|
||||
val pos = if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex)
|
||||
startDragging(pos)
|
||||
if (isBeingDragged) handleDrag(pos)
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
pointerIndex = ev.actionIndex
|
||||
if (pointerIndex < 0) {
|
||||
//("Got ACTION_POINTER_DOWN event but have an invalid action index.")
|
||||
return false
|
||||
}
|
||||
activePointerId = ev.getPointerId(pointerIndex)
|
||||
if (pointerIndex >= 0) activePointerId = ev.getPointerId(pointerIndex)
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (vertical) {
|
||||
topBeingSwiped.invoke(0f)
|
||||
bottomBeingSwiped.invoke(0f)
|
||||
} else {
|
||||
rightBeingSwiped.invoke(0f)
|
||||
leftBeingSwiped.invoke(0f)
|
||||
}
|
||||
resetSwipes()
|
||||
pointerIndex = ev.findPointerIndex(activePointerId)
|
||||
if (pointerIndex < 0) {
|
||||
//("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)
|
||||
}
|
||||
if (pointerIndex >= 0) finishSpinner(if (vertical) ev.getY(pointerIndex) else ev.getX(pointerIndex))
|
||||
activePointerId = INVALID_POINTER
|
||||
return false
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun startDragging(pos: Float) {
|
||||
val posDiff =
|
||||
if ((vertical && verticalPos == VerticalPosition.Top) || (!vertical && horizontalPos == HorizontalPosition.Left))
|
||||
pos - initialDown
|
||||
else
|
||||
initialDown - pos
|
||||
val posDiff = if ((vertical && verticalPos == VerticalPosition.Top) || (!vertical && horizontalPos == HorizontalPosition.Left))
|
||||
pos - initialDown
|
||||
else
|
||||
initialDown - pos
|
||||
if (posDiff > touchSlop && !isBeingDragged) {
|
||||
initialMotion = initialDown + touchSlop
|
||||
isBeingDragged = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishSpinner(overscrollDistance: Float) {
|
||||
|
||||
private fun handleDrag(pos: Float) {
|
||||
val overscroll = abs((pos - initialMotion) * DRAG_RATE)
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
if (vertical) {
|
||||
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)
|
||||
onTopSwiped.invoke()
|
||||
else
|
||||
onBottomSwiped.invoke()
|
||||
}
|
||||
} else {
|
||||
val totalDragDistance = Resources.getSystem().displayMetrics.widthPixels / dragDivider
|
||||
if (overscrollDistance > totalDragDistance)
|
||||
val swipeDistance = abs(overscrollDistance - initialMotion)
|
||||
if (swipeDistance > totalDragDistance) {
|
||||
if (horizontalPos == HorizontalPosition.Left)
|
||||
onLeftSwiped.invoke()
|
||||
else
|
||||
onRightSwiped.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,16 +50,16 @@ class NovelReadAdapter(
|
||||
val source =
|
||||
media.selected!!.sourceIndex.let { if (it >= novelReadSources.names.size) 0 else it }
|
||||
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(
|
||||
fragment.requireContext(),
|
||||
R.layout.item_dropdown,
|
||||
novelReadSources.names
|
||||
)
|
||||
)
|
||||
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
|
||||
binding.mediaSource.setOnItemClickListener { _, _, i, _ ->
|
||||
fragment.onSourceChange(i)
|
||||
search()
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.FragmentMediaSourceBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.novel.NovelDownloaderService
|
||||
@@ -47,7 +47,7 @@ class NovelReadFragment : Fragment(),
|
||||
DownloadTriggerCallback,
|
||||
DownloadedCheckCallback {
|
||||
|
||||
private var _binding: FragmentAnimeWatchBinding? = null
|
||||
private var _binding: FragmentMediaSourceBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private val model: MediaDetailsViewModel by activityViewModels()
|
||||
|
||||
@@ -214,11 +214,11 @@ class NovelReadFragment : Fragment(),
|
||||
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) {
|
||||
if (it) binding.animeSourceRecycler.scrollToPosition(0)
|
||||
if (it) binding.mediaSourceRecycler.scrollToPosition(0)
|
||||
}
|
||||
|
||||
continueEp = model.continueMedia ?: false
|
||||
@@ -237,7 +237,7 @@ class NovelReadFragment : Fragment(),
|
||||
this,
|
||||
this
|
||||
) // probably a better way to do this but it works
|
||||
binding.animeSourceRecycler.adapter =
|
||||
binding.mediaSourceRecycler.adapter =
|
||||
ConcatAdapter(headerAdapter, novelResponseAdapter)
|
||||
loaded = true
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
@@ -290,7 +290,7 @@ class NovelReadFragment : Fragment(),
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = FragmentAnimeWatchBinding.inflate(inflater, container, false)
|
||||
_binding = FragmentMediaSourceBinding.inflate(inflater, container, false)
|
||||
return _binding?.root
|
||||
}
|
||||
|
||||
@@ -304,12 +304,12 @@ class NovelReadFragment : Fragment(),
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.mediaInfoProgressBar.visibility = progress
|
||||
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
|
||||
binding.mediaSourceRecycler.layoutManager?.onRestoreInstanceState(state)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||
state = binding.mediaSourceRecycler.layoutManager?.onSaveInstanceState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -13,6 +13,7 @@ import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
|
||||
@@ -38,7 +39,7 @@ class NovelResponseAdapter(
|
||||
val binding = holder.binding
|
||||
val novel = list[position]
|
||||
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)
|
||||
binding.itemEpisodeTitle.text = novel.name
|
||||
@@ -93,27 +94,22 @@ class NovelResponseAdapter(
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener {
|
||||
val builder = androidx.appcompat.app.AlertDialog.Builder(
|
||||
fragment.requireContext(),
|
||||
R.style.MyPopup
|
||||
)
|
||||
builder.setTitle("Delete ${novel.name}?")
|
||||
builder.setMessage("Are you sure you want to delete ${novel.name}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadedCheckCallback.deleteDownload(novel)
|
||||
deleteDownload(novel.link)
|
||||
snackString("Deleted ${novel.name}")
|
||||
if (binding.itemEpisodeFiller.text.toString()
|
||||
.contains("Download", ignoreCase = true)
|
||||
) {
|
||||
binding.itemEpisodeFiller.text = ""
|
||||
it.context.customAlertDialog().apply {
|
||||
setTitle("Delete ${novel.name}?")
|
||||
setMessage("Are you sure you want to delete ${novel.name}?")
|
||||
setPosButton(R.string.yes) {
|
||||
downloadedCheckCallback.deleteDownload(novel)
|
||||
deleteDownload(novel.link)
|
||||
snackString("Deleted ${novel.name}")
|
||||
if (binding.itemEpisodeFiller.text.toString()
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package ani.dantotsu.media.novel.novelreader
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
@@ -45,12 +46,17 @@ import ani.dantotsu.tryWith
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.vipulog.ebookreader.Book
|
||||
import com.vipulog.ebookreader.EbookReaderEventListener
|
||||
import com.vipulog.ebookreader.EbookReaderView
|
||||
import com.vipulog.ebookreader.ReaderError
|
||||
import com.vipulog.ebookreader.ReaderFlow
|
||||
import com.vipulog.ebookreader.ReaderTheme
|
||||
import com.vipulog.ebookreader.RelocationInfo
|
||||
import com.vipulog.ebookreader.TocItem
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -190,6 +196,8 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun setupViews() {
|
||||
binding.bookReader.useSafeScope(this)
|
||||
|
||||
scope.launch { binding.bookReader.openBook(intent.data!!) }
|
||||
binding.bookReader.setEbookReaderListener(this)
|
||||
|
||||
@@ -540,4 +548,42 @@ class NovelReaderActivity : AppCompatActivity(), EbookReaderEventListener {
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ class ListActivity : AppCompatActivity() {
|
||||
|
||||
window.statusBarColor = primaryColor
|
||||
window.navigationBarColor = primaryColor
|
||||
binding.listed.visibility = View.GONE
|
||||
binding.listTabLayout.setBackgroundColor(primaryColor)
|
||||
binding.listAppBar.setBackgroundColor(primaryColor)
|
||||
binding.listTitle.setTextColor(primaryTextColor)
|
||||
|
||||
@@ -20,21 +20,18 @@ class AlarmManagerScheduler(private val context: Context) : TaskScheduler {
|
||||
return
|
||||
}
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = when (taskType) {
|
||||
TaskType.COMMENT_NOTIFICATION -> Intent(
|
||||
context,
|
||||
CommentNotificationReceiver::class.java
|
||||
)
|
||||
|
||||
TaskType.ANILIST_NOTIFICATION -> Intent(
|
||||
context,
|
||||
AnilistNotificationReceiver::class.java
|
||||
)
|
||||
val intent = when {
|
||||
taskType == TaskType.COMMENT_NOTIFICATION && PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1 ->
|
||||
Intent(context, CommentNotificationReceiver::class.java)
|
||||
|
||||
TaskType.SUBSCRIPTION_NOTIFICATION -> Intent(
|
||||
context,
|
||||
SubscriptionNotificationReceiver::class.java
|
||||
)
|
||||
taskType == TaskType.ANILIST_NOTIFICATION ->
|
||||
Intent(context, AnilistNotificationReceiver::class.java)
|
||||
|
||||
taskType == TaskType.SUBSCRIPTION_NOTIFICATION ->
|
||||
Intent(context, SubscriptionNotificationReceiver::class.java)
|
||||
|
||||
else -> return
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
@@ -64,21 +61,18 @@ class AlarmManagerScheduler(private val context: Context) : TaskScheduler {
|
||||
|
||||
override fun cancelTask(taskType: TaskType) {
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
val intent = when (taskType) {
|
||||
TaskType.COMMENT_NOTIFICATION -> Intent(
|
||||
context,
|
||||
CommentNotificationReceiver::class.java
|
||||
)
|
||||
|
||||
TaskType.ANILIST_NOTIFICATION -> Intent(
|
||||
context,
|
||||
AnilistNotificationReceiver::class.java
|
||||
)
|
||||
val intent = when {
|
||||
taskType == TaskType.COMMENT_NOTIFICATION && PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1 ->
|
||||
Intent(context, CommentNotificationReceiver::class.java)
|
||||
|
||||
TaskType.SUBSCRIPTION_NOTIFICATION -> Intent(
|
||||
context,
|
||||
SubscriptionNotificationReceiver::class.java
|
||||
)
|
||||
taskType == TaskType.ANILIST_NOTIFICATION ->
|
||||
Intent(context, AnilistNotificationReceiver::class.java)
|
||||
|
||||
taskType == TaskType.SUBSCRIPTION_NOTIFICATION ->
|
||||
Intent(context, SubscriptionNotificationReceiver::class.java)
|
||||
|
||||
else -> return
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getBroadcast(
|
||||
|
||||
32
app/src/main/java/ani/dantotsu/others/AlignTagHandler.kt
Normal file
32
app/src/main/java/ani/dantotsu/others/AlignTagHandler.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
46
app/src/main/java/ani/dantotsu/others/Anify.kt
Normal file
46
app/src/main/java/ani/dantotsu/others/Anify.kt
Normal 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
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.updateLayoutParams
|
||||
@@ -24,7 +25,10 @@ class CrashActivity : AppCompatActivity() {
|
||||
ThemeManager(this).applyTheme()
|
||||
initActivity(this)
|
||||
binding = ActivityCrashBinding.inflate(layoutInflater)
|
||||
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
setContentView(binding.root)
|
||||
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
|
||||
@@ -18,14 +18,6 @@ object Kitsu {
|
||||
val headers = mapOf(
|
||||
"Content-Type" 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 res = client.post(
|
||||
@@ -152,4 +144,4 @@ query {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
144
app/src/main/java/ani/dantotsu/others/Xubtitle.kt
Normal file
144
app/src/main/java/ani/dantotsu/others/Xubtitle.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package ani.dantotsu.others.calc
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -16,6 +20,8 @@ import ani.dantotsu.databinding.ActivityCalcBinding
|
||||
import ani.dantotsu.getThemeColor
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
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.NumberConverter.Companion.toBinary
|
||||
@@ -24,7 +30,13 @@ import ani.dantotsu.util.NumberConverter.Companion.toHex
|
||||
class CalcActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityCalcBinding
|
||||
private lateinit var code: String
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val runnable = Runnable {
|
||||
success()
|
||||
}
|
||||
private val stack = CalcStack()
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
@@ -73,6 +85,29 @@ class CalcActivity : AppCompatActivity() {
|
||||
binding.displayHex.text = ""
|
||||
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 {
|
||||
stack.remove()
|
||||
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() {
|
||||
hasPermission = true
|
||||
ContextCompat.startActivity(
|
||||
|
||||
@@ -39,7 +39,7 @@ object AnimeSources : WatchSources() {
|
||||
}
|
||||
|
||||
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 = sortPinnedAnimeSources(list, pinnedAnimeSources) + Lazier(
|
||||
{ OfflineAnimeParser() },
|
||||
|
||||
@@ -348,9 +348,6 @@ class DynamicMangaParser(extension: MangaExtension.Installed) : MangaParser() {
|
||||
val res = source.getChapterList(sManga)
|
||||
val reversedRes = res.reversed()
|
||||
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
|
||||
} catch (e: Exception) {
|
||||
Logger.log("loadChapters Exception: $e")
|
||||
|
||||
@@ -55,13 +55,11 @@ class OfflineAnimeParser : AnimeParser() {
|
||||
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(
|
||||
|
||||
@@ -43,11 +43,10 @@ class OfflineMangaParser : MangaParser() {
|
||||
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> {
|
||||
@@ -66,17 +65,16 @@ class OfflineMangaParser : MangaParser() {
|
||||
for (image in images) {
|
||||
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> {
|
||||
|
||||
@@ -159,7 +159,7 @@ class NovelExtensionManager(private val context: Context) {
|
||||
*
|
||||
* @param pkgName The package name of the application to uninstall.
|
||||
*/
|
||||
fun uninstallExtension(pkgName: String, context: Context) {
|
||||
fun uninstallExtension(pkgName: String) {
|
||||
installer.uninstallApk(pkgName)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.databinding.ActivityProfileBinding
|
||||
import ani.dantotsu.databinding.ItemProfileAppBarBinding
|
||||
import ani.dantotsu.initActivity
|
||||
@@ -30,15 +31,14 @@ import ani.dantotsu.media.user.ListActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.openImage
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.profile.activity.FeedFragment
|
||||
import ani.dantotsu.profile.activity.ActivityFragment
|
||||
import ani.dantotsu.profile.activity.ActivityFragment.Companion.ActivityType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.MarkdownCreatorActivity
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -136,7 +136,7 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
|
||||
|
||||
followButton.setOnClickListener {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val res = Anilist.query.toggleFollow(user.id)
|
||||
val res = Anilist.mutation.toggleFollow(user.id)
|
||||
if (res?.data?.toggleFollow != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
snackString(R.string.success)
|
||||
@@ -153,16 +153,18 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.action_view_on_anilist -> {
|
||||
openLinkInBrowser("https://anilist.co/user/${user.name}")
|
||||
openLinkInBrowser(getString(R.string.anilist_link, user.name))
|
||||
true
|
||||
}
|
||||
R.id.action_create_new_activity -> {
|
||||
ContextCompat.startActivity(
|
||||
context,
|
||||
Intent(context, MarkdownCreatorActivity::class.java)
|
||||
.putExtra("type", "activity"),
|
||||
null
|
||||
)
|
||||
R.id.action_share_profile -> {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.type = "text/plain"
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.anilist_link, user.name))
|
||||
startActivity(Intent.createChooser(shareIntent, "Share Profile"))
|
||||
true
|
||||
}
|
||||
R.id.action_copy_user_id -> {
|
||||
copyToClipboard(user.id.toString(), true)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
@@ -177,7 +179,11 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
|
||||
user.avatar?.medium ?: ""
|
||||
)
|
||||
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(
|
||||
bannerAnimations,
|
||||
@@ -199,7 +205,8 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
|
||||
profileAppBar.addOnOffsetChangedListener(context)
|
||||
|
||||
|
||||
profileFollowerCount.text = (respond.data.followerPage?.pageInfo?.total ?: 0).toString()
|
||||
profileFollowerCount.text =
|
||||
(respond.data.followerPage?.pageInfo?.total ?: 0).toString()
|
||||
profileFollowerCountContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
context,
|
||||
@@ -209,7 +216,8 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
|
||||
null
|
||||
)
|
||||
}
|
||||
profileFollowingCount.text = (respond.data.followingPage?.pageInfo?.total ?: 0).toString()
|
||||
profileFollowingCount.text =
|
||||
(respond.data.followingPage?.pageInfo?.total ?: 0).toString()
|
||||
profileFollowingCountContainer.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
context,
|
||||
@@ -320,7 +328,7 @@ class ProfileActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListene
|
||||
override fun getItemCount(): Int = 3
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> ProfileFragment.newInstance(user)
|
||||
1 -> FeedFragment.newInstance(user.id, false, -1)
|
||||
1 -> ActivityFragment.newInstance(ActivityType.OTHER_USER, user.id)
|
||||
2 -> StatsFragment.newInstance(user)
|
||||
else -> ProfileFragment.newInstance(user)
|
||||
}
|
||||
|
||||
@@ -252,14 +252,14 @@ class StatsFragment :
|
||||
stat?.statistics?.anime?.scores?.map {
|
||||
convertScore(
|
||||
it.score,
|
||||
stat.mediaListOptions.scoreFormat
|
||||
stat.mediaListOptions.scoreFormat.toString()
|
||||
)
|
||||
} ?: emptyList()
|
||||
} else {
|
||||
stat?.statistics?.manga?.scores?.map {
|
||||
convertScore(
|
||||
it.score,
|
||||
stat.mediaListOptions.scoreFormat
|
||||
stat.mediaListOptions.scoreFormat.toString()
|
||||
)
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.buildMarkwon
|
||||
@@ -18,7 +17,7 @@ import ani.dantotsu.profile.UsersDialogFragment
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.snackString
|
||||
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.viewbinding.BindableItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -29,23 +28,16 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
class ActivityItem(
|
||||
private val activity: Activity,
|
||||
private val parentAdapter: GroupieAdapter,
|
||||
val clickCallback: (Int, type: String) -> Unit,
|
||||
private val fragActivity: FragmentActivity
|
||||
) : BindableItem<ItemActivityBinding>() {
|
||||
private lateinit var binding: ItemActivityBinding
|
||||
private lateinit var repliesAdapter: GroupieAdapter
|
||||
|
||||
override fun bind(viewBinding: ItemActivityBinding, position: Int) {
|
||||
binding = viewBinding
|
||||
val context = binding.root.context
|
||||
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
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.activityUserAvatar.loadImage(
|
||||
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 notLikeColor = ContextCompat.getColor(binding.root.context, R.color.bg_opp)
|
||||
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>()
|
||||
activity.likes?.forEach { i ->
|
||||
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 {
|
||||
UsersDialogFragment().apply {
|
||||
userList(userList)
|
||||
show(fragActivity.supportFragmentManager, "dialog")
|
||||
show((context as FragmentActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
true
|
||||
}
|
||||
binding.activityLikeCount.text = (activity.likeCount ?: 0).toString()
|
||||
binding.activityLikeContainer.setOnClickListener {
|
||||
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
scope.launch {
|
||||
val res = Anilist.query.toggleLike(activity.id, "ACTIVITY")
|
||||
val res = Anilist.mutation.toggleLike(activity.id, "ACTIVITY")
|
||||
withContext(Dispatchers.Main) {
|
||||
if (res != null) {
|
||||
|
||||
if (activity.isLiked == true) {
|
||||
activity.likeCount = activity.likeCount?.minus(1)
|
||||
} 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) {
|
||||
"ListActivity" -> {
|
||||
val cover = activity.media?.coverImage?.large
|
||||
val banner = activity.media?.bannerImage
|
||||
binding.activityContent.visibility = View.GONE
|
||||
binding.activityBannerContainer.visibility = View.VISIBLE
|
||||
binding.activityPrivate.visibility = View.GONE
|
||||
binding.activityMediaName.text = activity.media?.title?.userPreferred
|
||||
val activityText = "${activity.user!!.name} ${activity.status} ${
|
||||
activity.progress
|
||||
@@ -156,11 +125,13 @@ class ActivityItem(
|
||||
binding.activityMediaName.setOnClickListener {
|
||||
clickCallback(activity.media?.id ?: -1, "MEDIA")
|
||||
}
|
||||
binding.activityEdit.isVisible = false
|
||||
}
|
||||
|
||||
"TextActivity" -> {
|
||||
binding.activityBannerContainer.visibility = View.GONE
|
||||
binding.activityContent.visibility = View.VISIBLE
|
||||
binding.activityPrivate.visibility = View.GONE
|
||||
if (!(context as android.app.Activity).isDestroyed) {
|
||||
val markwon = buildMarkwon(context, false)
|
||||
markwon.setMarkdown(
|
||||
@@ -174,11 +145,23 @@ class ActivityItem(
|
||||
binding.activityUserName.setOnClickListener {
|
||||
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" -> {
|
||||
binding.activityBannerContainer.visibility = View.GONE
|
||||
binding.activityContent.visibility = View.VISIBLE
|
||||
binding.activityPrivate.visibility = if (activity.isPrivate == true) View.VISIBLE else View.GONE
|
||||
if (!(context as android.app.Activity).isDestroyed) {
|
||||
val markwon = buildMarkwon(context, false)
|
||||
markwon.setMarkdown(
|
||||
@@ -192,6 +175,19 @@ class ActivityItem(
|
||||
binding.activityUserName.setOnClickListener {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package ani.dantotsu.profile.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.buildMarkwon
|
||||
@@ -13,6 +15,8 @@ import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.profile.UsersDialogFragment
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.AniMarkdown.Companion.getBasicAniHTML
|
||||
import ani.dantotsu.util.ActivityMarkdownCreator
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -22,23 +26,27 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
class ActivityReplyItem(
|
||||
private val reply: ActivityReply,
|
||||
private val parentId : Int,
|
||||
private val fragActivity: FragmentActivity,
|
||||
private val parentAdapter: GroupieAdapter,
|
||||
private val clickCallback: (Int, type: String) -> Unit,
|
||||
) : BindableItem<ItemActivityReplyBinding>() {
|
||||
private lateinit var binding: ItemActivityReplyBinding
|
||||
|
||||
override fun bind(viewBinding: ItemActivityReplyBinding, position: Int) {
|
||||
binding = viewBinding
|
||||
|
||||
val context = binding.root.context
|
||||
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
binding.activityUserAvatar.loadImage(reply.user.avatar?.medium)
|
||||
binding.activityUserName.text = reply.user.name
|
||||
binding.activityTime.text = ActivityItemBuilder.getDateTime(reply.createdAt)
|
||||
binding.activityLikeCount.text = reply.likeCount.toString()
|
||||
val likeColor = ContextCompat.getColor(binding.root.context, R.color.yt_red)
|
||||
val notLikeColor = ContextCompat.getColor(binding.root.context, R.color.bg_opp)
|
||||
val likeColor = ContextCompat.getColor(context, R.color.yt_red)
|
||||
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
|
||||
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))
|
||||
|
||||
val userList = arrayListOf<User>()
|
||||
reply.likes?.forEach { i ->
|
||||
userList.add(User(i.id, i.name.toString(), i.avatar?.medium, i.bannerImage))
|
||||
@@ -51,9 +59,8 @@ class ActivityReplyItem(
|
||||
true
|
||||
}
|
||||
binding.activityLikeContainer.setOnClickListener {
|
||||
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
scope.launch {
|
||||
val res = Anilist.query.toggleLike(reply.id, "ACTIVITY_REPLY")
|
||||
val res = Anilist.mutation.toggleLike(reply.id, "ACTIVITY_REPLY")
|
||||
withContext(Dispatchers.Main) {
|
||||
if (res != null) {
|
||||
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 {
|
||||
clickCallback(reply.userId, "USER")
|
||||
|
||||
@@ -2,6 +2,7 @@ package ani.dantotsu.profile.activity
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.updateLayoutParams
|
||||
@@ -10,16 +11,20 @@ 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.databinding.ActivityFeedBinding
|
||||
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.activity.ActivityFragment.Companion.ActivityType
|
||||
import ani.dantotsu.profile.notification.NotificationActivity
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
|
||||
class FeedActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityFeedBinding
|
||||
private lateinit var binding: ActivityNotificationBinding
|
||||
private var selected: Int = 0
|
||||
lateinit var navBar: AnimatedBottomBar
|
||||
|
||||
@@ -27,28 +32,29 @@ class FeedActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
initActivity(this)
|
||||
binding = ActivityFeedBinding.inflate(layoutInflater)
|
||||
binding = ActivityNotificationBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
navBar = binding.feedNavBar
|
||||
val navBarMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) 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.notificationTitle.text = getString(R.string.activities)
|
||||
binding.notificationToolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
}
|
||||
binding.listToolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
val activityId = intent.getIntExtra("activityId", -1)
|
||||
binding.feedViewPager.adapter =
|
||||
ViewPagerAdapter(supportFragmentManager, lifecycle, activityId)
|
||||
binding.feedViewPager.setCurrentItem(selected, false)
|
||||
binding.feedViewPager.isUserInputEnabled = false
|
||||
navBar = binding.notificationNavBar
|
||||
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
val tabs = listOf(
|
||||
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.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
@@ -58,24 +64,9 @@ class FeedActivity : AppCompatActivity() {
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
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() {
|
||||
@@ -88,12 +79,12 @@ class FeedActivity : AppCompatActivity() {
|
||||
lifecycle: Lifecycle,
|
||||
private val activityId: Int
|
||||
) : FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
override fun getItemCount(): Int = 2
|
||||
override fun getItemCount(): Int = if (activityId != -1) 1 else 2
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> FeedFragment.newInstance(null, false, activityId)
|
||||
else -> FeedFragment.newInstance(null, true, -1)
|
||||
0 -> ActivityFragment.newInstance(if (activityId != -1) ActivityType.ONE else ActivityType.USER, activityId = activityId)
|
||||
else -> ActivityFragment.newInstance(ActivityType.GLOBAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ani.dantotsu.home.status
|
||||
package ani.dantotsu.profile.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
@@ -14,9 +14,8 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.api.ActivityReply
|
||||
import ani.dantotsu.databinding.BottomSheetRecyclerBinding
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.activity.ActivityReplyItem
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.MarkdownCreatorActivity
|
||||
import ani.dantotsu.util.ActivityMarkdownCreator
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -49,7 +48,7 @@ class RepliesBottomDialog : BottomSheetDialogFragment() {
|
||||
binding.replyButton.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
context,
|
||||
Intent(context, MarkdownCreatorActivity::class.java)
|
||||
Intent(context, ActivityMarkdownCreator::class.java)
|
||||
.putExtra("type", "replyActivity")
|
||||
.putExtra("parentId", activityId),
|
||||
null
|
||||
@@ -58,29 +57,30 @@ class RepliesBottomDialog : BottomSheetDialogFragment() {
|
||||
activityId = requireArguments().getInt("activityId")
|
||||
loading(true)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val response = Anilist.query.getReplies(activityId)
|
||||
withContext(Dispatchers.Main) {
|
||||
loading(false)
|
||||
if (response != null) {
|
||||
replies.clear()
|
||||
replies.addAll(response.data.page.activityReplies)
|
||||
adapter.update(
|
||||
replies.map {
|
||||
ActivityReplyItem(
|
||||
it,
|
||||
requireActivity(),
|
||||
clickCallback = { int, _ ->
|
||||
onClick(int)
|
||||
}
|
||||
)
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadData() {
|
||||
val response = Anilist.query.getReplies(activityId)
|
||||
withContext(Dispatchers.Main) {
|
||||
loading(false)
|
||||
if (response != null) {
|
||||
replies.clear()
|
||||
replies.addAll(response.data.page.activityReplies)
|
||||
adapter.update(
|
||||
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) {
|
||||
@@ -101,6 +101,14 @@ class RepliesBottomDialog : BottomSheetDialogFragment() {
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
loading(true)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(activityId: Int): RepliesBottomDialog {
|
||||
return RepliesBottomDialog().apply {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package ani.dantotsu.profile.activity
|
||||
package ani.dantotsu.profile.notification
|
||||
|
||||
import android.view.View
|
||||
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.databinding.ItemNotificationBinding
|
||||
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.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toPx
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
|
||||
class NotificationItem(
|
||||
private val notification: Notification,
|
||||
val clickCallback: (Int, Int?, NotificationClickType) -> Unit
|
||||
) : BindableItem<ItemNotificationBinding>() {
|
||||
val type: NotificationFragment.Companion.NotificationType,
|
||||
val parentAdapter: GroupieAdapter,
|
||||
val clickCallback: (Int, Int?, NotificationClickType) -> Unit,
|
||||
|
||||
) : BindableItem<ItemNotificationBinding>() {
|
||||
private lateinit var binding: ItemNotificationBinding
|
||||
override fun bind(viewBinding: ItemNotificationBinding, position: Int) {
|
||||
binding = viewBinding
|
||||
@@ -24,6 +36,48 @@ class NotificationItem(
|
||||
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 {
|
||||
return R.layout.item_notification
|
||||
}
|
||||
@@ -32,7 +86,11 @@ class NotificationItem(
|
||||
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
|
||||
?: notification.user?.avatar?.medium else notification.media?.bannerImage
|
||||
@@ -347,6 +405,14 @@ class NotificationItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.notificationCoverUser.setOnLongClickListener {
|
||||
dialog()
|
||||
true
|
||||
}
|
||||
binding.notificationBannerImage.setOnLongClickListener {
|
||||
dialog()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ActivityExtensionsBinding
|
||||
import ani.dantotsu.databinding.DialogRepositoriesBinding
|
||||
import ani.dantotsu.databinding.ItemRepositoryBinding
|
||||
@@ -35,6 +34,7 @@ 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.customAlertDialog
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
@@ -43,6 +43,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Locale
|
||||
|
||||
class ExtensionsActivity : AppCompatActivity() {
|
||||
lateinit var binding: ActivityExtensionsBinding
|
||||
@@ -173,26 +174,24 @@ class ExtensionsActivity : AppCompatActivity() {
|
||||
initActivity(this)
|
||||
binding.languageselect.setOnClickListener {
|
||||
val languageOptions =
|
||||
LanguageMapper.Companion.Language.entries.map { it.name }.toTypedArray()
|
||||
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup)
|
||||
LanguageMapper.Companion.Language.entries.map { entry ->
|
||||
entry.name.lowercase().replace("_", " ")
|
||||
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() }
|
||||
}.toTypedArray()
|
||||
val listOrder: String = PrefManager.getVal(PrefName.LangSort)
|
||||
val index = LanguageMapper.Companion.Language.entries.toTypedArray()
|
||||
.indexOfFirst { it.code == listOrder }
|
||||
builder.setTitle("Language")
|
||||
builder.setSingleChoiceItems(languageOptions, index) { dialog, i ->
|
||||
PrefManager.setVal(
|
||||
PrefName.LangSort,
|
||||
LanguageMapper.Companion.Language.entries[i].code
|
||||
)
|
||||
val currentFragment =
|
||||
supportFragmentManager.findFragmentByTag("f${viewPager.currentItem}")
|
||||
if (currentFragment is SearchQueryHandler) {
|
||||
currentFragment.notifyDataChanged()
|
||||
customAlertDialog().apply {
|
||||
setTitle("Language")
|
||||
singleChoiceItems(languageOptions, index) { selected ->
|
||||
PrefManager.setVal(PrefName.LangSort, LanguageMapper.Companion.Language.entries[selected].code)
|
||||
val currentFragment = 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> {
|
||||
topMargin = statusBarHeight
|
||||
@@ -243,10 +242,10 @@ class ExtensionsActivity : AppCompatActivity() {
|
||||
)
|
||||
view.repositoryItem.text = item.removePrefix("https://raw.githubusercontent.com")
|
||||
view.repositoryItem.setOnClickListener {
|
||||
AlertDialog.Builder(this@ExtensionsActivity, R.style.MyPopup)
|
||||
.setTitle(R.string.rem_repository)
|
||||
.setMessage(item)
|
||||
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
|
||||
customAlertDialog().apply {
|
||||
setTitle(R.string.rem_repository)
|
||||
setMessage(item)
|
||||
setPosButton(R.string.ok) {
|
||||
val repos = PrefManager.getVal<Set<String>>(prefName).minus(item)
|
||||
PrefManager.setVal(prefName, repos)
|
||||
repoInventory.removeView(view.root)
|
||||
@@ -263,13 +262,10 @@ class ExtensionsActivity : AppCompatActivity() {
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
setNegButton(R.string.cancel)
|
||||
show()
|
||||
}
|
||||
}
|
||||
view.repositoryItem.setOnLongClickListener {
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
@@ -320,20 +316,18 @@ class ExtensionsActivity : AppCompatActivity() {
|
||||
dialogView.repoInventory.apply {
|
||||
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)
|
||||
alertDialog.show()
|
||||
alertDialog.window?.setDimAmount(0.8f)
|
||||
customAlertDialog().apply {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@ class FAQActivity : AppCompatActivity() {
|
||||
currContext()?.getString(R.string.question_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(
|
||||
R.drawable.ic_anilist,
|
||||
currContext()?.getString(R.string.question_6) ?: "",
|
||||
@@ -60,6 +65,11 @@ class FAQActivity : AppCompatActivity() {
|
||||
currContext()?.getString(R.string.question_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(
|
||||
R.drawable.ic_round_lock_open_24,
|
||||
currContext()?.getString(R.string.question_9) ?: "",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.settings
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
@@ -25,13 +24,15 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
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.settings.extensionprefs.AnimeSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
@@ -42,13 +43,10 @@ import kotlinx.coroutines.launch
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
|
||||
|
||||
private var _binding: FragmentExtensionsBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var extensionsRecyclerView: RecyclerView
|
||||
@@ -72,16 +70,15 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
if (allSettings.isNotEmpty()) {
|
||||
var selectedSetting = allSettings[0]
|
||||
if (allSettings.size > 1) {
|
||||
val names = allSettings.map { LanguageMapper.getLanguageName(it.lang) }
|
||||
val names = allSettings.map { getLanguageName(it.lang) }
|
||||
.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
|
||||
requireContext().customAlertDialog().apply {
|
||||
setTitle("Select a Source")
|
||||
singleChoiceItems(names, selectedIndex) { which ->
|
||||
itemSelected = true
|
||||
selectedIndex = which
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
dialog.dismiss()
|
||||
|
||||
val fragment =
|
||||
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
|
||||
@@ -93,13 +90,13 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.setOnDismissListener {
|
||||
onDismiss {
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
val fragment =
|
||||
@@ -121,15 +118,20 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
.show()
|
||||
}
|
||||
},
|
||||
{ pkg, forceDelete ->
|
||||
if (isAdded) { // Check if the fragment is currently added to its activity
|
||||
val context = requireContext() // Store context in a variable
|
||||
{ pkg ->
|
||||
if (isAdded) {
|
||||
animeExtensionManager.uninstallExtension(pkg.pkgName)
|
||||
snackString("Extension uninstalled")
|
||||
}
|
||||
}, { pkg ->
|
||||
if (isAdded) {
|
||||
val context = requireContext()
|
||||
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)
|
||||
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep ->
|
||||
val builder = NotificationCompat.Builder(
|
||||
@@ -144,7 +146,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
},
|
||||
{ error ->
|
||||
Injekt.get<CrashlyticsInterface>().logException(error)
|
||||
Logger.log(error) // Log the error
|
||||
Logger.log(error)
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
@@ -170,14 +172,13 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
}
|
||||
)
|
||||
} else {
|
||||
animeExtensionManager.uninstallExtension(pkg.pkgName)
|
||||
snackString("Extension uninstalled")
|
||||
snackString("No update available")
|
||||
}
|
||||
|
||||
}
|
||||
}, skipIcons
|
||||
)
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -197,17 +198,10 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val newList = extensionsAdapter.currentList.toMutableList()
|
||||
val fromPosition = viewHolder.absoluteAdapterPosition
|
||||
val toPosition = target.absoluteAdapterPosition
|
||||
if (fromPosition < toPosition) { //probably need to switch to a recyclerview adapter
|
||||
for (i in fromPosition until toPosition) {
|
||||
Collections.swap(newList, i, i + 1)
|
||||
}
|
||||
} else {
|
||||
for (i in fromPosition downTo toPosition + 1) {
|
||||
Collections.swap(newList, i, i - 1)
|
||||
}
|
||||
val newList = extensionsAdapter.currentList.toMutableList().apply {
|
||||
add(toPosition, removeAt(fromPosition))
|
||||
}
|
||||
extensionsAdapter.submitList(newList)
|
||||
return true
|
||||
@@ -269,7 +263,8 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
|
||||
private class AnimeExtensionsAdapter(
|
||||
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
|
||||
) : ListAdapter<AnimeExtension.Installed, AnimeExtensionsAdapter.ViewHolder>(
|
||||
DIFF_CALLBACK_INSTALLED
|
||||
@@ -295,7 +290,7 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val extension = getItem(position)
|
||||
val nsfw = if (extension.isNsfw) "(18+)" else ""
|
||||
val lang = LanguageMapper.getLanguageName(extension.lang)
|
||||
val lang = getLanguageName(extension.lang)
|
||||
holder.extensionNameTextView.text = extension.name
|
||||
val versionText = "$lang ${extension.versionName} $nsfw"
|
||||
holder.extensionVersionTextView.text = versionText
|
||||
@@ -303,20 +298,19 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
holder.extensionIconImageView.setImageDrawable(extension.icon)
|
||||
}
|
||||
if (extension.hasUpdate) {
|
||||
holder.closeTextView.setImageResource(R.drawable.ic_round_sync_24)
|
||||
holder.updateView.isVisible = true
|
||||
} else {
|
||||
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
|
||||
holder.updateView.isVisible = false
|
||||
}
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onUninstallClicked(extension, false)
|
||||
holder.deleteView.setOnClickListener {
|
||||
onUninstallClicked(extension)
|
||||
}
|
||||
holder.updateView.setOnClickListener {
|
||||
onUpdateClicked(extension)
|
||||
}
|
||||
holder.settingsImageView.setOnClickListener {
|
||||
onSettingsClicked(extension)
|
||||
}
|
||||
holder.closeTextView.setOnLongClickListener {
|
||||
onUninstallClicked(extension, true)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun filter(query: String, currentList: List<AnimeExtension.Installed>) {
|
||||
@@ -336,7 +330,8 @@ class InstalledAnimeExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
view.findViewById(R.id.extensionVersionTextView)
|
||||
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
|
||||
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 {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package ani.dantotsu.settings
|
||||
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
@@ -28,12 +26,14 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.databinding.FragmentExtensionsBinding
|
||||
import ani.dantotsu.others.LanguageMapper
|
||||
import ani.dantotsu.others.LanguageMapper.Companion.getLanguageName
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
@@ -44,7 +44,6 @@ import kotlinx.coroutines.launch
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
@@ -74,13 +73,12 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
val names = allSettings.map { LanguageMapper.getLanguageName(it.lang) }
|
||||
.toTypedArray()
|
||||
var selectedIndex = 0
|
||||
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
|
||||
.setTitle("Select a Source")
|
||||
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
|
||||
requireContext().customAlertDialog().apply {
|
||||
setTitle("Select a Source")
|
||||
singleChoiceItems(names, selectedIndex) { which ->
|
||||
itemSelected = true
|
||||
selectedIndex = which
|
||||
selectedSetting = allSettings[selectedIndex]
|
||||
dialog.dismiss()
|
||||
|
||||
// Move the fragment transaction here
|
||||
val fragment =
|
||||
@@ -93,13 +91,13 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
.setOnDismissListener {
|
||||
onDismiss {
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
show()
|
||||
}
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
val fragment =
|
||||
@@ -120,15 +118,20 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
.show()
|
||||
}
|
||||
},
|
||||
{ pkg: MangaExtension.Installed, forceDelete: Boolean ->
|
||||
if (isAdded) { // Check if the fragment is currently added to its activity
|
||||
val context = requireContext() // Store context in a variable
|
||||
{ pkg: MangaExtension.Installed ->
|
||||
if (isAdded) {
|
||||
mangaExtensionManager.uninstallExtension(pkg.pkgName)
|
||||
snackString("Extension uninstalled")
|
||||
}
|
||||
}, { pkg ->
|
||||
if (isAdded) {
|
||||
val context = requireContext()
|
||||
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)
|
||||
.observeOn(AndroidSchedulers.mainThread()) // Observe on main thread
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep ->
|
||||
val builder = NotificationCompat.Builder(
|
||||
@@ -143,7 +146,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
},
|
||||
{ error ->
|
||||
Injekt.get<CrashlyticsInterface>().logException(error)
|
||||
Logger.log(error) // Log the error
|
||||
Logger.log(error)
|
||||
val builder = NotificationCompat.Builder(
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_ERROR
|
||||
@@ -160,7 +163,7 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
context,
|
||||
Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_check)
|
||||
.setSmallIcon(R.drawable.ic_circle_check)
|
||||
.setContentTitle("Update complete")
|
||||
.setContentText("The extension has been successfully updated.")
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
@@ -169,9 +172,9 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
}
|
||||
)
|
||||
} else {
|
||||
mangaExtensionManager.uninstallExtension(pkg.pkgName)
|
||||
snackString("Extension uninstalled")
|
||||
snackString("No update available")
|
||||
}
|
||||
|
||||
}
|
||||
}, skipIcons
|
||||
)
|
||||
@@ -195,17 +198,10 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val newList = extensionsAdapter.currentList.toMutableList()
|
||||
val fromPosition = viewHolder.absoluteAdapterPosition
|
||||
val toPosition = target.absoluteAdapterPosition
|
||||
if (fromPosition < toPosition) { //probably need to switch to a recyclerview adapter
|
||||
for (i in fromPosition until toPosition) {
|
||||
Collections.swap(newList, i, i + 1)
|
||||
}
|
||||
} else {
|
||||
for (i in fromPosition downTo toPosition + 1) {
|
||||
Collections.swap(newList, i, i - 1)
|
||||
}
|
||||
val newList = extensionsAdapter.currentList.toMutableList().apply {
|
||||
add(toPosition, removeAt(fromPosition))
|
||||
}
|
||||
extensionsAdapter.submitList(newList)
|
||||
return true
|
||||
@@ -266,7 +262,8 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
|
||||
private class MangaExtensionsAdapter(
|
||||
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
|
||||
) : ListAdapter<MangaExtension.Installed, MangaExtensionsAdapter.ViewHolder>(
|
||||
DIFF_CALLBACK_INSTALLED
|
||||
@@ -276,24 +273,23 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
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 {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_extension, parent, false)
|
||||
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) {
|
||||
val extension = getItem(position) // Use getItem() from ListAdapter
|
||||
val extension = getItem(position)
|
||||
val nsfw = if (extension.isNsfw) "(18+)" else ""
|
||||
val lang = LanguageMapper.getLanguageName(extension.lang)
|
||||
val lang = getLanguageName(extension.lang)
|
||||
holder.extensionNameTextView.text = extension.name
|
||||
val versionText = "$lang ${extension.versionName} $nsfw"
|
||||
holder.extensionVersionTextView.text = versionText
|
||||
@@ -301,12 +297,15 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
holder.extensionIconImageView.setImageDrawable(extension.icon)
|
||||
}
|
||||
if (extension.hasUpdate) {
|
||||
holder.closeTextView.setImageResource(R.drawable.ic_round_sync_24)
|
||||
holder.updateView.isVisible = true
|
||||
} else {
|
||||
holder.closeTextView.setImageResource(R.drawable.ic_round_delete_24)
|
||||
holder.updateView.isVisible = false
|
||||
}
|
||||
holder.closeTextView.setOnClickListener {
|
||||
onUninstallClicked(extension, false)
|
||||
holder.deleteView.setOnClickListener {
|
||||
onUninstallClicked(extension)
|
||||
}
|
||||
holder.updateView.setOnClickListener {
|
||||
onUpdateClicked(extension)
|
||||
}
|
||||
holder.settingsImageView.setOnClickListener {
|
||||
onSettingsClicked(extension)
|
||||
@@ -330,7 +329,8 @@ class InstalledMangaExtensionsFragment : Fragment(), SearchQueryHandler {
|
||||
view.findViewById(R.id.extensionVersionTextView)
|
||||
val settingsImageView: ImageView = view.findViewById(R.id.settingsImageView)
|
||||
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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user