mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-12 13:06:16 +00:00
Compare commits
169 Commits
v3.1.0-bet
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a93b4f5b11 | ||
|
|
69c44b7d20 | ||
|
|
a684aac0b1 | ||
|
|
6c49839f87 | ||
|
|
7053a7b4b2 | ||
|
|
1c156053d0 | ||
|
|
6fa2f11db2 | ||
|
|
a5babea27c | ||
|
|
8a9b8cca7e | ||
|
|
7479f5f43b | ||
|
|
3ac9307329 | ||
|
|
f606bef2a5 | ||
|
|
f9f9767ecc | ||
|
|
31a67c8edb | ||
|
|
7fc69b4edd | ||
|
|
9dd59bb592 | ||
|
|
a8958a76cf | ||
|
|
482e867516 | ||
|
|
495322547e | ||
|
|
19740c82f9 | ||
|
|
3abfa821c7 | ||
|
|
ef70fb5238 | ||
|
|
4e8b6b5ff4 | ||
|
|
a90b7d5203 | ||
|
|
d422a1586f | ||
|
|
986d0fa4a8 | ||
|
|
66ed167bc8 | ||
|
|
1bb5f4d0ab | ||
|
|
f6d05ec375 | ||
|
|
c48028f3cd | ||
|
|
e41ab2ddac | ||
|
|
0779c0ca71 | ||
|
|
c229a5c717 | ||
|
|
7c4689dea6 | ||
|
|
eec8605069 | ||
|
|
0d365d55c5 | ||
|
|
7b8af6ea8a | ||
|
|
38d68a7976 | ||
|
|
30d6f48d23 | ||
|
|
88feb4d811 | ||
|
|
116de6324e | ||
|
|
b3e767d33d | ||
|
|
43dee6ee49 | ||
|
|
12c13be2aa | ||
|
|
6f1bb10dec | ||
|
|
f0a8d9bfd4 | ||
|
|
01a64c25fd | ||
|
|
b04a176870 | ||
|
|
eac4604b3d | ||
|
|
1686854632 | ||
|
|
8ad8637fce | ||
|
|
6d32900568 | ||
|
|
ce332d7ae5 | ||
|
|
0d22c92e3e | ||
|
|
0057363f4d | ||
|
|
d1400ff422 | ||
|
|
f13225e032 | ||
|
|
404d265a2d | ||
|
|
c2d69509fb | ||
|
|
d01e1c89e0 | ||
|
|
ff3372754a | ||
|
|
a4bd367f98 | ||
|
|
9fa326c571 | ||
|
|
3d4f5aaf4a | ||
|
|
90c6c08b48 | ||
|
|
d1e2ca8b5e | ||
|
|
56e557738c | ||
|
|
489dcc0b52 | ||
|
|
a993935433 | ||
|
|
5c1c639f53 | ||
|
|
02ec4a3605 | ||
|
|
441094ca17 | ||
|
|
b2d7af85c0 | ||
|
|
95b558118a | ||
|
|
b703337a16 | ||
|
|
3071f88681 | ||
|
|
c242770435 | ||
|
|
0fa2cf98d8 | ||
|
|
ffd9fecf26 | ||
|
|
7ec889a915 | ||
|
|
949ab7e87b | ||
|
|
cddad8edf1 | ||
|
|
4e76e8e6e7 | ||
|
|
a9331ffa32 | ||
|
|
545abf1f9a | ||
|
|
f191502a97 | ||
|
|
652ef219dd | ||
|
|
e8ca3f2222 | ||
|
|
bd1f3388f7 | ||
|
|
c37fefde73 | ||
|
|
74e88838f0 | ||
|
|
2cf73be675 | ||
|
|
b594258d28 | ||
|
|
f9ce897197 | ||
|
|
68b6fd030f | ||
|
|
f0536b3cad | ||
|
|
6a077fa48d | ||
|
|
ebb61d94dd | ||
|
|
a7cef9323e | ||
|
|
a8f7ff2a19 | ||
|
|
0214e6611b | ||
|
|
04b9b9e7ff | ||
|
|
7366aa1bf2 | ||
|
|
09c5d9ce91 | ||
|
|
79742f415b | ||
|
|
b09f26ed34 | ||
|
|
6eb654bf51 | ||
|
|
b109d50d89 | ||
|
|
f62bdf9360 | ||
|
|
46d16be835 | ||
|
|
c22fd6b66d | ||
|
|
ae95b61298 | ||
|
|
2180086573 | ||
|
|
665b558b1f | ||
|
|
37ba9341cc | ||
|
|
feb765448b | ||
|
|
a8ccf8d246 | ||
|
|
2f06ac6071 | ||
|
|
ed24e64b78 | ||
|
|
43fc9c17f5 | ||
|
|
83e7e4591d | ||
|
|
563a96cf98 | ||
|
|
124f4eb261 | ||
|
|
0052eba828 | ||
|
|
eda213a765 | ||
|
|
899af3ee1a | ||
|
|
124c8f5ed7 | ||
|
|
1670383619 | ||
|
|
903423b842 | ||
|
|
3ae59b8d22 | ||
|
|
d488d11573 | ||
|
|
6f685a4388 | ||
|
|
b644ba1866 | ||
|
|
74cab22eca | ||
|
|
ce7ae28e1e | ||
|
|
fdc1b31c44 | ||
|
|
1b4c8704ea | ||
|
|
5473ac8238 | ||
|
|
e52ea2628a | ||
|
|
0f8218482a | ||
|
|
8822ef6805 | ||
|
|
6ce41b8fbb | ||
|
|
11655bd38d | ||
|
|
ea75197120 | ||
|
|
6878d12b5c | ||
|
|
5800dcf3e7 | ||
|
|
b30047804a | ||
|
|
0b32636c1b | ||
|
|
43e560a893 | ||
|
|
b256f02f14 | ||
|
|
46c17dced1 | ||
|
|
72fe910c59 | ||
|
|
fb65cb601e | ||
|
|
5a78d68f67 | ||
|
|
f205463a51 | ||
|
|
2de8ffd367 | ||
|
|
f3f0daf7e7 | ||
|
|
2b4c9bf7a9 | ||
|
|
21e25fe7a7 | ||
|
|
7b36cd0d29 | ||
|
|
ce488ea536 | ||
|
|
7717974b9e | ||
|
|
37949c7e8e | ||
|
|
e7a60e07d8 | ||
|
|
7bce053202 | ||
|
|
a5304477c7 | ||
|
|
945018653e | ||
|
|
5e38b00c1f | ||
|
|
dec990c24c |
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🧑💻 Dantotsu Help on Discord
|
||||
url: https://discord.com/invite/4HPZ5nAWwM
|
||||
about: Get support, ask questions, and join the community discussions.
|
||||
|
||||
- name: 📱 Dantotsu Help on Telegram
|
||||
url: https://t.me/dantotsuapp
|
||||
about: Connect with the community, ask questions, and get help directly on Telegram.
|
||||
35
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
35
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: ❓ Question
|
||||
description: Submit a question or query related to Dantotsu
|
||||
labels: [question]
|
||||
body:
|
||||
- type: textarea
|
||||
id: question-details
|
||||
attributes:
|
||||
label: Question Details
|
||||
description: Provide a detailed explanation of your question or query.
|
||||
placeholder: |
|
||||
Example:
|
||||
"How do I customize the settings in Dartotsu to optimize performance?"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: related-features
|
||||
attributes:
|
||||
label: Related Features (if applicable)
|
||||
description: Mention any specific feature or section of Dantotsu related to your question.
|
||||
placeholder: |
|
||||
Example: "Settings > Performance"
|
||||
|
||||
- type: checkboxes
|
||||
id: submission-checklist
|
||||
attributes:
|
||||
label: Submission Checklist
|
||||
description: Review the following items before submitting your question.
|
||||
options:
|
||||
- label: I have searched existing issues to see if this question has already been answered.
|
||||
required: true
|
||||
- label: I have provided a clear and concise question title.
|
||||
required: true
|
||||
- label: I have provided all relevant details to understand my question fully.
|
||||
required: true
|
||||
101
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
Normal file
101
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
name: 🐛 Issue Report
|
||||
description: Report a bug or problem in Dantotsu
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Outline the steps needed to trigger the issue.
|
||||
placeholder: |
|
||||
Example:
|
||||
1. Navigate to the home screen.
|
||||
2. Click on "Start."
|
||||
3. Observe the error message.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-outcome
|
||||
attributes:
|
||||
label: Expected Outcome
|
||||
description: Describe what you expected to happen.
|
||||
placeholder: |
|
||||
Example:
|
||||
"The application should have successfully loaded the dashboard..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-outcome
|
||||
attributes:
|
||||
label: Actual Outcome
|
||||
description: Detail what actually occurred when following the steps.
|
||||
placeholder: |
|
||||
Example:
|
||||
"The app crashed and displayed an error message instead..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: error-logs
|
||||
attributes:
|
||||
label: Error Logs (if applicable)
|
||||
description: |
|
||||
If the issue involves crashes, please attach relevant logs. Access them via **Settings → About → Log to file → Share**.
|
||||
placeholder: |
|
||||
Paste the logs here or upload as an attachment.
|
||||
|
||||
- type: input
|
||||
id: dartotsu-version
|
||||
attributes:
|
||||
label: Dartotsu Version
|
||||
description: Specify the version of Dartotsu in which the issue occurred.
|
||||
placeholder: |
|
||||
Example: "1.2.3"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: os-version
|
||||
attributes:
|
||||
label: Operating System Version
|
||||
description: Mention the OS version you are using.
|
||||
placeholder: |
|
||||
Example: "Android 12"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device-info
|
||||
attributes:
|
||||
label: Device Information
|
||||
description: Provide your device name and model.
|
||||
placeholder: |
|
||||
Example: "Samsung Galaxy S21"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional Information
|
||||
placeholder: |
|
||||
Include any other relevant details or attachments that may help diagnose the issue.
|
||||
|
||||
- type: checkboxes
|
||||
id: submission-checklist
|
||||
attributes:
|
||||
label: Submission Checklist
|
||||
description: Ensure you've reviewed these items before submitting your report.
|
||||
options:
|
||||
- label: I have searched existing issues to confirm this is not a duplicate.
|
||||
required: true
|
||||
- label: I have provided a clear and descriptive title.
|
||||
required: true
|
||||
- label: I am using the **[latest](https://github.com/rebelonion/Dantotsu/latest)** version of Dantotsu. If not, I have provided a reason for not updating.
|
||||
required: true
|
||||
- label: I have updated all relevant extensions or dependencies.
|
||||
required: true
|
||||
- label: I have filled out all the requested information accurately.
|
||||
required: true
|
||||
54
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
Normal file
54
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: 🚀 Feature Request
|
||||
description: Propose a new feature to enhance Dantotsu
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-summary
|
||||
attributes:
|
||||
label: Feature Summary
|
||||
description: Provide a concise summary of the feature you'd like to see.
|
||||
placeholder: |
|
||||
Example:
|
||||
"Add support for dark mode..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: detailed-description
|
||||
attributes:
|
||||
label: Detailed Description
|
||||
description: Elaborate on how this feature should function and its potential impact.
|
||||
placeholder: |
|
||||
Example:
|
||||
"The dark mode should automatically activate based on system settings..."
|
||||
value: |
|
||||
### Current Behavior
|
||||
- Describe the current functionality or lack thereof.
|
||||
|
||||
### Proposed Solution
|
||||
- Detail how the feature should work and any potential benefits.
|
||||
|
||||
### Considerations
|
||||
- Mention any potential challenges or alternatives.
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Provide any other information, relevant screenshots, or references.
|
||||
placeholder: "Include links to relevant resources, external tools, or related issues."
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Submission Checklist
|
||||
description: Ensure you've completed these before submitting.
|
||||
options:
|
||||
- label: I have searched the existing issues and confirm that this feature has not been requested before.
|
||||
required: true
|
||||
- label: I have provided a clear and descriptive title.
|
||||
required: true
|
||||
- label: I am using the **[latest](https://github.com/rebelonion/Dantotsu/releases/latest)** version of Dantotsu. If not, I have provided a reason for using an older version.
|
||||
required: true
|
||||
- label: I understand that not all feature requests will be accepted, and if declined, I won't resubmit the same request.
|
||||
required: true
|
||||
297
.github/workflows/beta.yml
vendored
297
.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@v3
|
||||
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["rebel onion"]="\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="#bf2cc8"
|
||||
contributor_colors["ibo"]="#ff9b46"
|
||||
contributor_colors["aayush262"]="#5d689d"
|
||||
contributor_colors["Sadwhy"]="#ff7e95"
|
||||
contributor_colors["grayankit"]="#c51aa1"
|
||||
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=$(hex_to_decimal "$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=$(hex_to_decimal "$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=$(hex_to_decimal "$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 }}
|
||||
|
||||
84
.github/workflows/bug_greetings.yml
vendored
Normal file
84
.github/workflows/bug_greetings.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: Bug Report Greeting
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Check if the issue is labeled as a Bug Report
|
||||
id: check_bug_label
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
|
||||
LABELS=$(gh issue view $ISSUE_NUMBER --json labels --jq '.labels[].name')
|
||||
|
||||
if echo "$LABELS" | grep -q 'bug'; then
|
||||
echo "This issue is labeled as a bug report. Checking if the issue creator is the repository owner."
|
||||
echo "skip_label_check=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "This issue is not labeled as a bug report. Skipping greeting message."
|
||||
echo "skip_label_check=true" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Check if the issue creator is the repo owner
|
||||
if: env.skip_label_check == 'false'
|
||||
id: check_owner
|
||||
run: |
|
||||
ISSUE_AUTHOR=$(jq -r '.issue.user.login' "$GITHUB_EVENT_PATH")
|
||||
REPO_OWNER=$(jq -r '.repository.owner.login' "$GITHUB_EVENT_PATH")
|
||||
if [ "$ISSUE_AUTHOR" = "$REPO_OWNER" ]; then
|
||||
echo "The issue creator is the repository owner. Skipping greeting message."
|
||||
echo "skip=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "The issue creator is not the repository owner. Checking for previous bug reports..."
|
||||
echo "skip=false" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Check if the user has submitted a bug report before
|
||||
if: env.skip == 'false'
|
||||
id: check_first_bug_report
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ISSUE_AUTHOR=$(jq -r '.issue.user.login' "$GITHUB_EVENT_PATH")
|
||||
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
|
||||
|
||||
# Get all issues (both open and closed) by the author except the current one
|
||||
PREVIOUS_REPORTS=$(gh issue list --author "$ISSUE_AUTHOR" --label "Bug" --state all --json number --jq '. | map(select(.number != '$ISSUE_NUMBER')) | length')
|
||||
echo "User $ISSUE_AUTHOR has submitted $PREVIOUS_REPORTS bug report(s) previously"
|
||||
|
||||
if [ "$PREVIOUS_REPORTS" -eq 0 ]; then
|
||||
echo "This is the user's first bug report. Sending greeting message."
|
||||
echo "skip_first_report=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "User has previous bug reports. Skipping greeting message."
|
||||
echo "skip_first_report=true" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Send Greeting Message
|
||||
if: env.skip_label_check == 'false' && env.skip != 'true' && env.skip_first_report != 'true'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const issueNumber = context.payload.issue.number;
|
||||
const message = `
|
||||
**🛠️ Thank you for reporting a bug!**
|
||||
Your issue has been successfully submitted and is now awaiting review. We appreciate your help in making Dantotsu better.
|
||||
**🔍 What Happens Next**
|
||||
- Our team will investigate the issue and provide updates as soon as possible.
|
||||
- You may be asked for additional details or clarification if needed.
|
||||
- Once resolved, we'll notify you of the fix or provide a workaround.
|
||||
**👥 Connect with Us**
|
||||
- **[Discord](https://discord.com/invite/4HPZ5nAWwM)**: Engage with our community and ask questions.
|
||||
- **[Telegram](https://t.me/dantotsuapp)**: Reach out for real-time discussions and updates.
|
||||
We're working hard to resolve the issue and appreciate your patience!
|
||||
`;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: message
|
||||
});
|
||||
112
.github/workflows/extension-issue-handling.yml
vendored
Normal file
112
.github/workflows/extension-issue-handling.yml
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
name: Extension Issue Handling
|
||||
on:
|
||||
issues:
|
||||
types: [opened, labeled]
|
||||
|
||||
jobs:
|
||||
handle-extension-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Issue Content
|
||||
id: check-issue
|
||||
env:
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
run: |
|
||||
# Regex patterns for extension-related issues
|
||||
EXTENSION_REGEX_PATTERNS=(
|
||||
# Extension not working (more flexible match)
|
||||
".*(\w+)\s*(extension)?\s*(not working|doesn't work|does not work|cant work|can't work).*"
|
||||
|
||||
# No extension available
|
||||
".*(no|can't find|cannot find|missing).*extension.*"
|
||||
|
||||
# No repo or repositories available
|
||||
".*(no|can't find|cannot find|missing).*repo(s)?\s*(available|found|accessible).*"
|
||||
|
||||
# Specific server/stream issues
|
||||
".*(no streams|server).*(available|working).*"
|
||||
|
||||
# Variants of extension problems
|
||||
".*{.*}.*not working.*"
|
||||
".*{.*}.*extension.*(issue|problem).*"
|
||||
)
|
||||
|
||||
# Convert to lowercase for case-insensitive matching
|
||||
LOWER_TITLE=$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]')
|
||||
LOWER_BODY=$(echo "$ISSUE_BODY" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Flag to track issue type
|
||||
IS_EXTENSION_ISSUE=false
|
||||
IS_NO_EXTENSION_ISSUE=false
|
||||
|
||||
# Check title and body against regex patterns
|
||||
for pattern in "${EXTENSION_REGEX_PATTERNS[@]}"; do
|
||||
if [[ "$LOWER_TITLE" =~ $pattern ]] || [[ "$LOWER_BODY" =~ $pattern ]]; then
|
||||
IS_EXTENSION_ISSUE=true
|
||||
|
||||
# Special check for no extensions available
|
||||
if [[ "$LOWER_TITLE" =~ "no extension" ]] || [[ "$LOWER_TITLE" =~ "can't find extension" ]]; then
|
||||
IS_NO_EXTENSION_ISSUE=true
|
||||
fi
|
||||
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Explicitly output boolean values
|
||||
if [ "$IS_EXTENSION_ISSUE" = true ]; then
|
||||
echo "is_extension_issue=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_extension_issue=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "$IS_NO_EXTENSION_ISSUE" = true ]; then
|
||||
echo "is_no_extension_issue=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_no_extension_issue=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Comment and Close Extension Issue
|
||||
if: steps.check-issue.outputs.is_extension_issue == 'true'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const issueNumber = context.issue.number;
|
||||
|
||||
// Check if it's a "No Extension" issue
|
||||
if (${{ steps.check-issue.outputs.is_no_extension_issue }}) {
|
||||
// DMCA notice message
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: "# Automated Message\n" +
|
||||
"On 13 June 2024, the official Aniyomi repository got a DMCA notice and had to remove all of their extensions. Because of this, we will not be providing anyone with any links or extensions to avoid legal problems.\n" +
|
||||
"# How to add repos?\n" +
|
||||
"Although we do not give or maintain any repositories, we support adding custom repository links to your Dantotsu. \n" +
|
||||
"Go to `Profile > Settings > Extensions` then paste your anime or manga links there.\n" +
|
||||
"# How to find repos?\n" +
|
||||
"It's very easy. Search on Google. But remember that the URL must end with <u><b>index.min.json</b></u> or else it won't work.\n" +
|
||||
"`TLDR: We will not give repo links.`"
|
||||
});
|
||||
} else {
|
||||
// Standard extension issue message
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: `Dantotsu doesn't maintain extensions.
|
||||
If the extension doesn't work we cannot help you.
|
||||
Contact the owner of Respective Repo for extension-related problems`
|
||||
});
|
||||
}
|
||||
|
||||
// Close the issue
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
state: 'closed'
|
||||
});
|
||||
86
.github/workflows/feature_greetings.yml
vendored
Normal file
86
.github/workflows/feature_greetings.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: Feature Request Greeting
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check if the issue is labeled as a Feature Request
|
||||
id: check_feature_label
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
|
||||
LABELS=$(gh issue view $ISSUE_NUMBER --json labels --jq '.labels[].name')
|
||||
|
||||
if echo "$LABELS" | grep -q 'enhancement'; then
|
||||
echo "This issue is labeled as a feature request. Checking if the issue creator is the repository owner."
|
||||
echo "skip_label_check=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "This issue is not labeled as a feature request. Skipping greeting message."
|
||||
echo "skip_label_check=true" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Check if the user has submitted a feature request before
|
||||
if: env.skip_label_check == 'false'
|
||||
id: check_first_request
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
ISSUE_AUTHOR=$(jq -r '.issue.user.login' "$GITHUB_EVENT_PATH")
|
||||
REPO_OWNER=$(jq -r '.repository.owner.login' "$GITHUB_EVENT_PATH")
|
||||
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
|
||||
|
||||
if [ "$ISSUE_AUTHOR" = "$REPO_OWNER" ]; then
|
||||
echo "The issue creator is the repository owner. Skipping greeting message."
|
||||
echo "skip_first_request=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Checking for previous feature requests..."
|
||||
# Get all issues (both open and closed) by the author except the current one
|
||||
PREVIOUS_REQUESTS=$(gh issue list --author "$ISSUE_AUTHOR" --label "New Feature" --state all --json number --jq '. | map(select(.number != '$ISSUE_NUMBER')) | length')
|
||||
echo "User $ISSUE_AUTHOR has submitted $PREVIOUS_REQUESTS feature request(s) previously"
|
||||
|
||||
if [ "$PREVIOUS_REQUESTS" -eq 0 ]; then
|
||||
echo "This is the user's first feature request. Sending greeting message."
|
||||
echo "skip_first_request=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "User has previous feature requests. Skipping greeting message."
|
||||
echo "skip_first_request=true" >> $GITHUB_ENV
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Send Greeting Message
|
||||
if: env.skip_label_check == 'false' && env.skip_first_request == 'false'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const issueNumber = context.payload.issue.number;
|
||||
const message = `
|
||||
**💡 Thank you for your feature request!**
|
||||
Your request has been successfully submitted and is now under consideration. We value your input in shaping the future of Dantotsu.
|
||||
|
||||
**📈 What to Expect Next**
|
||||
- Our team will review your request and assess its feasibility.
|
||||
- We may reach out for additional details or clarification.
|
||||
- Updates on the request will be provided, and it may be scheduled for future development.
|
||||
|
||||
**👥 Stay Connected**
|
||||
- **[Discord](https://discord.com/invite/4HPZ5nAWwM)**: Join our community to discuss ideas and stay updated.
|
||||
- **[Telegram](https://t.me/dantotsuapp)**: Connect with us directly for real-time updates.
|
||||
|
||||
We appreciate your suggestion and look forward to potentially implementing it!
|
||||
`;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: message
|
||||
});
|
||||
80
.github/workflows/pr_greetings.yml
vendored
Normal file
80
.github/workflows/pr_greetings.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: PR Greetings
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Check if the PR creator is the repo owner or Weblate
|
||||
id: check_owner
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
|
||||
REPO_OWNER=$(jq -r '.repository.owner.login' "$GITHUB_EVENT_PATH")
|
||||
|
||||
if [ "$PR_AUTHOR" = "$REPO_OWNER" ] || [ "$PR_AUTHOR" = "weblate" ]; then
|
||||
echo "The PR creator is the repository owner or Weblate. Skipping greeting message."
|
||||
echo "skip=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "The PR creator is not the repository owner or Weblate. Checking for previous PRs..."
|
||||
|
||||
# Check for both open and closed pull requests by the author
|
||||
OPEN_PRS=$(gh pr list --author "$PR_AUTHOR" --state open --json number --jq '. | length')
|
||||
CLOSED_PRS=$(gh pr list --author "$PR_AUTHOR" --state closed --json number --jq '. | length')
|
||||
TOTAL_PRS=$((OPEN_PRS + CLOSED_PRS))
|
||||
|
||||
echo "User $PR_AUTHOR has created $TOTAL_PRS pull request(s) in total"
|
||||
echo "Open PRs: $OPEN_PRS"
|
||||
echo "Closed PRs: $CLOSED_PRS"
|
||||
|
||||
if [ "$TOTAL_PRS" -eq 1 ]; then
|
||||
echo "This is the user's first pull request. Sending greeting message."
|
||||
echo "skip=false" >> $GITHUB_ENV
|
||||
else
|
||||
echo "User has previous pull requests. Skipping greeting message."
|
||||
echo "skip=true" >> $GITHUB_ENV
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Send Greeting Message
|
||||
if: env.skip != 'true'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const message = `
|
||||
**🎉 Thank you for your contribution!**
|
||||
|
||||
Your Pull Request has been successfully submitted and is now awaiting review. We truly appreciate your efforts to improve Dantotsu.
|
||||
|
||||
**👥 Connect with the Community**
|
||||
|
||||
While you're here, why not join our communities to stay engaged?
|
||||
- **[Discord](https://discord.com/invite/4HPZ5nAWwM)**: Chat with fellow developers, ask questions, and get the latest updates.
|
||||
- **[Telegram](https://t.me/dantotsuapp)**: Connect directly with us for real-time discussions and updates.
|
||||
|
||||
**📋 What to Expect Next**
|
||||
- Our team will review your pull request as soon as possible.
|
||||
- You'll receive notifications if further information or changes are needed.
|
||||
- Once approved, your changes will be merged into the main project.
|
||||
|
||||
We're excited to collaborate with you. Stay tuned for updates!
|
||||
`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: message
|
||||
});
|
||||
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
|
||||
23
README.md
23
README.md
@@ -14,7 +14,26 @@ Dantotsu is an [Anilist](https://anilist.co/) only client.
|
||||
|
||||
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
|
||||
|
||||
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=030201&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
|
||||
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff" /></a>
|
||||
|
||||
## Terms of Use
|
||||
By downloading, installing, or using this application, you agree to:
|
||||
- Use the application in compliance with all applicable laws
|
||||
- Not use the application to infringe on copyrighted content
|
||||
- Take full responsibility for any extensions you install or use
|
||||
- Understand that the developer(s) are not responsible for third-party extensions or user actions
|
||||
|
||||
This application is designed for anime tracking and legal streaming service integration. The developers do not provide, maintain, or endorse any extensions that enable access to unauthorized content.
|
||||
|
||||
## Important Notice
|
||||
This application is an anime tracking and management tool. The extension system is designed to integrate with legal streaming services like Jellyfin.
|
||||
|
||||
We do not:
|
||||
- Provide or maintain any streaming extensions
|
||||
- Host or distribute copyrighted content
|
||||
- Endorse or encourage copyright infringement
|
||||
|
||||
Users are responsible for ensuring their use of this software complies with local laws and regulations.
|
||||
|
||||
### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
|
||||
|
||||
@@ -38,4 +57,4 @@ You can come hang out with our awesome community, request new features, and repo
|
||||
|
||||
## LICENSE 📜
|
||||
|
||||
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)
|
||||
Dantotsu is licensed under the Unabandon Public License (UPL). More info can be found [here.](LICENSE.md)
|
||||
|
||||
@@ -11,15 +11,14 @@ def gitCommitHash = providers.exec {
|
||||
}.standardOutput.asText.get().trim()
|
||||
|
||||
android {
|
||||
compileSdk 34
|
||||
compileSdk 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId "ani.dantotsu"
|
||||
minSdk 21
|
||||
targetSdk 34
|
||||
versionCode((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "3.1.0"
|
||||
versionCode 300100000
|
||||
targetSdk 35
|
||||
versionName "3.2.2"
|
||||
versionCode 300200200
|
||||
signingConfig signingConfigs.debug
|
||||
|
||||
}
|
||||
@@ -101,6 +100,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 +112,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 +122,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 +134,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 +160,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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.buildMarkwon
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.decodeBase64ToString
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
@@ -37,26 +39,88 @@ import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
object AppUpdater {
|
||||
private val fallbackStableUrl: String
|
||||
get() = "aHR0cHM6Ly9hcGkuZGFudG90c3UuYXBwL3VwZGF0ZXMvc3RhYmxl".decodeBase64ToString()
|
||||
private val fallbackBetaUrl: String
|
||||
get() = "aHR0cHM6Ly9hcGkuZGFudG90c3UuYXBwL3VwZGF0ZXMvYmV0YQ==".decodeBase64ToString()
|
||||
|
||||
@Serializable
|
||||
data class FallbackResponse(
|
||||
val version: String,
|
||||
val changelog: String,
|
||||
val downloadUrl: String? = null
|
||||
)
|
||||
|
||||
private suspend fun fetchUpdateInfo(repo: String, isDebug: Boolean): Pair<String, String>? {
|
||||
return try {
|
||||
fetchFromGithub(repo, isDebug)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Github fetch failed, trying fallback: ${e.message}")
|
||||
try {
|
||||
fetchFromFallback(isDebug)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Fallback fetch failed: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchFromGithub(repo: String, isDebug: Boolean): Pair<String, String> {
|
||||
return if (isDebug) {
|
||||
val res = client.get("https://api.github.com/repos/$repo/releases")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }
|
||||
.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v", "")
|
||||
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||
} else {
|
||||
val res = client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text
|
||||
res to res.substringAfter("# ").substringBefore("\n")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchFromFallback(isDebug: Boolean): Pair<String, String> {
|
||||
val url = if (isDebug) fallbackBetaUrl else fallbackStableUrl
|
||||
val response = CommentsAPI.requestBuilder().get(url).parsed<FallbackResponse>()
|
||||
return response.changelog to response.version
|
||||
}
|
||||
|
||||
private suspend fun fetchApkUrl(repo: String, version: String, isDebug: Boolean): String? {
|
||||
return try {
|
||||
fetchApkUrlFromGithub(repo, version)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Github APK fetch failed, trying fallback: ${e.message}")
|
||||
try {
|
||||
fetchApkUrlFromFallback(version, isDebug)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Fallback APK fetch failed: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchApkUrlFromGithub(repo: String, version: String): String? {
|
||||
val apks = client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<GithubResponse>().assets?.filter {
|
||||
it.browserDownloadURL.endsWith(".apk")
|
||||
}
|
||||
return apks?.firstOrNull()?.browserDownloadURL
|
||||
}
|
||||
|
||||
private suspend fun fetchApkUrlFromFallback(version: String, isDebug: Boolean): String? {
|
||||
val url = if (isDebug) fallbackBetaUrl else fallbackStableUrl
|
||||
return CommentsAPI.requestBuilder().get("$url/$version").parsed<FallbackResponse>().downloadUrl
|
||||
}
|
||||
|
||||
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
|
||||
if (post) snackString(currContext()?.getString(R.string.checking_for_update))
|
||||
val repo = activity.getString(R.string.repo)
|
||||
tryWithSuspend {
|
||||
val (md, version) = if (BuildConfig.DEBUG) {
|
||||
val res = client.get("https://api.github.com/repos/$repo/releases")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }
|
||||
.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v", "")
|
||||
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||
} else {
|
||||
val res =
|
||||
client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text
|
||||
res to res.substringAfter("# ").substringBefore("\n")
|
||||
}
|
||||
val (md, version) = fetchUpdateInfo(repo, BuildConfig.DEBUG) ?: return@tryWithSuspend
|
||||
|
||||
Logger.log("Git Version : $version")
|
||||
val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
|
||||
@@ -69,7 +133,11 @@ object AppUpdater {
|
||||
)
|
||||
addView(
|
||||
TextView(activity).apply {
|
||||
val markWon = buildMarkwon(activity, false)
|
||||
val markWon = try {
|
||||
buildMarkwon(activity, false)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return@runOnUiThread
|
||||
}
|
||||
markWon.setMarkdown(this, md)
|
||||
}
|
||||
)
|
||||
@@ -85,17 +153,11 @@ object AppUpdater {
|
||||
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
|
||||
MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
val apks =
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<GithubResponse>().assets?.filter {
|
||||
it.browserDownloadURL.endsWith(
|
||||
".apk"
|
||||
)
|
||||
}
|
||||
val apkToDownload = apks?.first()
|
||||
apkToDownload?.browserDownloadURL.apply {
|
||||
if (this != null) activity.downloadUpdate(version, this)
|
||||
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
val apkUrl = fetchApkUrl(repo, version, BuildConfig.DEBUG)
|
||||
if (apkUrl != null) {
|
||||
activity.downloadUpdate(version, apkUrl)
|
||||
} else {
|
||||
openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
@@ -108,8 +170,7 @@ object AppUpdater {
|
||||
}
|
||||
show(activity.supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (post) snackString(currContext()?.getString(R.string.no_update_found))
|
||||
}
|
||||
}
|
||||
@@ -140,8 +201,7 @@ object AppUpdater {
|
||||
|
||||
|
||||
//Blatantly kanged from https://github.com/LagradOst/CloudStream-3/blob/master/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
|
||||
private fun Activity.downloadUpdate(version: String, url: String): Boolean {
|
||||
|
||||
private fun Activity.downloadUpdate(version: String, url: String) {
|
||||
toast(getString(R.string.downloading_update, version))
|
||||
|
||||
val downloadManager = this.getSystemService<DownloadManager>()!!
|
||||
@@ -163,7 +223,7 @@ object AppUpdater {
|
||||
logError(e)
|
||||
-1
|
||||
}
|
||||
if (id == -1L) return true
|
||||
if (id == -1L) return
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
object : BroadcastReceiver() {
|
||||
@@ -184,7 +244,6 @@ object AppUpdater {
|
||||
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
|
||||
ContextCompat.RECEIVER_EXPORTED
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun openApk(context: Context, uri: Uri) {
|
||||
|
||||
@@ -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" />
|
||||
@@ -112,10 +113,9 @@
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/epub+zip" />
|
||||
<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 +131,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 +156,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 +196,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,24 +373,29 @@
|
||||
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:scheme="file" />
|
||||
<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" />
|
||||
<data android:host="add-repo"/>
|
||||
<data android:scheme="tachiyomi"/>
|
||||
<data android:scheme="aniyomi"/>
|
||||
<data android:scheme="novelyomi"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
|
||||
@@ -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
|
||||
@@ -120,7 +118,6 @@ import com.bumptech.glide.load.resource.gif.GifDrawable
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.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
|
||||
@@ -140,12 +137,9 @@ import io.noties.markwon.html.TagHandlerNoOp
|
||||
import io.noties.markwon.image.AsyncDrawable
|
||||
import io.noties.markwon.image.glide.GlideImagesPlugin
|
||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -154,10 +148,13 @@ 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
|
||||
import kotlin.collections.set
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.math.log2
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@@ -314,6 +311,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)
|
||||
}
|
||||
|
||||
@@ -855,6 +853,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 +873,7 @@ fun savePrefs(serialized: String, path: String, title: String, context: Context)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatMatches")
|
||||
fun savePrefs(
|
||||
serialized: String,
|
||||
path: String,
|
||||
@@ -921,6 +921,7 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
|
||||
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 {
|
||||
|
||||
@@ -1527,3 +1494,44 @@ 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
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
fun String.decodeBase64ToString(): String {
|
||||
return try {
|
||||
String(Base64.decode(this), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
""
|
||||
}
|
||||
}
|
||||
@@ -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,102 @@ 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.scheme == "novelyomi") && uri.host == "add-repo") {
|
||||
val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import")
|
||||
val (prefName, name) = when (uri.scheme) {
|
||||
"tachiyomi" -> PrefName.MangaExtensionRepos to "Manga"
|
||||
"aniyomi" -> PrefName.AnimeExtensionRepos to "Anime"
|
||||
"novelyomi" -> PrefName.NovelExtensionRepos to "Novel"
|
||||
else -> throw Exception("Invalid scheme")
|
||||
}
|
||||
val savedRepos: Set<String> = PrefManager.getVal(prefName)
|
||||
val newRepos = savedRepos.toMutableSet()
|
||||
AddRepositoryBottomSheet.addRepoWarning(this) {
|
||||
newRepos.add(url)
|
||||
PrefManager.setVal(prefName, newRepos)
|
||||
toast("$name 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 newIntent = Intent(this, this.javaClass)
|
||||
this.finish()
|
||||
startActivity(newIntent)
|
||||
}
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
} else if (name.endsWith(".ani")) {
|
||||
val decryptedJson = jsonString.toString(Charsets.UTF_8)
|
||||
if (PreferencePackager.unpack(decryptedJson)) {
|
||||
val newIntent = Intent(this, this.javaClass)
|
||||
this.finish()
|
||||
startActivity(newIntent)
|
||||
}
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,9 +26,17 @@ interface DownloadAddonApiV2 {
|
||||
statCallback: (Double) -> Unit
|
||||
): Long
|
||||
|
||||
suspend fun customFFMpeg(command: String, videoUrls: List<String>, logCallback: (String) -> Unit): Long
|
||||
suspend fun customFFMpeg(
|
||||
command: String,
|
||||
videoUrls: List<String>,
|
||||
logCallback: (String) -> Unit
|
||||
): Long
|
||||
|
||||
suspend fun customFFProbe(command: String, videoUrls: List<String>, logCallback: (String) -> Unit)
|
||||
suspend fun customFFProbe(
|
||||
command: String,
|
||||
videoUrls: List<String>,
|
||||
logCallback: (String) -> Unit
|
||||
)
|
||||
|
||||
fun getState(sessionId: Long): String
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate
|
||||
import ani.dantotsu.addons.AddonInstallReceiver
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.addons.AddonInstallReceiver
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
|
||||
@@ -41,7 +41,8 @@ class TorrentServerService : Service() {
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
): Int {
|
||||
extension = Injekt.get<TorrentAddonManager>().extension?.extension ?: return START_NOT_STICKY
|
||||
extension =
|
||||
Injekt.get<TorrentAddonManager>().extension?.extension ?: return START_NOT_STICKY
|
||||
intent?.let {
|
||||
if (it.action != null) {
|
||||
when (it.action) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
|
||||
@@ -2,15 +2,25 @@ package ani.dantotsu.connections.anilist
|
||||
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.media.Author
|
||||
import ani.dantotsu.media.Character
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.Studio
|
||||
import ani.dantotsu.profile.User
|
||||
import java.io.Serializable
|
||||
|
||||
data class SearchResults(
|
||||
interface SearchResults<T> {
|
||||
var search: String?
|
||||
var page: Int
|
||||
var results: MutableList<T>
|
||||
var hasNextPage: Boolean
|
||||
}
|
||||
|
||||
data class AniMangaSearchResults(
|
||||
val type: String,
|
||||
var isAdult: Boolean,
|
||||
var onList: Boolean? = null,
|
||||
var perPage: Int? = null,
|
||||
var search: String? = null,
|
||||
var countryOfOrigin: String? = null,
|
||||
var sort: String? = null,
|
||||
var genres: MutableList<String>? = null,
|
||||
@@ -23,10 +33,11 @@ data class SearchResults(
|
||||
var seasonYear: Int? = null,
|
||||
var startYear: Int? = null,
|
||||
var season: String? = null,
|
||||
var page: Int = 1,
|
||||
var results: MutableList<Media>,
|
||||
var hasNextPage: Boolean,
|
||||
) : Serializable {
|
||||
override var search: String? = null,
|
||||
override var page: Int = 1,
|
||||
override var results: MutableList<Media>,
|
||||
override var hasNextPage: Boolean,
|
||||
) : SearchResults<Media>, Serializable {
|
||||
fun toChipList(): List<SearchChip> {
|
||||
val list = mutableListOf<SearchChip>()
|
||||
sort?.let {
|
||||
@@ -108,4 +119,33 @@ data class SearchResults(
|
||||
val type: String,
|
||||
val text: String
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class CharacterSearchResults(
|
||||
override var search: String?,
|
||||
override var page: Int = 1,
|
||||
override var results: MutableList<Character>,
|
||||
override var hasNextPage: Boolean,
|
||||
) : SearchResults<Character>, Serializable
|
||||
|
||||
data class StudioSearchResults(
|
||||
override var search: String?,
|
||||
override var page: Int = 1,
|
||||
override var results: MutableList<Studio>,
|
||||
override var hasNextPage: Boolean,
|
||||
) : SearchResults<Studio>, Serializable
|
||||
|
||||
|
||||
data class StaffSearchResults(
|
||||
override var search: String?,
|
||||
override var page: Int = 1,
|
||||
override var results: MutableList<Author>,
|
||||
override var hasNextPage: Boolean,
|
||||
) : SearchResults<Author>, Serializable
|
||||
|
||||
data class UserSearchResults(
|
||||
override var search: String?,
|
||||
override var page: Int = 1,
|
||||
override var results: MutableList<User>,
|
||||
override var hasNextPage: Boolean,
|
||||
) : SearchResults<User>, Serializable
|
||||
@@ -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
|
||||
|
||||
@@ -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,54 @@ 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 +187,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 +241,179 @@ 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 {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||
@@ -126,7 +128,7 @@ class AnilistHomeViewModel : ViewModel() {
|
||||
class AnilistAnimeViewModel : ViewModel() {
|
||||
var searched = false
|
||||
var notSet = true
|
||||
lateinit var searchResults: SearchResults
|
||||
lateinit var aniMangaSearchResults: AniMangaSearchResults
|
||||
private val type = "ANIME"
|
||||
private val trending: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
@@ -135,7 +137,7 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
suspend fun loadTrending(i: Int) {
|
||||
val (season, year) = Anilist.currentSeasons[i]
|
||||
trending.postValue(
|
||||
Anilist.query.search(
|
||||
Anilist.query.searchAniManga(
|
||||
type,
|
||||
perPage = 12,
|
||||
sort = Anilist.sortBy[2],
|
||||
@@ -148,9 +150,9 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
|
||||
private val animePopular = MutableLiveData<SearchResults?>(null)
|
||||
private val animePopular = MutableLiveData<AniMangaSearchResults?>(null)
|
||||
|
||||
fun getPopular(): LiveData<SearchResults?> = animePopular
|
||||
fun getPopular(): LiveData<AniMangaSearchResults?> = animePopular
|
||||
suspend fun loadPopular(
|
||||
type: String,
|
||||
searchVal: String? = null,
|
||||
@@ -159,7 +161,7 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
onList: Boolean = true,
|
||||
) {
|
||||
animePopular.postValue(
|
||||
Anilist.query.search(
|
||||
Anilist.query.searchAniManga(
|
||||
type,
|
||||
search = searchVal,
|
||||
onList = if (onList) null else false,
|
||||
@@ -171,8 +173,8 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
|
||||
suspend fun loadNextPage(r: SearchResults) = animePopular.postValue(
|
||||
Anilist.query.search(
|
||||
suspend fun loadNextPage(r: AniMangaSearchResults) = animePopular.postValue(
|
||||
Anilist.query.searchAniManga(
|
||||
r.type,
|
||||
r.page + 1,
|
||||
r.perPage,
|
||||
@@ -222,7 +224,7 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
class AnilistMangaViewModel : ViewModel() {
|
||||
var searched = false
|
||||
var notSet = true
|
||||
lateinit var searchResults: SearchResults
|
||||
lateinit var aniMangaSearchResults: AniMangaSearchResults
|
||||
private val type = "MANGA"
|
||||
private val trending: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
@@ -230,7 +232,7 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
fun getTrending(): LiveData<MutableList<Media>> = trending
|
||||
suspend fun loadTrending() =
|
||||
trending.postValue(
|
||||
Anilist.query.search(
|
||||
Anilist.query.searchAniManga(
|
||||
type,
|
||||
perPage = 10,
|
||||
sort = Anilist.sortBy[2],
|
||||
@@ -240,8 +242,8 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
)
|
||||
|
||||
|
||||
private val mangaPopular = MutableLiveData<SearchResults?>(null)
|
||||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||
private val mangaPopular = MutableLiveData<AniMangaSearchResults?>(null)
|
||||
fun getPopular(): LiveData<AniMangaSearchResults?> = mangaPopular
|
||||
suspend fun loadPopular(
|
||||
type: String,
|
||||
searchVal: String? = null,
|
||||
@@ -250,7 +252,7 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
onList: Boolean = true,
|
||||
) {
|
||||
mangaPopular.postValue(
|
||||
Anilist.query.search(
|
||||
Anilist.query.searchAniManga(
|
||||
type,
|
||||
search = searchVal,
|
||||
onList = if (onList) null else false,
|
||||
@@ -262,8 +264,8 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
|
||||
suspend fun loadNextPage(r: SearchResults) = mangaPopular.postValue(
|
||||
Anilist.query.search(
|
||||
suspend fun loadNextPage(r: AniMangaSearchResults) = mangaPopular.postValue(
|
||||
Anilist.query.searchAniManga(
|
||||
r.type,
|
||||
r.page + 1,
|
||||
r.perPage,
|
||||
@@ -323,14 +325,131 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
class AnilistSearch : ViewModel() {
|
||||
|
||||
enum class SearchType {
|
||||
ANIME, MANGA, CHARACTER, STAFF, STUDIO, USER;
|
||||
|
||||
companion object {
|
||||
|
||||
fun SearchType.toAnilistString(): String {
|
||||
return when (this) {
|
||||
ANIME -> "ANIME"
|
||||
MANGA -> "MANGA"
|
||||
CHARACTER -> "CHARACTER"
|
||||
STAFF -> "STAFF"
|
||||
STUDIO -> "STUDIO"
|
||||
USER -> "USER"
|
||||
}
|
||||
}
|
||||
|
||||
fun fromString(string: String): SearchType {
|
||||
return when (string.uppercase()) {
|
||||
"ANIME" -> ANIME
|
||||
"MANGA" -> MANGA
|
||||
"CHARACTER" -> CHARACTER
|
||||
"STAFF" -> STAFF
|
||||
"STUDIO" -> STUDIO
|
||||
"USER" -> USER
|
||||
else -> throw IllegalArgumentException("Invalid search type")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var searched = false
|
||||
var notSet = true
|
||||
lateinit var searchResults: SearchResults
|
||||
private val result: MutableLiveData<SearchResults?> = MutableLiveData<SearchResults?>(null)
|
||||
lateinit var aniMangaSearchResults: AniMangaSearchResults
|
||||
private val aniMangaResult: MutableLiveData<AniMangaSearchResults?> =
|
||||
MutableLiveData<AniMangaSearchResults?>(null)
|
||||
|
||||
fun getSearch(): LiveData<SearchResults?> = result
|
||||
suspend fun loadSearch(r: SearchResults) = result.postValue(
|
||||
Anilist.query.search(
|
||||
lateinit var characterSearchResults: CharacterSearchResults
|
||||
private val characterResult: MutableLiveData<CharacterSearchResults?> =
|
||||
MutableLiveData<CharacterSearchResults?>(null)
|
||||
|
||||
lateinit var studioSearchResults: StudioSearchResults
|
||||
private val studioResult: MutableLiveData<StudioSearchResults?> =
|
||||
MutableLiveData<StudioSearchResults?>(null)
|
||||
|
||||
lateinit var staffSearchResults: StaffSearchResults
|
||||
private val staffResult: MutableLiveData<StaffSearchResults?> =
|
||||
MutableLiveData<StaffSearchResults?>(null)
|
||||
|
||||
lateinit var userSearchResults: UserSearchResults
|
||||
private val userResult: MutableLiveData<UserSearchResults?> =
|
||||
MutableLiveData<UserSearchResults?>(null)
|
||||
|
||||
fun <T> getSearch(type: SearchType): MutableLiveData<T?> {
|
||||
return when (type) {
|
||||
SearchType.ANIME, SearchType.MANGA -> aniMangaResult as MutableLiveData<T?>
|
||||
SearchType.CHARACTER -> characterResult as MutableLiveData<T?>
|
||||
SearchType.STUDIO -> studioResult as MutableLiveData<T?>
|
||||
SearchType.STAFF -> staffResult as MutableLiveData<T?>
|
||||
SearchType.USER -> userResult as MutableLiveData<T?>
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadSearch(type: SearchType) {
|
||||
when (type) {
|
||||
SearchType.ANIME, SearchType.MANGA -> loadAniMangaSearch(aniMangaSearchResults)
|
||||
SearchType.CHARACTER -> loadCharacterSearch(characterSearchResults)
|
||||
SearchType.STUDIO -> loadStudiosSearch(studioSearchResults)
|
||||
SearchType.STAFF -> loadStaffSearch(staffSearchResults)
|
||||
SearchType.USER -> loadUserSearch(userSearchResults)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadNextPage(type: SearchType) {
|
||||
when (type) {
|
||||
SearchType.ANIME, SearchType.MANGA -> loadNextAniMangaPage(aniMangaSearchResults)
|
||||
SearchType.CHARACTER -> loadNextCharacterPage(characterSearchResults)
|
||||
SearchType.STUDIO -> loadNextStudiosPage(studioSearchResults)
|
||||
SearchType.STAFF -> loadNextStaffPage(staffSearchResults)
|
||||
SearchType.USER -> loadNextUserPage(userSearchResults)
|
||||
}
|
||||
}
|
||||
|
||||
fun hasNextPage(type: SearchType): Boolean {
|
||||
return when (type) {
|
||||
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.hasNextPage
|
||||
SearchType.CHARACTER -> characterSearchResults.hasNextPage
|
||||
SearchType.STUDIO -> studioSearchResults.hasNextPage
|
||||
SearchType.STAFF -> staffSearchResults.hasNextPage
|
||||
SearchType.USER -> userSearchResults.hasNextPage
|
||||
}
|
||||
}
|
||||
|
||||
fun resultsIsNotEmpty(type: SearchType): Boolean {
|
||||
return when (type) {
|
||||
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.isNotEmpty()
|
||||
SearchType.CHARACTER -> characterSearchResults.results.isNotEmpty()
|
||||
SearchType.STUDIO -> studioSearchResults.results.isNotEmpty()
|
||||
SearchType.STAFF -> staffSearchResults.results.isNotEmpty()
|
||||
SearchType.USER -> userSearchResults.results.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun size(type: SearchType): Int {
|
||||
return when (type) {
|
||||
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.size
|
||||
SearchType.CHARACTER -> characterSearchResults.results.size
|
||||
SearchType.STUDIO -> studioSearchResults.results.size
|
||||
SearchType.STAFF -> staffSearchResults.results.size
|
||||
SearchType.USER -> userSearchResults.results.size
|
||||
}
|
||||
}
|
||||
|
||||
fun clearResults(type: SearchType) {
|
||||
when (type) {
|
||||
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.clear()
|
||||
SearchType.CHARACTER -> characterSearchResults.results.clear()
|
||||
SearchType.STUDIO -> studioSearchResults.results.clear()
|
||||
SearchType.STAFF -> staffSearchResults.results.clear()
|
||||
SearchType.USER -> userSearchResults.results.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadAniMangaSearch(r: AniMangaSearchResults) = aniMangaResult.postValue(
|
||||
Anilist.query.searchAniManga(
|
||||
r.type,
|
||||
r.page,
|
||||
r.perPage,
|
||||
@@ -352,8 +471,36 @@ class AnilistSearch : ViewModel() {
|
||||
)
|
||||
)
|
||||
|
||||
suspend fun loadNextPage(r: SearchResults) = result.postValue(
|
||||
Anilist.query.search(
|
||||
private suspend fun loadCharacterSearch(r: CharacterSearchResults) = characterResult.postValue(
|
||||
Anilist.query.searchCharacters(
|
||||
r.page,
|
||||
r.search,
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun loadStudiosSearch(r: StudioSearchResults) = studioResult.postValue(
|
||||
Anilist.query.searchStudios(
|
||||
r.page,
|
||||
r.search,
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun loadStaffSearch(r: StaffSearchResults) = staffResult.postValue(
|
||||
Anilist.query.searchStaff(
|
||||
r.page,
|
||||
r.search,
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun loadUserSearch(r: UserSearchResults) = userResult.postValue(
|
||||
Anilist.query.searchUsers(
|
||||
r.page,
|
||||
r.search,
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun loadNextAniMangaPage(r: AniMangaSearchResults) = aniMangaResult.postValue(
|
||||
Anilist.query.searchAniManga(
|
||||
r.type,
|
||||
r.page + 1,
|
||||
r.perPage,
|
||||
@@ -374,6 +521,35 @@ class AnilistSearch : ViewModel() {
|
||||
r.season
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun loadNextCharacterPage(r: CharacterSearchResults) =
|
||||
characterResult.postValue(
|
||||
Anilist.query.searchCharacters(
|
||||
r.page + 1,
|
||||
r.search,
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun loadNextStudiosPage(r: StudioSearchResults) = studioResult.postValue(
|
||||
Anilist.query.searchStudios(
|
||||
r.page + 1,
|
||||
r.search,
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun loadNextStaffPage(r: StaffSearchResults) = staffResult.postValue(
|
||||
Anilist.query.searchStaff(
|
||||
r.page + 1,
|
||||
r.search,
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun loadNextUserPage(r: UserSearchResults) = userResult.postValue(
|
||||
Anilist.query.searchUsers(
|
||||
r.page + 1,
|
||||
r.search,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
class GenresViewModel : ViewModel() {
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
package ani.dantotsu.connections.anilist
|
||||
|
||||
val standardPageInformation = """
|
||||
pageInfo {
|
||||
total
|
||||
perPage
|
||||
currentPage
|
||||
lastPage
|
||||
hasNextPage
|
||||
}
|
||||
""".prepare()
|
||||
|
||||
fun String.prepare() = this.trimIndent().replace("\n", " ").replace(""" """, "")
|
||||
|
||||
fun characterInformation(includeMediaInfo: Boolean) = """
|
||||
id
|
||||
name {
|
||||
first
|
||||
middle
|
||||
last
|
||||
full
|
||||
native
|
||||
userPreferred
|
||||
}
|
||||
image {
|
||||
large
|
||||
medium
|
||||
}
|
||||
age
|
||||
gender
|
||||
description
|
||||
dateOfBirth {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
${
|
||||
if (includeMediaInfo) """
|
||||
media(page: 0,sort:[POPULARITY_DESC,SCORE_DESC]) {
|
||||
$standardPageInformation
|
||||
edges {
|
||||
id
|
||||
voiceActors {
|
||||
id,
|
||||
name {
|
||||
userPreferred
|
||||
}
|
||||
languageV2,
|
||||
image {
|
||||
medium,
|
||||
large
|
||||
}
|
||||
}
|
||||
characterRole
|
||||
node {
|
||||
id
|
||||
idMal
|
||||
isAdult
|
||||
status
|
||||
chapters
|
||||
episodes
|
||||
nextAiringEpisode { episode }
|
||||
type
|
||||
meanScore
|
||||
isFavourite
|
||||
format
|
||||
bannerImage
|
||||
countryOfOrigin
|
||||
coverImage { large }
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
userPreferred
|
||||
}
|
||||
mediaListEntry {
|
||||
progress
|
||||
private
|
||||
score(format: POINT_100)
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}""".prepare() else ""
|
||||
}
|
||||
""".prepare()
|
||||
|
||||
fun studioInformation(page: Int, perPage: Int) = """
|
||||
id
|
||||
name
|
||||
isFavourite
|
||||
favourites
|
||||
media(page: $page, sort:START_DATE_DESC, perPage: $perPage) {
|
||||
$standardPageInformation
|
||||
edges {
|
||||
id
|
||||
node {
|
||||
id
|
||||
idMal
|
||||
isAdult
|
||||
status
|
||||
chapters
|
||||
episodes
|
||||
nextAiringEpisode { episode }
|
||||
type
|
||||
meanScore
|
||||
startDate{ year }
|
||||
isFavourite
|
||||
format
|
||||
bannerImage
|
||||
countryOfOrigin
|
||||
coverImage { large }
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
userPreferred
|
||||
}
|
||||
mediaListEntry {
|
||||
progress
|
||||
private
|
||||
score(format: POINT_100)
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".prepare()
|
||||
|
||||
fun staffInformation(page: Int, perPage: Int) = """
|
||||
id
|
||||
name {
|
||||
first
|
||||
middle
|
||||
last
|
||||
full
|
||||
native
|
||||
userPreferred
|
||||
}
|
||||
image {
|
||||
large
|
||||
medium
|
||||
}
|
||||
dateOfBirth {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
dateOfDeath {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
age
|
||||
yearsActive
|
||||
homeTown
|
||||
staffMedia(page: $page,sort:START_DATE_DESC, perPage: $perPage) {
|
||||
$standardPageInformation
|
||||
edges {
|
||||
staffRole
|
||||
id
|
||||
node {
|
||||
id
|
||||
idMal
|
||||
isAdult
|
||||
status
|
||||
chapters
|
||||
episodes
|
||||
nextAiringEpisode { episode }
|
||||
type
|
||||
meanScore
|
||||
startDate{ year }
|
||||
isFavourite
|
||||
format
|
||||
bannerImage
|
||||
countryOfOrigin
|
||||
coverImage { large }
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
userPreferred
|
||||
}
|
||||
mediaListEntry {
|
||||
progress
|
||||
private
|
||||
score(format: POINT_100)
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".prepare()
|
||||
|
||||
fun userInformation() = """
|
||||
id
|
||||
name
|
||||
about(asHtml: true)
|
||||
avatar {
|
||||
large
|
||||
medium
|
||||
}
|
||||
bannerImage
|
||||
isFollowing
|
||||
isFollower
|
||||
isBlocked
|
||||
siteUrl
|
||||
""".prepare()
|
||||
|
||||
fun aniMangaSearch(perPage: Int?) = """
|
||||
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
|
||||
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
|
||||
$standardPageInformation
|
||||
media(id: ${"$"}id, type: ${"$"}type, season: ${"$"}season, format_in: ${"$"}format, status: ${"$"}status, countryOfOrigin: ${"$"}countryOfOrigin, source: ${"$"}source, search: ${"$"}search, onList: ${"$"}onList, seasonYear: ${"$"}seasonYear, startDate_like: ${"$"}year, startDate_lesser: ${"$"}yearLesser, startDate_greater: ${"$"}yearGreater, episodes_lesser: ${"$"}episodeLesser, episodes_greater: ${"$"}episodeGreater, duration_lesser: ${"$"}durationLesser, duration_greater: ${"$"}durationGreater, chapters_lesser: ${"$"}chapterLesser, chapters_greater: ${"$"}chapterGreater, volumes_lesser: ${"$"}volumeLesser, volumes_greater: ${"$"}volumeGreater, licensedBy_in: ${"$"}licensedBy, isLicensed: ${"$"}isLicensed, genre_in: ${"$"}genres, genre_not_in: ${"$"}excludedGenres, tag_in: ${"$"}tags, tag_not_in: ${"$"}excludedTags, minimumTagRank: ${"$"}minimumTagRank, sort: ${"$"}sort, isAdult: ${"$"}isAdult) {
|
||||
${standardMediaInformation()}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".prepare()
|
||||
|
||||
fun standardMediaInformation() = """
|
||||
id
|
||||
idMal
|
||||
siteUrl
|
||||
isAdult
|
||||
status(version: 2)
|
||||
chapters
|
||||
episodes
|
||||
nextAiringEpisode {
|
||||
episode
|
||||
airingAt
|
||||
}
|
||||
type
|
||||
genres
|
||||
meanScore
|
||||
popularity
|
||||
favourites
|
||||
isFavourite
|
||||
format
|
||||
bannerImage
|
||||
countryOfOrigin
|
||||
coverImage {
|
||||
large
|
||||
extraLarge
|
||||
}
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
userPreferred
|
||||
}
|
||||
mediaListEntry {
|
||||
progress
|
||||
private
|
||||
score(format: POINT_100)
|
||||
status
|
||||
}
|
||||
""".prepare()
|
||||
|
||||
fun fullMediaInformation(id: Int) = """
|
||||
{
|
||||
Media(id: $id) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${standardMediaInformation()}
|
||||
source
|
||||
duration
|
||||
season
|
||||
seasonYear
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
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 {
|
||||
${standardMediaInformation()}
|
||||
}
|
||||
}
|
||||
}
|
||||
staffPreview: staff(perPage: 8, sort: [RELEVANCE, ID]) {
|
||||
edges {
|
||||
role
|
||||
node {
|
||||
id
|
||||
image {
|
||||
large
|
||||
medium
|
||||
}
|
||||
name {
|
||||
userPreferred
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recommendations(sort: RATING_DESC) {
|
||||
nodes {
|
||||
mediaRecommendation {
|
||||
${standardMediaInformation()}
|
||||
}
|
||||
}
|
||||
}
|
||||
externalLinks {
|
||||
url
|
||||
site
|
||||
}
|
||||
}
|
||||
Page(page: 1) {
|
||||
$standardPageInformation
|
||||
mediaList(isFollowing: true, sort: [STATUS], mediaId: $id) {
|
||||
id
|
||||
status
|
||||
score(format: POINT_100)
|
||||
progress
|
||||
progressVolumes
|
||||
user {
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
large
|
||||
medium
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
""".prepare()
|
||||
@@ -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?,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ data class FeedResponse(
|
||||
val page: ActivityPage
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ActivityPage(
|
||||
@SerialName("activities")
|
||||
|
||||
@@ -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>?,
|
||||
@@ -189,7 +189,7 @@ data class MediaTitle(
|
||||
|
||||
// The currently authenticated users preferred title language. Default romaji for non-authenticated
|
||||
@SerialName("userPreferred") var userPreferred: String,
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class MediaType {
|
||||
@@ -240,6 +240,21 @@ data class AiringSchedule(
|
||||
@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?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@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.
|
||||
@@ -433,7 +448,7 @@ data class MediaEdge(
|
||||
@SerialName("staffRole") var staffRole: String?,
|
||||
|
||||
// The voice actors of the character
|
||||
// @SerialName("voiceActors") var voiceActors: List<Staff>?,
|
||||
@SerialName("voiceActors") var voiceActors: List<Staff>?,
|
||||
|
||||
// The voice actors of the character with role date
|
||||
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,
|
||||
|
||||
@@ -69,12 +69,12 @@ data class User(
|
||||
// The user's previously used names.
|
||||
// @SerialName("previousNames") var previousNames: List<UserPreviousName>?,
|
||||
|
||||
): java.io.Serializable
|
||||
) : java.io.Serializable
|
||||
|
||||
@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,48 @@ 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 +206,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 +223,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
|
||||
@@ -371,8 +374,8 @@ object CommentsAPI {
|
||||
}
|
||||
|
||||
private fun errorMessage(reason: String) {
|
||||
Logger.log(reason)
|
||||
if (isOnline) snackString(reason)
|
||||
if (commentsEnabled) Logger.log(reason)
|
||||
if (isOnline && commentsEnabled) snackString(reason)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
@@ -408,7 +411,7 @@ object CommentsAPI {
|
||||
return map
|
||||
}
|
||||
|
||||
private fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
|
||||
fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
|
||||
return Requests(
|
||||
client,
|
||||
headerBuilder()
|
||||
|
||||
@@ -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 java.util.concurrent.TimeUnit.SECONDS
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import ani.dantotsu.client as app
|
||||
|
||||
@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,
|
||||
@@ -40,24 +35,24 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
||||
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
|
||||
}
|
||||
return json.encodeToString(Presence.Response(
|
||||
3,
|
||||
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(
|
||||
activities = listOf(
|
||||
Activity(
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// this code was kanged from the greatest mind of this era, aka shivam brahmkshatriya
|
||||
// please subscribe to my only fans here: https://github.com/brahmkshatriya
|
||||
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",
|
||||
"??",
|
||||
"??",
|
||||
@@ -260,7 +260,7 @@ class DownloadCompat {
|
||||
"$mangaLink/${it.name}",
|
||||
it.name,
|
||||
null,
|
||||
null,
|
||||
"Unknown",
|
||||
SChapter.create()
|
||||
)
|
||||
chapters.add(chapter)
|
||||
|
||||
@@ -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
|
||||
@@ -61,7 +60,7 @@ class DownloadsManager(private val context: Context) {
|
||||
onFinished: () -> Unit
|
||||
) {
|
||||
removeDownloadCompat(context, downloadedType, toast)
|
||||
downloadsList.remove(downloadedType)
|
||||
downloadsList.removeAll { it.titleName == downloadedType.titleName && it.chapterName == downloadedType.chapterName }
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
removeDirectory(downloadedType, toast)
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -235,7 +234,7 @@ class DownloadsManager(private val context: Context) {
|
||||
val directory =
|
||||
baseDirectory?.findFolder(downloadedType.titleName)
|
||||
?.findFolder(downloadedType.chapterName)
|
||||
downloadsList.remove(downloadedType)
|
||||
downloadsList.removeAll { it.titleName == downloadedType.titleName && it.chapterName == downloadedType.chapterName }
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
@@ -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(
|
||||
@@ -389,10 +401,13 @@ data class DownloadedType(
|
||||
@Deprecated("use pTitle instead")
|
||||
private val title: String? = null,
|
||||
@Deprecated("use pChapter instead")
|
||||
private val chapter: String? = null
|
||||
private val chapter: String? = null,
|
||||
val scanlator: String = "Unknown"
|
||||
) : Serializable {
|
||||
val titleName: String
|
||||
get() = title ?: pTitle.findValidName()
|
||||
val chapterName: String
|
||||
get() = chapter ?: pChapter.findValidName()
|
||||
val uniqueName: String
|
||||
get() = "$chapterName-${scanlator}"
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -48,6 +48,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 +203,24 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -288,10 +288,12 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
}
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||
val animeTitles =
|
||||
downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct()
|
||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.titleName.findValidName() == title }
|
||||
val tDownloads =
|
||||
downloadManager.animeDownloadedTypes.filter { it.titleName.findValidName() == title }
|
||||
val download = tDownloads.firstOrNull() ?: continue
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
if (offlineAnimeModel.title == "unknown") offlineAnimeModel.title = title
|
||||
@@ -319,17 +321,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 +399,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
return try {
|
||||
loadOfflineAnimeModelCompat(downloadedType)
|
||||
} catch (e: Exception) {
|
||||
@@ -401,7 +407,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) {
|
||||
@@ -222,64 +232,76 @@ class MangaDownloaderService : Service() {
|
||||
image.page,
|
||||
image.source
|
||||
)
|
||||
if (bitmap == null) {
|
||||
snackString("${task.chapter} - Retrying to download page ${index.ofLength(3)}, attempt ${retryCount + 1}.")
|
||||
}
|
||||
retryCount++
|
||||
}
|
||||
|
||||
if (bitmap != null) {
|
||||
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
|
||||
if (bitmap == null) {
|
||||
outputDir.deleteRecursively(this@MangaDownloaderService, false)
|
||||
throw Exception("${task.chapter} - Unable to download all pages after $retryCount attempts. Try again.")
|
||||
}
|
||||
|
||||
saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap)
|
||||
farthest++
|
||||
|
||||
builder.setProgress(task.imageData.size, farthest, false)
|
||||
|
||||
broadcastDownloadProgress(
|
||||
task.chapter,
|
||||
task.uniqueName,
|
||||
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,
|
||||
task.chapter,
|
||||
MediaType.MANGA
|
||||
MediaType.MANGA,
|
||||
scanlator = task.scanlator,
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.chapter)
|
||||
broadcastDownloadFinished(task.uniqueName)
|
||||
snackString("${task.title} - ${task.chapter} Download finished")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Exception while downloading file: ${e.message}")
|
||||
snackString("Exception while downloading file: ${e.message}")
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
broadcastDownloadFailed(task.chapter)
|
||||
broadcastDownloadFailed(task.uniqueName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 +314,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")
|
||||
@@ -411,11 +430,15 @@ class MangaDownloaderService : Service() {
|
||||
data class DownloadTask(
|
||||
val title: String,
|
||||
val chapter: String,
|
||||
val scanlator: String,
|
||||
val imageData: List<ImageData>,
|
||||
val sourceMedia: Media? = null,
|
||||
val retries: Int = 2,
|
||||
val simultaneousDownloads: Int = 2,
|
||||
)
|
||||
) {
|
||||
val uniqueName: String
|
||||
get() = "$chapter-$scanlator"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 1103
|
||||
|
||||
@@ -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) {
|
||||
@@ -177,6 +177,7 @@ object Helper {
|
||||
downloadManager
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
@OptIn(UnstableApi::class)
|
||||
fun getSimpleCache(context: Context): SimpleCache {
|
||||
@@ -189,6 +190,7 @@ object Helper {
|
||||
simpleCache!!
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private fun getDownloadDirectory(context: Context): File {
|
||||
@@ -200,12 +202,16 @@ object Helper {
|
||||
}
|
||||
return downloadDirectory!!
|
||||
}
|
||||
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private var download: DownloadManager? = null
|
||||
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private var simpleCache: SimpleCache? = null
|
||||
|
||||
@Deprecated("exoplayer download manager is no longer used")
|
||||
private var downloadDirectory: File? = null
|
||||
}
|
||||
@@ -22,9 +22,9 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.anilist.AniMangaSearchResults
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistAnimeViewModel
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.connections.anilist.getUserId
|
||||
import ani.dantotsu.databinding.FragmentAnimeBinding
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
@@ -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
|
||||
@@ -99,7 +100,7 @@ class AnimeFragment : Fragment() {
|
||||
var loading = true
|
||||
if (model.notSet) {
|
||||
model.notSet = false
|
||||
model.searchResults = SearchResults(
|
||||
model.aniMangaSearchResults = AniMangaSearchResults(
|
||||
"ANIME",
|
||||
isAdult = false,
|
||||
onList = false,
|
||||
@@ -108,7 +109,7 @@ class AnimeFragment : Fragment() {
|
||||
sort = Anilist.sortBy[1]
|
||||
)
|
||||
}
|
||||
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
|
||||
val popularAdaptor = MediaAdaptor(1, model.aniMangaSearchResults.results, requireActivity())
|
||||
val progressAdaptor = ProgressAdapter(searched = model.searched)
|
||||
val adapter = ConcatAdapter(animePageAdapter, popularAdaptor, progressAdaptor)
|
||||
binding.animePageRecyclerView.adapter = adapter
|
||||
@@ -141,7 +142,7 @@ class AnimeFragment : Fragment() {
|
||||
animePageAdapter.onIncludeListClick = { checked ->
|
||||
oldIncludeList = !checked
|
||||
loading = true
|
||||
model.searchResults.results.clear()
|
||||
model.aniMangaSearchResults.results.clear()
|
||||
popularAdaptor.notifyDataSetChanged()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = checked)
|
||||
@@ -151,17 +152,17 @@ class AnimeFragment : Fragment() {
|
||||
model.getPopular().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
if (oldIncludeList == (it.onList != false)) {
|
||||
val prev = model.searchResults.results.size
|
||||
model.searchResults.results.addAll(it.results)
|
||||
val prev = model.aniMangaSearchResults.results.size
|
||||
model.aniMangaSearchResults.results.addAll(it.results)
|
||||
popularAdaptor.notifyItemRangeInserted(prev, it.results.size)
|
||||
} else {
|
||||
model.searchResults.results.addAll(it.results)
|
||||
model.aniMangaSearchResults.results.addAll(it.results)
|
||||
popularAdaptor.notifyDataSetChanged()
|
||||
oldIncludeList = it.onList ?: true
|
||||
}
|
||||
model.searchResults.onList = it.onList
|
||||
model.searchResults.hasNextPage = it.hasNextPage
|
||||
model.searchResults.page = it.page
|
||||
model.aniMangaSearchResults.onList = it.onList
|
||||
model.aniMangaSearchResults.hasNextPage = it.hasNextPage
|
||||
model.aniMangaSearchResults.page = it.page
|
||||
if (it.hasNextPage)
|
||||
progressAdaptor.bar?.visibility = View.VISIBLE
|
||||
else {
|
||||
@@ -176,10 +177,10 @@ class AnimeFragment : Fragment() {
|
||||
RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
|
||||
if (!v.canScrollVertically(1)) {
|
||||
if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) {
|
||||
if (model.aniMangaSearchResults.hasNextPage && model.aniMangaSearchResults.results.isNotEmpty() && !loading) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
loading = true
|
||||
model.loadNextPage(model.searchResults)
|
||||
model.loadNextPage(model.aniMangaSearchResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,8 +277,9 @@ class AnimeFragment : Fragment() {
|
||||
running = true
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
Anilist.userid = PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
|
||||
?.toIntOrNull()
|
||||
Anilist.userid =
|
||||
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
|
||||
?.toIntOrNull()
|
||||
if (Anilist.userid == null) {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
@@ -289,15 +291,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
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -21,7 +20,6 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.MediaPageTransformer
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||
import ani.dantotsu.databinding.LayoutTrendingBinding
|
||||
import ani.dantotsu.getAppString
|
||||
@@ -83,13 +81,21 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
|
||||
updateAvatar()
|
||||
|
||||
trendingBinding.searchBar.hint = "ANIME"
|
||||
trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
|
||||
trendingBinding.searchBarText.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
it.context,
|
||||
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
|
||||
null
|
||||
)
|
||||
val context = binding.root.context
|
||||
if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) {
|
||||
ContextCompat.startActivity(
|
||||
context,
|
||||
Intent(context, SearchActivity::class.java).putExtra("type", "ANIME"),
|
||||
null
|
||||
)
|
||||
} else {
|
||||
SearchBottomSheet.newInstance().show(
|
||||
(context as AppCompatActivity).supportFragmentManager,
|
||||
"search"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
trendingBinding.userAvatar.setSafeOnClickListener {
|
||||
@@ -111,8 +117,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(
|
||||
@@ -259,7 +265,15 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
}
|
||||
|
||||
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View , more: View , string: String, media : MutableList<Media>) {
|
||||
fun init(
|
||||
adaptor: MediaAdaptor,
|
||||
recyclerView: RecyclerView,
|
||||
progress: View,
|
||||
title: View,
|
||||
more: View,
|
||||
string: String,
|
||||
media: MutableList<Media>
|
||||
) {
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
recyclerView.layoutManager =
|
||||
@@ -268,8 +282,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 +309,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
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 +92,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 {
|
||||
@@ -132,6 +133,12 @@ class HomeFragment : Fragment() {
|
||||
"dialog"
|
||||
)
|
||||
}
|
||||
binding.searchImageContainer.setSafeOnClickListener {
|
||||
SearchBottomSheet.newInstance().show(
|
||||
(it.context as androidx.appcompat.app.AppCompatActivity).supportFragmentManager,
|
||||
"search"
|
||||
)
|
||||
}
|
||||
binding.homeUserAvatarContainer.setOnLongClickListener {
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
ContextCompat.startActivity(
|
||||
@@ -456,51 +463,58 @@ 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
|
||||
// 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 +522,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()
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
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 com.google.android.material.textfield.TextInputEditText
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
|
||||
class LoginFragment : Fragment() {
|
||||
|
||||
@@ -94,38 +93,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() {
|
||||
|
||||
@@ -20,9 +20,9 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.anilist.AniMangaSearchResults
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistMangaViewModel
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.connections.anilist.getUserId
|
||||
import ani.dantotsu.databinding.FragmentMangaBinding
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
@@ -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
|
||||
@@ -93,7 +94,7 @@ class MangaFragment : Fragment() {
|
||||
var loading = true
|
||||
if (model.notSet) {
|
||||
model.notSet = false
|
||||
model.searchResults = SearchResults(
|
||||
model.aniMangaSearchResults = AniMangaSearchResults(
|
||||
"MANGA",
|
||||
isAdult = false,
|
||||
onList = false,
|
||||
@@ -102,7 +103,7 @@ class MangaFragment : Fragment() {
|
||||
sort = Anilist.sortBy[1]
|
||||
)
|
||||
}
|
||||
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
|
||||
val popularAdaptor = MediaAdaptor(1, model.aniMangaSearchResults.results, requireActivity())
|
||||
val progressAdaptor = ProgressAdapter(searched = model.searched)
|
||||
binding.mangaPageRecyclerView.adapter =
|
||||
ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
|
||||
@@ -134,10 +135,10 @@ class MangaFragment : Fragment() {
|
||||
RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
|
||||
if (!v.canScrollVertically(1)) {
|
||||
if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) {
|
||||
if (model.aniMangaSearchResults.hasNextPage && model.aniMangaSearchResults.results.isNotEmpty() && !loading) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
loading = true
|
||||
model.loadNextPage(model.searchResults)
|
||||
model.loadNextPage(model.aniMangaSearchResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +169,10 @@ class MangaFragment : Fragment() {
|
||||
}
|
||||
model.getPopularManga().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()), it)
|
||||
mangaPageAdapter.updateTrendingManga(
|
||||
MediaAdaptor(0, it, requireActivity()),
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
model.getPopularManhwa().observe(viewLifecycleOwner) {
|
||||
@@ -219,7 +223,7 @@ class MangaFragment : Fragment() {
|
||||
mangaPageAdapter.onIncludeListClick = { checked ->
|
||||
oldIncludeList = !checked
|
||||
loading = true
|
||||
model.searchResults.results.clear()
|
||||
model.aniMangaSearchResults.results.clear()
|
||||
popularAdaptor.notifyDataSetChanged()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = checked)
|
||||
@@ -229,17 +233,17 @@ class MangaFragment : Fragment() {
|
||||
model.getPopular().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
if (oldIncludeList == (it.onList != false)) {
|
||||
val prev = model.searchResults.results.size
|
||||
model.searchResults.results.addAll(it.results)
|
||||
val prev = model.aniMangaSearchResults.results.size
|
||||
model.aniMangaSearchResults.results.addAll(it.results)
|
||||
popularAdaptor.notifyItemRangeInserted(prev, it.results.size)
|
||||
} else {
|
||||
model.searchResults.results.addAll(it.results)
|
||||
model.aniMangaSearchResults.results.addAll(it.results)
|
||||
popularAdaptor.notifyDataSetChanged()
|
||||
oldIncludeList = it.onList ?: true
|
||||
}
|
||||
model.searchResults.onList = it.onList
|
||||
model.searchResults.hasNextPage = it.hasNextPage
|
||||
model.searchResults.page = it.page
|
||||
model.aniMangaSearchResults.onList = it.onList
|
||||
model.aniMangaSearchResults.hasNextPage = it.hasNextPage
|
||||
model.aniMangaSearchResults.page = it.page
|
||||
if (it.hasNextPage)
|
||||
progressAdaptor.bar?.visibility = View.VISIBLE
|
||||
else {
|
||||
@@ -261,8 +265,9 @@ class MangaFragment : Fragment() {
|
||||
running = true
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
Anilist.userid = PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
|
||||
?.toIntOrNull()
|
||||
Anilist.userid =
|
||||
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
|
||||
?.toIntOrNull()
|
||||
if (Anilist.userid == null) {
|
||||
getUserId(requireContext()) {
|
||||
load()
|
||||
@@ -274,15 +279,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,14 +80,23 @@ 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.searchBar.hint = binding.root.context.getString(R.string.search)
|
||||
trendingBinding.searchBarText.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
it.context,
|
||||
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"),
|
||||
null
|
||||
)
|
||||
val context = binding.root.context
|
||||
if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) {
|
||||
ContextCompat.startActivity(
|
||||
context,
|
||||
Intent(context, SearchActivity::class.java).putExtra("type", "MANGA"),
|
||||
null
|
||||
)
|
||||
} else {
|
||||
SearchBottomSheet.newInstance().show(
|
||||
(context as AppCompatActivity).supportFragmentManager,
|
||||
"search"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
trendingBinding.userAvatar.setSafeOnClickListener {
|
||||
@@ -257,10 +266,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
adaptor: MediaAdaptor,
|
||||
recyclerView: RecyclerView,
|
||||
progress: View,
|
||||
title: View ,
|
||||
more: View ,
|
||||
title: View,
|
||||
more: View,
|
||||
string: String,
|
||||
media : MutableList<Media>
|
||||
media: MutableList<Media>
|
||||
) {
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
@@ -296,8 +305,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()
|
||||
}
|
||||
}
|
||||
|
||||
74
app/src/main/java/ani/dantotsu/home/SearchBottomSheet.kt
Normal file
74
app/src/main/java/ani/dantotsu/home/SearchBottomSheet.kt
Normal file
@@ -0,0 +1,74 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
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 ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString
|
||||
import ani.dantotsu.databinding.BottomSheetSearchBinding
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
|
||||
class SearchBottomSheet : BottomSheetDialogFragment() {
|
||||
private var _binding: BottomSheetSearchBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetSearchBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.animeSearch.setOnClickListener {
|
||||
startActivity(requireContext(), SearchType.ANIME)
|
||||
dismiss()
|
||||
}
|
||||
binding.mangaSearch.setOnClickListener {
|
||||
startActivity(requireContext(), SearchType.MANGA)
|
||||
dismiss()
|
||||
}
|
||||
binding.characterSearch.setOnClickListener {
|
||||
startActivity(requireContext(), SearchType.CHARACTER)
|
||||
dismiss()
|
||||
}
|
||||
binding.staffSearch.setOnClickListener {
|
||||
startActivity(requireContext(), SearchType.STAFF)
|
||||
dismiss()
|
||||
}
|
||||
binding.studioSearch.setOnClickListener {
|
||||
startActivity(requireContext(), SearchType.STUDIO)
|
||||
dismiss()
|
||||
}
|
||||
binding.userSearch.setOnClickListener {
|
||||
startActivity(requireContext(), SearchType.USER)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startActivity(context: Context, type: SearchType) {
|
||||
ContextCompat.startActivity(
|
||||
context,
|
||||
Intent(context, SearchActivity::class.java).putExtra("type", type.toAnilistString()),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = SearchBottomSheet()
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs)
|
||||
|
||||
fun setColor(int: Int) {
|
||||
paint.color = if (int < booleanList.size && booleanList[int]) {
|
||||
Color.GRAY
|
||||
Color.GRAY
|
||||
} else {
|
||||
if (isUser) secondColor else primaryColor
|
||||
}
|
||||
@@ -58,7 +58,7 @@ class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs)
|
||||
} else {
|
||||
val effectiveAngle = totalAngle / parts
|
||||
for (i in 0 until parts) {
|
||||
val startAngle = i * (effectiveAngle + gapAngle) -90f
|
||||
val startAngle = i * (effectiveAngle + gapAngle) - 90f
|
||||
path.reset()
|
||||
path.addArc(
|
||||
centerX - radius,
|
||||
@@ -74,7 +74,7 @@ class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs)
|
||||
|
||||
}
|
||||
|
||||
fun setParts(parts: Int, list : List<Boolean> = mutableListOf(), isUser: Boolean) {
|
||||
fun setParts(parts: Int, list: List<Boolean> = mutableListOf(), isUser: Boolean) {
|
||||
this.parts = parts
|
||||
this.booleanList = list
|
||||
this.isUser = isUser
|
||||
|
||||
@@ -9,13 +9,14 @@ import androidx.core.view.updateLayoutParams
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.api.Activity
|
||||
import ani.dantotsu.databinding.ActivityStatusBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.home.status.listener.StoriesCallback
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.util.Logger
|
||||
|
||||
class StatusActivity : AppCompatActivity(), StoriesCallback {
|
||||
private lateinit var activity: ArrayList<User>
|
||||
@@ -44,12 +45,20 @@ 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 {
|
||||
for (activityItem in activity) {
|
||||
if (activityItem.id !in watchedActivity) {
|
||||
@@ -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) {
|
||||
@@ -75,15 +87,16 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
|
||||
binding.stories.pause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStoriesEnd() {
|
||||
position += 1
|
||||
if (position < activity.size) {
|
||||
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
|
||||
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,18 +105,19 @@ 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
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var user: ArrayList<User> = arrayListOf()
|
||||
}
|
||||
|
||||
@@ -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,50 +262,6 @@ 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")
|
||||
if (storyIndex == activityList.size) {
|
||||
@@ -359,6 +313,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 +329,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 +361,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 +387,7 @@ class Stories @JvmOverloads constructor(
|
||||
story.media?.id
|
||||
),
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
activity,
|
||||
(it.context as FragmentActivity),
|
||||
binding.coverImage,
|
||||
ViewCompat.getTransitionName(binding.coverImage)!!
|
||||
).toBundle()
|
||||
@@ -455,22 +421,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 +449,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 +467,73 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -62,10 +76,15 @@ class UserStatusAdapter(private val user: ArrayList<User>) :
|
||||
setAnimation(b.root.context, b.root)
|
||||
val user = user[position]
|
||||
b.profileUserAvatar.loadImage(user.pfp)
|
||||
b.profileUserName.text = if (Anilist.userid == user.id) getAppString(R.string.your_story) else user.name
|
||||
b.profileUserName.text =
|
||||
if (Anilist.userid == user.id) getAppString(R.string.your_story) else user.name
|
||||
val watchedActivity = PrefManager.getCustomVal<Set<Int>>("activities", setOf())
|
||||
val booleanList = user.activity.map { watchedActivity.contains(it.id) }
|
||||
b.profileUserStatusIndicator.setParts(user.activity.size, booleanList, user.id == Anilist.userid)
|
||||
b.profileUserStatusIndicator.setParts(
|
||||
user.activity.size,
|
||||
booleanList,
|
||||
user.id == Anilist.userid
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ data class Author(
|
||||
var name: String?,
|
||||
var image: String?,
|
||||
var role: String?,
|
||||
var age: Int? = null,
|
||||
var yearsActive: List<Int>? = null,
|
||||
var dateOfBirth: String? = null,
|
||||
var dateOfDeath: String? = null,
|
||||
var homeTown: String? = null,
|
||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
|
||||
var character: ArrayList<Character>? = null
|
||||
var character: ArrayList<Character>? = null,
|
||||
var isFav: Boolean = false
|
||||
) : Serializable
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -16,57 +18,127 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.EmptyAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityAuthorBinding
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistMutations
|
||||
import ani.dantotsu.databinding.ActivityCharacterBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.SpoilerPlugin
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.abs
|
||||
|
||||
class AuthorActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityAuthorBinding
|
||||
class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
||||
private lateinit var binding: ActivityCharacterBinding
|
||||
private val scope = lifecycleScope
|
||||
private val model: OtherDetailsViewModel by viewModels()
|
||||
private var author: Author? = null
|
||||
private lateinit var author: Author
|
||||
private var loaded = false
|
||||
|
||||
private var screenWidth: Float = 0f
|
||||
private val percent = 30
|
||||
private var mMaxScrollSize = 0
|
||||
private var isCollapsed = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
||||
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
initActivity(this)
|
||||
this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
|
||||
screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.transparent)
|
||||
|
||||
val screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
val banner =
|
||||
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
|
||||
|
||||
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.studioRecycler.updatePadding(bottom = 64f.px + navBarHeight)
|
||||
binding.studioTitle.isSelected = true
|
||||
banner.updateLayoutParams { height += statusBarHeight }
|
||||
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.characterCollapsing.minimumHeight = statusBarHeight
|
||||
binding.characterCover.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.characterRecyclerView.updatePadding(bottom = 64f.px + navBarHeight)
|
||||
binding.characterTitle.isSelected = true
|
||||
binding.characterAppBar.addOnOffsetChangedListener(this)
|
||||
|
||||
author = intent.getSerialized("author")
|
||||
binding.studioTitle.text = author?.name
|
||||
|
||||
binding.studioClose.setOnClickListener {
|
||||
binding.characterClose.setOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
author = intent.getSerialized("author") ?: return
|
||||
binding.characterTitle.text = author.name
|
||||
binding.characterCoverImage.loadImage(author.image)
|
||||
binding.characterCoverImage.setOnLongClickListener {
|
||||
ImageViewDialog.newInstance(
|
||||
this,
|
||||
author.name,
|
||||
author.image
|
||||
)
|
||||
}
|
||||
val link = "https://anilist.co/staff/${author.id}"
|
||||
binding.characterShare.setOnClickListener {
|
||||
val i = Intent(Intent.ACTION_SEND)
|
||||
i.type = "text/plain"
|
||||
i.putExtra(Intent.EXTRA_TEXT, link)
|
||||
startActivity(Intent.createChooser(i, author.name))
|
||||
}
|
||||
binding.characterShare.setOnLongClickListener {
|
||||
openLinkInBrowser(link)
|
||||
true
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
author.isFav =
|
||||
Anilist.query.isUserFav(AnilistMutations.FavType.STAFF, author.id)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.characterFav.setImageResource(
|
||||
if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.characterFav.setOnClickListener {
|
||||
scope.launch {
|
||||
lifecycleScope.launch {
|
||||
if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, author.id)) {
|
||||
author.isFav = !author.isFav
|
||||
binding.characterFav.setImageResource(
|
||||
if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
|
||||
)
|
||||
} else {
|
||||
snackString("Failed to toggle favorite")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
model.getAuthor().observe(this) {
|
||||
if (it != null) {
|
||||
author = it
|
||||
loaded = true
|
||||
binding.studioProgressBar.visibility = View.GONE
|
||||
binding.studioRecycler.visibility = View.VISIBLE
|
||||
if (author!!.yearMedia.isNullOrEmpty()) {
|
||||
binding.studioRecycler.visibility = View.GONE
|
||||
binding.characterProgress.visibility = View.GONE
|
||||
binding.characterRecyclerView.visibility = View.VISIBLE
|
||||
if (author.yearMedia.isNullOrEmpty()) {
|
||||
binding.characterRecyclerView.visibility = View.GONE
|
||||
}
|
||||
val titlePosition = arrayListOf<Int>()
|
||||
val concatAdapter = ConcatAdapter()
|
||||
val map = author!!.yearMedia ?: return@observe
|
||||
val map = author.yearMedia ?: return@observe
|
||||
val keys = map.keys.toTypedArray()
|
||||
var pos = 0
|
||||
|
||||
@@ -80,6 +152,10 @@ class AuthorActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
val desc = createDesc(author)
|
||||
val markWon = Markwon.builder(this).usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(SpoilerPlugin()).build()
|
||||
markWon.setMarkdown(binding.authorCharacterDesc, desc)
|
||||
for (i in keys.indices) {
|
||||
val medias = map[keys[i]]!!
|
||||
val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size
|
||||
@@ -90,18 +166,18 @@ class AuthorActivity : AppCompatActivity() {
|
||||
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
|
||||
concatAdapter.addAdapter(EmptyAdapter(empty))
|
||||
}
|
||||
binding.studioRecycler.adapter = concatAdapter
|
||||
binding.studioRecycler.layoutManager = gridLayoutManager
|
||||
binding.characterRecyclerView.adapter = concatAdapter
|
||||
binding.characterRecyclerView.layoutManager = gridLayoutManager
|
||||
|
||||
binding.charactersRecycler.visibility = View.VISIBLE
|
||||
binding.charactersText.visibility = View.VISIBLE
|
||||
binding.charactersRecycler.adapter =
|
||||
CharacterAdapter(author!!.character ?: arrayListOf())
|
||||
binding.charactersRecycler.layoutManager =
|
||||
binding.authorCharactersRecycler.visibility = View.VISIBLE
|
||||
binding.AuthorCharactersText.visibility = View.VISIBLE
|
||||
binding.authorCharactersRecycler.adapter =
|
||||
CharacterAdapter(author.character ?: arrayListOf())
|
||||
binding.authorCharactersRecycler.layoutManager =
|
||||
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
if (author!!.character.isNullOrEmpty()) {
|
||||
binding.charactersRecycler.visibility = View.GONE
|
||||
binding.charactersText.visibility = View.GONE
|
||||
if (author.character.isNullOrEmpty()) {
|
||||
binding.authorCharactersRecycler.visibility = View.GONE
|
||||
binding.AuthorCharactersText.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,14 +185,28 @@ class AuthorActivity : AppCompatActivity() {
|
||||
live.observe(this) {
|
||||
if (it) {
|
||||
scope.launch {
|
||||
if (author != null)
|
||||
withContext(Dispatchers.IO) { model.loadAuthor(author!!) }
|
||||
withContext(Dispatchers.IO) { model.loadAuthor(author) }
|
||||
live.postValue(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDesc(author: Author): String {
|
||||
val age = if (author.age != null) "${getString(R.string.age)} ${author.age}" else ""
|
||||
val yearsActive =
|
||||
if (author.yearsActive != null) "${getString(R.string.years_active)} ${author.yearsActive}" else ""
|
||||
val dob =
|
||||
if (author.dateOfBirth != null) "${getString(R.string.birthday)} ${author.dateOfBirth}" else ""
|
||||
val homeTown =
|
||||
if (author.homeTown != null) "${getString(R.string.hometown)} ${author.homeTown}" else ""
|
||||
val dod =
|
||||
if (author.dateOfDeath != null) "${getString(R.string.date_of_death)} ${author.dateOfDeath}" else ""
|
||||
|
||||
return "$age $yearsActive $dob $homeTown $dod"
|
||||
}
|
||||
|
||||
|
||||
override fun onDestroy() {
|
||||
if (Refresh.activity.containsKey(this.hashCode())) {
|
||||
Refresh.activity.remove(this.hashCode())
|
||||
@@ -125,7 +215,31 @@ class AuthorActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onOffsetChanged(appBar: AppBarLayout, i: Int) {
|
||||
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
|
||||
val percentage = abs(i) * 100 / mMaxScrollSize
|
||||
val cap = clamp((percent - percentage) / percent.toFloat(), 0f, 1f)
|
||||
|
||||
binding.characterCover.scaleX = 1f * cap
|
||||
binding.characterCover.scaleY = 1f * cap
|
||||
binding.characterCover.cardElevation = 32f * cap
|
||||
|
||||
binding.characterCover.visibility =
|
||||
if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
|
||||
if (percentage >= percent && !isCollapsed) {
|
||||
isCollapsed = true
|
||||
if (immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg)
|
||||
}
|
||||
if (percentage <= percent && isCollapsed) {
|
||||
isCollapsed = false
|
||||
if (immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.transparent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import ani.dantotsu.setAnimation
|
||||
import java.io.Serializable
|
||||
|
||||
class AuthorAdapter(
|
||||
private val authorList: ArrayList<Author>,
|
||||
private val authorList: MutableList<Author>,
|
||||
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
|
||||
val binding =
|
||||
@@ -26,7 +26,7 @@ class AuthorAdapter(
|
||||
override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
setAnimation(binding.root.context, holder.binding.root)
|
||||
val author = authorList[position]
|
||||
val author = authorList.getOrNull(position) ?: return
|
||||
binding.itemCompactRelation.text = author.role
|
||||
binding.itemCompactImage.loadImage(author.image)
|
||||
binding.itemCompactTitle.text = author.name
|
||||
|
||||
@@ -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,13 +9,14 @@ 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
|
||||
import java.io.Serializable
|
||||
|
||||
class CharacterAdapter(
|
||||
private val characterList: ArrayList<Character>
|
||||
private val characterList: MutableList<Character>
|
||||
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
|
||||
val binding =
|
||||
@@ -26,9 +27,8 @@ class CharacterAdapter(
|
||||
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
setAnimation(binding.root.context, holder.binding.root)
|
||||
val character = characterList[position]
|
||||
val whitespace = "${character.role} "
|
||||
character.voiceActor
|
||||
val character = characterList.getOrNull(position) ?: return
|
||||
val whitespace = "${if (character.role.lowercase() == "null") "" else character.role} "
|
||||
binding.itemCompactRelation.text = whitespace
|
||||
binding.itemCompactImage.loadImage(character.image)
|
||||
binding.itemCompactTitle.text = character.name
|
||||
@@ -55,6 +55,11 @@ class CharacterAdapter(
|
||||
).toBundle()
|
||||
)
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
copyToClipboard(
|
||||
characterList[bindingAdapterPosition].name ?: ""
|
||||
); true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -45,6 +46,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
private lateinit var character: Character
|
||||
private var loaded = false
|
||||
|
||||
private var isCollapsed = false
|
||||
private val percent = 30
|
||||
private var mMaxScrollSize = 0
|
||||
private var screenWidth: Float = 0f
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -71,6 +77,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
binding.characterClose.setOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
binding.authorCharactersRecycler.isVisible = false
|
||||
binding.AuthorCharactersText.isVisible = false
|
||||
binding.authorCharacterDesc.isVisible = false
|
||||
|
||||
character = intent.getSerialized("character") ?: return
|
||||
binding.characterTitle.text = character.name
|
||||
banner.loadImage(character.banner)
|
||||
@@ -158,11 +169,6 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
private var isCollapsed = false
|
||||
private val percent = 30
|
||||
private var mMaxScrollSize = 0
|
||||
private var screenWidth: Float = 0f
|
||||
|
||||
override fun onOffsetChanged(appBar: AppBarLayout, i: Int) {
|
||||
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
|
||||
val percentage = abs(i) * 100 / mMaxScrollSize
|
||||
|
||||
@@ -7,11 +7,9 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.buildMarkwon
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.ItemCharacterDetailsBinding
|
||||
import ani.dantotsu.others.SpoilerPlugin
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
|
||||
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
|
||||
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
|
||||
@@ -24,7 +22,9 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
||||
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val desc =
|
||||
(if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") +
|
||||
(if (character.id == 4004)
|
||||
" \n" else "") +
|
||||
(if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") +
|
||||
(if (character.dateOfBirth.toString() != "")
|
||||
"${currActivity()!!.getString(R.string.birthday)} ${character.dateOfBirth.toString()}" else "") +
|
||||
(if (character.gender != "null")
|
||||
@@ -41,8 +41,7 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
||||
} else "") + "\n" + character.description
|
||||
|
||||
binding.characterDesc.isTextSelectable
|
||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(SpoilerPlugin()).build()
|
||||
val markWon = buildMarkwon(activity)
|
||||
markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
|
||||
binding.voiceActorRecycler.adapter = AuthorAdapter(character.voiceActor ?: arrayListOf())
|
||||
binding.voiceActorRecycler.layoutManager = LinearLayoutManager(
|
||||
|
||||
77
app/src/main/java/ani/dantotsu/media/HeaderInterface.kt
Normal file
77
app/src/main/java/ani/dantotsu/media/HeaderInterface.kt
Normal file
@@ -0,0 +1,77 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.databinding.ItemSearchHeaderBinding
|
||||
|
||||
abstract class HeaderInterface : RecyclerView.Adapter<HeaderInterface.SearchHeaderViewHolder>() {
|
||||
private val itemViewType = 6969
|
||||
var search: Runnable? = null
|
||||
var requestFocus: Runnable? = null
|
||||
protected var textWatcher: TextWatcher? = null
|
||||
protected lateinit var searchHistoryAdapter: SearchHistoryAdapter
|
||||
protected lateinit var binding: ItemSearchHeaderBinding
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
||||
val binding =
|
||||
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SearchHeaderViewHolder(binding)
|
||||
}
|
||||
|
||||
fun setHistoryVisibility(visible: Boolean) {
|
||||
if (visible) {
|
||||
binding.searchResultLayout.startAnimation(fadeOutAnimation())
|
||||
binding.searchHistoryList.startAnimation(fadeInAnimation())
|
||||
binding.searchResultLayout.visibility = View.GONE
|
||||
binding.searchHistoryList.visibility = View.VISIBLE
|
||||
binding.searchByImage.visibility = View.VISIBLE
|
||||
} else {
|
||||
if (binding.searchResultLayout.visibility != View.VISIBLE) {
|
||||
binding.searchResultLayout.startAnimation(fadeInAnimation())
|
||||
binding.searchHistoryList.startAnimation(fadeOutAnimation())
|
||||
}
|
||||
|
||||
binding.searchResultLayout.visibility = View.VISIBLE
|
||||
binding.clearHistory.visibility = View.GONE
|
||||
binding.searchHistoryList.visibility = View.GONE
|
||||
binding.searchByImage.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun fadeInAnimation(): Animation {
|
||||
return AlphaAnimation(0f, 1f).apply {
|
||||
duration = 150
|
||||
}
|
||||
}
|
||||
|
||||
protected fun fadeOutAnimation(): Animation {
|
||||
return AlphaAnimation(1f, 0f).apply {
|
||||
duration = 150
|
||||
}
|
||||
}
|
||||
|
||||
protected fun updateClearHistoryVisibility() {
|
||||
binding.clearHistory.visibility =
|
||||
if (searchHistoryAdapter.itemCount > 0) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun addHistory() {
|
||||
if (::searchHistoryAdapter.isInitialized && binding.searchBarText.text.toString()
|
||||
.isNotBlank()
|
||||
) searchHistoryAdapter.add(binding.searchBarText.text.toString())
|
||||
}
|
||||
|
||||
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return itemViewType
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -251,10 +251,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
fun total() {
|
||||
val text = SpannableStringBuilder().apply {
|
||||
|
||||
val white = this@MediaDetailsActivity.getThemeColor(com.google.android.material.R.attr.colorOnBackground)
|
||||
val white =
|
||||
this@MediaDetailsActivity.getThemeColor(com.google.android.material.R.attr.colorOnBackground)
|
||||
if (media.userStatus != null) {
|
||||
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
||||
val colorSecondary = getThemeColor(com.google.android.material.R.attr.colorSecondary)
|
||||
val colorSecondary =
|
||||
getThemeColor(com.google.android.material.R.attr.colorSecondary)
|
||||
bold { color(colorSecondary) { append("${media.userProgress}") } }
|
||||
append(
|
||||
if (media.anime != null) getString(R.string.episodes_out_of) else getString(
|
||||
@@ -293,7 +295,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 +374,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 +428,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
|
||||
@@ -100,6 +101,16 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,8 @@ import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import java.util.ArrayList
|
||||
|
||||
class MediaListViewActivity: AppCompatActivity() {
|
||||
class MediaListViewActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMediaListViewBinding
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -52,7 +51,8 @@ class MediaListViewActivity: AppCompatActivity() {
|
||||
binding.listAppBar.setBackgroundColor(primaryColor)
|
||||
binding.listTitle.setTextColor(primaryTextColor)
|
||||
val screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
val mediaList = passedMedia ?: intent.getSerialized("media") as? ArrayList<Media> ?: ArrayList()
|
||||
val mediaList =
|
||||
passedMedia ?: intent.getSerialized("media") as? ArrayList<Media> ?: ArrayList()
|
||||
if (passedMedia != null) passedMedia = null
|
||||
val view = PrefManager.getCustomVal("mediaView", 0)
|
||||
var mediaView: View = when (view) {
|
||||
|
||||
@@ -44,7 +44,10 @@ class MediaSocialAdapter(
|
||||
profileUserName.text = user.name
|
||||
profileInfo.apply {
|
||||
text = when (user.status) {
|
||||
"CURRENT" -> if (type == "ANIME") getAppString(R.string.watching) else getAppString(R.string.reading)
|
||||
"CURRENT" -> if (type == "ANIME") getAppString(R.string.watching) else getAppString(
|
||||
R.string.reading
|
||||
)
|
||||
|
||||
else -> user.status ?: ""
|
||||
}
|
||||
visibility = View.VISIBLE
|
||||
@@ -63,10 +66,12 @@ class MediaSocialAdapter(
|
||||
profileCompactProgressContainer.visibility = View.VISIBLE
|
||||
|
||||
profileUserAvatar.setOnClickListener {
|
||||
ContextCompat.startActivity(root.context,
|
||||
ContextCompat.startActivity(
|
||||
root.context,
|
||||
Intent(root.context, ProfileActivity::class.java)
|
||||
.putExtra("userId", user.id),
|
||||
null)
|
||||
null
|
||||
)
|
||||
}
|
||||
profileUserAvatarContainer.setOnLongClickListener {
|
||||
ImageViewDialog.newInstance(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.databinding.ItemReviewsBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.openImage
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.activity.ActivityItemBuilder
|
||||
import ani.dantotsu.toast
|
||||
@@ -40,7 +32,7 @@ class ReviewAdapter(
|
||||
binding.reviewUserAvatar.loadImage(review.user?.avatar?.medium)
|
||||
binding.reviewText.text = review.summary
|
||||
binding.reviewPostTime.text = ActivityItemBuilder.getDateTime(review.createdAt)
|
||||
val text = "[${review.score/ 10.0f}]"
|
||||
val text = "[${review.score / 10.0f}]"
|
||||
binding.reviewTag.text = text
|
||||
binding.root.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
@@ -85,6 +77,7 @@ class ReviewAdapter(
|
||||
override fun initializeViewBinding(view: View): ItemReviewsBinding {
|
||||
return ItemReviewsBinding.bind(view)
|
||||
}
|
||||
|
||||
private fun userVote(type: String) {
|
||||
when (type) {
|
||||
"NO_VOTE" -> {
|
||||
|
||||
@@ -20,7 +20,6 @@ import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.openImage
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.activity.ActivityItemBuilder
|
||||
import ani.dantotsu.statusBarHeight
|
||||
@@ -52,8 +51,9 @@ class ReviewViewActivity : AppCompatActivity() {
|
||||
binding.userAvatar.loadImage(review.user?.avatar?.medium)
|
||||
binding.userTime.text = ActivityItemBuilder.getDateTime(review.createdAt)
|
||||
binding.userContainer.setOnClickListener {
|
||||
startActivity(Intent(this, ProfileActivity::class.java)
|
||||
.putExtra("userId", review.user?.id)
|
||||
startActivity(
|
||||
Intent(this, ProfileActivity::class.java)
|
||||
.putExtra("userId", review.user?.id)
|
||||
)
|
||||
}
|
||||
binding.userAvatar.openImage(
|
||||
@@ -61,8 +61,9 @@ class ReviewViewActivity : AppCompatActivity() {
|
||||
review.user?.avatar?.medium ?: ""
|
||||
)
|
||||
binding.userAvatar.setOnClickListener {
|
||||
startActivity(Intent(this, ProfileActivity::class.java)
|
||||
.putExtra("userId", review.user?.id)
|
||||
startActivity(
|
||||
Intent(this, ProfileActivity::class.java)
|
||||
.putExtra("userId", review.user?.id)
|
||||
)
|
||||
}
|
||||
binding.profileUserBio.settings.loadWithOverviewMode = true
|
||||
|
||||
@@ -13,12 +13,18 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.connections.anilist.AniMangaSearchResults
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
|
||||
import ani.dantotsu.connections.anilist.CharacterSearchResults
|
||||
import ani.dantotsu.connections.anilist.StaffSearchResults
|
||||
import ani.dantotsu.connections.anilist.StudioSearchResults
|
||||
import ani.dantotsu.connections.anilist.UserSearchResults
|
||||
import ani.dantotsu.databinding.ActivitySearchBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.profile.UsersAdapter
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
@@ -35,14 +41,25 @@ class SearchActivity : AppCompatActivity() {
|
||||
val model: AnilistSearch by viewModels()
|
||||
|
||||
var style: Int = 0
|
||||
lateinit var searchType: SearchType
|
||||
private var screenWidth: Float = 0f
|
||||
|
||||
private lateinit var mediaAdaptor: MediaAdaptor
|
||||
private lateinit var characterAdaptor: CharacterAdapter
|
||||
private lateinit var studioAdaptor: StudioAdapter
|
||||
private lateinit var staffAdaptor: AuthorAdapter
|
||||
private lateinit var usersAdapter: UsersAdapter
|
||||
|
||||
private lateinit var progressAdapter: ProgressAdapter
|
||||
private lateinit var concatAdapter: ConcatAdapter
|
||||
private lateinit var headerAdaptor: SearchAdapter
|
||||
private lateinit var headerAdaptor: HeaderInterface
|
||||
|
||||
lateinit var aniMangaResult: AniMangaSearchResults
|
||||
lateinit var characterResult: CharacterSearchResults
|
||||
lateinit var studioResult: StudioSearchResults
|
||||
lateinit var staffResult: StaffSearchResults
|
||||
lateinit var userResult: UserSearchResults
|
||||
|
||||
lateinit var result: SearchResults
|
||||
lateinit var updateChips: (() -> Unit)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -59,39 +76,117 @@ class SearchActivity : AppCompatActivity() {
|
||||
bottom = navBarHeight + 80f.px
|
||||
)
|
||||
|
||||
style = PrefManager.getVal(PrefName.SearchStyle)
|
||||
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
|
||||
if (!listOnly!!) listOnly = null
|
||||
|
||||
val notSet = model.notSet
|
||||
if (model.notSet) {
|
||||
model.notSet = false
|
||||
model.searchResults = SearchResults(
|
||||
intent.getStringExtra("type") ?: "ANIME",
|
||||
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
|
||||
onList = listOnly,
|
||||
search = intent.getStringExtra("query"),
|
||||
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
|
||||
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
|
||||
sort = intent.getStringExtra("sortBy"),
|
||||
status = intent.getStringExtra("status"),
|
||||
source = intent.getStringExtra("source"),
|
||||
countryOfOrigin = intent.getStringExtra("country"),
|
||||
season = intent.getStringExtra("season"),
|
||||
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")
|
||||
?.toIntOrNull() else null,
|
||||
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")
|
||||
?.toIntOrNull() else null,
|
||||
results = mutableListOf(),
|
||||
hasNextPage = false
|
||||
)
|
||||
searchType = SearchType.fromString(intent.getStringExtra("type") ?: "ANIME")
|
||||
when (searchType) {
|
||||
SearchType.ANIME, SearchType.MANGA -> {
|
||||
style = PrefManager.getVal(PrefName.SearchStyle)
|
||||
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
|
||||
if (!listOnly!!) listOnly = null
|
||||
|
||||
if (model.notSet) {
|
||||
model.notSet = false
|
||||
model.aniMangaSearchResults = AniMangaSearchResults(
|
||||
intent.getStringExtra("type") ?: "ANIME",
|
||||
isAdult = if (Anilist.adult) intent.getBooleanExtra(
|
||||
"hentai",
|
||||
false
|
||||
) else false,
|
||||
onList = listOnly,
|
||||
search = intent.getStringExtra("query"),
|
||||
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
|
||||
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
|
||||
sort = intent.getStringExtra("sortBy"),
|
||||
status = intent.getStringExtra("status"),
|
||||
source = intent.getStringExtra("source"),
|
||||
countryOfOrigin = intent.getStringExtra("country"),
|
||||
season = intent.getStringExtra("season"),
|
||||
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra(
|
||||
"seasonYear"
|
||||
)
|
||||
?.toIntOrNull() else null,
|
||||
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra(
|
||||
"seasonYear"
|
||||
)
|
||||
?.toIntOrNull() else null,
|
||||
results = mutableListOf(),
|
||||
hasNextPage = false
|
||||
)
|
||||
}
|
||||
|
||||
aniMangaResult = model.aniMangaSearchResults
|
||||
mediaAdaptor =
|
||||
MediaAdaptor(
|
||||
style,
|
||||
model.aniMangaSearchResults.results,
|
||||
this,
|
||||
matchParent = true
|
||||
)
|
||||
}
|
||||
|
||||
SearchType.CHARACTER -> {
|
||||
if (model.notSet) {
|
||||
model.notSet = false
|
||||
model.characterSearchResults = CharacterSearchResults(
|
||||
search = intent.getStringExtra("query"),
|
||||
results = mutableListOf(),
|
||||
hasNextPage = false
|
||||
)
|
||||
|
||||
characterResult = model.characterSearchResults
|
||||
characterAdaptor = CharacterAdapter(model.characterSearchResults.results)
|
||||
}
|
||||
}
|
||||
|
||||
SearchType.STUDIO -> {
|
||||
if (model.notSet) {
|
||||
model.notSet = false
|
||||
model.studioSearchResults = StudioSearchResults(
|
||||
search = intent.getStringExtra("query"),
|
||||
results = mutableListOf(),
|
||||
hasNextPage = false
|
||||
)
|
||||
|
||||
studioResult = model.studioSearchResults
|
||||
studioAdaptor = StudioAdapter(model.studioSearchResults.results)
|
||||
}
|
||||
}
|
||||
|
||||
SearchType.STAFF -> {
|
||||
if (model.notSet) {
|
||||
model.notSet = false
|
||||
model.staffSearchResults = StaffSearchResults(
|
||||
search = intent.getStringExtra("query"),
|
||||
results = mutableListOf(),
|
||||
hasNextPage = false
|
||||
)
|
||||
|
||||
staffResult = model.staffSearchResults
|
||||
staffAdaptor = AuthorAdapter(model.staffSearchResults.results)
|
||||
}
|
||||
}
|
||||
|
||||
SearchType.USER -> {
|
||||
if (model.notSet) {
|
||||
model.notSet = false
|
||||
model.userSearchResults = UserSearchResults(
|
||||
search = intent.getStringExtra("query"),
|
||||
results = mutableListOf(),
|
||||
hasNextPage = false
|
||||
)
|
||||
|
||||
userResult = model.userSearchResults
|
||||
usersAdapter = UsersAdapter(model.userSearchResults.results, grid = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = model.searchResults
|
||||
|
||||
progressAdapter = ProgressAdapter(searched = model.searched)
|
||||
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
|
||||
headerAdaptor = SearchAdapter(this, model.searchResults.type)
|
||||
headerAdaptor = if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) {
|
||||
SearchAdapter(this, searchType)
|
||||
} else {
|
||||
SupportingSearchAdapter(this, searchType)
|
||||
}
|
||||
|
||||
val gridSize = (screenWidth / 120f).toInt()
|
||||
val gridLayoutManager = GridLayoutManager(this, gridSize)
|
||||
@@ -108,7 +203,27 @@ class SearchActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
concatAdapter = ConcatAdapter(headerAdaptor, mediaAdaptor, progressAdapter)
|
||||
concatAdapter = when (searchType) {
|
||||
SearchType.ANIME, SearchType.MANGA -> {
|
||||
ConcatAdapter(headerAdaptor, mediaAdaptor, progressAdapter)
|
||||
}
|
||||
|
||||
SearchType.CHARACTER -> {
|
||||
ConcatAdapter(headerAdaptor, characterAdaptor, progressAdapter)
|
||||
}
|
||||
|
||||
SearchType.STUDIO -> {
|
||||
ConcatAdapter(headerAdaptor, studioAdaptor, progressAdapter)
|
||||
}
|
||||
|
||||
SearchType.STAFF -> {
|
||||
ConcatAdapter(headerAdaptor, staffAdaptor, progressAdapter)
|
||||
}
|
||||
|
||||
SearchType.USER -> {
|
||||
ConcatAdapter(headerAdaptor, usersAdapter, progressAdapter)
|
||||
}
|
||||
}
|
||||
|
||||
binding.searchRecyclerView.layoutManager = gridLayoutManager
|
||||
binding.searchRecyclerView.adapter = concatAdapter
|
||||
@@ -117,9 +232,9 @@ class SearchActivity : AppCompatActivity() {
|
||||
RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
|
||||
if (!v.canScrollVertically(1)) {
|
||||
if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) {
|
||||
if (model.hasNextPage(searchType) && model.resultsIsNotEmpty(searchType) && !loading) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadNextPage(model.searchResults)
|
||||
model.loadNextPage(searchType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,34 +242,110 @@ class SearchActivity : AppCompatActivity() {
|
||||
}
|
||||
})
|
||||
|
||||
model.getSearch().observe(this) {
|
||||
if (it != null) {
|
||||
model.searchResults.apply {
|
||||
onList = it.onList
|
||||
isAdult = it.isAdult
|
||||
perPage = it.perPage
|
||||
search = it.search
|
||||
sort = it.sort
|
||||
genres = it.genres
|
||||
excludedGenres = it.excludedGenres
|
||||
excludedTags = it.excludedTags
|
||||
tags = it.tags
|
||||
season = it.season
|
||||
startYear = it.startYear
|
||||
seasonYear = it.seasonYear
|
||||
status = it.status
|
||||
source = it.source
|
||||
format = it.format
|
||||
countryOfOrigin = it.countryOfOrigin
|
||||
page = it.page
|
||||
hasNextPage = it.hasNextPage
|
||||
when (searchType) {
|
||||
SearchType.ANIME, SearchType.MANGA -> {
|
||||
model.getSearch<AniMangaSearchResults>(searchType).observe(this) {
|
||||
if (it != null) {
|
||||
model.aniMangaSearchResults.apply {
|
||||
onList = it.onList
|
||||
isAdult = it.isAdult
|
||||
perPage = it.perPage
|
||||
search = it.search
|
||||
sort = it.sort
|
||||
genres = it.genres
|
||||
excludedGenres = it.excludedGenres
|
||||
excludedTags = it.excludedTags
|
||||
tags = it.tags
|
||||
season = it.season
|
||||
startYear = it.startYear
|
||||
seasonYear = it.seasonYear
|
||||
status = it.status
|
||||
source = it.source
|
||||
format = it.format
|
||||
countryOfOrigin = it.countryOfOrigin
|
||||
page = it.page
|
||||
hasNextPage = it.hasNextPage
|
||||
}
|
||||
|
||||
val prev = model.aniMangaSearchResults.results.size
|
||||
model.aniMangaSearchResults.results.addAll(it.results)
|
||||
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
|
||||
|
||||
progressAdapter.bar?.isVisible = it.hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val prev = model.searchResults.results.size
|
||||
model.searchResults.results.addAll(it.results)
|
||||
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
|
||||
SearchType.CHARACTER -> {
|
||||
model.getSearch<CharacterSearchResults>(searchType).observe(this) {
|
||||
if (it != null) {
|
||||
model.characterSearchResults.apply {
|
||||
search = it.search
|
||||
page = it.page
|
||||
hasNextPage = it.hasNextPage
|
||||
}
|
||||
|
||||
progressAdapter.bar?.isVisible = it.hasNextPage
|
||||
val prev = model.characterSearchResults.results.size
|
||||
model.characterSearchResults.results.addAll(it.results)
|
||||
characterAdaptor.notifyItemRangeInserted(prev, it.results.size)
|
||||
|
||||
progressAdapter.bar?.isVisible = it.hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchType.STUDIO -> {
|
||||
model.getSearch<StudioSearchResults>(searchType).observe(this) {
|
||||
if (it != null) {
|
||||
model.studioSearchResults.apply {
|
||||
search = it.search
|
||||
page = it.page
|
||||
hasNextPage = it.hasNextPage
|
||||
}
|
||||
|
||||
val prev = model.studioSearchResults.results.size
|
||||
model.studioSearchResults.results.addAll(it.results)
|
||||
studioAdaptor.notifyItemRangeInserted(prev, it.results.size)
|
||||
|
||||
progressAdapter.bar?.isVisible = it.hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchType.STAFF -> {
|
||||
model.getSearch<StaffSearchResults>(searchType).observe(this) {
|
||||
if (it != null) {
|
||||
model.staffSearchResults.apply {
|
||||
search = it.search
|
||||
page = it.page
|
||||
hasNextPage = it.hasNextPage
|
||||
}
|
||||
|
||||
val prev = model.staffSearchResults.results.size
|
||||
model.staffSearchResults.results.addAll(it.results)
|
||||
staffAdaptor.notifyItemRangeInserted(prev, it.results.size)
|
||||
|
||||
progressAdapter.bar?.isVisible = it.hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchType.USER -> {
|
||||
model.getSearch<UserSearchResults>(searchType).observe(this) {
|
||||
if (it != null) {
|
||||
model.userSearchResults.apply {
|
||||
search = it.search
|
||||
page = it.page
|
||||
hasNextPage = it.hasNextPage
|
||||
}
|
||||
|
||||
val prev = model.userSearchResults.results.size
|
||||
model.userSearchResults.results.addAll(it.results)
|
||||
usersAdapter.notifyItemRangeInserted(prev, it.results.size)
|
||||
|
||||
progressAdapter.bar?.isVisible = it.hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +370,35 @@ class SearchActivity : AppCompatActivity() {
|
||||
fun emptyMediaAdapter() {
|
||||
searchTimer.cancel()
|
||||
searchTimer.purge()
|
||||
mediaAdaptor.notifyItemRangeRemoved(0, model.searchResults.results.size)
|
||||
model.searchResults.results.clear()
|
||||
when (searchType) {
|
||||
SearchType.ANIME, SearchType.MANGA -> {
|
||||
mediaAdaptor.notifyItemRangeRemoved(0, model.aniMangaSearchResults.results.size)
|
||||
model.aniMangaSearchResults.results.clear()
|
||||
}
|
||||
|
||||
SearchType.CHARACTER -> {
|
||||
characterAdaptor.notifyItemRangeRemoved(
|
||||
0,
|
||||
model.characterSearchResults.results.size
|
||||
)
|
||||
model.characterSearchResults.results.clear()
|
||||
}
|
||||
|
||||
SearchType.STUDIO -> {
|
||||
studioAdaptor.notifyItemRangeRemoved(0, model.studioSearchResults.results.size)
|
||||
model.studioSearchResults.results.clear()
|
||||
}
|
||||
|
||||
SearchType.STAFF -> {
|
||||
staffAdaptor.notifyItemRangeRemoved(0, model.staffSearchResults.results.size)
|
||||
model.staffSearchResults.results.clear()
|
||||
}
|
||||
|
||||
SearchType.USER -> {
|
||||
usersAdapter.notifyItemRangeRemoved(0, model.userSearchResults.results.size)
|
||||
model.userSearchResults.results.clear()
|
||||
}
|
||||
}
|
||||
progressAdapter.bar?.visibility = View.GONE
|
||||
}
|
||||
|
||||
@@ -188,10 +406,30 @@ class SearchActivity : AppCompatActivity() {
|
||||
private var loading = false
|
||||
fun search() {
|
||||
headerAdaptor.setHistoryVisibility(false)
|
||||
val size = model.searchResults.results.size
|
||||
model.searchResults.results.clear()
|
||||
val size = model.size(searchType)
|
||||
model.clearResults(searchType)
|
||||
binding.searchRecyclerView.post {
|
||||
mediaAdaptor.notifyItemRangeRemoved(0, size)
|
||||
when (searchType) {
|
||||
SearchType.ANIME, SearchType.MANGA -> {
|
||||
mediaAdaptor.notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
|
||||
SearchType.CHARACTER -> {
|
||||
characterAdaptor.notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
|
||||
SearchType.STUDIO -> {
|
||||
studioAdaptor.notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
|
||||
SearchType.STAFF -> {
|
||||
staffAdaptor.notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
|
||||
SearchType.USER -> {
|
||||
usersAdapter.notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progressAdapter.bar?.visibility = View.VISIBLE
|
||||
@@ -202,7 +440,7 @@ class SearchActivity : AppCompatActivity() {
|
||||
override fun run() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
loading = true
|
||||
model.loadSearch(result)
|
||||
model.loadSearch(searchType)
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
@@ -213,8 +451,10 @@ class SearchActivity : AppCompatActivity() {
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun recycler() {
|
||||
mediaAdaptor.type = style
|
||||
mediaAdaptor.notifyDataSetChanged()
|
||||
if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) {
|
||||
mediaAdaptor.type = style
|
||||
mediaAdaptor.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
var state: Parcelable? = null
|
||||
|
||||
@@ -9,8 +9,6 @@ import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.PopupMenu
|
||||
@@ -22,8 +20,8 @@ import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
|
||||
import ani.dantotsu.App.Companion.context
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.databinding.ItemSearchHeaderBinding
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.imagesearch.ImageSearchActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
@@ -36,18 +34,11 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class SearchAdapter(private val activity: SearchActivity, private val type: String) :
|
||||
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
|
||||
private val itemViewType = 6969
|
||||
var search: Runnable? = null
|
||||
var requestFocus: Runnable? = null
|
||||
private var textWatcher: TextWatcher? = null
|
||||
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
|
||||
private lateinit var binding: ItemSearchHeaderBinding
|
||||
class SearchAdapter(private val activity: SearchActivity, private val type: SearchType) :
|
||||
HeaderInterface() {
|
||||
|
||||
private fun updateFilterTextViewDrawable() {
|
||||
val filterDrawable = when (activity.result.sort) {
|
||||
val filterDrawable = when (activity.aniMangaResult.sort) {
|
||||
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
|
||||
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
|
||||
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
|
||||
@@ -60,18 +51,13 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
|
||||
val binding =
|
||||
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SearchHeaderViewHolder(binding)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
|
||||
binding = holder.binding
|
||||
|
||||
searchHistoryAdapter = SearchHistoryAdapter(type) {
|
||||
binding.searchBarText.setText(it)
|
||||
binding.searchBarText.setSelection(it.length)
|
||||
}
|
||||
binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context)
|
||||
binding.searchHistoryList.adapter = searchHistoryAdapter
|
||||
@@ -79,6 +65,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
val imm: InputMethodManager =
|
||||
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
if (activity.searchType != SearchType.MANGA && activity.searchType != SearchType.ANIME) {
|
||||
throw IllegalArgumentException("Invalid search type (wrong adapter)")
|
||||
}
|
||||
|
||||
when (activity.style) {
|
||||
0 -> {
|
||||
binding.searchResultGrid.alpha = 1f
|
||||
@@ -91,7 +81,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
}
|
||||
}
|
||||
|
||||
binding.searchBar.hint = activity.result.type
|
||||
binding.searchBar.hint = activity.aniMangaResult.type
|
||||
if (PrefManager.getVal(PrefName.Incognito)) {
|
||||
val startIconDrawableRes = R.drawable.ic_incognito_24
|
||||
val startIconDrawable: Drawable? =
|
||||
@@ -99,11 +89,11 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
binding.searchBar.startIconDrawable = startIconDrawable
|
||||
}
|
||||
|
||||
var adult = activity.result.isAdult
|
||||
var listOnly = activity.result.onList
|
||||
var adult = activity.aniMangaResult.isAdult
|
||||
var listOnly = activity.aniMangaResult.onList
|
||||
|
||||
binding.searchBarText.removeTextChangedListener(textWatcher)
|
||||
binding.searchBarText.setText(activity.result.search)
|
||||
binding.searchBarText.setText(activity.aniMangaResult.search)
|
||||
|
||||
binding.searchAdultCheck.isChecked = adult
|
||||
binding.searchList.isChecked = listOnly == true
|
||||
@@ -124,49 +114,49 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
popupMenu.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.sort_by_score -> {
|
||||
activity.result.sort = Anilist.sortBy[0]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[0]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
|
||||
R.id.sort_by_popular -> {
|
||||
activity.result.sort = Anilist.sortBy[1]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[1]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
|
||||
R.id.sort_by_trending -> {
|
||||
activity.result.sort = Anilist.sortBy[2]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[2]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
|
||||
R.id.sort_by_recent -> {
|
||||
activity.result.sort = Anilist.sortBy[3]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[3]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
|
||||
R.id.sort_by_a_z -> {
|
||||
activity.result.sort = Anilist.sortBy[4]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[4]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
|
||||
R.id.sort_by_z_a -> {
|
||||
activity.result.sort = Anilist.sortBy[5]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[5]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
}
|
||||
|
||||
R.id.sort_by_pure_pain -> {
|
||||
activity.result.sort = Anilist.sortBy[6]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[6]
|
||||
activity.updateChips.invoke()
|
||||
activity.search()
|
||||
updateFilterTextViewDrawable()
|
||||
@@ -177,14 +167,20 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
popupMenu.show()
|
||||
true
|
||||
}
|
||||
if (activity.result.type != "ANIME") {
|
||||
if (activity.aniMangaResult.type != "ANIME") {
|
||||
binding.searchByImage.visibility = View.GONE
|
||||
}
|
||||
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 {
|
||||
activity.aniMangaResult.apply {
|
||||
search =
|
||||
if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
|
||||
onList = listOnly
|
||||
@@ -286,61 +282,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
requestFocus = Runnable { binding.searchBarText.requestFocus() }
|
||||
}
|
||||
|
||||
fun setHistoryVisibility(visible: Boolean) {
|
||||
if (visible) {
|
||||
binding.searchResultLayout.startAnimation(fadeOutAnimation())
|
||||
binding.searchHistoryList.startAnimation(fadeInAnimation())
|
||||
binding.searchResultLayout.visibility = View.GONE
|
||||
binding.searchHistoryList.visibility = View.VISIBLE
|
||||
binding.searchByImage.visibility = View.VISIBLE
|
||||
} else {
|
||||
if (binding.searchResultLayout.visibility != View.VISIBLE) {
|
||||
binding.searchResultLayout.startAnimation(fadeInAnimation())
|
||||
binding.searchHistoryList.startAnimation(fadeOutAnimation())
|
||||
}
|
||||
|
||||
binding.searchResultLayout.visibility = View.VISIBLE
|
||||
binding.searchHistoryList.visibility = View.GONE
|
||||
binding.searchByImage.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun fadeInAnimation(): Animation {
|
||||
return AlphaAnimation(0f, 1f).apply {
|
||||
duration = 150
|
||||
}
|
||||
}
|
||||
|
||||
private fun fadeOutAnimation(): Animation {
|
||||
return AlphaAnimation(1f, 0f).apply {
|
||||
duration = 150
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun addHistory() {
|
||||
if (::searchHistoryAdapter.isInitialized &&
|
||||
binding.searchBarText.text.toString().isNotBlank()
|
||||
)
|
||||
searchHistoryAdapter.add(binding.searchBarText.text.toString())
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return itemViewType
|
||||
}
|
||||
|
||||
|
||||
class SearchChipAdapter(
|
||||
val activity: SearchActivity,
|
||||
private val searchAdapter: SearchAdapter
|
||||
) :
|
||||
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
|
||||
private var chips = activity.result.toChipList()
|
||||
private var chips = activity.aniMangaResult.toChipList()
|
||||
|
||||
inner class SearchChipViewHolder(val binding: ItemChipBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
@@ -357,7 +304,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
holder.binding.root.apply {
|
||||
text = chip.text.replace("_", " ")
|
||||
setOnClickListener {
|
||||
activity.result.removeChip(chip)
|
||||
activity.aniMangaResult.removeChip(chip)
|
||||
update()
|
||||
activity.search()
|
||||
searchAdapter.updateFilterTextViewDrawable()
|
||||
@@ -367,7 +314,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun update() {
|
||||
chips = activity.result.toChipList()
|
||||
chips = activity.aniMangaResult.toChipList()
|
||||
notifyDataSetChanged()
|
||||
searchAdapter.updateFilterTextViewDrawable()
|
||||
}
|
||||
@@ -375,4 +322,3 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
|
||||
override fun getItemCount(): Int = chips.size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
private fun setSortByFilterImage() {
|
||||
val filterDrawable = when (activity.result.sort) {
|
||||
val filterDrawable = when (activity.aniMangaResult.sort) {
|
||||
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
|
||||
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
|
||||
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
|
||||
@@ -71,10 +71,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
private fun resetSearchFilter() {
|
||||
activity.result.sort = null
|
||||
activity.aniMangaResult.sort = null
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
|
||||
startBounceZoomAnimation(binding.sortByFilter)
|
||||
activity.result.countryOfOrigin = null
|
||||
activity.aniMangaResult.countryOfOrigin = null
|
||||
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
|
||||
startBounceZoomAnimation(binding.countryFilter)
|
||||
|
||||
@@ -98,10 +98,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
|
||||
activity = requireActivity() as SearchActivity
|
||||
|
||||
selectedGenres = activity.result.genres ?: mutableListOf()
|
||||
exGenres = activity.result.excludedGenres ?: mutableListOf()
|
||||
selectedTags = activity.result.tags ?: mutableListOf()
|
||||
exTags = activity.result.excludedTags ?: mutableListOf()
|
||||
selectedGenres = activity.aniMangaResult.genres ?: mutableListOf()
|
||||
exGenres = activity.aniMangaResult.excludedGenres ?: mutableListOf()
|
||||
selectedTags = activity.aniMangaResult.tags ?: mutableListOf()
|
||||
exTags = activity.aniMangaResult.excludedTags ?: mutableListOf()
|
||||
setSortByFilterImage()
|
||||
|
||||
binding.resetSearchFilter.setOnClickListener {
|
||||
@@ -126,7 +126,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
resetSearchFilter()
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
activity.result.apply {
|
||||
activity.aniMangaResult.apply {
|
||||
status =
|
||||
binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
|
||||
source =
|
||||
@@ -135,7 +135,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
season = binding.searchSeason.text.toString().ifBlank { null }
|
||||
startYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
seasonYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
sort = activity.result.sort
|
||||
sort = activity.aniMangaResult.sort
|
||||
genres = selectedGenres
|
||||
tags = selectedTags
|
||||
excludedGenres = exGenres
|
||||
@@ -155,43 +155,43 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.sort_by_score -> {
|
||||
activity.result.sort = Anilist.sortBy[0]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[0]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_popular -> {
|
||||
activity.result.sort = Anilist.sortBy[1]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[1]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_trending -> {
|
||||
activity.result.sort = Anilist.sortBy[2]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[2]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_recent -> {
|
||||
activity.result.sort = Anilist.sortBy[3]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[3]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_a_z -> {
|
||||
activity.result.sort = Anilist.sortBy[4]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[4]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_z_a -> {
|
||||
activity.result.sort = Anilist.sortBy[5]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[5]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
|
||||
R.id.sort_by_pure_pain -> {
|
||||
activity.result.sort = Anilist.sortBy[6]
|
||||
activity.aniMangaResult.sort = Anilist.sortBy[6]
|
||||
binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24)
|
||||
startBounceZoomAnimation()
|
||||
}
|
||||
@@ -212,25 +212,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
R.id.country_china -> {
|
||||
activity.result.countryOfOrigin = "CN"
|
||||
activity.aniMangaResult.countryOfOrigin = "CN"
|
||||
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
|
||||
startBounceZoomAnimation(binding.countryFilter)
|
||||
}
|
||||
|
||||
R.id.country_south_korea -> {
|
||||
activity.result.countryOfOrigin = "KR"
|
||||
activity.aniMangaResult.countryOfOrigin = "KR"
|
||||
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
|
||||
startBounceZoomAnimation(binding.countryFilter)
|
||||
}
|
||||
|
||||
R.id.country_japan -> {
|
||||
activity.result.countryOfOrigin = "JP"
|
||||
activity.aniMangaResult.countryOfOrigin = "JP"
|
||||
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
|
||||
startBounceZoomAnimation(binding.countryFilter)
|
||||
}
|
||||
|
||||
R.id.country_taiwan -> {
|
||||
activity.result.countryOfOrigin = "TW"
|
||||
activity.aniMangaResult.countryOfOrigin = "TW"
|
||||
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
|
||||
startBounceZoomAnimation(binding.countryFilter)
|
||||
}
|
||||
@@ -241,18 +241,18 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
binding.searchFilterApply.setOnClickListener {
|
||||
activity.result.apply {
|
||||
activity.aniMangaResult.apply {
|
||||
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
|
||||
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
|
||||
format = binding.searchFormat.text.toString().ifBlank { null }
|
||||
season = binding.searchSeason.text.toString().ifBlank { null }
|
||||
if (activity.result.type == "ANIME") {
|
||||
if (activity.aniMangaResult.type == "ANIME") {
|
||||
seasonYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
} else {
|
||||
startYear = binding.searchYear.text.toString().toIntOrNull()
|
||||
}
|
||||
sort = activity.result.sort
|
||||
countryOfOrigin = activity.result.countryOfOrigin
|
||||
sort = activity.aniMangaResult.sort
|
||||
countryOfOrigin = activity.aniMangaResult.countryOfOrigin
|
||||
genres = selectedGenres
|
||||
tags = selectedTags
|
||||
excludedGenres = exGenres
|
||||
@@ -266,8 +266,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
val format =
|
||||
if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
|
||||
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
|
||||
if (activity.aniMangaResult.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
|
||||
binding.searchStatus.setText(activity.aniMangaResult.status?.replace("_", " "))
|
||||
binding.searchStatus.setAdapter(
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
@@ -276,7 +276,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
)
|
||||
)
|
||||
|
||||
binding.searchSource.setText(activity.result.source?.replace("_", " "))
|
||||
binding.searchSource.setText(activity.aniMangaResult.source?.replace("_", " "))
|
||||
binding.searchSource.setAdapter(
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
@@ -285,19 +285,19 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
)
|
||||
)
|
||||
|
||||
binding.searchFormat.setText(activity.result.format)
|
||||
binding.searchFormat.setText(activity.aniMangaResult.format)
|
||||
binding.searchFormat.setAdapter(
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
R.layout.item_dropdown,
|
||||
(if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
|
||||
(if (activity.aniMangaResult.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
|
||||
)
|
||||
)
|
||||
|
||||
if (activity.result.type == "ANIME") {
|
||||
binding.searchYear.setText(activity.result.seasonYear?.toString())
|
||||
if (activity.aniMangaResult.type == "ANIME") {
|
||||
binding.searchYear.setText(activity.aniMangaResult.seasonYear?.toString())
|
||||
} else {
|
||||
binding.searchYear.setText(activity.result.startYear?.toString())
|
||||
binding.searchYear.setText(activity.aniMangaResult.startYear?.toString())
|
||||
}
|
||||
binding.searchYear.setAdapter(
|
||||
ArrayAdapter(
|
||||
@@ -308,9 +308,9 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
)
|
||||
)
|
||||
|
||||
if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE
|
||||
if (activity.aniMangaResult.type == "MANGA") binding.searchSeasonCont.visibility = GONE
|
||||
else {
|
||||
binding.searchSeason.setText(activity.result.season)
|
||||
binding.searchSeason.setText(activity.aniMangaResult.season)
|
||||
binding.searchSeason.setAdapter(
|
||||
ArrayAdapter(
|
||||
binding.root.context,
|
||||
@@ -346,7 +346,9 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
|
||||
binding.searchGenresGrid.isChecked = false
|
||||
|
||||
binding.searchFilterTags.adapter =
|
||||
FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
|
||||
FilterChipAdapter(
|
||||
Anilist.tags?.get(activity.aniMangaResult.isAdult) ?: listOf()
|
||||
) { chip ->
|
||||
val tag = chip.text.toString()
|
||||
chip.isChecked = selectedTags.contains(tag)
|
||||
chip.isCloseIconVisible = exTags.contains(tag)
|
||||
|
||||
@@ -7,48 +7,75 @@ import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
|
||||
import ani.dantotsu.databinding.ItemSearchHistoryBinding
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefManager.asLiveStringSet
|
||||
import ani.dantotsu.settings.saving.PrefManager.asLiveClass
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.SharedPreferenceStringSetLiveData
|
||||
import java.util.Locale
|
||||
import ani.dantotsu.settings.saving.SharedPreferenceClassLiveData
|
||||
import java.io.Serializable
|
||||
|
||||
class SearchHistoryAdapter(private val type: String, private val searchClicked: (String) -> Unit) :
|
||||
data class SearchHistory(val search: String, val time: Long) : Serializable {
|
||||
companion object {
|
||||
private const val serialVersionUID = 1L
|
||||
}
|
||||
}
|
||||
|
||||
class SearchHistoryAdapter(type: SearchType, private val searchClicked: (String) -> Unit) :
|
||||
ListAdapter<String, SearchHistoryAdapter.SearchHistoryViewHolder>(
|
||||
DIFF_CALLBACK_INSTALLED
|
||||
) {
|
||||
private var searchHistoryLiveData: SharedPreferenceStringSetLiveData? = null
|
||||
private var searchHistory: MutableSet<String>? = null
|
||||
private var historyType: PrefName = when (type.lowercase(Locale.ROOT)) {
|
||||
"anime" -> PrefName.AnimeSearchHistory
|
||||
"manga" -> PrefName.MangaSearchHistory
|
||||
else -> throw IllegalArgumentException("Invalid type")
|
||||
private var searchHistoryLiveData: SharedPreferenceClassLiveData<List<SearchHistory>>? = null
|
||||
private var searchHistory: MutableList<SearchHistory>? = null
|
||||
private var historyType: PrefName = when (type) {
|
||||
SearchType.ANIME -> PrefName.SortedAnimeSH
|
||||
SearchType.MANGA -> PrefName.SortedMangaSH
|
||||
SearchType.CHARACTER -> PrefName.SortedCharacterSH
|
||||
SearchType.STAFF -> PrefName.SortedStaffSH
|
||||
SearchType.STUDIO -> PrefName.SortedStudioSH
|
||||
SearchType.USER -> PrefName.SortedUserSH
|
||||
}
|
||||
|
||||
private fun MutableList<SearchHistory>?.sorted(): List<String>? =
|
||||
this?.sortedByDescending { it.time }?.map { it.search }
|
||||
|
||||
init {
|
||||
searchHistoryLiveData =
|
||||
PrefManager.getLiveVal(historyType, mutableSetOf<String>()).asLiveStringSet()
|
||||
searchHistoryLiveData?.observeForever {
|
||||
searchHistory = it.toMutableSet()
|
||||
submitList(searchHistory?.toList())
|
||||
PrefManager.getLiveVal(historyType, mutableListOf<SearchHistory>()).asLiveClass()
|
||||
searchHistoryLiveData?.observeForever { data ->
|
||||
searchHistory = data.toMutableList()
|
||||
submitList(searchHistory?.sorted())
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(item: String) {
|
||||
searchHistory?.remove(item)
|
||||
searchHistory?.let { list ->
|
||||
list.removeAll { it.search == item }
|
||||
}
|
||||
PrefManager.setVal(historyType, searchHistory)
|
||||
submitList(searchHistory?.toList())
|
||||
submitList(searchHistory?.sorted())
|
||||
}
|
||||
|
||||
fun add(item: String) {
|
||||
if (searchHistory?.contains(item) == true || item.isBlank()) return
|
||||
val maxSize = 25
|
||||
if (searchHistory?.any { it.search == item } == true || item.isBlank()) return
|
||||
if (PrefManager.getVal(PrefName.Incognito)) return
|
||||
searchHistory?.add(item)
|
||||
submitList(searchHistory?.toList())
|
||||
searchHistory?.add(SearchHistory(item, System.currentTimeMillis()))
|
||||
if ((searchHistory?.size ?: 0) > maxSize) {
|
||||
searchHistory?.removeAt(
|
||||
searchHistory?.sorted()?.lastIndex ?: 0
|
||||
)
|
||||
}
|
||||
submitList(searchHistory?.sorted())
|
||||
PrefManager.setVal(historyType, searchHistory)
|
||||
}
|
||||
|
||||
fun clearHistory() {
|
||||
searchHistory?.clear()
|
||||
PrefManager.setVal(historyType, searchHistory)
|
||||
submitList(searchHistory?.sorted())
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
|
||||
@@ -5,5 +5,8 @@ import java.io.Serializable
|
||||
data class Studio(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val isFavourite: Boolean?,
|
||||
val favourites: Int?,
|
||||
val imageUrl: String?,
|
||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
|
||||
) : Serializable
|
||||
|
||||
65
app/src/main/java/ani/dantotsu/media/StudioAdapter.kt
Normal file
65
app/src/main/java/ani/dantotsu/media/StudioAdapter.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.databinding.ItemCharacterBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.setAnimation
|
||||
import java.io.Serializable
|
||||
|
||||
class StudioAdapter(
|
||||
private val studioList: MutableList<Studio>
|
||||
) : RecyclerView.Adapter<StudioAdapter.StudioViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudioViewHolder {
|
||||
val binding =
|
||||
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return StudioViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: StudioViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
setAnimation(binding.root.context, holder.binding.root)
|
||||
val studio = studioList.getOrNull(position) ?: return
|
||||
binding.itemCompactRelation.isVisible = false
|
||||
binding.itemCompactImage.loadImage(studio.imageUrl)
|
||||
binding.itemCompactTitle.text = studio.name
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = studioList.size
|
||||
inner class StudioViewHolder(val binding: ItemCharacterBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
val studio = studioList[bindingAdapterPosition]
|
||||
ContextCompat.startActivity(
|
||||
itemView.context,
|
||||
Intent(
|
||||
itemView.context,
|
||||
StudioActivity::class.java
|
||||
).putExtra("studio", studio as Serializable),
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
itemView.context as Activity,
|
||||
Pair.create(
|
||||
binding.itemCompactImage,
|
||||
ViewCompat.getTransitionName(binding.itemCompactImage)!!
|
||||
),
|
||||
).toBundle()
|
||||
)
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
copyToClipboard(
|
||||
studioList[bindingAdapterPosition].name
|
||||
); true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,14 +72,14 @@ class SubtitleDownloader {
|
||||
|
||||
val client = Injekt.get<NetworkHelper>().client
|
||||
val request = Request.Builder().url(url).build()
|
||||
val reponse = client.newCall(request).execute()
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
if (!reponse.isSuccessful) {
|
||||
if (!response.isSuccessful) {
|
||||
snackString("Failed to download subtitle")
|
||||
return
|
||||
}
|
||||
|
||||
reponse.body.byteStream().use { input ->
|
||||
response.body.byteStream().use { input ->
|
||||
subtitleFile.openOutputStream(context, false).use { output ->
|
||||
if (output == null) throw Exception("Could not open output stream")
|
||||
input.copyTo(output)
|
||||
|
||||
143
app/src/main/java/ani/dantotsu/media/SupportingSearchAdapter.kt
Normal file
143
app/src/main/java/ani/dantotsu/media/SupportingSearchAdapter.kt
Normal file
@@ -0,0 +1,143 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.App.Companion.context
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
|
||||
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SupportingSearchAdapter(private val activity: SearchActivity, private val type: SearchType) :
|
||||
HeaderInterface() {
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
|
||||
binding = holder.binding
|
||||
|
||||
searchHistoryAdapter = SearchHistoryAdapter(type) {
|
||||
binding.searchBarText.setText(it)
|
||||
binding.searchBarText.setSelection(it.length)
|
||||
}
|
||||
binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context)
|
||||
binding.searchHistoryList.adapter = searchHistoryAdapter
|
||||
|
||||
val imm: InputMethodManager =
|
||||
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
|
||||
if (activity.searchType == SearchType.MANGA || activity.searchType == SearchType.ANIME) {
|
||||
throw IllegalArgumentException("Invalid search type (wrong adapter)")
|
||||
}
|
||||
|
||||
binding.searchByImage.visibility = View.GONE
|
||||
binding.searchResultGrid.visibility = View.GONE
|
||||
binding.searchResultList.visibility = View.GONE
|
||||
binding.searchFilter.visibility = View.GONE
|
||||
binding.searchAdultCheck.visibility = View.GONE
|
||||
binding.searchList.visibility = View.GONE
|
||||
binding.searchChipRecycler.visibility = View.GONE
|
||||
|
||||
binding.searchBar.hint = activity.searchType.toAnilistString()
|
||||
if (PrefManager.getVal(PrefName.Incognito)) {
|
||||
val startIconDrawableRes = R.drawable.ic_incognito_24
|
||||
val startIconDrawable: Drawable? =
|
||||
context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) }
|
||||
binding.searchBar.startIconDrawable = startIconDrawable
|
||||
}
|
||||
|
||||
binding.searchBarText.removeTextChangedListener(textWatcher)
|
||||
when (type) {
|
||||
SearchType.CHARACTER -> {
|
||||
binding.searchBarText.setText(activity.characterResult.search)
|
||||
}
|
||||
|
||||
SearchType.STUDIO -> {
|
||||
binding.searchBarText.setText(activity.studioResult.search)
|
||||
}
|
||||
|
||||
SearchType.STAFF -> {
|
||||
binding.searchBarText.setText(activity.staffResult.search)
|
||||
}
|
||||
|
||||
SearchType.USER -> {
|
||||
binding.searchBarText.setText(activity.userResult.search)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Invalid search type")
|
||||
}
|
||||
|
||||
binding.clearHistory.setOnClickListener {
|
||||
it.startAnimation(fadeOutAnimation())
|
||||
it.visibility = View.GONE
|
||||
searchHistoryAdapter.clearHistory()
|
||||
}
|
||||
updateClearHistoryVisibility()
|
||||
fun searchTitle() {
|
||||
val searchText = binding.searchBarText.text.toString().takeIf { it.isNotEmpty() }
|
||||
|
||||
val result: SearchResults<*> = when (type) {
|
||||
SearchType.CHARACTER -> activity.characterResult
|
||||
SearchType.STUDIO -> activity.studioResult
|
||||
SearchType.STAFF -> activity.staffResult
|
||||
SearchType.USER -> activity.userResult
|
||||
else -> throw IllegalArgumentException("Invalid search type")
|
||||
}
|
||||
|
||||
result.search = searchText
|
||||
activity.search()
|
||||
}
|
||||
|
||||
textWatcher = object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable) {}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
if (s.toString().isBlank()) {
|
||||
activity.emptyMediaAdapter()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(200)
|
||||
activity.runOnUiThread {
|
||||
setHistoryVisibility(true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setHistoryVisibility(false)
|
||||
searchTitle()
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.searchBarText.addTextChangedListener(textWatcher)
|
||||
|
||||
binding.searchBarText.setOnEditorActionListener { _, actionId, _ ->
|
||||
return@setOnEditorActionListener when (actionId) {
|
||||
EditorInfo.IME_ACTION_SEARCH -> {
|
||||
searchTitle()
|
||||
binding.searchBarText.clearFocus()
|
||||
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
binding.searchBar.setEndIconOnClickListener { searchTitle() }
|
||||
|
||||
search = Runnable { searchTitle() }
|
||||
requestFocus = Runnable { binding.searchBarText.requestFocus() }
|
||||
}
|
||||
}
|
||||
@@ -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,9 +18,10 @@ 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.ItemChipBinding
|
||||
import ani.dantotsu.databinding.ItemMediaSourceBinding
|
||||
import ani.dantotsu.displayTimer
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.loadImage
|
||||
@@ -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,14 @@ 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 +77,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 +91,7 @@ class AnimeWatchAdapter(
|
||||
R.string.subbed
|
||||
)
|
||||
|
||||
//PreferDub
|
||||
// PreferDub
|
||||
var changing = false
|
||||
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
|
||||
binding.animeSourceDubbedText.text =
|
||||
@@ -99,8 +101,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 +110,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 +152,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 +172,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 +192,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 +360,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 +385,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 +404,7 @@ class AnimeWatchAdapter(
|
||||
}
|
||||
|
||||
fun clearChips() {
|
||||
_binding?.animeSourceChipGroup?.removeAllViews()
|
||||
_binding?.mediaSourceChipGroup?.removeAllViews()
|
||||
}
|
||||
|
||||
fun handleEpisodes() {
|
||||
@@ -379,15 +420,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 +436,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 +448,65 @@ 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 +515,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 +535,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 +548,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 +558,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)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -28,10 +27,9 @@ import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.databinding.FragmentMediaSourceBinding
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
@@ -61,6 +59,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 +77,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 +104,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 +125,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 +149,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 +169,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 +202,7 @@ class AnimeWatchFragment : Fragment() {
|
||||
offlineMode = offlineMode
|
||||
)
|
||||
|
||||
binding.animeSourceRecycler.adapter =
|
||||
binding.mediaSourceRecycler.adapter =
|
||||
ConcatAdapter(headerAdapter, episodeAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
@@ -212,10 +211,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 +230,21 @@ 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 =
|
||||
@@ -247,14 +262,14 @@ class AnimeWatchFragment : Fragment() {
|
||||
) 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]
|
||||
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 +310,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,34 +399,33 @@ 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 ->
|
||||
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)
|
||||
}
|
||||
parentFragmentManager.beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
|
||||
.replace(R.id.fragmentExtensionsContainer, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
requireContext()
|
||||
.customAlertDialog()
|
||||
.apply {
|
||||
setTitle("Select a Source")
|
||||
singleChoiceItems(names) { which ->
|
||||
selectedSetting = allSettings[which]
|
||||
itemSelected = true
|
||||
requireActivity().runOnUiThread {
|
||||
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)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setOnDismissListener {
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
onDismiss {
|
||||
if (!itemSelected) {
|
||||
changeUIVisibility(true)
|
||||
}
|
||||
}
|
||||
show()
|
||||
}
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
} else {
|
||||
// If there's only one setting, proceed with the fragment transaction
|
||||
requireActivity().runOnUiThread {
|
||||
@@ -416,11 +434,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 +638,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 +682,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 {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media.anime
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -13,7 +12,6 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.updateProgress
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeGridBinding
|
||||
import ani.dantotsu.databinding.ItemEpisodeListBinding
|
||||
@@ -23,6 +21,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 +105,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 +139,9 @@ class EpisodeAdapter(
|
||||
}
|
||||
|
||||
handleProgress(
|
||||
binding.itemEpisodeProgressCont,
|
||||
binding.itemEpisodeProgress,
|
||||
binding.itemEpisodeProgressEmpty,
|
||||
binding.itemMediaProgressCont,
|
||||
binding.itemMediaProgress,
|
||||
binding.itemMediaProgressEmpty,
|
||||
media.id,
|
||||
ep.number
|
||||
)
|
||||
@@ -154,8 +153,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 +182,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 +207,9 @@ class EpisodeAdapter(
|
||||
}
|
||||
}
|
||||
handleProgress(
|
||||
binding.itemEpisodeProgressCont,
|
||||
binding.itemEpisodeProgress,
|
||||
binding.itemEpisodeProgressEmpty,
|
||||
binding.itemMediaProgressCont,
|
||||
binding.itemMediaProgress,
|
||||
binding.itemMediaProgressEmpty,
|
||||
media.id,
|
||||
ep.number
|
||||
)
|
||||
@@ -318,16 +317,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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ package ani.dantotsu.media.anime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ComponentName
|
||||
import android.content.DialogInterface
|
||||
@@ -444,15 +443,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 +462,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)
|
||||
}
|
||||
@@ -490,7 +482,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
)
|
||||
} else {
|
||||
val downloadAddonManager: DownloadAddonManager = Injekt.get()
|
||||
if (!downloadAddonManager.isAvailable()){
|
||||
if (!downloadAddonManager.isAvailable()) {
|
||||
val context = currContext() ?: requireContext()
|
||||
context.customAlertDialog().apply {
|
||||
setTitle(R.string.download_addon_not_installed)
|
||||
@@ -571,70 +563,73 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||
snackString(R.string.no_video_selected)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkAudioTracks() {
|
||||
val audioTracks = extractor.audioTracks.map { it.lang }
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -63,8 +63,12 @@ class TrackGroupDialogFragment(
|
||||
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
trackGroups[position].let { trackGroup ->
|
||||
if (overrideTrackNames?.getOrNull(position - (trackGroups.size - (overrideTrackNames?.size?:0))) != null) {
|
||||
val pair = overrideTrackNames!![position - (trackGroups.size - overrideTrackNames!!.size)]
|
||||
if (overrideTrackNames?.getOrNull(
|
||||
position - (trackGroups.size - (overrideTrackNames?.size ?: 0))
|
||||
) != null
|
||||
) {
|
||||
val pair =
|
||||
overrideTrackNames!![position - (trackGroups.size - overrideTrackNames!!.size)]
|
||||
binding.subtitleTitle.text =
|
||||
"[${pair.second}] ${pair.first}"
|
||||
} else when (val language = trackGroup.getTrackFormat(0).language?.lowercase()) {
|
||||
|
||||
@@ -15,12 +15,12 @@ import ani.dantotsu.databinding.ItemCommentsBinding
|
||||
import ani.dantotsu.getAppString
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.openImage
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.ColorEditor.Companion.adjustColorForContrast
|
||||
import ani.dantotsu.util.ColorEditor.Companion.getContrastRatio
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.viewbinding.BindableItem
|
||||
@@ -385,19 +385,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(
|
||||
|
||||
@@ -2,7 +2,6 @@ package ani.dantotsu.media.comments
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context.INPUT_METHOD_SERVICE
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
@@ -12,7 +11,6 @@ import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
@@ -25,6 +23,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
|
||||
@@ -32,8 +31,8 @@ import ani.dantotsu.setBaseline
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import ani.dantotsu.util.customAlertDialog
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.Section
|
||||
import io.noties.markwon.editor.MarkwonEditor
|
||||
@@ -160,35 +159,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 +315,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 +335,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 +371,6 @@ class CommentsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
@@ -579,31 +582,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")
|
||||
.setMessage(
|
||||
"I WILL BAN YOU WITHOUT HESITATION\n" +
|
||||
"1. No racism\n" +
|
||||
"2. No hate speech\n" +
|
||||
"3. No spam\n" +
|
||||
"4. No NSFW content\n" +
|
||||
"6. ENGLISH ONLY\n" +
|
||||
"7. No self promotion\n" +
|
||||
"8. No impersonation\n" +
|
||||
"9. No harassment\n" +
|
||||
"10. No illegal content\n" +
|
||||
"11. Anything you know you shouldn't comment\n"
|
||||
)
|
||||
.setPositiveButton("I Understand") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
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"
|
||||
)
|
||||
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 +709,4 @@ class CommentsFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import java.io.Serializable
|
||||
|
||||
data class Manga(
|
||||
var totalChapters: Int? = null,
|
||||
var selectedChapter: String? = null,
|
||||
var selectedChapter: MangaChapter? = null,
|
||||
var chapters: MutableMap<String, MangaChapter>? = null,
|
||||
var slug: String? = null,
|
||||
var author: Author? = null,
|
||||
|
||||
@@ -40,4 +40,6 @@ data class MangaChapter(
|
||||
private val dualPages = mutableListOf<Pair<MangaImage, MangaImage?>>()
|
||||
fun dualPages(): List<Pair<MangaImage, MangaImage?>> = dualPages
|
||||
|
||||
fun uniqueNumber(): String = "${number}-${scanlator ?: "Unknown"}"
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media.manga
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -63,7 +62,7 @@ class MangaChapterAdapter(
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
||||
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
|
||||
fragment.onMangaChapterClick(arr[bindingAdapterPosition])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +73,7 @@ class MangaChapterAdapter(
|
||||
fun startDownload(chapterNumber: String) {
|
||||
activeDownloads.add(chapterNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber }
|
||||
if (position != -1) {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
@@ -84,17 +83,17 @@ class MangaChapterAdapter(
|
||||
activeDownloads.remove(chapterNumber)
|
||||
downloadedChapters.add(chapterNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber }
|
||||
if (position != -1) {
|
||||
arr[position].progress = "Downloaded"
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDownload(chapterNumber: String) {
|
||||
downloadedChapters.remove(chapterNumber)
|
||||
fun deleteDownload(chapterNumber: MangaChapter) {
|
||||
downloadedChapters.remove(chapterNumber.uniqueNumber())
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber.uniqueNumber() }
|
||||
if (position != -1) {
|
||||
arr[position].progress = ""
|
||||
notifyItemChanged(position)
|
||||
@@ -105,7 +104,7 @@ class MangaChapterAdapter(
|
||||
activeDownloads.remove(chapterNumber)
|
||||
downloadedChapters.remove(chapterNumber)
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber }
|
||||
if (position != -1) {
|
||||
arr[position].progress = ""
|
||||
notifyItemChanged(position)
|
||||
@@ -114,7 +113,7 @@ class MangaChapterAdapter(
|
||||
|
||||
fun updateDownloadProgress(chapterNumber: String, progress: Int) {
|
||||
// Find the position of the chapter and notify only that item
|
||||
val position = arr.indexOfFirst { it.number == chapterNumber }
|
||||
val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber }
|
||||
if (position != -1) {
|
||||
arr[position].progress = "Downloading: ${progress}%"
|
||||
|
||||
@@ -127,7 +126,8 @@ class MangaChapterAdapter(
|
||||
if (position < 0 || position >= arr.size) return
|
||||
for (i in 0..<n) {
|
||||
if (position + i < arr.size) {
|
||||
val chapterNumber = arr[position + i].number
|
||||
val chapter = arr[position + i]
|
||||
val chapterNumber = chapter.uniqueNumber()
|
||||
if (activeDownloads.contains(chapterNumber)) {
|
||||
//do nothing
|
||||
continue
|
||||
@@ -135,8 +135,8 @@ class MangaChapterAdapter(
|
||||
//do nothing
|
||||
continue
|
||||
} else {
|
||||
fragment.onMangaChapterDownloadClick(chapterNumber)
|
||||
startDownload(chapterNumber)
|
||||
fragment.onMangaChapterDownloadClick(chapter)
|
||||
startDownload(chapter.uniqueNumber())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,28 +201,29 @@ class MangaChapterAdapter(
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
|
||||
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
|
||||
fragment.onMangaChapterClick(arr[bindingAdapterPosition])
|
||||
}
|
||||
binding.itemDownload.setOnClickListener {
|
||||
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
|
||||
val chapterNumber = arr[bindingAdapterPosition].number
|
||||
val chapter = arr[bindingAdapterPosition]
|
||||
val chapterNumber = chapter.uniqueNumber()
|
||||
if (activeDownloads.contains(chapterNumber)) {
|
||||
fragment.onMangaChapterStopDownloadClick(chapterNumber)
|
||||
fragment.onMangaChapterStopDownloadClick(chapter)
|
||||
return@setOnClickListener
|
||||
} else if (downloadedChapters.contains(chapterNumber)) {
|
||||
it.context.customAlertDialog().apply {
|
||||
setTitle("Delete Chapter")
|
||||
setMessage("Are you sure you want to delete ${chapterNumber}?")
|
||||
setPosButton(R.string.delete) {
|
||||
fragment.onMangaChapterRemoveDownloadClick(chapterNumber)
|
||||
fragment.onMangaChapterRemoveDownloadClick(chapter)
|
||||
}
|
||||
setNegButton(R.string.cancel)
|
||||
show()
|
||||
}
|
||||
return@setOnClickListener
|
||||
} else {
|
||||
fragment.onMangaChapterDownloadClick(chapterNumber)
|
||||
startDownload(chapterNumber)
|
||||
fragment.onMangaChapterDownloadClick(chapter)
|
||||
startDownload(chapter.uniqueNumber())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,7 +278,7 @@ class MangaChapterAdapter(
|
||||
is ChapterListViewHolder -> {
|
||||
val binding = holder.binding
|
||||
val ep = arr[position]
|
||||
holder.bind(ep.number, ep.progress)
|
||||
holder.bind(ep.uniqueNumber(), ep.progress)
|
||||
setAnimation(fragment.requireContext(), holder.binding.root)
|
||||
binding.itemChapterNumber.text = ep.number
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
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
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.NumberPicker
|
||||
import android.widget.TextView
|
||||
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,9 +22,10 @@ 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.ItemChipBinding
|
||||
import ani.dantotsu.databinding.ItemMediaSourceBinding
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.Media
|
||||
@@ -35,11 +39,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 +62,109 @@ 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 +173,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 +191,216 @@ 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))
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
if (!checkBox.isChecked) {
|
||||
hiddenScanlators.add(checkBox.text.toString())
|
||||
// Multi download
|
||||
//downloadNo.text = "0"
|
||||
mediaDownloadTop.setOnClickListener {
|
||||
fragment.requireContext().customAlertDialog().apply {
|
||||
setTitle("Multi Chapter Downloader")
|
||||
setMessage("Enter the number of chapters to download")
|
||||
val input = View.inflate(currContext(), R.layout.dialog_layout, null)
|
||||
val editText = input.findViewById<EditText>(R.id.downloadNo)
|
||||
setCustomView(input)
|
||||
setPosButton(R.string.ok) {
|
||||
val value = editText.text.toString().toIntOrNull()
|
||||
if (value != null && value > 0) {
|
||||
downloadNo.setText(value.toString(), TextView.BufferType.EDITABLE)
|
||||
fragment.multiDownload(value)
|
||||
} else {
|
||||
toast("Please enter a valid number")
|
||||
}
|
||||
}
|
||||
fragment.onScanlatorChange(hiddenScanlators)
|
||||
scanlatorSelectionListener?.onScanlatorsSelected()
|
||||
setNegButton(R.string.cancel)
|
||||
show()
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
}
|
||||
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")
|
||||
|
||||
// Standard image resource
|
||||
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
|
||||
snackString("Deleted the progress of Chapters for ${media.nameRomaji}")
|
||||
}
|
||||
setNegButton(R.string.no)
|
||||
show()
|
||||
}
|
||||
}
|
||||
resetProgressDef.text = getString(currContext()!!, R.string.clear_stored_chapter)
|
||||
|
||||
// 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
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
val value = downloadNo.text.toString().toIntOrNull()
|
||||
if (value != null && value > 0) {
|
||||
fragment.multiDownload(value)
|
||||
}
|
||||
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 +408,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 +419,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
|
||||
)
|
||||
@@ -394,16 +434,16 @@ class MangaReadAdapter(
|
||||
val startChapter = MediaNameAdapter.findChapterNumber(names[limit * (position)])
|
||||
val endChapter = MediaNameAdapter.findChapterNumber(names[last - 1])
|
||||
val startChapterString = if (startChapter != null) {
|
||||
"Ch.$startChapter"
|
||||
"Ch.%.1f".format(startChapter)
|
||||
} else {
|
||||
names[limit * (position)]
|
||||
}
|
||||
val endChapterString = if (endChapter != null) {
|
||||
"Ch.$endChapter"
|
||||
"Ch.%.1f".format(endChapter)
|
||||
} 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 +457,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 +476,7 @@ class MangaReadAdapter(
|
||||
}
|
||||
|
||||
fun clearChips() {
|
||||
_binding?.animeSourceChipGroup?.removeAllViews()
|
||||
_binding?.mediaSourceChipGroup?.removeAllViews()
|
||||
}
|
||||
|
||||
fun handleChapters() {
|
||||
@@ -444,7 +484,6 @@ class MangaReadAdapter(
|
||||
val binding = _binding
|
||||
if (binding != null) {
|
||||
if (media.manga?.chapters != null) {
|
||||
val chapters = media.manga.chapters!!.keys.toTypedArray()
|
||||
val anilistEp = (media.userProgress ?: 0).plus(1)
|
||||
val appEp = PrefManager.getNullableCustomVal(
|
||||
"${media.id}_current_chp",
|
||||
@@ -452,80 +491,104 @@ class MangaReadAdapter(
|
||||
String::class.java
|
||||
)
|
||||
?.toIntOrNull() ?: 1
|
||||
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||
val filteredChapters = chapters.filter { chapterKey ->
|
||||
val chapter = media.manga.chapters!![chapterKey]!!
|
||||
chapter.scanlator !in hiddenScanlators
|
||||
val continueNumber = (if (anilistEp > appEp) anilistEp else appEp).toString()
|
||||
val filteredChapters = media.manga.chapters!!.filter { chapter ->
|
||||
if (mangaReadSources[media.selected!!.sourceIndex] is OfflineMangaParser) {
|
||||
true
|
||||
} else {
|
||||
chapter.value.scanlator !in hiddenScanlators
|
||||
}
|
||||
}
|
||||
val formattedChapters = filteredChapters.map {
|
||||
MediaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
|
||||
MediaNameAdapter.findChapterNumber(it.value.number)?.toInt()
|
||||
?.toString() to it.key
|
||||
}
|
||||
if (formattedChapters.contains(continueEp)) {
|
||||
continueEp = chapters[formattedChapters.indexOf(continueEp)]
|
||||
binding.animeSourceContinue.visibility = View.VISIBLE
|
||||
if (formattedChapters.any { it.first == continueNumber }) {
|
||||
var continueEp =
|
||||
media.manga.chapters!![formattedChapters.first { it.first == continueNumber }.second]
|
||||
binding.sourceContinue.visibility = View.VISIBLE
|
||||
handleProgress(
|
||||
binding.itemEpisodeProgressCont,
|
||||
binding.itemEpisodeProgress,
|
||||
binding.itemEpisodeProgressEmpty,
|
||||
binding.itemMediaProgressCont,
|
||||
binding.itemMediaProgress,
|
||||
binding.itemMediaProgressEmpty,
|
||||
media.id,
|
||||
continueEp
|
||||
continueEp!!.number
|
||||
)
|
||||
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > 0.8f) {
|
||||
val e = chapters.indexOf(continueEp)
|
||||
if (e != -1 && e + 1 < chapters.size) {
|
||||
continueEp = chapters[e + 1]
|
||||
if ((binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams).weight > 0.8f) {
|
||||
val numberPlusOne =
|
||||
formattedChapters.indexOfFirst { it.first?.toIntOrNull() == continueNumber.toInt() + 1 }
|
||||
if (numberPlusOne != -1) {
|
||||
continueEp =
|
||||
media.manga.chapters!![formattedChapters[numberPlusOne].second]
|
||||
}
|
||||
}
|
||||
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 ""
|
||||
continueEp!!.number,
|
||||
if (!continueEp.title.isNullOrEmpty()) continueEp.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 +602,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 +614,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 +624,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
|
||||
@@ -53,6 +52,7 @@ import ani.dantotsu.parsers.DynamicMangaParser
|
||||
import ani.dantotsu.parsers.HMangaSources
|
||||
import ani.dantotsu.parsers.MangaParser
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.parsers.OfflineMangaParser
|
||||
import ani.dantotsu.setNavigationTheme
|
||||
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
@@ -60,6 +60,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 +75,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 +102,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 +122,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 +145,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 +165,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
|
||||
@@ -195,11 +196,11 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
|
||||
for (download in downloadManager.mangaDownloadedTypes) {
|
||||
if (media.compareName(download.titleName)) {
|
||||
chapterAdapter.stopDownload(download.chapterName)
|
||||
chapterAdapter.stopDownload(download.uniqueName)
|
||||
}
|
||||
}
|
||||
|
||||
binding.animeSourceRecycler.adapter =
|
||||
binding.mediaSourceRecycler.adapter =
|
||||
ConcatAdapter(headerAdapter, chapterAdapter)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
@@ -214,8 +215,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 +232,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,12 +245,12 @@ 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)
|
||||
|
||||
|
||||
for (chapter in chaptersToDownload) {
|
||||
onMangaChapterDownloadClick(chapter.title!!)
|
||||
onMangaChapterDownloadClick(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,9 +261,14 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
val chapters = loadedChapters[media.selected!!.sourceIndex]
|
||||
if (chapters != null) {
|
||||
headerAdapter.options = getScanlators(chapters)
|
||||
val filteredChapters = chapters.filterNot { (_, chapter) ->
|
||||
chapter.scanlator in headerAdapter.hiddenScanlators
|
||||
}
|
||||
val filteredChapters =
|
||||
if (model.mangaReadSources?.get(media.selected!!.sourceIndex) is OfflineMangaParser) {
|
||||
chapters
|
||||
} else {
|
||||
chapters.filterNot { (_, chapter) ->
|
||||
chapter.scanlator in headerAdapter.hiddenScanlators
|
||||
}
|
||||
}
|
||||
|
||||
media.manga?.chapters = filteredChapters.toMutableMap()
|
||||
|
||||
@@ -386,14 +392,12 @@ 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)
|
||||
@@ -405,13 +409,14 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
.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) {
|
||||
@@ -432,9 +437,9 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
}
|
||||
}
|
||||
|
||||
fun onMangaChapterClick(i: String) {
|
||||
fun onMangaChapterClick(i: MangaChapter) {
|
||||
model.continueMedia = false
|
||||
media.manga?.chapters?.get(i)?.let {
|
||||
media.manga?.chapters?.get(i.uniqueNumber())?.let {
|
||||
media.manga?.selectedChapter = i
|
||||
model.saveSelected(media.id, media.selected!!)
|
||||
ChapterLoaderDialog.newInstance(it, true)
|
||||
@@ -442,7 +447,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
}
|
||||
}
|
||||
|
||||
fun onMangaChapterDownloadClick(i: String) {
|
||||
fun onMangaChapterDownloadClick(i: MangaChapter) {
|
||||
activity?.let {
|
||||
if (!isNotificationPermissionGranted()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
@@ -455,7 +460,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
}
|
||||
fun continueDownload() {
|
||||
model.continueMedia = false
|
||||
media.manga?.chapters?.get(i)?.let { chapter ->
|
||||
media.manga?.chapters?.get(i.uniqueNumber())?.let { chapter ->
|
||||
val parser =
|
||||
model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
|
||||
parser?.let {
|
||||
@@ -466,9 +471,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
val downloadTask = MangaDownloaderService.DownloadTask(
|
||||
title = media.mainName(),
|
||||
chapter = chapter.title!!,
|
||||
scanlator = chapter.scanlator ?: "Unknown",
|
||||
imageData = images,
|
||||
sourceMedia = media,
|
||||
retries = 2,
|
||||
retries = 25,
|
||||
simultaneousDownloads = 2
|
||||
)
|
||||
|
||||
@@ -485,7 +491,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
|
||||
// Inform the adapter that the download has started
|
||||
withContext(Dispatchers.Main) {
|
||||
chapterAdapter.startDownload(i)
|
||||
chapterAdapter.startDownload(i.uniqueNumber())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -516,11 +522,11 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
}
|
||||
|
||||
|
||||
fun onMangaChapterRemoveDownloadClick(i: String) {
|
||||
fun onMangaChapterRemoveDownloadClick(i: MangaChapter) {
|
||||
downloadManager.removeDownload(
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
i.number,
|
||||
MediaType.MANGA
|
||||
)
|
||||
) {
|
||||
@@ -528,7 +534,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
}
|
||||
}
|
||||
|
||||
fun onMangaChapterStopDownloadClick(i: String) {
|
||||
fun onMangaChapterStopDownloadClick(i: MangaChapter) {
|
||||
val cancelIntent = Intent().apply {
|
||||
action = MangaDownloaderService.ACTION_CANCEL_DOWNLOAD
|
||||
putExtra(MangaDownloaderService.EXTRA_CHAPTER, i)
|
||||
@@ -539,11 +545,11 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
|
||||
downloadManager.removeDownload(
|
||||
DownloadedType(
|
||||
media.mainName(),
|
||||
i,
|
||||
i.number,
|
||||
MediaType.MANGA
|
||||
)
|
||||
) {
|
||||
chapterAdapter.purgeDownload(i)
|
||||
chapterAdapter.purgeDownload(i.uniqueNumber())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,9 +590,11 @@ 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
|
||||
media.manga?.chapters?.values?.maxOfOrNull {
|
||||
MediaNameAdapter.findChapterNumber(it.number) ?: 0f
|
||||
} ?: 0f
|
||||
selected.latest =
|
||||
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
|
||||
|
||||
@@ -618,14 +626,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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user