1
0
mirror of https://github.com/KokaKiwi/BarInsta synced 2024-11-22 06:37:30 +00:00

Merge branch 'master' into pr/934

This commit is contained in:
Austin Huang 2021-06-13 12:02:51 -04:00
commit f961c422ca
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
695 changed files with 26946 additions and 27591 deletions

View File

@ -79,6 +79,25 @@
"code"
]
},
{
"login": "stamatiap",
"name": "Stamatia Papageorgiou",
"avatar_url": "https://avatars.githubusercontent.com/u/57223967?v=4",
"profile": "https://github.com/stamatiap",
"contributions": [
"code",
"translation"
]
},
{
"login": "The-EDev",
"name": "Farook Al-Sammarraie",
"avatar_url": "https://avatars.githubusercontent.com/u/60552923?v=4",
"profile": "https://github.com/The-EDev",
"contributions": [
"code"
]
},
{
"login": "Zopieux",
"name": "Alexandre Macabies",

5
.codebeatsettings Normal file
View File

@ -0,0 +1,5 @@
{
"JAVA": {
"TOO_MANY_IVARS": [8, 10, 20, 30]
}
}

View File

@ -14,6 +14,7 @@ assignees: 'austinhuang0131'
- [ ] My app is on the latest version available on GitHub or F-Droid. I understand that any other version is not supported.
- [ ] I certify that all actions I have performed on the app constitute human behaviour. I am using the app responsibly and in a way identical to using the official app. Specifically, I did not perform botting or automated key clicks.
- [ ] I have considered other possible reasons for my ban, and I cannot find any substantial claim other than the usage of this app.
- [ ] Instagram has indicated that this is a permanent ban, not a required password change or a temporary lock.
## Answer honestly. Check accordingly to your situation.

View File

@ -6,9 +6,9 @@ labels: bug
assignees: ''
---
<!-- If you choose not to follow this format, you should erase the entire body. -->
<!-- Frequent reporters can ignore this template as long as it's clear and concise. -->
- [ ] My app is on the latest version. I understand that any other version is not supported.
- [ ] My app is *at least* at the current release version. I understand that any versions before that is not supported.
- [ ] I have read [the FAQ](https://barinsta.austinhuang.me/en/latest/faq).
<!--
@ -30,4 +30,4 @@ Describe the bug here. Don't stress too much, but do include the key points.
## Additional info
<!-- If you have log (and it's working), upload it here. -->
<!-- If you have a crash dump, paste it here. -->

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
blank_issues_enabled: true
contact_links:
- name: Community Chatrooms
url: https://barinsta.austinhuang.me/en/latest/chat/
about: Chat with developers and users alike!
- name: /r/Barinsta
url: https://reddit.com/r/barinsta
about: Start a discussion on our subreddit!
- name: Repository Discussions
url: https://github.com/austinhuang0131/barinsta/discussions
about: Start a discussion in this repo!

View File

@ -1,16 +0,0 @@
---
name: General questions & feedback
about: These should be submitted to either one of our chatrooms, the discussions, or r/barinsta on reddit.
title: "[Q]"
labels: question
assignees: ''
---
STOP!!! STOP!!! STOP!!!
General questions & feedback should be submitted to
* one of our chatrooms (see README.md), or
* the GitHub discussions section, or
* r/barinsta on reddit.
STOP!!! STOP!!! STOP!!!

View File

@ -15,18 +15,19 @@ jobs:
uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1
uses: actions/setup-java@v2
with:
java-version: 1.8
distribution: 'zulu'
java-version: '8'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build Github unsigned apk
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre --project-prop split
- name: Sign APK
uses: r0adkll/sign-android-release@v1
uses: ammargitham/sign-android-release@v1.1.1
# ID used to access action output
id: sign_app
with:
@ -45,7 +46,8 @@ jobs:
uses: actions/upload-artifact@v2
with:
name: barinsta_nightly_${{ steps.date.outputs.date }}
path: ${{steps.sign_app.outputs.signedReleaseFile}}
# path: ${{steps.sign_app.outputs.signedReleaseFile}}
path: app/build/outputs/apk/github/release/*-signed.apk
# Send success notification
- name: Send success Telegram notification
@ -55,7 +57,8 @@ jobs:
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }}
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }}
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} completed successfully.\nhttps://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
document: ${{steps.sign_app.outputs.signedReleaseFile}}
# document: ${{steps.sign_app.outputs.signedReleaseFile}}
document: app/build/outputs/apk/github/release/*-signed.apk
# Send failure notification
- name: Send failure Telegram notification

View File

@ -14,20 +14,21 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1
uses: actions/setup-java@v2
with:
java-version: 1.8
distribution: 'zulu'
java-version: '8'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build Github unsigned pre-release apk
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre
run: ./gradlew assembleGithubRelease --stacktrace --project-prop pre --project-prop split
- name: Sign APK
uses: r0adkll/sign-android-release@v1
uses: ammargitham/sign-android-release@v1.1.1
# ID used to access action output
id: sign_app
with:
@ -36,18 +37,19 @@ jobs:
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Get current date and time
id: date
run: echo "::set-output name=date::$(date +'%Y%m%d_%H%M%S')"
# Create artifact
# Create artifact
- name: Create apk artifact
uses: actions/upload-artifact@v2
with:
name: barinsta_pre-release_${{ steps.date.outputs.date }}
path: ${{steps.sign_app.outputs.signedReleaseFile}}
# path: ${{steps.sign_app.outputs.signedReleaseFile}}
path: app/build/outputs/apk/github/release/*-signed.apk
# Send success notification
- name: Send success Telegram notification
if: ${{ success() }}
@ -56,8 +58,9 @@ jobs:
to: ${{ secrets.TELEGRAM_BUILDS_CHANNEL_TO }}
token: ${{ secrets.TELEGRAM_BUILDS_BOT_TOKEN }}
message: "${{ github.workflow }} ${{ github.job }} #${{ github.run_number }} completed successfully.\nURL: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
document: ${{steps.sign_app.outputs.signedReleaseFile}}
# document: ${{steps.sign_app.outputs.signedReleaseFile}}
document: app/build/outputs/apk/github/release/*-signed.apk
# Send failure notification
- name: Send failure Telegram notification
if: ${{ failure() }}

2
.gitignore vendored
View File

@ -20,3 +20,5 @@ app/release
/sentry.properties
/app/fdroid/
/app/github/
/repo
/.fdroid.yml

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
<bytecodeTargetLevel target="11" />
</component>
</project>

View File

@ -7,7 +7,6 @@
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

View File

@ -40,7 +40,7 @@
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -3,6 +3,7 @@
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />

View File

@ -6,10 +6,10 @@
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
[![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.svg?v=103)](https://github.com/ellerbrock/open-source-badges/)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://makeapullrequest.com)
[![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](./LICENSE)
[![GitHub stars](https://img.shields.io/github/stars/austinhuang0131/instagrabber.svg?style=social&label=Star)](https://GitHub.com/austinhuang0131/barinsta/stargazers/)<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-42-orange.svg)](#contributors)
[![All Contributors](https://img.shields.io/badge/all_contributors-44-orange.svg)](#contributors)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
Instagram client; previously known as InstaGrabber.
@ -27,12 +27,12 @@ Version status: ![F-Droid](https://img.shields.io/f-droid/v/me.austinhuang.insta
## Screenshots
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg" alt="Profile" width="15%"/></a>
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg" alt="Post" width="15%"/></a>
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg" alt="Comments" width="15%"/></a>
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg" alt="Story" width="15%"/></a>
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg" alt="Hashtag" width="15%"/></a>
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg" alt="Discover Topics" width="15%"/></a>
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" alt="Profile" width="15%"/></a>
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" alt="Post" width="15%"/></a>
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" alt="Comments" width="15%"/></a>
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" alt="Story" width="15%"/></a>
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" alt="Hashtag" width="15%"/></a>
<a href="https://github.com/austinhuang0131/instagrabber/blob/master/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png"><img src="./fastlane/metadata/android/en-US/images/phoneScreenshots/6.png" alt="Discover Topics" width="15%"/></a>
## We need maintainers!
@ -63,49 +63,53 @@ Prominent contributors are listed here in the [all-contributors](https://allcont
</tr>
<tr>
<td align="center"><a href="https://github.com/MeLlamoPablo"><img src="https://avatars.githubusercontent.com/u/11708035?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pablo Rodríguez</b></sub></a><br /><a href="https://github.com/austinhuang0131/barinsta/commits?author=MeLlamoPablo" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/stamatiap"><img src="https://avatars.githubusercontent.com/u/57223967?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Stamatia Papageorgiou</b></sub></a><br /><a href="https://github.com/austinhuang0131/barinsta/commits?author=stamatiap" title="Code">💻</a> <a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/The-EDev"><img src="https://avatars.githubusercontent.com/u/60552923?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Farook Al-Sammarraie</b></sub></a><br /><a href="https://github.com/austinhuang0131/barinsta/commits?author=The-EDev" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Zopieux"><img src="https://avatars.githubusercontent.com/u/81353?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexandre Macabies</b></sub></a><br /><a href="https://github.com/austinhuang0131/barinsta/commits?author=Zopieux" title="Code">💻</a></td>
<td align="center"><a href="https://snajdovski.github.io"><img src="https://avatars2.githubusercontent.com/u/42580385?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Stefan Najdovski</b></sub></a><br /><a href="#design-snajdovski" title="Design">🎨</a> <a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/CrazyMarvin"><img src="https://avatars3.githubusercontent.com/u/15004217?v=4?s=100" width="100px;" alt=""/><br /><sub><b>CrazyMarvin</b></sub></a><br /><a href="#financial-CrazyMarvin" title="Financial">💵</a></td>
<td align="center"><a href="http://kevinthomas.dev"><img src="https://avatars2.githubusercontent.com/u/15370181?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Thomas</b></sub></a><br /><a href="#financial-KevinNThomas" title="Financial">💵</a></td>
<td align="center"><a href="https://github.com/Shadowspear123"><img src="https://avatars1.githubusercontent.com/u/50462281?s=100" width="100px;" alt=""/><br /><sub><b>Shadowspear123</b></sub></a><br /><a href="#blog-Shadowspear123" title="Blogposts">📝</a> <a href="https://github.com/austinhuang0131/barinsta/issues?q=author%3AShadowspear123" title="Bug reports">🐛</a> <a href="#ideas-Shadowspear123" title="Ideas, Planning, & Feedback">🤔</a> <a href="#question-Shadowspear123" title="Answering Questions">💬</a></td>
</tr>
<tr>
<td align="center"><a href="https://kevinthomas.dev"><img src="https://avatars2.githubusercontent.com/u/15370181?s=100" width="100px;" alt=""/><br /><sub><b>Kevin Thomas</b></sub></a><br /><a href="#financial-KevinNThomas" title="Financial">💵</a></td>
<td align="center"><a href="https://github.com/Shadowspear123"><img src="https://avatars1.githubusercontent.com/u/50462281?s=100" width="100px;" alt=""/><br /><sub><b>Shadowspear123</b></sub></a><br /><a href="#blog-Shadowspear123" title="Blogposts">📝</a> <a href="https://github.com/austinhuang0131/barinsta/issues?q=author%3AShadowspear123" title="Bug reports">🐛</a> <a href="#ideas-Shadowspear123" title="Ideas, Planning, & Feedback">🤔</a> <a href="#question-Shadowspear123" title="Answering Questions">💬</a></td>
<td align="center"><a href="https://github.com/RickyM7"><img src="https://avatars3.githubusercontent.com/u/24703825?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ricardo</b></sub></a><br /><a href="https://github.com/austinhuang0131/barinsta/issues?q=author%3ARickyM7" title="Bug reports">🐛</a> <a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Akrai"><img src="https://avatars1.githubusercontent.com/u/5624597?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Akrai</b></sub></a><br /><a href="#ideas-Akrai" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/avtkal"><img src="https://avatars.githubusercontent.com/u/63205014?v=4?s=100" width="100px;" alt=""/><br /><sub><b>avtkal</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/cizordj"><img src="https://avatars2.githubusercontent.com/u/32869222?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Cézar Augusto</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/dimitrist19"><img src="https://avatars.githubusercontent.com/u/56406468?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dimitris T</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/farzadx"><img src="https://avatars2.githubusercontent.com/u/70059397?v=4?s=100" width="100px;" alt=""/><br /><sub><b>farzadx</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/dimitrist19"><img src="https://avatars.githubusercontent.com/u/56406468?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dimitris T</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/farzadx"><img src="https://avatars2.githubusercontent.com/u/70059397?v=4?s=100" width="100px;" alt=""/><br /><sub><b>farzadx</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/faydin"><img src="https://avatars2.githubusercontent.com/u/22706676?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Fatih Aydın</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/fouze555"><img src="https://avatars3.githubusercontent.com/u/71935341?v=4?s=100" width="100px;" alt=""/><br /><sub><b>fouze555</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Galang23"><img src="https://avatars3.githubusercontent.com/u/13700948?s=100" width="100px;" alt=""/><br /><sub><b>Galang23</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/initdebugs"><img src="https://avatars0.githubusercontent.com/u/75781464?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Initdebugs</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://janek.xyz/"><img src="https://avatars3.githubusercontent.com/u/8365659?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jakub Janek</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/GenosseFlosse"><img src="https://avatars.githubusercontent.com/u/59205524?v=4?s=100" width="100px;" alt=""/><br /><sub><b>GenosseFlosse</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center"><a href="https://janek.xyz/"><img src="https://avatars3.githubusercontent.com/u/8365659?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jakub Janek</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/GenosseFlosse"><img src="https://avatars.githubusercontent.com/u/59205524?v=4?s=100" width="100px;" alt=""/><br /><sub><b>GenosseFlosse</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://becauseofprog.fr/"><img src="https://avatars3.githubusercontent.com/u/24623168?s=100" width="100px;" alt=""/><br /><sub><b>kernoeb</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/MoaufmKlo"><img src="https://avatars1.githubusercontent.com/u/45636897?s=100" width="100px;" alt=""/><br /><sub><b>MoaufmKlo</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/nalinalini"><img src="https://avatars0.githubusercontent.com/u/65640431?v=4?s=100" width="100px;" alt=""/><br /><sub><b>nalinalini</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/peterge1998"><img src="https://avatars2.githubusercontent.com/u/47355238?s=100" width="100px;" alt=""/><br /><sub><b>peterge1998</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/PierreM0"><img src="https://avatars3.githubusercontent.com/u/71077853?v=4?s=100" width="100px;" alt=""/><br /><sub><b>PierreM0</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Pyrobauve"><img src="https://avatars.githubusercontent.com/u/48654473?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pyrobauve</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/PierreM0"><img src="https://avatars3.githubusercontent.com/u/71077853?v=4?s=100" width="100px;" alt=""/><br /><sub><b>PierreM0</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Pyrobauve"><img src="https://avatars.githubusercontent.com/u/48654473?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pyrobauve</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/RAMAR-RAR"><img src="https://avatars3.githubusercontent.com/u/47423745?s=100" width="100px;" alt=""/><br /><sub><b>RAMAR-RAR</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/rohang02"><img src="https://avatars3.githubusercontent.com/u/47921164?v=4?s=100" width="100px;" alt=""/><br /><sub><b>rohang02</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/retiolus"><img src="https://avatars1.githubusercontent.com/u/65604466?v=4?s=100" width="100px;" alt=""/><br /><sub><b>retiolus</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/rikishi0071"><img src="https://avatars3.githubusercontent.com/u/18183855?v=4?s=100" width="100px;" alt=""/><br /><sub><b>rikishi0071</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://gitlab.com/sandboiii"><img src="https://avatars.githubusercontent.com/u/17468894?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexey Peschany</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Sitavi"><img src="https://avatars.githubusercontent.com/u/80586127?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sitavi</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center"><a href="https://gitlab.com/sandboiii"><img src="https://avatars.githubusercontent.com/u/17468894?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexey Peschany</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Sitavi"><img src="https://avatars.githubusercontent.com/u/80586127?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sitavi</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://stillu.cc/"><img src="https://avatars2.githubusercontent.com/u/5843208?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Still Hsu</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/Lego8486"><img src="https://avatars1.githubusercontent.com/u/47414485?s=100" width="100px;" alt=""/><br /><sub><b>Ten_Lego</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/wagnim"><img src="https://avatars0.githubusercontent.com/u/30241419?s=100" width="100px;" alt=""/><br /><sub><b>wagnim</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/wokija"><img src="https://avatars.githubusercontent.com/u/14982166?v=4?s=100" width="100px;" alt=""/><br /><sub><b>wokija</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/ysakamoto"><img src="https://avatars3.githubusercontent.com/u/1331642?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ysakamoto</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
<td align="center"><a href="https://github.com/ZDVokoun"><img src="https://avatars.githubusercontent.com/u/76393152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ZDVokoun</b></sub></a><br /><a href="https://crowdin.com/project/instagrabber" title="Translation">🌍</a></td>
</tr>
@ -121,7 +125,7 @@ This app's predecessor, InstaGrabber, was originally made by [@AwaisKing](https:
Barinsta
Copyright (C) 2020-2021 Austin Huang <im@austinhuang.me>
Ammar Githam <ammargitham786@gmail.com>
Ammar Githam <ammar.githam@outlook.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,5 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: "androidx.navigation.safeargs"
apply plugin: 'kotlin-kapt'
apply from: 'sentry.gradle'
def getGitHash = { ->
@ -20,8 +22,8 @@ android {
minSdkVersion 21
targetSdkVersion 30
versionCode 61
versionName '19.2.0'
versionCode 63
versionName '19.2.2'
multiDexEnabled true
@ -33,6 +35,12 @@ android {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
compileOptions {
@ -76,6 +84,27 @@ android {
}
}
splits {
// Configures multiple APKs based on ABI.
abi {
// Enables building multiple APKs per ABI.
enable project.hasProperty("split") && !gradle.startParameter.taskNames.isEmpty() && gradle.startParameter.taskNames.get(0).contains('Release')
// By default all ABIs are included, so use reset() and include to specify that we only
// want APKs for x86 and x86_64.
// Resets the list of ABIs that Gradle should create APKs for to none.
reset()
// Specifies a list of ABIs that Gradle should create APKs for.
include "x86", "x86_64", "arm64-v8a", "armeabi-v7a"
// Specifies that we want to also generate a universal APK that includes all ABIs.
universalApk true
}
}
android.applicationVariants.all { variant ->
if (variant.flavorName != "github") return
variant.outputs.all { output ->
@ -84,17 +113,47 @@ android {
// def versionCode = variant.versionCode
def flavor = variant.flavorName
def suffix = "${versionName}-${flavor}_${builtType}" // eg. 19.1.0-github_debug or release
def flavorBuiltType = "${flavor}_${builtType}"
def suffix
// For x86 and x86_64, the versionNames are already overridden
if (versionName.contains(flavorBuiltType)) {
suffix = "${versionName}"
} else {
suffix = "${versionName}-${flavorBuiltType}" // eg. 19.1.0-github_debug or release
}
if (builtType.toString() == 'release' && project.hasProperty("pre")) {
buildConfigField("boolean", "isPre", "true")
// append latest commit short hash for pre-release
suffix = "${versionName}.${getGitHash()}-${flavor}" // eg. 19.1.0.b123456-github
flavorBuiltType = "${getGitHash()}-${flavor}"
// For x86 and x86_64, the versionNames are already overridden
if (versionName.contains(flavorBuiltType)) {
suffix = "${versionName}"
} else {
// append latest commit short hash for pre-release
suffix = "${versionName}.${flavorBuiltType}" // eg. 19.1.0.b123456-github
}
}
output.versionNameOverride = suffix
outputFileName = "barinsta_${suffix}.apk"
def abi = output.getFilter(com.android.build.OutputFile.ABI)
// println(abi + ", " + versionName + ", " + flavor + ", " + builtType + ", " + suffix)
outputFileName = abi == null ? "barinsta_${suffix}.apk" : "barinsta_${suffix}_${abi}.apk"
}
}
packagingOptions {
// Exclude file to avoid
// Error: Duplicate files during packaging of APK
exclude 'META-INF/LICENSE.md'
exclude 'META-INF/LICENSE-notice.md'
exclude 'META-INF/atomicfu.kotlin_module'
}
testOptions.unitTests {
includeAndroidResources = true
}
}
configurations.all {
@ -104,49 +163,61 @@ configurations.all {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
def appcompat_version = "1.2.0"
def nav_version = '2.3.4'
def exoplayer_version = '2.13.2'
def nav_version = '2.3.5'
def exoplayer_version = '2.13.3'
implementation 'com.google.android.material:material:1.4.0-alpha02'
implementation 'com.google.android.material:material:1.4.0-beta01'
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
implementation "androidx.appcompat:appcompat:$appcompat_version"
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
implementation "androidx.recyclerview:recyclerview:1.2.0-rc01"
implementation "androidx.recyclerview:recyclerview:1.2.0"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.preference:preference:1.1.1"
implementation "androidx.work:work-runtime:2.5.0"
implementation 'androidx.palette:palette:1.0.0'
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation 'com.google.guava:guava:27.0.1-android'
def core_version = "1.6.0-beta01"
implementation "androidx.core:core:$core_version"
// Fragment
implementation "androidx.fragment:fragment-ktx:1.3.4"
// Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
// Room
def room_version = "2.2.6"
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-guava:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// CameraX
def camerax_version = "1.1.0-alpha03"
def camerax_version = "1.1.0-alpha04"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:1.0.0-alpha22"
implementation "androidx.camera:camera-view:1.0.0-alpha24"
// EmojiCompat
def emoji_compat_version = "1.1.0"
implementation "androidx.emoji:emoji:$emoji_compat_version"
implementation "androidx.emoji:emoji-appcompat:$emoji_compat_version"
implementation 'me.austinhuang:AutoLinkTextViewV2:-SNAPSHOT'
// Work
def work_version = '2.5.0'
implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0"
implementation 'com.facebook.fresco:fresco:2.3.0'
implementation 'com.facebook.fresco:animated-webp:2.3.0'
@ -157,12 +228,27 @@ dependencies {
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'org.apache.commons:commons-imaging:1.0-alpha2'
implementation 'com.github.ammargitham:uCrop:2.3-beta'
implementation 'com.github.skydoves:balloon:1.3.4'
implementation 'com.github.ammargitham:AutoLinkTextViewV2:3.2.0'
implementation 'com.github.ammargitham:uCrop:2.3-native-beta-2'
implementation 'com.github.ammargitham:android-gpuimage:2.1.1-beta4'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
githubImplementation 'io.sentry:sentry-android:4.3.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
testImplementation "androidx.test.ext:junit-ktx:1.1.2"
testImplementation "androidx.test:core-ktx:1.3.0"
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.robolectric:robolectric:4.5.1"
androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
androidTestImplementation 'androidx.test:core:1.3.0'
androidTestImplementation 'com.android.support:support-annotations:28.0.0'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation "androidx.room:room-testing:2.3.0"
}

View File

@ -0,0 +1,227 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "232e618b3bfcb4661336b359d036c455",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `username` TEXT, `cookie` TEXT, `full_name` TEXT, `profile_pic` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cookie",
"columnName": "cookie",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "fullName",
"columnName": "full_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "profilePic",
"columnName": "profile_pic",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "favorites",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query_text` TEXT, `type` TEXT, `display_name` TEXT, `pic_url` TEXT, `date_added` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayName",
"columnName": "display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "picUrl",
"columnName": "pic_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "dm_last_notified",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thread_id` TEXT, `last_notified_msg_ts` INTEGER, `last_notified_at` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "threadId",
"columnName": "thread_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastNotifiedMsgTs",
"columnName": "last_notified_msg_ts",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastNotifiedAt",
"columnName": "last_notified_at",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_dm_last_notified_thread_id",
"unique": true,
"columnNames": [
"thread_id"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_last_notified_thread_id` ON `${TABLE_NAME}` (`thread_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "recent_searches",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ig_id` TEXT NOT NULL, `name` TEXT NOT NULL, `username` TEXT, `pic_url` TEXT, `type` TEXT NOT NULL, `last_searched_on` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "igId",
"columnName": "ig_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "picUrl",
"columnName": "pic_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSearchedOn",
"columnName": "last_searched_on",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_recent_searches_ig_id_type",
"unique": true,
"columnNames": [
"ig_id",
"type"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_recent_searches_ig_id_type` ON `${TABLE_NAME}` (`ig_id`, `type`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '232e618b3bfcb4661336b359d036c455')"
]
}
}

View File

@ -0,0 +1,51 @@
package awais.instagrabber.db;
import androidx.room.Room;
import androidx.room.migration.Migration;
import androidx.room.testing.MigrationTestHelper;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import static awais.instagrabber.db.AppDatabase.MIGRATION_4_5;
import static awais.instagrabber.db.AppDatabase.MIGRATION_5_6;
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
private static final Migration[] ALL_MIGRATIONS = new Migration[]{MIGRATION_4_5, MIGRATION_5_6};
@Rule
public MigrationTestHelper helper;
public MigrationTest() {
final String canonicalName = AppDatabase.class.getCanonicalName();
assert canonicalName != null;
helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
canonicalName,
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrateAll() throws IOException {
// Create earliest version of the database. Have to start with 4 since that is the version we migrated to Room.
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 4);
db.close();
// Open latest version of the database. Room will validate the schema
// once all migrations execute.
AppDatabase appDb = Room.databaseBuilder(InstrumentationRegistry.getInstrumentation().getTargetContext(),
AppDatabase.class,
TEST_DB)
.addMigrations(ALL_MIGRATIONS).build();
appDb.getOpenHelper().getWritableDatabase();
appDb.close();
}
}

View File

@ -0,0 +1,82 @@
package awais.instagrabber.db.dao;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.room.Room;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.runner.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.runner.RunWith;
import java.time.LocalDateTime;
import java.util.List;
import awais.instagrabber.db.AppDatabase;
import awais.instagrabber.db.entities.RecentSearch;
import awais.instagrabber.models.enums.FavoriteType;
@RunWith(AndroidJUnit4.class)
public class RecentSearchDaoTest {
private static final String TAG = RecentSearchDaoTest.class.getSimpleName();
private RecentSearchDao dao;
private AppDatabase db;
@Before
public void createDb() {
final Context context = ApplicationProvider.getApplicationContext();
db = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build();
dao = db.recentSearchDao();
}
@After
public void closeDb() {
db.close();
}
@Test
public void writeQueryDelete() {
final RecentSearch recentSearch = insertRecentSearch("1", "test1", FavoriteType.HASHTAG);
final RecentSearch byIgIdAndType = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG);
Assertions.assertEquals(recentSearch, byIgIdAndType);
dao.deleteRecentSearch(byIgIdAndType);
final RecentSearch deleted = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG);
Assertions.assertNull(deleted);
}
@Test
public void queryAllOrdered() {
final List<RecentSearch> insertListReversed = ImmutableList
.<RecentSearch>builder()
.add(insertRecentSearch("1", "test1", FavoriteType.HASHTAG))
.add(insertRecentSearch("2", "test2", FavoriteType.LOCATION))
.add(insertRecentSearch("3", "test3", FavoriteType.USER))
.add(insertRecentSearch("4", "test4", FavoriteType.USER))
.add(insertRecentSearch("5", "test5", FavoriteType.USER))
.build()
.reverse(); // important
final List<RecentSearch> fromDb = dao.getAllRecentSearches();
Assertions.assertIterableEquals(insertListReversed, fromDb);
}
@NonNull
private RecentSearch insertRecentSearch(final String igId, final String name, final FavoriteType type) {
final RecentSearch recentSearch = new RecentSearch(
igId,
name,
null,
null,
type,
LocalDateTime.now()
);
dao.insertRecentSearch(recentSearch);
return recentSearch;
}
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
<string name="enable_sentry">Povolit Sentry</string>
<string name="sentry_summary">Sentry je listener/handler, který zaznamenává chyby a asynchronně je posílá na Sentry.io</string>
<string name="sentry_start_next_launch">Sentry se spustí při příštím spuštění</string>
</resources>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
<string name="enable_sentry">Aktiviere Sentry</string>
<string name="sentry_summary">Sentry ist ein Listener/Handler für Fehler, der den Fehler/das Ereignis asynchron an Sentry.io sendet</string>
<string name="sentry_start_next_launch">Sentry startet beim nächsten Start</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Sentry 활성화</string>
<string name="sentry_summary">Sentry는 Sentry.io에 오류를 비동기적으로 보내는 오류 처리기입니다</string>
<string name="sentry_start_next_launch">Sentry는 다음 출시에 시작됩니다</string>
</resources>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
<string name="enable_sentry">Sentry inschakelen</string>
<string name="sentry_summary">Sentry is een luister/handler voor fouten die asynchroon de fout/gebeurtenis versturen naar Sentry.io</string>
<string name="sentry_start_next_launch">Sentry zal starten bij de volgende lancering</string>
</resources>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Włącz Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_summary">Sentry jest słuchaczem/obsługą błędów, które asynchronicznie wysyłają błąd/zdarzenie do Sentry.io</string>
<string name="sentry_start_next_launch">Sentry rozpocznie się przy następnym uruchomieniu</string>
</resources>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Включить режим \"часового\"</string>
<string name="sentry_summary">\"Часовой\" - это слушатель/обработчик ошибок, который асинхронно отправляет ошибку/событие на Sentry.io</string>
<string name="sentry_start_next_launch">\"Часовой\" будет запущен при следующем запуске</string>
<string name="enable_sentry">Включить Sentry</string>
<string name="sentry_summary">Sentry - это обработчик событий, который асинхронно отправляет сообщения об ошибках/поломках на Sentry.io</string>
<string name="sentry_start_next_launch">Sentry включится при следующем запуске приложения</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
</resources>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
<string name="enable_sentry">Bật Sentry</string>
<string name="sentry_summary">Sentry là một thiết bị nghe/giải quyết cho những lỗi mà gửi những lỗi/sự kiện đến Sentry.io một cách tách biệt</string>
<string name="sentry_start_next_launch">Sentry sẽ được bật vào lần khởi động kế tiếp</string>
</resources>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
<string name="enable_sentry">啟用 Sentry</string>
<string name="sentry_summary">Sentry 將會錯誤報告發送至 Sentry.io</string>
<string name="sentry_start_next_launch">下次啟用應用程式時將會開啟 Sentry</string>
</resources>

View File

@ -26,8 +26,7 @@
<activity
android:name=".activities.MainActivity"
android:launchMode="singleTop"
android:taskAffinity=".Main"
android:windowSoftInputMode="adjustResize">
android:taskAffinity=".Main">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
@ -51,13 +50,14 @@
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="ig.me" />
<data android:host="www.ig.me" />
<data android:host="instagr.am" />
<data android:host="www.instagr.am" />
<data android:host="instagram.com" />
<data android:host="www.instagram.com" />
<data android:pathPrefix="/" />
<data android:pathPrefix="/p" />
<data android:pathPrefix="/explore/tags" />
<data android:pathPrefix="/explore/locations" />
</intent-filter>
</activity>
<activity
@ -76,41 +76,6 @@
android:screenOrientation="portrait"
android:taskAffinity="awais.instagrabber.errorreport"
android:theme="@android:style/Theme.DeviceDefault.Dialog" />
<activity
android:name=".activities.DirectDownload"
android:allowEmbedded="false"
android:allowTaskReparenting="false"
android:autoRemoveFromRecents="true"
android:clearTaskOnLaunch="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:description="@string/direct_download_desc"
android:documentLaunchMode="never"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true"
android:label="@string/direct_download"
android:launchMode="singleTask"
android:lockTaskMode="never"
android:noHistory="false"
android:theme="@style/Theme.AppCompat.Translucent">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEARCH" />
<action android:name="android.intent.action.WEB_SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="ig.me" />
<data android:scheme="http" />
<data android:scheme="https" />
</intent-filter>
</activity>
<activity
android:name=".activities.Login"
android:label="@string/login"
@ -173,4 +138,4 @@
android:value="Noto Color Emoji Compat" />
</application>
</manifest>
</manifest>

View File

@ -1,91 +0,0 @@
package awais.instagrabber;
import android.app.Application;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipelineConfig;
import java.net.CookieHandler;
import java.text.SimpleDateFormat;
import java.util.UUID;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.LocaleUtils;
import awais.instagrabber.utils.SettingsHelper;
import awais.instagrabber.utils.TextUtils;
import awaisomereport.CrashReporter;
import static awais.instagrabber.utils.CookieUtils.NET_COOKIE_MANAGER;
import static awais.instagrabber.utils.Utils.applicationHandler;
import static awais.instagrabber.utils.Utils.cacheDir;
import static awais.instagrabber.utils.Utils.clipboardManager;
import static awais.instagrabber.utils.Utils.datetimeParser;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class InstaGrabberApplication extends Application {
private static final String TAG = "InstaGrabberApplication";
@Override
public void onCreate() {
super.onCreate();
CookieHandler.setDefault(NET_COOKIE_MANAGER);
if (settingsHelper == null) {
settingsHelper = new SettingsHelper(this);
}
if (!BuildConfig.DEBUG) {
CrashReporter.get(this).start();
}
// logCollector = new LogCollector(this);
if (BuildConfig.DEBUG) {
try {
Class.forName("dalvik.system.CloseGuard")
.getMethod("setEnabled", boolean.class)
.invoke(null, true);
} catch (Exception e) {
Log.e(TAG, "Error", e);
}
}
// final Set<RequestListener> requestListeners = new HashSet<>();
// requestListeners.add(new RequestLoggingListener());
final ImagePipelineConfig imagePipelineConfig = ImagePipelineConfig
.newBuilder(this)
// .setMainDiskCacheConfig(diskCacheConfig)
// .setRequestListeners(requestListeners)
.setDownsampleEnabled(true)
.build();
Fresco.initialize(this, imagePipelineConfig);
// FLog.setMinimumLoggingLevel(FLog.VERBOSE);
if (applicationHandler == null) {
applicationHandler = new Handler(getApplicationContext().getMainLooper());
}
if (cacheDir == null) {
cacheDir = getCacheDir().getAbsolutePath();
}
LocaleUtils.setLocale(getBaseContext());
if (clipboardManager == null)
clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
if (datetimeParser == null)
datetimeParser = new SimpleDateFormat(
settingsHelper.getBoolean(Constants.CUSTOM_DATE_TIME_FORMAT_ENABLED) ?
settingsHelper.getString(Constants.CUSTOM_DATE_TIME_FORMAT) :
settingsHelper.getString(Constants.DATE_TIME_FORMAT), LocaleUtils.getCurrentLocale());
if (TextUtils.isEmpty(settingsHelper.getString(Constants.DEVICE_UUID))) {
settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString());
}
}
}

View File

@ -0,0 +1,71 @@
package awais.instagrabber
import android.app.Application
import android.content.ClipboardManager
import android.util.Log
import awais.instagrabber.fragments.settings.PreferenceKeys.CUSTOM_DATE_TIME_FORMAT
import awais.instagrabber.fragments.settings.PreferenceKeys.CUSTOM_DATE_TIME_FORMAT_ENABLED
import awais.instagrabber.fragments.settings.PreferenceKeys.DATE_TIME_FORMAT
import awais.instagrabber.utils.*
import awais.instagrabber.utils.LocaleUtils.currentLocale
import awais.instagrabber.utils.Utils.settingsHelper
import awais.instagrabber.utils.extensions.TAG
import awaisomereport.CrashReporter
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
import java.net.CookieHandler
import java.time.format.DateTimeFormatter
import java.util.*
@Suppress("unused")
class InstaGrabberApplication : Application() {
override fun onCreate() {
super.onCreate()
CookieHandler.setDefault(NET_COOKIE_MANAGER)
settingsHelper = SettingsHelper(this)
setupCrashReporter()
setupCloseGuard()
setupFresco()
Utils.cacheDir = cacheDir.absolutePath
Utils.clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
LocaleUtils.setLocale(baseContext)
val pattern = if (settingsHelper.getBoolean(CUSTOM_DATE_TIME_FORMAT_ENABLED)) {
settingsHelper.getString(CUSTOM_DATE_TIME_FORMAT)
} else {
settingsHelper.getString(DATE_TIME_FORMAT)
}
TextUtils.setFormatter(DateTimeFormatter.ofPattern(pattern, currentLocale))
if (TextUtils.isEmpty(settingsHelper.getString(Constants.DEVICE_UUID))) {
settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString())
}
}
private fun setupCrashReporter() {
if (BuildConfig.DEBUG) return
CrashReporter.get(this).start()
// logCollector = new LogCollector(this);
}
private fun setupCloseGuard() {
if (!BuildConfig.DEBUG) return
try {
Class.forName("dalvik.system.CloseGuard")
.getMethod("setEnabled", Boolean::class.javaPrimitiveType)
.invoke(null, true)
} catch (e: Exception) {
Log.e(TAG, "Error", e)
}
}
private fun setupFresco() {
// final Set<RequestListener> requestListeners = new HashSet<>();
// requestListeners.add(new RequestLoggingListener());
val imagePipelineConfig = ImagePipelineConfig
.newBuilder(this) // .setMainDiskCacheConfig(diskCacheConfig)
// .setRequestListeners(requestListeners)
.setDownsampleEnabled(true)
.build()
Fresco.initialize(this, imagePipelineConfig)
// FLog.setMinimumLoggingLevel(FLog.VERBOSE);
}
}

View File

@ -1,21 +0,0 @@
package awais.instagrabber.activities;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import awais.instagrabber.utils.LocaleUtils;
import awais.instagrabber.utils.ThemeUtils;
public abstract class BaseLanguageActivity extends AppCompatActivity {
protected BaseLanguageActivity() {
LocaleUtils.updateConfig(this);
}
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
ThemeUtils.changeTheme(this);
super.onCreate(savedInstanceState);
}
}

View File

@ -0,0 +1,18 @@
package awais.instagrabber.activities
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import awais.instagrabber.utils.LocaleUtils
import awais.instagrabber.utils.ThemeUtils
abstract class BaseLanguageActivity protected constructor() : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtils.changeTheme(this)
super.onCreate(savedInstanceState)
}
init {
@Suppress("LeakingThis")
LocaleUtils.updateConfig(this)
}
}

View File

@ -1,281 +0,0 @@
package awais.instagrabber.activities;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.content.ContextCompat;
import androidx.documentfile.provider.DocumentFile;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import awais.instagrabber.databinding.ActivityCameraBinding;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.PermissionUtils;
import awais.instagrabber.utils.Utils;
public class CameraActivity extends BaseLanguageActivity {
private static final String TAG = CameraActivity.class.getSimpleName();
private static final int CAMERA_REQUEST_CODE = 100;
private static final String FILE_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS";
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat(FILE_FORMAT, Locale.US);
private ActivityCameraBinding binding;
private ImageCapture imageCapture;
private DocumentFile outputDirectory;
private ExecutorService cameraExecutor;
private int displayId = -1;
private final DisplayManager.DisplayListener displayListener = new DisplayManager.DisplayListener() {
@Override
public void onDisplayAdded(final int displayId) {}
@Override
public void onDisplayRemoved(final int displayId) {}
@Override
public void onDisplayChanged(final int displayId) {
if (displayId == CameraActivity.this.displayId) {
imageCapture.setTargetRotation(binding.getRoot().getDisplay().getRotation());
}
}
};
private DisplayManager displayManager;
private ProcessCameraProvider cameraProvider;
private int lensFacing;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityCameraBinding.inflate(LayoutInflater.from(getBaseContext()));
setContentView(binding.getRoot());
Utils.transparentStatusBar(this, true, false);
displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
outputDirectory = DownloadUtils.getCameraDir();
cameraExecutor = Executors.newSingleThreadExecutor();
displayManager.registerDisplayListener(displayListener, null);
binding.viewFinder.post(() -> {
displayId = binding.viewFinder.getDisplay().getDisplayId();
updateUi();
checkPermissionsAndSetupCamera();
});
}
@Override
protected void onResume() {
super.onResume();
// Make sure that all permissions are still present, since the
// user could have removed them while the app was in paused state.
if (!PermissionUtils.hasCameraPerms(this)) {
PermissionUtils.requestCameraPerms(this, CAMERA_REQUEST_CODE);
}
}
@Override
public void onConfigurationChanged(@NonNull final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Redraw the camera UI controls
updateUi();
// Enable or disable switching between cameras
updateCameraSwitchButton();
}
@Override
protected void onDestroy() {
super.onDestroy();
Utils.transparentStatusBar(this, false, false);
cameraExecutor.shutdown();
displayManager.unregisterDisplayListener(displayListener);
}
private void updateUi() {
binding.cameraCaptureButton.setOnClickListener(v -> {
try {
takePhoto();
} catch (IOException e) {
Log.e(TAG, "updateUi: ", e);
}
});
// Disable the button until the camera is set up
binding.switchCamera.setEnabled(false);
// Listener for button used to switch cameras. Only called if the button is enabled
binding.switchCamera.setOnClickListener(v -> {
lensFacing = CameraSelector.LENS_FACING_FRONT == lensFacing ? CameraSelector.LENS_FACING_BACK
: CameraSelector.LENS_FACING_FRONT;
// Re-bind use cases to update selected camera
bindCameraUseCases();
});
binding.close.setOnClickListener(v -> {
setResult(Activity.RESULT_CANCELED);
finish();
});
}
private void checkPermissionsAndSetupCamera() {
if (PermissionUtils.hasCameraPerms(this)) {
setupCamera();
return;
}
PermissionUtils.requestCameraPerms(this, CAMERA_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
if (requestCode == CAMERA_REQUEST_CODE) {
if (PermissionUtils.hasCameraPerms(this)) {
setupCamera();
}
}
}
private void setupCamera() {
final ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(() -> {
try {
cameraProvider = cameraProviderFuture.get();
// Select lensFacing depending on the available cameras
lensFacing = -1;
if (hasBackCamera()) {
lensFacing = CameraSelector.LENS_FACING_BACK;
} else if (hasFrontCamera()) {
lensFacing = CameraSelector.LENS_FACING_FRONT;
}
if (lensFacing == -1) {
throw new IllegalStateException("Back and front camera are unavailable");
}
// Enable or disable switching between cameras
updateCameraSwitchButton();
// Build and bind the camera use cases
bindCameraUseCases();
} catch (ExecutionException | InterruptedException | CameraInfoUnavailableException e) {
Log.e(TAG, "setupCamera: ", e);
}
}, ContextCompat.getMainExecutor(this));
}
private void bindCameraUseCases() {
final int rotation = binding.viewFinder.getDisplay().getRotation();
// CameraSelector
final CameraSelector cameraSelector = new CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build();
// Preview
final Preview preview = new Preview.Builder()
// Set initial target rotation
.setTargetRotation(rotation)
.build();
// ImageCapture
imageCapture = new ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
// Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
.setTargetRotation(rotation)
.build();
cameraProvider.unbindAll();
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture);
preview.setSurfaceProvider(binding.viewFinder.getSurfaceProvider());
}
private void takePhoto() throws IOException {
if (imageCapture == null) return;
final String extension = "jpg";
final String fileName = SIMPLE_DATE_FORMAT.format(System.currentTimeMillis()) + "." + extension;
// final File photoFile = new File(outputDirectory, fileName);
final String mimeType = "image/jpg";
final DocumentFile photoFile = outputDirectory.createFile(mimeType, fileName);
if (photoFile == null) {
Log.e(TAG, "takePhoto: photoFile is null!");
return;
}
final OutputStream outputStream = getContentResolver().openOutputStream(photoFile.getUri());
if (outputStream == null) return;
final ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(outputStream).build();
imageCapture.takePicture(
outputFileOptions,
cameraExecutor,
new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull final ImageCapture.OutputFileResults outputFileResults) {
try { outputStream.close(); } catch (IOException ignored) {}
final Intent intent = new Intent();
intent.setData(photoFile.getUri());
setResult(Activity.RESULT_OK, intent);
finish();
Log.d(TAG, "onImageSaved: " + photoFile.getUri());
}
@Override
public void onError(@NonNull final ImageCaptureException exception) {
Log.e(TAG, "onError: ", exception);
try { outputStream.close(); } catch (IOException ignored) {}
}
}
);
// We can only change the foreground Drawable using API level 23+ API
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// // Display flash animation to indicate that photo was captured
// final ConstraintLayout container = binding.getRoot();
// container.postDelayed(() -> {
// container.setForeground(new ColorDrawable(Color.WHITE));
// container.postDelayed(() -> container.setForeground(null), 50);
// }, 100);
// }
}
/**
* Enabled or disabled a button to switch cameras depending on the available cameras
*/
private void updateCameraSwitchButton() {
try {
binding.switchCamera.setEnabled(hasBackCamera() && hasFrontCamera());
} catch (CameraInfoUnavailableException e) {
binding.switchCamera.setEnabled(false);
}
}
/**
* Returns true if the device has an available back camera. False otherwise
*/
private boolean hasBackCamera() throws CameraInfoUnavailableException {
if (cameraProvider == null) return false;
return cameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA);
}
/**
* Returns true if the device has an available front camera. False otherwise
*/
private boolean hasFrontCamera() throws CameraInfoUnavailableException {
if (cameraProvider == null) {
return false;
}
return cameraProvider.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA);
}
}

View File

@ -0,0 +1,248 @@
package awais.instagrabber.activities
import android.content.Intent
import android.content.res.Configuration
import android.hardware.display.DisplayManager
import android.hardware.display.DisplayManager.DisplayListener
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.webkit.MimeTypeMap
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import awais.instagrabber.databinding.ActivityCameraBinding
import awais.instagrabber.utils.DirectoryUtils
import awais.instagrabber.utils.PermissionUtils
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG
import com.google.common.io.Files
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class CameraActivity : BaseLanguageActivity() {
private lateinit var binding: ActivityCameraBinding
private lateinit var outputDirectory: File
private lateinit var displayManager: DisplayManager
private lateinit var cameraExecutor: ExecutorService
private var imageCapture: ImageCapture? = null
private var displayId = -1
private var cameraProvider: ProcessCameraProvider? = null
private var lensFacing = 0
private val cameraRequestCode = 100
private val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
private val displayListener: DisplayListener = object : DisplayListener {
override fun onDisplayAdded(displayId: Int) {}
override fun onDisplayRemoved(displayId: Int) {}
override fun onDisplayChanged(displayId: Int) {
if (displayId == this@CameraActivity.displayId) {
imageCapture?.targetRotation = binding.root.display.rotation
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCameraBinding.inflate(LayoutInflater.from(baseContext))
setContentView(binding.root)
Utils.transparentStatusBar(this, true, false)
displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager
outputDirectory = DirectoryUtils.getOutputMediaDirectory(this, "Camera")
cameraExecutor = Executors.newSingleThreadExecutor()
displayManager.registerDisplayListener(displayListener, null)
binding.viewFinder.post {
displayId = binding.viewFinder.display.displayId
updateUi()
checkPermissionsAndSetupCamera()
}
}
override fun onResume() {
super.onResume()
// Make sure that all permissions are still present, since the
// user could have removed them while the app was in paused state.
if (!PermissionUtils.hasCameraPerms(this)) {
PermissionUtils.requestCameraPerms(this, cameraRequestCode)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// Redraw the camera UI controls
updateUi()
// Enable or disable switching between cameras
updateCameraSwitchButton()
}
override fun onDestroy() {
super.onDestroy()
Utils.transparentStatusBar(this, false, false)
cameraExecutor.shutdown()
displayManager.unregisterDisplayListener(displayListener)
}
private fun updateUi() {
binding.cameraCaptureButton.setOnClickListener { takePhoto() }
// Disable the button until the camera is set up
binding.switchCamera.isEnabled = false
// Listener for button used to switch cameras. Only called if the button is enabled
binding.switchCamera.setOnClickListener {
lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) CameraSelector.LENS_FACING_BACK else CameraSelector.LENS_FACING_FRONT
// Re-bind use cases to update selected camera
bindCameraUseCases()
}
binding.close.setOnClickListener {
setResult(RESULT_CANCELED)
finish()
}
}
private fun checkPermissionsAndSetupCamera() {
if (PermissionUtils.hasCameraPerms(this)) {
setupCamera()
return
}
PermissionUtils.requestCameraPerms(this, cameraRequestCode)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == cameraRequestCode) {
if (PermissionUtils.hasCameraPerms(this)) {
setupCamera()
}
}
}
private fun setupCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
try {
cameraProvider = cameraProviderFuture.get()
// Select lensFacing depending on the available cameras
lensFacing = -1
if (hasBackCamera()) {
lensFacing = CameraSelector.LENS_FACING_BACK
} else if (hasFrontCamera()) {
lensFacing = CameraSelector.LENS_FACING_FRONT
}
check(lensFacing != -1) { "Back and front camera are unavailable" }
// Enable or disable switching between cameras
updateCameraSwitchButton()
// Build and bind the camera use cases
bindCameraUseCases()
} catch (e: ExecutionException) {
Log.e(TAG, "setupCamera: ", e)
} catch (e: InterruptedException) {
Log.e(TAG, "setupCamera: ", e)
} catch (e: CameraInfoUnavailableException) {
Log.e(TAG, "setupCamera: ", e)
}
}, ContextCompat.getMainExecutor(this))
}
private fun bindCameraUseCases() {
val rotation = binding.viewFinder.display.rotation
// CameraSelector
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build()
// Preview
val preview = Preview.Builder() // Set initial target rotation
.setTargetRotation(rotation)
.build()
// ImageCapture
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
.setTargetRotation(rotation)
.build()
cameraProvider?.unbindAll()
cameraProvider?.bindToLifecycle(this, cameraSelector, preview, imageCapture)
preview.setSurfaceProvider(binding.viewFinder.surfaceProvider)
}
private fun takePhoto() {
if (imageCapture == null) return
val photoFile = File(outputDirectory, simpleDateFormat.format(System.currentTimeMillis()) + ".jpg")
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
imageCapture?.takePicture(
outputFileOptions,
cameraExecutor,
object : ImageCapture.OnImageSavedCallback {
@Suppress("UnstableApiUsage")
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val uri = Uri.fromFile(photoFile)
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(Files.getFileExtension(photoFile.name))
MediaScannerConnection.scanFile(
this@CameraActivity,
arrayOf(photoFile.absolutePath),
arrayOf(mimeType)
) { _: String?, uri1: Uri? ->
Log.d(TAG, "onImageSaved: scan complete")
val intent = Intent()
intent.data = uri1
setResult(RESULT_OK, intent)
finish()
}
Log.d(TAG, "onImageSaved: $uri")
}
override fun onError(exception: ImageCaptureException) {
Log.e(TAG, "onError: ", exception)
}
}
)
// We can only change the foreground Drawable using API level 23+ API
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// // Display flash animation to indicate that photo was captured
// final ConstraintLayout container = binding.getRoot();
// container.postDelayed(() -> {
// container.setForeground(new ColorDrawable(Color.WHITE));
// container.postDelayed(() -> container.setForeground(null), 50);
// }, 100);
// }
}
/**
* Enabled or disabled a button to switch cameras depending on the available cameras
*/
private fun updateCameraSwitchButton() {
try {
binding.switchCamera.isEnabled = hasBackCamera() && hasFrontCamera()
} catch (e: CameraInfoUnavailableException) {
binding.switchCamera.isEnabled = false
}
}
/**
* Returns true if the device has an available back camera. False otherwise
*/
@Throws(CameraInfoUnavailableException::class)
private fun hasBackCamera(): Boolean {
return if (cameraProvider == null) false else cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
}
/**
* Returns true if the device has an available front camera. False otherwise
*/
@Throws(CameraInfoUnavailableException::class)
private fun hasFrontCamera(): Boolean {
return if (cameraProvider == null) {
false
} else cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
}
}

View File

@ -1,149 +0,0 @@
package awais.instagrabber.activities;
import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import awais.instagrabber.R;
import awais.instagrabber.asyncs.PostFetcher;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.IntentModel;
import awais.instagrabber.models.enums.IntentModelType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.IntentUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
public final class DirectDownload extends AppCompatActivity {
private static final int NOTIFICATION_ID = 1900000000;
private static final int STORAGE_PERM_REQUEST_CODE = 8020;
private boolean contextFound = false;
private Intent intent;
private Context context;
private NotificationManagerCompat notificationManager;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_direct);
}
@Override
public void onWindowAttributesChanged(final WindowManager.LayoutParams params) {
super.onWindowAttributesChanged(params);
if (!contextFound) {
intent = getIntent();
context = getApplicationContext();
if (intent != null && context != null) {
contextFound = true;
checkPermissions();
}
}
}
@Override
public Resources getResources() {
if (!contextFound) {
intent = getIntent();
context = getApplicationContext();
if (intent != null && context != null) {
contextFound = true;
checkPermissions();
}
}
return super.getResources();
}
private synchronized void checkPermissions() {
// if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
doDownload();
// return;
// }
// ActivityCompat.requestPermissions(this, DownloadUtils.PERMS, STORAGE_PERM_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
final boolean granted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (requestCode == STORAGE_PERM_REQUEST_CODE && granted) {
doDownload();
}
}
private synchronized void doDownload() {
CookieUtils.setupCookies(Utils.settingsHelper.getString(Constants.COOKIE));
notificationManager = NotificationManagerCompat.from(getApplicationContext());
final Intent intent = getIntent();
final String action = intent.getAction();
if (TextUtils.isEmpty(action) || Intent.ACTION_MAIN.equals(action)) {
finish();
return;
}
boolean error = true;
String data = null;
final Bundle extras = intent.getExtras();
if (extras != null) {
final Object extraData = extras.get(Intent.EXTRA_TEXT);
if (extraData != null) {
error = false;
data = extraData.toString();
}
}
if (error) {
final Uri intentData = intent.getData();
if (intentData != null) data = intentData.toString();
}
if (data == null || TextUtils.isEmpty(data)) {
finish();
return;
}
final IntentModel model = IntentUtils.parseUrl(data);
if (model == null || model.getType() != IntentModelType.POST) {
finish();
return;
}
final String text = model.getText();
new PostFetcher(text, new FetchListener<Media>() {
@Override
public void doBefore() {
if (notificationManager == null) return;
final Notification fetchingPostNotification = new NotificationCompat.Builder(getApplicationContext(), Constants.DOWNLOAD_CHANNEL_ID)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setSmallIcon(R.drawable.ic_download)
.setAutoCancel(false)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setContentText(getString(R.string.direct_download_loading))
.build();
notificationManager.notify(NOTIFICATION_ID, fetchingPostNotification);
}
@Override
public void onResult(final Media result) {
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
if (result == null) {
finish();
return;
}
DownloadUtils.download(getApplicationContext(), result);
finish();
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}

View File

@ -1,140 +0,0 @@
package awais.instagrabber.activities;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.annotation.Nullable;
import awais.instagrabber.R;
import awais.instagrabber.databinding.ActivityLoginBinding;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.TextUtils;
public final class Login extends BaseLanguageActivity implements View.OnClickListener {
private final WebViewClient webViewClient = new WebViewClient() {
@Override
public void onPageStarted(final WebView view, final String url, final Bitmap favicon) {
webViewUrl = url;
}
@Override
public void onPageFinished(final WebView view, final String url) {
webViewUrl = url;
final String mainCookie = CookieUtils.getCookie(url);
if (TextUtils.isEmpty(mainCookie) || !mainCookie.contains("; ds_user_id=")) {
ready = true;
return;
}
if (mainCookie.contains("; ds_user_id=") && ready) {
returnCookieResult(mainCookie);
}
}
};
private void returnCookieResult(final String mainCookie) {
final Intent intent = new Intent();
intent.putExtra("cookie", mainCookie);
setResult(Constants.LOGIN_RESULT_CODE, intent);
finish();
}
private final WebChromeClient webChromeClient = new WebChromeClient();
private String webViewUrl;
private boolean ready = false;
private ActivityLoginBinding loginBinding;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loginBinding = ActivityLoginBinding.inflate(LayoutInflater.from(getApplicationContext()));
setContentView(loginBinding.getRoot());
initWebView();
loginBinding.cookies.setOnClickListener(this);
loginBinding.refresh.setOnClickListener(this);
}
@Override
public void onClick(final View v) {
if (v == loginBinding.refresh) {
loginBinding.webView.loadUrl("https://instagram.com/");
return;
}
if (v == loginBinding.cookies) {
final String mainCookie = CookieUtils.getCookie(webViewUrl);
if (TextUtils.isEmpty(mainCookie) || !mainCookie.contains("; ds_user_id=")) {
Toast.makeText(this, R.string.login_error_loading_cookies, Toast.LENGTH_SHORT).show();
return;
}
returnCookieResult(mainCookie);
}
}
@SuppressLint("SetJavaScriptEnabled")
private void initWebView() {
if (loginBinding != null) {
loginBinding.webView.setWebChromeClient(webChromeClient);
loginBinding.webView.setWebViewClient(webViewClient);
final WebSettings webSettings = loginBinding.webView.getSettings();
if (webSettings != null) {
webSettings.setUserAgentString("Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Mobile Safari/537.36");
webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setDisplayZoomControls(false);
webSettings.setLoadWithOverviewMode(true);
webSettings.setUseWideViewPort(true);
webSettings.setAllowFileAccessFromFileURLs(true);
webSettings.setAllowUniversalAccessFromFileURLs(true);
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
CookieManager.getInstance().removeAllCookies(null);
CookieManager.getInstance().flush();
} else {
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(getApplicationContext());
cookieSyncMngr.startSync();
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
cookieManager.removeSessionCookie();
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
}
loginBinding.webView.loadUrl("https://instagram.com/");
}
}
@Override
protected void onPause() {
if (loginBinding != null) loginBinding.webView.onPause();
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
if (loginBinding != null) loginBinding.webView.onResume();
}
@Override
protected void onDestroy() {
if (loginBinding != null) loginBinding.webView.destroy();
super.onDestroy();
}
}

View File

@ -0,0 +1,119 @@
package awais.instagrabber.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.webkit.*
import android.widget.Toast
import awais.instagrabber.R
import awais.instagrabber.databinding.ActivityLoginBinding
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.getCookie
class Login : BaseLanguageActivity(), View.OnClickListener {
private var webViewUrl: String? = null
private var ready = false
private lateinit var loginBinding: ActivityLoginBinding
private val webChromeClient = WebChromeClient()
private val webViewClient: WebViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
webViewUrl = url
}
override fun onPageFinished(view: WebView, url: String) {
webViewUrl = url
val mainCookie = getCookie(url)
if (mainCookie.isNullOrBlank() || !mainCookie.contains("; ds_user_id=")) {
ready = true
return
}
if (mainCookie.contains("; ds_user_id=") && ready) {
returnCookieResult(mainCookie)
}
}
}
private fun returnCookieResult(mainCookie: String?) {
val intent = Intent()
intent.putExtra("cookie", mainCookie)
setResult(Constants.LOGIN_RESULT_CODE, intent)
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loginBinding = ActivityLoginBinding.inflate(LayoutInflater.from(applicationContext))
setContentView(loginBinding.root)
initWebView()
loginBinding.cookies.setOnClickListener(this)
loginBinding.refresh.setOnClickListener(this)
}
override fun onClick(v: View) {
if (v === loginBinding.refresh) {
loginBinding.webView.loadUrl("https://instagram.com/")
return
}
if (v === loginBinding.cookies) {
val mainCookie = getCookie(webViewUrl)
if (mainCookie.isNullOrBlank() || !mainCookie.contains("; ds_user_id=")) {
Toast.makeText(this, R.string.login_error_loading_cookies, Toast.LENGTH_SHORT).show()
return
}
returnCookieResult(mainCookie)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
loginBinding.webView.webChromeClient = webChromeClient
loginBinding.webView.webViewClient = webViewClient
val webSettings = loginBinding.webView.settings
webSettings.userAgentString =
"Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Mobile Safari/537.36"
webSettings.javaScriptEnabled = true
webSettings.domStorageEnabled = true
webSettings.setSupportZoom(true)
webSettings.builtInZoomControls = true
webSettings.displayZoomControls = false
webSettings.loadWithOverviewMode = true
webSettings.useWideViewPort = true
webSettings.allowFileAccessFromFileURLs = true
webSettings.allowUniversalAccessFromFileURLs = true
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
CookieManager.getInstance().removeAllCookies(null)
CookieManager.getInstance().flush()
} else {
val cookieSyncMngr = CookieSyncManager.createInstance(applicationContext)
cookieSyncMngr.startSync()
val cookieManager = CookieManager.getInstance()
cookieManager.removeAllCookie()
cookieManager.removeSessionCookie()
cookieSyncMngr.stopSync()
cookieSyncMngr.sync()
}
loginBinding.webView.loadUrl("https://instagram.com/")
}
override fun onPause() {
loginBinding.webView.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
loginBinding.webView.onResume()
}
override fun onDestroy() {
loginBinding.webView.destroy()
super.onDestroy()
}
}

View File

@ -1,973 +0,0 @@
package awais.instagrabber.activities;
import android.animation.LayoutTransition;
import android.annotation.SuppressLint;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.provider.BaseColumns;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.AutoCompleteTextView;
import android.widget.Toast;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.provider.FontRequest;
import androidx.emoji.text.EmojiCompat;
import androidx.emoji.text.FontRequestEmojiCompatConfig;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.NavDestination;
import androidx.navigation.ui.NavigationUI;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.google.android.material.badge.BadgeDrawable;
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.R;
import awais.instagrabber.adapters.SuggestionsAdapter;
import awais.instagrabber.asyncs.PostFetcher;
import awais.instagrabber.customviews.emoji.EmojiVariantManager;
import awais.instagrabber.databinding.ActivityMainBinding;
import awais.instagrabber.fragments.PostViewV2Fragment;
import awais.instagrabber.fragments.directmessages.DirectMessageInboxFragmentDirections;
import awais.instagrabber.fragments.main.FeedFragment;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.models.IntentModel;
import awais.instagrabber.models.Tab;
import awais.instagrabber.models.enums.SuggestionType;
import awais.instagrabber.repositories.responses.search.SearchItem;
import awais.instagrabber.repositories.responses.search.SearchResponse;
import awais.instagrabber.services.ActivityCheckerService;
import awais.instagrabber.services.DMSyncAlarmReceiver;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.FlavorTown;
import awais.instagrabber.utils.IntentUtils;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.utils.emoji.EmojiParser;
import awais.instagrabber.viewmodels.AppStateViewModel;
import awais.instagrabber.viewmodels.DirectInboxViewModel;
import awais.instagrabber.webservices.RetrofitFactory;
import awais.instagrabber.webservices.SearchService;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static awais.instagrabber.utils.Constants.EXTRA_INITIAL_URI;
import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class MainActivity extends BaseLanguageActivity implements FragmentManager.OnBackStackChangedListener {
private static final String TAG = "MainActivity";
private static final String FIRST_FRAGMENT_GRAPH_INDEX_KEY = "firstFragmentGraphIndex";
private static final String LAST_SELECT_NAV_MENU_ID = "lastSelectedNavMenuId";
private ActivityMainBinding binding;
private LiveData<NavController> currentNavControllerLiveData;
private MenuItem searchMenuItem;
private SuggestionsAdapter suggestionAdapter;
private AutoCompleteTextView searchAutoComplete;
private SearchView searchView;
private SearchService searchService;
private boolean showSearch = true;
private Handler suggestionsFetchHandler;
private int firstFragmentGraphIndex;
private int lastSelectedNavMenuId;
private boolean isActivityCheckerServiceBound = false;
private boolean isBackStackEmpty = false;
private boolean isLoggedIn;
private HideBottomViewOnScrollBehavior<BottomNavigationView> behavior;
private List<Tab> currentTabs;
private List<Integer> showBottomViewDestinations = Collections.emptyList();
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(final ComponentName name, final IBinder service) {
// final ActivityCheckerService.LocalBinder binder = (ActivityCheckerService.LocalBinder) service;
// final ActivityCheckerService activityCheckerService = binder.getService();
isActivityCheckerServiceBound = true;
}
@Override
public void onServiceDisconnected(final ComponentName name) {
isActivityCheckerServiceBound = false;
}
};
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
try {
DownloadUtils.init(this);
} catch (DownloadUtils.ReselectDocumentTreeException e) {
super.onCreate(savedInstanceState);
final Intent intent = new Intent(this, DirectorySelectActivity.class);
intent.putExtra(EXTRA_INITIAL_URI, e.getInitialUri());
startActivity(intent);
finish();
return;
}
RetrofitFactory.setup(this);
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setupCookie();
if (settingsHelper.getBoolean(Constants.FLAG_SECURE))
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
setContentView(binding.getRoot());
final Toolbar toolbar = binding.toolbar;
setSupportActionBar(toolbar);
createNotificationChannels();
try {
final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.bottomNavView.getLayoutParams();
//noinspection unchecked
behavior = (HideBottomViewOnScrollBehavior<BottomNavigationView>) layoutParams.getBehavior();
} catch (Exception e) {
Log.e(TAG, "onCreate: ", e);
}
if (savedInstanceState == null) {
setupBottomNavigationBar(true);
}
setupSuggestions();
if (!BuildConfig.isPre) {
final boolean checkUpdates = settingsHelper.getBoolean(Constants.CHECK_UPDATES);
if (checkUpdates) FlavorTown.updateCheck(this);
}
FlavorTown.changelogCheck(this);
new ViewModelProvider(this).get(AppStateViewModel.class); // Just initiate the App state here
final Intent intent = getIntent();
handleIntent(intent);
if (isLoggedIn && settingsHelper.getBoolean(Constants.CHECK_ACTIVITY)) {
bindActivityCheckerService();
}
getSupportFragmentManager().addOnBackStackChangedListener(this);
// Initialise the internal map
AppExecutors.getInstance().tasksThread().execute(() -> {
EmojiParser.setup(this);
EmojiVariantManager.getInstance();
});
initEmojiCompat();
searchService = SearchService.getInstance();
// initDmService();
initDmUnreadCount();
}
private void setupCookie() {
final String cookie = settingsHelper.getString(Constants.COOKIE);
long userId = 0;
String csrfToken = null;
if (!TextUtils.isEmpty(cookie)) {
userId = CookieUtils.getUserIdFromCookie(cookie);
csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie);
}
if (TextUtils.isEmpty(cookie) || userId == 0 || TextUtils.isEmpty(csrfToken)) {
isLoggedIn = false;
return;
}
final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID);
if (TextUtils.isEmpty(deviceUuid)) {
settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString());
}
CookieUtils.setupCookies(cookie);
isLoggedIn = true;
}
private void initDmService() {
if (!isLoggedIn) return;
final boolean enabled = settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH);
if (!enabled) return;
DMSyncAlarmReceiver.setAlarm(this);
}
private void initDmUnreadCount() {
if (!isLoggedIn) return;
final DirectInboxViewModel directInboxViewModel = new ViewModelProvider(this).get(DirectInboxViewModel.class);
directInboxViewModel.getUnseenCount().observe(this, unseenCountResource -> {
if (unseenCountResource == null) return;
final Integer unseenCount = unseenCountResource.data;
setNavBarDMUnreadCountBadge(unseenCount == null ? 0 : unseenCount);
});
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
searchMenuItem = menu.findItem(R.id.search);
if (showSearch && currentNavControllerLiveData != null) {
final NavController navController = currentNavControllerLiveData.getValue();
if (navController != null) {
final NavDestination currentDestination = navController.getCurrentDestination();
if (currentDestination != null) {
final int destinationId = currentDestination.getId();
showSearch = destinationId == R.id.profileFragment;
}
}
}
if (!showSearch) {
searchMenuItem.setVisible(false);
return true;
}
return setupSearchView();
}
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
outState.putString(FIRST_FRAGMENT_GRAPH_INDEX_KEY, String.valueOf(firstFragmentGraphIndex));
if (binding != null) {
outState.putString(LAST_SELECT_NAV_MENU_ID, String.valueOf(binding.bottomNavView.getSelectedItemId()));
}
super.onSaveInstanceState(outState);
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
final String key = (String) savedInstanceState.get(FIRST_FRAGMENT_GRAPH_INDEX_KEY);
if (key != null) {
try {
firstFragmentGraphIndex = Integer.parseInt(key);
} catch (NumberFormatException ignored) { }
}
final String lastSelected = (String) savedInstanceState.get(LAST_SELECT_NAV_MENU_ID);
if (lastSelected != null) {
try {
lastSelectedNavMenuId = Integer.parseInt(lastSelected);
} catch (NumberFormatException ignored) { }
}
setupBottomNavigationBar(false);
}
@Override
public boolean onSupportNavigateUp() {
if (currentNavControllerLiveData == null) return false;
final NavController navController = currentNavControllerLiveData.getValue();
if (navController == null) return false;
return navController.navigateUp();
}
@Override
protected void onNewIntent(final Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
@Override
protected void onDestroy() {
try {
super.onDestroy();
} catch (Exception e) {
Log.e(TAG, "onDestroy: ", e);
}
unbindActivityCheckerService();
try {
RetrofitFactory.getInstance().destroy();
} catch (Exception ignored) {}
DownloadUtils.destroy();
}
@Override
public void onBackPressed() {
int currentNavControllerBackStack = 2;
if (currentNavControllerLiveData != null) {
final NavController navController = currentNavControllerLiveData.getValue();
if (navController != null) {
@SuppressLint("RestrictedApi") final Deque<NavBackStackEntry> backStack = navController.getBackStack();
currentNavControllerBackStack = backStack.size();
}
}
if (isTaskRoot() && isBackStackEmpty && currentNavControllerBackStack == 2) {
finishAfterTransition();
return;
}
if (!isFinishing()) {
try {
super.onBackPressed();
} catch (Exception e) {
Log.e(TAG, "onBackPressed: ", e);
finish();
}
}
}
@Override
public void onBackStackChanged() {
final int backStackEntryCount = getSupportFragmentManager().getBackStackEntryCount();
isBackStackEmpty = backStackEntryCount == 0;
}
private void createNotificationChannels() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext());
notificationManager.createNotificationChannel(new NotificationChannel(Constants.DOWNLOAD_CHANNEL_ID,
Constants.DOWNLOAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT));
notificationManager.createNotificationChannel(new NotificationChannel(Constants.ACTIVITY_CHANNEL_ID,
Constants.ACTIVITY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT));
notificationManager.createNotificationChannel(new NotificationChannel(Constants.DM_UNREAD_CHANNEL_ID,
Constants.DM_UNREAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT));
final NotificationChannel silentNotificationChannel = new NotificationChannel(Constants.SILENT_NOTIFICATIONS_CHANNEL_ID,
Constants.SILENT_NOTIFICATIONS_CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW);
silentNotificationChannel.setSound(null, null);
notificationManager.createNotificationChannel(silentNotificationChannel);
}
private void setupSuggestions() {
suggestionsFetchHandler = new Handler();
suggestionAdapter = new SuggestionsAdapter(this, (type, query) -> {
if (searchMenuItem != null) searchMenuItem.collapseActionView();
if (searchView != null && !searchView.isIconified()) searchView.setIconified(true);
if (currentNavControllerLiveData == null) return;
final NavController navController = currentNavControllerLiveData.getValue();
if (navController == null) return;
final Bundle bundle = new Bundle();
switch (type) {
case TYPE_LOCATION:
bundle.putLong("locationId", Long.parseLong(query));
navController.navigate(R.id.action_global_locationFragment, bundle);
break;
case TYPE_HASHTAG:
bundle.putString("hashtag", query);
navController.navigate(R.id.action_global_hashTagFragment, bundle);
break;
case TYPE_USER:
bundle.putString("username", query);
navController.navigate(R.id.action_global_profileFragment, bundle);
break;
}
});
}
private boolean setupSearchView() {
final View actionView = searchMenuItem.getActionView();
if (!(actionView instanceof SearchView)) return false;
searchView = (SearchView) actionView;
searchView.setSuggestionsAdapter(suggestionAdapter);
searchView.setMaxWidth(Integer.MAX_VALUE);
final View searchText = searchView.findViewById(R.id.search_src_text);
if (searchText instanceof AutoCompleteTextView) {
searchAutoComplete = (AutoCompleteTextView) searchText;
}
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
private boolean searchUser;
private boolean searchHash;
private Call<SearchResponse> prevSuggestionAsync;
private final String[] COLUMNS = {
BaseColumns._ID,
Constants.EXTRAS_USERNAME,
Constants.EXTRAS_NAME,
Constants.EXTRAS_TYPE,
"query",
"pfp",
"verified"
};
private String currentSearchQuery;
private final Callback<SearchResponse> cb = new Callback<SearchResponse>() {
@Override
public void onResponse(@NonNull final Call<SearchResponse> call,
@NonNull final Response<SearchResponse> response) {
final MatrixCursor cursor;
final SearchResponse body = response.body();
if (body == null) {
cursor = null;
return;
}
final List<SearchItem> result = new ArrayList<>();
if (isLoggedIn) {
if (body.getList() != null) {
result.addAll(searchHash ? body.getList()
.stream()
.filter(i -> i.getUser() == null)
.collect(Collectors.toList())
: body.getList());
}
} else {
if (body.getUsers() != null && !searchHash) result.addAll(body.getUsers());
if (body.getHashtags() != null) result.addAll(body.getHashtags());
if (body.getPlaces() != null) result.addAll(body.getPlaces());
}
cursor = new MatrixCursor(COLUMNS, 0);
for (int i = 0; i < result.size(); i++) {
final SearchItem suggestionModel = result.get(i);
if (suggestionModel != null) {
Object[] objects = null;
if (suggestionModel.getUser() != null)
objects = new Object[]{
suggestionModel.getPosition(),
suggestionModel.getUser().getUsername(),
suggestionModel.getUser().getFullName(),
SuggestionType.TYPE_USER,
suggestionModel.getUser().getUsername(),
suggestionModel.getUser().getProfilePicUrl(),
suggestionModel.getUser().isVerified()};
else if (suggestionModel.getHashtag() != null)
objects = new Object[]{
suggestionModel.getPosition(),
suggestionModel.getHashtag().getName(),
suggestionModel.getHashtag().getSubtitle(),
SuggestionType.TYPE_HASHTAG,
suggestionModel.getHashtag().getName(),
"res:/" + R.drawable.ic_hashtag,
false};
else if (suggestionModel.getPlace() != null)
objects = new Object[]{
suggestionModel.getPosition(),
suggestionModel.getPlace().getTitle(),
suggestionModel.getPlace().getSubtitle(),
SuggestionType.TYPE_LOCATION,
suggestionModel.getPlace().getLocation().getPk(),
"res:/" + R.drawable.ic_location,
false};
cursor.addRow(objects);
}
}
suggestionAdapter.changeCursor(cursor);
}
@Override
public void onFailure(@NonNull final Call<SearchResponse> call,
@NonNull Throwable t) {
if (!call.isCanceled()) {
Log.e(TAG, "Exception on search:", t);
}
}
};
private final Runnable runnable = () -> {
cancelSuggestionsAsync();
if (TextUtils.isEmpty(currentSearchQuery)) {
suggestionAdapter.changeCursor(null);
return;
}
searchUser = currentSearchQuery.charAt(0) == '@';
searchHash = currentSearchQuery.charAt(0) == '#';
if (currentSearchQuery.length() == 1 && (searchHash || searchUser)) {
if (searchAutoComplete != null) {
searchAutoComplete.setThreshold(2);
}
} else {
if (searchAutoComplete != null) {
searchAutoComplete.setThreshold(1);
}
prevSuggestionAsync = searchService.search(isLoggedIn,
searchUser || searchHash ? currentSearchQuery.substring(1)
: currentSearchQuery,
searchUser ? "user" : (searchHash ? "hashtag" : "blended"));
suggestionAdapter.changeCursor(null);
prevSuggestionAsync.enqueue(cb);
}
};
private void cancelSuggestionsAsync() {
if (prevSuggestionAsync != null)
try {
prevSuggestionAsync.cancel();
} catch (final Exception ignored) {}
}
@Override
public boolean onQueryTextSubmit(final String query) {
return onQueryTextChange(query);
}
@Override
public boolean onQueryTextChange(final String query) {
suggestionsFetchHandler.removeCallbacks(runnable);
currentSearchQuery = query;
suggestionsFetchHandler.postDelayed(runnable, 800);
return true;
}
});
return true;
}
private void setupBottomNavigationBar(final boolean setDefaultTabFromSettings) {
currentTabs = !isLoggedIn ? setupAnonBottomNav() : setupMainBottomNav();
final List<Integer> mainNavList = currentTabs.stream()
.map(Tab::getNavigationResId)
.collect(Collectors.toList());
showBottomViewDestinations = currentTabs.stream()
.map(Tab::getStartDestinationFragmentId)
.collect(Collectors.toList());
if (setDefaultTabFromSettings) {
setSelectedTab(currentTabs);
} else {
binding.bottomNavView.setSelectedItemId(lastSelectedNavMenuId);
}
final LiveData<NavController> navControllerLiveData = setupWithNavController(
binding.bottomNavView,
mainNavList,
getSupportFragmentManager(),
R.id.main_nav_host,
getIntent(),
firstFragmentGraphIndex);
navControllerLiveData.observe(this, navController -> setupNavigation(binding.toolbar, navController));
currentNavControllerLiveData = navControllerLiveData;
binding.bottomNavView.setOnNavigationItemReselectedListener(item -> {
// Log.d(TAG, "setupBottomNavigationBar: item: " + item);
final Fragment navHostFragment = getSupportFragmentManager().findFragmentById(R.id.main_nav_host);
if (navHostFragment != null) {
final Fragment fragment = navHostFragment.getChildFragmentManager().getPrimaryNavigationFragment();
if (fragment instanceof FeedFragment) {
((FeedFragment) fragment).scrollToTop();
}
}
});
}
private void setSelectedTab(final List<Tab> tabs) {
final String defaultTabResNameString = settingsHelper.getString(Constants.DEFAULT_TAB);
try {
int navId = 0;
if (!TextUtils.isEmpty(defaultTabResNameString)) {
navId = getResources().getIdentifier(defaultTabResNameString, "navigation", getPackageName());
}
final int navGraph = isLoggedIn ? R.navigation.feed_nav_graph
: R.navigation.profile_nav_graph;
final int defaultNavId = navId <= 0 ? navGraph : navId;
int index = Iterators.indexOf(tabs.iterator(), tab -> {
if (tab == null) return false;
return tab.getNavigationResId() == defaultNavId;
});
if (index < 0 || index >= tabs.size()) index = 0;
firstFragmentGraphIndex = index;
setBottomNavSelectedTab(tabs.get(index));
} catch (Exception e) {
Log.e(TAG, "Error parsing id", e);
}
}
private List<Tab> setupAnonBottomNav() {
final int selectedItemId = binding.bottomNavView.getSelectedItemId();
final Tab profileTab = new Tab(R.drawable.ic_person_24,
getString(R.string.profile),
false,
"profile_nav_graph",
R.navigation.profile_nav_graph,
R.id.profile_nav_graph,
R.id.profileFragment);
final Tab moreTab = new Tab(R.drawable.ic_more_horiz_24,
getString(R.string.more),
false,
"more_nav_graph",
R.navigation.more_nav_graph,
R.id.more_nav_graph,
R.id.morePreferencesFragment);
final Menu menu = binding.bottomNavView.getMenu();
menu.clear();
menu.add(0, profileTab.getNavigationRootId(), 0, profileTab.getTitle()).setIcon(profileTab.getIconResId());
menu.add(0, moreTab.getNavigationRootId(), 0, moreTab.getTitle()).setIcon(moreTab.getIconResId());
if (selectedItemId != R.id.profile_nav_graph && selectedItemId != R.id.more_nav_graph) {
setBottomNavSelectedTab(profileTab);
}
return ImmutableList.of(profileTab, moreTab);
}
private List<Tab> setupMainBottomNav() {
final Menu menu = binding.bottomNavView.getMenu();
menu.clear();
final List<Tab> navTabList = Utils.getNavTabList(this).first;
for (final Tab tab : navTabList) {
menu.add(0, tab.getNavigationRootId(), 0, tab.getTitle()).setIcon(tab.getIconResId());
}
return navTabList;
}
private void setBottomNavSelectedTab(@NonNull final Tab tab) {
binding.bottomNavView.setSelectedItemId(tab.getNavigationRootId());
}
private void setBottomNavSelectedTab(@SuppressWarnings("SameParameterValue") @IdRes final int navGraphRootId) {
binding.bottomNavView.setSelectedItemId(navGraphRootId);
}
// @NonNull
// private List<Integer> getMainNavList(final int main_nav_ids) {
// final TypedArray navIds = getResources().obtainTypedArray(main_nav_ids);
// final List<Integer> mainNavList = new ArrayList<>(navIds.length());
// final int length = navIds.length();
// for (int i = 0; i < length; i++) {
// final int resourceId = navIds.getResourceId(i, -1);
// if (resourceId < 0) continue;
// mainNavList.add(resourceId);
// }
// navIds.recycle();
// return mainNavList;
// }
private void setupNavigation(final Toolbar toolbar, final NavController navController) {
if (navController == null) return;
NavigationUI.setupWithNavController(toolbar, navController);
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
if (destination.getId() == R.id.directMessagesThreadFragment && arguments != null) {
// Set the thread title earlier for better ux
final String title = arguments.getString("title");
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null && !TextUtils.isEmpty(title)) {
actionBar.setTitle(title);
}
}
// below is a hack to check if we are at the end of the current stack, to setup the search view
binding.appBarLayout.setExpanded(true, true);
final int destinationId = destination.getId();
@SuppressLint("RestrictedApi") final Deque<NavBackStackEntry> backStack = navController.getBackStack();
setupMenu(backStack.size(), destinationId);
final boolean contains = showBottomViewDestinations.contains(destinationId);
binding.bottomNavView.setVisibility(contains ? View.VISIBLE : View.GONE);
if (contains && behavior != null) {
behavior.slideUp(binding.bottomNavView);
}
// explicitly hide keyboard when we navigate
final View view = getCurrentFocus();
Utils.hideKeyboard(view);
});
}
private void setupMenu(final int backStackSize, final int destinationId) {
if (searchMenuItem == null) return;
if (backStackSize >= 2 && destinationId == R.id.profileFragment) {
showSearch = true;
searchMenuItem.setVisible(true);
return;
}
showSearch = false;
searchMenuItem.setVisible(false);
}
private void setScrollingBehaviour() {
final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.mainNavHost.getLayoutParams();
layoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior());
binding.mainNavHost.requestLayout();
}
private void removeScrollingBehaviour() {
final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.mainNavHost.getLayoutParams();
layoutParams.setBehavior(null);
binding.mainNavHost.requestLayout();
}
private void handleIntent(final Intent intent) {
if (intent == null) return;
final String action = intent.getAction();
final String type = intent.getType();
// Log.d(TAG, action + " " + type);
if (Intent.ACTION_MAIN.equals(action)) return;
if (Constants.ACTION_SHOW_ACTIVITY.equals(action)) {
showActivityView();
return;
}
if (Constants.ACTION_SHOW_DM_THREAD.equals(action)) {
showThread(intent);
return;
}
if (Intent.ACTION_SEND.equals(action) && type != null) {
if (type.equals("text/plain")) {
handleUrl(intent.getStringExtra(Intent.EXTRA_TEXT));
}
return;
}
if (Intent.ACTION_VIEW.equals(action)) {
final Uri data = intent.getData();
if (data == null) return;
handleUrl(data.toString());
}
}
private void showThread(@NonNull final Intent intent) {
final String threadId = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_ID);
final String threadTitle = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_TITLE);
navigateToThread(threadId, threadTitle);
}
public void navigateToThread(final String threadId, final String threadTitle) {
if (threadId == null || threadTitle == null) return;
currentNavControllerLiveData.observe(this, new Observer<NavController>() {
@Override
public void onChanged(final NavController navController) {
if (navController == null) return;
if (navController.getGraph().getId() != R.id.direct_messages_nav_graph) return;
try {
final NavDestination currentDestination = navController.getCurrentDestination();
if (currentDestination != null && currentDestination.getId() == R.id.directMessagesInboxFragment) {
// if we are already on the inbox page, navigate to the thread
// need handler.post() to wait for the fragment manager to be ready to navigate
new Handler().post(() -> {
final DirectMessageInboxFragmentDirections.ActionInboxToThread action = DirectMessageInboxFragmentDirections
.actionInboxToThread(threadId, threadTitle);
navController.navigate(action);
});
return;
}
// add a destination change listener to navigate to thread once we are on the inbox page
navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() {
@Override
public void onDestinationChanged(@NonNull final NavController controller,
@NonNull final NavDestination destination,
@Nullable final Bundle arguments) {
if (destination.getId() == R.id.directMessagesInboxFragment) {
final DirectMessageInboxFragmentDirections.ActionInboxToThread action = DirectMessageInboxFragmentDirections
.actionInboxToThread(threadId, threadTitle);
controller.navigate(action);
controller.removeOnDestinationChangedListener(this);
}
}
});
// pop back stack until we reach the inbox page
navController.popBackStack(R.id.directMessagesInboxFragment, false);
} finally {
currentNavControllerLiveData.removeObserver(this);
}
}
});
final int selectedItemId = binding.bottomNavView.getSelectedItemId();
if (selectedItemId != R.navigation.direct_messages_nav_graph) {
setBottomNavSelectedTab(R.id.direct_messages_nav_graph);
}
}
private void handleUrl(final String url) {
if (url == null) return;
// Log.d(TAG, url);
final IntentModel intentModel = IntentUtils.parseUrl(url);
if (intentModel == null) return;
showView(intentModel);
}
private void showView(final IntentModel intentModel) {
switch (intentModel.getType()) {
case USERNAME:
showProfileView(intentModel);
break;
case POST:
showPostView(intentModel);
break;
case LOCATION:
showLocationView(intentModel);
break;
case HASHTAG:
showHashtagView(intentModel);
break;
case UNKNOWN:
default:
Log.w(TAG, "Unknown model type received!");
}
}
private void showProfileView(@NonNull final IntentModel intentModel) {
final String username = intentModel.getText();
// Log.d(TAG, "username: " + username);
if (currentNavControllerLiveData == null) return;
final NavController navController = currentNavControllerLiveData.getValue();
if (navController == null) return;
final Bundle bundle = new Bundle();
bundle.putString("username", "@" + username);
navController.navigate(R.id.action_global_profileFragment, bundle);
}
private void showPostView(@NonNull final IntentModel intentModel) {
final String shortCode = intentModel.getText();
// Log.d(TAG, "shortCode: " + shortCode);
final AlertDialog alertDialog = new AlertDialog.Builder(this)
.setCancelable(false)
.setView(R.layout.dialog_opening_post)
.create();
alertDialog.show();
new PostFetcher(shortCode, feedModel -> {
if (feedModel != null) {
final PostViewV2Fragment fragment = PostViewV2Fragment
.builder(feedModel)
.build();
fragment.setOnShowListener(dialog -> alertDialog.dismiss());
fragment.show(getSupportFragmentManager(), "post_view");
return;
}
Toast.makeText(getApplicationContext(), R.string.post_not_found, Toast.LENGTH_SHORT).show();
alertDialog.dismiss();
}).execute();
}
private void showLocationView(@NonNull final IntentModel intentModel) {
final String locationId = intentModel.getText();
// Log.d(TAG, "locationId: " + locationId);
if (currentNavControllerLiveData == null) return;
final NavController navController = currentNavControllerLiveData.getValue();
if (navController == null) return;
final Bundle bundle = new Bundle();
bundle.putLong("locationId", Long.parseLong(locationId));
navController.navigate(R.id.action_global_locationFragment, bundle);
}
private void showHashtagView(@NonNull final IntentModel intentModel) {
final String hashtag = intentModel.getText();
// Log.d(TAG, "hashtag: " + hashtag);
if (currentNavControllerLiveData == null) return;
final NavController navController = currentNavControllerLiveData.getValue();
if (navController == null) return;
final Bundle bundle = new Bundle();
bundle.putString("hashtag", hashtag);
navController.navigate(R.id.action_global_hashTagFragment, bundle);
}
private void showActivityView() {
if (currentNavControllerLiveData == null) return;
final NavController navController = currentNavControllerLiveData.getValue();
if (navController == null) return;
final Bundle bundle = new Bundle();
bundle.putString("type", "notif");
navController.navigate(R.id.action_global_notificationsViewerFragment, bundle);
}
private void bindActivityCheckerService() {
bindService(new Intent(this, ActivityCheckerService.class), serviceConnection, Context.BIND_AUTO_CREATE);
isActivityCheckerServiceBound = true;
}
private void unbindActivityCheckerService() {
if (!isActivityCheckerServiceBound) return;
unbindService(serviceConnection);
isActivityCheckerServiceBound = false;
}
@NonNull
public BottomNavigationView getBottomNavView() {
return binding.bottomNavView;
}
public void setCollapsingView(@NonNull final View view) {
binding.collapsingToolbarLayout.addView(view, 0);
}
public void removeCollapsingView(@NonNull final View view) {
binding.collapsingToolbarLayout.removeView(view);
}
public void setToolbar(final Toolbar toolbar) {
binding.appBarLayout.setVisibility(View.GONE);
removeScrollingBehaviour();
setSupportActionBar(toolbar);
if (currentNavControllerLiveData == null) return;
setupNavigation(toolbar, currentNavControllerLiveData.getValue());
}
public void resetToolbar() {
binding.appBarLayout.setVisibility(View.VISIBLE);
setScrollingBehaviour();
setSupportActionBar(binding.toolbar);
if (currentNavControllerLiveData == null) return;
setupNavigation(binding.toolbar, currentNavControllerLiveData.getValue());
}
public CollapsingToolbarLayout getCollapsingToolbarView() {
return binding.collapsingToolbarLayout;
}
public AppBarLayout getAppbarLayout() {
return binding.appBarLayout;
}
public void removeLayoutTransition() {
binding.getRoot().setLayoutTransition(null);
}
public void setLayoutTransition() {
binding.getRoot().setLayoutTransition(new LayoutTransition());
}
private void initEmojiCompat() {
// Use a downloadable font for EmojiCompat
final FontRequest fontRequest = new FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.com_google_android_gms_fonts_certs);
final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(getApplicationContext(), fontRequest);
config.setReplaceAll(true)
// .setUseEmojiAsDefaultStyle(true)
.registerInitCallback(new EmojiCompat.InitCallback() {
@Override
public void onInitialized() {
Log.i(TAG, "EmojiCompat initialized");
}
@Override
public void onFailed(@Nullable Throwable throwable) {
Log.e(TAG, "EmojiCompat initialization failed", throwable);
}
});
EmojiCompat.init(config);
}
public Toolbar getToolbar() {
return binding.toolbar;
}
public View getRootView() {
return binding.getRoot();
}
public List<Tab> getCurrentTabs() {
return currentTabs;
}
// public boolean isNavRootInCurrentTabs(@IdRes final int navRootId) {
// return showBottomViewDestinations.stream().anyMatch(id -> id == navRootId);
// }
private void setNavBarDMUnreadCountBadge(final int unseenCount) {
final BadgeDrawable badge = binding.bottomNavView.getOrCreateBadge(R.id.direct_messages_nav_graph);
if (badge == null) return;
if (unseenCount == 0) {
badge.setVisible(false);
badge.clearNumber();
return;
}
if (badge.getVerticalOffset() != 10) {
badge.setVerticalOffset(10);
}
badge.setNumber(unseenCount);
badge.setVisible(true);
}
}

View File

@ -0,0 +1,821 @@
package awais.instagrabber.activities
import android.animation.LayoutTransition
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.*
import android.text.Editable
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.NotificationManagerCompat
import androidx.core.provider.FontRequest
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.EmojiCompat.InitCallback
import androidx.emoji.text.FontRequestEmojiCompatConfig
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavController.OnDestinationChangedListener
import androidx.navigation.NavDestination
import androidx.navigation.ui.NavigationUI
import awais.instagrabber.BuildConfig
import awais.instagrabber.R
import awais.instagrabber.customviews.emoji.EmojiVariantManager
import awais.instagrabber.customviews.helpers.RootViewDeferringInsetsCallback
import awais.instagrabber.customviews.helpers.TextWatcherAdapter
import awais.instagrabber.databinding.ActivityMainBinding
import awais.instagrabber.fragments.PostViewV2Fragment
import awais.instagrabber.fragments.directmessages.DirectMessageInboxFragmentDirections
import awais.instagrabber.fragments.settings.PreferenceKeys
import awais.instagrabber.models.IntentModel
import awais.instagrabber.models.Resource
import awais.instagrabber.models.Tab
import awais.instagrabber.models.enums.IntentModelType
import awais.instagrabber.services.ActivityCheckerService
import awais.instagrabber.services.DMSyncAlarmReceiver
import awais.instagrabber.utils.*
import awais.instagrabber.utils.AppExecutors.tasksThread
import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.TextUtils.shortcodeToId
import awais.instagrabber.utils.emoji.EmojiParser
import awais.instagrabber.viewmodels.AppStateViewModel
import awais.instagrabber.viewmodels.DirectInboxViewModel
import awais.instagrabber.webservices.GraphQLRepository
import awais.instagrabber.webservices.MediaRepository
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.textfield.TextInputLayout
import com.google.common.collect.ImmutableList
import com.google.common.collect.Iterators
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import java.util.stream.Collectors
class MainActivity : BaseLanguageActivity(), FragmentManager.OnBackStackChangedListener {
private lateinit var binding: ActivityMainBinding
private var currentNavControllerLiveData: LiveData<NavController>? = null
private var searchMenuItem: MenuItem? = null
private var firstFragmentGraphIndex = 0
private var lastSelectedNavMenuId = 0
private var isActivityCheckerServiceBound = false
private var isBackStackEmpty = false
private var isLoggedIn = false
private var deviceUuid: String? = null
private var csrfToken: String? = null
private var userId: Long = 0
// private var behavior: HideBottomViewOnScrollBehavior<BottomNavigationView>? = null
var currentTabs: List<Tab> = emptyList()
private set
private var showBottomViewDestinations: List<Int> = emptyList()
private val serviceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
// final ActivityCheckerService.LocalBinder binder = (ActivityCheckerService.LocalBinder) service;
// final ActivityCheckerService activityCheckerService = binder.getService();
isActivityCheckerServiceBound = true
}
override fun onServiceDisconnected(name: ComponentName) {
isActivityCheckerServiceBound = false
}
}
private val mediaRepository: MediaRepository by lazy { MediaRepository.getInstance() }
private val graphQLRepository: GraphQLRepository by lazy { GraphQLRepository.getInstance() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
instance = this
binding = ActivityMainBinding.inflate(layoutInflater)
setupCookie()
if (Utils.settingsHelper.getBoolean(PreferenceKeys.FLAG_SECURE)) {
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
}
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
setupInsetsCallback()
createNotificationChannels()
// try {
// val layoutParams = binding.bottomNavView.layoutParams as CoordinatorLayout.LayoutParams
// @Suppress("UNCHECKED_CAST")
// behavior = layoutParams.behavior as HideBottomViewOnScrollBehavior<BottomNavigationView>
// } catch (e: Exception) {
// Log.e(TAG, "onCreate: ", e)
// }
if (savedInstanceState == null) {
setupBottomNavigationBar(true)
}
if (!BuildConfig.isPre) {
val checkUpdates = Utils.settingsHelper.getBoolean(PreferenceKeys.CHECK_UPDATES)
if (checkUpdates) FlavorTown.updateCheck(this)
}
FlavorTown.changelogCheck(this)
ViewModelProvider(this).get(AppStateViewModel::class.java) // Just initiate the App state here
handleIntent(intent)
if (isLoggedIn && Utils.settingsHelper.getBoolean(PreferenceKeys.CHECK_ACTIVITY)) {
bindActivityCheckerService()
}
supportFragmentManager.addOnBackStackChangedListener(this)
// Initialise the internal map
tasksThread.execute {
EmojiParser.getInstance(this)
EmojiVariantManager.getInstance()
}
initEmojiCompat()
// initDmService();
initDmUnreadCount()
initSearchInput()
}
private fun setupInsetsCallback() {
val deferringInsetsCallback = RootViewDeferringInsetsCallback(
WindowInsetsCompat.Type.systemBars(),
WindowInsetsCompat.Type.ime()
)
ViewCompat.setWindowInsetsAnimationCallback(binding.root, deferringInsetsCallback)
ViewCompat.setOnApplyWindowInsetsListener(binding.root, deferringInsetsCallback)
WindowCompat.setDecorFitsSystemWindows(window, false)
}
private fun setupCookie() {
val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
userId = 0
csrfToken = null
if (cookie.isNotBlank()) {
userId = getUserIdFromCookie(cookie)
csrfToken = getCsrfTokenFromCookie(cookie)
}
if (cookie.isBlank() || userId == 0L || csrfToken.isNullOrBlank()) {
isLoggedIn = false
return
}
deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
if (isEmpty(deviceUuid)) {
Utils.settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString())
}
setupCookies(cookie)
isLoggedIn = true
}
@Suppress("unused")
private fun initDmService() {
if (!isLoggedIn) return
val enabled = Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH)
if (!enabled) return
DMSyncAlarmReceiver.setAlarm(this)
}
private fun initDmUnreadCount() {
if (!isLoggedIn) return
val directInboxViewModel = ViewModelProvider(this).get(DirectInboxViewModel::class.java)
directInboxViewModel.unseenCount.observe(this, { unseenCountResource: Resource<Int?>? ->
if (unseenCountResource == null) return@observe
val unseenCount = unseenCountResource.data
setNavBarDMUnreadCountBadge(unseenCount ?: 0)
})
}
private fun initSearchInput() {
binding.searchInputLayout.setEndIconOnClickListener {
val editText = binding.searchInputLayout.editText ?: return@setEndIconOnClickListener
editText.setText("")
}
binding.searchInputLayout.addOnEditTextAttachedListener { textInputLayout: TextInputLayout ->
textInputLayout.isEndIconVisible = false
val editText = textInputLayout.editText ?: return@addOnEditTextAttachedListener
editText.addTextChangedListener(object : TextWatcherAdapter() {
override fun afterTextChanged(s: Editable) {
binding.searchInputLayout.isEndIconVisible = !isEmpty(s)
}
})
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
searchMenuItem = menu.findItem(R.id.search)
val navController = currentNavControllerLiveData?.value
if (navController != null) {
val currentDestination = navController.currentDestination
if (currentDestination != null) {
@SuppressLint("RestrictedApi") val backStack = navController.backStack
setupMenu(backStack.size, currentDestination.id)
}
}
// if (binding.searchInputLayout.getVisibility() == View.VISIBLE) {
// searchMenuItem.setVisible(false).setEnabled(false);
// return true;
// }
// searchMenuItem.setVisible(true).setEnabled(true);
// if (showSearch && currentNavControllerLiveData != null) {
// final NavController navController = currentNavControllerLiveData.getValue();
// if (navController != null) {
// final NavDestination currentDestination = navController.getCurrentDestination();
// if (currentDestination != null) {
// final int destinationId = currentDestination.getId();
// showSearch = destinationId == R.id.profileFragment;
// }
// }
// }
// if (!showSearch) {
// searchMenuItem.setVisible(false);
// return true;
// }
// return setupSearchView();
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.search) {
val navController = currentNavControllerLiveData?.value ?: return false
try {
navController.navigate(R.id.action_global_search)
return true
} catch (e: Exception) {
Log.e(TAG, "onOptionsItemSelected: ", e)
}
return false
}
return super.onOptionsItemSelected(item)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putString(FIRST_FRAGMENT_GRAPH_INDEX_KEY, firstFragmentGraphIndex.toString())
outState.putString(LAST_SELECT_NAV_MENU_ID, binding.bottomNavView.selectedItemId.toString())
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
val key = savedInstanceState[FIRST_FRAGMENT_GRAPH_INDEX_KEY] as String?
if (key != null) {
try {
firstFragmentGraphIndex = key.toInt()
} catch (ignored: NumberFormatException) {
}
}
val lastSelected = savedInstanceState[LAST_SELECT_NAV_MENU_ID] as String?
if (lastSelected != null) {
try {
lastSelectedNavMenuId = lastSelected.toInt()
} catch (ignored: NumberFormatException) {
}
}
setupBottomNavigationBar(false)
}
override fun onSupportNavigateUp(): Boolean {
if (currentNavControllerLiveData == null) return false
val navController = currentNavControllerLiveData?.value ?: return false
return navController.navigateUp()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
override fun onDestroy() {
try {
super.onDestroy()
} catch (e: Exception) {
Log.e(TAG, "onDestroy: ", e)
}
unbindActivityCheckerService()
// try {
// RetrofitFactory.getInstance().destroy()
// } catch (e: Exception) {
// Log.e(TAG, "onDestroy: ", e)
// }
instance = null
}
override fun onBackPressed() {
var currentNavControllerBackStack = 2
currentNavControllerLiveData?.let {
val navController = it.value
if (navController != null) {
@SuppressLint("RestrictedApi") val backStack = navController.backStack
currentNavControllerBackStack = backStack.size
}
}
if (isTaskRoot && isBackStackEmpty && currentNavControllerBackStack == 2) {
finishAfterTransition()
return
}
if (!isFinishing) {
try {
super.onBackPressed()
} catch (e: Exception) {
Log.e(TAG, "onBackPressed: ", e)
finish()
}
}
}
override fun onBackStackChanged() {
val backStackEntryCount = supportFragmentManager.backStackEntryCount
isBackStackEmpty = backStackEntryCount == 0
}
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val notificationManager = NotificationManagerCompat.from(applicationContext)
notificationManager.createNotificationChannel(NotificationChannel(
Constants.DOWNLOAD_CHANNEL_ID,
Constants.DOWNLOAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
))
notificationManager.createNotificationChannel(NotificationChannel(
Constants.ACTIVITY_CHANNEL_ID,
Constants.ACTIVITY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
))
notificationManager.createNotificationChannel(NotificationChannel(
Constants.DM_UNREAD_CHANNEL_ID,
Constants.DM_UNREAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
))
val silentNotificationChannel = NotificationChannel(
Constants.SILENT_NOTIFICATIONS_CHANNEL_ID,
Constants.SILENT_NOTIFICATIONS_CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
silentNotificationChannel.setSound(null, null)
notificationManager.createNotificationChannel(silentNotificationChannel)
}
private fun setupBottomNavigationBar(setDefaultTabFromSettings: Boolean) {
currentTabs = if (!isLoggedIn) setupAnonBottomNav() else setupMainBottomNav()
val mainNavList = currentTabs.stream()
.map(Tab::navigationResId)
.collect(Collectors.toList())
showBottomViewDestinations = currentTabs.asSequence().map {
it.startDestinationFragmentId
}.toMutableList().apply {
add(R.id.postViewFragment)
add(R.id.favoritesFragment)
}
if (setDefaultTabFromSettings) {
setSelectedTab(currentTabs)
} else {
binding.bottomNavView.selectedItemId = lastSelectedNavMenuId
}
val navControllerLiveData = NavigationExtensions.setupWithNavController(
binding.bottomNavView,
mainNavList,
supportFragmentManager,
R.id.main_nav_host,
intent,
firstFragmentGraphIndex)
navControllerLiveData.observe(this, { navController: NavController? -> setupNavigation(binding.toolbar, navController) })
currentNavControllerLiveData = navControllerLiveData
}
private fun setSelectedTab(tabs: List<Tab>) {
val defaultTabResNameString = Utils.settingsHelper.getString(Constants.DEFAULT_TAB)
try {
var navId = 0
if (!isEmpty(defaultTabResNameString)) {
navId = resources.getIdentifier(defaultTabResNameString, "navigation", packageName)
}
val navGraph = if (isLoggedIn) R.navigation.feed_nav_graph else R.navigation.profile_nav_graph
val defaultNavId = if (navId <= 0) navGraph else navId
var index = Iterators.indexOf(tabs.iterator()) { tab: Tab? ->
if (tab == null) return@indexOf false
tab.navigationResId == defaultNavId
}
if (index < 0 || index >= tabs.size) index = 0
firstFragmentGraphIndex = index
setBottomNavSelectedTab(tabs[index])
} catch (e: Exception) {
Log.e(TAG, "Error parsing id", e)
}
}
private fun setupAnonBottomNav(): List<Tab> {
val selectedItemId = binding.bottomNavView.selectedItemId
val favoriteTab = Tab(R.drawable.ic_star_24,
getString(R.string.title_favorites),
false,
"favorites_nav_graph",
R.navigation.favorites_nav_graph,
R.id.favorites_nav_graph,
R.id.favoritesFragment)
val profileTab = Tab(R.drawable.ic_person_24,
getString(R.string.profile),
false,
"profile_nav_graph",
R.navigation.profile_nav_graph,
R.id.profile_nav_graph,
R.id.profileFragment)
val moreTab = Tab(R.drawable.ic_more_horiz_24,
getString(R.string.more),
false,
"more_nav_graph",
R.navigation.more_nav_graph,
R.id.more_nav_graph,
R.id.morePreferencesFragment)
val menu = binding.bottomNavView.menu
menu.clear()
menu.add(0, favoriteTab.navigationRootId, 0, favoriteTab.title).setIcon(favoriteTab.iconResId)
menu.add(0, profileTab.navigationRootId, 0, profileTab.title).setIcon(profileTab.iconResId)
menu.add(0, moreTab.navigationRootId, 0, moreTab.title).setIcon(moreTab.iconResId)
if (selectedItemId != R.id.profile_nav_graph && selectedItemId != R.id.more_nav_graph && selectedItemId != R.id.favorites_nav_graph) {
setBottomNavSelectedTab(profileTab)
}
return ImmutableList.of(favoriteTab, profileTab, moreTab)
}
private fun setupMainBottomNav(): List<Tab> {
val menu = binding.bottomNavView.menu
menu.clear()
val navTabList = Utils.getNavTabList(this).first
for ((iconResId, title, _, _, _, navigationRootId) in navTabList) {
menu.add(0, navigationRootId, 0, title).setIcon(iconResId)
}
return navTabList
}
private fun setBottomNavSelectedTab(tab: Tab) {
binding.bottomNavView.selectedItemId = tab.navigationRootId
}
private fun setBottomNavSelectedTab(@IdRes navGraphRootId: Int) {
binding.bottomNavView.selectedItemId = navGraphRootId
}
private fun setupNavigation(toolbar: Toolbar, navController: NavController?) {
if (navController == null) return
NavigationUI.setupWithNavController(toolbar, navController)
navController.addOnDestinationChangedListener(OnDestinationChangedListener { _: NavController?, destination: NavDestination, arguments: Bundle? ->
if (destination.id == R.id.directMessagesThreadFragment && arguments != null) {
// Set the thread title earlier for better ux
val title = arguments.getString("title")
val actionBar = supportActionBar
if (actionBar != null && !isEmpty(title)) {
actionBar.title = title
}
}
// below is a hack to check if we are at the end of the current stack, to setup the search view
binding.appBarLayout.setExpanded(true, true)
val destinationId = destination.id
@SuppressLint("RestrictedApi") val backStack = navController.backStack
setupMenu(backStack.size, destinationId)
val contains = showBottomViewDestinations.contains(destinationId)
binding.root.post {
binding.bottomNavView.visibility = if (contains) View.VISIBLE else View.GONE
// if (contains) {
// behavior?.slideUp(binding.bottomNavView)
// }
}
// explicitly hide keyboard when we navigate
val view = currentFocus
Utils.hideKeyboard(view)
})
}
private fun setupMenu(backStackSize: Int, destinationId: Int) {
val searchMenuItem = searchMenuItem ?: return
if (backStackSize >= 2 && SEARCH_VISIBLE_DESTINATIONS.contains(destinationId)) {
searchMenuItem.isVisible = true
return
}
searchMenuItem.isVisible = false
}
private fun setScrollingBehaviour() {
val layoutParams = binding.mainNavHost.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.behavior = ScrollingViewBehavior()
binding.mainNavHost.requestLayout()
}
private fun removeScrollingBehaviour() {
val layoutParams = binding.mainNavHost.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.behavior = null
binding.mainNavHost.requestLayout()
}
private fun handleIntent(intent: Intent?) {
if (intent == null) return
val action = intent.action
val type = intent.type
// Log.d(TAG, action + " " + type);
if (Intent.ACTION_MAIN == action) return
if (Constants.ACTION_SHOW_ACTIVITY == action) {
showActivityView()
return
}
if (Constants.ACTION_SHOW_DM_THREAD == action) {
showThread(intent)
return
}
if (Intent.ACTION_SEND == action && type != null) {
if (type == "text/plain") {
handleUrl(intent.getStringExtra(Intent.EXTRA_TEXT))
}
return
}
if (Intent.ACTION_VIEW == action) {
val data = intent.data ?: return
handleUrl(data.toString())
}
}
private fun showThread(intent: Intent) {
val threadId = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_ID)
val threadTitle = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_TITLE)
navigateToThread(threadId, threadTitle)
}
fun navigateToThread(threadId: String?, threadTitle: String?) {
if (threadId == null || threadTitle == null) return
currentNavControllerLiveData?.observe(this, object : Observer<NavController?> {
override fun onChanged(navController: NavController?) {
if (navController == null) return
if (navController.graph.id != R.id.direct_messages_nav_graph) return
try {
val currentDestination = navController.currentDestination
if (currentDestination != null && currentDestination.id == R.id.directMessagesInboxFragment) {
// if we are already on the inbox page, navigate to the thread
// need handler.post() to wait for the fragment manager to be ready to navigate
Handler(Looper.getMainLooper()).post {
val action = DirectMessageInboxFragmentDirections
.actionInboxToThread(threadId, threadTitle)
navController.navigate(action)
}
return
}
// add a destination change listener to navigate to thread once we are on the inbox page
navController.addOnDestinationChangedListener(object : OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?,
) {
if (destination.id == R.id.directMessagesInboxFragment) {
val action = DirectMessageInboxFragmentDirections
.actionInboxToThread(threadId, threadTitle)
controller.navigate(action)
controller.removeOnDestinationChangedListener(this)
}
}
})
// pop back stack until we reach the inbox page
navController.popBackStack(R.id.directMessagesInboxFragment, false)
} finally {
currentNavControllerLiveData?.removeObserver(this)
}
}
})
val selectedItemId = binding.bottomNavView.selectedItemId
if (selectedItemId != R.navigation.direct_messages_nav_graph) {
setBottomNavSelectedTab(R.id.direct_messages_nav_graph)
}
}
private fun handleUrl(url: String?) {
if (url == null) return
// Log.d(TAG, url);
val intentModel = IntentUtils.parseUrl(url) ?: return
showView(intentModel)
}
private fun showView(intentModel: IntentModel) {
when (intentModel.type) {
IntentModelType.USERNAME -> showProfileView(intentModel)
IntentModelType.POST -> showPostView(intentModel)
IntentModelType.LOCATION -> showLocationView(intentModel)
IntentModelType.HASHTAG -> showHashtagView(intentModel)
IntentModelType.UNKNOWN -> Log.w(TAG, "Unknown model type received!")
// else -> Log.w(TAG, "Unknown model type received!")
}
}
private fun showProfileView(intentModel: IntentModel) {
val username = intentModel.text
// Log.d(TAG, "username: " + username);
val currentNavControllerLiveData = currentNavControllerLiveData ?: return
val navController = currentNavControllerLiveData.value
val bundle = Bundle()
bundle.putString("username", "@$username")
try {
navController?.navigate(R.id.action_global_profileFragment, bundle)
} catch (e: Exception) {
Log.e(TAG, "showProfileView: ", e)
}
}
private fun showPostView(intentModel: IntentModel) {
val shortCode = intentModel.text
// Log.d(TAG, "shortCode: " + shortCode);
val alertDialog = AlertDialog.Builder(this)
.setCancelable(false)
.setView(R.layout.dialog_opening_post)
.create()
alertDialog.show()
lifecycleScope.launch(Dispatchers.IO) {
try {
val media = if (isLoggedIn) mediaRepository.fetch(shortcodeToId(shortCode)) else graphQLRepository.fetchPost(shortCode)
withContext(Dispatchers.Main) {
if (media == null) {
Toast.makeText(applicationContext, R.string.post_not_found, Toast.LENGTH_SHORT).show()
return@withContext
}
val currentNavControllerLiveData = currentNavControllerLiveData ?: return@withContext
val navController = currentNavControllerLiveData.value
val bundle = Bundle()
bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media)
try {
navController?.navigate(R.id.action_global_post_view, bundle)
} catch (e: Exception) {
Log.e(TAG, "showPostView: ", e)
}
}
} catch (e: Exception) {
Log.e(TAG, "showPostView: ", e)
} finally {
withContext(Dispatchers.Main) {
alertDialog.dismiss()
}
}
}
}
private fun showLocationView(intentModel: IntentModel) {
val locationId = intentModel.text
// Log.d(TAG, "locationId: " + locationId);
val currentNavControllerLiveData = currentNavControllerLiveData ?: return
val navController = currentNavControllerLiveData.value
val bundle = Bundle()
bundle.putLong("locationId", locationId.toLong())
navController?.navigate(R.id.action_global_locationFragment, bundle)
}
private fun showHashtagView(intentModel: IntentModel) {
val hashtag = intentModel.text
// Log.d(TAG, "hashtag: " + hashtag);
val currentNavControllerLiveData = currentNavControllerLiveData ?: return
val navController = currentNavControllerLiveData.value
val bundle = Bundle()
bundle.putString("hashtag", hashtag)
navController?.navigate(R.id.action_global_hashTagFragment, bundle)
}
private fun showActivityView() {
val currentNavControllerLiveData = currentNavControllerLiveData ?: return
val navController = currentNavControllerLiveData.value
val bundle = Bundle()
bundle.putString("type", "notif")
navController?.navigate(R.id.action_global_notificationsViewerFragment, bundle)
}
private fun bindActivityCheckerService() {
bindService(Intent(this, ActivityCheckerService::class.java), serviceConnection, BIND_AUTO_CREATE)
isActivityCheckerServiceBound = true
}
private fun unbindActivityCheckerService() {
if (!isActivityCheckerServiceBound) return
unbindService(serviceConnection)
isActivityCheckerServiceBound = false
}
val bottomNavView: BottomNavigationView
get() = binding.bottomNavView
fun setCollapsingView(view: View) {
try {
binding.collapsingToolbarLayout.addView(view, 0)
} catch (e: Exception) {
Log.e(TAG, "setCollapsingView: ", e)
}
}
fun removeCollapsingView(view: View) {
try {
binding.collapsingToolbarLayout.removeView(view)
} catch (e: Exception) {
Log.e(TAG, "removeCollapsingView: ", e)
}
}
fun resetToolbar() {
binding.appBarLayout.visibility = View.VISIBLE
setScrollingBehaviour()
setSupportActionBar(binding.toolbar)
val currentNavControllerLiveData = currentNavControllerLiveData ?: return
setupNavigation(binding.toolbar, currentNavControllerLiveData.value)
}
val collapsingToolbarView: CollapsingToolbarLayout
get() = binding.collapsingToolbarLayout
val appbarLayout: AppBarLayout
get() = binding.appBarLayout
fun removeLayoutTransition() {
binding.root.layoutTransition = null
}
fun setLayoutTransition() {
binding.root.layoutTransition = LayoutTransition()
}
private fun initEmojiCompat() {
// Use a downloadable font for EmojiCompat
val fontRequest = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.com_google_android_gms_fonts_certs)
val config: EmojiCompat.Config = FontRequestEmojiCompatConfig(applicationContext, fontRequest)
config.setReplaceAll(true) // .setUseEmojiAsDefaultStyle(true)
.registerInitCallback(object : InitCallback() {
override fun onInitialized() {
Log.i(TAG, "EmojiCompat initialized")
}
override fun onFailed(throwable: Throwable?) {
Log.e(TAG, "EmojiCompat initialization failed", throwable)
}
})
EmojiCompat.init(config)
}
var toolbar: Toolbar
get() = binding.toolbar
set(toolbar) {
binding.appBarLayout.visibility = View.GONE
removeScrollingBehaviour()
setSupportActionBar(toolbar)
if (currentNavControllerLiveData == null) return
setupNavigation(toolbar, currentNavControllerLiveData?.value)
}
val rootView: View
get() = binding.root
private fun setNavBarDMUnreadCountBadge(unseenCount: Int) {
val badge = binding.bottomNavView.getOrCreateBadge(R.id.direct_messages_nav_graph)
if (unseenCount == 0) {
badge.isVisible = false
badge.clearNumber()
return
}
if (badge.verticalOffset != 10) {
badge.verticalOffset = 10
}
badge.number = unseenCount
badge.isVisible = true
}
fun showSearchView(): TextInputLayout {
binding.searchInputLayout.visibility = View.VISIBLE
return binding.searchInputLayout
}
fun hideSearchView() {
binding.searchInputLayout.visibility = View.GONE
}
companion object {
private const val TAG = "MainActivity"
private const val FIRST_FRAGMENT_GRAPH_INDEX_KEY = "firstFragmentGraphIndex"
private const val LAST_SELECT_NAV_MENU_ID = "lastSelectedNavMenuId"
private val SEARCH_VISIBLE_DESTINATIONS: List<Int> = ImmutableList.of(
R.id.feedFragment,
R.id.profileFragment,
R.id.directMessagesInboxFragment,
R.id.discoverFragment,
R.id.favoritesFragment,
R.id.hashTagFragment,
R.id.locationFragment
)
@JvmStatic
var instance: MainActivity? = null
private set
}
}

View File

@ -1,195 +1,60 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import awais.instagrabber.adapters.viewholder.comments.ChildCommentViewHolder;
import awais.instagrabber.adapters.viewholder.comments.ParentCommentViewHolder;
import awais.instagrabber.adapters.viewholder.CommentViewHolder;
import awais.instagrabber.databinding.ItemCommentBinding;
import awais.instagrabber.databinding.ItemCommentSmallBinding;
import awais.instagrabber.models.CommentModel;
import awais.instagrabber.models.Comment;
public final class CommentsAdapter extends ListAdapter<CommentModel, RecyclerView.ViewHolder> {
private static final int TYPE_PARENT = 1;
private static final int TYPE_CHILD = 2;
private final Map<Integer, Integer> positionTypeMap = new HashMap<>();
// private final Filter filter = new Filter() {
// @NonNull
// @Override
// protected FilterResults performFiltering(final CharSequence filter) {
// final FilterResults results = new FilterResults();
// results.values = commentModels;
//
// final int commentsLen = commentModels == null ? 0 : commentModels.size();
// if (commentModels != null && commentsLen > 0 && !TextUtils.isEmpty(filter)) {
// final String query = filter.toString().toLowerCase();
// final ArrayList<CommentModel> filterList = new ArrayList<>(commentsLen);
//
// for (final CommentModel commentModel : commentModels) {
// final String commentText = commentModel.getText().toString().toLowerCase();
//
// if (commentText.contains(query)) filterList.add(commentModel);
// else {
// final List<CommentModel> childCommentModels = commentModel.getChildCommentModels();
// if (childCommentModels != null) {
// for (final CommentModel childCommentModel : childCommentModels) {
// final String childCommentText = childCommentModel.getText().toString().toLowerCase();
// if (childCommentText.contains(query)) filterList.add(commentModel);
// }
// }
// }
// }
// filterList.trimToSize();
// results.values = filterList.toArray(new CommentModel[0]);
// }
//
// return results;
// }
//
// @Override
// protected void publishResults(final CharSequence constraint, @NonNull final FilterResults results) {
// if (results.values instanceof List) {
// //noinspection unchecked
// filteredCommentModels = (List<CommentModel>) results.values;
// notifyDataSetChanged();
// }
// }
// };
private static final DiffUtil.ItemCallback<CommentModel> DIFF_CALLBACK = new DiffUtil.ItemCallback<CommentModel>() {
public final class CommentsAdapter extends ListAdapter<Comment, CommentViewHolder> {
private static final DiffUtil.ItemCallback<Comment> DIFF_CALLBACK = new DiffUtil.ItemCallback<Comment>() {
@Override
public boolean areItemsTheSame(@NonNull final CommentModel oldItem, @NonNull final CommentModel newItem) {
return oldItem.getId().equals(newItem.getId());
public boolean areItemsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) {
return Objects.equals(oldItem.getPk(), newItem.getPk());
}
@Override
public boolean areContentsTheSame(@NonNull final CommentModel oldItem, @NonNull final CommentModel newItem) {
return oldItem.getId().equals(newItem.getId());
public boolean areContentsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) {
return Objects.equals(oldItem, newItem);
}
};
private final CommentCallback commentCallback;
private CommentModel selected, toChangeLike;
private int selectedIndex, likedIndex;
public CommentsAdapter(final CommentCallback commentCallback) {
private final boolean showingReplies;
private final CommentCallback commentCallback;
private final long currentUserId;
public CommentsAdapter(final long currentUserId,
final boolean showingReplies,
final CommentCallback commentCallback) {
super(DIFF_CALLBACK);
this.showingReplies = showingReplies;
this.currentUserId = currentUserId;
this.commentCallback = commentCallback;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final Context context = parent.getContext();
final LayoutInflater layoutInflater = LayoutInflater.from(context);
if (type == TYPE_PARENT) {
final ItemCommentBinding binding = ItemCommentBinding.inflate(layoutInflater, parent, false);
return new ParentCommentViewHolder(binding);
}
final ItemCommentSmallBinding binding = ItemCommentSmallBinding.inflate(layoutInflater, parent, false);
return new ChildCommentViewHolder(binding);
public CommentViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemCommentBinding binding = ItemCommentBinding.inflate(layoutInflater, parent, false);
return new CommentViewHolder(binding, currentUserId, commentCallback);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
CommentModel commentModel = getItem(position);
if (commentModel == null) return;
final int type = getItemViewType(position);
final boolean selected = this.selected != null && this.selected.getId().equals(commentModel.getId());
final boolean toLike = this.toChangeLike != null && this.toChangeLike.getId().equals(commentModel.getId());
if (toLike) commentModel = this.toChangeLike;
if (type == TYPE_PARENT) {
final ParentCommentViewHolder viewHolder = (ParentCommentViewHolder) holder;
viewHolder.bind(commentModel, selected, commentCallback);
return;
}
final ChildCommentViewHolder viewHolder = (ChildCommentViewHolder) holder;
viewHolder.bind(commentModel, selected, commentCallback);
}
@Override
public void submitList(@Nullable final List<CommentModel> list) {
final List<CommentModel> flatList = flattenList(list);
super.submitList(flatList);
}
@Override
public void submitList(@Nullable final List<CommentModel> list, @Nullable final Runnable commitCallback) {
final List<CommentModel> flatList = flattenList(list);
super.submitList(flatList, commitCallback);
}
private List<CommentModel> flattenList(final List<CommentModel> list) {
if (list == null) {
return Collections.emptyList();
}
final List<CommentModel> flatList = new ArrayList<>();
int lastCommentIndex = -1;
for (final CommentModel parent : list) {
lastCommentIndex++;
flatList.add(parent);
positionTypeMap.put(lastCommentIndex, TYPE_PARENT);
final List<CommentModel> children = parent.getChildCommentModels();
if (children != null) {
for (final CommentModel child : children) {
lastCommentIndex++;
flatList.add(child);
positionTypeMap.put(lastCommentIndex, TYPE_CHILD);
}
}
}
return flatList;
}
@Override
public int getItemViewType(final int position) {
final Integer type = positionTypeMap.get(position);
if (type == null) {
return TYPE_PARENT;
}
return type;
}
public void setSelected(final CommentModel commentModel) {
this.selected = commentModel;
selectedIndex = getCurrentList().indexOf(commentModel);
notifyItemChanged(selectedIndex);
}
public void clearSelection() {
this.selected = null;
notifyItemChanged(selectedIndex);
}
public void setLiked(final CommentModel commentModel, final boolean liked) {
likedIndex = getCurrentList().indexOf(commentModel);
CommentModel newCommentModel = commentModel;
newCommentModel.setLiked(liked);
this.toChangeLike = newCommentModel;
notifyItemChanged(likedIndex);
}
public CommentModel getSelected() {
return selected;
public void onBindViewHolder(@NonNull final CommentViewHolder holder, final int position) {
final Comment comment = getItem(position);
holder.bind(comment, showingReplies && position == 0, showingReplies && position != 0);
}
public interface CommentCallback {
void onClick(final CommentModel comment);
void onClick(final Comment comment);
void onHashtagClick(final String hashtag);
@ -198,5 +63,15 @@ public final class CommentsAdapter extends ListAdapter<CommentModel, RecyclerVie
void onURLClick(final String url);
void onEmailClick(final String emailAddress);
void onLikeClick(final Comment comment, boolean liked, final boolean isReply);
void onRepliesClick(final Comment comment);
void onViewLikes(Comment comment);
void onTranslate(Comment comment);
void onDelete(Comment comment, boolean isReply);
}
}

View File

@ -1,6 +1,5 @@
package awais.instagrabber.adapters;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.ViewGroup;
@ -13,8 +12,10 @@ import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
@ -57,7 +58,6 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.DateUtils;
public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = DirectItemsAdapter.class.getSimpleName();
@ -139,7 +139,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
return new HeaderViewHolder(LayoutDmHeaderBinding.inflate(layoutInflater, parent, false));
}
final LayoutDmBaseBinding baseBinding = LayoutDmBaseBinding.inflate(layoutInflater, parent, false);
final DirectItemType directItemType = DirectItemType.valueOf(type);
final DirectItemType directItemType = DirectItemType.Companion.getId(type);
final DirectItemViewHolder itemViewHolder = getItemViewHolder(layoutInflater, baseBinding, directItemType);
itemViewHolder.setLongClickListener(longClickListener);
return itemViewHolder;
@ -292,12 +292,15 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
private List<DirectItemOrHeader> sectionAndSort(final List<DirectItem> list) {
final List<DirectItemOrHeader> itemOrHeaders = new ArrayList<>();
Date prevSectionDate = null;
LocalDate prevSectionDate = null;
for (int i = 0; i < list.size(); i++) {
final DirectItem item = list.get(i);
if (item == null) continue;
if (item == null || item.getDate() == null) continue;
final DirectItemOrHeader prev = itemOrHeaders.isEmpty() ? null : itemOrHeaders.get(itemOrHeaders.size() - 1);
if (prev != null && prev.item != null && DateUtils.isSameDay(prev.item.getDate(), item.getDate())) {
if (prev != null
&& prev.item != null
&& prev.item.getDate() != null
&& prev.item.getDate().toLocalDate().isEqual(item.getDate().toLocalDate())) {
// just add item
final DirectItemOrHeader itemOrHeader = new DirectItemOrHeader();
itemOrHeader.item = item;
@ -320,7 +323,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
final DirectItemOrHeader itemOrHeader = new DirectItemOrHeader();
itemOrHeader.item = item;
itemOrHeaders.add(itemOrHeader);
prevSectionDate = DateUtils.dateAtZeroHours(item.getDate());
prevSectionDate = item.getDate().toLocalDate();
}
return itemOrHeaders;
}
@ -352,7 +355,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
}
public static class DirectItemOrHeader {
Date date;
LocalDate date;
public DirectItem item;
public boolean isHeader() {
@ -377,12 +380,13 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
this.binding = binding;
}
public void bind(final Date date) {
public void bind(final LocalDate date) {
if (date == null) {
binding.header.setText("");
return;
}
binding.header.setText(DateFormat.getDateFormat(itemView.getContext()).format(date));
final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
binding.header.setText(dateFormatter.format(date));
}
}
@ -406,6 +410,8 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.
void onReactionClick(DirectItem item, int position);
void onOptionSelect(DirectItem item, @IdRes int itemId, final Function<DirectItem, Void> callback);
void onAddReactionListener(DirectItem item);
}
public interface DirectItemInternalLongClickListener {

View File

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.TopicClusterViewHolder;
import awais.instagrabber.databinding.ItemDiscoverTopicBinding;
import awais.instagrabber.repositories.responses.discover.TopicCluster;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.ResponseBodyUtils;
public class DiscoverTopicsAdapter extends ListAdapter<TopicCluster, TopicClusterViewHolder> {
@ -50,6 +51,8 @@ public class DiscoverTopicsAdapter extends ListAdapter<TopicCluster, TopicCluste
}
public interface OnTopicClickListener {
void onTopicClick(TopicCluster topicCluster, View root, View cover, View title, int titleColor, int backgroundColor);
void onTopicClick(TopicCluster topicCluster, View cover, int titleColor, int backgroundColor);
void onTopicLongClick(Media coverMedia);
}
}

View File

@ -19,7 +19,7 @@ import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.FavoriteViewHolder;
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
import awais.instagrabber.databinding.ItemSuggestionBinding;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
@ -73,7 +73,7 @@ public class FavoritesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
// header
return new FavSectionViewHolder(ItemFavSectionHeaderBinding.inflate(inflater, parent, false));
}
final ItemSuggestionBinding binding = ItemSuggestionBinding.inflate(inflater, parent, false);
final ItemSearchResultBinding binding = ItemSearchResultBinding.inflate(inflater, parent, false);
return new FavoriteViewHolder(binding);
}

View File

@ -4,19 +4,12 @@ import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import awais.instagrabber.adapters.viewholder.FeedStoryViewHolder;
import awais.instagrabber.databinding.ItemHighlightBinding;
import awais.instagrabber.models.FeedStoryModel;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.Utils;
public final class FeedStoriesAdapter extends ListAdapter<FeedStoryModel, FeedStoryViewHolder> {
private final OnFeedStoryClickListener listener;
@ -29,7 +22,7 @@ public final class FeedStoriesAdapter extends ListAdapter<FeedStoryModel, FeedSt
@Override
public boolean areContentsTheSame(@NonNull final FeedStoryModel oldItem, @NonNull final FeedStoryModel newItem) {
return oldItem.getStoryMediaId().equals(newItem.getStoryMediaId()) && oldItem.isFullyRead().equals(newItem.isFullyRead());
return oldItem.getStoryMediaId().equals(newItem.getStoryMediaId()) && oldItem.isFullyRead() == newItem.isFullyRead();
}
};

View File

@ -23,23 +23,29 @@ public final class FeedStoriesListAdapter extends ListAdapter<FeedStoryModel, St
private List<FeedStoryModel> list;
private final Filter filter = new Filter() {
@Nullable
@NonNull
@Override
protected FilterResults performFiltering(final CharSequence filter) {
final boolean isFilterEmpty = TextUtils.isEmpty(filter);
final String query = isFilterEmpty ? null : filter.toString().toLowerCase();
for (FeedStoryModel item : list) {
if (isFilterEmpty) item.setShown(true);
else item.setShown(item.getProfileModel().getUsername().toLowerCase().contains(query));
final String query = TextUtils.isEmpty(filter) ? null : filter.toString().toLowerCase();
List<FeedStoryModel> filteredList = list;
if (list != null && query != null) {
filteredList = list.stream()
.filter(feedStoryModel -> feedStoryModel.getProfileModel()
.getUsername()
.toLowerCase()
.contains(query))
.collect(Collectors.toList());
}
return null;
final FilterResults filterResults = new FilterResults();
filterResults.count = filteredList != null ? filteredList.size() : 0;
filterResults.values = filteredList;
return filterResults;
}
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results) {
submitList(list);
notifyDataSetChanged();
//noinspection unchecked
submitList((List<FeedStoryModel>) results.values, true);
}
};
@ -51,7 +57,7 @@ public final class FeedStoriesListAdapter extends ListAdapter<FeedStoryModel, St
@Override
public boolean areContentsTheSame(@NonNull final FeedStoryModel oldItem, @NonNull final FeedStoryModel newItem) {
return oldItem.getStoryMediaId().equals(newItem.getStoryMediaId()) && oldItem.isFullyRead().equals(newItem.isFullyRead());
return oldItem.getStoryMediaId().equals(newItem.getStoryMediaId()) && oldItem.isFullyRead() == newItem.isFullyRead();
}
};
@ -65,10 +71,16 @@ public final class FeedStoriesListAdapter extends ListAdapter<FeedStoryModel, St
return filter;
}
private void submitList(@Nullable final List<FeedStoryModel> list, final boolean isFiltered) {
if (!isFiltered) {
this.list = list;
}
super.submitList(list);
}
@Override
public void submitList(final List<FeedStoryModel> list) {
super.submitList(list.stream().filter(i -> i.isShown()).collect(Collectors.toList()));
this.list = list;
submitList(list, false);
}
@NonNull
@ -82,11 +94,11 @@ public final class FeedStoriesListAdapter extends ListAdapter<FeedStoryModel, St
@Override
public void onBindViewHolder(@NonNull final StoryListViewHolder holder, final int position) {
final FeedStoryModel model = getItem(position);
holder.bind(model, position, listener);
holder.bind(model, listener);
}
public interface OnFeedStoryClickListener {
void onFeedStoryClick(final FeedStoryModel model, final int position);
void onFeedStoryClick(final FeedStoryModel model);
void onProfileClick(final String username);
}

View File

@ -34,7 +34,7 @@ public final class LikesAdapter extends RecyclerView.Adapter<FollowsViewHolder>
@Override
public void onBindViewHolder(@NonNull final FollowsViewHolder holder, final int position) {
final User model = profileModels.get(position);
holder.bind(model, null, onClickListener);
holder.bind(model, onClickListener);
}
@Override

View File

@ -23,8 +23,8 @@ public final class NotificationsAdapter extends ListAdapter<Notification, Notifi
private static final DiffUtil.ItemCallback<Notification> DIFF_CALLBACK = new DiffUtil.ItemCallback<Notification>() {
@Override
public boolean areItemsTheSame(@NonNull final Notification oldItem, @NonNull final Notification newItem) {
return oldItem.getPk().equals(newItem.getPk());
public boolean areItemsTheSame(final Notification oldItem, final Notification newItem) {
return oldItem != null && newItem != null && oldItem.getPk().equals(newItem.getPk());
}
@Override

View File

@ -11,20 +11,19 @@ import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.TopicClusterViewHolder;
import awais.instagrabber.databinding.ItemDiscoverTopicBinding;
import awais.instagrabber.repositories.responses.saved.SavedCollection;
import awais.instagrabber.utils.ResponseBodyUtils;
public class SavedCollectionsAdapter extends ListAdapter<SavedCollection, TopicClusterViewHolder> {
private static final DiffUtil.ItemCallback<SavedCollection> DIFF_CALLBACK = new DiffUtil.ItemCallback<SavedCollection>() {
@Override
public boolean areItemsTheSame(@NonNull final SavedCollection oldItem, @NonNull final SavedCollection newItem) {
return oldItem.getId().equals(newItem.getId());
return oldItem.getCollectionId().equals(newItem.getCollectionId());
}
@Override
public boolean areContentsTheSame(@NonNull final SavedCollection oldItem, @NonNull final SavedCollection newItem) {
if (oldItem.getCoverMedias() != null && newItem.getCoverMedias() != null
&& oldItem.getCoverMedias().size() == newItem.getCoverMedias().size()) {
return oldItem.getCoverMedias().get(0).getId().equals(newItem.getCoverMedias().get(0).getId());
if (oldItem.getCoverMediaList() != null && newItem.getCoverMediaList() != null
&& oldItem.getCoverMediaList().size() == newItem.getCoverMediaList().size()) {
return oldItem.getCoverMediaList().get(0).getId().equals(newItem.getCoverMediaList().get(0).getId());
}
else if (oldItem.getCoverMedia() != null && newItem.getCoverMedia() != null) {
return oldItem.getCoverMedia().getId().equals(newItem.getCoverMedia().getId());

View File

@ -0,0 +1,33 @@
package awais.instagrabber.adapters;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import java.util.List;
import awais.instagrabber.fragments.search.SearchCategoryFragment;
import awais.instagrabber.models.enums.FavoriteType;
public class SearchCategoryAdapter extends FragmentStateAdapter {
private final List<FavoriteType> categories;
public SearchCategoryAdapter(@NonNull final Fragment fragment,
@NonNull final List<FavoriteType> categories) {
super(fragment);
this.categories = categories;
}
@NonNull
@Override
public Fragment createFragment(final int position) {
return SearchCategoryFragment.newInstance(categories.get(position));
}
@Override
public int getItemCount() {
return categories.size();
}
}

View File

@ -0,0 +1,215 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.AdapterListUpdateCallback;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.SearchItemViewHolder;
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.fragments.search.SearchCategoryFragment.OnSearchItemClickListener;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.repositories.responses.search.SearchItem;
public final class SearchItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = SearchItemsAdapter.class.getSimpleName();
private static final DiffUtil.ItemCallback<SearchItemOrHeader> DIFF_CALLBACK = new DiffUtil.ItemCallback<SearchItemOrHeader>() {
@Override
public boolean areItemsTheSame(@NonNull final SearchItemOrHeader oldItem, @NonNull final SearchItemOrHeader newItem) {
return Objects.equals(oldItem, newItem);
}
@Override
public boolean areContentsTheSame(@NonNull final SearchItemOrHeader oldItem, @NonNull final SearchItemOrHeader newItem) {
return Objects.equals(oldItem, newItem);
}
};
private static final String RECENT = "recent";
private static final String FAVORITE = "favorite";
private static final int VIEW_TYPE_HEADER = 0;
private static final int VIEW_TYPE_ITEM = 1;
private final OnSearchItemClickListener onSearchItemClickListener;
private final AsyncListDiffer<SearchItemOrHeader> differ;
public SearchItemsAdapter(final OnSearchItemClickListener onSearchItemClickListener) {
differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(DIFF_CALLBACK).build());
this.onSearchItemClickListener = onSearchItemClickListener;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
if (viewType == VIEW_TYPE_HEADER) {
return new HeaderViewHolder(ItemFavSectionHeaderBinding.inflate(layoutInflater, parent, false));
}
final ItemSearchResultBinding binding = ItemSearchResultBinding.inflate(layoutInflater, parent, false);
return new SearchItemViewHolder(binding, onSearchItemClickListener);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (getItemViewType(position) == VIEW_TYPE_HEADER) {
final SearchItemOrHeader searchItemOrHeader = getItem(position);
if (!searchItemOrHeader.isHeader()) return;
((HeaderViewHolder) holder).bind(searchItemOrHeader.header);
return;
}
((SearchItemViewHolder) holder).bind(getItem(position).searchItem);
}
protected SearchItemOrHeader getItem(int position) {
return differ.getCurrentList().get(position);
}
@Override
public int getItemCount() {
return differ.getCurrentList().size();
}
@Override
public int getItemViewType(final int position) {
return getItem(position).isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM;
}
public void submitList(@Nullable final List<SearchItem> list) {
if (list == null) {
differ.submitList(null);
return;
}
differ.submitList(sectionAndSort(list));
}
public void submitList(@Nullable final List<SearchItem> list, @Nullable final Runnable commitCallback) {
if (list == null) {
differ.submitList(null, commitCallback);
return;
}
differ.submitList(sectionAndSort(list), commitCallback);
}
@NonNull
private List<SearchItemOrHeader> sectionAndSort(@NonNull final List<SearchItem> list) {
final boolean containsRecentOrFavorite = list.stream().anyMatch(searchItem -> searchItem.isRecent() || searchItem.isFavorite());
// Don't do anything if not showing recent results
if (!containsRecentOrFavorite) {
return list.stream()
.map(SearchItemOrHeader::new)
.collect(Collectors.toList());
}
final List<SearchItem> listCopy = new ArrayList<>(list);
Collections.sort(listCopy, (o1, o2) -> {
final boolean bothRecent = o1.isRecent() && o2.isRecent();
if (bothRecent) {
// Don't sort
return 0;
}
final boolean bothFavorite = o1.isFavorite() && o2.isFavorite();
if (bothFavorite) {
if (o1.getType() == o2.getType()) return 0;
// keep users at top
if (o1.getType() == FavoriteType.USER) return -1;
if (o2.getType() == FavoriteType.USER) return 1;
// keep locations at bottom
if (o1.getType() == FavoriteType.LOCATION) return 1;
if (o2.getType() == FavoriteType.LOCATION) return -1;
}
// keep recents at top
if (o1.isRecent()) return -1;
if (o2.isRecent()) return 1;
return 0;
});
final List<SearchItemOrHeader> itemOrHeaders = new ArrayList<>();
for (int i = 0; i < listCopy.size(); i++) {
final SearchItem searchItem = listCopy.get(i);
final SearchItemOrHeader prev = itemOrHeaders.isEmpty() ? null : itemOrHeaders.get(itemOrHeaders.size() - 1);
boolean prevWasSameType = prev != null && ((prev.searchItem.isRecent() && searchItem.isRecent())
|| (prev.searchItem.isFavorite() && searchItem.isFavorite()));
if (prevWasSameType) {
// just add the item
itemOrHeaders.add(new SearchItemOrHeader(searchItem));
continue;
}
// add header and item
// add header only if search item is recent or favorite
if (searchItem.isRecent() || searchItem.isFavorite()) {
itemOrHeaders.add(new SearchItemOrHeader(searchItem.isRecent() ? RECENT : FAVORITE));
}
itemOrHeaders.add(new SearchItemOrHeader(searchItem));
}
return itemOrHeaders;
}
private static class SearchItemOrHeader {
String header;
SearchItem searchItem;
public SearchItemOrHeader(final SearchItem searchItem) {
this.searchItem = searchItem;
}
public SearchItemOrHeader(final String header) {
this.header = header;
}
boolean isHeader() {
return header != null;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SearchItemOrHeader that = (SearchItemOrHeader) o;
return Objects.equals(header, that.header) &&
Objects.equals(searchItem, that.searchItem);
}
@Override
public int hashCode() {
return Objects.hash(header, searchItem);
}
}
public static class HeaderViewHolder extends RecyclerView.ViewHolder {
private final ItemFavSectionHeaderBinding binding;
public HeaderViewHolder(@NonNull final ItemFavSectionHeaderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final String header) {
if (header == null) return;
final int headerText;
switch (header) {
case RECENT:
headerText = R.string.recent;
break;
case FAVORITE:
headerText = R.string.title_favorites;
break;
default:
headerText = R.string.unknown;
break;
}
binding.getRoot().setText(headerText);
}
}
}

View File

@ -1,11 +1,17 @@
package awais.instagrabber.adapters;
import android.view.View;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import awais.instagrabber.repositories.responses.Media;
public class SliderCallbackAdapter implements SliderItemsAdapter.SliderCallback {
@Override
public void onThumbnailLoaded(final int position) {}
@Override
public void onItemClicked(final int position) {}
public void onItemClicked(final int position, final Media media, final View view) {}
@Override
public void onPlayerPlay(final int position) {}
@ -15,4 +21,12 @@ public class SliderCallbackAdapter implements SliderItemsAdapter.SliderCallback
@Override
public void onPlayerRelease(final int position) {}
@Override
public void onFullScreenModeChanged(final boolean isFullScreen, final StyledPlayerView playerView) {}
@Override
public boolean isInFullScreen() {
return false;
}
}

View File

@ -1,28 +1,27 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import awais.instagrabber.adapters.viewholder.SliderItemViewHolder;
import awais.instagrabber.adapters.viewholder.SliderPhotoViewHolder;
import awais.instagrabber.adapters.viewholder.SliderVideoViewHolder;
import awais.instagrabber.customviews.VerticalDragHelper;
import awais.instagrabber.databinding.ItemSliderPhotoBinding;
import awais.instagrabber.databinding.LayoutExoCustomControlsBinding;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
public final class SliderItemsAdapter extends ListAdapter<Media, SliderItemViewHolder> {
private final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener;
private final boolean loadVideoOnItemClick;
private final SliderCallback sliderCallback;
private final LayoutExoCustomControlsBinding controlsBinding;
private static final DiffUtil.ItemCallback<Media> DIFF_CALLBACK = new DiffUtil.ItemCallback<Media>() {
@Override
@ -36,15 +35,11 @@ public final class SliderItemsAdapter extends ListAdapter<Media, SliderItemViewH
}
};
public SliderItemsAdapter(final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener,
final LayoutExoCustomControlsBinding controlsBinding,
final boolean loadVideoOnItemClick,
public SliderItemsAdapter(final boolean loadVideoOnItemClick,
final SliderCallback sliderCallback) {
super(DIFF_CALLBACK);
this.onVerticalDragListener = onVerticalDragListener;
this.loadVideoOnItemClick = loadVideoOnItemClick;
this.sliderCallback = sliderCallback;
this.controlsBinding = controlsBinding;
}
@NonNull
@ -55,12 +50,12 @@ public final class SliderItemsAdapter extends ListAdapter<Media, SliderItemViewH
switch (mediaItemType) {
case MEDIA_TYPE_VIDEO: {
final LayoutVideoPlayerWithThumbnailBinding binding = LayoutVideoPlayerWithThumbnailBinding.inflate(inflater, parent, false);
return new SliderVideoViewHolder(binding, onVerticalDragListener, controlsBinding, loadVideoOnItemClick);
return new SliderVideoViewHolder(binding, loadVideoOnItemClick);
}
case MEDIA_TYPE_IMAGE:
default:
final ItemSliderPhotoBinding binding = ItemSliderPhotoBinding.inflate(inflater, parent, false);
return new SliderPhotoViewHolder(binding, onVerticalDragListener);
return new SliderPhotoViewHolder(binding);
}
}
@ -143,12 +138,16 @@ public final class SliderItemsAdapter extends ListAdapter<Media, SliderItemViewH
public interface SliderCallback {
void onThumbnailLoaded(int position);
void onItemClicked(int position);
void onItemClicked(int position, final Media media, final View view);
void onPlayerPlay(int position);
void onPlayerPause(int position);
void onPlayerRelease(int position);
void onFullScreenModeChanged(boolean isFullScreen, final StyledPlayerView playerView);
boolean isInFullScreen();
}
}

View File

@ -1,77 +0,0 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.cursoradapter.widget.CursorAdapter;
import awais.instagrabber.databinding.ItemSuggestionBinding;
import awais.instagrabber.models.enums.SuggestionType;
public final class SuggestionsAdapter extends CursorAdapter {
private static final String TAG = "SuggestionsAdapter";
private final OnSuggestionClickListener onSuggestionClickListener;
public SuggestionsAdapter(final Context context,
final OnSuggestionClickListener onSuggestionClickListener) {
super(context, null, FLAG_REGISTER_CONTENT_OBSERVER);
this.onSuggestionClickListener = onSuggestionClickListener;
}
@Override
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
final LayoutInflater layoutInflater = LayoutInflater.from(context);
final ItemSuggestionBinding binding = ItemSuggestionBinding.inflate(layoutInflater, parent, false);
return binding.getRoot();
// return layoutInflater.inflate(R.layout.item_suggestion, parent, false);
}
@Override
public void bindView(@NonNull final View view, final Context context, @NonNull final Cursor cursor) {
// i, username, fullname, type, query, picUrl, verified
// 0, 1 , 2 , 3 , 4 , 5 , 6
final String fullName = cursor.getString(2);
String username = cursor.getString(1);
String picUrl = cursor.getString(5);
final boolean verified = cursor.getString(6).charAt(0) == 't';
final String type = cursor.getString(3);
SuggestionType suggestionType = null;
try {
suggestionType = SuggestionType.valueOf(type);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unknown suggestion type: " + type, e);
}
if (suggestionType == null) return;
String query = cursor.getString(4);
switch (suggestionType) {
case TYPE_USER:
username = '@' + username;
break;
case TYPE_HASHTAG:
username = '#' + username;
break;
}
if (onSuggestionClickListener != null) {
final SuggestionType finalSuggestionType = suggestionType;
view.setOnClickListener(v -> onSuggestionClickListener.onSuggestionClick(finalSuggestionType, query));
}
final ItemSuggestionBinding binding = ItemSuggestionBinding.bind(view);
binding.isVerified.setVisibility(verified ? View.VISIBLE : View.GONE);
binding.tvUsername.setText(username);
binding.tvFullName.setVisibility(View.VISIBLE);
binding.tvFullName.setText(fullName);
binding.ivProfilePic.setImageURI(picUrl);
}
public interface OnSuggestionClickListener {
void onSuggestionClick(final SuggestionType type, final String query);
}
}

View File

@ -0,0 +1,209 @@
package awais.instagrabber.adapters.viewholder;
import android.content.Context;
import android.content.res.Resources;
import android.util.TypedValue;
import android.view.Menu;
import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.CommentsAdapter.CommentCallback;
import awais.instagrabber.customviews.ProfilePicView;
import awais.instagrabber.databinding.ItemCommentBinding;
import awais.instagrabber.models.Comment;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.Utils;
public final class CommentViewHolder extends RecyclerView.ViewHolder {
private final ItemCommentBinding binding;
private final long currentUserId;
private final CommentCallback commentCallback;
@ColorInt
private int parentCommentHighlightColor;
private PopupMenu optionsPopup;
public CommentViewHolder(@NonNull final ItemCommentBinding binding,
final long currentUserId,
final CommentCallback commentCallback) {
super(binding.getRoot());
this.binding = binding;
this.currentUserId = currentUserId;
this.commentCallback = commentCallback;
final Context context = itemView.getContext();
if (context == null) return;
final Resources.Theme theme = context.getTheme();
if (theme == null) return;
final TypedValue typedValue = new TypedValue();
final boolean resolved = theme.resolveAttribute(R.attr.parentCommentHighlightColor, typedValue, true);
if (resolved) {
parentCommentHighlightColor = typedValue.data;
}
}
public void bind(final Comment comment, final boolean isReplyParent, final boolean isReply) {
if (comment == null) return;
itemView.setOnClickListener(v -> {
if (commentCallback != null) {
commentCallback.onClick(comment);
}
});
if (isReplyParent && parentCommentHighlightColor != 0) {
itemView.setBackgroundColor(parentCommentHighlightColor);
} else {
itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent));
}
setupCommentText(comment, isReply);
binding.date.setText(comment.getDateTime());
setLikes(comment, isReply);
setReplies(comment, isReply);
setUser(comment, isReply);
setupOptions(comment, isReply);
}
private void setupCommentText(@NonNull final Comment comment, final boolean isReply) {
binding.comment.clearOnURLClickListeners();
binding.comment.clearOnHashtagClickListeners();
binding.comment.clearOnMentionClickListeners();
binding.comment.clearOnEmailClickListeners();
binding.comment.setText(comment.getText());
binding.comment.setTextSize(TypedValue.COMPLEX_UNIT_SP, isReply ? 12 : 14);
binding.comment.addOnHashtagListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onHashtagClick(originalText);
});
binding.comment.addOnMentionClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onMentionClick(originalText);
});
binding.comment.addOnEmailClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onEmailClick(originalText);
});
binding.comment.addOnURLClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onURLClick(originalText);
});
binding.comment.setOnLongClickListener(v -> {
Utils.copyText(itemView.getContext(), comment.getText());
return true;
});
binding.comment.setOnClickListener(v -> commentCallback.onClick(comment));
}
private void setUser(@NonNull final Comment comment, final boolean isReply) {
final User user = comment.getUser();
if (user == null) return;
binding.username.setUsername(user.getUsername(), user.isVerified());
binding.username.setTextAppearance(itemView.getContext(), isReply ? R.style.TextAppearance_MaterialComponents_Subtitle2
: R.style.TextAppearance_MaterialComponents_Subtitle1);
binding.username.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onMentionClick("@" + user.getUsername());
});
binding.profilePic.setImageURI(user.getProfilePicUrl());
binding.profilePic.setSize(isReply ? ProfilePicView.Size.SMALLER : ProfilePicView.Size.SMALL);
binding.profilePic.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onMentionClick("@" + user.getUsername());
});
}
private void setLikes(@NonNull final Comment comment, final boolean isReply) {
// final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes);
binding.likes.setText(String.valueOf(comment.getCommentLikeCount()));
binding.likes.setOnLongClickListener(v -> {
if (commentCallback == null) return false;
commentCallback.onViewLikes(comment);
return true;
});
if (currentUserId == 0) { // not logged in
binding.likes.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onViewLikes(comment);
});
return;
}
final boolean liked = comment.getLiked();
final int resId = liked ? R.drawable.ic_like : R.drawable.ic_not_liked;
binding.likes.setCompoundDrawablesRelativeWithSize(ContextCompat.getDrawable(itemView.getContext(), resId), null, null, null);
binding.likes.setOnClickListener(v -> {
if (commentCallback == null) return;
// toggle like
commentCallback.onLikeClick(comment, !liked, isReply);
});
}
private void setReplies(@NonNull final Comment comment, final boolean isReply) {
final int replies = comment.getChildCommentCount();
binding.replies.setVisibility(View.VISIBLE);
final String text = isReply ? "" : String.valueOf(replies);
// final String string = itemView.getResources().getQuantityString(R.plurals.replies_count, replies, replies);
binding.replies.setText(text);
binding.replies.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onRepliesClick(comment);
});
}
private void setupOptions(final Comment comment, final boolean isReply) {
binding.options.setOnClickListener(v -> {
if (optionsPopup == null) {
createOptionsPopupMenu(comment, isReply);
}
if (optionsPopup == null) return;
optionsPopup.show();
});
}
private void createOptionsPopupMenu(final Comment comment, final boolean isReply) {
if (optionsPopup == null) {
final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(itemView.getContext(), R.style.popupMenuStyle);
optionsPopup = new PopupMenu(themeWrapper, binding.options);
} else {
optionsPopup.getMenu().clear();
}
optionsPopup.getMenuInflater().inflate(R.menu.comment_options_menu, optionsPopup.getMenu());
final User user = comment.getUser();
if (currentUserId == 0 || user == null || user.getPk() != currentUserId) {
final Menu menu = optionsPopup.getMenu();
menu.removeItem(R.id.delete);
}
optionsPopup.setOnMenuItemClickListener(item -> {
if (commentCallback == null) return false;
int itemId = item.getItemId();
if (itemId == R.id.translate) {
commentCallback.onTranslate(comment);
return true;
}
if (itemId == R.id.delete) {
commentCallback.onDelete(comment, isReply);
}
return true;
});
}
// private void setupReply(final Comment comment) {
// if (!isLoggedIn) {
// binding.reply.setVisibility(View.GONE);
// return;
// }
// binding.reply.setOnClickListener(v -> {
// if (commentCallback == null) return;
// // toggle like
// commentCallback.onReplyClick(comment);
// });
// }
}

View File

@ -1,26 +1,26 @@
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import awais.instagrabber.R;
public final class DiscoverViewHolder extends RecyclerView.ViewHolder {
public final SimpleDraweeView postImage;
public final ImageView typeIcon;
public final View selectedView;
// public final View progressView;
public DiscoverViewHolder(@NonNull final View itemView) {
super(itemView);
typeIcon = itemView.findViewById(R.id.typeIcon);
postImage = itemView.findViewById(R.id.postImage);
selectedView = itemView.findViewById(R.id.selectedView);
// progressView = itemView.findViewById(R.id.progressView);
}
}
//package awais.instagrabber.adapters.viewholder;
//
//import android.view.View;
//import android.widget.ImageView;
//
//import androidx.annotation.NonNull;
//import androidx.recyclerview.widget.RecyclerView;
//
//import com.facebook.drawee.view.SimpleDraweeView;
//
//import awais.instagrabber.R;
//
//public final class DiscoverViewHolder extends RecyclerView.ViewHolder {
// public final SimpleDraweeView postImage;
// public final ImageView typeIcon;
// public final View selectedView;
// // public final View progressView;
//
// public DiscoverViewHolder(@NonNull final View itemView) {
// super(itemView);
// typeIcon = itemView.findViewById(R.id.typeIcon);
// postImage = itemView.findViewById(R.id.postImage);
// selectedView = itemView.findViewById(R.id.selectedView);
// // progressView = itemView.findViewById(R.id.progressView);
// }
//}

View File

@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.adapters.FavoritesAdapter;
import awais.instagrabber.databinding.ItemSuggestionBinding;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.utils.Constants;
@ -14,12 +14,12 @@ import awais.instagrabber.utils.Constants;
public class FavoriteViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = "FavoriteViewHolder";
private final ItemSuggestionBinding binding;
private final ItemSearchResultBinding binding;
public FavoriteViewHolder(@NonNull final ItemSuggestionBinding binding) {
public FavoriteViewHolder(@NonNull final ItemSearchResultBinding binding) {
super(binding.getRoot());
this.binding = binding;
binding.isVerified.setVisibility(View.GONE);
binding.verified.setVisibility(View.GONE);
}
public void bind(final Favorite model,
@ -36,12 +36,12 @@ public class FavoriteViewHolder extends RecyclerView.ViewHolder {
return longClickListener.onLongClick(model);
});
if (model.getType() == FavoriteType.HASHTAG) {
binding.ivProfilePic.setImageURI(Constants.DEFAULT_HASH_TAG_PIC);
binding.profilePic.setImageURI(Constants.DEFAULT_HASH_TAG_PIC);
} else {
binding.ivProfilePic.setImageURI(model.getPicUrl());
binding.profilePic.setImageURI(model.getPicUrl());
}
binding.tvFullName.setText(model.getDisplayName());
binding.tvUsername.setVisibility(View.VISIBLE);
binding.title.setVisibility(View.VISIBLE);
binding.subtitle.setText(model.getDisplayName());
String query = model.getQuery();
switch (model.getType()) {
case HASHTAG:
@ -51,11 +51,11 @@ public class FavoriteViewHolder extends RecyclerView.ViewHolder {
query = "@" + query;
break;
case LOCATION:
binding.tvUsername.setVisibility(View.GONE);
binding.title.setVisibility(View.GONE);
break;
default:
// do nothing
}
binding.tvUsername.setText(query);
binding.title.setText(query);
}
}

View File

@ -19,11 +19,12 @@ import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.asyncs.DownloadedCheckerAsyncTask;
import awais.instagrabber.databinding.ItemFeedGridBinding;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
@ -68,7 +69,9 @@ public class FeedGridItemViewHolder extends RecyclerView.ViewHolder {
setUserDetails(media, layoutPreferences);
String thumbnailUrl = null;
final int typeIconRes;
switch (media.getMediaType()) {
final MediaItemType mediaType = media.getMediaType();
if (mediaType == null) return;
switch (mediaType) {
case MEDIA_TYPE_IMAGE:
typeIconRes = -1;
thumbnailUrl = ResponseBodyUtils.getThumbUrl(media);
@ -102,31 +105,28 @@ public class FeedGridItemViewHolder extends RecyclerView.ViewHolder {
binding.typeIcon.setVisibility(View.VISIBLE);
binding.typeIcon.setImageResource(typeIconRes);
}
final DownloadedCheckerAsyncTask task = new DownloadedCheckerAsyncTask(result -> {
final List<Boolean> checkList = result.get(media.getPk());
if (checkList == null || checkList.isEmpty()) {
return;
}
switch (media.getMediaType()) {
case MEDIA_TYPE_IMAGE:
case MEDIA_TYPE_VIDEO:
binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(R.color.green_A400)));
break;
case MEDIA_TYPE_SLIDER:
binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
final List<Media> carouselMedia = media.getCarouselMedia();
boolean allDownloaded = checkList.size() == (carouselMedia == null ? 0 : carouselMedia.size());
if (allDownloaded) {
allDownloaded = checkList.stream().allMatch(downloaded -> downloaded);
}
binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(
allDownloaded ? R.color.green_A400 : R.color.yellow_400)));
break;
default:
}
});
task.execute(media);
final List<Boolean> checkList = DownloadUtils.checkDownloaded(media);
if (checkList.isEmpty()) {
return;
}
switch (media.getMediaType()) {
case MEDIA_TYPE_IMAGE:
case MEDIA_TYPE_VIDEO:
binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(R.color.green_A400)));
break;
case MEDIA_TYPE_SLIDER:
binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
final List<Media> carouselMedia = media.getCarouselMedia();
boolean allDownloaded = checkList.size() == (carouselMedia == null ? 0 : carouselMedia.size());
if (allDownloaded) {
allDownloaded = checkList.stream().allMatch(downloaded -> downloaded);
}
binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(
allDownloaded ? R.color.green_A400 : R.color.yellow_400)));
break;
default:
}
}
private void setThumbImage(final String thumbnailUrl) {

View File

@ -33,7 +33,7 @@ public class FilterViewHolder extends RecyclerView.ViewHolder {
this.binding = binding;
this.tuneFilters = tuneFilters;
this.onFilterClickListener = onFilterClickListener;
appExecutors = AppExecutors.getInstance();
appExecutors = AppExecutors.INSTANCE;
}
public void bind(final int position, final String originalKey, final Bitmap originalBitmap, final Filter<?> item, final boolean isSelected) {
@ -55,13 +55,13 @@ public class FilterViewHolder extends RecyclerView.ViewHolder {
final Bitmap bitmap = BitmapUtils.getBitmapFromMemCache(filterKey);
if (bitmap == null) {
final GPUImageFilter filter = item.getInstance();
appExecutors.tasksThread().submit(() -> {
appExecutors.getTasksThread().submit(() -> {
GPUImage.getBitmapForMultipleFilters(
originalBitmap,
ImmutableList.<GPUImageFilter>builder().add(filter).addAll(tuneFilters).build(),
filteredBitmap -> {
BitmapUtils.addBitmapToMemoryCache(filterKey, filteredBitmap, true);
appExecutors.mainThread().execute(() -> binding.getRoot().post(() -> binding.preview.setImageBitmap(filteredBitmap)));
appExecutors.getMainThread().execute(() -> binding.getRoot().post(() -> binding.preview.setImageBitmap(filteredBitmap)));
}
);
});

View File

@ -2,10 +2,9 @@ package awais.instagrabber.adapters.viewholder;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.User;
@ -14,23 +13,19 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder {
private final ItemFollowBinding binding;
public FollowsViewHolder(final ItemFollowBinding binding) {
public FollowsViewHolder(@NonNull final ItemFollowBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final User model,
final List<Long> admins,
final View.OnClickListener onClickListener) {
if (model == null) return;
itemView.setTag(model);
itemView.setOnClickListener(onClickListener);
binding.tvUsername.setText(model.getUsername());
binding.tvFullName.setText(model.getFullName());
if (admins != null && admins.contains(model.getPk())) {
binding.isAdmin.setVisibility(View.VISIBLE);
}
binding.ivProfilePic.setImageURI(model.getProfilePicUrl());
binding.username.setUsername("@" + model.getUsername(), model.isVerified());
binding.fullName.setText(model.getFullName());
binding.profilePic.setImageURI(model.getProfilePicUrl());
}
public void bind(final FollowModel model,
@ -38,8 +33,8 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder {
if (model == null) return;
itemView.setTag(model);
itemView.setOnClickListener(onClickListener);
binding.tvUsername.setText(model.getUsername());
binding.tvFullName.setText(model.getFullName());
binding.ivProfilePic.setImageURI(model.getProfilePicUrl());
binding.username.setUsername("@" + model.getUsername());
binding.fullName.setText(model.getFullName());
binding.profilePic.setImageURI(model.getProfilePicUrl());
}
}

View File

@ -0,0 +1,80 @@
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.fragments.search.SearchCategoryFragment.OnSearchItemClickListener;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.repositories.responses.Hashtag;
import awais.instagrabber.repositories.responses.Place;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.search.SearchItem;
public class SearchItemViewHolder extends RecyclerView.ViewHolder {
private final ItemSearchResultBinding binding;
private final OnSearchItemClickListener onSearchItemClickListener;
public SearchItemViewHolder(@NonNull final ItemSearchResultBinding binding,
final OnSearchItemClickListener onSearchItemClickListener) {
super(binding.getRoot());
this.binding = binding;
this.onSearchItemClickListener = onSearchItemClickListener;
}
public void bind(final SearchItem searchItem) {
if (searchItem == null) return;
final FavoriteType type = searchItem.getType();
if (type == null) return;
String title;
String subtitle;
String picUrl;
boolean isVerified = false;
switch (type) {
case USER:
final User user = searchItem.getUser();
title = "@" + user.getUsername();
subtitle = user.getFullName();
picUrl = user.getProfilePicUrl();
isVerified = user.isVerified();
break;
case HASHTAG:
final Hashtag hashtag = searchItem.getHashtag();
title = "#" + hashtag.getName();
subtitle = hashtag.getSubtitle();
picUrl = "res:/" + R.drawable.ic_hashtag;
break;
case LOCATION:
final Place place = searchItem.getPlace();
title = place.getTitle();
subtitle = place.getSubtitle();
picUrl = "res:/" + R.drawable.ic_location;
break;
default:
return;
}
itemView.setOnClickListener(v -> {
if (onSearchItemClickListener != null) {
onSearchItemClickListener.onSearchItemClick(searchItem);
}
});
binding.delete.setVisibility(searchItem.isRecent() ? View.VISIBLE : View.GONE);
if (searchItem.isRecent()) {
binding.delete.setEnabled(true);
binding.delete.setOnClickListener(v -> {
if (onSearchItemClickListener != null) {
binding.delete.setEnabled(false);
onSearchItemClickListener.onSearchItemDelete(searchItem);
}
});
}
binding.title.setText(title);
binding.subtitle.setText(subtitle);
binding.profilePic.setImageURI(picUrl);
binding.verified.setVisibility(isVerified ? View.VISIBLE : View.GONE);
}
}

View File

@ -2,7 +2,6 @@ package awais.instagrabber.adapters.viewholder;
import android.graphics.drawable.Animatable;
import android.net.Uri;
import android.view.GestureDetector;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
@ -14,8 +13,8 @@ import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import awais.instagrabber.adapters.SliderItemsAdapter;
import awais.instagrabber.customviews.VerticalDragHelper;
import awais.instagrabber.customviews.drawee.AnimatedZoomableController;
import awais.instagrabber.customviews.drawee.DoubleTapGestureListener;
import awais.instagrabber.databinding.ItemSliderPhotoBinding;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.ResponseBodyUtils;
@ -24,13 +23,10 @@ public class SliderPhotoViewHolder extends SliderItemViewHolder {
private static final String TAG = "FeedSliderPhotoViewHolder";
private final ItemSliderPhotoBinding binding;
private final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener;
public SliderPhotoViewHolder(@NonNull final ItemSliderPhotoBinding binding,
final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener) {
public SliderPhotoViewHolder(@NonNull final ItemSliderPhotoBinding binding) {
super(binding.getRoot());
this.binding = binding;
this.onVerticalDragListener = onVerticalDragListener;
}
public void bind(@NonNull final Media model,
@ -62,74 +58,19 @@ public class SliderPhotoViewHolder extends SliderItemViewHolder {
})
.setLowResImageRequest(ImageRequest.fromUri(ResponseBodyUtils.getThumbUrl(model)))
.build());
// binding.getRoot().setOnClickListener(v -> {
// if (sliderCallback != null) {
// sliderCallback.onItemClicked(position);
// }
// });
binding.getRoot().setTapListener(new GestureDetector.SimpleOnGestureListener() {
final DoubleTapGestureListener tapListener = new DoubleTapGestureListener(binding.getRoot()) {
@Override
public boolean onSingleTapUp(final MotionEvent e) {
public boolean onSingleTapConfirmed(final MotionEvent e) {
if (sliderCallback != null) {
sliderCallback.onItemClicked(position);
return true;
sliderCallback.onItemClicked(position, model, binding.getRoot());
}
return false;
return super.onSingleTapConfirmed(e);
}
});
};
binding.getRoot().setTapListener(tapListener);
final AnimatedZoomableController zoomableController = AnimatedZoomableController.newInstance();
zoomableController.setMaxScaleFactor(3f);
binding.getRoot().setZoomableController(zoomableController);
if (onVerticalDragListener != null) {
binding.getRoot().setOnVerticalDragListener(onVerticalDragListener);
}
binding.getRoot().setZoomingEnabled(true);
}
// private void setDimensions(final FeedModel feedModel, final int spanCount, final boolean animate) {
// final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams();
// final int deviceWidth = Utils.displayMetrics.widthPixels;
// final int spanWidth = deviceWidth / spanCount;
// final int spanHeight = NumberUtils.getResultingHeight(spanWidth, feedModel.getImageHeight(), feedModel.getImageWidth());
// final int width = spanWidth == 0 ? deviceWidth : spanWidth;
// final int height = spanHeight == 0 ? deviceWidth + 1 : spanHeight;
// if (animate) {
// Animation animation = AnimationUtils.expand(
// binding.imageViewer,
// layoutParams.width,
// layoutParams.height,
// width,
// height,
// new Animation.AnimationListener() {
// @Override
// public void onAnimationStart(final Animation animation) {
// showOrHideDetails(spanCount);
// }
//
// @Override
// public void onAnimationEnd(final Animation animation) {
// // showOrHideDetails(spanCount);
// }
//
// @Override
// public void onAnimationRepeat(final Animation animation) {
//
// }
// });
// binding.imageViewer.startAnimation(animation);
// } else {
// layoutParams.width = width;
// layoutParams.height = height;
// binding.imageViewer.requestLayout();
// }
// }
//
// private void showOrHideDetails(final int spanCount) {
// if (spanCount == 1) {
// binding.itemFeedTop.getRoot().setVisibility(View.VISIBLE);
// binding.itemFeedBottom.getRoot().setVisibility(View.VISIBLE);
// } else {
// binding.itemFeedTop.getRoot().setVisibility(View.GONE);
// binding.itemFeedBottom.getRoot().setVisibility(View.GONE);
// }
// }
}

View File

@ -7,17 +7,17 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import java.util.List;
import awais.instagrabber.adapters.SliderItemsAdapter;
import awais.instagrabber.customviews.VerticalDragHelper;
import awais.instagrabber.customviews.VideoPlayerCallbackAdapter;
import awais.instagrabber.customviews.VideoPlayerViewHelper;
import awais.instagrabber.databinding.LayoutExoCustomControlsBinding;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.VideoVersion;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
@ -28,40 +28,23 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
private static final String TAG = "SliderVideoViewHolder";
private final LayoutVideoPlayerWithThumbnailBinding binding;
private final LayoutExoCustomControlsBinding controlsBinding;
private final boolean loadVideoOnItemClick;
private final GestureDetector.OnGestureListener videoPlayerViewGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(final MotionEvent e) {
binding.playerView.performClick();
return true;
}
};
private VideoPlayerViewHelper videoPlayerViewHelper;
@SuppressLint("ClickableViewAccessibility")
public SliderVideoViewHolder(@NonNull final LayoutVideoPlayerWithThumbnailBinding binding,
final VerticalDragHelper.OnVerticalDragListener onVerticalDragListener,
final LayoutExoCustomControlsBinding controlsBinding,
final boolean loadVideoOnItemClick) {
super(binding.getRoot());
this.binding = binding;
this.controlsBinding = controlsBinding;
this.loadVideoOnItemClick = loadVideoOnItemClick;
// if (onVerticalDragListener != null) {
// final VerticalDragHelper thumbnailVerticalDragHelper = new VerticalDragHelper(binding.thumbnailParent);
// final VerticalDragHelper playerVerticalDragHelper = new VerticalDragHelper(binding.playerView);
// thumbnailVerticalDragHelper.setOnVerticalDragListener(onVerticalDragListener);
// playerVerticalDragHelper.setOnVerticalDragListener(onVerticalDragListener);
// binding.thumbnailParent.setOnTouchListener((v, event) -> {
// final boolean onDragTouch = thumbnailVerticalDragHelper.onDragTouch(event);
// if (onDragTouch) {
// return true;
// }
// return thumbnailVerticalDragHelper.onGestureTouchEvent(event);
// });
// }
final GestureDetector.OnGestureListener videoPlayerViewGestureListener = new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(final MotionEvent e) {
binding.playerView.performClick();
return true;
}
};
final GestureDetector gestureDetector = new GestureDetector(itemView.getContext(), videoPlayerViewGestureListener);
binding.playerView.setOnTouchListener((v, event) -> {
gestureDetector.onTouchEvent(event);
@ -72,13 +55,13 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
public void bind(@NonNull final Media media,
final int position,
final SliderItemsAdapter.SliderCallback sliderCallback) {
final float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f;
final float vol = settingsHelper.getBoolean(PreferenceKeys.MUTED_VIDEOS) ? 0f : 1f;
final VideoPlayerViewHelper.VideoPlayerCallback videoPlayerCallback = new VideoPlayerCallbackAdapter() {
@Override
public void onThumbnailClick() {
if (sliderCallback != null) {
sliderCallback.onItemClicked(position);
sliderCallback.onItemClicked(position, media, binding.getRoot());
}
}
@ -121,6 +104,21 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
sliderCallback.onPlayerRelease(position);
}
}
@Override
public void onFullScreenModeChanged(final boolean isFullScreen, final StyledPlayerView playerView) {
if (sliderCallback != null) {
sliderCallback.onFullScreenModeChanged(isFullScreen, playerView);
}
}
@Override
public boolean isInFullScreen() {
if (sliderCallback != null) {
return sliderCallback.isInFullScreen();
}
return false;
}
};
final float aspectRatio = (float) media.getOriginalWidth() / media.getOriginalHeight();
String videoUrl = null;
@ -139,16 +137,10 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
aspectRatio,
ResponseBodyUtils.getThumbUrl(media),
loadVideoOnItemClick,
controlsBinding,
videoPlayerCallback);
// binding.itemFeedBottom.btnMute.setOnClickListener(v -> {
// final float newVol = videoPlayerViewHelper.toggleMute();
// setMuteIcon(newVol);
// Utils.sessionVolumeFull = newVol == 1f;
// });
binding.playerView.setOnClickListener(v -> {
if (sliderCallback != null) {
sliderCallback.onItemClicked(position);
sliderCallback.onItemClicked(position, media, binding.getRoot());
}
});
}
@ -162,62 +154,4 @@ public class SliderVideoViewHolder extends SliderItemViewHolder {
if (videoPlayerViewHelper == null) return;
videoPlayerViewHelper.releasePlayer();
}
public void resetPlayerTimeline() {
if (videoPlayerViewHelper == null) return;
videoPlayerViewHelper.resetTimeline();
}
public void removeCallbacks() {
if (videoPlayerViewHelper == null) return;
videoPlayerViewHelper.removeCallbacks();
}
// private void setDimensions(final FeedModel feedModel, final int spanCount, final boolean animate) {
// final ViewGroup.LayoutParams layoutParams = binding.imageViewer.getLayoutParams();
// final int deviceWidth = Utils.displayMetrics.widthPixels;
// final int spanWidth = deviceWidth / spanCount;
// final int spanHeight = NumberUtils.getResultingHeight(spanWidth, feedModel.getImageHeight(), feedModel.getImageWidth());
// final int width = spanWidth == 0 ? deviceWidth : spanWidth;
// final int height = spanHeight == 0 ? deviceWidth + 1 : spanHeight;
// if (animate) {
// Animation animation = AnimationUtils.expand(
// binding.imageViewer,
// layoutParams.width,
// layoutParams.height,
// width,
// height,
// new Animation.AnimationListener() {
// @Override
// public void onAnimationStart(final Animation animation) {
// showOrHideDetails(spanCount);
// }
//
// @Override
// public void onAnimationEnd(final Animation animation) {
// // showOrHideDetails(spanCount);
// }
//
// @Override
// public void onAnimationRepeat(final Animation animation) {
//
// }
// });
// binding.imageViewer.startAnimation(animation);
// } else {
// layoutParams.width = width;
// layoutParams.height = height;
// binding.imageViewer.requestLayout();
// }
// }
//
// private void showOrHideDetails(final int spanCount) {
// if (spanCount == 1) {
// binding.itemFeedTop.getRoot().setVisibility(View.VISIBLE);
// binding.itemFeedBottom.getRoot().setVisibility(View.VISIBLE);
// } else {
// binding.itemFeedTop.getRoot().setVisibility(View.GONE);
// binding.itemFeedBottom.getRoot().setVisibility(View.GONE);
// }
// }
}

View File

@ -20,7 +20,6 @@ public final class StoryListViewHolder extends RecyclerView.ViewHolder {
}
public void bind(final FeedStoryModel model,
final int position,
final OnFeedStoryClickListener notificationClickListener) {
if (model == null) return;
@ -53,7 +52,7 @@ public final class StoryListViewHolder extends RecyclerView.ViewHolder {
itemView.setOnClickListener(v -> {
if (notificationClickListener == null) return;
notificationClickListener.onFeedStoryClick(model, position);
notificationClickListener.onFeedStoryClick(model);
});
}

View File

@ -54,12 +54,14 @@ public class TopicClusterViewHolder extends RecyclerView.ViewHolder {
if (onTopicClickListener != null) {
itemView.setOnClickListener(v -> onTopicClickListener.onTopicClick(
topicCluster,
binding.getRoot(),
binding.cover,
binding.title,
titleColor.get(),
backgroundColor.get()
));
itemView.setOnLongClickListener(v -> {
onTopicClickListener.onTopicLongClick(topicCluster.getCoverMedia());
return true;
});
}
// binding.title.setTransitionName("title-" + topicCluster.getId());
binding.cover.setTransitionName("cover-" + topicCluster.getId());
@ -126,11 +128,11 @@ public class TopicClusterViewHolder extends RecyclerView.ViewHolder {
backgroundColor.get()
));
}
// binding.title.setTransitionName("title-" + topicCluster.getId());
binding.cover.setTransitionName("cover-" + topicCluster.getId());
final String thumbUrl = ResponseBodyUtils.getThumbUrl(topicCluster.getCoverMedias() == null
// binding.title.setTransitionName("title-" + topicCluster.getCollectionId());
binding.cover.setTransitionName("cover-" + topicCluster.getCollectionId());
final String thumbUrl = ResponseBodyUtils.getThumbUrl(topicCluster.getCoverMediaList() == null
? topicCluster.getCoverMedia()
: topicCluster.getCoverMedias().get(0));
: topicCluster.getCoverMediaList().get(0));
if (thumbUrl == null) {
binding.cover.setImageURI((String) null);
} else {
@ -172,6 +174,6 @@ public class TopicClusterViewHolder extends RecyclerView.ViewHolder {
}, CallerThreadExecutor.getInstance());
binding.cover.setImageRequest(imageRequest);
}
binding.title.setText(topicCluster.getTitle());
binding.title.setText(topicCluster.getCollectionName());
}
}

View File

@ -1,95 +0,0 @@
package awais.instagrabber.adapters.viewholder.comments;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.CommentsAdapter.CommentCallback;
import awais.instagrabber.databinding.ItemCommentSmallBinding;
import awais.instagrabber.models.CommentModel;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.Utils;
public final class ChildCommentViewHolder extends RecyclerView.ViewHolder {
private final ItemCommentSmallBinding binding;
public ChildCommentViewHolder(@NonNull final ItemCommentSmallBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final CommentModel comment,
final boolean selected,
final CommentCallback commentCallback) {
if (comment == null) return;
if (commentCallback != null) {
itemView.setOnClickListener(v -> commentCallback.onClick(comment));
}
if (selected) {
itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_selected));
} else {
itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent));
}
setupCommentText(comment, commentCallback);
binding.tvDate.setText(comment.getDateTime());
setLiked(comment.getLiked());
setLikes((int) comment.getLikes());
setUser(comment);
}
private void setupCommentText(final CommentModel comment, final CommentCallback commentCallback) {
binding.tvComment.clearOnURLClickListeners();
binding.tvComment.clearOnHashtagClickListeners();
binding.tvComment.clearOnMentionClickListeners();
binding.tvComment.clearOnEmailClickListeners();
binding.tvComment.setText(comment.getText());
binding.tvComment.addOnHashtagListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onHashtagClick(originalText);
});
binding.tvComment.addOnMentionClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onMentionClick(originalText);
});
binding.tvComment.addOnEmailClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onEmailClick(originalText);
});
binding.tvComment.addOnURLClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onURLClick(originalText);
});
binding.tvComment.setOnLongClickListener(v -> {
Utils.copyText(itemView.getContext(), comment.getText());
return true;
});
binding.tvComment.setOnClickListener(v -> commentCallback.onClick(comment));
}
private void setUser(final CommentModel comment) {
final User profileModel = comment.getProfileModel();
if (profileModel == null) return;
binding.tvUsername.setText(profileModel.getUsername());
binding.ivProfilePic.setImageURI(profileModel.getProfilePicUrl());
binding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE);
}
private void setLikes(final int likes) {
final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes);
binding.tvLikes.setText(likesString);
}
public final void setLiked(final boolean liked) {
if (liked) {
itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_liked));
}
}
}

View File

@ -1,95 +0,0 @@
package awais.instagrabber.adapters.viewholder.comments;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.CommentsAdapter.CommentCallback;
import awais.instagrabber.databinding.ItemCommentBinding;
import awais.instagrabber.models.CommentModel;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.Utils;
public final class ParentCommentViewHolder extends RecyclerView.ViewHolder {
private final ItemCommentBinding binding;
public ParentCommentViewHolder(@NonNull final ItemCommentBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final CommentModel comment,
final boolean selected,
final CommentCallback commentCallback) {
if (comment == null) return;
if (commentCallback != null) {
itemView.setOnClickListener(v -> commentCallback.onClick(comment));
}
if (selected) {
itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_selected));
} else {
itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent));
}
setupCommentText(comment, commentCallback);
binding.tvDate.setText(comment.getDateTime());
setLiked(comment.getLiked());
setLikes((int) comment.getLikes());
setUser(comment);
}
private void setupCommentText(final CommentModel comment, final CommentCallback commentCallback) {
binding.tvComment.clearOnURLClickListeners();
binding.tvComment.clearOnHashtagClickListeners();
binding.tvComment.clearOnMentionClickListeners();
binding.tvComment.clearOnEmailClickListeners();
binding.tvComment.setText(comment.getText());
binding.tvComment.addOnHashtagListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onHashtagClick(originalText);
});
binding.tvComment.addOnMentionClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onMentionClick(originalText);
});
binding.tvComment.addOnEmailClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onEmailClick(originalText);
});
binding.tvComment.addOnURLClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onURLClick(originalText);
});
binding.tvComment.setOnLongClickListener(v -> {
Utils.copyText(itemView.getContext(), comment.getText());
return true;
});
binding.tvComment.setOnClickListener(v -> commentCallback.onClick(comment));
}
private void setUser(final CommentModel comment) {
final User profileModel = comment.getProfileModel();
if (profileModel == null) return;
binding.tvUsername.setText(profileModel.getUsername());
binding.ivProfilePic.setImageURI(profileModel.getProfilePicUrl());
binding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE);
}
private void setLikes(final int likes) {
final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes);
binding.tvLikes.setText(likesString);
}
public final void setLiked(final boolean liked) {
if (liked) {
itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_liked));
}
}
}

View File

@ -14,7 +14,7 @@ import java.util.HashSet;
import awais.instagrabber.R;
import awais.instagrabber.adapters.KeywordsFilterAdapter;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.utils.SettingsHelper;
public class KeywordsFilterDialogViewHolder extends RecyclerView.ViewHolder {
@ -34,7 +34,7 @@ public class KeywordsFilterDialogViewHolder extends RecyclerView.ViewHolder {
final String s = items.get(position);
SettingsHelper settingsHelper = new SettingsHelper(context);
items.remove(position);
settingsHelper.putStringSet(Constants.KEYWORD_FILTERS, new HashSet<>(items));
settingsHelper.putStringSet(PreferenceKeys.KEYWORD_FILTERS, new HashSet<>(items));
adapter.notifyDataSetChanged();
final String message = context.getString(R.string.removed_keywords, s);
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();

View File

@ -17,11 +17,9 @@ import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.DirectMessageInboxAdapter.OnItemClickListener;
import awais.instagrabber.databinding.LayoutDmInboxItemBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadDirectStory;
import awais.instagrabber.utils.DMUtils;
import awais.instagrabber.utils.TextUtils;
@ -136,7 +134,7 @@ public final class DirectInboxItemViewHolder extends RecyclerView.ViewHolder {
private void setDateTime(@NonNull final DirectItem item) {
final long timestamp = item.getTimestamp() / 1000;
final String dateTimeString = TextUtils.getRelativeDateTimeString(itemView.getContext(), timestamp);
final String dateTimeString = TextUtils.getRelativeDateTimeString(timestamp);
binding.tvDate.setText(dateTimeString);
}

View File

@ -22,6 +22,7 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemActionLog;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.TextRange;
import awais.instagrabber.utils.TextUtils;
public class DirectItemActionLogViewHolder extends DirectItemViewHolder {
@ -45,16 +46,16 @@ public class DirectItemActionLogViewHolder extends DirectItemViewHolder {
final DirectItemActionLog actionLog = directItemModel.getActionLog();
final String text = actionLog.getDescription();
final SpannableStringBuilder sb = new SpannableStringBuilder(text);
final List<DirectItemActionLog.TextRange> bold = actionLog.getBold();
final List<TextRange> bold = actionLog.getBold();
if (bold != null && !bold.isEmpty()) {
for (final DirectItemActionLog.TextRange textRange : bold) {
for (final TextRange textRange : bold) {
final StyleSpan boldStyleSpan = new StyleSpan(Typeface.BOLD);
sb.setSpan(boldStyleSpan, textRange.getStart(), textRange.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
}
}
final List<DirectItemActionLog.TextRange> textAttributes = actionLog.getTextAttributes();
final List<TextRange> textAttributes = actionLog.getTextAttributes();
if (textAttributes != null && !textAttributes.isEmpty()) {
for (final DirectItemActionLog.TextRange textAttribute : textAttributes) {
for (final TextRange textAttribute : textAttributes) {
if (!TextUtils.isEmpty(textAttribute.getColor())) {
final ForegroundColorSpan colorSpan = new ForegroundColorSpan(itemView.getResources().getColor(R.color.deep_orange_400));
sb.setSpan(colorSpan, textAttribute.getStart(), textAttribute.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);

View File

@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.backends.pipeline.Fresco;
@ -23,6 +22,7 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemAnimatedMedia;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.Utils;
@ -48,7 +48,7 @@ public class DirectItemAnimatedMediaViewHolder extends DirectItemViewHolder {
final AnimatedMediaFixedHeight fixedHeight = images.getFixedHeight();
if (fixedHeight == null) return;
final String url = fixedHeight.getWebp();
final Pair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
final NullSafePair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
fixedHeight.getHeight(),
fixedHeight.getWidth(),
mediaImageMaxHeight,
@ -56,8 +56,8 @@ public class DirectItemAnimatedMediaViewHolder extends DirectItemViewHolder {
);
binding.ivAnimatedMessage.setVisibility(View.VISIBLE);
final ViewGroup.LayoutParams layoutParams = binding.ivAnimatedMessage.getLayoutParams();
final int width = widthHeight.first != null ? widthHeight.first : 0;
final int height = widthHeight.second != null ? widthHeight.second : 0;
final int width = widthHeight.first;
final int height = widthHeight.second;
layoutParams.width = width;
layoutParams.height = height;
binding.ivAnimatedMessage.requestLayout();

View File

@ -6,7 +6,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.drawable.ScalingUtils;
@ -31,6 +30,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemClip;
import awais.instagrabber.repositories.responses.directmessages.DirectItemFelixShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
@ -103,15 +103,15 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder {
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.setRoundingParams(roundingParams)
.build());
final Pair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
final NullSafePair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.mediaPreview.getLayoutParams();
layoutParams.width = widthHeight.first != null ? widthHeight.first : 0;
layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
layoutParams.width = widthHeight.first;
layoutParams.height = widthHeight.second;
binding.mediaPreview.requestLayout();
binding.mediaPreview.setTag(url);
binding.mediaPreview.setImageURI(url);

View File

@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
@ -14,11 +13,11 @@ import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmMediaBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.ImageVersions2;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
@ -53,16 +52,16 @@ public class DirectItemMediaViewHolder extends DirectItemViewHolder {
binding.typeIcon.setVisibility(modelMediaType == MediaItemType.MEDIA_TYPE_VIDEO || modelMediaType == MediaItemType.MEDIA_TYPE_SLIDER
? View.VISIBLE
: View.GONE);
final Pair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
final NullSafePair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.mediaPreview.getLayoutParams();
final int width = widthHeight.first != null ? widthHeight.first : 0;
final int width = widthHeight.first;
layoutParams.width = width;
layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
layoutParams.height = widthHeight.second;
binding.mediaPreview.requestLayout();
binding.bgTime.getLayoutParams().width = width;
binding.bgTime.requestLayout();

View File

@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
@ -21,6 +20,7 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
@ -170,15 +170,15 @@ public class DirectItemRavenMediaViewHolder extends DirectItemViewHolder {
binding.typeIcon.setVisibility(modelMediaType == MediaItemType.MEDIA_TYPE_VIDEO || modelMediaType == MediaItemType.MEDIA_TYPE_SLIDER
? View.VISIBLE
: View.GONE);
final Pair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
final NullSafePair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
media.getOriginalHeight(),
media.getOriginalWidth(),
mediaImageMaxHeight,
maxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.preview.getLayoutParams();
layoutParams.width = widthHeight.first != null ? widthHeight.first : 0;
layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
layoutParams.width = widthHeight.first;
layoutParams.height = widthHeight.second;
binding.preview.requestLayout();
final String thumbUrl = ResponseBodyUtils.getThumbUrl(media);
binding.preview.setImageURI(thumbUrl);

View File

@ -5,7 +5,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.drawable.ScalingUtils;
@ -17,12 +16,12 @@ import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmStoryShareBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.ImageVersions2;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
@ -76,15 +75,15 @@ public class DirectItemStoryShareViewHolder extends DirectItemViewHolder {
.setRoundingParams(roundingParams)
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
.build());
final Pair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
final NullSafePair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
storyShareMedia.getOriginalHeight(),
storyShareMedia.getOriginalWidth(),
mediaImageMaxHeight,
mediaImageMaxWidth
);
final ViewGroup.LayoutParams layoutParams = binding.ivMediaPreview.getLayoutParams();
layoutParams.width = widthHeight.first != null ? widthHeight.first : 0;
layoutParams.height = widthHeight.second != null ? widthHeight.second : 0;
layoutParams.width = widthHeight.first;
layoutParams.height = widthHeight.second;
binding.ivMediaPreview.requestLayout();
final String thumbUrl = ResponseBodyUtils.getThumbUrl(storyShareMedia);
binding.ivMediaPreview.setImageURI(thumbUrl);

View File

@ -17,9 +17,9 @@ import awais.instagrabber.databinding.LayoutDmActionLogBinding;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemActionLog;
import awais.instagrabber.repositories.responses.directmessages.DirectItemVideoCallEvent;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.TextRange;
import awais.instagrabber.utils.TextUtils;
public class DirectItemVideoCallEventViewHolder extends DirectItemViewHolder {
@ -41,9 +41,9 @@ public class DirectItemVideoCallEventViewHolder extends DirectItemViewHolder {
final DirectItemVideoCallEvent videoCallEvent = directItemModel.getVideoCallEvent();
final String text = videoCallEvent.getDescription();
final SpannableStringBuilder sb = new SpannableStringBuilder(text);
final List<DirectItemActionLog.TextRange> textAttributes = videoCallEvent.getTextAttributes();
final List<TextRange> textAttributes = videoCallEvent.getTextAttributes();
if (textAttributes != null && !textAttributes.isEmpty()) {
for (final DirectItemActionLog.TextRange textAttribute : textAttributes) {
for (final TextRange textAttribute : textAttributes) {
if (!TextUtils.isEmpty(textAttribute.getColor())) {
final ForegroundColorSpan colorSpan = new ForegroundColorSpan(itemView.getResources().getColor(R.color.deep_orange_400));
sb.setSpan(colorSpan, textAttribute.getStart(), textAttribute.getEnd(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);

View File

@ -5,7 +5,6 @@ import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.text.format.DateFormat;
import android.view.Gravity;
import android.view.View;
import android.view.ViewConfiguration;
@ -25,6 +24,9 @@ import androidx.transition.TransitionManager;
import com.google.android.material.transition.MaterialFade;
import com.google.common.collect.ImmutableList;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
@ -144,7 +146,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
}
setupReply(item, messageDirection);
setReactions(item, position);
if (item.getRepliedToMessage() == null && item.showForwardAttribution()) {
if (item.getRepliedToMessage() == null && item.getShowForwardAttribution()) {
setForwardInfo(messageDirection);
}
}
@ -163,7 +165,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
binding.ivProfilePic.setVisibility(messageDirection == MessageDirection.INCOMING && thread.isGroup() ? View.VISIBLE : View.GONE);
binding.tvUsername.setVisibility(messageDirection == MessageDirection.INCOMING && thread.isGroup() ? View.VISIBLE : View.GONE);
if (messageDirection == MessageDirection.INCOMING && thread.isGroup()) {
final User user = getUser(item.getUserId(), thread.getUsers());
final List<User> allUsers = new LinkedList(thread.getUsers());
allUsers.addAll(thread.getLeftUsers());
final User user = getUser(item.getUserId(), allUsers);
if (user != null) {
binding.tvUsername.setText(user.getUsername());
binding.ivProfilePic.setImageURI(user.getProfilePicUrl());
@ -193,7 +197,10 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
if (showMessageInfo()) {
binding.messageInfo.setVisibility(View.VISIBLE);
binding.deliveryStatus.setVisibility(messageDirection == MessageDirection.OUTGOING ? View.VISIBLE : View.GONE);
binding.messageTime.setText(DateFormat.getTimeFormat(itemView.getContext()).format(item.getDate()));
if (item.getDate() != null) {
final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
binding.messageTime.setText(dateFormatter.format(item.getDate()));
}
if (messageDirection == MessageDirection.OUTGOING) {
if (item.isPending()) {
binding.deliveryStatus.setImageResource(R.drawable.ic_check_24);
@ -216,7 +223,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
private void setupReply(final DirectItem item, final MessageDirection messageDirection) {
if (item.getRepliedToMessage() != null) {
setReply(item, messageDirection, thread.getUsers());
final List<User> allUsers = new LinkedList(thread.getUsers());
allUsers.addAll(thread.getLeftUsers());
setReply(item, messageDirection, allUsers);
} else {
binding.quoteLine.setVisibility(View.GONE);
binding.replyContainer.setVisibility(View.GONE);
@ -551,6 +560,10 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple
menu.setOnDismissListener(() -> setSelected(false));
menu.setOnReactionClickListener(emoji -> callback.onReaction(item, emoji));
menu.setOnOptionSelectListener((itemId, cb) -> callback.onOptionSelect(item, itemId, cb));
menu.setOnAddReactionListener(() -> {
menu.dismiss();
itemView.postDelayed(() -> callback.onAddReactionListener(item), 300);
});
menu.show(itemView, location);
}

View File

@ -4,7 +4,6 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.ItemTouchHelper;
import com.facebook.drawee.backends.pipeline.Fresco;
@ -16,6 +15,8 @@ import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemXma;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.XmaUrlInfo;
import awais.instagrabber.utils.NullSafePair;
import awais.instagrabber.utils.NumberUtils;
public class DirectItemXmaViewHolder extends DirectItemViewHolder {
@ -35,15 +36,15 @@ public class DirectItemXmaViewHolder extends DirectItemViewHolder {
@Override
public void bindItem(final DirectItem item, final MessageDirection messageDirection) {
final DirectItemXma xma = item.getXma();
final DirectItemXma.XmaUrlInfo playableUrlInfo = xma.getPlayableUrlInfo();
final DirectItemXma.XmaUrlInfo previewUrlInfo = xma.getPreviewUrlInfo();
final XmaUrlInfo playableUrlInfo = xma.getPlayableUrlInfo();
final XmaUrlInfo previewUrlInfo = xma.getPreviewUrlInfo();
if (playableUrlInfo == null && previewUrlInfo == null) {
binding.ivAnimatedMessage.setController(null);
return;
}
final DirectItemXma.XmaUrlInfo urlInfo = playableUrlInfo != null ? playableUrlInfo : previewUrlInfo;
final XmaUrlInfo urlInfo = playableUrlInfo != null ? playableUrlInfo : previewUrlInfo;
final String url = urlInfo.getUrl();
final Pair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
final NullSafePair<Integer, Integer> widthHeight = NumberUtils.calculateWidthHeight(
urlInfo.getHeight(),
urlInfo.getWidth(),
mediaImageMaxHeight,
@ -51,8 +52,8 @@ public class DirectItemXmaViewHolder extends DirectItemViewHolder {
);
binding.ivAnimatedMessage.setVisibility(View.VISIBLE);
final ViewGroup.LayoutParams layoutParams = binding.ivAnimatedMessage.getLayoutParams();
final int width = widthHeight.first != null ? widthHeight.first : 0;
final int height = widthHeight.second != null ? widthHeight.second : 0;
final int width = widthHeight.first;
final int height = widthHeight.second;
layoutParams.width = width;
layoutParams.height = height;
binding.ivAnimatedMessage.requestLayout();

View File

@ -31,7 +31,7 @@ public class DirectReactionViewHolder extends RecyclerView.ViewHolder {
this.onReactionClickListener = onReactionClickListener;
binding.info.setVisibility(View.GONE);
binding.secondaryImage.setVisibility(View.VISIBLE);
emojiParser = EmojiParser.getInstance();
emojiParser = EmojiParser.Companion.getInstance(itemView.getContext());
}
public void bind(final DirectItemEmojiReaction reaction,

View File

@ -37,7 +37,7 @@ public class RecipientThreadViewHolder extends RecyclerView.ViewHolder {
final DirectThread thread,
final boolean showSelection,
final boolean isSelected) {
if (thread == null) return;
if (thread == null || thread.getUsers().size() == 0) return;
binding.getRoot().setOnClickListener(v -> {
if (onThreadClickListener == null) return;
onThreadClickListener.onClick(position, RankedRecipient.of(thread), isSelected);

View File

@ -55,7 +55,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
private void setupComments(@NonNull final Media feedModel) {
final long commentsCount = feedModel.getCommentCount();
bottomBinding.commentsCount.setText(String.valueOf(commentsCount));
bottomBinding.commentsCount.setOnClickListener(v -> feedItemCallback.onCommentsClick(feedModel));
bottomBinding.btnComments.setOnClickListener(v -> feedItemCallback.onCommentsClick(feedModel));
}
private void setupProfilePic(@NonNull final Media media) {
@ -75,6 +75,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
// final SpannableString spannableString = new SpannableString();
// spannableString.setSpan(new CommentMentionClickSpan(), 0, titleLen, 0);
final User user = media.getUser();
if (user == null) return;
final String title = "@" + user.getUsername();
topBinding.title.setText(title);
topBinding.title.setOnClickListener(v -> feedItemCallback.onNameClick(media, topBinding.ivProfilePic));
@ -120,8 +121,7 @@ public abstract class FeedItemViewHolder extends RecyclerView.ViewHolder {
topBinding.title.setLayoutParams(new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT
));
}
else {
} else {
final String locationName = location.getName();
if (TextUtils.isEmpty(locationName)) {
topBinding.location.setVisibility(View.GONE);

View File

@ -45,9 +45,9 @@ public class FeedSliderViewHolder extends FeedItemViewHolder {
final String text = "1/" + sliderItemLen;
binding.mediaCounter.setText(text);
binding.mediaList.setOffscreenPageLimit(1);
final SliderItemsAdapter adapter = new SliderItemsAdapter(null, null, false, new SliderCallbackAdapter() {
final SliderItemsAdapter adapter = new SliderItemsAdapter(false, new SliderCallbackAdapter() {
@Override
public void onItemClicked(final int position) {
public void onItemClicked(final int position, final Media media, final View view) {
feedItemCallback.onSliderClick(feedModel, position);
}
});

View File

@ -18,9 +18,9 @@ import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.customviews.VideoPlayerCallbackAdapter;
import awais.instagrabber.customviews.VideoPlayerViewHelper;
import awais.instagrabber.databinding.ItemFeedVideoBinding;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.VideoVersion;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.NumberUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
@ -65,7 +65,7 @@ public class FeedVideoViewHolder extends FeedItemViewHolder {
// Log.d(TAG, "Binding post: " + feedModel.getPostId());
this.media = media;
binding.itemFeedBottom.tvVideoViews.setText(String.valueOf(media.getViewCount()));
final float vol = settingsHelper.getBoolean(Constants.MUTED_VIDEOS) ? 0f : 1f;
final float vol = settingsHelper.getBoolean(PreferenceKeys.MUTED_VIDEOS) ? 0f : 1f;
final VideoPlayerViewHelper.VideoPlayerCallback videoPlayerCallback = new VideoPlayerCallbackAdapter() {
@Override
@ -97,7 +97,7 @@ public class FeedVideoViewHolder extends FeedItemViewHolder {
aspectRatio,
ResponseBodyUtils.getThumbUrl(media),
false,
null,
// null,
videoPlayerCallback);
binding.videoPost.thumbnail.post(() -> {
if (media.getOriginalHeight() > 0.8 * Utils.displayMetrics.heightPixels) {

View File

@ -1,268 +0,0 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.CommentModel;
import awais.instagrabber.repositories.responses.FriendshipStatus;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.NetworkUtils;
import awais.instagrabber.utils.TextUtils;
//import awaisomereport.LogCollector;
//import static awais.instagrabber.utils.Utils.logCollector;
public final class CommentsFetcher extends AsyncTask<Void, Void, List<CommentModel>> {
private static final String TAG = "CommentsFetcher";
private final String shortCode, endCursor;
private final FetchListener<List<CommentModel>> fetchListener;
public CommentsFetcher(final String shortCode, final String endCursor, final FetchListener<List<CommentModel>> fetchListener) {
this.shortCode = shortCode;
this.endCursor = endCursor;
this.fetchListener = fetchListener;
}
@NonNull
@Override
protected List<CommentModel> doInBackground(final Void... voids) {
/*
"https://www.instagram.com/graphql/query/?query_hash=97b41c52301f77ce508f55e66d17620e&variables=" + "{\"shortcode\":\"" + shortcode + "\",\"first\":50,\"after\":\"" + endCursor + "\"}";
97b41c52301f77ce508f55e66d17620e -> for comments
51fdd02b67508306ad4484ff574a0b62 -> for child comments
https://www.instagram.com/graphql/query/?query_hash=51fdd02b67508306ad4484ff574a0b62&variables={"comment_id":"18100041898085322","first":50,"after":""}
*/
final List<CommentModel> commentModels = getParentComments();
if (commentModels != null) {
for (final CommentModel commentModel : commentModels) {
final List<CommentModel> childCommentModels = commentModel.getChildCommentModels();
if (childCommentModels != null) {
final int childCommentsLen = childCommentModels.size();
final CommentModel lastChild = childCommentModels.get(childCommentsLen - 1);
if (lastChild != null && lastChild.hasNextPage() && !TextUtils.isEmpty(lastChild.getEndCursor())) {
final List<CommentModel> remoteChildComments = getChildComments(commentModel.getId());
commentModel.setChildCommentModels(remoteChildComments);
lastChild.setPageCursor(false, null);
}
}
}
}
return commentModels;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final List<CommentModel> result) {
if (fetchListener != null) fetchListener.onResult(result);
}
@NonNull
private synchronized List<CommentModel> getChildComments(final String commentId) {
final List<CommentModel> commentModels = new ArrayList<>();
String childEndCursor = "";
while (childEndCursor != null) {
final String url = "https://www.instagram.com/graphql/query/?query_hash=51fdd02b67508306ad4484ff574a0b62&variables=" +
"{\"comment_id\":\"" + commentId + "\",\"first\":50,\"after\":\"" + childEndCursor + "\"}";
try {
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) break;
else {
final JSONObject data = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("data")
.getJSONObject("comment")
.getJSONObject("edge_threaded_comments");
final JSONObject pageInfo = data.getJSONObject("page_info");
childEndCursor = pageInfo.getString("end_cursor");
if (TextUtils.isEmpty(childEndCursor)) childEndCursor = null;
final JSONArray childComments = data.optJSONArray("edges");
if (childComments != null) {
final int length = childComments.length();
for (int i = 0; i < length; ++i) {
final JSONObject childComment = childComments.getJSONObject(i).optJSONObject("node");
if (childComment != null) {
final JSONObject owner = childComment.getJSONObject("owner");
final User user = new User(
owner.optLong(Constants.EXTRAS_ID, 0),
owner.getString(Constants.EXTRAS_USERNAME),
null,
false,
owner.getString("profile_pic_url"),
null,
new FriendshipStatus(false, false, false, false, false, false, false, false, false, false),
false, false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, null,
null, null, null);
final JSONObject likedBy = childComment.optJSONObject("edge_liked_by");
commentModels.add(new CommentModel(childComment.getString(Constants.EXTRAS_ID),
childComment.getString("text"),
childComment.getLong("created_at"),
likedBy != null ? likedBy.optLong("count", 0) : 0,
childComment.getBoolean("viewer_has_liked"),
user));
}
}
}
}
conn.disconnect();
} catch (final Exception e) {
// if (logCollector != null)
// logCollector.appendException(e,
// LogCollector.LogFile.ASYNC_COMMENTS_FETCHER,
// "getChildComments",
// new Pair<>("commentModels.size", commentModels.size()));
if (BuildConfig.DEBUG) Log.e(TAG, "", e);
if (fetchListener != null) fetchListener.onFailure(e);
break;
}
}
return commentModels;
}
@NonNull
private synchronized List<CommentModel> getParentComments() {
final List<CommentModel> commentModels = new ArrayList<>();
final String url = "https://www.instagram.com/graphql/query/?query_hash=bc3296d1ce80a24b1b6e40b1e72903f5&variables=" +
"{\"shortcode\":\"" + shortCode + "\",\"first\":50,\"after\":\"" + endCursor.replace("\"", "\\\"") + "\"}";
try {
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) return null;
else {
final JSONObject parentComments = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("data")
.getJSONObject("shortcode_media")
.getJSONObject(
"edge_media_to_parent_comment");
final JSONObject pageInfo = parentComments.getJSONObject("page_info");
final String foundEndCursor = pageInfo.optString("end_cursor");
final boolean hasNextPage = pageInfo.optBoolean("has_next_page", !TextUtils.isEmpty(foundEndCursor));
// final boolean containsToken = endCursor.contains("bifilter_token");
// if (!Utils.isEmpty(endCursor) && (containsToken || endCursor.contains("cached_comments_cursor"))) {
// final JSONObject endCursorObject = new JSONObject(endCursor);
// endCursor = endCursorObject.optString("cached_comments_cursor");
//
// if (!Utils.isEmpty(endCursor))
// endCursor = "{\\\"cached_comments_cursor\\\": \\\"" + endCursor + "\\\", ";
// else
// endCursor = "{";
//
// endCursor = endCursor + "\\\"bifilter_token\\\": \\\"" + endCursorObject.getString("bifilter_token") + "\\\"}";
// }
// else if (containsToken) endCursor = null;
final JSONArray comments = parentComments.getJSONArray("edges");
final int commentsLen = comments.length();
for (int i = 0; i < commentsLen; ++i) {
final JSONObject comment = comments.getJSONObject(i).getJSONObject("node");
final JSONObject owner = comment.getJSONObject("owner");
final User user = new User(
owner.optLong(Constants.EXTRAS_ID, 0),
owner.getString(Constants.EXTRAS_USERNAME),
null,
false,
owner.getString("profile_pic_url"),
null,
new FriendshipStatus(false, false, false, false, false, false, false, false, false, false),
owner.optBoolean("is_verified"),
false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, null, null,
null, null);
final JSONObject likedBy = comment.optJSONObject("edge_liked_by");
final String commentId = comment.getString(Constants.EXTRAS_ID);
final CommentModel commentModel = new CommentModel(commentId,
comment.getString("text"),
comment.getLong("created_at"),
likedBy != null ? likedBy.optLong("count", 0) : 0,
comment.getBoolean("viewer_has_liked"),
user);
if (i == 0 && !foundEndCursor.contains("tao_cursor"))
commentModel.setPageCursor(hasNextPage, TextUtils.isEmpty(foundEndCursor) ? null : foundEndCursor);
JSONObject tempJsonObject;
final JSONArray childCommentsArray;
final int childCommentsLen;
if ((tempJsonObject = comment.optJSONObject("edge_threaded_comments")) != null &&
(childCommentsArray = tempJsonObject.optJSONArray("edges")) != null
&& (childCommentsLen = childCommentsArray.length()) > 0) {
final String childEndCursor;
final boolean childHasNextPage;
if ((tempJsonObject = tempJsonObject.optJSONObject("page_info")) != null) {
childEndCursor = tempJsonObject.optString("end_cursor");
childHasNextPage = tempJsonObject.optBoolean("has_next_page", !TextUtils.isEmpty(childEndCursor));
} else {
childEndCursor = null;
childHasNextPage = false;
}
final List<CommentModel> childCommentModels = new ArrayList<>();
for (int j = 0; j < childCommentsLen; ++j) {
final JSONObject childComment = childCommentsArray.getJSONObject(j).getJSONObject("node");
tempJsonObject = childComment.getJSONObject("owner");
final User childUser = new User(
tempJsonObject.optLong(Constants.EXTRAS_ID, 0),
tempJsonObject.getString(Constants.EXTRAS_USERNAME),
null,
false,
tempJsonObject.getString("profile_pic_url"),
null,
new FriendshipStatus(false, false, false, false, false, false, false, false, false, false),
tempJsonObject.optBoolean("is_verified"), false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0,
null, null, null, null, null, null);
tempJsonObject = childComment.optJSONObject("edge_liked_by");
childCommentModels.add(new CommentModel(childComment.getString(Constants.EXTRAS_ID),
childComment.getString("text"),
childComment.getLong("created_at"),
tempJsonObject != null ? tempJsonObject.optLong("count", 0) : 0,
childComment.getBoolean("viewer_has_liked"),
childUser));
}
childCommentModels.get(childCommentsLen - 1).setPageCursor(childHasNextPage, childEndCursor);
commentModel.setChildCommentModels(childCommentModels);
}
commentModels.add(commentModel);
}
}
conn.disconnect();
} catch (final Exception e) {
// if (logCollector != null)
// logCollector.appendException(e, LogCollector.LogFile.ASYNC_COMMENTS_FETCHER, "getParentComments",
// new Pair<>("commentModelsList.size", commentModels.size()));
if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e);
if (fetchListener != null) fetchListener.onFailure(e);
return null;
}
return commentModels;
}
}

View File

@ -1,80 +0,0 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.util.Collections;
import java.util.Locale;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.DirectMessagesService;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class CreateThreadAction extends AsyncTask<Void, Void, Void> {
private static final String TAG = "CommentAction";
private final String cookie;
private final long userId;
private final OnTaskCompleteListener onTaskCompleteListener;
private final DirectMessagesService directMessagesService;
public CreateThreadAction(final String cookie, final long userId, final OnTaskCompleteListener onTaskCompleteListener) {
this.cookie = cookie;
this.userId = userId;
this.onTaskCompleteListener = onTaskCompleteListener;
directMessagesService = DirectMessagesService.getInstance(CookieUtils.getCsrfTokenFromCookie(cookie),
CookieUtils.getUserIdFromCookie(cookie),
Utils.settingsHelper.getString(Constants.DEVICE_UUID));
}
protected Void doInBackground(Void... lmao) {
final Call<DirectThread> createThreadRequest = directMessagesService.createThread(Collections.singletonList(userId), null);
createThreadRequest.enqueue(new Callback<DirectThread>() {
@Override
public void onResponse(@NonNull final Call<DirectThread> call, @NonNull final Response<DirectThread> response) {
if (!response.isSuccessful()) {
if (response.errorBody() != null) {
try {
final String string = response.errorBody().string();
final String msg = String.format(Locale.US,
"onResponse: url: %s, responseCode: %d, errorBody: %s",
call.request().url().toString(),
response.code(),
string);
Log.e(TAG, msg);
} catch (IOException e) {
Log.e(TAG, "onResponse: ", e);
}
}
Log.e(TAG, "onResponse: request was not successful and response error body was null");
}
onTaskCompleteListener.onTaskComplete(response.body());
if (response.body() == null) {
Log.e(TAG, "onResponse: thread is null");
}
}
@Override
public void onFailure(@NonNull final Call<DirectThread> call, @NonNull final Throwable t) {
onTaskCompleteListener.onTaskComplete(null);
}
});
return null;
}
// @Override
// protected void onPostExecute() {
// }
public interface OnTaskCompleteListener {
void onTaskComplete(final DirectThread thread);
}
}

View File

@ -1,42 +0,0 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.DownloadUtils;
public final class DownloadedCheckerAsyncTask extends AsyncTask<Media, Void, Map<String, List<Boolean>>> {
private static final String TAG = "DownloadedCheckerAsyncTask";
private final OnCheckResultListener listener;
public DownloadedCheckerAsyncTask(final OnCheckResultListener listener) {
this.listener = listener;
}
@Override
protected Map<String, List<Boolean>> doInBackground(final Media... feedModels) {
if (feedModels == null) {
return null;
}
final Map<String, List<Boolean>> map = new HashMap<>();
for (final Media media : feedModels) {
map.put(media.getPk(), DownloadUtils.checkDownloaded(media));
}
return map;
}
@Override
protected void onPostExecute(final Map<String, List<Boolean>> result) {
if (listener == null) return;
listener.onResult(result);
}
public interface OnCheckResultListener {
void onResult(final Map<String, List<Boolean>> result);
}
}

View File

@ -7,13 +7,15 @@ import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Hashtag;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.TagsService;
import kotlinx.coroutines.Dispatchers;
public class HashtagPostFetchService implements PostFetcher.PostFetchService {
private final TagsService tagsService;
private final GraphQLService graphQLService;
private final GraphQLRepository graphQLRepository;
private final Hashtag hashtagModel;
private String nextMaxId;
private boolean moreAvailable;
@ -23,7 +25,7 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService {
this.hashtagModel = hashtagModel;
this.isLoggedIn = isLoggedIn;
tagsService = isLoggedIn ? TagsService.getInstance() : null;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
}
@Override
@ -48,7 +50,17 @@ public class HashtagPostFetchService implements PostFetcher.PostFetchService {
}
};
if (isLoggedIn) tagsService.fetchPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb);
else graphQLService.fetchHashtagPosts(hashtagModel.getName().toLowerCase(), nextMaxId, cb);
else graphQLRepository.fetchHashtagPosts(
hashtagModel.getName().toLowerCase(),
nextMaxId,
CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(postsFetchResponse);
}, Dispatchers.getIO())
);
}
@Override

View File

@ -7,13 +7,15 @@ import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Location;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.LocationService;
import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
public class LocationPostFetchService implements PostFetcher.PostFetchService {
private final LocationService locationService;
private final GraphQLService graphQLService;
private final GraphQLRepository graphQLRepository;
private final Location locationModel;
private String nextMaxId;
private boolean moreAvailable;
@ -23,7 +25,7 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService {
this.locationModel = locationModel;
this.isLoggedIn = isLoggedIn;
locationService = isLoggedIn ? LocationService.getInstance() : null;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
}
@Override
@ -48,7 +50,17 @@ public class LocationPostFetchService implements PostFetcher.PostFetchService {
}
};
if (isLoggedIn) locationService.fetchPosts(locationModel.getPk(), nextMaxId, cb);
else graphQLService.fetchLocationPosts(locationModel.getPk(), nextMaxId, cb);
else graphQLRepository.fetchLocationPosts(
locationModel.getPk(),
nextMaxId,
CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(postsFetchResponse);
}, Dispatchers.getIO())
);
}
@Override

View File

@ -1,160 +0,0 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.NetworkUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
//import awaisomereport.LogCollector;
//import static awais.instagrabber.utils.Utils.logCollector;
public final class PostFetcher extends AsyncTask<Void, Void, Media> {
private static final String TAG = "PostFetcher";
private final String shortCode;
private final FetchListener<Media> fetchListener;
public PostFetcher(final String shortCode, final FetchListener<Media> fetchListener) {
this.shortCode = shortCode;
this.fetchListener = fetchListener;
}
@Override
protected Media doInBackground(final Void... voids) {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL("https://www.instagram.com/p/" + shortCode + "/?__a=1").openConnection();
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
final JSONObject media = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("graphql")
.getJSONObject("shortcode_media");
// ProfileModel profileModel = null;
// if (media.has("owner")) {
// final JSONObject owner = media.getJSONObject("owner");
// profileModel = new ProfileModel(
// owner.optBoolean("is_private"),
// owner.optBoolean("is_private"),
// owner.optBoolean("is_verified"),
// owner.optString("id"),
// owner.optString("username"),
// owner.optString("full_name"),
// null,
// null,
// owner.optString("profile_pic_url"),
// owner.optString("profile_pic_url"),
// owner.optInt("edge_owner_to_timeline_media"),
// owner.optInt("edge_followed_by"),
// -1,
// owner.optBoolean("followed_by_viewer"),
// false,
// owner.optBoolean("restricted_by_viewer"),
// owner.optBoolean("blocked_by_viewer"),
// owner.optBoolean("requested_by_viewer")
// );
// }
// final long timestamp = media.getLong("taken_at_timestamp");
//
// final boolean isVideo = media.has("is_video") && media.optBoolean("is_video");
// final boolean isSlider = media.has("edge_sidecar_to_children");
//
// final MediaItemType mediaItemType;
// if (isSlider) mediaItemType = MediaItemType.MEDIA_TYPE_SLIDER;
// else if (isVideo) mediaItemType = MediaItemType.MEDIA_TYPE_VIDEO;
// else mediaItemType = MediaItemType.MEDIA_TYPE_IMAGE;
//
// final String postCaption;
// final JSONObject mediaToCaption = media.optJSONObject("edge_media_to_caption");
// if (mediaToCaption == null) postCaption = null;
// else {
// final JSONArray captions = mediaToCaption.optJSONArray("edges");
// postCaption = captions != null && captions.length() > 0 ?
// captions.getJSONObject(0).getJSONObject("node").optString("text") : null;
// }
//
// JSONObject commentObject = media.optJSONObject("edge_media_to_parent_comment");
// final long commentsCount = commentObject != null ? commentObject.optLong("count") : 0;
// final FeedModel.Builder feedModelBuilder = new FeedModel.Builder()
// .setItemType(mediaItemType)
// .setPostId(media.getString(Constants.EXTRAS_ID))
// .setDisplayUrl(isVideo ? media.getString("video_url")
// : ResponseBodyUtils.getHighQualityImage(media))
// .setThumbnailUrl(media.getString("display_url"))
// .setImageHeight(media.getJSONObject("dimensions").getInt("height"))
// .setImageWidth(media.getJSONObject("dimensions").getInt("width"))
// .setShortCode(shortCode)
// .setPostCaption(TextUtils.isEmpty(postCaption) ? null : postCaption)
// .setProfileModel(profileModel)
// .setViewCount(isVideo && media.has("video_view_count")
// ? media.getLong("video_view_count")
// : -1)
// .setTimestamp(timestamp)
// .setLiked(media.getBoolean("viewer_has_liked"))
// .setBookmarked(media.getBoolean("viewer_has_saved"))
// .setLikesCount(media.getJSONObject("edge_media_preview_like")
// .getLong("count"))
// .setLocationName(media.isNull("location")
// ? null
// : media.getJSONObject("location").optString("name"))
// .setLocationId(media.isNull("location")
// ? null
// : media.getJSONObject("location").optString("id"))
// .setCommentsCount(commentsCount);
// if (isSlider) {
// final JSONArray children = media.getJSONObject("edge_sidecar_to_children").getJSONArray("edges");
// final List<PostChild> postModels = new ArrayList<>();
// for (int i = 0; i < children.length(); ++i) {
// final JSONObject childNode = children.getJSONObject(i).getJSONObject("node");
// final boolean isChildVideo = childNode.getBoolean("is_video");
// postModels.add(new PostChild.Builder()
// .setItemType(isChildVideo ? MediaItemType.MEDIA_TYPE_VIDEO
// : MediaItemType.MEDIA_TYPE_IMAGE)
// .setDisplayUrl(isChildVideo ? childNode.getString("video_url")
// : childNode.getString("display_url"))
// .setShortCode(media.getString(Constants.EXTRAS_SHORTCODE))
// .setVideoViews(isChildVideo && childNode.has("video_view_count")
// ? childNode.getLong("video_view_count")
// : -1)
// .setThumbnailUrl(childNode.getString("display_url"))
// .setHeight(childNode.getJSONObject("dimensions").getInt("height"))
// .setWidth(childNode.getJSONObject("dimensions").getInt("width"))
// .build());
// }
// feedModelBuilder.setSliderItems(postModels);
// }
// return feedModelBuilder.build();
return ResponseBodyUtils.parseGraphQLItem(media, null);
}
} catch (Exception e) {
// if (logCollector != null) {
// logCollector.appendException(e, LogCollector.LogFile.ASYNC_POST_FETCHER, "doInBackground");
// }
Log.e(TAG, "Error fetching post", e);
} finally {
if (conn != null) {
conn.disconnect();
}
}
return null;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected void onPostExecute(final Media feedModel) {
if (fetchListener != null) fetchListener.onResult(feedModel);
}
}

View File

@ -7,14 +7,16 @@ import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.ProfileService;
import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
public class ProfilePostFetchService implements PostFetcher.PostFetchService {
private static final String TAG = "ProfilePostFetchService";
private final ProfileService profileService;
private final GraphQLService graphQLService;
private final GraphQLRepository graphQLRepository;
private final User profileModel;
private final boolean isLoggedIn;
private String nextMaxId;
@ -23,7 +25,7 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService {
public ProfilePostFetchService(final User profileModel, final boolean isLoggedIn) {
this.profileModel = profileModel;
this.isLoggedIn = isLoggedIn;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
profileService = isLoggedIn ? ProfileService.getInstance() : null;
}
@ -49,7 +51,19 @@ public class ProfilePostFetchService implements PostFetcher.PostFetchService {
}
};
if (isLoggedIn) profileService.fetchPosts(profileModel.getPk(), nextMaxId, cb);
else graphQLService.fetchProfilePosts(profileModel.getPk(), 30, nextMaxId, profileModel, cb);
else graphQLRepository.fetchProfilePosts(
profileModel.getPk(),
30,
nextMaxId,
profileModel,
CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
if (throwable != null) {
cb.onFailure(throwable);
return;
}
cb.onSuccess(postsFetchResponse);
}, Dispatchers.getIO())
);
}
@Override

View File

@ -7,13 +7,15 @@ import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.enums.PostItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.utils.CoroutineUtilsKt;
import awais.instagrabber.webservices.GraphQLRepository;
import awais.instagrabber.webservices.ProfileService;
import awais.instagrabber.webservices.ServiceCallback;
import kotlinx.coroutines.Dispatchers;
public class SavedPostFetchService implements PostFetcher.PostFetchService {
private final ProfileService profileService;
private final GraphQLService graphQLService;
private final GraphQLRepository graphQLRepository;
private final long profileId;
private final PostItemType type;
private final boolean isLoggedIn;
@ -27,7 +29,7 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService {
this.type = type;
this.isLoggedIn = isLoggedIn;
this.collectionId = collectionId;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
graphQLRepository = isLoggedIn ? null : GraphQLRepository.Companion.getInstance();
profileService = isLoggedIn ? ProfileService.getInstance() : null;
}
@ -58,7 +60,18 @@ public class SavedPostFetchService implements PostFetcher.PostFetchService {
break;
case TAGGED:
if (isLoggedIn) profileService.fetchTagged(profileId, nextMaxId, callback);
else graphQLService.fetchTaggedPosts(profileId, 30, nextMaxId, callback);
else graphQLRepository.fetchTaggedPosts(
profileId,
30,
nextMaxId,
CoroutineUtilsKt.getContinuation((postsFetchResponse, throwable) -> {
if (throwable != null) {
callback.onFailure(throwable);
return;
}
callback.onSuccess(postsFetchResponse);
}, Dispatchers.getIO())
);
break;
case COLLECTION:
case SAVED:

View File

@ -105,12 +105,15 @@ public class ChatMessageLayout extends FrameLayout {
viewPartMainLastLineWidth = viewPartMainLineCount > 0
? ((TextView) firstChild).getLayout().getLineWidth(viewPartMainLineCount - 1)
: 0;
// also include start left padding
viewPartMainLastLineWidth += firstChild.getPaddingLeft();
}
if (viewPartMainLineCount > 1 && !(viewPartMainLastLineWidth + viewPartInfoWidth > viewPartMain.getMeasuredWidth())) {
final float lastLineWithInfoWidth = viewPartMainLastLineWidth + viewPartInfoWidth;
if (viewPartMainLineCount > 1 && lastLineWithInfoWidth <= viewPartMain.getMeasuredWidth()) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight;
} else if (viewPartMainLineCount > 1 && (viewPartMainLastLineWidth + viewPartInfoWidth > availableWidth)) {
} else if (viewPartMainLineCount > 1 && (lastLineWithInfoWidth > availableWidth)) {
widthSize += viewPartMainWidth;
heightSize += viewPartMainHeight + viewPartInfoHeight;
} else if (viewPartMainLineCount == 1 && (viewPartMainWidth + viewPartInfoWidth > availableWidth)) {
@ -120,6 +123,16 @@ public class ChatMessageLayout extends FrameLayout {
heightSize += viewPartMainHeight;
widthSize += viewPartMainWidth + viewPartInfoWidth;
}
// if (isInEditMode()) {
// TextView wDebugView = (TextView) ((ViewGroup) this.getParent()).findViewWithTag("debug");
// wDebugView.setText(lastLineWithInfoWidth
// + "\n" + availableWidth
// + "\n" + viewPartMain.getMeasuredWidth()
// + "\n" + (lastLineWithInfoWidth <= viewPartMain.getMeasuredWidth())
// + "\n" + (lastLineWithInfoWidth > availableWidth)
// + "\n" + (viewPartMainWidth + viewPartInfoWidth > availableWidth));
// }
}
setMeasuredDimension(widthSize, heightSize);
super.onMeasure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));

View File

@ -80,7 +80,7 @@ public class DirectItemContextMenu extends PopupWindow {
if (!showReactions && (options == null || options.isEmpty())) {
throw new IllegalArgumentException("showReactions is set false and options are empty");
}
reactionsManager = ReactionsManager.getInstance();
reactionsManager = ReactionsManager.getInstance(context);
final Resources resources = context.getResources();
emojiSize = resources.getDimensionPixelSize(R.dimen.reaction_picker_emoji_size);
emojiMargin = resources.getDimensionPixelSize(R.dimen.reaction_picker_emoji_margin);

View File

@ -0,0 +1,165 @@
package awais.instagrabber.customviews;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.transition.ChangeBounds;
import androidx.transition.Transition;
import androidx.transition.TransitionManager;
import androidx.transition.TransitionSet;
import java.time.Duration;
import awais.instagrabber.customviews.helpers.ChangeText;
import awais.instagrabber.utils.NumberUtils;
public class FormattedNumberTextView extends AppCompatTextView {
private static final String TAG = FormattedNumberTextView.class.getSimpleName();
private static final Transition TRANSITION;
private long number = Long.MIN_VALUE;
private boolean showAbbreviation = true;
private boolean animateChanges = false;
private boolean toggleOnClick = true;
private boolean autoToggleToAbbreviation = true;
private long autoToggleTimeoutMs = Duration.ofSeconds(2).toMillis();
private boolean initDone = false;
static {
final TransitionSet transitionSet = new TransitionSet();
final ChangeText changeText = new ChangeText().setChangeBehavior(ChangeText.CHANGE_BEHAVIOR_OUT_IN);
transitionSet.addTransition(changeText).addTransition(new ChangeBounds());
TRANSITION = transitionSet;
}
public FormattedNumberTextView(@NonNull final Context context) {
super(context);
init();
}
public FormattedNumberTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
init();
}
public FormattedNumberTextView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
if (initDone) return;
setupClickToggle();
initDone = true;
}
private void setupClickToggle() {
setOnClickListener(null);
}
private OnClickListener getWrappedClickListener(@Nullable final OnClickListener l) {
if (!toggleOnClick) {
return l;
}
return v -> {
toggleAbbreviation();
if (l != null) {
l.onClick(this);
}
};
}
public void setNumber(final long number) {
if (this.number == number) return;
this.number = number;
format();
}
public void clearNumber() {
if (number == Long.MIN_VALUE) return;
number = Long.MIN_VALUE;
format();
}
public void setShowAbbreviation(final boolean showAbbreviation) {
if (this.showAbbreviation && showAbbreviation) return;
this.showAbbreviation = showAbbreviation;
format();
}
public boolean isShowAbbreviation() {
return showAbbreviation;
}
private void toggleAbbreviation() {
if (number == Long.MIN_VALUE) return;
setShowAbbreviation(!showAbbreviation);
}
public void setToggleOnClick(final boolean toggleOnClick) {
this.toggleOnClick = toggleOnClick;
}
public boolean isToggleOnClick() {
return toggleOnClick;
}
public void setAutoToggleToAbbreviation(final boolean autoToggleToAbbreviation) {
this.autoToggleToAbbreviation = autoToggleToAbbreviation;
}
public boolean isAutoToggleToAbbreviation() {
return autoToggleToAbbreviation;
}
public void setAutoToggleTimeoutMs(final long autoToggleTimeoutMs) {
this.autoToggleTimeoutMs = autoToggleTimeoutMs;
}
public long getAutoToggleTimeoutMs() {
return autoToggleTimeoutMs;
}
public void setAnimateChanges(final boolean animateChanges) {
this.animateChanges = animateChanges;
}
public boolean isAnimateChanges() {
return animateChanges;
}
@Override
public void setOnClickListener(@Nullable final OnClickListener l) {
super.setOnClickListener(getWrappedClickListener(l));
}
private void format() {
post(() -> {
if (animateChanges) {
try {
TransitionManager.beginDelayedTransition((ViewGroup) getParent(), TRANSITION);
} catch (Exception e) {
Log.e(TAG, "format: ", e);
}
}
if (number == Long.MIN_VALUE) {
setText(null);
return;
}
if (showAbbreviation) {
setText(NumberUtils.abbreviate(number, null));
return;
}
setText(String.valueOf(number));
if (autoToggleToAbbreviation) {
getHandler().postDelayed(() -> setShowAbbreviation(true), autoToggleTimeoutMs);
}
});
}
}

View File

@ -0,0 +1,75 @@
package awais.instagrabber.customviews;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.navigation.NavDestination;
import androidx.navigation.NavOptions;
import androidx.navigation.Navigator;
import androidx.navigation.fragment.FragmentNavigator;
import awais.instagrabber.R;
@Navigator.Name("fragment")
public class FragmentNavigatorWithDefaultAnimations extends FragmentNavigator {
private final NavOptions emptyNavOptions = new NavOptions.Builder().build();
// private final NavOptions defaultNavOptions = new NavOptions.Builder()
// .setEnterAnim(R.animator.nav_default_enter_anim)
// .setExitAnim(R.animator.nav_default_exit_anim)
// .setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
// .setPopExitAnim(R.animator.nav_default_pop_exit_anim)
// .build();
private final NavOptions defaultNavOptions = new NavOptions.Builder()
.setEnterAnim(R.anim.slide_in_right)
.setExitAnim(R.anim.slide_out_left)
.setPopEnterAnim(android.R.anim.slide_in_left)
.setPopExitAnim(android.R.anim.slide_out_right)
.build();
public FragmentNavigatorWithDefaultAnimations(@NonNull final Context context,
@NonNull final FragmentManager manager,
final int containerId) {
super(context, manager, containerId);
}
@Nullable
@Override
public NavDestination navigate(@NonNull final Destination destination,
@Nullable final Bundle args,
@Nullable final NavOptions navOptions,
@Nullable final Navigator.Extras navigatorExtras) {
// this will try to fill in empty animations with defaults when no shared element transitions are set
// https://developer.android.com/guide/navigation/navigation-animate-transitions#shared-element
final boolean shouldUseTransitionsInstead = navigatorExtras != null;
final NavOptions navOptions1 = shouldUseTransitionsInstead ? navOptions : fillEmptyAnimationsWithDefaults(navOptions);
return super.navigate(destination, args, navOptions1, navigatorExtras);
}
private NavOptions fillEmptyAnimationsWithDefaults(@Nullable final NavOptions navOptions) {
if (navOptions == null) {
return defaultNavOptions;
}
return copyNavOptionsWithDefaultAnimations(navOptions);
}
@NonNull
private NavOptions copyNavOptionsWithDefaultAnimations(@NonNull final NavOptions navOptions) {
return new NavOptions.Builder()
.setLaunchSingleTop(navOptions.shouldLaunchSingleTop())
.setPopUpTo(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive())
.setEnterAnim(navOptions.getEnterAnim() == emptyNavOptions.getEnterAnim()
? defaultNavOptions.getEnterAnim() : navOptions.getEnterAnim())
.setExitAnim(navOptions.getExitAnim() == emptyNavOptions.getExitAnim()
? defaultNavOptions.getExitAnim() : navOptions.getExitAnim())
.setPopEnterAnim(navOptions.getPopEnterAnim() == emptyNavOptions.getPopEnterAnim()
? defaultNavOptions.getPopEnterAnim() : navOptions.getPopEnterAnim())
.setPopExitAnim(navOptions.getPopExitAnim() == emptyNavOptions.getPopExitAnim()
? defaultNavOptions.getPopExitAnim() : navOptions.getPopExitAnim())
.build();
}
}

View File

@ -0,0 +1,246 @@
package awais.instagrabber.customviews;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowInsetsAnimation;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.NestedScrollingParent3;
import androidx.core.view.NestedScrollingParentHelper;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import java.util.Arrays;
import awais.instagrabber.customviews.helpers.SimpleImeAnimationController;
import awais.instagrabber.utils.ViewUtils;
import static androidx.core.view.ViewCompat.TYPE_TOUCH;
public final class InsetsAnimationLinearLayout extends LinearLayout implements NestedScrollingParent3 {
private final NestedScrollingParentHelper nestedScrollingParentHelper = new NestedScrollingParentHelper(this);
private final SimpleImeAnimationController imeAnimController = new SimpleImeAnimationController();
private final int[] tempIntArray2 = new int[2];
private final int[] startViewLocation = new int[2];
private View currentNestedScrollingChild;
private int dropNextY;
private boolean scrollImeOffScreenWhenVisible = true;
private boolean scrollImeOnScreenWhenNotVisible = true;
private boolean scrollImeOffScreenWhenVisibleOnFling = false;
private boolean scrollImeOnScreenWhenNotVisibleOnFling = false;
public InsetsAnimationLinearLayout(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
}
public InsetsAnimationLinearLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public final boolean getScrollImeOffScreenWhenVisible() {
return scrollImeOffScreenWhenVisible;
}
public final void setScrollImeOffScreenWhenVisible(boolean scrollImeOffScreenWhenVisible) {
this.scrollImeOffScreenWhenVisible = scrollImeOffScreenWhenVisible;
}
public final boolean getScrollImeOnScreenWhenNotVisible() {
return scrollImeOnScreenWhenNotVisible;
}
public final void setScrollImeOnScreenWhenNotVisible(boolean scrollImeOnScreenWhenNotVisible) {
this.scrollImeOnScreenWhenNotVisible = scrollImeOnScreenWhenNotVisible;
}
public boolean getScrollImeOffScreenWhenVisibleOnFling() {
return scrollImeOffScreenWhenVisibleOnFling;
}
public void setScrollImeOffScreenWhenVisibleOnFling(final boolean scrollImeOffScreenWhenVisibleOnFling) {
this.scrollImeOffScreenWhenVisibleOnFling = scrollImeOffScreenWhenVisibleOnFling;
}
public boolean getScrollImeOnScreenWhenNotVisibleOnFling() {
return scrollImeOnScreenWhenNotVisibleOnFling;
}
public void setScrollImeOnScreenWhenNotVisibleOnFling(final boolean scrollImeOnScreenWhenNotVisibleOnFling) {
this.scrollImeOnScreenWhenNotVisibleOnFling = scrollImeOnScreenWhenNotVisibleOnFling;
}
public SimpleImeAnimationController getImeAnimController() {
return imeAnimController;
}
@Override
public boolean onStartNestedScroll(@NonNull final View child,
@NonNull final View target,
final int axes,
final int type) {
return (axes & SCROLL_AXIS_VERTICAL) != 0 && type == TYPE_TOUCH;
}
@Override
public void onNestedScrollAccepted(@NonNull final View child,
@NonNull final View target,
final int axes,
final int type) {
nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type);
currentNestedScrollingChild = child;
}
@Override
public void onNestedPreScroll(@NonNull final View target,
final int dx,
final int dy,
@NonNull final int[] consumed,
final int type) {
if (imeAnimController.isInsetAnimationRequestPending()) {
consumed[0] = dx;
consumed[1] = dy;
} else {
int deltaY = dy;
if (dropNextY != 0) {
consumed[1] = dropNextY;
deltaY = dy - dropNextY;
dropNextY = 0;
}
if (deltaY < 0) {
if (imeAnimController.isInsetAnimationInProgress()) {
consumed[1] -= imeAnimController.insetBy(-deltaY);
} else if (scrollImeOffScreenWhenVisible && !imeAnimController.isInsetAnimationRequestPending()) {
WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this);
if (rootWindowInsets != null) {
if (rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) {
startControlRequest();
consumed[1] = deltaY;
}
}
}
}
}
}
@Override
public void onNestedScroll(@NonNull final View target,
final int dxConsumed,
final int dyConsumed,
final int dxUnconsumed,
final int dyUnconsumed,
final int type,
@NonNull final int[] consumed) {
if (dyUnconsumed > 0) {
if (imeAnimController.isInsetAnimationInProgress()) {
consumed[1] = -imeAnimController.insetBy(-dyUnconsumed);
} else if (scrollImeOnScreenWhenNotVisible && !imeAnimController.isInsetAnimationRequestPending()) {
WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this);
if (rootWindowInsets != null) {
if (!rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) {
startControlRequest();
consumed[1] = dyUnconsumed;
}
}
}
}
}
@Override
public boolean onNestedFling(@NonNull final View target,
final float velocityX,
final float velocityY,
final boolean consumed) {
if (imeAnimController.isInsetAnimationInProgress()) {
imeAnimController.animateToFinish(velocityY);
return true;
} else {
boolean imeVisible = false;
final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(this);
if (rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())) {
imeVisible = true;
}
if (velocityY > 0 && scrollImeOnScreenWhenNotVisibleOnFling && !imeVisible) {
imeAnimController.startAndFling(this, velocityY);
return true;
} else if (velocityY < 0 && scrollImeOffScreenWhenVisibleOnFling && imeVisible) {
imeAnimController.startAndFling(this, velocityY);
return true;
} else {
return false;
}
}
}
@Override
public void onStopNestedScroll(@NonNull final View target, final int type) {
nestedScrollingParentHelper.onStopNestedScroll(target, type);
if (imeAnimController.isInsetAnimationInProgress() && !imeAnimController.isInsetAnimationFinishing()) {
imeAnimController.animateToFinish(null);
}
reset();
}
@Override
public void dispatchWindowInsetsAnimationPrepare(@NonNull final WindowInsetsAnimation animation) {
super.dispatchWindowInsetsAnimationPrepare(animation);
ViewUtils.suppressLayoutCompat(this, false);
}
private void startControlRequest() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return;
}
ViewUtils.suppressLayoutCompat(this, true);
if (currentNestedScrollingChild != null) {
currentNestedScrollingChild.getLocationInWindow(startViewLocation);
}
imeAnimController.startControlRequest(this, windowInsetsAnimationControllerCompat -> onControllerReady());
}
private void onControllerReady() {
if (currentNestedScrollingChild != null) {
imeAnimController.insetBy(0);
int[] location = tempIntArray2;
currentNestedScrollingChild.getLocationInWindow(location);
dropNextY = location[1] - startViewLocation[1];
}
}
private void reset() {
dropNextY = 0;
Arrays.fill(startViewLocation, 0);
ViewUtils.suppressLayoutCompat(this, false);
}
@Override
public void onNestedScrollAccepted(@NonNull final View child,
@NonNull final View target,
final int axes) {
onNestedScrollAccepted(child, target, axes, TYPE_TOUCH);
}
@Override
public void onNestedScroll(@NonNull final View target,
final int dxConsumed,
final int dyConsumed,
final int dxUnconsumed,
final int dyUnconsumed,
final int type) {
onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, tempIntArray2);
}
@Override
public void onStopNestedScroll(@NonNull final View target) {
onStopNestedScroll(target, TYPE_TOUCH);
}
}

View File

@ -0,0 +1,33 @@
package awais.instagrabber.customviews;
import android.content.Context;
import android.util.AttributeSet;
import android.view.WindowInsets;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
public class InsetsNotifyingCoordinatorLayout extends CoordinatorLayout {
public InsetsNotifyingCoordinatorLayout(@NonNull final Context context) {
super(context);
}
public InsetsNotifyingCoordinatorLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
}
public InsetsNotifyingCoordinatorLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
int childCount = getChildCount();
for (int index = 0; index < childCount; index++) {
getChildAt(index).dispatchApplyWindowInsets(insets);
}
return insets;
}
}

View File

@ -0,0 +1,35 @@
package awais.instagrabber.customviews;
import android.content.Context;
import android.util.AttributeSet;
import android.view.WindowInsets;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
public class InsetsNotifyingLinearLayout extends LinearLayout {
public InsetsNotifyingLinearLayout(final Context context) {
super(context);
}
public InsetsNotifyingLinearLayout(final Context context, @Nullable final AttributeSet attrs) {
super(context, attrs);
}
public InsetsNotifyingLinearLayout(final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public InsetsNotifyingLinearLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
int childCount = getChildCount();
for (int index = 0; index < childCount; index++) {
getChildAt(index).dispatchApplyWindowInsets(insets);
}
return insets;
}
}

View File

@ -0,0 +1,60 @@
package awais.instagrabber.customviews;
import android.os.Bundle;
import androidx.annotation.NavigationRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.NavController;
import androidx.navigation.Navigator;
import androidx.navigation.fragment.FragmentNavigator;
import androidx.navigation.fragment.NavHostFragment;
public class NavHostFragmentWithDefaultAnimations extends NavHostFragment {
private static final String KEY_GRAPH_ID = "android-support-nav:fragment:graphId";
private static final String KEY_START_DESTINATION_ARGS =
"android-support-nav:fragment:startDestinationArgs";
private static final String KEY_NAV_CONTROLLER_STATE =
"android-support-nav:fragment:navControllerState";
private static final String KEY_DEFAULT_NAV_HOST = "android-support-nav:fragment:defaultHost";
@NonNull
public static NavHostFragment create(@NavigationRes int graphResId) {
return create(graphResId, null);
}
@NonNull
public static NavHostFragment create(@NavigationRes int graphResId,
@Nullable Bundle startDestinationArgs) {
Bundle b = null;
if (graphResId != 0) {
b = new Bundle();
b.putInt(KEY_GRAPH_ID, graphResId);
}
if (startDestinationArgs != null) {
if (b == null) {
b = new Bundle();
}
b.putBundle(KEY_START_DESTINATION_ARGS, startDestinationArgs);
}
final NavHostFragmentWithDefaultAnimations result = new NavHostFragmentWithDefaultAnimations();
if (b != null) {
result.setArguments(b);
}
return result;
}
@NonNull
@Override
protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
return new FragmentNavigatorWithDefaultAnimations(requireContext(), getChildFragmentManager(), getId());
}
@Override
protected void onCreateNavController(@NonNull final NavController navController) {
super.onCreateNavController(navController);
navController.getNavigatorProvider()
.addNavigator(new FragmentNavigatorWithDefaultAnimations(requireContext(), getChildFragmentManager(), getId()));
}
}

View File

@ -3,6 +3,8 @@ package awais.instagrabber.customviews;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -25,15 +27,16 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration;
import awais.instagrabber.customviews.helpers.PostFetcher;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge;
import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.KeywordsFilterUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.Utils;
@ -60,14 +63,17 @@ public class PostsRecyclerView extends RecyclerView {
private FeedAdapterV2.FeedItemCallback feedItemCallback;
private boolean shouldScrollToTop;
private FeedAdapterV2.SelectionModeCallback selectionModeCallback;
private Function<ViewGroup, View> headerViewCreator;
private Function<View, Void> headerBinder;
private boolean refresh = true;
private final List<FetchStatusChangeListener> fetchStatusChangeListeners = new ArrayList<>();
private final FetchListener<List<Media>> fetchListener = new FetchListener<List<Media>>() {
@Override
public void onResult(final List<Media> result) {
final int currentPage = lazyLoader.getCurrentPage();
if (currentPage == 0) {
if (refresh) {
refresh = false;
mediaViewModel.getList().postValue(result);
shouldScrollToTop = true;
dispatchFetchStatus();
@ -75,8 +81,8 @@ public class PostsRecyclerView extends RecyclerView {
}
final List<Media> models = mediaViewModel.getList().getValue();
final List<Media> modelsCopy = models == null ? new ArrayList<>() : new ArrayList<>(models);
if (settingsHelper.getBoolean(Constants.TOGGLE_KEYWORD_FILTER)) {
final ArrayList<String> items = new ArrayList<>(settingsHelper.getStringSet(Constants.KEYWORD_FILTERS));
if (settingsHelper.getBoolean(PreferenceKeys.TOGGLE_KEYWORD_FILTER)) {
final ArrayList<String> items = new ArrayList<>(settingsHelper.getStringSet(PreferenceKeys.KEYWORD_FILTERS));
modelsCopy.addAll(new KeywordsFilterUtils(items).filter(result));
} else {
modelsCopy.addAll(result);
@ -192,22 +198,25 @@ public class PostsRecyclerView extends RecyclerView {
}
private void initSelf() {
mediaViewModel = new ViewModelProvider(viewModelStoreOwner).get(MediaViewModel.class);
mediaViewModel.getList().observe(lifeCycleOwner, list -> {
if (list.size() <= 0) return;
feedAdapter.submitList(list, () -> {
// postDelayed(this::fetchMoreIfPossible, 1000);
if (!shouldScrollToTop) return;
smoothScrollToPosition(0);
shouldScrollToTop = false;
});
});
try {
mediaViewModel = new ViewModelProvider(viewModelStoreOwner).get(MediaViewModel.class);
} catch (Exception e) {
Log.e(TAG, "initSelf: ", e);
}
if (mediaViewModel == null) return;
mediaViewModel.getList().observe(lifeCycleOwner, list -> feedAdapter.submitList(list, () -> {
// postDelayed(this::fetchMoreIfPossible, 1000);
if (!shouldScrollToTop) return;
shouldScrollToTop = false;
post(() -> smoothScrollToPosition(0));
}));
postFetcher = new PostFetcher(postFetchService, fetchListener);
if (layoutPreferences.getHasGap()) {
addItemDecoration(gridSpacingItemDecoration);
}
setHasFixedSize(true);
setNestedScrollingEnabled(true);
setItemAnimator(null);
lazyLoader = new RecyclerLazyLoaderAtEdge(layoutManager, (page) -> {
if (postFetcher.hasMore()) {
postFetcher.fetch();
@ -311,11 +320,12 @@ public class PostsRecyclerView extends RecyclerView {
}
public void refresh() {
refresh = true;
if (lazyLoader != null) {
lazyLoader.resetState();
}
if (postFetcher != null) {
mediaViewModel.getList().postValue(Collections.emptyList());
// mediaViewModel.getList().postValue(Collections.emptyList());
postFetcher.reset();
postFetcher.fetch();
}

View File

@ -70,6 +70,9 @@ public final class ProfilePicView extends CircularImageView {
case SMALL:
dimenRes = R.dimen.profile_pic_size_small;
break;
case SMALLER:
dimenRes = R.dimen.profile_pic_size_smaller;
break;
case TINY:
dimenRes = R.dimen.profile_pic_size_tiny;
break;
@ -113,7 +116,8 @@ public final class ProfilePicView extends CircularImageView {
TINY(0),
SMALL(1),
REGULAR(2),
LARGE(3);
LARGE(3),
SMALLER(4);
private final int value;
private static final Map<Integer, Size> map = new HashMap<>();

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