Compare commits
3 Commits
master
...
dispatcher
Author | SHA1 | Date | |
---|---|---|---|
ac0156748b | |||
8c817b5938 | |||
4857a3d0ac |
21
.drone.yml
Normal file
21
.drone.yml
Normal file
@ -0,0 +1,21 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: android
|
||||
|
||||
steps:
|
||||
- name: code-analysis
|
||||
image: mingc/android-build-box:latest
|
||||
failure: ignore
|
||||
commands:
|
||||
- ls -la
|
||||
- ./gradlew sonarqube -Dsonar.projectKey=RFS2 -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_LOGIN -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\""
|
||||
environment:
|
||||
SONAR_HOST_URL:
|
||||
from_secret: sonarScannerHostUrl
|
||||
SONAR_LOGIN:
|
||||
from_secret: sonarScannerLogin
|
||||
|
||||
- name: build
|
||||
image: mingc/android-build-box:latest
|
||||
commands:
|
||||
- ./gradlew :androidApp:build -PignoreGitVersion=true -P appLoginUrl="\"URL\"" -P appLoginUsername="\"LOGIN\"" -P appLoginPassword="\"PASS\"" -P pushCache=false
|
@ -1,24 +0,0 @@
|
||||
name: Build
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
BuildAndTest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags -p
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
- name: Configure gradle...
|
||||
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
|
||||
- name: Build and test
|
||||
run: ./gradlew build --stacktrace
|
@ -1,51 +0,0 @@
|
||||
name: Create tag
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.gitea/workflows/common_build.yml
|
||||
createTagAndChangelog:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Config git
|
||||
run: |
|
||||
git config --global user.email aminecmi+gitea@pm.me
|
||||
git config --global user.name Gitea
|
||||
- name: Creating the tag
|
||||
run: |
|
||||
git fetch --tags -p
|
||||
PREV=$(git describe --tags --abbrev=0)
|
||||
./build.sh --publish --from-ci
|
||||
- name: Generating changelog
|
||||
run: |
|
||||
VER=$(git describe --tags --abbrev=0)
|
||||
CHANGELOG=$(git log $PREV..HEAD --pretty="- %s")
|
||||
echo "**$VER**\n\n$CHANGELOG\n\n--------------------------------------------------------------------\n\n$(cat CHANGELOG.md)" > CHANGELOG.md
|
||||
git add CHANGELOG.md
|
||||
git commit -m "Changelog for $VER"
|
||||
git push origin ${VER}
|
||||
git push origin master
|
||||
- name: copy file via ssh password
|
||||
uses: garygrossgarten/github-action-scp@release
|
||||
with:
|
||||
host: amine-bouabdallaoui.fr
|
||||
username: ubuntu
|
||||
privateKey: ${{ secrets.PRIVATE_KEY }}
|
||||
local: version.txt
|
||||
target: /home/ubuntu/
|
||||
- name: deploy version file
|
||||
uses: appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: amine-bouabdallaoui.fr
|
||||
username: ubuntu
|
||||
key: ${{ secrets.PRIVATE_KEY }}
|
||||
script: cd /home/ubuntu && sudo rm -rf /var/www/amine/version.txt && sudo chown www-data:www-data ./version.txt && sudo mv version.txt /var/www/amine/
|
@ -1,26 +0,0 @@
|
||||
name: Check PR code
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
Lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
||||
java-version: '17'
|
||||
- name: Install klint
|
||||
run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/
|
||||
- name: Install detekt
|
||||
run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.1/detekt-cli-1.23.1.zip && unzip detekt-cli-1.23.1.zip
|
||||
- name: Linting...
|
||||
run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' || true
|
||||
- name: Detecting...
|
||||
run: ./detekt-cli-1.23.1/bin/detekt-cli --all-rules --excludes '**/shared/build/**/*.kt' || true
|
||||
build:
|
||||
uses: ./.gitea/workflows/common_build.yml
|
@ -1,9 +0,0 @@
|
||||
name: Check master code
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.gitea/workflows/common_build.yml
|
@ -1,48 +0,0 @@
|
||||
name: Create release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags -p
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
- name: Configure gradle...
|
||||
run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=false\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000" >> ~/.gradle/gradle.properties
|
||||
- name: Generate APK
|
||||
run: ./gradlew :androidApp:assembleGithubConfigRelease
|
||||
- name: Get Key
|
||||
run: wget ${{ secrets.KEY_URL }}
|
||||
- name: Zippalign
|
||||
run: $ANDROID_HOME/build-tools/31.0.0/zipalign -f -v 4 androidApp/build/outputs/apk/githubConfig/release/androidApp-githubConfig-release-unsigned.apk androidApp/build/outputs/apk/githubConfig/release/android-prod-released-ziped.apk
|
||||
- name: Sigh
|
||||
run: $ANDROID_HOME/build-tools/31.0.0/apksigner sign -v --out signed.apk --ks ./key --ks-key-alias ${{ secrets.KEY_ALIAS }} --ks-pass pass:${{ secrets.KEYSTORE_PASSWORD }} --v1-signing-enabled true --v2-signing-enabled true androidApp/build/outputs/apk/githubConfig/release/android-prod-released-ziped.apk
|
||||
- name: Verify
|
||||
run: $ANDROID_HOME/build-tools/31.0.0/apksigner verify signed.apk
|
||||
- name: Release
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
files: signed.apk
|
||||
api_key: ${{ secrets.API_KEY }}
|
||||
- name: Send mail
|
||||
uses: https://github.com/dawidd6/action-send-mail@v4
|
||||
with:
|
||||
connection_url: ${{ secrets.MAIL_CONNECTION }}
|
||||
to: ${{ secrets.MAIL_TO }}
|
||||
from: ${{ secrets.MAIL_FROM }}
|
||||
subject: Mapping file
|
||||
priority: high
|
||||
convert_markdown: true
|
||||
body: Nouveu fichier de mapping
|
||||
attachments: androidApp/build/outputs/mapping/githubConfigRelease/mapping.txt
|
27
.github/CONTRIBUTING.md
vendored
27
.github/CONTRIBUTING.md
vendored
@ -10,7 +10,7 @@ Please read the guidelines before contributing, and follow them (or try to) when
|
||||
|
||||
There are many ways to contribute to this project, you could [translate the app](https://crowdin.com/project/readerforselfoss), report bugs, request missing features, suggest enhancements and changes to existing ones. You also can improve the README with useful tips that could help the other users.
|
||||
|
||||
You can fork the repository, and [help me solve some issues](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs%22) or [develop new things](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/issues)
|
||||
You can fork the repository, and [help me solve some issues](https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs%22) or [develop new things](https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/issues)
|
||||
|
||||
### What I can't help you with.
|
||||
|
||||
@ -46,3 +46,28 @@ Always check if the web version of your instance is working.
|
||||
I won't provide any selfoss instance url. If you want to help, but to not have one, you'll have to install one, and use it.
|
||||
|
||||
All the details to need are [here](https://selfoss.aditu.de/).
|
||||
|
||||
# Build the project
|
||||
|
||||
You can directly import this project into IntellIJ/Android Studio.
|
||||
|
||||
You'll have to:
|
||||
|
||||
- Define some parameters either in `~/.gradle/gradle.properties` or as gradle parameters (see the examples)
|
||||
|
||||
- appLoginUrl, appLoginUsername and appLoginPassword: url, username and password of a selfoss instance. **These are only used for tests. They can be empty if you don't test API calls.**
|
||||
|
||||
### Examples:
|
||||
#### Inside ~/.gradle/gradle.properties
|
||||
|
||||
```
|
||||
appLoginUrl="URL" # It can be empty.
|
||||
appLoginUsername="LOGIN" # It can be empty.
|
||||
appLoginPassword="PASS" # It can be empty.
|
||||
```
|
||||
|
||||
#### As gradle parameters
|
||||
|
||||
```
|
||||
./gradlew .... -P appLoginUrl="URL" -P appLoginUsername="LOGIN" -P appLoginPassword="PASS"
|
||||
```
|
||||
|
292
CHANGELOG.md
292
CHANGELOG.md
@ -1,295 +1,3 @@
|
||||
**v124041081**
|
||||
|
||||
- chore: comment.
|
||||
- fix: Last time fixing the parsing date hack before moving it to os version.
|
||||
- Changelog for v124030731 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v124030731**
|
||||
|
||||
- fix: Basic auth and password can have non whitspace characters. Fixes 142.
|
||||
- Changelog for v124020451 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v124020451**
|
||||
|
||||
- fix: Fixed handling of position in card adapter.
|
||||
- Changelog for v124010301 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v124010301**
|
||||
|
||||
- fix: This may fix the oom errors.
|
||||
- Changelog for v124010191 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v124010191**
|
||||
|
||||
- fix: moving listeners.
|
||||
- chore: removed a useless log.
|
||||
- Changelog for v124010032 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v124010032**
|
||||
|
||||
- fix: Another date format thing.
|
||||
- Changelog for v124010031 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v124010031**
|
||||
|
||||
- fix: Checking if selfoss instance.
|
||||
- fix: handle three characters lenght hexcode colors.
|
||||
- Changelog for v123113311 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123113311**
|
||||
|
||||
- chore: Source tracker url in the menu.
|
||||
- fix: Handle kodein proguard rules.
|
||||
- Changelog for v123102961 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123102961**
|
||||
|
||||
- chore: domain changes.
|
||||
- Changelog for v123102852 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123102852**
|
||||
|
||||
- chore: lint cleaning.
|
||||
- Changelog for v123102841 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123102841**
|
||||
|
||||
- chore: cleaning ci steps and upgrading dependencies.
|
||||
- feat: Self signed ssl support.
|
||||
- Changelog for v123061811 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123061811**
|
||||
|
||||
- feat: Added confirmation dialog for disconnect item menu.
|
||||
- Changelog for v123061651 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123061651**
|
||||
|
||||
- i18n: Translation update.
|
||||
- i18n: Translation update.
|
||||
- i18n: Translation update.
|
||||
- fix: avoid trying to open invalid image urls.
|
||||
- Changelog for v123051471 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123051471**
|
||||
|
||||
- fix: images could be null.
|
||||
- fix: Check if color is not empty before parsing it.
|
||||
- chore: Removed unused log.
|
||||
- Changelog for v123051331 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123051331**
|
||||
|
||||
- fix: illegal input.
|
||||
- Changelog for v123051321 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123051321**
|
||||
|
||||
- debug: Debug null context.
|
||||
- Changelog for v123051301 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123051301**
|
||||
|
||||
- feat: Basic auth from url. Fixes #142 (#143)
|
||||
- debug: Debug index out of bound exception.
|
||||
- Changelog for v123051211 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123051211**
|
||||
|
||||
- fix: Sometimes url isn't even defined.
|
||||
- Changelog for v123041021 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123041021**
|
||||
|
||||
- fix: 'Enable Core Library Desugaring to support older Android versions' (#138) from davidoskky/ReaderForSelfoss-multiplatform:desugaring into master
|
||||
- Enable Core Library Desugaring to support older Android versions
|
||||
- Changelog for v123030851 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123030851**
|
||||
|
||||
- chore: replace textDrawable library (#136)
|
||||
- refactor: Remove slow login check. Closes #135.
|
||||
- ci: send the mapping file after a release.
|
||||
- Changelog for v123030751 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123030751**
|
||||
|
||||
- debug: added a lot to pinpoint the url issue.
|
||||
- feat: Use /sources/stats in the home (#133)
|
||||
- Changelog for v123030681 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123030681**
|
||||
|
||||
- fix: Unread and starred can be null.
|
||||
- Fixed version number issue.
|
||||
- Changelog for v123030621 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123030621**
|
||||
|
||||
- fix: url required issue.
|
||||
- fix: Canvas reused issue.
|
||||
- Changelog for v123020572 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123020572**
|
||||
|
||||
- fix: requirecontext issues ?
|
||||
- debug: activity not found exception.
|
||||
- Changelog for v123020571 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123020571**
|
||||
|
||||
- chore: remove errors logging.
|
||||
- fix: quickfix for url param not provided for some sources.
|
||||
- Update 'CHANGELOG.md'
|
||||
- Changelog for v123020523 [CI SKIP]
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123020523**
|
||||
|
||||
- fix: Git changelog.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123020491**
|
||||
|
||||
- fix: Fixed acra bug reporting.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123010301**
|
||||
|
||||
- Chore: acra config.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123010281**
|
||||
|
||||
- improvement: Improve right to left support (#130) Co-authored-by: davidoskky <davidoskky@hidden.hidden> Co-committed-by: davidoskky <davidoskky@hidden.hidden>
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123010261**
|
||||
|
||||
- feat: Handle public instances (#126) Co-authored-by: davidoskky <davidoskky@hidden.hidden> Co-committed-by: davidoskky <davidoskky@hidden.hidden>
|
||||
- ci: Pull request should trigger ci.
|
||||
- fix: Complete the disconnection before redirecting to the login screen
|
||||
- Complete the disconnection before redirecting to the login screen
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123010241**
|
||||
|
||||
- Merge pull request 'feat: swipe down to close images' (#122) from davidoskky/ReaderForSelfoss-multiplatform:swipe_down into master
|
||||
- Remove unnecessary definition
|
||||
- Remove unused import
|
||||
- Adjust the image closing animation
|
||||
- Add a dark hue to the underlying article when swiping to close images
|
||||
- Rename activity style to avoid interferences
|
||||
- Adapt the style of the image activity to the rest of the application
|
||||
- Resolve issues when swiping down to close images
|
||||
- Close the image fragment only if the image has been dragged down
|
||||
- Animate swipe down to close images
|
||||
- Swipe down to close images
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v123010041**
|
||||
|
||||
- Merge pull request 'scroll-tag-filters' (#124) from scroll-tag-filters into master
|
||||
- fix: added POST_NOTIFICATIONS to fix notifications issues.
|
||||
- fix: scrollable filter sheet.
|
||||
- enhancement: Ellipsize chips text.
|
||||
- Cleaning.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v122123641**
|
||||
|
||||
- feat: Disable the failing source in the filter sheet.
|
||||
- feat: Display the source error in the sources list.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v122123631**
|
||||
|
||||
- build: Added back maven repos (see https://gitlab.com/fdroid/fdroiddata/-/commit/1fb9d60dc58511abc2bb4eb321977922a0682c8b#note_1223925153)
|
||||
- build: Added back maven repos (see https://gitlab.com/fdroid/fdroiddata/-/commit/1fb9d60dc58511abc2bb4eb321977922a0682c8b#note_1223925153)
|
||||
- debug: trying to resolve `Canvas: trying to use a recycled bitmap`.
|
||||
- fix: NPE may be caused by the binding or the title that was null.
|
||||
- chore: Skip drone pipeline on changelog push.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
**v122123621**
|
||||
|
||||
- fix: Automatic CHANGELOG generation.
|
||||
- Merge pull request 'Sources Upsert' (#119) from sources-edit into master
|
||||
- Source update screen.
|
||||
- Sources menu.
|
||||
- chore: Automatic CHANGELOG generation.
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
# V2/Multiplatform rewrite
|
||||
|
||||
**v1**
|
||||
|
||||
- The app has the same functionalities as before.
|
||||
|
||||
|
||||
--------------------------------------------------------------------
|
||||
|
||||
# Old version changes
|
||||
|
||||
**1.7.x**
|
||||
|
||||
- Hiding tags with 0 articles
|
||||
|
16
README.md
16
README.md
@ -1,4 +1,4 @@
|
||||
# ReaderForSelfoss-multiplatform [![Build Status](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/actions/workflows/on_push.yml/badge.svg)](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderForSelfoss-multiplatform/actions?workflow=on_push.yml&actor=0&status=0)
|
||||
# ReaderForSelfoss-multiplatform
|
||||
|
||||
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/readerforselfoss/localized.svg)](https://crowdin.com/project/readerforselfoss)
|
||||
|
||||
@ -10,6 +10,10 @@ If you are a user, you can still create new issues. I'll fix them when I can.
|
||||
|
||||
<a href="https://f-droid.org/packages/bou.amine.apps.readerforselfossv2.android"><img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"></a>
|
||||
|
||||
## Screen captures
|
||||
|
||||
<img src="res//fr-card.png?raw=true" alt="card view" width="400"/> <img src="res//fr-list.png?raw=true" alt="list view" width="400"/>
|
||||
|
||||
## Like my app ?
|
||||
|
||||
<a href="https://www.buymeacoffee.com/aminecmi" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/lato-orange.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a>
|
||||
@ -18,15 +22,15 @@ If you are a user, you can still create new issues. I'll fix them when I can.
|
||||
|
||||
1. **You'll have to have a Selfoss instance running.** You'll find everything you need to install it [here](https://selfoss.aditu.de/).
|
||||
|
||||
2. Check the [Contribution guide](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md).
|
||||
2. Check the [Contribution guide](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md).
|
||||
|
||||
3. Build the project by following [these steps](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide)
|
||||
3. Build the project by following [these steps](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide)
|
||||
|
||||
## Useful links
|
||||
|
||||
- [Check what changed](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/CHANGELOG.md)
|
||||
- [See what I'm doing](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/projects/1)
|
||||
- [Create an issue, or request a new feature](https://gitea.amine-bouabdallaoui.fr/Louvorg/ReaderforSelfoss-multiplatform/issues)
|
||||
- [Check what changed](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/src/branch/master/CHANGELOG.md)
|
||||
- [See what I'm doing](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/projects/1)
|
||||
- [Create an issue, or request a new feature](https://gitea.amine-louveau.fr/Louvorg/ReaderforSelfoss-multiplatform/issues)
|
||||
- [Help translation the app](https://crowdin.com/project/readerforselfoss)
|
||||
|
||||
## Contributors (V1) (Alphabetical order) ❤️
|
||||
|
@ -1,22 +1,19 @@
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
val ignoreGitVersion: String by project
|
||||
val acraVersion = "5.9.7"
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
id("com.mikepenz.aboutlibraries.plugin")
|
||||
id("org.jetbrains.kotlinx.kover")
|
||||
}
|
||||
|
||||
fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
|
||||
val result: String = ByteArrayOutputStream().use { outputStream ->
|
||||
var result: String = ByteArrayOutputStream().use { outputStream ->
|
||||
project.exec {
|
||||
commandLine = cmd.split(" ")
|
||||
standardOutput = outputStream
|
||||
isIgnoreExitValue = ignore
|
||||
isIgnoreExitValue = ignore ?: false
|
||||
}
|
||||
outputStream.toString()
|
||||
}
|
||||
@ -24,15 +21,16 @@ fun Project.execWithOutput(cmd: String, ignore: Boolean = false): String {
|
||||
}
|
||||
|
||||
fun gitVersion(): String {
|
||||
var process = ""
|
||||
val maybeTagOfCurrentCommit = execWithOutput("git -C ../ describe --contains HEAD", true)
|
||||
val process = if (maybeTagOfCurrentCommit.isEmpty()) {
|
||||
process = if (maybeTagOfCurrentCommit.isEmpty()) {
|
||||
println("No tag on current commit. Will take the latest one.")
|
||||
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-refname --format='%(refname:short)' --count=1")
|
||||
execWithOutput("git -C ../ for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1")
|
||||
} else {
|
||||
println("Tag found on current commit")
|
||||
execWithOutput("git -C ../ describe --contains HEAD")
|
||||
}
|
||||
return process.replace("^0", "").replace("'", "").substring(1).replace("\\.", "").trim()
|
||||
return process.replace("'", "").substring(1).replace("\\.", "").trim()
|
||||
}
|
||||
|
||||
fun versionCodeFromGit(): Int {
|
||||
@ -55,24 +53,20 @@ fun versionNameFromGit(): String {
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
// Flag to enable support for the new language APIs
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
// For Kotlin projects
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
compileSdk = 34
|
||||
compileSdk = 31
|
||||
buildToolsVersion = "31.0.0"
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
defaultConfig {
|
||||
applicationId = "bou.amine.apps.readerforselfossv2.android"
|
||||
minSdk = 25
|
||||
targetSdk = 34
|
||||
minSdk = 21
|
||||
targetSdk = 31
|
||||
versionCode = versionCodeFromGit()
|
||||
versionName = versionNameFromGit()
|
||||
|
||||
@ -85,11 +79,6 @@ android {
|
||||
// tests
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
@ -97,63 +86,93 @@ android {
|
||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
||||
}
|
||||
getByName("debug") {
|
||||
buildConfigField("String", "LOGIN_URL", properties["appLoginUrl"] as String)
|
||||
buildConfigField("String", "LOGIN_PASSWORD", properties["appLoginPassword"] as String)
|
||||
buildConfigField("String", "LOGIN_USERNAME", properties["appLoginUsername"] as String)
|
||||
}
|
||||
}
|
||||
flavorDimensions.add("build")
|
||||
productFlavors {
|
||||
create("githubConfig") {
|
||||
create("github") {
|
||||
versionNameSuffix = "-github"
|
||||
dimension = "build"
|
||||
}
|
||||
}
|
||||
namespace = "bou.amine.apps.readerforselfossv2.android"
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
|
||||
|
||||
implementation(project(":shared"))
|
||||
implementation("com.google.android.material:material:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
|
||||
implementation("com.google.android.material:material:1.5.0")
|
||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
|
||||
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||
|
||||
// Testing
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0-alpha02")
|
||||
androidTestImplementation("androidx.test:runner:1.3.1-alpha02")
|
||||
// Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
|
||||
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.4.0-alpha02")
|
||||
// Espresso-intents for validation and stubbing of Intents
|
||||
androidTestImplementation("androidx.test.espresso:espresso-intents:3.4.0-alpha02")
|
||||
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
|
||||
|
||||
// Android Support
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.9.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.1")
|
||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||
implementation("com.google.android.material:material:1.5.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
|
||||
implementation("androidx.legacy:legacy-support-v4:1.0.0")
|
||||
implementation("androidx.vectordrawable:vectordrawable:1.2.0-beta01")
|
||||
implementation("androidx.vectordrawable:vectordrawable:1.2.0-alpha02")
|
||||
implementation("androidx.browser:browser:1.4.0")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.annotation:annotation:1.7.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.8.1")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("org.jsoup:jsoup:1.15.4")
|
||||
implementation("androidx.annotation:annotation:1.3.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.7.1")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||
implementation("org.jsoup:jsoup:1.14.3")
|
||||
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||
|
||||
//multidex
|
||||
implementation("androidx.multidex:multidex:2.0.1")
|
||||
|
||||
// About
|
||||
implementation("com.mikepenz:aboutlibraries-core:10.5.1")
|
||||
implementation("com.mikepenz:aboutlibraries:10.5.1")
|
||||
implementation("com.mikepenz:aboutlibraries-core:8.9.4")
|
||||
implementation("com.mikepenz:aboutlibraries:8.9.4")
|
||||
implementation("com.mikepenz:aboutlibraries-definitions:8.9.4")
|
||||
|
||||
// Async
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
|
||||
|
||||
// Retrofit + http logging + okhttp
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||
implementation("com.burgstaller:okhttp-digest:2.5")
|
||||
|
||||
// Material-ish things
|
||||
implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0")
|
||||
implementation("com.amulyakhare:com.amulyakhare.textdrawable:1.0.1")
|
||||
|
||||
// glide
|
||||
kapt("com.github.bumptech.glide:compiler:4.15.0")
|
||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.15.0")
|
||||
kapt("com.github.bumptech.glide:compiler:4.11.0")
|
||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.1.1")
|
||||
|
||||
// Drawer
|
||||
implementation("com.mikepenz:materialdrawer:8.4.5")
|
||||
|
||||
// Themes
|
||||
implementation("com.52inc:scoops:1.0.0")
|
||||
implementation("com.jaredrummler:colorpicker:1.1.0")
|
||||
implementation("com.github.rubensousa:floatingtoolbar:1.5.1")
|
||||
|
||||
// Pager
|
||||
implementation("me.relex:circleindicator:2.1.6")
|
||||
implementation("androidx.viewpager2:viewpager2:1.1.0-beta02")
|
||||
implementation("androidx.viewpager2:viewpager2:1.1.0-beta01")
|
||||
|
||||
//Dependency Injection
|
||||
implementation("org.kodein.di:kodein-di:7.14.0")
|
||||
@ -169,46 +188,16 @@ dependencies {
|
||||
//PhotoView
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.core:core-ktx:1.8.0")
|
||||
|
||||
// implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
|
||||
// implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
|
||||
// implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
|
||||
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
|
||||
|
||||
// Network information
|
||||
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
|
||||
implementation("com.github.ln-12:multiplatform-connectivity-status:1.3.0")
|
||||
|
||||
// SQLDELIGHT
|
||||
implementation("com.squareup.sqldelight:android-driver:1.5.4")
|
||||
|
||||
//test
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("io.mockk:mockk:1.12.0")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
||||
|
||||
implementation("ch.acra:acra-http:$acraVersion")
|
||||
implementation("ch.acra:acra-toast:$acraVersion")
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
outputs.upToDateWhen { false }
|
||||
useJUnit()
|
||||
testLogging {
|
||||
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
|
||||
events = setOf(
|
||||
org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED,
|
||||
org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED,
|
||||
org.gradle.api.tasks.testing.logging.TestLogEvent.STANDARD_ERROR
|
||||
)
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
offlineMode = true
|
||||
fetchRemoteLicense = false
|
||||
fetchRemoteFunding = false
|
||||
includePlatform = false
|
||||
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||
duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||
duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
||||
implementation("com.squareup.sqldelight:android-driver:1.5.3")
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
|
||||
git fetch --tags -p
|
||||
|
||||
BASE_VERSION="1"
|
||||
BASE_VERSION="1.7"
|
||||
LAST_TAG=$(git tag -l | sort -V | tail -1)
|
||||
|
||||
INITIAL_VERSION="${BASE_VERSION//./}$(date '+%y%m%j')"
|
||||
@ -21,11 +21,11 @@ VERSION="${INITIAL_VERSION}${TODAYS_VERSION}"
|
||||
|
||||
PARAMS_EXCEPT_PUBLISH=$(echo $1 | sed 's/\-\-publish//')
|
||||
|
||||
./version.sh ${VERSION} ${PARAMS_EXCEPT_PUBLISH} $@
|
||||
./version.sh ${VERSION} ${PARAMS_EXCEPT_PUBLISH}
|
||||
|
||||
if [[ "$@" == *'--publish'* ]]
|
||||
then
|
||||
./publish-version.sh ${VERSION} $@
|
||||
./publish-version.sh ${VERSION}
|
||||
else
|
||||
echo "Did not publish. If you wanted to do so, call the script with \"--publish\" or \"--publish-local\"."
|
||||
fi
|
50
androidApp/proguard-rules.pro
vendored
50
androidApp/proguard-rules.pro
vendored
@ -30,8 +30,15 @@
|
||||
<fields>;
|
||||
}
|
||||
|
||||
-dontwarn okio.**
|
||||
-dontwarn retrofit2.Platform$Java8
|
||||
-keep class retrofit.** { *; }
|
||||
-keepclasseswithmembers class * {
|
||||
@retrofit.http.* <methods>;
|
||||
}
|
||||
-keepattributes *Annotation*,Signature
|
||||
-keepattributes Exceptions
|
||||
-dontwarn okio.**
|
||||
-dontwarn javax.annotation.Nullable
|
||||
-dontwarn javax.annotation.ParametersAreNonnullByDefault
|
||||
|
||||
@ -48,50 +55,11 @@
|
||||
public *;
|
||||
}
|
||||
|
||||
-dontwarn com.anupcowkur.reservoir.**
|
||||
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
-keep class android.support.v7.widget.SearchView { *; }
|
||||
|
||||
# maybe remove later ?
|
||||
-keep class * extends androidx.fragment.app.Fragment
|
||||
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
# Keep `Companion` object fields of serializable classes.
|
||||
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
|
||||
-if @kotlinx.serialization.Serializable class **
|
||||
-keepclassmembers class <1> {
|
||||
static <1>$Companion Companion;
|
||||
}
|
||||
|
||||
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
|
||||
-if @kotlinx.serialization.Serializable class ** {
|
||||
static **$* *;
|
||||
}
|
||||
-keepclassmembers class <2>$<3> {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
# Keep `INSTANCE.serializer()` of serializable objects.
|
||||
-if @kotlinx.serialization.Serializable class ** {
|
||||
public static ** INSTANCE;
|
||||
}
|
||||
-keepclassmembers class <1> {
|
||||
public static <1> INSTANCE;
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||
|
||||
-dontwarn io.mockk.**
|
||||
-keep class io.mockk.** { *; }
|
||||
|
||||
|
||||
|
||||
# Kodein
|
||||
-keep, allowobfuscation, allowoptimization class org.kodein.type.TypeReference
|
||||
-keep, allowobfuscation, allowoptimization class org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest
|
||||
|
||||
-keep, allowobfuscation, allowoptimization class * extends org.kodein.type.TypeReference
|
||||
-keep, allowobfuscation, allowoptimization class * extends org.kodein.type.JVMAbstractTypeToken$Companion$WrappingTest
|
11
androidApp/publish-version.sh
Executable file
11
androidApp/publish-version.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NOTE: This is copy/pasted in jenkins
|
||||
|
||||
rm -f version.txt
|
||||
printf "versionName=$1-github\nversionCode=$1" >> version.txt
|
||||
|
||||
# You'll need to change server as your server and define a VERSION_PATH.
|
||||
scp version.txt server:$VERSION_PATH
|
||||
|
||||
rm version.txt
|
@ -0,0 +1,3 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
// TODO: test source adding
|
@ -0,0 +1,102 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.test.InstrumentationRegistry
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||
import androidx.test.espresso.action.ViewActions.pressKey
|
||||
import androidx.test.espresso.action.ViewActions.typeText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import android.view.KeyEvent
|
||||
import androidx.test.espresso.matcher.RootMatchers.isDialog
|
||||
import bou.amine.apps.readerforselfossv2.android.HomeActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.LoginActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class HomeActivityEspressoTest {
|
||||
lateinit var context: Context
|
||||
|
||||
@Rule @JvmField
|
||||
val rule = ActivityTestRule(HomeActivity::class.java, true, false)
|
||||
|
||||
@Before
|
||||
fun clearData() {
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
val editor =
|
||||
context
|
||||
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
editor.clear()
|
||||
|
||||
editor.putString("url", BuildConfig.LOGIN_URL)
|
||||
editor.putString("login", BuildConfig.LOGIN_USERNAME)
|
||||
editor.putString("password", BuildConfig.LOGIN_PASSWORD)
|
||||
|
||||
editor.commit()
|
||||
|
||||
Intents.init()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun menuItems() {
|
||||
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(
|
||||
withMenu(
|
||||
id = R.id.action_search,
|
||||
titleId = R.string.menu_home_search
|
||||
)
|
||||
).perform(click())
|
||||
|
||||
onView(withId(R.id.search_bar)).check(matches(isDisplayed()))
|
||||
|
||||
onView(withId(R.id.search_src_text)).perform(
|
||||
typeText("android"),
|
||||
pressKey(KeyEvent.KEYCODE_SEARCH),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withContentDescription(R.string.abc_toolbar_collapse_description)).perform(click())
|
||||
|
||||
openActionBarOverflowOrOptionsMenu(context)
|
||||
|
||||
onView(withMenu(id = R.id.refresh, titleId = R.string.menu_home_refresh))
|
||||
.perform(click())
|
||||
|
||||
onView(withText(android.R.string.ok))
|
||||
.inRoot(isDialog()).check(matches(isDisplayed())).perform(click())
|
||||
|
||||
openActionBarOverflowOrOptionsMenu(context)
|
||||
|
||||
onView(withText(R.string.action_disconnect)).perform(click())
|
||||
|
||||
intended(hasComponent(LoginActivity::class.java.name))
|
||||
}
|
||||
|
||||
// TODO: test articles opening and actions for cards and lists
|
||||
|
||||
@After
|
||||
fun releaseIntents() {
|
||||
Intents.release()
|
||||
}
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.test.InstrumentationRegistry
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
|
||||
import androidx.test.espresso.action.ViewActions.pressBack
|
||||
import androidx.test.espresso.action.ViewActions.typeText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.Intents.times
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import bou.amine.apps.readerforselfossv2.android.HomeActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.LoginActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
import com.mikepenz.aboutlibraries.ui.LibsActivity
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class LoginActivityEspressoTest {
|
||||
|
||||
@Rule @JvmField
|
||||
val rule = ActivityTestRule(LoginActivity::class.java, true, false)
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var url: String
|
||||
private lateinit var username: String
|
||||
private lateinit var password: String
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val editor =
|
||||
context
|
||||
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
editor.clear()
|
||||
editor.commit()
|
||||
|
||||
|
||||
url = BuildConfig.LOGIN_URL
|
||||
username = BuildConfig.LOGIN_USERNAME
|
||||
password = BuildConfig.LOGIN_PASSWORD
|
||||
|
||||
Intents.init()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun menuItems() {
|
||||
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
openActionBarOverflowOrOptionsMenu(context)
|
||||
|
||||
onView(withText(R.string.action_about)).perform(click())
|
||||
|
||||
intended(hasComponent(LibsActivity::class.java.name), times(1))
|
||||
|
||||
onView(isRoot()).perform(pressBack())
|
||||
|
||||
intended(hasComponent(LoginActivity::class.java.name))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wrongLoginUrl() {
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withId(R.id.loginProgress))
|
||||
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||
|
||||
onView(withId(R.id.urlView)).perform(click()).perform(typeText("WRONGURL"))
|
||||
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
|
||||
onView(withId(R.id.urlView)).check(matches(isHintOrErrorEnabled()))
|
||||
}
|
||||
|
||||
// TODO: Add tests for multiple false urls with dialog
|
||||
|
||||
@Test
|
||||
fun emptyAuthData() {
|
||||
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||
|
||||
onView(withId(R.id.withLogin)).perform(click())
|
||||
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
|
||||
onView(withId(R.id.loginView)).check(matches(isHintOrErrorEnabled()))
|
||||
onView(withId(R.id.passwordView)).check(matches(isHintOrErrorEnabled()))
|
||||
|
||||
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||
typeText(username),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withId(R.id.passwordView)).check(matches(isHintOrErrorEnabled()))
|
||||
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
|
||||
onView(withId(R.id.passwordView)).check(
|
||||
matches(
|
||||
isHintOrErrorEnabled()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wrongAuthData() {
|
||||
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||
|
||||
onView(withId(R.id.withLogin)).perform(click())
|
||||
|
||||
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||
typeText(username),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withId(R.id.passwordView)).perform(click()).perform(
|
||||
typeText("WRONGPASS"),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
|
||||
onView(withId(R.id.urlView)).check(matches(isHintOrErrorEnabled()))
|
||||
onView(withId(R.id.loginView)).check(matches(isHintOrErrorEnabled()))
|
||||
onView(withId(R.id.passwordView)).check(matches(isHintOrErrorEnabled()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun workingAuth() {
|
||||
|
||||
rule.launchActivity(Intent())
|
||||
|
||||
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
|
||||
|
||||
onView(withId(R.id.withLogin)).perform(click())
|
||||
|
||||
onView(withId(R.id.loginView)).perform(click()).perform(
|
||||
typeText(username),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withId(R.id.passwordView)).perform(click()).perform(
|
||||
typeText(password),
|
||||
closeSoftKeyboard()
|
||||
)
|
||||
|
||||
onView(withId(R.id.signInButton)).perform(click())
|
||||
|
||||
Thread.sleep(2000)
|
||||
intended(hasComponent(HomeActivity::class.java.name))
|
||||
}
|
||||
|
||||
@After
|
||||
fun releaseIntents() {
|
||||
Intents.release()
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import androidx.test.InstrumentationRegistry.getInstrumentation
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.Intents.times
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import bou.amine.apps.readerforselfossv2.android.HomeActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.LoginActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.MainActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
import org.junit.After
|
||||
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MainActivityEspressoTest {
|
||||
|
||||
lateinit var intent: Intent
|
||||
lateinit var preferencesEditor: SharedPreferences.Editor
|
||||
private lateinit var url: String
|
||||
private lateinit var username: String
|
||||
private lateinit var password: String
|
||||
|
||||
@Rule @JvmField
|
||||
val rule = ActivityTestRule(MainActivity::class.java, true, false)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
intent = Intent()
|
||||
val context = getInstrumentation().targetContext
|
||||
|
||||
// create a SharedPreferences editor
|
||||
preferencesEditor = context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE).edit()
|
||||
|
||||
url = BuildConfig.LOGIN_URL
|
||||
username = BuildConfig.LOGIN_USERNAME
|
||||
password = BuildConfig.LOGIN_PASSWORD
|
||||
|
||||
Intents.init()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkFirstOpenLaunchesIntro() {
|
||||
preferencesEditor.putString("url", "")
|
||||
preferencesEditor.putString("password", "")
|
||||
preferencesEditor.putString("login", "")
|
||||
preferencesEditor.commit()
|
||||
|
||||
rule.launchActivity(intent)
|
||||
|
||||
intended(hasComponent(LoginActivity::class.java.name))
|
||||
intended(hasComponent(HomeActivity::class.java.name), times(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkNotFirstOpenLaunchesLogin() {
|
||||
preferencesEditor.putString("url", url)
|
||||
preferencesEditor.putString("password", password)
|
||||
preferencesEditor.putString("login", username)
|
||||
preferencesEditor.commit()
|
||||
|
||||
rule.launchActivity(intent)
|
||||
|
||||
intended(hasComponent(MainActivity::class.java.name))
|
||||
intended(hasComponent(HomeActivity::class.java.name))
|
||||
}
|
||||
|
||||
@After
|
||||
fun releaseIntents() {
|
||||
Intents.release()
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import org.hamcrest.Description
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.TypeSafeMatcher
|
||||
|
||||
fun isHintOrErrorEnabled(): Matcher<View> =
|
||||
object : TypeSafeMatcher<View>() {
|
||||
override fun describeTo(description: Description?) {
|
||||
}
|
||||
|
||||
override fun matchesSafely(item: View?): Boolean {
|
||||
if (item !is EditText) {
|
||||
return false
|
||||
}
|
||||
|
||||
return item.error.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun withMenu(id: Int, titleId: Int): Matcher<View> =
|
||||
Matchers.anyOf(
|
||||
ViewMatchers.withId(id),
|
||||
ViewMatchers.withText(titleId)
|
||||
)
|
@ -1,23 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
package="bou.amine.apps.readerforselfossv2.android">
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
tools:replace="android:allowBackup"
|
||||
android:name=".MyApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@style/NoBar"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:configChanges="uiMode">
|
||||
android:theme="@style/NoBar">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:theme="@style/SplashTheme"
|
||||
@ -53,7 +48,7 @@
|
||||
android:value=".HomeActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".UpsertSourceActivity"
|
||||
android:name=".AddSourceActivity"
|
||||
android:parentActivityName=".SourcesActivity"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
@ -70,10 +65,13 @@
|
||||
android:name=".ReaderActivity">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ImageActivity"
|
||||
android:theme="@style/Theme.AppCompat.ImageActivity">
|
||||
android:name=".ImageActivity">
|
||||
</activity>
|
||||
|
||||
<meta-data
|
||||
android:name="bou.amine.apps.readerforselfossv2.android.utils.glide.SelfSignedGlideModule"
|
||||
android:value="GlideModule" />
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
|
||||
@ -81,5 +79,8 @@
|
||||
android:value="true" />
|
||||
|
||||
<meta-data android:name="android.max_aspect" android:value="2.1" />
|
||||
<meta-data
|
||||
android:name="preloaded_fonts"
|
||||
android:resource="@array/preloaded_fonts" />
|
||||
</application>
|
||||
</manifest>
|
@ -1,9 +0,0 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import org.acra.ACRA
|
||||
import org.acra.ktx.sendSilentlyWithAcra
|
||||
|
||||
fun Throwable.sendSilentlyWithAcraWithName(name: String) {
|
||||
ACRA.errorReporter.putCustomData("error_source", name)
|
||||
this.sendSilentlyWithAcra()
|
||||
}
|
@ -0,0 +1,200 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityAddSourceBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid
|
||||
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import com.ftinc.scoop.Scoop
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
|
||||
class AddSourceActivity : AppCompatActivity(), DIAware {
|
||||
|
||||
private var mSpoutsValue: String? = null
|
||||
|
||||
private lateinit var appColors: AppColors
|
||||
private lateinit var binding: ActivityAddSourceBinding
|
||||
|
||||
override val di by closestDI()
|
||||
private val repository : Repository by instance()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
appColors = AppColors(this@AddSourceActivity)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAddSourceBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
|
||||
setContentView(view)
|
||||
|
||||
val scoop = Scoop.getInstance()
|
||||
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
|
||||
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
|
||||
|
||||
val drawable = binding.nameInput.background
|
||||
drawable.setTint(appColors.colorAccent)
|
||||
|
||||
|
||||
// TODO: clean
|
||||
binding.nameInput.background = drawable
|
||||
|
||||
val drawable1 = binding.sourceUri.background
|
||||
drawable1.setTint(appColors.colorAccent)
|
||||
|
||||
binding.sourceUri.background = drawable1
|
||||
|
||||
val drawable2 = binding.tags.background
|
||||
drawable2.setTint(appColors.colorAccent)
|
||||
|
||||
binding.tags.background = drawable2
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput)
|
||||
|
||||
binding.saveBtn.setTextColor(appColors.colorAccent)
|
||||
|
||||
binding.saveBtn.setOnClickListener {
|
||||
handleSaveSource(
|
||||
binding.tags,
|
||||
binding.nameInput.text.toString(),
|
||||
binding.sourceUri.text.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val config = Config()
|
||||
|
||||
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) {
|
||||
mustLoginToAddSource()
|
||||
} else {
|
||||
handleSpoutsSpinner(binding.spoutsSpinner, binding.progress, binding.formContainer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSpoutsSpinner(
|
||||
spoutsSpinner: Spinner,
|
||||
mProgress: ProgressBar,
|
||||
formContainer: ConstraintLayout
|
||||
) {
|
||||
val spoutsKV = HashMap<String, String>()
|
||||
spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) {
|
||||
if (view != null) {
|
||||
val spoutName = (view as TextView).text.toString()
|
||||
mSpoutsValue = spoutsKV[spoutName]
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(adapterView: AdapterView<*>) {
|
||||
mSpoutsValue = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun handleSpoutFailure(networkIssue: Boolean = false) {
|
||||
Toast.makeText(
|
||||
this@AddSourceActivity,
|
||||
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
mProgress.visibility = View.GONE
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val items = repository.getSpouts()
|
||||
if (items != null) {
|
||||
val itemsStrings = items.map { it.value.name }
|
||||
for ((key, value) in items) {
|
||||
spoutsKV[value.name] = key
|
||||
}
|
||||
|
||||
mProgress.visibility = View.GONE
|
||||
formContainer.visibility = View.VISIBLE
|
||||
|
||||
val spinnerArrayAdapter =
|
||||
ArrayAdapter(
|
||||
this@AddSourceActivity,
|
||||
android.R.layout.simple_spinner_item,
|
||||
itemsStrings
|
||||
)
|
||||
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spoutsSpinner.adapter = spinnerArrayAdapter
|
||||
} else {
|
||||
handleSpoutFailure()
|
||||
}
|
||||
} catch (e: NetworkUnavailableException) {
|
||||
handleSpoutFailure(networkIssue = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeGetDetailsFromIntentSharing(
|
||||
intent: Intent,
|
||||
sourceUri: EditText,
|
||||
nameInput: EditText
|
||||
) {
|
||||
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
|
||||
sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
|
||||
nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
|
||||
}
|
||||
}
|
||||
|
||||
private fun mustLoginToAddSource() {
|
||||
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
|
||||
val i = Intent(this, LoginActivity::class.java)
|
||||
startActivity(i)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun handleSaveSource(tags: EditText, title: String, url: String) {
|
||||
|
||||
val sourceDetailsUnavailable =
|
||||
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
|
||||
|
||||
when {
|
||||
sourceDetailsUnavailable -> {
|
||||
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
else -> {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val successfullyAddedSource = repository.createSource(
|
||||
title,
|
||||
url,
|
||||
mSpoutsValue!!,
|
||||
tags.text.toString(),
|
||||
"",
|
||||
)
|
||||
if (successfullyAddedSource) {
|
||||
finish()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@AddSourceActivity,
|
||||
R.string.cant_create_source,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,6 @@ package bou.amine.apps.readerforselfossv2.android
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
@ -11,8 +10,8 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivityImageBindin
|
||||
import bou.amine.apps.readerforselfossv2.android.fragments.ImageFragment
|
||||
|
||||
class ImageActivity : AppCompatActivity() {
|
||||
private lateinit var allImages: ArrayList<String>
|
||||
private var position: Int = 0
|
||||
private lateinit var allImages : ArrayList<String>
|
||||
private var position : Int = 0
|
||||
|
||||
private lateinit var binding: ActivityImageBinding
|
||||
|
||||
@ -24,6 +23,7 @@ class ImageActivity : AppCompatActivity() {
|
||||
setContentView(view)
|
||||
|
||||
setSupportActionBar(binding.toolBar)
|
||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
allImages = intent.getStringArrayListExtra("allImages") as ArrayList<String>
|
||||
@ -31,52 +31,12 @@ class ImageActivity : AppCompatActivity() {
|
||||
|
||||
binding.pager.adapter = ScreenSlidePagerAdapter(this)
|
||||
binding.pager.setCurrentItem(position, false)
|
||||
|
||||
val transitionListener =
|
||||
object : MotionLayout.TransitionListener {
|
||||
override fun onTransitionStarted(
|
||||
motionLayout: MotionLayout?,
|
||||
startId: Int,
|
||||
endId: Int,
|
||||
) {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
override fun onTransitionChange(
|
||||
motionLayout: MotionLayout?,
|
||||
startId: Int,
|
||||
endId: Int,
|
||||
progress: Float,
|
||||
) {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
override fun onTransitionCompleted(
|
||||
motionLayout: MotionLayout?,
|
||||
currentId: Int,
|
||||
) {
|
||||
if (motionLayout?.currentState == binding.root.endState) {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTransitionTrigger(
|
||||
motionLayout: MotionLayout?,
|
||||
triggerId: Int,
|
||||
positive: Boolean,
|
||||
progress: Float,
|
||||
) {
|
||||
// Nothing
|
||||
}
|
||||
}
|
||||
binding.root.setTransitionListener(transitionListener)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -85,8 +45,9 @@ class ImageActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||
|
||||
override fun getItemCount(): Int = allImages.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment = ImageFragment.newInstance(allImages[position])
|
||||
}
|
||||
}
|
||||
}
|
@ -2,9 +2,7 @@ package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
@ -12,38 +10,39 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||
import com.russhwolf.settings.Settings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.acra.ACRA
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class LoginActivity : AppCompatActivity(), DIAware {
|
||||
private var inValidCount: Int = 0
|
||||
private var isWithLogin = false
|
||||
class LoginActivity() : AppCompatActivity(), DIAware {
|
||||
|
||||
private var inValidCount: Int = 0
|
||||
private var isWithSelfSignedCert = false
|
||||
private var isWithLogin = false
|
||||
private var isWithHTTPLogin = false
|
||||
|
||||
private val settings = Settings()
|
||||
private lateinit var appColors: AppColors
|
||||
private lateinit var binding: ActivityLoginBinding
|
||||
|
||||
override val di by closestDI()
|
||||
private val repository: Repository by instance()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
private val repository : Repository by instance()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
appColors = AppColors(this@LoginActivity)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
handleTheme()
|
||||
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
|
||||
@ -53,20 +52,22 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
||||
|
||||
handleBaseUrlFail()
|
||||
|
||||
if (appSettingsService.getBaseUrl().isNotEmpty()) {
|
||||
showProgress(true)
|
||||
if (settings.getString("url", "").isNotEmpty()) {
|
||||
goToMain()
|
||||
}
|
||||
|
||||
handleActions()
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant") // Constant is fetched from the settings
|
||||
private fun handleTheme() {
|
||||
AppCompatDelegate.setDefaultNightMode(appSettingsService.getCurrentTheme())
|
||||
}
|
||||
|
||||
private fun handleActions() {
|
||||
|
||||
binding.withSelfhostedCert.setOnCheckedChangeListener { _, b ->
|
||||
isWithSelfSignedCert = !isWithSelfSignedCert
|
||||
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||
|
||||
binding.warningText.visibility = visi
|
||||
}
|
||||
|
||||
binding.passwordView.setOnEditorActionListener(
|
||||
TextView.OnEditorActionListener { _, id, _ ->
|
||||
if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
|
||||
@ -74,7 +75,7 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
||||
return@OnEditorActionListener true
|
||||
}
|
||||
false
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
binding.signInButton.setOnClickListener { attemptLogin() }
|
||||
@ -86,6 +87,14 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
||||
binding.loginView.visibility = visi
|
||||
binding.passwordView.visibility = visi
|
||||
}
|
||||
|
||||
binding.withHttpLogin.setOnCheckedChangeListener { _, b ->
|
||||
isWithHTTPLogin = !isWithHTTPLogin
|
||||
val visi: Int = if (b) View.VISIBLE else View.GONE
|
||||
|
||||
binding.httpLoginView.visibility = visi
|
||||
binding.httpPasswordView.visibility = visi
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBaseUrlFail() {
|
||||
@ -95,107 +104,54 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
||||
alertDialog.setMessage(getString(R.string.base_url_error))
|
||||
alertDialog.setButton(
|
||||
AlertDialog.BUTTON_NEUTRAL,
|
||||
"OK",
|
||||
"OK"
|
||||
) { dialog, _ -> dialog.dismiss() }
|
||||
alertDialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun goToMain() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
repository.updateApiInformation()
|
||||
ACRA.errorReporter.putCustomData("SELFOSS_API_VERSION", appSettingsService.getApiVersion().toString())
|
||||
}
|
||||
val intent = Intent(this, HomeActivity::class.java)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun preferenceError() {
|
||||
appSettingsService.resetLoginInformation()
|
||||
|
||||
private fun preferenceError(t: Throwable) {
|
||||
settings.remove("url")
|
||||
settings.remove("login")
|
||||
settings.remove("httpUserName")
|
||||
settings.remove("password")
|
||||
settings.remove("httpPassword")
|
||||
binding.urlView.error = getString(R.string.wrong_infos)
|
||||
binding.loginView.error = getString(R.string.wrong_infos)
|
||||
binding.passwordView.error = getString(R.string.wrong_infos)
|
||||
binding.httpLoginView.error = getString(R.string.wrong_infos)
|
||||
binding.httpPasswordView.error = getString(R.string.wrong_infos)
|
||||
}
|
||||
|
||||
private fun attemptLogin() {
|
||||
|
||||
// Reset errors.
|
||||
binding.urlView.error = null
|
||||
binding.loginView.error = null
|
||||
binding.httpLoginView.error = null
|
||||
binding.passwordView.error = null
|
||||
binding.httpPasswordView.error = null
|
||||
|
||||
// Store values at the time of the login attempt.
|
||||
val url = binding.urlView.text.toString().trim()
|
||||
val login = binding.loginView.text.toString().trim()
|
||||
val password = binding.passwordView.text.toString().trim()
|
||||
val url = binding.urlView.text.toString()
|
||||
val login = binding.loginView.text.toString()
|
||||
val httpLogin = binding.httpLoginView.text.toString()
|
||||
val password = binding.passwordView.text.toString()
|
||||
val httpPassword = binding.httpPasswordView.text.toString()
|
||||
|
||||
failInvalidUrl(url)
|
||||
failLoginDetails(password, login)
|
||||
|
||||
showProgress(true)
|
||||
|
||||
appSettingsService.updateSelfSigned(binding.selfSigned.isChecked)
|
||||
|
||||
repository.refreshLoginInformation(url, login, password)
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
repository.updateApiInformation()
|
||||
} catch (e: Exception) {
|
||||
if (e.message?.startsWith("No transformation found") == true) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.application_selfoss_only,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
preferenceError()
|
||||
showProgress(false)
|
||||
}
|
||||
}
|
||||
val result = repository.login()
|
||||
if (result) {
|
||||
val errorFetching = repository.checkIfFetchFails()
|
||||
if (!errorFetching) {
|
||||
goToMain()
|
||||
} else {
|
||||
preferenceError()
|
||||
}
|
||||
} else {
|
||||
preferenceError()
|
||||
}
|
||||
showProgress(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun failLoginDetails(
|
||||
password: String,
|
||||
login: String,
|
||||
) {
|
||||
var lastFocusedView: View? = null
|
||||
var cancel = false
|
||||
if (isWithLogin) {
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
binding.passwordView.error = getString(R.string.error_invalid_password)
|
||||
lastFocusedView = binding.passwordView
|
||||
cancel = true
|
||||
}
|
||||
var focusView: View? = null
|
||||
|
||||
if (TextUtils.isEmpty(login)) {
|
||||
binding.loginView.error = getString(R.string.error_field_required)
|
||||
lastFocusedView = binding.loginView
|
||||
cancel = true
|
||||
}
|
||||
}
|
||||
maybeCancelAndFocusView(cancel, lastFocusedView)
|
||||
}
|
||||
|
||||
private fun failInvalidUrl(url: String) {
|
||||
val focusView = binding.urlView
|
||||
var cancel = false
|
||||
if (url.isBaseUrlInvalid()) {
|
||||
cancel = true
|
||||
if (!url.isBaseUrlValid(this@LoginActivity)) {
|
||||
binding.urlView.error = getString(R.string.login_url_problem)
|
||||
focusView = binding.urlView
|
||||
cancel = true
|
||||
inValidCount++
|
||||
if (inValidCount == 3) {
|
||||
val alertDialog = AlertDialog.Builder(this).create()
|
||||
@ -203,21 +159,59 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
||||
alertDialog.setMessage(getString(R.string.text_wrong_url))
|
||||
alertDialog.setButton(
|
||||
AlertDialog.BUTTON_NEUTRAL,
|
||||
"OK",
|
||||
"OK"
|
||||
) { dialog, _ -> dialog.dismiss() }
|
||||
alertDialog.show()
|
||||
inValidCount = 0
|
||||
}
|
||||
}
|
||||
maybeCancelAndFocusView(cancel, focusView)
|
||||
}
|
||||
|
||||
private fun maybeCancelAndFocusView(
|
||||
cancel: Boolean,
|
||||
focusView: View?,
|
||||
) {
|
||||
if (isWithLogin) {
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
binding.passwordView.error = getString(R.string.error_invalid_password)
|
||||
focusView = binding.passwordView
|
||||
cancel = true
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(login)) {
|
||||
binding.loginView.error = getString(R.string.error_field_required)
|
||||
focusView = binding.loginView
|
||||
cancel = true
|
||||
}
|
||||
}
|
||||
|
||||
if (isWithHTTPLogin) {
|
||||
if (TextUtils.isEmpty(httpPassword)) {
|
||||
binding.httpPasswordView.error = getString(R.string.error_invalid_password)
|
||||
focusView = binding.httpPasswordView
|
||||
cancel = true
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(httpLogin)) {
|
||||
binding.httpLoginView.error = getString(R.string.error_field_required)
|
||||
focusView = binding.httpLoginView
|
||||
cancel = true
|
||||
}
|
||||
}
|
||||
|
||||
if (cancel) {
|
||||
focusView?.requestFocus()
|
||||
} else {
|
||||
showProgress(true)
|
||||
|
||||
repository.refreshLoginInformation(url, login, password, httpLogin, httpPassword, isWithSelfSignedCert)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val result = repository.login()
|
||||
if (result) {
|
||||
goToMain()
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
preferenceError(Exception("Not success"))
|
||||
}
|
||||
}
|
||||
}
|
||||
showProgress(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,28 +223,26 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
||||
.animate()
|
||||
.setDuration(shortAnimTime.toLong())
|
||||
.alpha(
|
||||
if (show) 0F else 1F,
|
||||
).setListener(
|
||||
object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
},
|
||||
)
|
||||
if (show) 0F else 1F
|
||||
).setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
|
||||
binding.loginProgress
|
||||
.animate()
|
||||
.setDuration(shortAnimTime.toLong())
|
||||
.alpha(
|
||||
if (show) 1F else 0F,
|
||||
).setListener(
|
||||
object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
},
|
||||
)
|
||||
if (show) 1F else 0F
|
||||
).setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
@ -260,17 +252,10 @@ class LoginActivity : AppCompatActivity(), DIAware {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.issue_tracker -> {
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(AppSettingsService.trackerUrl))
|
||||
startActivity(browserIntent)
|
||||
return true
|
||||
}
|
||||
R.id.about -> {
|
||||
LibsBuilder()
|
||||
.withAboutIconShown(true)
|
||||
.withAboutVersionShown(true)
|
||||
.withAboutSpecial2("Bug reports").withAboutSpecial2Description(AppSettingsService.trackerUrl)
|
||||
.withAboutSpecial1("Project Page").withAboutSpecial1Description(AppSettingsService.sourceUrl)
|
||||
.start(this)
|
||||
true
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivityMainBinding
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
|
@ -3,49 +3,44 @@ package bou.amine.apps.readerforselfossv2.android
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import androidx.preference.PreferenceManager
|
||||
import bou.amine.apps.readerforselfossv2.DI.networkModule
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
|
||||
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
|
||||
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
|
||||
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.ftinc.scoop.Scoop
|
||||
import com.github.ln_12.library.ConnectivityStatus
|
||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||
import com.russhwolf.settings.Settings
|
||||
import io.github.aakira.napier.DebugAntilog
|
||||
import io.github.aakira.napier.Napier
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.acra.ACRA
|
||||
import org.acra.ReportField
|
||||
import org.acra.config.httpSender
|
||||
import org.acra.config.toast
|
||||
import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
import org.acra.sender.HttpSender
|
||||
import org.kodein.di.*
|
||||
|
||||
class MyApp : MultiDexApplication(), DIAware {
|
||||
|
||||
override val di by DI.lazy {
|
||||
bind<AppSettingsService>() with singleton { AppSettingsService(ACRA.isACRASenderServiceProcess()) }
|
||||
import(networkModule)
|
||||
bind<DriverFactory>() with singleton { DriverFactory(applicationContext) }
|
||||
bind<ReaderForSelfossDB>() with singleton { ReaderForSelfossDB(driverFactory.createDriver()) }
|
||||
bind<Repository>() with
|
||||
singleton {
|
||||
Repository(
|
||||
instance(),
|
||||
instance(),
|
||||
isConnectionAvailable,
|
||||
instance(),
|
||||
)
|
||||
}
|
||||
bind<Repository>() with singleton { Repository(instance(), instance(), connectivityStatus, instance()) }
|
||||
bind<ConnectivityStatus>() with singleton { ConnectivityStatus(applicationContext) }
|
||||
bind<AppViewModel>() with singleton { AppViewModel(repository = instance()) }
|
||||
}
|
||||
@ -54,90 +49,51 @@ class MyApp : MultiDexApplication(), DIAware {
|
||||
private val viewModel: AppViewModel by instance()
|
||||
private val connectivityStatus: ConnectivityStatus by instance()
|
||||
private val driverFactory: DriverFactory by instance()
|
||||
|
||||
// TODO: handle with the "previous" way
|
||||
private val isConnectionAvailable: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
private lateinit var config: Config
|
||||
private lateinit var settings : Settings
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Napier.base(DebugAntilog())
|
||||
config = Config()
|
||||
settings = Settings()
|
||||
|
||||
if (!ACRA.isACRASenderServiceProcess()) {
|
||||
tryToHandleBug()
|
||||
initDrawerImageLoader()
|
||||
|
||||
handleNotificationChannels()
|
||||
initTheme()
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(
|
||||
AppLifeCycleObserver(
|
||||
connectivityStatus,
|
||||
repository,
|
||||
),
|
||||
)
|
||||
tryToHandleBug()
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
viewModel.networkAvailableProvider.collect { networkAvailable ->
|
||||
val toastMessage =
|
||||
if (networkAvailable) {
|
||||
repository.handleDBActions()
|
||||
R.string.network_connectivity_retrieved
|
||||
} else {
|
||||
R.string.network_connectivity_lost
|
||||
}
|
||||
handleNotificationChannels()
|
||||
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
toastMessage,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifeCycleObserver(connectivityStatus, repository))
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
viewModel.networkAvailableProvider.collect { networkAvailable ->
|
||||
val toastMessage = if (networkAvailable) {
|
||||
repository.handleDBActions()
|
||||
R.string.network_connectivity_retrieved
|
||||
} else {
|
||||
R.string.network_connectivity_lost
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
toastMessage,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
viewModel.toastMessageProvider.collect { toastMessage ->
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
toastMessage,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
repository.migrate(driverFactory)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
|
||||
initAcra {
|
||||
reportFormat = StringFormat.JSON
|
||||
reportContent =
|
||||
listOf(
|
||||
ReportField.REPORT_ID,
|
||||
ReportField.INSTALLATION_ID,
|
||||
ReportField.APP_VERSION_CODE,
|
||||
ReportField.APP_VERSION_NAME,
|
||||
ReportField.BUILD,
|
||||
ReportField.ANDROID_VERSION,
|
||||
ReportField.BRAND,
|
||||
ReportField.PHONE_MODEL,
|
||||
ReportField.AVAILABLE_MEM_SIZE,
|
||||
ReportField.TOTAL_MEM_SIZE,
|
||||
ReportField.STACK_TRACE,
|
||||
ReportField.APPLICATION_LOG,
|
||||
ReportField.LOGCAT,
|
||||
ReportField.INITIAL_CONFIGURATION,
|
||||
ReportField.CRASH_CONFIGURATION,
|
||||
ReportField.IS_SILENT,
|
||||
ReportField.USER_APP_START_DATE,
|
||||
ReportField.USER_COMMENT,
|
||||
ReportField.USER_CRASH_DATE,
|
||||
ReportField.USER_EMAIL,
|
||||
ReportField.CUSTOM_DATA,
|
||||
)
|
||||
toast {
|
||||
// required
|
||||
text = getString(R.string.crash_toast_text)
|
||||
length = Toast.LENGTH_SHORT
|
||||
}
|
||||
httpSender {
|
||||
uri =
|
||||
"https://bugs.amine-bouabdallaoui.fr/report" // best guess, you may need to adjust this
|
||||
basicAuthLogin = "qMEscjj89Gwt6cPR"
|
||||
basicAuthPassword = "Yo58QFlGzFaWlBzP"
|
||||
httpMethod = HttpSender.Method.POST
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNotificationChannels() {
|
||||
@ -146,42 +102,60 @@ class MyApp : MultiDexApplication(), DIAware {
|
||||
|
||||
val name = getString(R.string.notification_channel_sync)
|
||||
val importance = NotificationManager.IMPORTANCE_LOW
|
||||
val mChannel = NotificationChannel(AppSettingsService.syncChannelId, name, importance)
|
||||
val mChannel = NotificationChannel(Config.syncChannelId, name, importance)
|
||||
|
||||
val newItemsChannelname = getString(R.string.new_items_channel_sync)
|
||||
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
val newItemsChannelmChannel =
|
||||
NotificationChannel(
|
||||
AppSettingsService.newItemsChannelId,
|
||||
newItemsChannelname,
|
||||
newItemsChannelimportance,
|
||||
)
|
||||
val newItemsChannelmChannel = NotificationChannel(Config.newItemsChannelId, newItemsChannelname, newItemsChannelimportance)
|
||||
|
||||
notificationManager.createNotificationChannel(mChannel)
|
||||
notificationManager.createNotificationChannel(newItemsChannelmChannel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDrawerImageLoader() {
|
||||
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
||||
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
|
||||
Glide.with(imageView.context)
|
||||
.loadMaybeBasicAuth(config, uri.toString())
|
||||
.apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
override fun cancel(imageView: ImageView) {
|
||||
Glide.with(imageView.context).clear(imageView)
|
||||
}
|
||||
|
||||
override fun placeholder(ctx: Context, tag: String?): Drawable {
|
||||
return baseContext.resources.getDrawable(R.mipmap.ic_launcher)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initTheme() {
|
||||
Scoop.waffleCone()
|
||||
.addFlavor(getString(R.string.default_theme), R.style.NoBar, true)
|
||||
.addFlavor(getString(R.string.default_dark_theme), R.style.NoBarDark, false)
|
||||
.setSharedPreferences(PreferenceManager.getDefaultSharedPreferences(this))
|
||||
.initialize()
|
||||
}
|
||||
|
||||
private fun tryToHandleBug() {
|
||||
val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, e ->
|
||||
if (e is NoClassDefFoundError &&
|
||||
e.stackTrace.asList().any {
|
||||
if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any {
|
||||
it.toString().contains("android.view.ViewDebug")
|
||||
}
|
||||
) {
|
||||
// Nothing
|
||||
}) {
|
||||
Unit
|
||||
} else {
|
||||
oldHandler.uncaughtException(thread, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppLifeCycleObserver(
|
||||
val connectivityStatus: ConnectivityStatus,
|
||||
val repository: Repository,
|
||||
) : DefaultLifecycleObserver {
|
||||
class AppLifeCycleObserver(val connectivityStatus: ConnectivityStatus, val repository: Repository) : DefaultLifecycleObserver {
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
super.onResume(owner)
|
||||
repository.connectionMonitored = true
|
||||
@ -194,4 +168,4 @@ class MyApp : MultiDexApplication(), DIAware {
|
||||
super.onPause(owner)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -12,9 +12,12 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityReaderBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.fragments.ArticleFragment
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import com.ftinc.scoop.Scoop
|
||||
import com.russhwolf.settings.Settings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -23,23 +26,27 @@ import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class ReaderActivity : AppCompatActivity(), DIAware {
|
||||
|
||||
private var markOnScroll: Boolean = false
|
||||
private var currentItem: Int = 0
|
||||
private lateinit var appColors: AppColors
|
||||
|
||||
private lateinit var toolbarMenu: Menu
|
||||
|
||||
private lateinit var binding: ActivityReaderBinding
|
||||
|
||||
private var allItems: ArrayList<SelfossModel.Item> = ArrayList()
|
||||
private var activeAlignment: Int = 1
|
||||
private val JUSTIFY = 1
|
||||
private val ALIGN_LEFT = 2
|
||||
|
||||
override val di by closestDI()
|
||||
private val repository: Repository by instance()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
|
||||
private fun showMenuItem(willAddToFavorite: Boolean) {
|
||||
if (willAddToFavorite) {
|
||||
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.WHITE)
|
||||
toolbarMenu.findItem(R.id.star).icon.setTint(Color.WHITE)
|
||||
} else {
|
||||
toolbarMenu.findItem(R.id.star).icon?.setTint(Color.RED)
|
||||
toolbarMenu.findItem(R.id.star).icon.setTint(Color.RED)
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,30 +58,34 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
||||
showMenuItem(false)
|
||||
}
|
||||
|
||||
private var settings = Settings()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
appColors = AppColors(this)
|
||||
binding = ActivityReaderBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
|
||||
setContentView(view)
|
||||
|
||||
val scoop = Scoop.getInstance()
|
||||
scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
|
||||
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
|
||||
|
||||
setSupportActionBar(binding.toolBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
markOnScroll = settings.getBoolean("mark_on_scroll", false)
|
||||
activeAlignment = settings.getInt("text_align", JUSTIFY)
|
||||
|
||||
if (allItems.isEmpty()) {
|
||||
finish()
|
||||
}
|
||||
|
||||
currentItem = intent.getIntExtra("currentItem", 0)
|
||||
|
||||
allItems = repository.getReaderItems()
|
||||
|
||||
if (allItems.isEmpty() || currentItem > allItems.size) {
|
||||
finish()
|
||||
}
|
||||
|
||||
try {
|
||||
readItem(allItems[currentItem])
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
finish()
|
||||
}
|
||||
readItem(allItems[currentItem])
|
||||
|
||||
binding.pager.adapter = ScreenSlidePagerAdapter(this)
|
||||
binding.pager.setCurrentItem(currentItem, false)
|
||||
@ -87,9 +98,10 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
||||
}
|
||||
|
||||
private fun readItem(item: SelfossModel.Item) {
|
||||
if (appSettingsService.isMarkOnScrollEnabled() && !appSettingsService.getPublicAccess()) {
|
||||
if (markOnScroll) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.markAsRead(item)
|
||||
// TODO: Handle failure
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -101,15 +113,15 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
||||
|
||||
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) :
|
||||
FragmentStateAdapter(fa) {
|
||||
|
||||
override fun getItemCount(): Int = allItems.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment = ArticleFragment.newInstance(allItems[position])
|
||||
override fun createFragment(position: Int): Fragment =
|
||||
ArticleFragment.newInstance(allItems[position])
|
||||
|
||||
}
|
||||
|
||||
override fun onKeyDown(
|
||||
keyCode: Int,
|
||||
event: KeyEvent?,
|
||||
): Boolean {
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
return when (keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
val currentFragment =
|
||||
@ -130,7 +142,7 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
||||
}
|
||||
|
||||
private fun alignmentMenu() {
|
||||
val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT
|
||||
val showJustify = activeAlignment == ALIGN_LEFT
|
||||
toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify
|
||||
toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify
|
||||
}
|
||||
@ -140,32 +152,28 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
||||
inflater.inflate(R.menu.reader_menu, menu)
|
||||
toolbarMenu = menu
|
||||
|
||||
if (allItems.isNotEmpty() && allItems[currentItem].starred) {
|
||||
canRemoveFromFavorite()
|
||||
} else {
|
||||
canFavorite()
|
||||
}
|
||||
alignmentMenu()
|
||||
|
||||
if (appSettingsService.getPublicAccess()) {
|
||||
menu.removeItem(R.id.star)
|
||||
} else {
|
||||
if (allItems.isNotEmpty() && allItems[currentItem].starred) {
|
||||
canRemoveFromFavorite()
|
||||
} else {
|
||||
canFavorite()
|
||||
}
|
||||
binding.pager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
|
||||
binding.pager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
|
||||
if (allItems[position].starred) {
|
||||
canRemoveFromFavorite()
|
||||
} else {
|
||||
canFavorite()
|
||||
}
|
||||
readItem(allItems[position])
|
||||
if (allItems[position].starred) {
|
||||
canRemoveFromFavorite()
|
||||
} else {
|
||||
canFavorite()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
readItem(allItems[position])
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
@ -184,36 +192,40 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
||||
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
R.id.star -> {
|
||||
if (allItems[binding.pager.currentItem].starred) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.unstarr(allItems[binding.pager.currentItem])
|
||||
// TODO: Handle failure
|
||||
}
|
||||
afterUnsave()
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.starr(allItems[binding.pager.currentItem])
|
||||
// TODO: Handle failure
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
}
|
||||
R.id.align_left -> {
|
||||
switchAlignmentSetting(AppSettingsService.ALIGN_LEFT)
|
||||
activeAlignment = ALIGN_LEFT
|
||||
switchAlignmentSetting()
|
||||
refreshFragment()
|
||||
}
|
||||
R.id.align_justify -> {
|
||||
switchAlignmentSetting(AppSettingsService.JUSTIFY)
|
||||
activeAlignment = JUSTIFY
|
||||
switchAlignmentSetting()
|
||||
refreshFragment()
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun switchAlignmentSetting(allignment: Int) {
|
||||
appSettingsService.changeAllignment(allignment)
|
||||
private fun switchAlignmentSetting() {
|
||||
settings.putInt("text_align", activeAlignment)
|
||||
alignmentMenu()
|
||||
}
|
||||
|
||||
@ -223,4 +235,8 @@ class ReaderActivity : AppCompatActivity(), DIAware {
|
||||
startActivity(intent)
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
|
||||
companion object {
|
||||
var allItems: ArrayList<SelfossModel.Item> = ArrayList()
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,11 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import com.ftinc.scoop.Scoop
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -18,15 +21,22 @@ import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class SourcesActivity : AppCompatActivity(), DIAware {
|
||||
|
||||
private lateinit var appColors: AppColors
|
||||
private lateinit var binding: ActivitySourcesBinding
|
||||
|
||||
override val di by closestDI()
|
||||
private val repository: Repository by instance()
|
||||
private val repository : Repository by instance()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
appColors = AppColors(this@SourcesActivity)
|
||||
binding = ActivitySourcesBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
|
||||
val scoop = Scoop.getInstance()
|
||||
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
|
||||
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(view)
|
||||
@ -35,8 +45,8 @@ class SourcesActivity : AppCompatActivity(), DIAware {
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
||||
binding.fab.rippleColor = resources.getColor(R.color.colorAccentDark)
|
||||
binding.fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
|
||||
binding.fab.rippleColor = appColors.colorAccentDark
|
||||
binding.fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
@ -48,33 +58,38 @@ class SourcesActivity : AppCompatActivity(), DIAware {
|
||||
super.onResume()
|
||||
val mLayoutManager = LinearLayoutManager(this)
|
||||
|
||||
var items: ArrayList<SelfossModel.SourceDetail>
|
||||
var items: ArrayList<SelfossModel.Source>
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = mLayoutManager
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val response = repository.getSourcesDetails()
|
||||
if (response.isNotEmpty()) {
|
||||
val response = repository.getSources()
|
||||
if (response != null) {
|
||||
items = response
|
||||
val mAdapter =
|
||||
SourcesListAdapter(
|
||||
this@SourcesActivity,
|
||||
items,
|
||||
)
|
||||
val mAdapter = SourcesListAdapter(
|
||||
this@SourcesActivity, items
|
||||
)
|
||||
binding.recyclerView.adapter = mAdapter
|
||||
mAdapter.notifyDataSetChanged()
|
||||
if (items.isEmpty()) {
|
||||
Toast.makeText(
|
||||
this@SourcesActivity,
|
||||
R.string.nothing_here,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@SourcesActivity,
|
||||
R.string.cant_get_sources,
|
||||
Toast.LENGTH_SHORT,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
binding.fab.setOnClickListener {
|
||||
startActivity(Intent(this@SourcesActivity, UpsertSourceActivity::class.java))
|
||||
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,210 +0,0 @@
|
||||
package bou.amine.apps.readerforselfossv2.android
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlInvalid
|
||||
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class UpsertSourceActivity : AppCompatActivity(), DIAware {
|
||||
private var existingSource: SelfossModel.SourceDetail? = null
|
||||
private var mSpoutsValue: String? = null
|
||||
|
||||
private lateinit var binding: ActivityUpsertSourceBinding
|
||||
|
||||
override val di by closestDI()
|
||||
private val repository: Repository by instance()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityUpsertSourceBinding.inflate(layoutInflater)
|
||||
val view = binding.root
|
||||
|
||||
existingSource = repository.getSelectedSource()
|
||||
if (existingSource != null) {
|
||||
binding.formContainer.visibility = View.GONE
|
||||
binding.progress.visibility = View.VISIBLE
|
||||
}
|
||||
val title = if (existingSource == null) R.string.add_source else R.string.update_source
|
||||
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
if (supportFragmentManager.backStackEntryCount == 0) {
|
||||
setTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
setContentView(view)
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
supportActionBar?.title = resources.getString(title)
|
||||
|
||||
maybeGetDetailsFromIntentSharing(intent)
|
||||
|
||||
binding.saveBtn.setOnClickListener {
|
||||
handleSaveSource()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initFields(items: Map<String, SelfossModel.Spout>) {
|
||||
binding.nameInput.setText(existingSource!!.title)
|
||||
binding.tags.setText(existingSource!!.tags?.joinToString(", "))
|
||||
binding.sourceUri.setText(existingSource!!.params?.url)
|
||||
binding.spoutsSpinner.setSelection(items.keys.indexOf(existingSource!!.spout))
|
||||
binding.progress.visibility = View.GONE
|
||||
binding.formContainer.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val baseUrl = appSettingsService.getBaseUrl()
|
||||
if (baseUrl.isEmpty() || baseUrl.isBaseUrlInvalid()) {
|
||||
mustLoginToAddSource()
|
||||
} else {
|
||||
handleSpoutsSpinner()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSpoutsSpinner() {
|
||||
val spoutsKV = HashMap<String, String>()
|
||||
binding.spoutsSpinner.onItemSelectedListener =
|
||||
object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
adapterView: AdapterView<*>,
|
||||
view: View?,
|
||||
i: Int,
|
||||
l: Long,
|
||||
) {
|
||||
if (view != null) {
|
||||
val spoutName = (view as TextView).text.toString()
|
||||
mSpoutsValue = spoutsKV[spoutName]
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(adapterView: AdapterView<*>) {
|
||||
mSpoutsValue = null
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSpoutFailure(networkIssue: Boolean = false) {
|
||||
Toast.makeText(
|
||||
this@UpsertSourceActivity,
|
||||
if (networkIssue) R.string.cant_get_spouts_no_network else R.string.cant_get_spouts,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
binding.progress.visibility = View.GONE
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val items = repository.getSpouts()
|
||||
if (items.isNotEmpty()) {
|
||||
val itemsStrings = items.map { it.value.name }
|
||||
for ((key, value) in items) {
|
||||
spoutsKV[value.name] = key
|
||||
}
|
||||
|
||||
binding.progress.visibility = View.GONE
|
||||
binding.formContainer.visibility = View.VISIBLE
|
||||
|
||||
val spinnerArrayAdapter =
|
||||
ArrayAdapter(
|
||||
this@UpsertSourceActivity,
|
||||
android.R.layout.simple_spinner_item,
|
||||
itemsStrings,
|
||||
)
|
||||
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
binding.spoutsSpinner.adapter = spinnerArrayAdapter
|
||||
|
||||
if (existingSource != null) {
|
||||
initFields(items)
|
||||
}
|
||||
} else {
|
||||
handleSpoutFailure()
|
||||
}
|
||||
} catch (e: NetworkUnavailableException) {
|
||||
handleSpoutFailure(networkIssue = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeGetDetailsFromIntentSharing(intent: Intent) {
|
||||
if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
|
||||
binding.sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
|
||||
binding.nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
|
||||
}
|
||||
}
|
||||
|
||||
private fun mustLoginToAddSource() {
|
||||
Toast.makeText(this, getString(R.string.addStringNoUrl), Toast.LENGTH_SHORT).show()
|
||||
val i = Intent(this, LoginActivity::class.java)
|
||||
startActivity(i)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun handleSaveSource() {
|
||||
val url = binding.sourceUri.text.toString()
|
||||
|
||||
val sourceDetailsUnavailable =
|
||||
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
|
||||
|
||||
when {
|
||||
sourceDetailsUnavailable -> {
|
||||
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
else -> {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val successfullyAddedSource =
|
||||
if (existingSource != null) {
|
||||
repository.updateSource(
|
||||
existingSource!!.id,
|
||||
binding.nameInput.text.toString(),
|
||||
url,
|
||||
mSpoutsValue!!,
|
||||
binding.tags.text.toString(),
|
||||
)
|
||||
} else {
|
||||
repository.createSource(
|
||||
binding.nameInput.text.toString(),
|
||||
url,
|
||||
mSpoutsValue!!,
|
||||
binding.tags.text.toString(),
|
||||
)
|
||||
}
|
||||
if (successfullyAddedSource) {
|
||||
finish()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@UpsertSourceActivity,
|
||||
R.string.cant_create_source,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
repository.unsetSelectedSource()
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -8,18 +9,19 @@ import android.widget.ImageView.ScaleType
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
|
||||
import bou.amine.apps.readerforselfossv2.android.model.*
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.*
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
|
||||
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import com.bumptech.glide.Glide
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -30,80 +32,42 @@ import org.kodein.di.instance
|
||||
|
||||
class ItemCardAdapter(
|
||||
override val app: Activity,
|
||||
override val items: ArrayList<SelfossModel.Item>,
|
||||
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
|
||||
override var items: ArrayList<SelfossModel.Item>,
|
||||
private val helper: CustomTabActivityHelper,
|
||||
private val internalBrowser: Boolean,
|
||||
private val articleViewer: Boolean,
|
||||
private val fullHeightCards: Boolean,
|
||||
override val appColors: AppColors,
|
||||
override val config: Config,
|
||||
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
|
||||
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
|
||||
override lateinit var binding: CardItemBinding
|
||||
private val c: Context = app.baseContext
|
||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||
private val imageMaxHeight: Int =
|
||||
c.resources.getDimension(R.dimen.card_image_max_height).toInt()
|
||||
|
||||
override val di: DI by closestDI(app)
|
||||
override val repository: Repository by instance()
|
||||
override val appSettingsService: AppSettingsService by instance()
|
||||
override val repository : Repository by instance()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ViewHolder {
|
||||
binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
private fun handleClickListeners(holderBinding: CardItemBinding, position: Int) {
|
||||
holderBinding.favButton.setOnClickListener {
|
||||
val item = items[position]
|
||||
if (item.starred) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.unstarr(item)
|
||||
}
|
||||
binding.favButton.isSelected = false
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.starr(item)
|
||||
}
|
||||
binding.favButton.isSelected = true
|
||||
}
|
||||
}
|
||||
|
||||
binding.shareBtn.setOnClickListener {
|
||||
val item = items[position]
|
||||
c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded())
|
||||
}
|
||||
|
||||
binding.browserBtn.setOnClickListener {
|
||||
c.openInBrowserAsNewTask(items[position])
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
with(holder) {
|
||||
val itm = items[position]
|
||||
|
||||
handleClickListeners(binding, position)
|
||||
handleLinkOpening(binding, position)
|
||||
|
||||
binding.favButton.isSelected = itm.starred
|
||||
if (appSettingsService.getPublicAccess()) {
|
||||
binding.favButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
binding.title.text = itm.title.getHtmlDecoded()
|
||||
|
||||
binding.title.setOnTouchListener(LinkOnTouchListener())
|
||||
|
||||
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
|
||||
binding.title.setLinkTextColor(appColors.colorAccent)
|
||||
|
||||
binding.sourceTitleAndDate.text = try {
|
||||
itm.sourceAuthorAndDate()
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcraWithName("ItemCardAdapter parse date")
|
||||
itm.sourceAuthorOnly()
|
||||
}
|
||||
binding.sourceTitleAndDate.text = itm.sourceAndDateText(repository.dateUtils)
|
||||
|
||||
if (!appSettingsService.isFullHeightCardsEnabled()) {
|
||||
if (!fullHeightCards) {
|
||||
binding.itemImage.maxHeight = imageMaxHeight
|
||||
binding.itemImage.scaleType = ScaleType.CENTER_CROP
|
||||
}
|
||||
@ -114,16 +78,80 @@ class ItemCardAdapter(
|
||||
binding.itemImage.setImageDrawable(null)
|
||||
} else {
|
||||
binding.itemImage.visibility = View.VISIBLE
|
||||
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage)
|
||||
c.bitmapCenterCrop(config, itm.getThumbnail(repository.baseUrl), binding.itemImage)
|
||||
}
|
||||
|
||||
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
||||
binding.sourceImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
|
||||
val color = generator.getColor(itm.title.getHtmlDecoded())
|
||||
|
||||
val drawable =
|
||||
TextDrawable
|
||||
.builder()
|
||||
.round()
|
||||
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
|
||||
binding.sourceImage.setImageDrawable(drawable)
|
||||
} else {
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
|
||||
c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.sourceImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
handleClickListeners()
|
||||
handleCustomTabActions()
|
||||
}
|
||||
|
||||
private fun handleClickListeners() {
|
||||
|
||||
binding.favButton.setOnClickListener {
|
||||
val item = items[bindingAdapterPosition]
|
||||
if (item.starred) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.unstarr(item)
|
||||
// TODO: Handle failure
|
||||
}
|
||||
item.starred = false
|
||||
binding.favButton.isSelected = false
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.starr(item)
|
||||
// TODO: Handle failure
|
||||
}
|
||||
item.starred = true
|
||||
binding.favButton.isSelected = true
|
||||
}
|
||||
}
|
||||
|
||||
binding.shareBtn.setOnClickListener {
|
||||
val item = items[bindingAdapterPosition]
|
||||
c.shareLink(item.getLinkDecoded(), item.title.getHtmlDecoded())
|
||||
}
|
||||
|
||||
binding.browserBtn.setOnClickListener {
|
||||
c.openInBrowserAsNewTask(items[bindingAdapterPosition])
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCustomTabActions() {
|
||||
val customTabsIntent = c.buildCustomTabsIntent()
|
||||
helper.bindCustomTabsService(app)
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
c.openItemUrl(
|
||||
items,
|
||||
bindingAdapterPosition,
|
||||
items[bindingAdapterPosition].getLinkDecoded(),
|
||||
customTabsIntent,
|
||||
internalBrowser,
|
||||
articleViewer,
|
||||
app
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,76 +1,105 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.android.model.*
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.*
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
|
||||
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class ItemListAdapter(
|
||||
override val app: Activity,
|
||||
override val items: ArrayList<SelfossModel.Item>,
|
||||
override val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit,
|
||||
override var items: ArrayList<SelfossModel.Item>,
|
||||
private val helper: CustomTabActivityHelper,
|
||||
private val internalBrowser: Boolean,
|
||||
private val articleViewer: Boolean,
|
||||
override val appColors: AppColors,
|
||||
override val config: Config,
|
||||
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
|
||||
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
|
||||
override lateinit var binding: ListItemBinding
|
||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||
private val c: Context = app.baseContext
|
||||
|
||||
override val di: DI by closestDI(app)
|
||||
override val repository: Repository by instance()
|
||||
override val appSettingsService: AppSettingsService by instance()
|
||||
override val repository : Repository by instance()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ViewHolder {
|
||||
binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
with(holder) {
|
||||
val itm = items[position]
|
||||
|
||||
handleLinkOpening(binding, position)
|
||||
|
||||
binding.title.text = itm.title.getHtmlDecoded()
|
||||
|
||||
binding.title.setOnTouchListener(LinkOnTouchListener())
|
||||
|
||||
binding.title.setLinkTextColor(c.resources.getColor(R.color.colorAccent))
|
||||
binding.title.setLinkTextColor(appColors.colorAccent)
|
||||
|
||||
binding.sourceTitleAndDate.text = try {
|
||||
itm.sourceAuthorAndDate()
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcraWithName("ItemListAdapter parse date")
|
||||
itm.sourceAuthorOnly()
|
||||
}
|
||||
binding.sourceTitleAndDate.text = itm.sourceAndDateText(repository.dateUtils)
|
||||
|
||||
if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
|
||||
|
||||
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
||||
binding.itemImage.setBackgroundAndText(itm.sourcetitle.getHtmlDecoded())
|
||||
val color = generator.getColor(itm.title.getHtmlDecoded())
|
||||
|
||||
val drawable =
|
||||
TextDrawable
|
||||
.builder()
|
||||
.round()
|
||||
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
|
||||
|
||||
binding.itemImage.setImageDrawable(drawable)
|
||||
} else {
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
|
||||
c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage)
|
||||
}
|
||||
} else {
|
||||
c.circularDrawable(itm.getThumbnail(repository.baseUrl), binding.itemImage)
|
||||
c.bitmapCenterCrop(config, itm.getThumbnail(repository.baseUrl), binding.itemImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
init {
|
||||
handleCustomTabActions()
|
||||
}
|
||||
|
||||
private fun handleCustomTabActions() {
|
||||
val customTabsIntent = c.buildCustomTabsIntent()
|
||||
helper.bindCustomTabsService(app)
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
c.openItemUrl(
|
||||
items,
|
||||
bindingAdapterPosition,
|
||||
items[bindingAdapterPosition].getLinkDecoded(),
|
||||
customTabsIntent,
|
||||
internalBrowser,
|
||||
articleViewer,
|
||||
app
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,14 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.utils.ItemType
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -19,134 +17,105 @@ import kotlinx.coroutines.launch
|
||||
import org.kodein.di.DIAware
|
||||
|
||||
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware {
|
||||
abstract val items: ArrayList<SelfossModel.Item>
|
||||
abstract var items: ArrayList<SelfossModel.Item>
|
||||
abstract val repository: Repository
|
||||
abstract val binding: ViewBinding
|
||||
abstract val appSettingsService: AppSettingsService
|
||||
abstract val app: Activity
|
||||
abstract val updateHomeItems: (ArrayList<SelfossModel.Item>) -> Unit
|
||||
|
||||
protected val c: Context get() = app.baseContext
|
||||
abstract val appColors: AppColors
|
||||
abstract val config: Config
|
||||
abstract val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
|
||||
|
||||
fun updateAllItems(items: ArrayList<SelfossModel.Item>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
updateHomeItems(items)
|
||||
this.items = items
|
||||
notifyDataSetChanged()
|
||||
updateItems(this.items)
|
||||
}
|
||||
|
||||
private fun unmarkSnackbar(
|
||||
item: SelfossModel.Item,
|
||||
position: Int,
|
||||
) {
|
||||
val s =
|
||||
Snackbar
|
||||
.make(
|
||||
app.findViewById(R.id.coordLayout),
|
||||
R.string.marked_as_read,
|
||||
Snackbar.LENGTH_LONG,
|
||||
)
|
||||
.setAction(R.string.undo_string) {
|
||||
unreadItemAtIndex(item, position, false)
|
||||
}
|
||||
|
||||
val view = s.view
|
||||
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
|
||||
tv.setTextColor(Color.WHITE)
|
||||
s.show()
|
||||
}
|
||||
|
||||
private fun markSnackbar(
|
||||
item: SelfossModel.Item,
|
||||
position: Int,
|
||||
) {
|
||||
val s =
|
||||
Snackbar
|
||||
.make(
|
||||
app.findViewById(R.id.coordLayout),
|
||||
R.string.marked_as_unread,
|
||||
Snackbar.LENGTH_LONG,
|
||||
)
|
||||
.setAction(R.string.undo_string) {
|
||||
readItemAtIndex(item, position, false)
|
||||
}
|
||||
|
||||
val view = s.view
|
||||
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
|
||||
tv.setTextColor(Color.WHITE)
|
||||
s.show()
|
||||
}
|
||||
|
||||
protected fun handleLinkOpening(holderBinding: ViewBinding, position: Int) {
|
||||
holderBinding.root.setOnClickListener {
|
||||
repository.setReaderItems(items)
|
||||
c.openItemUrl(
|
||||
position,
|
||||
items[position].getLinkDecoded(),
|
||||
appSettingsService.isArticleViewerEnabled(),
|
||||
app,
|
||||
private fun unmarkSnackbar(i: SelfossModel.Item, position: Int) {
|
||||
val s = Snackbar
|
||||
.make(
|
||||
app.findViewById(R.id.coordLayout),
|
||||
R.string.marked_as_read,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
}
|
||||
.setAction(R.string.undo_string) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
unreadItemAtIndex(position, false)
|
||||
}
|
||||
}
|
||||
|
||||
val view = s.view
|
||||
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
|
||||
tv.setTextColor(Color.WHITE)
|
||||
s.show()
|
||||
}
|
||||
|
||||
private fun markSnackbar(position: Int) {
|
||||
val s = Snackbar
|
||||
.make(
|
||||
app.findViewById(R.id.coordLayout),
|
||||
R.string.marked_as_unread,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setAction(R.string.undo_string) {
|
||||
readItemAtIndex(position)
|
||||
}
|
||||
|
||||
val view = s.view
|
||||
val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
|
||||
tv.setTextColor(Color.WHITE)
|
||||
s.show()
|
||||
}
|
||||
|
||||
fun handleItemAtIndex(position: Int) {
|
||||
if (items[position].unread) {
|
||||
readItemAtIndex(items[position], position)
|
||||
readItemAtIndex(position)
|
||||
} else {
|
||||
unreadItemAtIndex(items[position], position)
|
||||
unreadItemAtIndex(position)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readItemAtIndex(
|
||||
item: SelfossModel.Item,
|
||||
position: Int,
|
||||
showSnackbar: Boolean = true,
|
||||
) {
|
||||
private fun readItemAtIndex(position: Int, showSnackbar: Boolean = true) {
|
||||
val i = items[position]
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.markAsRead(item)
|
||||
repository.markAsRead(i)
|
||||
}
|
||||
if (repository.displayedItems == ItemType.UNREAD) {
|
||||
items.remove(item)
|
||||
items.remove(i)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, itemCount)
|
||||
updateHomeItems(items)
|
||||
updateItems(items)
|
||||
} else {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
if (showSnackbar) {
|
||||
unmarkSnackbar(item, position)
|
||||
unmarkSnackbar(i, position)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unreadItemAtIndex(
|
||||
item: SelfossModel.Item,
|
||||
position: Int,
|
||||
showSnackbar: Boolean = true,
|
||||
) {
|
||||
private fun unreadItemAtIndex(position: Int, showSnackbar: Boolean = true) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.unmarkAsRead(item)
|
||||
repository.unmarkAsRead(items[position])
|
||||
// Todo: SharedItems.unreadItem(app, api, db, items[position])
|
||||
// TODO: update db
|
||||
|
||||
}
|
||||
notifyItemChanged(position)
|
||||
if (showSnackbar) {
|
||||
markSnackbar(item, position)
|
||||
markSnackbar(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun addItemAtIndex(
|
||||
item: SelfossModel.Item,
|
||||
position: Int,
|
||||
) {
|
||||
fun addItemAtIndex(item: SelfossModel.Item, position: Int) {
|
||||
items.add(position, item)
|
||||
notifyItemInserted(position)
|
||||
updateHomeItems(items)
|
||||
updateItems(items)
|
||||
|
||||
}
|
||||
|
||||
fun addItemsAtEnd(newItems: List<SelfossModel.Item>) {
|
||||
val oldSize = items.size
|
||||
items.addAll(newItems)
|
||||
notifyItemRangeInserted(oldSize, newItems.size)
|
||||
updateHomeItems(items)
|
||||
}
|
||||
updateItems(items)
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
}
|
||||
}
|
||||
}
|
@ -2,22 +2,23 @@ package bou.amine.apps.readerforselfossv2.android.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.Toast
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -28,76 +29,70 @@ import org.kodein.di.instance
|
||||
|
||||
class SourcesListAdapter(
|
||||
private val app: Activity,
|
||||
private val items: ArrayList<SelfossModel.SourceDetail>,
|
||||
private val items: ArrayList<SelfossModel.Source>
|
||||
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
|
||||
private val c: Context = app.baseContext
|
||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||
private lateinit var config: Config
|
||||
private lateinit var binding: SourceListItemBinding
|
||||
|
||||
override val di: DI by closestDI(app)
|
||||
private val repository: Repository by instance()
|
||||
private val repository : Repository by instance()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ViewHolder {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return ViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val itm = items[position]
|
||||
|
||||
val deleteBtn: Button = holder.mView.findViewById(R.id.deleteBtn)
|
||||
|
||||
deleteBtn.setOnClickListener {
|
||||
val (id, title) = items[position]
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val successfullyDeletedSource = repository.deleteSource(id, title)
|
||||
if (successfullyDeletedSource) {
|
||||
items.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemRangeChanged(position, itemCount)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
app,
|
||||
R.string.can_delete_source,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder.mView.setOnClickListener {
|
||||
val source = items[position]
|
||||
|
||||
repository.setSelectedSource(source)
|
||||
app.startActivity(Intent(app, UpsertSourceActivity::class.java))
|
||||
}
|
||||
config = Config()
|
||||
|
||||
if (itm.getIcon(repository.baseUrl).isEmpty()) {
|
||||
binding.itemImage.setBackgroundAndText(itm.title.getHtmlDecoded())
|
||||
} else {
|
||||
c.circularDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
|
||||
}
|
||||
val color = generator.getColor(itm.title.getHtmlDecoded())
|
||||
|
||||
if (!itm.error.isNullOrBlank()) {
|
||||
binding.errorText.visibility = View.VISIBLE
|
||||
binding.errorText.text = itm.error
|
||||
val drawable =
|
||||
TextDrawable
|
||||
.builder()
|
||||
.round()
|
||||
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
|
||||
binding.itemImage.setImageDrawable(drawable)
|
||||
} else {
|
||||
binding.errorText.visibility = View.GONE
|
||||
c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage)
|
||||
}
|
||||
|
||||
binding.sourceTitle.text = itm.title.getHtmlDecoded()
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int) = position.toLong()
|
||||
|
||||
override fun getItemViewType(position: Int) = position
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView)
|
||||
inner class ViewHolder(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
|
||||
|
||||
init {
|
||||
handleClickListeners()
|
||||
}
|
||||
|
||||
private fun handleClickListeners() {
|
||||
|
||||
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
|
||||
|
||||
deleteBtn.setOnClickListener {
|
||||
val (id) = items[adapterPosition]
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val successfullyDeletedSource = repository.deleteSource(id)
|
||||
if (successfullyDeletedSource) {
|
||||
items.removeAt(adapterPosition)
|
||||
notifyItemRemoved(adapterPosition)
|
||||
notifyItemRangeChanged(adapterPosition, itemCount)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
app,
|
||||
R.string.can_delete_source,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.api.mercury
|
||||
|
||||
import com.google.gson.GsonBuilder
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Call
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
|
||||
class MercuryApi() {
|
||||
private val service: MercuryService
|
||||
|
||||
init {
|
||||
|
||||
val interceptor = HttpLoggingInterceptor()
|
||||
interceptor.level = HttpLoggingInterceptor.Level.NONE
|
||||
val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
|
||||
|
||||
val gson = GsonBuilder()
|
||||
.setLenient()
|
||||
.create()
|
||||
val retrofit =
|
||||
Retrofit
|
||||
.Builder()
|
||||
.baseUrl("https://www.amine-louveau.fr")
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
service = retrofit.create(MercuryService::class.java)
|
||||
}
|
||||
|
||||
fun parseUrl(url: String): Call<ParsedContent> {
|
||||
return service.parseUrl(url)
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.api.mercury
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class ParsedContent(
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("content") val content: String?,
|
||||
@SerializedName("date_published") val date_published: String,
|
||||
@SerializedName("lead_image_url") val lead_image_url: String?,
|
||||
@SerializedName("dek") val dek: String,
|
||||
@SerializedName("url") val url: String,
|
||||
@SerializedName("domain") val domain: String,
|
||||
@SerializedName("excerpt") val excerpt: String,
|
||||
@SerializedName("total_pages") val total_pages: Int,
|
||||
@SerializedName("rendered_pages") val rendered_pages: Int,
|
||||
@SerializedName("next_page_url") val next_page_url: String
|
||||
) : Parcelable {
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<ParsedContent> =
|
||||
object : Parcelable.Creator<ParsedContent> {
|
||||
override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
|
||||
override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(source: Parcel) : this(
|
||||
title = source.readString().orEmpty(),
|
||||
content = source.readString(),
|
||||
date_published = source.readString().orEmpty(),
|
||||
lead_image_url = source.readString(),
|
||||
dek = source.readString().orEmpty(),
|
||||
url = source.readString().orEmpty(),
|
||||
domain = source.readString().orEmpty(),
|
||||
excerpt = source.readString().orEmpty(),
|
||||
total_pages = source.readInt(),
|
||||
rendered_pages = source.readInt(),
|
||||
next_page_url = source.readString().orEmpty()
|
||||
)
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeString(title)
|
||||
dest.writeString(content)
|
||||
dest.writeString(date_published)
|
||||
dest.writeString(lead_image_url)
|
||||
dest.writeString(dek)
|
||||
dest.writeString(url)
|
||||
dest.writeString(domain)
|
||||
dest.writeString(excerpt)
|
||||
dest.writeInt(total_pages)
|
||||
dest.writeInt(rendered_pages)
|
||||
dest.writeString(next_page_url)
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.api.mercury
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface MercuryService {
|
||||
@GET("parser.php")
|
||||
fun parseUrl(@Query("link") link: String): Call<ParsedContent>
|
||||
}
|
@ -14,10 +14,12 @@ import bou.amine.apps.readerforselfossv2.android.MainActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.MyApp
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.model.preloadImages
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible
|
||||
import bou.amine.apps.readerforselfossv2.dao.ACTION
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import com.russhwolf.settings.Settings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -25,92 +27,90 @@ import org.kodein.di.DIAware
|
||||
import org.kodein.di.instance
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params), DIAware {
|
||||
|
||||
class LoadingWorker(val context: Context, params: WorkerParameters) :
|
||||
Worker(context, params),
|
||||
DIAware {
|
||||
override val di by lazy { (applicationContext as MyApp).di }
|
||||
private val repository: Repository by instance()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
private val repository : Repository by instance()
|
||||
|
||||
override fun doWork(): Result {
|
||||
if (appSettingsService.isPeriodicRefreshEnabled() && isNetworkAccessible(context)) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val notificationManager =
|
||||
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
override fun doWork(): Result {
|
||||
val settings = Settings()
|
||||
val periodicRefresh = settings.getBoolean("periodic_refresh", false)
|
||||
if (periodicRefresh && isNetworkAccessible(context)) {
|
||||
|
||||
val notification =
|
||||
NotificationCompat.Builder(applicationContext, AppSettingsService.syncChannelId)
|
||||
.setContentTitle(context.getString(R.string.loading_notification_title))
|
||||
.setContentText(context.getString(R.string.loading_notification_text))
|
||||
.setOngoing(true)
|
||||
.setPriority(PRIORITY_LOW)
|
||||
.setChannelId(AppSettingsService.syncChannelId)
|
||||
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val notificationManager =
|
||||
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
notificationManager.notify(1, notification.build())
|
||||
val notification =
|
||||
NotificationCompat.Builder(applicationContext, Config.syncChannelId)
|
||||
.setContentTitle(context.getString(R.string.loading_notification_title))
|
||||
.setContentText(context.getString(R.string.loading_notification_text))
|
||||
.setOngoing(true)
|
||||
.setPriority(PRIORITY_LOW)
|
||||
.setChannelId(Config.syncChannelId)
|
||||
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
|
||||
|
||||
repository.handleDBActions()
|
||||
notificationManager.notify(1, notification.build())
|
||||
|
||||
val apiItems = repository.tryToCacheItemsAndGetNewOnes()
|
||||
if (appSettingsService.isNotifyNewItemsEnabled()) {
|
||||
launch {
|
||||
handleNewItemsNotification(apiItems, notificationManager)
|
||||
}
|
||||
}
|
||||
apiItems.map { it.preloadImages(context) }
|
||||
val notifyNewItems = settings.getBoolean("notify_new_items", false)
|
||||
|
||||
repository.handleDBActions()
|
||||
|
||||
launch {
|
||||
handleNewItemsNotification(repository.tryToCacheItemsAndGetNewOnes(), notifyNewItems, notificationManager)
|
||||
}
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun handleNewItemsNotification(
|
||||
newItems: List<SelfossModel.Item>?,
|
||||
notificationManager: NotificationManager,
|
||||
notifyNewItems: Boolean,
|
||||
notificationManager: NotificationManager
|
||||
) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val apiItems = newItems.orEmpty()
|
||||
val apiItems = newItems.orEmpty()
|
||||
|
||||
val newSize = apiItems.filter { it.unread }.size
|
||||
if (newSize > 0) {
|
||||
val intent =
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
|
||||
val newSize = apiItems.filter { it.unread }.size
|
||||
if (notifyNewItems && newSize > 0) {
|
||||
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
val pflags =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val pendingIntent: PendingIntent =
|
||||
PendingIntent.getActivity(context, 0, intent, pflags)
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, pflags)
|
||||
|
||||
val newItemsNotification =
|
||||
NotificationCompat.Builder(
|
||||
applicationContext,
|
||||
AppSettingsService.newItemsChannelId,
|
||||
)
|
||||
.setContentTitle(context.getString(R.string.new_items_notification_title))
|
||||
.setContentText(
|
||||
context.getString(
|
||||
R.string.new_items_notification_text,
|
||||
newSize,
|
||||
),
|
||||
)
|
||||
.setPriority(PRIORITY_DEFAULT)
|
||||
.setChannelId(AppSettingsService.newItemsChannelId)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
|
||||
val newItemsNotification =
|
||||
NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
|
||||
.setContentTitle(context.getString(R.string.new_items_notification_title))
|
||||
.setContentText(
|
||||
context.getString(
|
||||
R.string.new_items_notification_text,
|
||||
newSize
|
||||
)
|
||||
)
|
||||
.setPriority(PRIORITY_DEFAULT)
|
||||
.setChannelId(Config.newItemsChannelId)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
|
||||
|
||||
Timer("", false).schedule(4000) {
|
||||
notificationManager.notify(2, newItemsNotification.build())
|
||||
Timer("", false).schedule(4000) {
|
||||
notificationManager.notify(2, newItemsNotification.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
apiItems.map { it.preloadImages(context) }
|
||||
Timer("", false).schedule(4000) {
|
||||
notificationManager.cancel(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.fragments
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.TypedArray
|
||||
@ -9,7 +8,6 @@ import android.graphics.Typeface
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
@ -17,24 +15,23 @@ import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.fragment.app.Fragment
|
||||
import bou.amine.apps.readerforselfossv2.android.ImageActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.api.mercury.MercuryApi
|
||||
import bou.amine.apps.readerforselfossv2.android.api.mercury.ParsedContent
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.model.ParecelableItem
|
||||
import bou.amine.apps.readerforselfossv2.android.model.toModel
|
||||
import bou.amine.apps.readerforselfossv2.android.model.toParcelable
|
||||
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
|
||||
import bou.amine.apps.readerforselfossv2.android.model.*
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.*
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.isUrlValid
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
|
||||
import bou.amine.apps.readerforselfossv2.model.MercuryModel
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.rest.MercuryApi
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||
import bou.amine.apps.readerforselfossv2.utils.getImages
|
||||
import bou.amine.apps.readerforselfossv2.utils.getThumbnail
|
||||
@ -44,6 +41,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.russhwolf.settings.Settings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -51,38 +49,52 @@ import org.kodein.di.DI
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.x.closestDI
|
||||
import org.kodein.di.instance
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
private const val IMAGE_JPG = "image/jpg"
|
||||
|
||||
class ArticleFragment : Fragment(), DIAware {
|
||||
private var fontSize: Int = 16
|
||||
private lateinit var item: SelfossModel.Item
|
||||
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
|
||||
private lateinit var url: String
|
||||
private lateinit var contentText: String
|
||||
private lateinit var contentSource: String
|
||||
private lateinit var contentImage: String
|
||||
private lateinit var contentTitle: String
|
||||
private lateinit var allImages: ArrayList<String>
|
||||
private lateinit var allImages : ArrayList<String>
|
||||
private lateinit var fab: FloatingActionButton
|
||||
private lateinit var appColors: AppColors
|
||||
private lateinit var textAlignment: String
|
||||
private lateinit var binding: FragmentArticleBinding
|
||||
private lateinit var config: Config
|
||||
private var _binding: FragmentArticleBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override val di: DI by closestDI()
|
||||
override val di : DI by closestDI()
|
||||
private val repository: Repository by instance()
|
||||
private val appSettingsService: AppSettingsService by instance()
|
||||
|
||||
private var settings = Settings()
|
||||
|
||||
private var typeface: Typeface? = null
|
||||
private var resId: Int = 0
|
||||
private var font = ""
|
||||
private var staticBar = false
|
||||
|
||||
private val mercuryApi: MercuryApi by instance()
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (mCustomTabActivityHelper != null) {
|
||||
mCustomTabActivityHelper!!.unbindCustomTabsService(activity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
appColors = AppColors(requireActivity())
|
||||
config = Config()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val pi: ParecelableItem = requireArguments().getParcelable(ARG_ITEMS)!!
|
||||
@ -93,36 +105,88 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
try {
|
||||
binding = FragmentArticleBinding.inflate(inflater, container, false)
|
||||
_binding = FragmentArticleBinding.inflate(inflater, container, false)
|
||||
|
||||
url = item.getLinkDecoded()
|
||||
contentText = item.content
|
||||
contentTitle = item.title.getHtmlDecoded()
|
||||
contentImage = item.getThumbnail(repository.baseUrl)
|
||||
contentSource = try {
|
||||
item.sourceAuthorAndDate()
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcraWithName("Article Fragment parse date")
|
||||
item.sourceAuthorOnly()
|
||||
}
|
||||
contentSource = item.sourceAndDateText(repository.dateUtils)
|
||||
allImages = item.getImages()
|
||||
|
||||
fontSize = appSettingsService.getFontSize()
|
||||
staticBar = appSettingsService.isStaticBarEnabled()
|
||||
font = appSettingsService.getFont()
|
||||
fontSize = settings.getString("reader_font_size", "16").toInt()
|
||||
staticBar = settings.getBoolean("reader_static_bar", false)
|
||||
|
||||
font = settings.getString("reader_font", "")
|
||||
if (font.isNotEmpty()) {
|
||||
resId = requireContext().resources.getIdentifier(font, "font", requireContext().packageName)
|
||||
typeface = try {
|
||||
ResourcesCompat.getFont(requireContext(), resId)!!
|
||||
} catch (e: java.lang.Exception) {
|
||||
// Just to be sure
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
refreshAlignment()
|
||||
|
||||
fab = binding.fab
|
||||
|
||||
fab.backgroundTintList = ColorStateList.valueOf(resources.getColor(R.color.colorAccent))
|
||||
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
|
||||
|
||||
fab.rippleColor = resources.getColor(R.color.colorAccentDark)
|
||||
fab.rippleColor = appColors.colorAccentDark
|
||||
|
||||
val floatingToolbar: FloatingToolbar = handleFloatingToolbar()
|
||||
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
|
||||
floatingToolbar.attachFab(fab)
|
||||
|
||||
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
|
||||
|
||||
val customTabsIntent = requireActivity().buildCustomTabsIntent()
|
||||
mCustomTabActivityHelper = CustomTabActivityHelper()
|
||||
mCustomTabActivityHelper!!.bindCustomTabsService(activity)
|
||||
|
||||
|
||||
floatingToolbar.setClickListener(
|
||||
object : FloatingToolbar.ItemClickListener {
|
||||
override fun onItemClick(item: MenuItem) {
|
||||
when (item.itemId) {
|
||||
R.id.more_action -> getContentFromMercury(customTabsIntent)
|
||||
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
|
||||
R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
|
||||
R.id.unread_action -> if (context != null) {
|
||||
if (this@ArticleFragment.item.unread) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.markAsRead(this@ArticleFragment.item)
|
||||
}
|
||||
this@ArticleFragment.item.unread = false
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.marked_as_read,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.unmarkAsRead(this@ArticleFragment.item)
|
||||
}
|
||||
this@ArticleFragment.item.unread = true
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.marked_as_unread,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: MenuItem?) {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (staticBar) {
|
||||
fab.hide()
|
||||
@ -134,7 +198,28 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
binding.source.typeface = typeface
|
||||
}
|
||||
|
||||
handleContent()
|
||||
if (contentText.isEmptyOrNullOrNullString()) {
|
||||
getContentFromMercury(customTabsIntent)
|
||||
} else {
|
||||
binding.titleView.text = contentTitle
|
||||
if (typeface != null) {
|
||||
binding.titleView.typeface = typeface
|
||||
}
|
||||
|
||||
htmlToWebview()
|
||||
|
||||
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
Glide
|
||||
.with(requireContext())
|
||||
.asBitmap()
|
||||
.loadMaybeBasicAuth(config, contentImage)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(binding.imageView)
|
||||
} else {
|
||||
binding.imageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
binding.nestedScrollView.setOnScrollChangeListener(
|
||||
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||
@ -148,314 +233,221 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
} catch (e: InflateException) {
|
||||
e.sendSilentlyWithAcraWithName("webview not available")
|
||||
if (context != null) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
|
||||
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
) { _, _ ->
|
||||
appSettingsService.disableArticleViewer()
|
||||
requireActivity().finish()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
|
||||
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
|
||||
.setPositiveButton(android.R.string.ok
|
||||
) { _, _ ->
|
||||
settings.putBoolean("prefer_article_viewer", false)
|
||||
requireActivity().finish()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun handleContent() {
|
||||
if (contentText.isEmptyOrNullOrNullString()) {
|
||||
if (repository.isNetworkAvailable()) {
|
||||
getContentFromMercury()
|
||||
}
|
||||
} else {
|
||||
binding.titleView.text = contentTitle
|
||||
if (typeface != null) {
|
||||
binding.titleView.typeface = typeface
|
||||
}
|
||||
|
||||
htmlToWebview()
|
||||
|
||||
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
Glide
|
||||
.with(requireContext())
|
||||
.asBitmap()
|
||||
.load(contentImage)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(binding.imageView)
|
||||
} else {
|
||||
binding.imageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFloatingToolbar(): FloatingToolbar {
|
||||
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
|
||||
if (appSettingsService.getPublicAccess()) {
|
||||
floatingToolbar.setMenu(R.menu.reader_toolbar_no_read)
|
||||
}
|
||||
floatingToolbar.attachFab(fab)
|
||||
|
||||
floatingToolbar.background = ColorDrawable(resources.getColor(R.color.colorAccent))
|
||||
|
||||
floatingToolbar.setClickListener(
|
||||
object : FloatingToolbar.ItemClickListener {
|
||||
override fun onItemClick(item: MenuItem) {
|
||||
when (item.itemId) {
|
||||
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
|
||||
R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
|
||||
R.id.unread_action ->
|
||||
if (context != null) {
|
||||
if (this@ArticleFragment.item.unread) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.markAsRead(this@ArticleFragment.item)
|
||||
}
|
||||
this@ArticleFragment.item.unread = false
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.marked_as_read,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
repository.unmarkAsRead(this@ArticleFragment.item)
|
||||
}
|
||||
this@ArticleFragment.item.unread = true
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.marked_as_unread,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: MenuItem?) {
|
||||
// We do nothing
|
||||
}
|
||||
},
|
||||
)
|
||||
return floatingToolbar
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private fun refreshAlignment() {
|
||||
textAlignment =
|
||||
when (appSettingsService.getActiveAllignment()) {
|
||||
1 -> "justify"
|
||||
2 -> "left"
|
||||
else -> "justify"
|
||||
}
|
||||
textAlignment = when (settings.getInt("text_align", 1)) {
|
||||
1 -> "justify"
|
||||
2 -> "left"
|
||||
else -> "justify"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContentFromMercury() {
|
||||
private fun getContentFromMercury(customTabsIntent: CustomTabsIntent) {
|
||||
if (repository.isNetworkAvailable()) {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
val parser = MercuryApi()
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val response = mercuryApi.query(url)
|
||||
if (response.success && response.data != null) {
|
||||
handleMercuryData(response.data!!)
|
||||
} else {
|
||||
openInBrowserAfterFailing()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
openInBrowserAfterFailing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMercuryData(data: MercuryModel.ParsedContent) {
|
||||
if (data.error == true || data.failed == true) {
|
||||
openInBrowserAfterFailing()
|
||||
} else {
|
||||
binding.titleView.text = data.title.orEmpty()
|
||||
if (typeface != null) {
|
||||
binding.titleView.typeface = typeface
|
||||
}
|
||||
URL(data.url)
|
||||
url = data.url!!
|
||||
|
||||
contentText = data.content.orEmpty()
|
||||
htmlToWebview()
|
||||
|
||||
handleLeadImage(data?.lead_image_url)
|
||||
|
||||
binding.nestedScrollView.scrollTo(0, 0)
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLeadImage(lead_image_url: String?) {
|
||||
if (!lead_image_url.isNullOrEmpty() && context != null) {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
Glide
|
||||
.with(requireContext())
|
||||
.asBitmap()
|
||||
.load(
|
||||
lead_image_url,
|
||||
)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(binding.imageView)
|
||||
} else {
|
||||
binding.imageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleImageLoading() {
|
||||
binding.webcontent.webViewClient =
|
||||
object : WebViewClient() {
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
url: String,
|
||||
): Boolean {
|
||||
return if (context != null && url.isUrlValid() && binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
|
||||
try {
|
||||
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
e.sendSilentlyWithAcraWithName("activityNotFound > $url")
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
url: String,
|
||||
): WebResourceResponse? {
|
||||
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
||||
if (url.lowercase(Locale.US).contains(".jpg") ||
|
||||
url.lowercase(Locale.US)
|
||||
.contains(".jpeg")
|
||||
parser.parseUrl(url).enqueue(
|
||||
object : Callback<ParsedContent> {
|
||||
override fun onResponse(
|
||||
call: Call<ParsedContent>,
|
||||
response: Response<ParsedContent>
|
||||
) {
|
||||
// TODO: clean all the following after finding the mercury content issue
|
||||
try {
|
||||
val image =
|
||||
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
|
||||
return WebResourceResponse(
|
||||
IMAGE_JPG,
|
||||
"UTF-8",
|
||||
getBitmapInputStream(image, Bitmap.CompressFormat.JPEG),
|
||||
)
|
||||
} catch (e: ExecutionException) {
|
||||
// Do nothing
|
||||
}
|
||||
} else if (url.lowercase(Locale.US).contains(".png")) {
|
||||
try {
|
||||
val image =
|
||||
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
|
||||
return WebResourceResponse(
|
||||
IMAGE_JPG,
|
||||
"UTF-8",
|
||||
getBitmapInputStream(image, Bitmap.CompressFormat.PNG),
|
||||
)
|
||||
} catch (e: ExecutionException) {
|
||||
// Do nothing
|
||||
}
|
||||
} else if (url.lowercase(Locale.US).contains(".webp")) {
|
||||
try {
|
||||
val image =
|
||||
Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
|
||||
return WebResourceResponse(
|
||||
IMAGE_JPG,
|
||||
"UTF-8",
|
||||
getBitmapInputStream(image, Bitmap.CompressFormat.WEBP),
|
||||
)
|
||||
} catch (e: ExecutionException) {
|
||||
// Do nothing
|
||||
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
|
||||
try {
|
||||
binding.titleView.text = response.body()!!.title
|
||||
if (typeface != null) {
|
||||
binding.titleView.typeface = typeface
|
||||
}
|
||||
try {
|
||||
// Note: Mercury may return relative urls... If it does the url val will not be changed.
|
||||
URL(response.body()!!.url)
|
||||
url = response.body()!!.url
|
||||
} catch (e: MalformedURLException) {
|
||||
// Mercury returned a relative url. We do nothing.
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
|
||||
try {
|
||||
contentText = response.body()!!.content.orEmpty()
|
||||
htmlToWebview()
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
|
||||
try {
|
||||
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
try {
|
||||
Glide
|
||||
.with(requireContext())
|
||||
.asBitmap()
|
||||
.loadMaybeBasicAuth(
|
||||
config,
|
||||
response.body()!!.lead_image_url.orEmpty()
|
||||
)
|
||||
.apply(RequestOptions.fitCenterTransform())
|
||||
.into(binding.imageView)
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
} else {
|
||||
binding.imageView.visibility = View.GONE
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
binding.nestedScrollView.scrollTo(0, 0)
|
||||
|
||||
binding.progressBar.visibility = View.GONE
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
openInBrowserAfterFailing(customTabsIntent)
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (context != null) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.shouldInterceptRequest(view, url)
|
||||
override fun onFailure(
|
||||
call: Call<ParsedContent>,
|
||||
t: Throwable
|
||||
) = openInBrowserAfterFailing(customTabsIntent)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun htmlToWebview() {
|
||||
if (context != null) {
|
||||
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
|
||||
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
|
||||
val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent)
|
||||
|
||||
binding.webcontent.settings.standardFontFamily = a.getString(0)
|
||||
binding.webcontent.visibility = View.VISIBLE
|
||||
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
|
||||
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
|
||||
|
||||
val colorOnSurface = TypedValue()
|
||||
requireContext().theme.resolveAttribute(R.attr.colorOnSurface, colorOnSurface, true)
|
||||
|
||||
val colorSurface = TypedValue()
|
||||
requireContext().theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
|
||||
binding.webcontent.settings.standardFontFamily = a.getString(0)
|
||||
binding.webcontent.visibility = View.VISIBLE
|
||||
|
||||
binding.webcontent.settings.useWideViewPort = true
|
||||
binding.webcontent.settings.loadWithOverviewMode = true
|
||||
binding.webcontent.settings.javaScriptEnabled = false
|
||||
// TODO: Set the color strings programmatically
|
||||
val (stringTextColor, stringBackgroundColor) = if (appColors.isDarkTheme) {
|
||||
Pair("#FFFFFF", "#303030")
|
||||
} else {
|
||||
Pair("#212121", "#FAFAFA")
|
||||
}
|
||||
|
||||
handleImageLoading()
|
||||
binding.webcontent.settings.useWideViewPort = true
|
||||
binding.webcontent.settings.loadWithOverviewMode = true
|
||||
binding.webcontent.settings.javaScriptEnabled = false
|
||||
|
||||
val gestureDetector =
|
||||
GestureDetector(
|
||||
activity,
|
||||
object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
return performClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event) }
|
||||
|
||||
binding.webcontent.settings.layoutAlgorithm =
|
||||
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
|
||||
|
||||
var baseUrl: String? = null
|
||||
|
||||
try {
|
||||
val itemUrl = URL(url)
|
||||
baseUrl = itemUrl.protocol + "://" + itemUrl.host
|
||||
} catch (e: MalformedURLException) {
|
||||
e.sendSilentlyWithAcraWithName("htmlToWebview > $url")
|
||||
binding.webcontent.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
|
||||
if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
|
||||
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val fontName =
|
||||
when (font) {
|
||||
getString(R.string.open_sans_font_id) -> "Open Sans"
|
||||
getString(R.string.roboto_font_id) -> "Roboto"
|
||||
getString(R.string.source_code_pro_font_id) -> "Source Code Pro"
|
||||
else -> ""
|
||||
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
|
||||
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
||||
if (url.lowercase(Locale.US).contains(".jpg") || url.lowercase(Locale.US).contains(".jpeg")) {
|
||||
try {
|
||||
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
|
||||
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG))
|
||||
}catch ( e : ExecutionException) {}
|
||||
}
|
||||
else if (url.lowercase(Locale.US).contains(".png")) {
|
||||
try {
|
||||
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
|
||||
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG))
|
||||
}catch ( e : ExecutionException) {}
|
||||
}
|
||||
else if (url.lowercase(Locale.US).contains(".webp")) {
|
||||
try {
|
||||
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
|
||||
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP))
|
||||
}catch ( e : ExecutionException) {}
|
||||
}
|
||||
|
||||
val fontLinkAndStyle =
|
||||
if (font.isNotEmpty()) {
|
||||
"""<link href="https://fonts.googleapis.com/css?family=${
|
||||
fontName.replace(
|
||||
" ",
|
||||
"+",
|
||||
)
|
||||
}" rel="stylesheet">
|
||||
return super.shouldInterceptRequest(view, url)
|
||||
}
|
||||
}
|
||||
|
||||
val gestureDetector = GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapUp(e: MotionEvent?): Boolean {
|
||||
return performClick()
|
||||
}
|
||||
})
|
||||
|
||||
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)}
|
||||
|
||||
binding.webcontent.settings.layoutAlgorithm =
|
||||
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
|
||||
|
||||
var baseUrl: String? = null
|
||||
|
||||
try {
|
||||
val itemUrl = URL(url)
|
||||
baseUrl = itemUrl.protocol + "://" + itemUrl.host
|
||||
} catch (e: MalformedURLException) {
|
||||
}
|
||||
|
||||
val fontName = when (font) {
|
||||
getString(R.string.open_sans_font_id) -> "Open Sans"
|
||||
getString(R.string.roboto_font_id) -> "Roboto"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val fontLinkAndStyle = if (font.isNotEmpty()) {
|
||||
"""<link href="https://fonts.googleapis.com/css?family=${fontName.replace(" ", "+")}" rel="stylesheet">
|
||||
|<style>
|
||||
| * {
|
||||
| font-family: '$fontName';
|
||||
| }
|
||||
|</style>
|
||||
""".trimMargin()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
""".trimMargin()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
binding.webcontent.loadDataWithBaseURL(
|
||||
baseUrl,
|
||||
"""<html>
|
||||
binding.webcontent.loadDataWithBaseURL(
|
||||
baseUrl,
|
||||
"""<html>
|
||||
|<head>
|
||||
| <meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
| <style>
|
||||
@ -466,15 +458,10 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
| max-width: 100%;
|
||||
| }
|
||||
| a {
|
||||
| color: ${
|
||||
String.format(
|
||||
"#%06X",
|
||||
0xFFFFFF and resources.getColor(R.color.colorAccent),
|
||||
)
|
||||
} !important;
|
||||
| color: $stringColor !important;
|
||||
| }
|
||||
| *:not(a) {
|
||||
| color: ${String.format("#%06X", 0xFFFFFF and colorOnSurface.data)};
|
||||
| color: $stringTextColor;
|
||||
| }
|
||||
| * {
|
||||
| font-size: ${fontSize}px;
|
||||
@ -482,26 +469,11 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
| word-break: break-word;
|
||||
| overflow:hidden;
|
||||
| line-height: 1.5em;
|
||||
| background-color: ${
|
||||
String.format(
|
||||
"#%06X",
|
||||
0xFFFFFF and colorSurface.data,
|
||||
)
|
||||
};
|
||||
| background-color: $stringBackgroundColor;
|
||||
| }
|
||||
| body, html {
|
||||
| background-color: ${
|
||||
String.format(
|
||||
"#%06X",
|
||||
0xFFFFFF and colorSurface.data,
|
||||
)
|
||||
} !important;
|
||||
| border-color: ${
|
||||
String.format(
|
||||
"#%06X",
|
||||
0xFFFFFF and colorSurface.data,
|
||||
)
|
||||
} !important;
|
||||
| background-color: $stringBackgroundColor !important;
|
||||
| border-color: $stringBackgroundColor !important;
|
||||
| padding: 0 !important;
|
||||
| margin: 0 !important;
|
||||
| }
|
||||
@ -511,50 +483,45 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
| pre, code {
|
||||
| white-space: pre-wrap;
|
||||
| width:100%;
|
||||
| background-color: ${
|
||||
String.format(
|
||||
"#%06X",
|
||||
0xFFFFFF and colorSurface.data,
|
||||
)
|
||||
};
|
||||
| background-color: $stringBackgroundColor;
|
||||
| }
|
||||
| </style>
|
||||
| $fontLinkAndStyle
|
||||
|</head>
|
||||
|<body>
|
||||
| $contentText
|
||||
|</body>
|
||||
""".trimMargin(),
|
||||
"text/html",
|
||||
"utf-8",
|
||||
null,
|
||||
)
|
||||
}
|
||||
|</body>""".trimMargin(),
|
||||
"text/html",
|
||||
"utf-8",
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun scrollDown() {
|
||||
val height = binding.nestedScrollView.measuredHeight
|
||||
binding.nestedScrollView.smoothScrollBy(0, height / 2)
|
||||
binding.nestedScrollView.smoothScrollBy(0, height/2)
|
||||
}
|
||||
|
||||
fun scrollUp() {
|
||||
val height = binding.nestedScrollView.measuredHeight
|
||||
binding.nestedScrollView.smoothScrollBy(0, -height / 2)
|
||||
binding.nestedScrollView.smoothScrollBy(0, -height/2)
|
||||
}
|
||||
|
||||
private fun openInBrowserAfterFailing() {
|
||||
private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
if (context != null) {
|
||||
requireContext().openInBrowserAsNewTask(this@ArticleFragment.item)
|
||||
} else {
|
||||
Exception("openInBrowserAfterFailing context is null").sendSilentlyWithAcraWithName("openInBrowserAfterFailing > $context")
|
||||
}
|
||||
requireActivity().openItemUrlInternalBrowser(
|
||||
url,
|
||||
customTabsIntent,
|
||||
requireActivity()
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_ITEMS = "items"
|
||||
|
||||
fun newInstance(item: SelfossModel.Item): ArticleFragment {
|
||||
fun newInstance(
|
||||
item: SelfossModel.Item
|
||||
): ArticleFragment {
|
||||
val fragment = ArticleFragment()
|
||||
val args = Bundle()
|
||||
args.putParcelable(ARG_ITEMS, item.toParcelable())
|
||||
@ -564,12 +531,10 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
}
|
||||
|
||||
fun performClick(): Boolean {
|
||||
if (allImages != null && (
|
||||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
|
||||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
|
||||
)
|
||||
) {
|
||||
val position: Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
|
||||
if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
|
||||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
|
||||
|
||||
val position : Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
|
||||
|
||||
val intent = Intent(activity, ImageActivity::class.java)
|
||||
intent.putExtra("allImages", allImages)
|
||||
@ -579,4 +544,6 @@ class ArticleFragment : Fragment(), DIAware {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,195 +0,0 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.fragments
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import bou.amine.apps.readerforselfossv2.android.HomeActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.utils.getColorHexCode
|
||||
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
|
||||
import bou.amine.apps.readerforselfossv2.utils.getIcon
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.ViewTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.x.closestDI
|
||||
import org.kodein.di.instance
|
||||
|
||||
class FilterSheetFragment : BottomSheetDialogFragment(), DIAware {
|
||||
private lateinit var binding: FilterFragmentBinding
|
||||
override val di: DI by closestDI()
|
||||
private val repository: Repository by instance()
|
||||
|
||||
private var selectedChip: Chip? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
binding =
|
||||
FilterFragmentBinding.inflate(
|
||||
inflater,
|
||||
container,
|
||||
false,
|
||||
)
|
||||
|
||||
val context: Context? = context
|
||||
|
||||
if (context == null) {
|
||||
dismiss()
|
||||
Exception("FilterSheetFragment context is null").sendSilentlyWithAcraWithName("FilterSheetFragment > onCreateView")
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
handleTagChips(context)
|
||||
handleSourceChips(context)
|
||||
|
||||
binding.progressBar2.visibility = GONE
|
||||
binding.filterView.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
binding.floatingActionButton2.setOnClickListener {
|
||||
(activity as HomeActivity).getElementsAccordingToTab()
|
||||
(activity as HomeActivity).fetchOnEmptyList()
|
||||
dismiss()
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private suspend fun handleSourceChips(context: Context) {
|
||||
val sourceGroup = binding.sourcesGroup
|
||||
|
||||
repository.getSourcesDetailsOrStats().forEachIndexed { _, source ->
|
||||
val c = Chip(context)
|
||||
c.ellipsize = TextUtils.TruncateAt.END
|
||||
|
||||
Glide.with(context)
|
||||
.load(source.getIcon(repository.baseUrl))
|
||||
.into(
|
||||
object : ViewTarget<Chip?, Drawable?>(c) {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
transition: Transition<in Drawable?>?,
|
||||
) {
|
||||
try {
|
||||
c.chipIcon = resource
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcraWithName("sources > onResourceReady")
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
c.text = source.title.getHtmlDecoded()
|
||||
|
||||
c.setOnCloseIconClickListener {
|
||||
(it as Chip).isCloseIconVisible = false
|
||||
selectedChip = null
|
||||
repository.setSourceFilter(null)
|
||||
}
|
||||
|
||||
c.setOnClickListener {
|
||||
if (selectedChip != null) {
|
||||
selectedChip!!.isCloseIconVisible = false
|
||||
}
|
||||
(it as Chip).isCloseIconVisible = true
|
||||
selectedChip = it
|
||||
repository.setSourceFilter(source)
|
||||
|
||||
repository.setTagFilter(null)
|
||||
}
|
||||
|
||||
if (repository.sourceFilter.value?.equals(source) == true) {
|
||||
c.isCloseIconVisible = true
|
||||
selectedChip = c
|
||||
}
|
||||
|
||||
c.isEnabled = source.error.isNullOrBlank()
|
||||
|
||||
if (!source.error.isNullOrBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
c.tooltipText = source.error
|
||||
}
|
||||
|
||||
sourceGroup.addView(c)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleTagChips(context: Context) {
|
||||
val tagGroup = binding.tagsGroup
|
||||
|
||||
val tags = repository.getTags()
|
||||
|
||||
tags.forEachIndexed { _, tag ->
|
||||
val c = Chip(context)
|
||||
c.ellipsize = TextUtils.TruncateAt.END
|
||||
c.text = tag.tag
|
||||
|
||||
if (tag.color.isNotEmpty()) {
|
||||
try {
|
||||
val gd = GradientDrawable()
|
||||
val gdColor =
|
||||
try {
|
||||
Color.parseColor(tag.getColorHexCode())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.sendSilentlyWithAcraWithName("color issue " + tag.color + " / " + tag.getColorHexCode())
|
||||
resources.getColor(R.color.colorPrimary)
|
||||
}
|
||||
gd.setColor(gdColor)
|
||||
gd.shape = GradientDrawable.RECTANGLE
|
||||
gd.setSize(30, 30)
|
||||
gd.cornerRadius = 30F
|
||||
c.chipIcon = gd
|
||||
} catch (e: Exception) {
|
||||
e.sendSilentlyWithAcraWithName("tags > GradientDrawable")
|
||||
}
|
||||
}
|
||||
|
||||
c.setOnCloseIconClickListener {
|
||||
(it as Chip).isCloseIconVisible = false
|
||||
selectedChip = null
|
||||
repository.setTagFilter(null)
|
||||
}
|
||||
|
||||
c.setOnClickListener {
|
||||
if (selectedChip != null) {
|
||||
selectedChip!!.isCloseIconVisible = false
|
||||
}
|
||||
(it as Chip).isCloseIconVisible = true
|
||||
selectedChip = it
|
||||
repository.setTagFilter(tag)
|
||||
|
||||
repository.setSourceFilter(null)
|
||||
}
|
||||
|
||||
if (repository.tagFilter.value?.equals(tag) == true) {
|
||||
c.isCloseIconVisible = true
|
||||
selectedChip = c
|
||||
}
|
||||
|
||||
tagGroup.addView(c)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "FilterModalBottomSheet"
|
||||
}
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.*
|
||||
import androidx.fragment.app.Fragment
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentImageBinding
|
||||
import com.bumptech.glide.Glide
|
||||
@ -11,7 +9,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
|
||||
class ImageFragment : Fragment() {
|
||||
private lateinit var imageUrl: String
|
||||
|
||||
private lateinit var imageUrl : String
|
||||
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
|
||||
private var _binding: FragmentImageBinding? = null
|
||||
private val binding get() = _binding
|
||||
@ -22,20 +21,16 @@ class ImageFragment : Fragment() {
|
||||
imageUrl = requireArguments().getString("imageUrl")!!
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
_binding = FragmentImageBinding.inflate(inflater, container, false)
|
||||
val view = binding?.root
|
||||
|
||||
binding!!.photoView.visibility = View.VISIBLE
|
||||
Glide.with(requireActivity())
|
||||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(imageUrl)
|
||||
.into(binding!!.photoView)
|
||||
Glide.with(activity)
|
||||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(imageUrl)
|
||||
.into(binding!!.photoView)
|
||||
|
||||
return view
|
||||
}
|
||||
@ -48,7 +43,9 @@ class ImageFragment : Fragment() {
|
||||
companion object {
|
||||
private const val ARG_IMAGE = "imageUrl"
|
||||
|
||||
fun newInstance(imageUrl: String): ImageFragment {
|
||||
fun newInstance(
|
||||
imageUrl : String
|
||||
): ImageFragment {
|
||||
val fragment = ImageFragment()
|
||||
val args = Bundle()
|
||||
args.putString(ARG_IMAGE, imageUrl)
|
||||
@ -56,4 +53,4 @@ class ImageFragment : Fragment() {
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,28 +2,27 @@ package bou.amine.apps.readerforselfossv2.android.model
|
||||
|
||||
import android.content.Context
|
||||
import android.webkit.URLUtil
|
||||
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.utils.getImages
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
|
||||
fun SelfossModel.Item.preloadImages(context: Context): Boolean {
|
||||
fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
|
||||
val imageUrls = this.getImages()
|
||||
|
||||
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
|
||||
|
||||
|
||||
try {
|
||||
for (url in imageUrls) {
|
||||
if (URLUtil.isValidUrl(url)) {
|
||||
if ( URLUtil.isValidUrl(url)) {
|
||||
Glide.with(context).asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(url).submit()
|
||||
}
|
||||
}
|
||||
} catch (e: Error) {
|
||||
e.sendSilentlyWithAcraWithName("preloadImages")
|
||||
} catch (e : Error) {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -36,8 +35,8 @@ fun String.toTextDrawableString(): String {
|
||||
try {
|
||||
textDrawable.append(s[0])
|
||||
} catch (e: StringIndexOutOfBoundsException) {
|
||||
e.sendSilentlyWithAcraWithName("toTextDrawableString")
|
||||
// We do nothing
|
||||
}
|
||||
}
|
||||
return textDrawable.toString()
|
||||
}
|
||||
}
|
@ -3,8 +3,9 @@ package bou.amine.apps.readerforselfossv2.android.model
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
fun SelfossModel.Item.toParcelable(): ParecelableItem =
|
||||
fun SelfossModel.Item.toParcelable() : ParecelableItem =
|
||||
ParecelableItem(
|
||||
this.id,
|
||||
this.datetime,
|
||||
@ -16,11 +17,9 @@ fun SelfossModel.Item.toParcelable(): ParecelableItem =
|
||||
this.icon,
|
||||
this.link,
|
||||
this.sourcetitle,
|
||||
this.tags.joinToString(","),
|
||||
this.author,
|
||||
this.tags.joinToString(",")
|
||||
)
|
||||
|
||||
fun ParecelableItem.toModel(): SelfossModel.Item =
|
||||
fun ParecelableItem.toModel() : SelfossModel.Item =
|
||||
SelfossModel.Item(
|
||||
this.id,
|
||||
this.datetime,
|
||||
@ -32,32 +31,28 @@ fun ParecelableItem.toModel(): SelfossModel.Item =
|
||||
this.icon,
|
||||
this.link,
|
||||
this.sourcetitle,
|
||||
this.tags.split(","),
|
||||
this.author,
|
||||
this.tags.split(",")
|
||||
)
|
||||
|
||||
data class ParecelableItem(
|
||||
val id: Int,
|
||||
val datetime: String,
|
||||
val title: String,
|
||||
val content: String,
|
||||
var unread: Boolean,
|
||||
var starred: Boolean,
|
||||
val thumbnail: String?,
|
||||
val icon: String?,
|
||||
val link: String,
|
||||
val sourcetitle: String,
|
||||
val tags: String,
|
||||
val author: String?,
|
||||
@SerializedName("id") val id: Int,
|
||||
@SerializedName("datetime") val datetime: String,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("content") val content: String,
|
||||
@SerializedName("unread") var unread: Boolean,
|
||||
@SerializedName("starred") var starred: Boolean,
|
||||
@SerializedName("thumbnail") val thumbnail: String?,
|
||||
@SerializedName("icon") val icon: String?,
|
||||
@SerializedName("link") val link: String,
|
||||
@SerializedName("sourcetitle") val sourcetitle: String,
|
||||
@SerializedName("tags") val tags: String
|
||||
) : Parcelable {
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<ParecelableItem> =
|
||||
object : Parcelable.Creator<ParecelableItem> {
|
||||
override fun createFromParcel(source: Parcel): ParecelableItem = ParecelableItem(source)
|
||||
|
||||
override fun newArray(size: Int): Array<ParecelableItem?> = arrayOfNulls(size)
|
||||
}
|
||||
val CREATOR: Parcelable.Creator<ParecelableItem> = object : Parcelable.Creator<ParecelableItem> {
|
||||
override fun createFromParcel(source: Parcel): ParecelableItem = ParecelableItem(source)
|
||||
override fun newArray(size: Int): Array<ParecelableItem?> = arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(source: Parcel) : this(
|
||||
@ -71,16 +66,12 @@ data class ParecelableItem(
|
||||
icon = source.readString(),
|
||||
link = source.readString().orEmpty(),
|
||||
sourcetitle = source.readString().orEmpty(),
|
||||
tags = source.readString().orEmpty(),
|
||||
author = source.readString().orEmpty(),
|
||||
tags = source.readString().orEmpty()
|
||||
)
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
override fun writeToParcel(
|
||||
dest: Parcel,
|
||||
flags: Int,
|
||||
) {
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeInt(id)
|
||||
dest.writeString(datetime)
|
||||
dest.writeString(title)
|
||||
@ -92,6 +83,5 @@ data class ParecelableItem(
|
||||
dest.writeString(link)
|
||||
dest.writeString(sourcetitle)
|
||||
dest.writeString(tags)
|
||||
dest.writeString(author)
|
||||
}
|
||||
}
|
||||
}
|
@ -3,43 +3,45 @@ package bou.amine.apps.readerforselfossv2.android.settings
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.InputFilter
|
||||
import android.text.InputType
|
||||
import android.text.TextWatcher
|
||||
import android.text.*
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceManager
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding
|
||||
import bou.amine.apps.readerforselfossv2.android.sendSilentlyWithAcraWithName
|
||||
import bou.amine.apps.readerforselfossv2.service.AppSettingsService
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
import com.ftinc.scoop.Scoop
|
||||
import com.russhwolf.settings.Settings
|
||||
import java.lang.NumberFormatException
|
||||
|
||||
private const val TITLE_TAG = "settingsActivityTitle"
|
||||
|
||||
class SettingsActivity :
|
||||
AppCompatActivity(),
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
|
||||
DIAware {
|
||||
override val di by closestDI()
|
||||
class SettingsActivity : AppCompatActivity(),
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("dark_theme", false)) {
|
||||
setTheme(R.style.NoBarDark)
|
||||
}
|
||||
val binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
|
||||
val scoop = Scoop.getInstance()
|
||||
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
|
||||
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
|
||||
|
||||
setContentView(binding.root)
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, MainPreferenceFragment())
|
||||
.commit()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, MainPreferenceFragment())
|
||||
.commit()
|
||||
} else {
|
||||
title = savedInstanceState.getCharSequence(TITLE_TAG)
|
||||
}
|
||||
@ -63,168 +65,124 @@ class SettingsActivity :
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
return if (supportFragmentManager.popBackStackImmediate()) {
|
||||
if (supportFragmentManager.popBackStackImmediate()) {
|
||||
supportActionBar?.title = getText(R.string.title_activity_settings)
|
||||
false
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
true
|
||||
return true
|
||||
}
|
||||
return super.onSupportNavigateUp()
|
||||
}
|
||||
|
||||
override fun onPreferenceStartFragment(
|
||||
caller: PreferenceFragmentCompat,
|
||||
pref: Preference,
|
||||
caller: PreferenceFragmentCompat,
|
||||
pref: Preference
|
||||
): Boolean {
|
||||
// Instantiate the new Fragment
|
||||
val args = pref.extras
|
||||
val fragment =
|
||||
supportFragmentManager.fragmentFactory.instantiate(
|
||||
val fragment = supportFragmentManager.fragmentFactory.instantiate(
|
||||
classLoader,
|
||||
pref.fragment.toString(),
|
||||
).apply {
|
||||
arguments = args
|
||||
setTargetFragment(caller, 0)
|
||||
}
|
||||
pref.fragment
|
||||
).apply {
|
||||
arguments = args
|
||||
setTargetFragment(caller, 0)
|
||||
}
|
||||
// Replace the existing Fragment with the new Fragment
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.settings, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
.replace(R.id.settings, fragment)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
title = pref.title
|
||||
supportActionBar?.title = title
|
||||
return true
|
||||
}
|
||||
|
||||
class MainPreferenceFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.pref_main, rootKey)
|
||||
|
||||
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
|
||||
Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
|
||||
true
|
||||
}
|
||||
|
||||
preferenceManager.findPreference<Preference>("action_about")?.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener { _ ->
|
||||
context?.let {
|
||||
LibsBuilder()
|
||||
.withAboutIconShown(true)
|
||||
.withAboutVersionShown(true)
|
||||
.start(it)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GeneralPreferenceFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.pref_general, rootKey)
|
||||
|
||||
val editTextPreference = preferenceManager.findPreference<EditTextPreference>("prefer_api_items_number")
|
||||
editTextPreference?.setOnBindEditTextListener { editText ->
|
||||
editText.inputType = InputType.TYPE_CLASS_NUMBER
|
||||
editText.filters =
|
||||
arrayOf(
|
||||
editText.filters = arrayOf(
|
||||
InputFilter { source, _, _, dest, _, _ ->
|
||||
try {
|
||||
val input: Int = (dest.toString() + source.toString()).toInt()
|
||||
if (input in 1..200) return@InputFilter null
|
||||
} catch (nfe: NumberFormatException) {
|
||||
nfe.sendSilentlyWithAcraWithName("GeneralPreferenceFragment")
|
||||
Toast.makeText(activity, R.string.items_number_should_be_number, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
""
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ArticleViewerPreferenceFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.pref_viewer, rootKey)
|
||||
|
||||
val fontSize = preferenceManager.findPreference<EditTextPreference>("reader_font_size")
|
||||
fontSize?.setOnBindEditTextListener { editText ->
|
||||
editText.inputType = InputType.TYPE_CLASS_NUMBER
|
||||
editText.addTextChangedListener {
|
||||
object : TextWatcher {
|
||||
override fun beforeTextChanged(
|
||||
charSequence: CharSequence,
|
||||
i: Int,
|
||||
i1: Int,
|
||||
i2: Int,
|
||||
) {
|
||||
// We do nothing
|
||||
}
|
||||
|
||||
override fun onTextChanged(
|
||||
charSequence: CharSequence,
|
||||
i: Int,
|
||||
i1: Int,
|
||||
i2: Int,
|
||||
) {
|
||||
// We do nothing
|
||||
}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
try {
|
||||
editText.textSize = editable.toString().toInt().toFloat()
|
||||
} catch (e: NumberFormatException) {
|
||||
e.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > afterTextChanged")
|
||||
}
|
||||
editText.addTextChangedListener { object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
try {
|
||||
editText.textSize = editable.toString().toInt().toFloat()
|
||||
} catch (e: NumberFormatException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
editText.filters =
|
||||
arrayOf(
|
||||
} }
|
||||
editText.filters = arrayOf(
|
||||
InputFilter { source, _, _, dest, _, _ ->
|
||||
try {
|
||||
val input = (dest.toString() + source.toString()).toInt()
|
||||
if (input > 0) return@InputFilter null
|
||||
} catch (nfe: NumberFormatException) {
|
||||
nfe.sendSilentlyWithAcraWithName("ArticleViewerPreferenceFragment > filters")
|
||||
}
|
||||
""
|
||||
},
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OfflinePreferenceFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.pref_offline, rootKey)
|
||||
}
|
||||
}
|
||||
|
||||
class ThemePreferenceFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.pref_theme, rootKey)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
preferenceManager.findPreference<Preference>("currentMode")?.onPreferenceChangeListener =
|
||||
Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) // ListPreference Only takes string-arrays ¯\_(ツ)_/¯
|
||||
true
|
||||
}
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
inflater.inflate(R.menu.settings_theme, menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val id = item.itemId
|
||||
if (id == R.id.clear) {
|
||||
val settings = Settings()
|
||||
settings.remove("color_primary")
|
||||
settings.remove("color_primary_dark")
|
||||
settings.remove("color_accent")
|
||||
settings.remove("color_accent_dark")
|
||||
settings.remove("dark_theme")
|
||||
requireActivity().recreate()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,38 +192,29 @@ class SettingsActivity :
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.pref_links, rootKey)
|
||||
|
||||
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
openUrl(Uri.parse(AppSettingsService.trackerUrl))
|
||||
true
|
||||
}
|
||||
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
openUrl(Uri.parse(Config.trackerUrl))
|
||||
true
|
||||
}
|
||||
|
||||
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
openUrl(Uri.parse(AppSettingsService.sourceUrl))
|
||||
false
|
||||
}
|
||||
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
openUrl(Uri.parse(Config.sourceUrl))
|
||||
false
|
||||
}
|
||||
|
||||
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
openUrl(Uri.parse(AppSettingsService.translationUrl))
|
||||
false
|
||||
}
|
||||
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
openUrl(Uri.parse(Config.translationUrl))
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExperimentalPreferenceFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.pref_experimental, rootKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.themes
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.annotation.ColorInt
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import com.russhwolf.settings.Settings
|
||||
|
||||
class AppColors(a: Activity) {
|
||||
|
||||
@ColorInt val colorPrimary: Int
|
||||
@ColorInt val colorPrimaryDark: Int
|
||||
@ColorInt val colorAccent: Int
|
||||
@ColorInt val colorAccentDark: Int
|
||||
@ColorInt val colorBackground: Int
|
||||
@ColorInt val textColor: Int
|
||||
val isDarkTheme: Boolean
|
||||
|
||||
init {
|
||||
val settings = Settings()
|
||||
|
||||
colorPrimary =
|
||||
settings.getInt(
|
||||
"color_primary",
|
||||
a.resources.getColor(R.color.colorPrimary)
|
||||
)
|
||||
colorPrimaryDark =
|
||||
settings.getInt(
|
||||
"color_primary_dark",
|
||||
a.resources.getColor(R.color.colorPrimaryDark)
|
||||
)
|
||||
colorAccent =
|
||||
settings.getInt(
|
||||
"color_accent",
|
||||
a.resources.getColor(R.color.colorAccent)
|
||||
)
|
||||
colorAccentDark =
|
||||
settings.getInt(
|
||||
"color_accent_dark",
|
||||
a.resources.getColor(R.color.colorAccentDark)
|
||||
)
|
||||
isDarkTheme =
|
||||
settings.getBoolean(
|
||||
"dark_theme",
|
||||
false
|
||||
)
|
||||
|
||||
colorBackground = if (isDarkTheme) {
|
||||
a.setTheme(R.style.NoBarDark)
|
||||
a.resources.getColor(R.color.darkBackground)
|
||||
} else {
|
||||
a.setTheme(R.style.NoBar)
|
||||
a.resources.getColor(R.color.grey_50)
|
||||
}
|
||||
|
||||
textColor = if (isDarkTheme) {
|
||||
a.resources.getColor(R.color.white)
|
||||
} else {
|
||||
a.resources.getColor(R.color.grey_900)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.themes
|
||||
|
||||
enum class Toppings(val value: Int) {
|
||||
PRIMARY(1),
|
||||
PRIMARY_DARK(2),
|
||||
ACCENT(3),
|
||||
ACCENT_DARK(4)
|
||||
}
|
@ -5,10 +5,7 @@ import android.content.Intent
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
|
||||
|
||||
fun Context.shareLink(
|
||||
itemUrl: String,
|
||||
itemTitle: String,
|
||||
) {
|
||||
fun Context.shareLink(itemUrl: String, itemTitle: String) {
|
||||
val sendIntent = Intent()
|
||||
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
sendIntent.action = Intent.ACTION_SEND
|
||||
@ -18,7 +15,7 @@ fun Context.shareLink(
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
sendIntent,
|
||||
getString(R.string.share),
|
||||
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
getString(R.string.share)
|
||||
).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import kotlin.math.abs
|
||||
|
||||
class CircleImageView
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : RelativeLayout(context, attrs, defStyleAttr) {
|
||||
val view: View
|
||||
val imageView: ShapeableImageView
|
||||
val textView: TextView
|
||||
|
||||
private val colorScheme =
|
||||
listOf(
|
||||
-0x1a8c8d,
|
||||
-0xf9d6e,
|
||||
-0x459738,
|
||||
-0x6a8a33,
|
||||
-0x867935,
|
||||
-0x9b4a0a,
|
||||
-0xb03c09,
|
||||
-0xb22f1f,
|
||||
-0xb24954,
|
||||
-0x7e387c,
|
||||
-0x512a7f,
|
||||
-0x759b,
|
||||
-0x2b1ea9,
|
||||
-0x2ab1,
|
||||
-0x48b3,
|
||||
-0x5e7781,
|
||||
-0x6f5b52,
|
||||
)
|
||||
|
||||
init {
|
||||
view = LayoutInflater.from(context).inflate(R.layout.circle_image_view, this, true)
|
||||
imageView = view.findViewById(R.id.circleImage)
|
||||
textView = view.findViewById(R.id.circleText)
|
||||
}
|
||||
|
||||
fun setBackgroundAndText(text: String) {
|
||||
val circleDrawable = GradientDrawable()
|
||||
val color = colorFromIdentifier(text)
|
||||
circleDrawable.setColor(color)
|
||||
imageView.setImageDrawable(circleDrawable)
|
||||
|
||||
textView.text = text.toTextDrawableString()
|
||||
}
|
||||
|
||||
private fun colorFromIdentifier(key: String): Int {
|
||||
return colorScheme[abs(key.hashCode()) % colorScheme.size]
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import bou.amine.apps.readerforselfossv2.android.LoginActivity
|
||||
import com.russhwolf.settings.Settings
|
||||
|
||||
class Config {
|
||||
|
||||
val settings = Settings()
|
||||
|
||||
val baseUrl: String
|
||||
get() = settings.getString("url", "")
|
||||
|
||||
val userLogin: String
|
||||
get() = settings.getString("login", "")
|
||||
|
||||
val userPassword: String
|
||||
get() = settings.getString("password", "")
|
||||
|
||||
val httpUserLogin: String
|
||||
get() = settings.getString("httpUserName", "")
|
||||
|
||||
val httpUserPassword: String
|
||||
get() = settings.getString("httpPassword", "")
|
||||
|
||||
companion object {
|
||||
const val settingsName = "paramsselfoss"
|
||||
|
||||
const val feedbackEmail = "aminecmi@pm.me.com"
|
||||
|
||||
const val translationUrl = "https://crwd.in/readerforselfoss"
|
||||
|
||||
const val sourceUrl = "https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform"
|
||||
|
||||
const val trackerUrl = "https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
|
||||
|
||||
const val syncChannelId = "sync-channel-id"
|
||||
|
||||
const val newItemsChannelId = "new-items-channel-id"
|
||||
|
||||
var apiVersion = 0
|
||||
|
||||
/* Execute logout and clear all settings to default */
|
||||
fun logoutAndRedirect(
|
||||
c: Context,
|
||||
callingActivity: Activity,
|
||||
baseUrlFail: Boolean = false
|
||||
): Boolean {
|
||||
val settings = Settings()
|
||||
settings.clear()
|
||||
val intent = Intent(c, LoginActivity::class.java)
|
||||
if (baseUrlFail) {
|
||||
intent.putExtra("baseUrlFail", baseUrlFail)
|
||||
}
|
||||
c.startActivity(intent)
|
||||
callingActivity.finish()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
fun getUnsafeHttpClient(): OkHttpClient.Builder =
|
||||
try {
|
||||
// Create a trust manager that does not validate certificate chains
|
||||
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> =
|
||||
arrayOf()
|
||||
|
||||
@Throws(CertificateException::class)
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<java.security.cert.X509Certificate>,
|
||||
authType: String
|
||||
) {
|
||||
}
|
||||
|
||||
@Throws(CertificateException::class)
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<java.security.cert.X509Certificate>,
|
||||
authType: String
|
||||
) {
|
||||
}
|
||||
})
|
||||
|
||||
// Install the all-trusting trust manager
|
||||
val sslContext = SSLContext.getInstance("SSL")
|
||||
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
|
||||
|
||||
val sslSocketFactory = sslContext.socketFactory
|
||||
|
||||
OkHttpClient.Builder()
|
||||
.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
|
||||
.hostnameVerifier { _, _ -> true }
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException(e)
|
||||
}
|
@ -1,9 +1,13 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.text.Spannable
|
||||
import android.text.style.ClickableSpan
|
||||
import android.util.Patterns
|
||||
@ -11,41 +15,155 @@ import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.ReaderActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
fun Context.openItemUrl(
|
||||
fun Context.buildCustomTabsIntent(): CustomTabsIntent {
|
||||
|
||||
val actionIntent = Intent(Intent.ACTION_SEND)
|
||||
actionIntent.type = "text/plain"
|
||||
val pflags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val createPendingShareIntent: PendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
actionIntent,
|
||||
pflags
|
||||
)
|
||||
|
||||
val intentBuilder = CustomTabsIntent.Builder()
|
||||
|
||||
// TODO: change to primary when it's possible to customize custom tabs title color
|
||||
//intentBuilder.setToolbarColor(c.getResources().getColor(R.color.colorPrimary));
|
||||
intentBuilder.setToolbarColor(resources.getColor(R.color.colorAccentDark))
|
||||
intentBuilder.setShowTitle(true)
|
||||
|
||||
|
||||
intentBuilder.setStartAnimations(
|
||||
this,
|
||||
R.anim.slide_in_right,
|
||||
R.anim.slide_out_left
|
||||
)
|
||||
intentBuilder.setExitAnimations(
|
||||
this,
|
||||
android.R.anim.slide_in_left,
|
||||
android.R.anim.slide_out_right
|
||||
)
|
||||
|
||||
val closeicon = BitmapFactory.decodeResource(resources, R.drawable.ic_close_white_24dp)
|
||||
intentBuilder.setCloseButtonIcon(closeicon)
|
||||
|
||||
val shareLabel = this.getString(R.string.label_share)
|
||||
val icon = BitmapFactory.decodeResource(
|
||||
resources,
|
||||
R.drawable.ic_share_white_24dp
|
||||
)
|
||||
intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent)
|
||||
|
||||
return intentBuilder.build()
|
||||
}
|
||||
|
||||
fun Context.openItemUrlInternally(
|
||||
allItems: ArrayList<SelfossModel.Item>,
|
||||
currentItem: Int,
|
||||
linkDecoded: String,
|
||||
customTabsIntent: CustomTabsIntent,
|
||||
articleViewer: Boolean,
|
||||
app: Activity,
|
||||
app: Activity
|
||||
) {
|
||||
if (articleViewer) {
|
||||
ReaderActivity.allItems = allItems
|
||||
val intent = Intent(this, ReaderActivity::class.java)
|
||||
intent.putExtra("currentItem", currentItem)
|
||||
app.startActivity(intent)
|
||||
} else {
|
||||
this.openItemUrlInternalBrowser(
|
||||
linkDecoded,
|
||||
customTabsIntent,
|
||||
app)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.openItemUrlInternalBrowser(
|
||||
linkDecoded: String,
|
||||
customTabsIntent: CustomTabsIntent,
|
||||
app: Activity
|
||||
) {
|
||||
try {
|
||||
CustomTabActivityHelper.openCustomTab(
|
||||
app,
|
||||
customTabsIntent,
|
||||
Uri.parse(linkDecoded)
|
||||
) { _, uri ->
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
openInBrowser(linkDecoded, app)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.openItemUrl(
|
||||
allItems: ArrayList<SelfossModel.Item>,
|
||||
currentItem: Int,
|
||||
linkDecoded: String,
|
||||
customTabsIntent: CustomTabsIntent,
|
||||
internalBrowser: Boolean,
|
||||
articleViewer: Boolean,
|
||||
app: Activity
|
||||
) {
|
||||
|
||||
if (!linkDecoded.isUrlValid()) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
this.getString(R.string.cant_open_invalid_url),
|
||||
Toast.LENGTH_LONG,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
if (articleViewer) {
|
||||
val intent = Intent(this, ReaderActivity::class.java)
|
||||
intent.putExtra("currentItem", currentItem)
|
||||
app.startActivity(intent)
|
||||
if (!internalBrowser) {
|
||||
openInBrowser(linkDecoded, app)
|
||||
} else if (articleViewer) {
|
||||
this.openItemUrlInternally(
|
||||
allItems,
|
||||
currentItem,
|
||||
linkDecoded,
|
||||
customTabsIntent,
|
||||
articleViewer,
|
||||
app
|
||||
)
|
||||
} else {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.data = Uri.parse(linkDecoded.toStringUriWithHttp())
|
||||
startActivity(intent)
|
||||
this.openItemUrlInternalBrowser(
|
||||
linkDecoded,
|
||||
customTabsIntent,
|
||||
app
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isUrlValid(): Boolean = this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
|
||||
private fun openInBrowser(linkDecoded: String, app: Activity) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(linkDecoded)
|
||||
try {
|
||||
app.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(app.baseContext, e.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun String.isBaseUrlInvalid(): Boolean {
|
||||
fun String.isUrlValid(): Boolean =
|
||||
this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
|
||||
|
||||
fun String.isBaseUrlValid(ctx: Context): Boolean {
|
||||
val baseUrl = this.toHttpUrlOrNull()
|
||||
var existsAndEndsWithSlash = false
|
||||
if (baseUrl != null) {
|
||||
@ -53,7 +171,7 @@ fun String.isBaseUrlInvalid(): Boolean {
|
||||
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
|
||||
}
|
||||
|
||||
return !(Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash)
|
||||
return Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash
|
||||
}
|
||||
|
||||
fun Context.openInBrowserAsNewTask(i: SelfossModel.Item) {
|
||||
@ -63,11 +181,8 @@ fun Context.openInBrowserAsNewTask(i: SelfossModel.Item) {
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
class LinkOnTouchListener : View.OnTouchListener {
|
||||
override fun onTouch(
|
||||
v: View?,
|
||||
event: MotionEvent?,
|
||||
): Boolean {
|
||||
class LinkOnTouchListener: View.OnTouchListener {
|
||||
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||
var ret = false
|
||||
val widget: TextView = v as TextView
|
||||
val text: CharSequence = widget.text
|
||||
@ -76,8 +191,7 @@ class LinkOnTouchListener : View.OnTouchListener {
|
||||
val action = event!!.action
|
||||
|
||||
if (action == MotionEvent.ACTION_UP ||
|
||||
action == MotionEvent.ACTION_DOWN
|
||||
) {
|
||||
action == MotionEvent.ACTION_DOWN) {
|
||||
var x: Float = event.x
|
||||
var y: Float = event.y
|
||||
|
||||
|
@ -8,4 +8,5 @@ fun TextBadgeItem.removeBadge(): TextBadgeItem {
|
||||
return this
|
||||
}
|
||||
|
||||
fun TextBadgeItem.maybeShow(): TextBadgeItem = if (this.isHidden) this.show() else this
|
||||
fun TextBadgeItem.maybeShow(): TextBadgeItem =
|
||||
if (this.isHidden) this.show() else this
|
||||
|
@ -0,0 +1,153 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils.customtabs;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import androidx.browser.customtabs.CustomTabsClient;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.browser.customtabs.CustomTabsServiceConnection;
|
||||
import androidx.browser.customtabs.CustomTabsSession;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is a helper class to manage the connection to the Custom Tabs Service.
|
||||
*/
|
||||
public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
||||
private CustomTabsSession mCustomTabsSession;
|
||||
private CustomTabsClient mClient;
|
||||
private CustomTabsServiceConnection mConnection;
|
||||
private ConnectionCallback mConnectionCallback;
|
||||
|
||||
/**
|
||||
* Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView.
|
||||
*
|
||||
* @param activity The host activity.
|
||||
* @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available.
|
||||
* @param uri the Uri to be opened.
|
||||
* @param fallback a CustomTabFallback to be used if Custom Tabs is not available.
|
||||
*/
|
||||
public static void openCustomTab(Activity activity,
|
||||
CustomTabsIntent customTabsIntent,
|
||||
Uri uri,
|
||||
CustomTabFallback fallback) {
|
||||
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
||||
|
||||
//If we cant find a package name, it means theres no browser that supports
|
||||
//Chrome Custom Tabs installed. So, we fallback to the webview
|
||||
if (packageName == null) {
|
||||
if (fallback != null) {
|
||||
fallback.openUri(activity, uri);
|
||||
}
|
||||
} else {
|
||||
customTabsIntent.intent.setPackage(packageName);
|
||||
customTabsIntent.launchUrl(activity, uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds the Activity from the Custom Tabs Service.
|
||||
*
|
||||
* @param activity the activity that is connected to the service.
|
||||
*/
|
||||
public void unbindCustomTabsService(Activity activity) {
|
||||
if (mConnection == null) return;
|
||||
activity.unbindService(mConnection);
|
||||
mClient = null;
|
||||
mCustomTabsSession = null;
|
||||
mConnection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or retrieves an exiting CustomTabsSession.
|
||||
*
|
||||
* @return a CustomTabsSession.
|
||||
*/
|
||||
public CustomTabsSession getSession() {
|
||||
if (mClient == null) {
|
||||
mCustomTabsSession = null;
|
||||
} else if (mCustomTabsSession == null) {
|
||||
mCustomTabsSession = mClient.newSession(null);
|
||||
}
|
||||
return mCustomTabsSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a Callback to be called when connected or disconnected from the Custom Tabs Service.
|
||||
*
|
||||
* @param connectionCallback
|
||||
*/
|
||||
public void setConnectionCallback(ConnectionCallback connectionCallback) {
|
||||
this.mConnectionCallback = connectionCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the Activity to the Custom Tabs Service.
|
||||
*
|
||||
* @param activity the activity to be binded to the service.
|
||||
*/
|
||||
public void bindCustomTabsService(Activity activity) {
|
||||
if (mClient != null) return;
|
||||
|
||||
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
||||
if (packageName == null) return;
|
||||
|
||||
mConnection = new ServiceConnection(this);
|
||||
CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if call to mayLaunchUrl was accepted.
|
||||
* @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
|
||||
*/
|
||||
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
|
||||
if (mClient == null) return false;
|
||||
|
||||
CustomTabsSession session = getSession();
|
||||
return session != null && session.mayLaunchUrl(uri, extras, otherLikelyBundles);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(CustomTabsClient client) {
|
||||
mClient = client;
|
||||
mClient.warmup(0L);
|
||||
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected() {
|
||||
mClient = null;
|
||||
mCustomTabsSession = null;
|
||||
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* A Callback for when the service is connected or disconnected. Use those callbacks to
|
||||
* handle UI changes when the service is connected or disconnected.
|
||||
*/
|
||||
public interface ConnectionCallback {
|
||||
/**
|
||||
* Called when the service is connected.
|
||||
*/
|
||||
void onCustomTabsConnected();
|
||||
|
||||
/**
|
||||
* Called when the service is disconnected.
|
||||
*/
|
||||
void onCustomTabsDisconnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* To be used as a fallback to open the Uri when Custom Tabs is not available.
|
||||
*/
|
||||
public interface CustomTabFallback {
|
||||
/**
|
||||
* @param activity The Activity that wants to open the Uri.
|
||||
* @param uri The uri to be opened by the fallback.
|
||||
*/
|
||||
void openUri(Activity activity, Uri uri);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils.customtabs;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import androidx.browser.customtabs.CustomTabsService;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.helpers.KeepAliveService;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
class CustomTabsHelper {
|
||||
private static final String TAG = "CustomTabsHelper";
|
||||
private static final String STABLE_PACKAGE = "com.android.chrome";
|
||||
private static final String BETA_PACKAGE = "com.chrome.beta";
|
||||
private static final String DEV_PACKAGE = "com.chrome.dev";
|
||||
private static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
|
||||
private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
|
||||
"android.support.customtabs.extra.KEEP_ALIVE";
|
||||
|
||||
private static String sPackageNameToUse;
|
||||
|
||||
private CustomTabsHelper() {
|
||||
}
|
||||
|
||||
public static void addKeepAliveExtra(Context context, Intent intent) {
|
||||
Intent keepAliveIntent = new Intent().setClassName(
|
||||
context.getPackageName(), KeepAliveService.class.getCanonicalName());
|
||||
intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
|
||||
* the one chosen by the user if there is one, otherwise makes a best effort to return a
|
||||
* valid package name.
|
||||
* <p>
|
||||
* This is <strong>not</strong> threadsafe.
|
||||
*
|
||||
* @param context {@link Context} to use for accessing {@link PackageManager}.
|
||||
* @return The package name recommended to use for connecting to custom tabs related components.
|
||||
*/
|
||||
public static String getPackageNameToUse(Context context) {
|
||||
if (sPackageNameToUse != null) return sPackageNameToUse;
|
||||
|
||||
PackageManager pm = context.getPackageManager();
|
||||
// Get default VIEW intent handler.
|
||||
Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
|
||||
ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
|
||||
String defaultViewHandlerPackageName = null;
|
||||
if (defaultViewHandlerInfo != null) {
|
||||
defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
|
||||
}
|
||||
|
||||
// Get all apps that can handle VIEW intents.
|
||||
List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, 0);
|
||||
List<String> packagesSupportingCustomTabs = new ArrayList<>();
|
||||
for (ResolveInfo info : resolvedActivityList) {
|
||||
Intent serviceIntent = new Intent();
|
||||
serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
|
||||
serviceIntent.setPackage(info.activityInfo.packageName);
|
||||
if (pm.resolveService(serviceIntent, 0) != null) {
|
||||
packagesSupportingCustomTabs.add(info.activityInfo.packageName);
|
||||
}
|
||||
}
|
||||
|
||||
// Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
|
||||
// and service calls.
|
||||
if (packagesSupportingCustomTabs.isEmpty()) {
|
||||
sPackageNameToUse = null;
|
||||
} else if (packagesSupportingCustomTabs.size() == 1) {
|
||||
sPackageNameToUse = packagesSupportingCustomTabs.get(0);
|
||||
} else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
|
||||
&& !hasSpecializedHandlerIntents(context, activityIntent)
|
||||
&& packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
|
||||
sPackageNameToUse = defaultViewHandlerPackageName;
|
||||
} else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
|
||||
sPackageNameToUse = STABLE_PACKAGE;
|
||||
} else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
|
||||
sPackageNameToUse = BETA_PACKAGE;
|
||||
} else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
|
||||
sPackageNameToUse = DEV_PACKAGE;
|
||||
} else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
|
||||
sPackageNameToUse = LOCAL_PACKAGE;
|
||||
}
|
||||
return sPackageNameToUse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check whether there is a specialized handler for a given intent.
|
||||
*
|
||||
* @param intent The intent to check with.
|
||||
* @return Whether there is a specialized handler for the given intent.
|
||||
*/
|
||||
private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) {
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
List<ResolveInfo> handlers = pm.queryIntentActivities(
|
||||
intent,
|
||||
PackageManager.GET_RESOLVED_FILTER);
|
||||
if (handlers == null || handlers.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (ResolveInfo resolveInfo : handlers) {
|
||||
IntentFilter filter = resolveInfo.filter;
|
||||
if (filter == null) continue;
|
||||
if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue;
|
||||
if (resolveInfo.activityInfo == null) continue;
|
||||
return true;
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "Runtime exception while getting specialized handlers");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All possible chrome package names that provide custom tabs feature.
|
||||
*/
|
||||
public static String[] getPackages() {
|
||||
return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE};
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils.customtabs;
|
||||
|
||||
|
||||
import android.content.ComponentName;
|
||||
import androidx.browser.customtabs.CustomTabsClient;
|
||||
import androidx.browser.customtabs.CustomTabsServiceConnection;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Implementation for the CustomTabsServiceConnection that avoids leaking the
|
||||
* ServiceConnectionCallback
|
||||
*/
|
||||
public class ServiceConnection extends CustomTabsServiceConnection {
|
||||
// A weak reference to the ServiceConnectionCallback to avoid leaking it.
|
||||
private WeakReference<ServiceConnectionCallback> mConnectionCallback;
|
||||
|
||||
public ServiceConnection(ServiceConnectionCallback connectionCallback) {
|
||||
mConnectionCallback = new WeakReference<>(connectionCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
|
||||
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
|
||||
if (connectionCallback != null) connectionCallback.onServiceConnected(client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
|
||||
if (connectionCallback != null) connectionCallback.onServiceDisconnected();
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils.customtabs;
|
||||
|
||||
|
||||
import androidx.browser.customtabs.CustomTabsClient;
|
||||
|
||||
|
||||
public interface ServiceConnectionCallback {
|
||||
/**
|
||||
* Called when the service is connected.
|
||||
*
|
||||
* @param client a CustomTabsClient
|
||||
*/
|
||||
void onServiceConnected(CustomTabsClient client);
|
||||
|
||||
/**
|
||||
* Called when the service is disconnected.
|
||||
*/
|
||||
void onServiceDisconnected();
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils.customtabs.helpers;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
|
||||
public class KeepAliveService extends Service {
|
||||
private static final Binder sBinder = new Binder();
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return sBinder;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomBaseViewHolder.java */
|
||||
package bou.amine.apps.readerforselfossv2.android.utils.drawer
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
|
||||
open class CustomBaseViewHolder(var view: View) : RecyclerView.ViewHolder(view) {
|
||||
var icon: ImageView = view.findViewById(R.id.material_drawer_icon)
|
||||
var name: TextView = view.findViewById(R.id.material_drawer_name)
|
||||
var description: TextView = view.findViewById(R.id.material_drawer_description)
|
||||
}
|
@ -2,40 +2,68 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Base64
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||
import android.widget.ImageView
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
|
||||
fun Context.bitmapCenterCrop(
|
||||
url: String,
|
||||
iv: ImageView,
|
||||
) = Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(url)
|
||||
.apply(RequestOptions.centerCropTransform())
|
||||
.into(iv)
|
||||
|
||||
fun Context.circularDrawable(
|
||||
url: String,
|
||||
view: CircleImageView,
|
||||
) {
|
||||
view.textView.text = ""
|
||||
|
||||
fun Context.bitmapCenterCrop(config: Config, url: String, iv: ImageView) =
|
||||
Glide.with(this)
|
||||
.load(url)
|
||||
.into(view.imageView)
|
||||
.asBitmap()
|
||||
.loadMaybeBasicAuth(config, url)
|
||||
.apply(RequestOptions.centerCropTransform())
|
||||
.into(iv)
|
||||
|
||||
fun Context.circularBitmapDrawable(config: Config, url: String, iv: ImageView) =
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.loadMaybeBasicAuth(config, url)
|
||||
.apply(RequestOptions.centerCropTransform())
|
||||
.into(object : BitmapImageViewTarget(iv) {
|
||||
override fun setResource(resource: Bitmap?) {
|
||||
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(
|
||||
resources,
|
||||
resource
|
||||
)
|
||||
circularBitmapDrawable.isCircular = true
|
||||
iv.setImageDrawable(circularBitmapDrawable)
|
||||
}
|
||||
})
|
||||
|
||||
fun RequestBuilder<Bitmap>.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Bitmap> {
|
||||
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
|
||||
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
|
||||
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
|
||||
builder.addHeader("Authorization", basicAuth)
|
||||
}
|
||||
val glideUrl = GlideUrl(url, builder.build())
|
||||
return this.load(glideUrl)
|
||||
}
|
||||
|
||||
fun getBitmapInputStream(
|
||||
bitmap: Bitmap,
|
||||
compressFormat: Bitmap.CompressFormat,
|
||||
): InputStream {
|
||||
fun RequestManager.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Drawable> {
|
||||
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
|
||||
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
|
||||
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
|
||||
builder.addHeader("Authorization", basicAuth)
|
||||
}
|
||||
val glideUrl = GlideUrl(url, builder.build())
|
||||
return this.load(glideUrl)
|
||||
}
|
||||
|
||||
fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream {
|
||||
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(compressFormat, 80, byteArrayOutputStream)
|
||||
val bitmapData: ByteArray = byteArrayOutputStream.toByteArray()
|
||||
return ByteArrayInputStream(bitmapData)
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils.glide
|
||||
|
||||
import android.content.Context
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.getUnsafeHttpClient
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.GlideModule
|
||||
import com.russhwolf.settings.Settings
|
||||
import java.io.InputStream
|
||||
|
||||
class SelfSignedGlideModule : GlideModule {
|
||||
|
||||
override fun applyOptions(context: Context?, builder: GlideBuilder?) {
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context?, glide: Glide?, registry: Registry?) {
|
||||
|
||||
if (context != null) {
|
||||
val settings = Settings()
|
||||
if (settings.getBoolean("isSelfSignedCert", false)) {
|
||||
val client = getUnsafeHttpClient().build()
|
||||
|
||||
registry?.append(
|
||||
GlideUrl::class.java,
|
||||
InputStream::class.java,
|
||||
com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader.Factory(client)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -26,4 +26,4 @@ fun isNetworkAccessible(context: Context): Boolean {
|
||||
val network = connectivityManager.activeNetworkInfo ?: return false
|
||||
return network.isConnectedOrConnecting
|
||||
}
|
||||
}
|
||||
}
|
@ -2,16 +2,30 @@ package bou.amine.apps.readerforselfossv2.android.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.model.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.repository.Repository
|
||||
import bou.amine.apps.readerforselfossv2.utils.ItemType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AppViewModel(private val repository: Repository) : ViewModel() {
|
||||
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
|
||||
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
|
||||
private val _refreshingIndicatorProvider = MutableSharedFlow<Boolean>()
|
||||
val refreshingIndicatorProvider = _refreshingIndicatorProvider.asSharedFlow()
|
||||
private val _toastMessageProvider = MutableSharedFlow<Int>()
|
||||
val toastMessageProvider = _toastMessageProvider.asSharedFlow()
|
||||
private var wasConnected = true
|
||||
|
||||
private val _items = MutableStateFlow(ArrayList<SelfossModel.Item>())
|
||||
val items = _items.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
repository.isConnectionAvailable.collect { isConnected ->
|
||||
@ -19,13 +33,52 @@ class AppViewModel(private val repository: Repository) : ViewModel() {
|
||||
if (isConnected && !wasConnected && repository.connectionMonitored) {
|
||||
_networkAvailableProvider.emit(true)
|
||||
wasConnected = true
|
||||
} else if (!isConnected && wasConnected && repository.connectionMonitored)
|
||||
{
|
||||
_networkAvailableProvider.emit(false)
|
||||
wasConnected = false
|
||||
}
|
||||
} else if (!isConnected && wasConnected && repository.connectionMonitored){
|
||||
_networkAvailableProvider.emit(false)
|
||||
wasConnected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateRemote() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
_toastMessageProvider.emit(R.string.refresh_in_progress)
|
||||
val updatedRemote = repository.updateRemote()
|
||||
if (updatedRemote) {
|
||||
_toastMessageProvider.emit(R.string.refresh_success_response)
|
||||
} else {
|
||||
_toastMessageProvider.emit(R.string.refresh_failer_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getItems(appendResults: Boolean, itemType: ItemType) {
|
||||
// TODO: Find a way to use Dispatchers.IO without creating conflicts
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
_refreshingIndicatorProvider.emit(true)
|
||||
repository.displayedItems = itemType
|
||||
val items = if (appendResults) {
|
||||
repository.getOlderItems()
|
||||
} else {
|
||||
repository.getNewerItems()
|
||||
}
|
||||
_items.emit(items)
|
||||
_refreshingIndicatorProvider.emit(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun markAllAsRead() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
_refreshingIndicatorProvider.emit(true)
|
||||
val success = repository.markAllAsRead(items.value)
|
||||
if (success) {
|
||||
_toastMessageProvider.emit(R.string.all_posts_read)
|
||||
} else {
|
||||
_toastMessageProvider.emit(R.string.all_posts_not_read)
|
||||
}
|
||||
_refreshingIndicatorProvider.emit(false)
|
||||
}
|
||||
}
|
||||
}
|
16
androidApp/src/main/res/anim/slide_in_right.xml
Normal file
16
androidApp/src/main/res/anim/slide_in_right.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2015 Google Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate android:fromXDelta="100%p" android:toXDelta="0"
|
||||
android:duration="@android:integer/config_mediumAnimTime"/>
|
||||
</set>
|
16
androidApp/src/main/res/anim/slide_out_left.xml
Normal file
16
androidApp/src/main/res/anim/slide_out_left.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2015 Google Inc. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate android:fromXDelta="0" android:toXDelta="-100%p"
|
||||
android:duration="@android:integer/config_mediumAnimTime"/>
|
||||
</set>
|
@ -1,11 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item>
|
||||
<shape android:shape="rectangle" >
|
||||
<solid android:color="?attr/colorSurface" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:drawable="@color/ic_launcher_background"/>
|
||||
|
||||
<item>
|
||||
<bitmap
|
||||
|
BIN
androidApp/src/main/res/drawable/bg.png
Normal file
BIN
androidApp/src/main/res/drawable/bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 406 B |
@ -1,5 +0,0 @@
|
||||
<bitmap
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:dither="true"
|
||||
android:src="@drawable/checktile"
|
||||
android:tileMode="repeat"/>
|
Binary file not shown.
Before Width: | Height: | Size: 235 B |
@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
|
||||
</vector>
|
@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M13,12h7v1.5h-7zM13,9.5h7L20,11h-7zM13,14.5h7L20,16h-7zM21,4L3,4c-1.1,0 -2,0.9 -2,2v13c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,6c0,-1.1 -0.9,-2 -2,-2zM21,19h-9L12,6h9v13z"/>
|
||||
</vector>
|
5
androidApp/src/main/res/drawable/ic_close_white_24dp.xml
Normal file
5
androidApp/src/main/res/drawable/ic_close_white_24dp.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
</vector>
|
7
androidApp/src/main/res/font/open_sans.xml
Normal file
7
androidApp/src/main/res/font/open_sans.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:fontProviderAuthority="com.google.android.gms.fonts"
|
||||
app:fontProviderPackage="com.google.android.gms"
|
||||
app:fontProviderQuery="Open Sans"
|
||||
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
|
||||
</font-family>
|
7
androidApp/src/main/res/font/roboto.xml
Normal file
7
androidApp/src/main/res/font/roboto.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:fontProviderAuthority="com.google.android.gms.fonts"
|
||||
app:fontProviderPackage="com.google.android.gms"
|
||||
app:fontProviderQuery="Roboto"
|
||||
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
|
||||
</font-family>
|
@ -4,7 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity">
|
||||
tools:context="bou.amine.apps.readerforselfossv2.android.AddSourceActivity">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@ -14,86 +14,120 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar app:popupTheme="?attr/toolbarPopupTheme" android:theme="@style/ToolBarStyle"
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize" />
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:theme="@style/ToolBarStyle"
|
||||
app:popupTheme="?attr/toolbarPopupTheme" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/formContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:id="@+id/formContainer"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
app:layout_constraintVertical_bias="0.0">
|
||||
|
||||
|
||||
<EditText
|
||||
android:id="@+id/nameInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:autofillHints="false"
|
||||
android:hint="@string/add_source_hint_name"
|
||||
android:inputType="text"
|
||||
android:textColorHint="?android:textColorPrimary"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/sourceUri"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:autofillHints="false"
|
||||
android:hint="@string/add_source_hint_url"
|
||||
android:inputType="textUri"
|
||||
android:textColorHint="?android:textColorPrimary"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/nameInput" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/tags"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:autofillHints="false"
|
||||
android:hint="@string/add_source_hint_tags"
|
||||
android:inputType="text"
|
||||
android:textColorHint="?android:textColorPrimary"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/sourceUri" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spoutsSpinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tags" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/saveBtn"
|
||||
<TextView
|
||||
android:text="@string/add_source"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/textView2"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large"
|
||||
android:textAlignment="center"
|
||||
android:layout_marginTop="16dp"
|
||||
android:elevation="5dp"
|
||||
android:text="@string/add_source_save"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:gravity="center_horizontal" />
|
||||
|
||||
<EditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:id="@+id/nameInput"
|
||||
android:layout_marginTop="32dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView2"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/spoutsSpinner" />
|
||||
android:inputType="text"
|
||||
android:hint="@string/add_source_hint_name"
|
||||
android:textColorHint="?android:textColorPrimary"
|
||||
android:autofillHints="false" />
|
||||
|
||||
<EditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:ems="10"
|
||||
android:id="@+id/sourceUri"
|
||||
android:hint="@string/add_source_hint_url"
|
||||
android:textColorHint="?android:textColorPrimary"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/nameInput"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:autofillHints="false" />
|
||||
|
||||
<EditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:id="@+id/tags"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/sourceUri"
|
||||
android:hint="@string/add_source_hint_tags"
|
||||
android:textColorHint="?android:textColorPrimary"
|
||||
android:inputType="text"
|
||||
android:autofillHints="false" />
|
||||
|
||||
<Spinner
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/spoutsSpinner"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tags"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_height="40dp"
|
||||
android:theme="@style/App.Spinner"/>
|
||||
|
||||
<Button
|
||||
android:text="@string/add_source_save"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/saveBtn"
|
||||
android:elevation="5dp"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/spoutsSpinner"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintVertical_bias="0.0"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -102,6 +136,8 @@
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
@ -1,12 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawerContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="bou.amine.apps.readerforselfossv2.android.HomeActivity"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="bou.amine.apps.readerforselfossv2.android.HomeActivity">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/coordLayout"
|
||||
@ -31,10 +32,8 @@
|
||||
android:id="@+id/toolBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:theme="@style/ToolBarStyle"
|
||||
app:popupTheme="?attr/toolbarPopupTheme"
|
||||
|
||||
/>
|
||||
app:theme="@style/ToolBarStyle"
|
||||
app:popupTheme="?attr/toolbarPopupTheme" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
@ -46,19 +45,19 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:background="?android:attr/windowBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingTop="100dp"
|
||||
android:text="@string/nothing_here"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
|
||||
android:background="@android:color/transparent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
@ -70,7 +69,7 @@
|
||||
android:paddingBottom="60dp"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:listitem="@layout/list_item" />
|
||||
tools:listitem="@layout/list_item"/>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
@ -78,13 +77,17 @@
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<com.ashokvarma.bottomnavigation.BottomNavigationBar
|
||||
android:layout_gravity="bottom"
|
||||
android:id="@+id/bottomBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="bottom"
|
||||
app:bnbActiveColor="@color/colorAccent"
|
||||
app:bnbBackgroundColor="?attr/bottomBarBackground" />
|
||||
android:layout_height="60dp"/>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView
|
||||
android:id="@+id/mainDrawer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true" />
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
@ -1,40 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutDescription="@xml/image_close_scene">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar android:theme="@style/ToolBarStyle"
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
|
||||
/>
|
||||
app:popupTheme="?attr/toolbarPopupTheme"
|
||||
app:theme="@style/ToolBarStyle" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scrollView"
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintTop_toBottomOf="@+id/appBarLayout">
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/appBarLayout" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -1,12 +1,11 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
tools:context="bou.amine.apps.readerforselfossv2.android.LoginActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
@ -15,16 +14,18 @@
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:theme="@style/ToolBarStyle"
|
||||
app:theme="@style/ToolBarStyle"
|
||||
app:popupTheme="?attr/toolbarPopupTheme" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/activity_horizontal_margin">
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin">
|
||||
<!-- Login progress -->
|
||||
<ProgressBar
|
||||
android:id="@+id/loginProgress"
|
||||
@ -32,72 +33,106 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"/>
|
||||
|
||||
<LinearLayout
|
||||
<ScrollView
|
||||
android:id="@+id/loginForm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/urlView"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_url"
|
||||
android:imeOptions="actionUnspecified"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textUri"
|
||||
android:maxLines="1"
|
||||
android:minHeight="48dp" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/selfSigned"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/disable_ssl"
|
||||
android:textAlignment="viewStart" />
|
||||
<EditText
|
||||
android:id="@+id/urlView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/prompt_url"
|
||||
android:imeOptions="actionUnspecified"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textUri"
|
||||
android:maxLines="1" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/withLogin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/withLoginSwitch"
|
||||
android:textAlignment="viewStart" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:text="@string/withLoginSwitch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:id="@+id/withLogin"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/loginView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="username"
|
||||
android:hint="@string/prompt_login"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:minHeight="48dp"
|
||||
android:visibility="gone" />
|
||||
<EditText
|
||||
android:id="@+id/loginView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="username"
|
||||
android:hint="@string/prompt_login"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:visibility="gone" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/passwordView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:hint="@string/prompt_password"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:minHeight="48dp"
|
||||
android:visibility="gone" />
|
||||
<EditText
|
||||
android:id="@+id/passwordView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:hint="@string/prompt_password"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/signInButton"
|
||||
style="?android:textAppearanceSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/action_sign_in"
|
||||
android:textStyle="bold" />
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/withHttpLogin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/withHttpLoginSwitch" />
|
||||
|
||||
</LinearLayout>
|
||||
<EditText
|
||||
android:id="@+id/httpLoginView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="username"
|
||||
android:hint="@string/prompt_http_login"
|
||||
android:inputType="text"
|
||||
android:visibility="gone" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/httpPasswordView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:hint="@string/prompt_http_password"
|
||||
android:inputType="textPassword"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/withSelfhostedCert"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/self_hosted_cert_switch" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/warningText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/self_signed_cert_warning"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/signInButton"
|
||||
style="?android:textAppearanceSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/action_sign_in"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -17,10 +17,8 @@
|
||||
android:id="@+id/toolBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:theme="@style/ToolBarStyle"
|
||||
app:popupTheme="?attr/toolbarPopupTheme"
|
||||
|
||||
/>
|
||||
app:theme="@style/ToolBarStyle" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@ -9,11 +8,11 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar app:popupTheme="?attr/toolbarPopupTheme" android:theme="@style/ToolBarStyle"
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
/>
|
||||
android:theme="@style/ToolBarStyle" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
|
@ -10,12 +10,12 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar app:popupTheme="?attr/toolbarPopupTheme" android:theme="@style/ToolBarStyle"
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
|
||||
/>
|
||||
app:theme="@style/ToolBarStyle"
|
||||
app:popupTheme="?attr/toolbarPopupTheme" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
@ -24,8 +24,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:listitem="@layout/source_list_item">
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
|
@ -1,18 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.cardview.widget.CardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintHorizontal_bias="0.62"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
card_view:cardElevation="2dp"
|
||||
card_view:cardUseCompatPadding="true"
|
||||
card_view:layout_constraintBottom_toBottomOf="parent">
|
||||
card_view:layout_constraintBottom_toBottomOf="parent"
|
||||
app:cardBackgroundColor="?cardBackgroundColor">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -24,8 +29,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:cropToPadding="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/background_splash"
|
||||
card_view:layout_constraintBottom_toTopOf="@+id/constraintLayout" />
|
||||
@ -35,17 +40,18 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemImage">
|
||||
|
||||
<bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
|
||||
<ImageView
|
||||
android:id="@+id/sourceImage"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/background_splash" />
|
||||
|
||||
@ -53,58 +59,70 @@
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:gravity="start"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/sourceImage"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintLeft_toRightOf="@+id/sourceImage"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/sourceImage"
|
||||
tools:text="Titre" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sourceTitleAndDate"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title"
|
||||
tools:text="Google Actualité Il y a 5h" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="start"
|
||||
android:textAlignment="viewStart"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
app:layout_constraintLeft_toLeftOf="@+id/title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title"
|
||||
tools:text="Google Actualité Il y a 5h" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/sourceTitleAndDate">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/browserBtn"
|
||||
android:id="@+id/favButton"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/reader_action_open"
|
||||
android:elevation="5dp"
|
||||
android:padding="4dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:srcCompat="@drawable/ic_open_in_browser_black_24dp"
|
||||
app:tint="?android:attr/textColorPrimary" />
|
||||
app:srcCompat="@drawable/ic_menu_heart_60dp"
|
||||
app:tint="@color/ic_menu_heart_color" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/shareBtn"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_toLeftOf="@+id/favButton"
|
||||
android:layout_toStartOf="@+id/favButton"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/share"
|
||||
android:elevation="5dp"
|
||||
android:padding="4dp"
|
||||
android:scaleType="centerCrop"
|
||||
@ -112,21 +130,23 @@
|
||||
app:tint="?android:attr/textColorPrimary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/favButton"
|
||||
android:id="@+id/browserBtn"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_toLeftOf="@+id/shareBtn"
|
||||
android:layout_toStartOf="@+id/shareBtn"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/add_to_favs_reader"
|
||||
android:elevation="5dp"
|
||||
android:padding="4dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:srcCompat="@drawable/ic_menu_heart_60dp"
|
||||
app:tint="@color/ic_menu_heart_color" />
|
||||
app:srcCompat="@drawable/ic_open_in_browser_black_24dp"
|
||||
app:tint="?android:attr/textColorPrimary" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/circleImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
app:shapeAppearanceOverlay="@style/circleImageView"
|
||||
app:srcCompat="@drawable/background_splash" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/circleText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:ellipsize="none"
|
||||
android:gravity="center"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/white"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="20sp"
|
||||
android:typeface="normal" />
|
||||
</RelativeLayout>
|
@ -1,96 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar2"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/filterView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filterTagsTitle"
|
||||
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/filter_item_tags"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/filterSourcesTitle"
|
||||
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/filter_item_sources"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tagsGroup" />
|
||||
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/tagsGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/filterTagsTitle"
|
||||
app:singleSelection="true">
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/sourcesGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/filterSourcesTitle">
|
||||
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/floatingActionButton2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/menu_home_search"
|
||||
android:focusable="true"
|
||||
app:backgroundTint="@color/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:rippleColor="@color/colorAccentDark"
|
||||
app:srcCompat="@drawable/ic_menu_search_white_24dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,4 +1,5 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
@ -21,22 +22,10 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="200dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/source"
|
||||
@ -47,23 +36,40 @@
|
||||
android:layout_marginRight="16dp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/titleView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
||||
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webcontent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:background="?attr/webviewBackground"
|
||||
android:paddingBottom="48dp"
|
||||
android:textColorLink="?attr/colorAccent"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:paddingBottom="48dp"
|
||||
android:background="?android:colorBackground"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/source"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@ -74,10 +80,10 @@
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|bottom|end"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_gravity="end|bottom|right">
|
||||
|
||||
<com.github.rubensousa.floatingtoolbar.FloatingToolbar
|
||||
android:id="@+id/floatingToolbar"
|
||||
@ -90,11 +96,12 @@
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_gravity="end|bottom|right"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:src="@drawable/ic_add_white_24dp"
|
||||
app:backgroundTint="?attr/colorAccent"
|
||||
app:fabSize="mini"
|
||||
@ -105,11 +112,11 @@
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0.8"
|
||||
android:visibility="gone"
|
||||
android:animateLayoutChanges="true"
|
||||
android:alpha="0.8"
|
||||
android:background="@color/black"
|
||||
android:clickable="false"
|
||||
android:visibility="gone">
|
||||
android:clickable="false">
|
||||
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
|
@ -1,16 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.github.chrisbanes.photoview.PhotoView
|
||||
android:id="@+id/photoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerVertical="true"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/checkerboard"
|
||||
app:srcCompat="@android:drawable/screen_background_dark" />
|
||||
<com.github.chrisbanes.photoview.PhotoView
|
||||
android:id="@+id/photoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:background="@android:color/black"
|
||||
app:srcCompat="@android:drawable/screen_background_dark" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -3,16 +3,17 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="88dp">
|
||||
|
||||
<bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
|
||||
<ImageView
|
||||
android:id="@+id/itemImage"
|
||||
android:layout_width="46dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginTop="21dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginLeft="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
@ -23,30 +24,39 @@
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif"
|
||||
android:gravity="start"
|
||||
android:maxLines="3"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textAllCaps="false"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemImage"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Titre" />
|
||||
tools:text="Titre"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sourceTitleAndDate"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="66dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="start"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemImage"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemImage"
|
||||
tools:text="Google Actualité Il y a 5h" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Google Actualité Il y a 5h"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="16dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -3,74 +3,48 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/deleteBtn"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@drawable/ic_remove_circle_outline_black_24dp"
|
||||
android:backgroundTint="?android:textColorSecondary"
|
||||
android:contentDescription="@string/remove_source"
|
||||
android:elevation="4dp"
|
||||
app:iconSize="34dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
|
||||
<bou.amine.apps.readerforselfossv2.android.utils.CircleImageView
|
||||
<ImageView
|
||||
android:id="@+id/itemImage"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sourceTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="17dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="start"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAlignment="textStart"
|
||||
android:textSize="13sp"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@+id/errorText"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/deleteBtn"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemImage"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Source title" />
|
||||
tools:text="source title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10sp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="@color/red"
|
||||
android:textStyle="italic"
|
||||
android:visibility="gone"
|
||||
<Button
|
||||
android:id="@+id/deleteBtn"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="34dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/ic_remove_circle_outline_black_24dp"
|
||||
android:backgroundTint="?android:textColorSecondary"
|
||||
android:elevation="4dp"
|
||||
android:contentDescription="@string/remove_source"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/deleteBtn"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemImage"
|
||||
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
|
||||
tools:visibility="visible" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -8,40 +8,18 @@
|
||||
app:showAsAction="ifRoom|collapseActionView"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView" />
|
||||
|
||||
<item android:id="@+id/action_filter"
|
||||
android:title="@string/menu_home_filter"
|
||||
android:icon="@drawable/ic_baseline_filter_alt_24"
|
||||
android:orderInCategory="1"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item android:id="@+id/readAll"
|
||||
android:icon="@drawable/ic_menu_done_all_white_24dp"
|
||||
android:title="@string/readAll"
|
||||
android:orderInCategory="2"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item android:id="@+id/action_sources"
|
||||
android:title="@string/menu_home_sources"
|
||||
android:orderInCategory="97"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item android:id="@+id/action_settings"
|
||||
android:title="@string/title_activity_settings"
|
||||
android:orderInCategory="98"
|
||||
app:showAsAction="never"/>
|
||||
android:orderInCategory="1"
|
||||
app:showAsAction="always"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/refresh"
|
||||
app:showAsAction="never"
|
||||
android:orderInCategory="101"
|
||||
android:icon="@drawable/ic_menu_refresh_white_24dp"
|
||||
android:orderInCategory="99"
|
||||
android:title="@string/menu_home_refresh" />
|
||||
|
||||
<item
|
||||
android:id="@+id/issue_tracker"
|
||||
app:showAsAction="never"
|
||||
android:orderInCategory="103"
|
||||
android:title="@string/issue_tracker_link" />
|
||||
|
||||
<item android:id="@+id/action_disconnect"
|
||||
android:title="@string/action_disconnect"
|
||||
android:orderInCategory="104"
|
||||
|
@ -3,13 +3,6 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
|
||||
|
||||
<item
|
||||
android:id="@+id/issue_tracker"
|
||||
app:showAsAction="never"
|
||||
android:orderInCategory="101"
|
||||
android:title="@string/issue_tracker_link" />
|
||||
|
||||
<item android:id="@+id/about"
|
||||
android:title="@string/action_about"
|
||||
android:orderInCategory="102"
|
||||
|
@ -8,6 +8,12 @@
|
||||
android:title="@string/unmark"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/more_action"
|
||||
android:icon="@drawable/ic_chrome_reader_mode_white_24dp"
|
||||
android:title="@string/reader_action_more"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/open_action"
|
||||
android:icon="@drawable/ic_open_in_browser_white_24dp"
|
||||
|
@ -1,16 +0,0 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/open_action"
|
||||
android:icon="@drawable/ic_open_in_browser_white_24dp"
|
||||
android:title="@string/reader_action_open"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/share_action"
|
||||
android:icon="@drawable/ic_share_white_24dp"
|
||||
android:title="@string/reader_action_share"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
10
androidApp/src/main/res/menu/settings_theme.xml
Normal file
10
androidApp/src/main/res/menu/settings_theme.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/clear"
|
||||
android:icon="@drawable/ic_history_white_24dp"
|
||||
android:title="@string/drawer_action_clear"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
@ -3,13 +3,17 @@
|
||||
<string name="app_name">"Lector per a Selfoss"</string>
|
||||
<string name="title_activity_login">"Inicia la sessió"</string>
|
||||
<string name="prompt_password">"Contrasenya"</string>
|
||||
<string name="prompt_http_password">"Contrasenya HTTP"</string>
|
||||
<string name="action_sign_in">"Vés-hi"</string>
|
||||
<string name="error_invalid_password">"La contrasenya és massa curta"</string>
|
||||
<string name="error_field_required">"Camp necessari"</string>
|
||||
<string name="prompt_url">"URL"</string>
|
||||
<string name="withLoginSwitch">"Autenticació (si és necessària)"</string>
|
||||
<string name="withHttpLoginSwitch">"Autenticació HTTP (si és necessària)"</string>
|
||||
<string name="login_url_problem">"Pot ser que falti una \"/\" al final de l'url."</string>
|
||||
<string name="prompt_login">"Nom d'usuari"</string>
|
||||
<string name="prompt_http_login">"Nom d'usuari HTTP"</string>
|
||||
<string name="label_share">"Comparteix"</string>
|
||||
<string name="readAll">"Llegeix-ho tot"</string>
|
||||
<string name="action_disconnect">"Desconnecta't"</string>
|
||||
<string name="title_activity_settings">"Configuració"</string>
|
||||
@ -22,6 +26,9 @@
|
||||
<string name="wrong_infos">"Torneu a comprovar la informació."</string>
|
||||
<string name="all_posts_not_read">"No s'han llegit totes les publicacions"</string>
|
||||
<string name="all_posts_read">"S'han llegit totes les publicacions"</string>
|
||||
<string name="cant_get_favs">"No es poden obtenir preferits"</string>
|
||||
<string name="cant_get_new_elements">"No es pot accedir als articles nous"</string>
|
||||
<string name="cant_get_read">"No es poden llegir els articles"</string>
|
||||
<string name="nothing_here">"No hi ha res"</string>
|
||||
<string name="tab_new">"Nou"</string>
|
||||
<string name="tab_read">"Tot"</string>
|
||||
@ -41,14 +48,29 @@
|
||||
<string name="issue_tracker_summary">"Informa d'un error o pregunta sobre funcions noves"</string>
|
||||
<string name="warning_wrong_url">"ADVERTÈNCIA"</string>
|
||||
<string name="pref_switch_card_view_title">"Visualització de targeta"</string>
|
||||
<string name="cant_mark_favortie">"No es pot marcar l'article com a preferit"</string>
|
||||
<string name="cant_unmark_favortie">"No es pot treure l'element de preferits"</string>
|
||||
<string name="share">"Comparteix"</string>
|
||||
<string name="rating_prompt_title">"Us agrada l'aplicació?"</string>
|
||||
<string name="rating_prompt_yes">"Sí."</string>
|
||||
<string name="rating_prompt_no">"No gaire…"</string>
|
||||
<string name="rating_prompt_feedback_title">"Ens podeu dir per què?"</string>
|
||||
<string name="rating_prompt_feedback_yes">"D'acord."</string>
|
||||
<string name="rating_prompt_feedback_no">"Ara no."</string>
|
||||
<string name="rating_prompt_rating_title">"Perfecte! Ens podeu puntuar a la Botiga?"</string>
|
||||
<string name="rating_prompt_rating_yes">"Sí."</string>
|
||||
<string name="rating_prompt_rating_no">"Ara no."</string>
|
||||
<string name="rating_prompt_thanks">"Gràcies. La vostra opinió ens ajuda a millorar l'aplicació."</string>
|
||||
<string name="switch_unread_count">"Mostra el recompte d'articles no llegits amb un distintiu a la barra inferior."</string>
|
||||
<string name="switch_unread_count_title">"Recompte d'articles no llegits"</string>
|
||||
<string name="display_all_counts_title">"Recompte d'articles llegits i preferits"</string>
|
||||
<string name="text_wrong_url">"Sembla que esteu utilitzant un URL no vàlid. Assegureu-vos que és correcte, i si el problema persisteix, poseu-vos en contacte amb mi (a través de l'enllaç de contacte que hi ha a la Botiga). Tingueu en compte que per utilitzar aquesta aplicació cal que també utilitzeu Selfoss. Si no, no podreu accedir a canals RSS."</string>
|
||||
<string name="pref_article_viewer_title">"Obre els enllaços dins de l'aplicació"</string>
|
||||
<string name="pref_article_viewer_on">"Els articles s'obriran dins de l'aplicació"</string>
|
||||
<string name="pref_article_viewer_off">"Els articles s'obriran amb el navegador predeterminat"</string>
|
||||
<string name="pref_general_internal_browser_title">"Obre els enllaços dins de l'aplicació"</string>
|
||||
<string name="pref_general_internal_browser_on">"Els articles s'obriran dins de l'aplicació"</string>
|
||||
<string name="pref_general_internal_browser_off">"Els articles s'obriran amb el navegador predeterminat"</string>
|
||||
<string name="prefer_article_viewer_title">"Obre el visualitzador d'articles"</string>
|
||||
<string name="prefer_article_viewer_on">"S'obrirà el visualitzador d'articles en lloc del navegador intern"</string>
|
||||
<string name="prefer_article_viewer_off">"S'obrirà el navegador intern en lloc del visualitzador d'articles"</string>
|
||||
<string name="pref_general_category_links">"Gestió d'enllaços"</string>
|
||||
<string name="pref_general_category_displaying">"Visualització"</string>
|
||||
<string name="pref_switch_card_view_on">"Els articles es mostraran com a targetes"</string>
|
||||
@ -61,28 +83,58 @@
|
||||
<string name="card_height_on">L\'alçada de les targetes s\'ajustarà al seu contingut</string>
|
||||
<string name="card_height_off">L\'alçada de les targetes serà fixa</string>
|
||||
<string name="source_code">Codi font</string>
|
||||
<string name="filter_item_tags">Etiquetes</string>
|
||||
<string name="filter_item_sources">Fonts</string>
|
||||
<string name="cant_mark_read">No es pot marcar l\'article com a llegit</string>
|
||||
<string name="cant_mark_unread">Can\'t mark article as unread</string>
|
||||
<string name="drawer_error_loading_tags">S\'ha produït un error en carregar les etiquetes</string>
|
||||
<string name="drawer_error_loading_sources">S\'ha produït un error en carregar les fonts</string>
|
||||
<string name="drawer_item_filters">Filtres</string>
|
||||
<string name="drawer_action_clear">Esborra</string>
|
||||
<string name="drawer_item_tags">Etiquetes</string>
|
||||
<string name="drawer_item_sources">Fonts</string>
|
||||
<string name="drawer_action_edit">Edita</string>
|
||||
<string name="no_tags_loaded">No s\'ha carregat cap etiqueta</string>
|
||||
<string name="no_sources_loaded">No s\'ha carregat cap font</string>
|
||||
<string name="drawer_loading">S\'està carregant…</string>
|
||||
<string name="menu_home_search">Cerca</string>
|
||||
<string name="can_delete_source">No es pot suprimir la font</string>
|
||||
<string name="base_url_error">S\'ha produït un error en comunicar-se amb la instància de Selfoss. Si el problema persisteix, posa\'t en contacte amb mi.</string>
|
||||
<string name="pref_header_theme">Temes</string>
|
||||
<string name="default_theme">Predeterminat</string>
|
||||
<string name="default_dark_theme">Predeterminat/Fosc</string>
|
||||
<string name="pref_header_debug">Depuració</string>
|
||||
<string name="self_hosted_cert_switch">Utilitzeu un certificat autoallotjat?</string>
|
||||
<string name="self_signed_cert_warning">Per raons de seguretat, els certificats autosignats no seran compatibles per defecte. En activar aquesta opció, sereu responsable de qualsevol problema de seguretat que es pugui produir.</string>
|
||||
<string name="pref_selfoss_category">API de Selfoss</string>
|
||||
<string name="pref_api_items_number_title">Nombre d\'elements carregats</string>
|
||||
<string name="pref_hidden_tags">Etiquetes ocultes</string>
|
||||
<string name="summary_debug_identifier">Identificador de depuració</string>
|
||||
<string name="unique_id_to_clipboard">S\'ha copiat l\'identificador al porta-retalls</string>
|
||||
<string name="display_header_drawer_summary">Mostra una capçalera amb la instància URL de Selfoss al panell lateral.</string>
|
||||
<string name="display_header_drawer_title">Capçalera de menú</string>
|
||||
<string name="pref_general_infinite_loading_title">Carrega articles en desplaçar</string>
|
||||
<string name="translation">Traducció</string>
|
||||
<string name="cant_open_invalid_url">L\'element URL no és vàlid. Estic intentant solucionar aquest problema perquè l\'aplicació no falli.</string>
|
||||
<string name="drawer_report_bug">Informa d\'un error</string>
|
||||
<string name="items_number_should_be_number">El nombre d\'elements ha de ser enter.</string>
|
||||
<string name="reader_action_more">Més informació</string>
|
||||
<string name="reader_action_open">Obre al navegador</string>
|
||||
<string name="reader_action_share">Comparteix</string>
|
||||
<string name="pref_switch_actions_pager_scroll_on">Es marcaran els articles com a llegits en lliscar el dit d\'un article a l\'altre.</string>
|
||||
<string name="add_to_favs_reader">Afegeix als preferits</string>
|
||||
<string name="remove_to_favs_reader">Suprimeix dels preferits</string>
|
||||
<string name="pref_content_reader_font_size">Mida de la lletra del lector d’articles</string>
|
||||
<string name="pref_header_viewer">Visualitzador d\'articles</string>
|
||||
<string name="refresh_dialog_message">Aquesta acció actualitzarà la vostra instància de Selfoss.</string>
|
||||
<string name="markall_dialog_message">Aquesta acció marcarà els elements com a llegits.</string>
|
||||
<string name="pref_switch_actions_pager_scroll">Marca com a llegit en lliscar el dit</string>
|
||||
<string name="pref_switch_actions_pager_scroll_off">No es marcaran els articles com a llegits en lliscar el dit d\'un article a l\'altre.</string>
|
||||
<string name="pref_acra_alwaysaccept">Envia informes d\'error automàtics</string>
|
||||
<string name="pref_acra_alwaysaccept_enabled">S\'enviaran informes d\'error automàticament</string>
|
||||
<string name="pref_acra_alwaysaccept_disabled">Us preguntarem abans d\'enviar un informe d\'error.</string>
|
||||
<string name="pref_debug_crash_reports">Informes d\'error</string>
|
||||
<string name="pref_debug_debug_logs">Registre de depuració (s\'enviarà automàticament)</string>
|
||||
<string name="acra_login">Habilita el registre</string>
|
||||
<string name="drawer_item_hidden_tags">Etiquetes ocultes</string>
|
||||
<string name="unmark">Marca com no llegit</string>
|
||||
<string name="pref_header_offline">Sense connexió i memòria clau</string>
|
||||
<string name="pref_switch_items_caching_off">Els articles no es guardaran a la memòria del dispositiu i l\'aplicació no es podrà utilitzar sense connexió.</string>
|
||||
@ -90,6 +142,7 @@
|
||||
<string name="pref_switch_items_caching">Guarda els elements per utilitzar-los sense connexió</string>
|
||||
<string name="pref_switch_update_sources">Check for new sources and tags</string>
|
||||
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
|
||||
<string name="no_network_connectivity">Sense connexió!</string>
|
||||
<string name="network_connectivity_lost">"Network connection lost"</string>
|
||||
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
|
||||
<string name="pref_switch_periodic_refresh">Sincronitza els articles</string>
|
||||
@ -97,7 +150,7 @@
|
||||
<string name="pref_switch_periodic_refresh_on">Els articles se sincronitzaran periòdicament</string>
|
||||
<string name="pref_periodic_refresh_minutes_title"><![CDATA[Interval de sincronització ( >= 15 minuts)]]></string>
|
||||
<string name="pref_switch_refresh_when_charging">Sincronitza només quan el telèfon s\'està carregant</string>
|
||||
<string name="loading_notification_title">S\'està carregant…</string>
|
||||
<string name="loading_notification_title">S\'està carregant...</string>
|
||||
<string name="loading_notification_text">Selfoss està sincronitzant els articles</string>
|
||||
<string name="notification_channel_sync">Notificació de sincronització</string>
|
||||
<string name="new_items_channel_sync">Notificació d\'elements nous</string>
|
||||
@ -116,19 +169,4 @@
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
|
||||
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
|
||||
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
|
||||
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
|
||||
<string name="menu_home_filter">Filters</string>
|
||||
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
|
||||
<string name="menu_home_sources">Sources</string>
|
||||
<string name="update_source">Update source</string>
|
||||
<string name="confirm_disconnect_title">Disconnect ?</string>
|
||||
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
|
||||
<string name="disable_ssl">Disable SSL</string>
|
||||
</resources>
|
||||
|
@ -3,13 +3,17 @@
|
||||
<string name="app_name">"Reader für selfoss"</string>
|
||||
<string name="title_activity_login">"Anmelden"</string>
|
||||
<string name="prompt_password">"Passwort"</string>
|
||||
<string name="prompt_http_password">"HTTP Passwort"</string>
|
||||
<string name="action_sign_in">"Fortfahren"</string>
|
||||
<string name="error_invalid_password">"Passwort ist nicht lang genug"</string>
|
||||
<string name="error_field_required">"Pflichtfeld"</string>
|
||||
<string name="prompt_url">"URL"</string>
|
||||
<string name="withLoginSwitch">"Anmeldung erforderlich?"</string>
|
||||
<string name="withHttpLoginSwitch">"HTTP Anmeldung erforderlich?"</string>
|
||||
<string name="login_url_problem">"Ups. Du musst eventuell ein \"/\" am Ende der URL anhängen."</string>
|
||||
<string name="prompt_login">"Benutzername"</string>
|
||||
<string name="prompt_http_login">"HTTP Benutzername"</string>
|
||||
<string name="label_share">"Teilen"</string>
|
||||
<string name="readAll">"Alle gelesen"</string>
|
||||
<string name="action_disconnect">"Verbindung trennen"</string>
|
||||
<string name="title_activity_settings">"Einstellungen"</string>
|
||||
@ -22,6 +26,9 @@
|
||||
<string name="wrong_infos">"Überprüfe deine Angaben noch einmal."</string>
|
||||
<string name="all_posts_not_read">"Nicht alle Beiträge wurden gelesen"</string>
|
||||
<string name="all_posts_read">"Alle Beiträge wurden gelesen"</string>
|
||||
<string name="cant_get_favs">"Favoriten können nicht abgerufen werden"</string>
|
||||
<string name="cant_get_new_elements">"Neue Artikel können nicht abgerufen werden"</string>
|
||||
<string name="cant_get_read">"Gelese Artikel können nicht abgerufen werden"</string>
|
||||
<string name="nothing_here">"Keine Einträge vorhanden"</string>
|
||||
<string name="tab_new">"Neu"</string>
|
||||
<string name="tab_read">"Alle"</string>
|
||||
@ -41,14 +48,29 @@
|
||||
<string name="issue_tracker_summary">"Melde einen Bug oder rege ein neues Feature an"</string>
|
||||
<string name="warning_wrong_url">"WARNUNG"</string>
|
||||
<string name="pref_switch_card_view_title">"Kachelansicht"</string>
|
||||
<string name="cant_mark_favortie">"Artikel kann nicht als Favorit markiert werden"</string>
|
||||
<string name="cant_unmark_favortie">"Eintrag kann nicht aus Favoriten entfernt werden"</string>
|
||||
<string name="share">"Teilen"</string>
|
||||
<string name="rating_prompt_title">"Gefällt Dir die App?"</string>
|
||||
<string name="rating_prompt_yes">"Ja!"</string>
|
||||
<string name="rating_prompt_no">"Nicht wirklich…"</string>
|
||||
<string name="rating_prompt_feedback_title">"Magst du uns sagen warum?"</string>
|
||||
<string name="rating_prompt_feedback_yes">"OK!"</string>
|
||||
<string name="rating_prompt_feedback_no">"Nicht jetzt."</string>
|
||||
<string name="rating_prompt_rating_title">"Wunderbar! Magst du uns im Play Store bewerten?"</string>
|
||||
<string name="rating_prompt_rating_yes">"Sicher!"</string>
|
||||
<string name="rating_prompt_rating_no">"Nicht jetzt."</string>
|
||||
<string name="rating_prompt_thanks">"Vielen Dank, dein Feedback hilft die App zu verbessern!"</string>
|
||||
<string name="switch_unread_count">"Zeige die Zahl ungelesener Artikel in der unteren Leiste."</string>
|
||||
<string name="switch_unread_count_title">"Zeige Anzahl ungelesener Artikel"</string>
|
||||
<string name="display_all_counts_title">"Zeige Anzahl der Favoriten und gelesenen Artikel"</string>
|
||||
<string name="text_wrong_url">"Sie scheinen eine ungültige URL verwenden. Stellen Sie sicher, dass die URL richtig ist. Sollte das Problem weiterhin bestehen kontaktieren Sie mich (über den Playstore-Kontakt-Link). Bitte beachten Sie, dass Sie Selfoss benötigen um RSS-Feeds zu lesen."</string>
|
||||
<string name="pref_article_viewer_title">"Öffne Links innerhalb der App"</string>
|
||||
<string name="pref_article_viewer_on">"Artikel werden innerhalb der App geöffnet"</string>
|
||||
<string name="pref_article_viewer_off">"Artikel werden mit deinem Standard-Browser geöffnet"</string>
|
||||
<string name="pref_general_internal_browser_title">"Öffne Links innerhalb der App"</string>
|
||||
<string name="pref_general_internal_browser_on">"Artikel werden innerhalb der App geöffnet"</string>
|
||||
<string name="pref_general_internal_browser_off">"Artikel werden mit deinem Standard-Browser geöffnet"</string>
|
||||
<string name="prefer_article_viewer_title">"Verwenden Sie den Artikel-viewer"</string>
|
||||
<string name="prefer_article_viewer_on">"Artikel-Viewer wird anstelle des internen Browser verwendet"</string>
|
||||
<string name="prefer_article_viewer_off">"Der internen Browser wird anstelle des Artikel-Viewer verwendet"</string>
|
||||
<string name="pref_general_category_links">"Umgang mit Links"</string>
|
||||
<string name="pref_general_category_displaying">"Ansicht"</string>
|
||||
<string name="pref_switch_card_view_on">"Artikel werden als Kacheln angezeigt"</string>
|
||||
@ -61,28 +83,58 @@
|
||||
<string name="card_height_on">Kartenhöhe passt sich Inhalt an</string>
|
||||
<string name="card_height_off">Kartenhöhe ist fix</string>
|
||||
<string name="source_code">Quellcode</string>
|
||||
<string name="filter_item_tags">Tags</string>
|
||||
<string name="filter_item_sources">Quellen</string>
|
||||
<string name="cant_mark_read">Artikel kann nicht als gelesen markiert werden</string>
|
||||
<string name="cant_mark_unread">Can\'t mark article as unread</string>
|
||||
<string name="drawer_error_loading_tags">Fehler beim Laden der Tags…</string>
|
||||
<string name="drawer_error_loading_sources">Fehler beim Laden der Quellen…</string>
|
||||
<string name="drawer_item_filters">Filter</string>
|
||||
<string name="drawer_action_clear">leeren</string>
|
||||
<string name="drawer_item_tags">Tags</string>
|
||||
<string name="drawer_item_sources">Quellen</string>
|
||||
<string name="drawer_action_edit">bearbeiten</string>
|
||||
<string name="no_tags_loaded">No tags loaded</string>
|
||||
<string name="no_sources_loaded">Keine Quellen geladen</string>
|
||||
<string name="drawer_loading">Lade…</string>
|
||||
<string name="menu_home_search">Suche</string>
|
||||
<string name="can_delete_source">Can\'t delete the source…</string>
|
||||
<string name="base_url_error">Beim Versuch deine Selfoss-Instanz zu erreichen ist ein Fehler aufgetreten. Solltet dieser Fehler bestehen bleiben, trete bitte mit mir in Kontakt.</string>
|
||||
<string name="pref_header_theme">Designs</string>
|
||||
<string name="default_theme">Standard</string>
|
||||
<string name="default_dark_theme">Standard (Dunkel)</string>
|
||||
<string name="pref_header_debug">Debug</string>
|
||||
<string name="self_hosted_cert_switch">Verwenden Sie einen selbst gehostetes Zertifikat?</string>
|
||||
<string name="self_signed_cert_warning">Due to security reasons, self signed certificates are not supported by default. By activating this, I\'ll not be responsible of any security problem you encounter.</string>
|
||||
<string name="pref_selfoss_category">selfoss API</string>
|
||||
<string name="pref_api_items_number_title">Loaded items number</string>
|
||||
<string name="pref_hidden_tags">Hidden Tags</string>
|
||||
<string name="summary_debug_identifier">Debug identifier</string>
|
||||
<string name="unique_id_to_clipboard">Identifier copied to your clipboard</string>
|
||||
<string name="display_header_drawer_summary">Display a header with the selfoss instance url on the lateral drawer.</string>
|
||||
<string name="display_header_drawer_title">Account header</string>
|
||||
<string name="pref_general_infinite_loading_title">Load more articles on scroll</string>
|
||||
<string name="translation">Übersetzung</string>
|
||||
<string name="cant_open_invalid_url">The item url is invalid. I\'m looking into solving this issue so the app won\'t crash.</string>
|
||||
<string name="drawer_report_bug">Melde einen Fehler</string>
|
||||
<string name="items_number_should_be_number">The items number should be an integer.</string>
|
||||
<string name="reader_action_more">Read more</string>
|
||||
<string name="reader_action_open">Im Browser öffnen</string>
|
||||
<string name="reader_action_share">Teilen</string>
|
||||
<string name="pref_switch_actions_pager_scroll_on">Mark articles as read when swiping between articles.</string>
|
||||
<string name="add_to_favs_reader">Zu Favoriten hinzufügen</string>
|
||||
<string name="remove_to_favs_reader">Aus Favoriten entfernen</string>
|
||||
<string name="pref_content_reader_font_size">Article reader content font size</string>
|
||||
<string name="pref_header_viewer">Article viewer</string>
|
||||
<string name="refresh_dialog_message">This will refresh your Selfoss instance.</string>
|
||||
<string name="markall_dialog_message">Dies wird alle Elemente als gelesen markieren.</string>
|
||||
<string name="pref_switch_actions_pager_scroll">Beim Wischen als gelesen markieren</string>
|
||||
<string name="pref_switch_actions_pager_scroll_off">Don\'t mark articles as read when swiping.</string>
|
||||
<string name="pref_acra_alwaysaccept">Automatically send crash reports</string>
|
||||
<string name="pref_acra_alwaysaccept_enabled">Fehlerberichte werden automatisch gesendet</string>
|
||||
<string name="pref_acra_alwaysaccept_disabled">Will ask everytime when sending crash reports.</string>
|
||||
<string name="pref_debug_crash_reports">Fehlerberichte</string>
|
||||
<string name="pref_debug_debug_logs">Debug logging (these will be sent without a dialog)</string>
|
||||
<string name="acra_login">Protokollierung aktivieren</string>
|
||||
<string name="drawer_item_hidden_tags">Hidden Tags</string>
|
||||
<string name="unmark">Eintrag als ungelesen markieren</string>
|
||||
<string name="pref_header_offline">Offline and cache</string>
|
||||
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
|
||||
@ -90,6 +142,7 @@
|
||||
<string name="pref_switch_items_caching">Save items for offline use</string>
|
||||
<string name="pref_switch_update_sources">Check for new sources and tags</string>
|
||||
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
|
||||
<string name="no_network_connectivity">Nicht verbunden !</string>
|
||||
<string name="network_connectivity_lost">"Die Netzwerkverbindung wurde unterbrochen"</string>
|
||||
<string name="network_connectivity_retrieved">"Netzwerkverbindung ist jetzt verfügbar"</string>
|
||||
<string name="pref_switch_periodic_refresh">Synchronisiere Artikel</string>
|
||||
@ -97,7 +150,7 @@
|
||||
<string name="pref_switch_periodic_refresh_on">Die Artikel werden regelmäßig synchronisiert</string>
|
||||
<string name="pref_periodic_refresh_minutes_title"><![CDATA[Sync interval ( >= 15 minutes)]]></string>
|
||||
<string name="pref_switch_refresh_when_charging">Nur aktualisieren, wenn das Telefon aufgeladen wird</string>
|
||||
<string name="loading_notification_title">Lädt…</string>
|
||||
<string name="loading_notification_title">Lädt...</string>
|
||||
<string name="loading_notification_text">Selfoss synchronisiert Ihre Artikel</string>
|
||||
<string name="notification_channel_sync">Sync notification</string>
|
||||
<string name="new_items_channel_sync">New items notification</string>
|
||||
@ -116,19 +169,4 @@
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
|
||||
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
|
||||
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
|
||||
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
|
||||
<string name="menu_home_filter">Filters</string>
|
||||
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
|
||||
<string name="menu_home_sources">Sources</string>
|
||||
<string name="update_source">Update source</string>
|
||||
<string name="confirm_disconnect_title">Disconnect ?</string>
|
||||
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
|
||||
<string name="disable_ssl">Disable SSL</string>
|
||||
</resources>
|
||||
|
@ -3,13 +3,17 @@
|
||||
<string name="app_name">"Lector para Selfoss"</string>
|
||||
<string name="title_activity_login">"Iniciar sesión"</string>
|
||||
<string name="prompt_password">"Contraseña"</string>
|
||||
<string name="prompt_http_password">"Contraseña HTTP"</string>
|
||||
<string name="action_sign_in">"Empezar"</string>
|
||||
<string name="error_invalid_password">"La contraseña no es suficientemente larga"</string>
|
||||
<string name="error_field_required">"Campo requerido"</string>
|
||||
<string name="prompt_url">"Url"</string>
|
||||
<string name="withLoginSwitch">"Inicio de sesión requerido ?"</string>
|
||||
<string name="withHttpLoginSwitch">"Inicio de sesión HTTP requerido ?"</string>
|
||||
<string name="login_url_problem">"Oops. Puede que necesite añadir un \"/\" al final de la url."</string>
|
||||
<string name="prompt_login">"Nombre de usuario"</string>
|
||||
<string name="prompt_http_login">"Nombre de usuario HTTP"</string>
|
||||
<string name="label_share">"Compartir"</string>
|
||||
<string name="readAll">"Leer todo"</string>
|
||||
<string name="action_disconnect">"Desconectar"</string>
|
||||
<string name="title_activity_settings">"Configuración"</string>
|
||||
@ -22,6 +26,9 @@
|
||||
<string name="wrong_infos">"Revise sus datos de nuevo."</string>
|
||||
<string name="all_posts_not_read">"No todas las publicaciones fueron leídas"</string>
|
||||
<string name="all_posts_read">"Todas las publicaciones fueron leídas"</string>
|
||||
<string name="cant_get_favs">"No se pueden obtener favoritos"</string>
|
||||
<string name="cant_get_new_elements">"No puede recibir nuevos artículos"</string>
|
||||
<string name="cant_get_read">"No puede recibir artículos leídos"</string>
|
||||
<string name="nothing_here">"Nada aquí"</string>
|
||||
<string name="tab_new">"Nuevo"</string>
|
||||
<string name="tab_read">"Todo"</string>
|
||||
@ -41,14 +48,29 @@
|
||||
<string name="issue_tracker_summary">"Reportar un error o solicitar una nueva función"</string>
|
||||
<string name="warning_wrong_url">"ADVERTENCIA"</string>
|
||||
<string name="pref_switch_card_view_title">"Vista de la tarjeta"</string>
|
||||
<string name="cant_mark_favortie">"No puede marcar el artículo como favorito"</string>
|
||||
<string name="cant_unmark_favortie">"No se puede quitar el artículo de favoritos"</string>
|
||||
<string name="share">"Compartir"</string>
|
||||
<string name="rating_prompt_title">"¿Disfrutando la aplicación?"</string>
|
||||
<string name="rating_prompt_yes">"¡Sí!"</string>
|
||||
<string name="rating_prompt_no">"La verdad es que no…"</string>
|
||||
<string name="rating_prompt_feedback_title">"¿Puede decirnos por qué?"</string>
|
||||
<string name="rating_prompt_feedback_yes">"¡Vale!"</string>
|
||||
<string name="rating_prompt_feedback_no">"Ahora no."</string>
|
||||
<string name="rating_prompt_rating_title">"¡Excelente! ¿Puede valorarnos en la tienda?"</string>
|
||||
<string name="rating_prompt_rating_yes">"¡Claro!"</string>
|
||||
<string name="rating_prompt_rating_no">"No en este momento."</string>
|
||||
<string name="rating_prompt_thanks">"¡Gracias, sus comentarios ayudan a mejorar la aplicación!"</string>
|
||||
<string name="switch_unread_count">"Mostrar el recuento no leído como una insignia de la barra inferior."</string>
|
||||
<string name="switch_unread_count_title">"Mostrar recuento no leído"</string>
|
||||
<string name="display_all_counts_title">"Mostrar recuento de favoritos y leídos"</string>
|
||||
<string name="text_wrong_url">"Parece estar tratando de utilizar una dirección URL inválida. Asegúrese de que sea correcta y si el problema persiste, póngase en contacto conmigo (mediante el enlace de contacto de la tienda). Tenga en cuenta que la aplicación necesita utilizar Selfoss. No se puede acceder al contenido RSS sin él."</string>
|
||||
<string name="pref_article_viewer_title">"Abrir enlaces dentro de la aplicación"</string>
|
||||
<string name="pref_article_viewer_on">"Los artículos se abrirán dentro de la aplicación"</string>
|
||||
<string name="pref_article_viewer_off">"Los artículos se abrirán con tu navegador predeterminado"</string>
|
||||
<string name="pref_general_internal_browser_title">"Abrir enlaces dentro de la aplicación"</string>
|
||||
<string name="pref_general_internal_browser_on">"Los artículos se abrirán dentro de la aplicación"</string>
|
||||
<string name="pref_general_internal_browser_off">"Los artículos se abrirán con tu navegador predeterminado"</string>
|
||||
<string name="prefer_article_viewer_title">"Utilizar el visor de artículo"</string>
|
||||
<string name="prefer_article_viewer_on">"Se usará el visor de artículos en lugar del navegador interno"</string>
|
||||
<string name="prefer_article_viewer_off">"Se utilizará el navegador interno en lugar del visor de artículo"</string>
|
||||
<string name="pref_general_category_links">"Control de enlaces"</string>
|
||||
<string name="pref_general_category_displaying">"Mostrando"</string>
|
||||
<string name="pref_switch_card_view_on">"Los artículos se mostrarán como tarjetas"</string>
|
||||
@ -61,28 +83,58 @@
|
||||
<string name="card_height_on">Altura de tarjetas se ajustará a su contenido</string>
|
||||
<string name="card_height_off">Se fijará la altura de la tarjeta</string>
|
||||
<string name="source_code">Código fuente</string>
|
||||
<string name="filter_item_tags">Etiquetas</string>
|
||||
<string name="filter_item_sources">Fuentes</string>
|
||||
<string name="cant_mark_read">No puede marcar el artículo como leído</string>
|
||||
<string name="cant_mark_unread">No se puede marcar el artículo como no leído</string>
|
||||
<string name="drawer_error_loading_tags">Error al cargar etiquetas…</string>
|
||||
<string name="drawer_error_loading_sources">Error al cargar fuentes…</string>
|
||||
<string name="drawer_item_filters">Filtros</string>
|
||||
<string name="drawer_action_clear">limpiar</string>
|
||||
<string name="drawer_item_tags">Etiquetas</string>
|
||||
<string name="drawer_item_sources">Fuentes</string>
|
||||
<string name="drawer_action_edit">editar</string>
|
||||
<string name="no_tags_loaded">No hay etiquetas cargadas</string>
|
||||
<string name="no_sources_loaded">No hay fuentes cargadas</string>
|
||||
<string name="drawer_loading">Cargando…</string>
|
||||
<string name="menu_home_search">Buscar</string>
|
||||
<string name="can_delete_source">No se puede eliminar la fuente…</string>
|
||||
<string name="base_url_error">Hubo un problema al intentar comunicarse con su instancia de Selfoss. Si el problema persiste, póngase en contacto conmigo.</string>
|
||||
<string name="pref_header_theme">Temas</string>
|
||||
<string name="default_theme">Predeterminado</string>
|
||||
<string name="default_dark_theme">Predeterminado/Oscuro</string>
|
||||
<string name="pref_header_debug">Depurar</string>
|
||||
<string name="self_hosted_cert_switch">Utilizando un certificado alojado propiamente ?</string>
|
||||
<string name="self_signed_cert_warning">Por razones de seguridad, los certificados propios no son compatibles por defecto. Activando esto, no seré responsable de cualquier problema de seguridad que encuentre.</string>
|
||||
<string name="pref_selfoss_category">Api de Selfoss</string>
|
||||
<string name="pref_api_items_number_title">Número de artículos cargados</string>
|
||||
<string name="pref_hidden_tags">Etiquetas ocultas</string>
|
||||
<string name="summary_debug_identifier">Identificador de depuración</string>
|
||||
<string name="unique_id_to_clipboard">Identificador copiado a su portapapeles</string>
|
||||
<string name="display_header_drawer_summary">Mostrar una cabecera con la url de instancia de selfoss en el cajón lateral.</string>
|
||||
<string name="display_header_drawer_title">Cabecera de cuenta</string>
|
||||
<string name="pref_general_infinite_loading_title">Cargar más artículos en desplazamiento</string>
|
||||
<string name="translation">Traducción</string>
|
||||
<string name="cant_open_invalid_url">La url del elemento no es válida. Estoy buscando resolver este problema para que la aplicación no colapse.</string>
|
||||
<string name="drawer_report_bug">Reportar un error</string>
|
||||
<string name="items_number_should_be_number">El número de artículos debe ser un número entero.</string>
|
||||
<string name="reader_action_more">Leer más</string>
|
||||
<string name="reader_action_open">Abrir en el navegador</string>
|
||||
<string name="reader_action_share">Compartir</string>
|
||||
<string name="pref_switch_actions_pager_scroll_on">Marcar artículos como leidos al desplazarse entre ellos.</string>
|
||||
<string name="add_to_favs_reader">Añadir a Favoritos</string>
|
||||
<string name="remove_to_favs_reader">Eliminar de favoritos</string>
|
||||
<string name="pref_content_reader_font_size">Tamaño de la fuente del lector</string>
|
||||
<string name="pref_header_viewer">Visor de artículos</string>
|
||||
<string name="refresh_dialog_message">Esto actualizará su instancia de Selfoss.</string>
|
||||
<string name="markall_dialog_message">Esto marcará todos los artículos como leídos.</string>
|
||||
<string name="pref_switch_actions_pager_scroll">Marcar artículos como leídos al deslizar con el dedo hacia los lados</string>
|
||||
<string name="pref_switch_actions_pager_scroll_off">No marcar artículos como leídos al deslizar con el dedo hacia los lados.</string>
|
||||
<string name="pref_acra_alwaysaccept">Enviar automáticamente informe de fallos</string>
|
||||
<string name="pref_acra_alwaysaccept_enabled">Se enviaran automáticamente los informes de fallos</string>
|
||||
<string name="pref_acra_alwaysaccept_disabled">Le preguntará al enviar informes de fallos.</string>
|
||||
<string name="pref_debug_crash_reports">Informe de fallos</string>
|
||||
<string name="pref_debug_debug_logs">Registro de depuración (éstos se enviarán sin diálogo)</string>
|
||||
<string name="acra_login">Habilitar el registro</string>
|
||||
<string name="drawer_item_hidden_tags">Etiquetas ocultas</string>
|
||||
<string name="unmark">Marcar artículo como no leído</string>
|
||||
<string name="pref_header_offline">Sin conexión y caché</string>
|
||||
<string name="pref_switch_items_caching_off">Los artículos no se guardarán en la memoria del dispositivo y la aplicación no se podrá utilizar sin conexión.</string>
|
||||
@ -90,6 +142,7 @@
|
||||
<string name="pref_switch_items_caching">Guardar elementos para uso sin conexión</string>
|
||||
<string name="pref_switch_update_sources">Check for new sources and tags</string>
|
||||
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
|
||||
<string name="no_network_connectivity">Sin conexión!</string>
|
||||
<string name="network_connectivity_lost">"Network connection lost"</string>
|
||||
<string name="network_connectivity_retrieved">"Network connection is now available"</string>
|
||||
<string name="pref_switch_periodic_refresh">Sincronizar artículos</string>
|
||||
@ -97,7 +150,7 @@
|
||||
<string name="pref_switch_periodic_refresh_on">Los artículos se sincronizarán periódicamente</string>
|
||||
<string name="pref_periodic_refresh_minutes_title"><![CDATA[Intervalo de sincronización (>= 15 minutos)]]></string>
|
||||
<string name="pref_switch_refresh_when_charging">Sólo refrescar cuando el teléfono está cargando</string>
|
||||
<string name="loading_notification_title">Cargando…</string>
|
||||
<string name="loading_notification_title">Cargando...</string>
|
||||
<string name="loading_notification_text">Selfoss está sincronizando tus artículos</string>
|
||||
<string name="notification_channel_sync">Notificación de sincronización</string>
|
||||
<string name="new_items_channel_sync">Notificación de elementos nuevos</string>
|
||||
@ -116,19 +169,4 @@
|
||||
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
|
||||
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
|
||||
<string name="remove_source">Remove source</string>
|
||||
<string name="pref_theme_title">Light/Dark mode</string>
|
||||
<string name="mode_dark">Dark mode</string>
|
||||
<string name="mode_system">Follow the system setting</string>
|
||||
<string name="mode_light">Light mode</string>
|
||||
<string name="gdpr_dialog_title">The app does not share any personal data about you.</string>
|
||||
<string name="gdpr_dialog_message"><![CDATA[Crash reports sending is now enabled. It can be disabled from the settings page. Keep in mind that crash reports are essential for the app development.]]></string>
|
||||
<string name="crash_toast_text">A crash occured. Sending the details to the developper.</string>
|
||||
<string name="pref_switch_disable_acra">"Disable automatic bug reporting. "</string>
|
||||
<string name="menu_home_filter">Filters</string>
|
||||
<string name="application_selfoss_only">This app only works with a Selfoss instance, and no other RSS feed.</string>
|
||||
<string name="menu_home_sources">Sources</string>
|
||||
<string name="update_source">Update source</string>
|
||||
<string name="confirm_disconnect_title">Disconnect ?</string>
|
||||
<string name="confirm_disconnect_description">You will be disconnected from your selfoss instance.</string>
|
||||
<string name="disable_ssl">Disable SSL</string>
|
||||
</resources>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user